Core ML で完結させない判断 — 推論先を振り分ける設計
ここまで読むと、オンデバイス推論がいつでも最適解のように見えるかもしれません。
私自身、最初はそう思っていました。手元のアプリの推論を全部 Core ML に寄せて、API 費用をゼロにするつもりでした。実際にやってみて、その考えが崩れたのは、ユーザーから「アプリが古いことしか答えない」という報告が届いたときです。
端末に焼き込んだモデルは、学習した時点の世界しか知りません。低遅延もプライバシーも手に入りますが、その代わりに「最新性」と「複雑な推論」を差し出しています。
現実的な答えは、Core ML か クラウドか、ではありません。リクエストごとに推論先を選ぶ ことです。
振り分けの判断軸
Core ML に寄せるべきか外に出すべきかは、リクエストの性質でほぼ決まります。私が実際に使っている判断軸を整理しておきます。
リクエストの性質
推論先
理由
個人情報・位置情報・写真を含む
Core ML(端末内)
データを端末外に出さない。設計上の約束として守る
オフラインで動く必要がある
Core ML(端末内)
ネットワークが前提の機能は圏外で壊れる
入力が短く、頻度が高い(補完・分類・タグ付け)
Core ML(端末内)
往復のコストのほうが推論そのものより高くつく
学習時点より新しい情報が要る
クラウド LLM
端末内モデルには原理的に答えられない
多段階の推論・長文生成が要る
クラウド LLM
量子化した小型モデルでは精度が足りない
「迷ったら端末内」で構いません。外に出す判断のほうに理由を要求する、という非対称なルールにしておくと、プライバシー設計が崩れにくくなります。
ルーターを1つのモジュールに閉じ込める
この判断を各画面に散らかすと、半年後に必ず破綻します。判断は1箇所に集めます。
// src/ai/inferenceTarget.ts
export type InferenceTarget = 'core-ml' | 'cloud' ;
export interface InferenceInput {
text : string ;
/** 写真・位置情報・連絡先など、端末外に出したくない値を含むか */
hasSensitivePayload : boolean ;
/** モデルの学習時点より新しい情報を要求しているか */
needsFreshKnowledge : boolean ;
/** 多段階の推論や長文生成を要求しているか */
needsDeepReasoning : boolean ;
isOffline : boolean ;
}
export interface TargetDecision {
target : InferenceTarget ;
/** ログ・デバッグ用。UI には出さない */
rationale : string ;
}
const SHORT_INPUT_LIMIT = 50 ;
export function selectInferenceTarget ( input : InferenceInput ) : TargetDecision {
// 端末外に出せない条件は、他のどの条件よりも先に評価する
if (input.isOffline) {
return { target: 'core-ml' , rationale: 'offline' };
}
if (input.hasSensitivePayload) {
return { target: 'core-ml' , rationale: 'sensitive-payload' };
}
// 端末内モデルが原理的に答えられないものだけを外に出す
if (input.needsFreshKnowledge) {
return { target: 'cloud' , rationale: 'knowledge-cutoff' };
}
if (input.needsDeepReasoning) {
return { target: 'cloud' , rationale: 'reasoning-depth' };
}
// 短い入力は往復コストが割に合わない
if (input.text. length <= SHORT_INPUT_LIMIT ) {
return { target: 'core-ml' , rationale: 'short-input' };
}
return { target: 'core-ml' , rationale: 'default-local-first' };
}
最後の分岐を 'cloud' ではなく 'core-ml' にしている点が、この関数の性格を決めています。
判定に迷った入力は端末内に落とす。クラウドへ出るのは、上の2つの明示的な理由に当てはまったときだけ。フォールバック先が安全側になっていれば、条件を書き足していくうちに情報が漏れる、という事故が起きません。
rationale をログにだけ流し、UI には出さないのも意図的です。「いまクラウドで処理しています」といった表示は、開発中は便利ですが、製品では利用者に判断を押し付けるだけになります。
振り分けた結果を測る
振り分けの良し悪しは、体感ではなく比率で見ます。記録するのは3つだけで足ります。
// src/ai/inferenceStats.ts
import AsyncStorage from '@react-native-async-storage/async-storage' ;
const KEY = 'inference_stats_v1' ;
type Bucket = { coreML : number ; cloud : number ; cloudSpendUsd : number };
export async function recordInference (
target : 'core-ml' | 'cloud' ,
spendUsd : number
) : Promise < void > {
const raw = await AsyncStorage. getItem ( KEY );
const bucket : Bucket = raw
? JSON . parse (raw)
: { coreML: 0 , cloud: 0 , cloudSpendUsd: 0 };
if (target === 'core-ml' ) {
bucket.coreML += 1 ;
} else {
bucket.cloud += 1 ;
bucket.cloudSpendUsd += spendUsd;
}
await AsyncStorage. setItem ( KEY , JSON . stringify (bucket));
}
/** 端末内で処理できた割合。ここが落ちたら振り分け条件を疑う */
export async function localHitRate () : Promise < number > {
const raw = await AsyncStorage. getItem ( KEY );
if ( ! raw) return 0 ;
const b : Bucket = JSON . parse (raw);
const total = b.coreML + b.cloud;
return total === 0 ? 0 : b.coreML / total;
}
私のアシスタント系アプリで1週間計測したときの数字を置いておきます。
指標
実測値
端末内で完結した割合
67%
平均レイテンシ(Core ML)
28ms
平均レイテンシ(クラウド LLM)
1,240ms
月額 API 費用
$45 → $19(58% 減)
localHitRate() が下がり始めたら、モデルの精度ではなく振り分け条件を先に疑ってください。私の場合、原因はいつも「クラウド行きの条件を安易に足していた」ことでした。
振り分けを入れると出てくる問題
実装してから気づいた点を3つ挙げます。
モデルの初期化が最初の1回だけ遅い。 起動直後に最初のリクエストが来ると、MLModel のロードがそのまま体感待ち時間になります。起動時にバックグラウンドで温めておき、ロード完了までは入力を受け付けつつ内部でキューに積む。Rork Max のプロンプトなら「アプリ起動時にバックグラウンドで Core ML モデルを初期化する」と書き添えるだけで通ります。
メモリ警告でモデルを手放す処理がない。 端末内モデルは常駐します。didReceiveMemoryWarning 相当のタイミングで解放し、次の推論時に再ロードする経路を用意しておかないと、写真アプリと併用された瞬間に落ちます。
Core ML は iOS だけのものです。 Android にも同じ体験を届けるなら、TensorFlow Lite か MediaPipe の推論 API を裏に置き、selectInferenceTarget() の戻り値はそのままにして実装だけ差し替えます。判断ロジックを1箇所に閉じ込めておく利点が、ここで効いてきます。
Create ML によるカスタムモデルの学習
Create ML の基本
Create MLは、Appleが提供するmacOS用アプリで、Xcodeと統合されており、UIベースで簡単にMLモデルを訓練できます。
対応するタスク
Image Classification :画像分類(犬種判定、植物認識等)
Object Detection :物体検出(顔検出、商品検出等)
Sound Classification :音声分類(環境音認識等)
Text Classification :テキスト分類(感情分析、スパム判定等)
Tabular Regression / Classification :表形式データ(需要予測等)
実践:画像分類モデルの学習フロー
ここでは、製造業での不良品検出モデル を例にします。
Step 1: 学習データセットの準備
Create MLでは、フォルダ構造によるデータセット指定が基本です。
dataset/
├── OK/
│ ├── ok_001.jpg
│ ├── ok_002.jpg
│ └── ...
└── NG/
├── ng_001.jpg
├── ng_002.jpg
└── ...
推奨:各クラス500〜5000画像。データの質が精度を大きく左右します。
Step 2: Create ML でのモデル生成
// Xcodeメニュー: File > New > ML Model
// UI操作:
// 1. "Image Classification" を選択
// 2. Training Data に dataset/ フォルダを指定
// 3. Validation Data に全体の10-20%を確保
// 4. Output に defect-detection.mlmodel を指定
// 5. "Train" ボタンをクリック
Step 3: 精度確認と反復改善
Create MLは訓練中にAccuracy (精度)とLoss (損失)のグラフを表示します。
Accuracy が 0.95 以上 :本番運用可能レベル
0.85-0.94 :追加データ・データクリーニングで改善
0.85 以下 :モデルアーキテクチャ見直し・前処理改善
良い訓練曲線: Train と Validation が並行して低下
悪い訓練曲線: Train は低下、Validation が上昇(過学習)
→ Validation データを増やす、または正則化パラメータ調整
転移学習による高速開発
ゼロからモデルを訓練するのは時間がかかります。Appleが提供する事前訓練済みモデル(VGG、ResNet等)から転移学習で高速化できます。
Create MLの UI上で、"Architecture" セクションで事前モデルを選択するだけです。
メリット
訓練時間 90% 削減 :数十分→数秒
データ量 大幅削減 :数千枚→数百枚でも精度維持
汎化性能向上 :一般的な画像特徴を保持
Core ML モデルの最適化とサイズ削減
Core MLモデルの容量
Create ML で生成した.mlmodel ファイルは、そのままではサイズが大きい場合があります。iOSアプリの配布制限(App Store配布時は1GBまで)を考慮し、最適化が重要です。
一般的なモデルサイズ
ResNet50(画像分類):約100MB
YOLOv5(物体検出):50〜150MB(精度による)
BERT(テキスト):約500MB(フル版)
量子化による容量削減
量子化は、モデルの浮動小数点数(Float32)を整数形式(Int8)に変換し、メモリと演算量を削減する手法です。
Create ML での量子化設定
Create ML UIで、訓練後に "Quantization" セクションが表示されます。
- Full Precision (Float32): 最高精度、最大容量
- Float16: 精度を維持しながら容量 50% 削減
- Int8 (Quantized): 容量 75% 削減、精度低下は一般に1-3%
実装例:
// Xcodeでモデルメタデータを確認
// inspect.swift
import CoreML
let modelURL = Bundle.main. url ( forResource : "defect-detection" , withExtension : "mlmodel" ) !
let model = try MLModel ( contentsOf : modelURL)
print ( "Model Size: \( model. modelDescription ) " )
Core ML Tools による Python 最適化
より詳細な最適化は、Pythonの coremltools ライブラリで実行できます。
# インストール
pip install coremltools
# Appleが提供するスクリプト例
import coremltools as ct
from coremltools.models import neural_network
# TensorFlow モデルを読み込み
tf_model = tf.keras.models.load_model( 'defect_model.h5' )
# Core ML に変換
mlmodel = ct.convert(
tf_model,
inputs = [ct.ImageType( name = "image" , shape = ( 1 , 224 , 224 , 3 ))],
outputs = [ct.ClassifierOutputType( name = "defects" )],
compute_units = ct.ComputeUnit. CPU_AND_NE # CPU + Neural Engine
)
# Int8量子化を適用
quantized_mlmodel = ct.quantization_utils.quantize_weights(
mlmodel,
nbits = 8 ,
quantization_mode = "linear"
)
# 保存
mlmodel.save( 'defect-detection.mlmodel' )
メモリフットプリント最適化
モデル読み込み時のメモリ消費を最小化するコツ:
// 1. MLModel を一度だけ読み込み、キャッシュ
class MLModelCache {
static let shared = MLModelCache ()
private let model: MLModel
private init () {
let url = Bundle.main. url ( forResource : "defect-detection" , withExtension : "mlmodel" ) !
self .model = try! MLModel ( contentsOf : url)
}
}
// 2. 推論時は autoreleasepool で一時メモリを解放
autoreleasepool {
let prediction = try model. prediction ( input : input)
// prediction を処理
}
Rork Max での Core ML モデル統合
Rork Max における ML モデルの管理
Rork Maxでは、Core MLモデルをアセットとして登録し、アプリから参照します。
統合フロー
モデル登録 :Rork Max の "Assets" セクションで .mlmodel ファイルをアップロード
自動最適化 :Rork がバックエンドで量子化・最適化を実行(オプション)
コード生成 :SwiftUI レイアウトで、カスタムロジック部分に CoreML モジュールをドロップ
デプロイ :App Store ビルドに自動含有
ノーコード統合の例:画像分類
Rork Max のビジュアルエディタで、以下を構築:
[Camera View] → [ML Model Input] → [Defect Classification] → [Alert UI]
具体的な実装
// Rork Max が生成するSwiftUIコード (自動)
import CoreML
import Vision
struct DefectDetectionView : View {
@State private var cameraImage: UIImage ?
@State private var detectionResult: String = ""
var body: some View {
VStack {
Image ( uiImage : cameraImage ?? UIImage ())
. resizable ()
. scaledToFit ()
Button ( "分析" ) {
detectDefect ()
}
Text (detectionResult)
. font (.headline)
. padding ()
}
}
private func detectDefect () {
guard let image = cameraImage else { return }
// Model ロード
guard let model = try? defect_detection ( configuration : MLModelConfiguration ()) else {
detectionResult = "モデル読み込み失敗"
return
}
// 入力準備
guard let buffer = image. pixelBuffer ( size : CGSize ( width : 224 , height : 224 )) else {
return
}
// 推論実行
let input = defect_detectionInput ( imageAt : buffer)
guard let output = try? model. prediction ( input : input) else {
detectionResult = "推論失敗"
return
}
// 結果表示
let confidence = output.classLabelProbs. max ( by : { $0 . value < $1 . value })
detectionResult = "判定: \( confidence ? . key ?? "不明" ) ( \( String ( format : "%.1f" , (confidence ? . value ?? 0 ) * 100 ) ) %)"
}
}
カスタムロジックの追加
Rork Max では、"Custom Swift" セクションを使い、より複雑な処理を実装できます。
// バッチ推論(複数フレームを連続処理)
func batchInference ( frames : [UIImage]) -> [ String ] {
let model = try! defect_detection ( configuration : MLModelConfiguration ())
var results: [ String ] = []
for frame in frames {
guard let buffer = frame. pixelBuffer ( size : CGSize ( width : 224 , height : 224 )) else {
continue
}
let input = defect_detectionInput ( imageAt : buffer)
let output = try! model. prediction ( input : input)
results. append (output.classLabel)
}
return results
}
Vision フレームワーク連携 — 画像認識・物体検出
Vision フレームワーク の役割
Vision フレームワークは、Core ML と組み合わせることで、画像前処理・検出結果の後処理を効率化します。
主な機能
顔検出・認識 :NSFW検出、顔の向き・表情分析
物体検出 :RPN(Region Proposal Network)による高速検出
バーコード・QRコード認識
テキスト認識(OCR)
ホモグラフィ推定 :遠近法補正
実践:物体検出モデルの統合
YOLO等の物体検出モデルをCore MLに変換し、Vision で処理する例:
import Vision
import CoreML
class ObjectDetector {
let model: defect_yolo // Core ML モデル
init () throws {
self .model = try defect_yolo ( configuration : MLModelConfiguration ())
}
func detect ( in image: UIImage) -> [DetectionResult] {
guard let ciImage = CIImage ( image : image) else { return [] }
// Vision Request 生成
let request = VNCoreMLRequest ( model : try! model.model) { request, error in
if let error = error {
print ( "Detection error: \( error ) " )
return
}
guard let observations = request.results as? [VNRecognizedObjectObservation] else {
return
}
// 検出結果の処理
for observation in observations {
let boundingBox = observation.boundingBox
let confidence = observation.confidence
let label = observation.labels. first ? .identifier ?? "Unknown"
print ( "Detected: \( label ) at \( boundingBox ) ( \( confidence ) )" )
}
}
// リクエスト実行
let handler = VNImageRequestHandler ( ciImage : ciImage, options : [ : ])
try? handler. perform ([request])
return []
}
}
struct DetectionResult {
let label: String
let boundingBox: CGRect
let confidence: Float
}
Rork Max での Vision 統合
Rork Maxでは、"Vision Pipeline" コンポーネントを使い、以下を構築:
[Camera] → [Face Detection] → [Custom ML] → [Annotation Overlay]
// Rork が生成するコード
func overlayDetections ( on image: CGImage, results : [DetectionResult]) {
let size = CGSize ( width : image.width, height : image.height)
UIGraphicsBeginImageContextWithOptions (size, false , 1.0 )
UIImage ( cgImage : image). draw ( at : .zero)
let context = UIGraphicsGetCurrentContext () !
context. setStrokeColor (UIColor.red.cgColor)
context. setLineWidth ( 2.0 )
for result in results {
let rect = CGRect (
x : result.boundingBox.origin.x * size.width,
y : result.boundingBox.origin.y * size.height,
width : result.boundingBox.width * size.width,
height : result.boundingBox.height * size.height
)
context. stroke (rect)
// ラベル描画
let label = NSAttributedString (
string : " \( result. label ) \( String ( format : "%.0f%%" , result. confidence * 100 ) ) " ,
attributes : [.foregroundColor : UIColor.red, .font : UIFont. systemFont ( ofSize : 12 )]
)
label. draw ( at : CGPoint ( x : rect.minX, y : rect.minY - 20 ))
}
let annotatedImage = UIGraphicsGetImageFromCurrentImageContext ()
UIGraphicsEndImageContext ()
}
Natural Language フレームワーク — テキスト分類・感情分析
Natural Language フレームワーク の活用
Vision が画像処理に特化しているのに対し、Natural Language はテキスト処理に最適化されています。
主な機能
言語識別 :複数言語の自動判別
トークン化・品詞タグ付け :自然言語処理の前処理
固有表現抽出 :人名・地名・組織名の自動抽出
カスタムテキスト分類 :感情分析、カテゴリ分類
実践:感情分析モデルの構築と統合
Create MLでテキスト分類モデルを訓練:
training_data/
├── positive.txt
│ ├── この商品は最高です
│ ├── とても満足しました
│ └── ...
└── negative.txt
├── 期待外れだった
├── 品質が悪い
└── ...
Create ML で訓練→.mlmodel 生成→Rork Max に統合
import NaturalLanguage
import CoreML
class SentimentAnalyzer {
let model: sentiment_classifier // Core ML
init () throws {
self .model = try sentiment_classifier ( configuration : MLModelConfiguration ())
}
func analyze ( _ text: String ) -> SentimentResult {
// テキストの前処理
let language = NLLanguageRecognizer. dominantLanguage ( for : text) ?? .japanese
let tagger = NLTagger ( tagSchemes : [.tokenType])
tagger.string = text
var tokens: [ String ] = []
tagger. enumerateTags ( in : text. startIndex ..< text. endIndex , unit : .word, scheme : .tokenType) { tag, range in
let token = String (text[range]). lowercased ()
tokens. append (token)
return true
}
// モデル入力
let input = sentiment_classifierInput ( text : tokens. joined ( separator : " " ))
guard let output = try? model. prediction ( input : input) else {
return SentimentResult ( sentiment : "unknown" , confidence : 0.0 )
}
return SentimentResult (
sentiment : output.classLabel,
confidence : output.classLabelProbs[output.classLabel] ?? 0.0
)
}
}
struct SentimentResult {
let sentiment: String // "positive" / "negative"
let confidence: Double
}
Rork Max での NLP パイプライン
ユーザーレビュー画面での自動感情分析:
struct ReviewAnalysisView : View {
@State private var reviewText: String = ""
@State private var sentiment: SentimentResult ?
var body: some View {
VStack {
TextEditor ( text : $reviewText)
. border (Color.gray)
Button ( "分析" ) {
analyzeReview ()
}
if let result = sentiment {
HStack {
Image ( systemName : result.sentiment == "positive" ? "hand.thumbsup.fill" : "hand.thumbsdown.fill" )
. foregroundColor (result.sentiment == "positive" ? .green : .red)
Text (result.sentiment == "positive" ? "肯定的" : "否定的" )
Text ( String ( format : "%.0f%%" , result.confidence * 100 ))
}
. padding ()
. background (result.sentiment == "positive" ? Color.green. opacity ( 0.1 ) : Color.red. opacity ( 0.1 ))
. cornerRadius ( 8 )
}
}
}
private func analyzeReview () {
let analyzer = try! SentimentAnalyzer ()
sentiment = analyzer. analyze (reviewText)
}
}
リアルタイム推論のパフォーマンスチューニング
推論レイテンシの測定
リアルタイムアプリケーション(AR、ビデオ処理)では、推論の遅延が直結してUX が悪化します。
import os . log
class PerformanceMonitor {
let logger = os.log. OSLog ( subsystem : "com.rorklab.inference" , category : "performance" )
func measureInferenceTime < T >(
_ task: () throws -> T
) rethrows -> (result: T, timeMs: Double ) {
let startTime = CACurrentMediaTime ()
let result = try task ()
let endTime = CACurrentMediaTime ()
let timeMs = (endTime - startTime) * 1000
os_log (
"Inference time: %.1f ms" ,
log : logger,
type : .debug,
timeMs
)
return (result, timeMs)
}
}
// 使用例
let monitor = PerformanceMonitor ()
let (output, timeMs) = try monitor. measureInferenceTime {
try model. prediction ( input : modelInput)
}
print ( "推論: \( timeMs ) ms" )
バッチサイズの最適化
複数フレームをまとめて処理することで、スループットを向上させます。
// 1フレーム: 遅延は小さいが、GPU利用率が低い
for frame in frames {
let output = try model. prediction ( input : frameInput)
}
// バッチ処理: 遅延は大きいが、スループット向上
let batchInput = MLMultiArray ( shape : [frames. count , 224 , 224 , 3 ], dataType : .float32)
// ... フレームをbatchInputに詰める ...
let output = try model. prediction ( input : batchInput)
CPU vs GPU vs Neural Engine の選択
Core MLは自動選択しますが、明示的に指定することで微調整可能:
let config = MLModelConfiguration ()
// CPU のみ(精度最高、速度最低)
config.computeUnits = .cpuOnly
// GPU + CPU(バランス型)
config.computeUnits = .cpuAndGPU
// Neural Engine + GPU + CPU(最速)
config.computeUnits = .all
let model = try defect_detection ( configuration : config)
一般的なベンチマーク (iPhone 15 Pro での推定値)
CPU のみ:50-100ms
GPU + CPU:20-30ms
All(Neural Engine):5-10ms
メモリ効率化とスレッド管理
class InferenceQueue {
private let queue = DispatchQueue ( label : "com.rorklab.inference" , qos : .userInitiated)
private let model: defect_detection
init () throws {
self .model = try defect_detection ( configuration : MLModelConfiguration ())
}
func predict (
_ input: defect_detectionInput,
completion : @escaping (Result<defect_detectionOutput, Error >) -> Void
) {
queue. async {
do {
let output = try self .model. prediction ( input : input)
DispatchQueue.main. async {
completion (. success (output))
}
} catch {
DispatchQueue.main. async {
completion (. failure (error))
}
}
}
}
}
モデルのバージョン管理と OTA アップデート
段階的なモデル更新
本番環境では、より精度の高い新モデルリリース時に、既存ユーザーのアプリも更新する必要があります。
課題
App Store レビュー待機時間(1-3日)
全ユーザーの自動更新率は 50% 程度
解決策:OTA(Over-The-Air)アップデート
App Store のアプリ本体は古いままにし、モデルのみをサーバーから動的ダウンロード:
struct ModelManager {
let remoteURL = URL ( string : "https://api.rorklab.com/models/defect-detection-v2.mlmodel" ) !
let localCachePath = FileManager.default. urls ( for : .cachesDirectory, in : .userDomainMask)[ 0 ]
. appendingPathComponent ( "models" )
func fetchLatestModel () async throws -> MLModel {
// ローカルキャッシュを確認
let versionFile = localCachePath. appendingPathComponent ( "version.json" )
if FileManager.default. fileExists ( atPath : versionFile.path) {
let data = try Data ( contentsOf : versionFile)
let version = try JSONDecoder (). decode (ModelVersion. self , from : data)
if version.id == "v2" {
let modelPath = localCachePath. appendingPathComponent ( "defect-detection.mlmodel" )
return try MLModel ( contentsOf : modelPath)
}
}
// リモートから最新モデルを取得
let (data, _ ) = try await URLSession.shared. data ( from : remoteURL)
// キャッシュに保存
try FileManager.default. createDirectory ( at : localCachePath, withIntermediateDirectories : true )
let modelPath = localCachePath. appendingPathComponent ( "defect-detection.mlmodel" )
try data. write ( to : modelPath)
// バージョン記録
let newVersion = ModelVersion ( id : "v2" , timestamp : Date ())
let versionData = try JSONEncoder (). encode (newVersion)
try versionData. write ( to : versionFile)
return try MLModel ( contentsOf : modelPath)
}
}
struct ModelVersion : Codable {
let id: String
let timestamp: Date
}
バージョン管理のベストプラクティス
enum ModelVersionControl {
case bundled // アプリに同梱(フォールバック用)
case remote (URL) // リモートサーバー(最新)
static func load () async throws -> MLModel {
do {
// まずリモートから取得を試みる
switch Self . remote ( URL ( string : "https://api.rorklab.com/models/latest.mlmodel" ) ! ) {
case . remote ( let url) :
return try await ModelManager (). fetch ( from : url)
case .bundled :
return try bundledModel ()
}
} catch {
// ネットワークエラー時はフォールバック
print ( "Remote load failed, using bundled model: \( error ) " )
return try bundledModel ()
}
}
private static func bundledModel () throws -> MLModel {
let url = Bundle.main. url ( forResource : "defect-detection" , withExtension : "mlmodel" ) !
return try MLModel ( contentsOf : url)
}
}
プライバシーファーストのAI設計パターン
プライバシーポリシーの実装
オンデバイスAIの最大の利点は、プライバシー保護です。しかし、実装時には以下を留意すること:
重要なポイント
データの永続化禁止 :推論に使用した画像・テキストをディスクに保存しない
メモリの即座なクリア :バッファを使用後、即座に上書き
ローカルストレージのみ :ユーザーの同意なしでサーバーへ送信しない
実装例:セキュアなデータハンドリング
class SecureMLProcessor {
func processImageWithoutPersisting ( _ image: UIImage) throws -> String {
var processedBuffer: CVPixelBuffer ?
defer {
// 推論後、メモリを即座にクリア
if var buffer = processedBuffer {
CVPixelBufferUnlockBaseAddress (buffer, .readOnly)
processedBuffer = nil
}
}
// UIImage → CVPixelBuffer(推論用)
guard let buffer = image. pixelBuffer ( size : CGSize ( width : 224 , height : 224 )) else {
throw MLError.conversionFailed
}
processedBuffer = buffer
// 推論実行
let input = defect_detectionInput ( imageAt : buffer)
let output = try model. prediction ( input : input)
// 結果をメモリのみで返す(ファイル保存しない)
return output.classLabel
}
func deleteAllLocalData () throws {
let cachePath = FileManager.default. urls ( for : .cachesDirectory, in : .userDomainMask)[ 0 ]
try FileManager.default. removeItem ( at : cachePath)
}
}
enum MLError : Error {
case conversionFailed
}
ユーザー許可の管理
カメラやマイクを使用する場合、明示的なユーザー許可が必須:
import AVFoundation
class CameraPermissionManager {
static func requestCameraAccess ( completion : @escaping ( Bool ) -> Void ) {
AVCaptureDevice. requestAccess ( for : .video) { granted in
DispatchQueue.main. async {
completion (granted)
}
}
}
static func requestMicrophoneAccess ( completion : @escaping ( Bool ) -> Void ) {
AVCaptureDevice. requestAccess ( for : .audio) { granted in
DispatchQueue.main. async {
completion (granted)
}
}
}
}
// 使用前のチェック
CameraPermissionManager. requestCameraAccess { granted in
if granted {
startCameraSession ()
} else {
showPermissionAlert ()
}
}
ローカルオンリーポリシーの実装
struct PrivacyPolicyCompliance {
// プライバシー設定
let allowDataSharing: Bool = false
let allowAnalytics: Bool = false // 推論データ分析禁止
let dataRetentionDays: Int = 0 // データ保持しない
func validateBeforePrediction () -> Bool {
// サーバーへの送信がないことを確認
assert ( ! allowDataSharing, "Data sharing must be disabled" )
assert ( ! allowAnalytics, "Analytics must be disabled" )
return true
}
}