Puoi utilizzare ML Kit per eseguire l'inferenza on-device con un modello TensorFlow Lite.
ML Kit può utilizzare i modelli TensorFlow Lite solo su dispositivi con iOS 9 o versioni successive.
Prima di iniziare
- Se non hai ancora aggiunto Firebase alla tua app, segui i passaggi descritti nella guida introduttiva.
- Includi le librerie ML Kit nel tuo Podfile:
pod 'Firebase/MLModelInterpreter', '6.25.0'
Dopo aver installato o aggiornato i pod del progetto, assicurati di aprire il progetto Xcode utilizzando il relativo.xcworkspace
. - Nell'app, importa Firebase:
Swift
import Firebase
Objective-C
@import Firebase;
- Converti il modello TensorFlow che vuoi utilizzare in formato TensorFlow Lite. Consulta TOCO: TensorFlow Lite Optimizing Converter.
Ospittare o raggruppare il modello
Prima di poter utilizzare un modello TensorFlow Lite per l'inferenza nella tua app, devi renderlo disponibile per ML Kit. ML Kit può utilizzare i modelli TensorFlow Lite ospitati in remoto utilizzando Firebase, inclusi nel file binario dell'app o entrambi.
Se ospiti un modello su Firebase, puoi aggiornarlo senza rilasciare una nuova versione dell'app e puoi utilizzare Remote Config e A/B Testing per pubblicare dinamicamente modelli diversi per gruppi diversi di utenti.
Se scegli di fornire solo il modello ospitandolo su Firebase e di non includerlo nella tua app, puoi ridurre le dimensioni del download iniziale dell'app. Tuttavia, tieni presente che se il modello non è incluso nella tua app, qualsiasi funzionalità correlata al modello non sarà disponibile finché l'app non lo scarica per la prima volta.
Se combini il modello con l'app, puoi assicurarti che le funzionalità di ML dell'app continuino a funzionare anche quando il modello ospitato su Firebase non è disponibile.
Ospitare i modelli su Firebase
Per ospitare il modello TensorFlow Lite su Firebase:
- Nella sezione ML Kit della console Firebase, fai clic sulla scheda Personalizzata.
- Fai clic su Aggiungi modello personalizzato (o Aggiungi un altro modello).
- Specifica un nome che verrà utilizzato per identificare il modello nel progetto Firebase, quindi carica il file del modello TensorFlow Lite (di solito termina con
.tflite
o.lite
).
Dopo aver aggiunto un modello personalizzato al tuo progetto Firebase, puoi fare riferimento al modello nelle tue app utilizzando il nome specificato. Puoi caricare un nuovo modello TensorFlow Lite in qualsiasi momento e l'app lo scaricherà e inizierà a utilizzarlo al successivo riavvio. Puoi definire le condizioni del dispositivo necessarie per consentire all'app di tentare di aggiornare il modello (vedi di seguito).
Raggruppare i modelli con un'app
Per raggruppare il modello TensorFlow Lite con l'app, aggiungi il file del modello (in genere termina con .tflite
o .lite
) al progetto Xcode, assicurandoti di selezionare Copia risorse del bundle. Il file del modello verrà incluso nel bundle dell'app e sarà disponibile per ML Kit.
Carica il modello
Per utilizzare il modello TensorFlow Lite nella tua app, configura prima ML Kit con le posizioni in cui è disponibile il modello: da remoto utilizzando Firebase, nello spazio di archiviazione locale o in entrambe le posizioni. Se specifichi sia un modello locale che uno remoto, puoi utilizzare il modello remoto se è disponibile e utilizzare il modello archiviato localmente se il modello remoto non è disponibile.
Configurare un modello ospitato su Firebase
Se hai ospitato il modello con Firebase, crea un oggetto CustomRemoteModel
,
specificando il nome assegnato al modello quando lo hai pubblicato:
Swift
let remoteModel = CustomRemoteModel(
name: "your_remote_model" // The name you assigned in the Firebase console.
)
Objective-C
// Initialize using the name you assigned in the Firebase console.
FIRCustomRemoteModel *remoteModel =
[[FIRCustomRemoteModel alloc] initWithName:@"your_remote_model"];
Quindi, avvia l'attività di download del modello, specificando le condizioni in cui vuoi consentire il download. Se il modello non è sul dispositivo o se è disponibile una versione più recente, l'attività lo scarica in modo asincrono da Firebase:
Swift
let downloadConditions = ModelDownloadConditions(
allowsCellularAccess: true,
allowsBackgroundDownloading: true
)
let downloadProgress = ModelManager.modelManager().download(
remoteModel,
conditions: downloadConditions
)
Objective-C
FIRModelDownloadConditions *downloadConditions =
[[FIRModelDownloadConditions alloc] initWithAllowsCellularAccess:YES
allowsBackgroundDownloading:YES];
NSProgress *downloadProgress =
[[FIRModelManager modelManager] downloadRemoteModel:remoteModel
conditions:downloadConditions];
Molte app avviano l'attività di download nel codice di inizializzazione, ma puoi farlo in qualsiasi momento prima di dover utilizzare il modello.
Configurare un modello locale
Se hai aggregato il modello con la tua app, crea un oggetto CustomLocalModel
,
specificando il nome file del modello TensorFlow Lite:
Swift
guard let modelPath = Bundle.main.path(
forResource: "your_model",
ofType: "tflite",
inDirectory: "your_model_directory"
) else { /* Handle error. */ }
let localModel = CustomLocalModel(modelPath: modelPath)
Objective-C
NSString *modelPath = [NSBundle.mainBundle pathForResource:@"your_model"
ofType:@"tflite"
inDirectory:@"your_model_directory"];
FIRCustomLocalModel *localModel =
[[FIRCustomLocalModel alloc] initWithModelPath:modelPath];
Creare un interprete dal modello
Dopo aver configurato le origini del modello, crea un oggetto
ModelInterpreter
da una di queste.
Se hai solo un modello raggruppato a livello locale, passa l'oggetto CustomLocalModel
a modelInterpreter(localModel:)
:
Swift
let interpreter = ModelInterpreter.modelInterpreter(localModel: localModel)
Objective-C
FIRModelInterpreter *interpreter =
[FIRModelInterpreter modelInterpreterForLocalModel:localModel];
Se hai un modello ospitato in remoto, dovrai verificare che sia stato scaricato prima di eseguirlo. Puoi controllare lo stato del compito di download del modello utilizzando il metodo isModelDownloaded(remoteModel:)
del gestore dei modelli.
Anche se devi confermarlo solo prima di eseguire l'interprete, se hai sia un modello ospitato in remoto sia un modello in bundle locale, potrebbe essere sensato eseguire questo controllo durante l'inizializzazione di ModelInterpreter
: crea un interprete dal modello remoto se è stato scaricato e dal modello locale in caso contrario.
Swift
var interpreter: ModelInterpreter
if ModelManager.modelManager().isModelDownloaded(remoteModel) {
interpreter = ModelInterpreter.modelInterpreter(remoteModel: remoteModel)
} else {
interpreter = ModelInterpreter.modelInterpreter(localModel: localModel)
}
Objective-C
FIRModelInterpreter *interpreter;
if ([[FIRModelManager modelManager] isModelDownloaded:remoteModel]) {
interpreter = [FIRModelInterpreter modelInterpreterForRemoteModel:remoteModel];
} else {
interpreter = [FIRModelInterpreter modelInterpreterForLocalModel:localModel];
}
Se hai solo un modello ospitato in remoto, devi disattivare le funzionalità correlate al modello, ad esempio disattivare o nascondere parte dell'interfaccia utente, finché non confermi che il modello è stato scaricato.
Puoi ottenere lo stato del download del modello collegando gli osservatori al Centro notifiche predefinito. Assicurati di utilizzare un riferimento debole a self
nel blocco osservatore, poiché i download possono richiedere del tempo e l'oggetto di origine può essere liberato al termine del download. Ad esempio:
Swift
NotificationCenter.default.addObserver( forName: .firebaseMLModelDownloadDidSucceed, object: nil, queue: nil ) { [weak self] notification in guard let strongSelf = self, let userInfo = notification.userInfo, let model = userInfo[ModelDownloadUserInfoKey.remoteModel.rawValue] as? RemoteModel, model.name == "your_remote_model" else { return } // The model was downloaded and is available on the device } NotificationCenter.default.addObserver( forName: .firebaseMLModelDownloadDidFail, object: nil, queue: nil ) { [weak self] notification in guard let strongSelf = self, let userInfo = notification.userInfo, let model = userInfo[ModelDownloadUserInfoKey.remoteModel.rawValue] as? RemoteModel else { return } let error = userInfo[ModelDownloadUserInfoKey.error.rawValue] // ... }
Objective-C
__weak typeof(self) weakSelf = self; [NSNotificationCenter.defaultCenter addObserverForName:FIRModelDownloadDidSucceedNotification object:nil queue:nil usingBlock:^(NSNotification *_Nonnull note) { if (weakSelf == nil | note.userInfo == nil) { return; } __strong typeof(self) strongSelf = weakSelf; FIRRemoteModel *model = note.userInfo[FIRModelDownloadUserInfoKeyRemoteModel]; if ([model.name isEqualToString:@"your_remote_model"]) { // The model was downloaded and is available on the device } }]; [NSNotificationCenter.defaultCenter addObserverForName:FIRModelDownloadDidFailNotification object:nil queue:nil usingBlock:^(NSNotification *_Nonnull note) { if (weakSelf == nil | note.userInfo == nil) { return; } __strong typeof(self) strongSelf = weakSelf; NSError *error = note.userInfo[FIRModelDownloadUserInfoKeyError]; }];
Specifica l'input e l'output del modello
Poi, configura i formati di input e output dell'interprete del modello.
Un modello TensorFlow Lite riceve come input e produce come output uno o più array multidimensionali. Questi array contengono valori byte
,
int
, long
o float
. Devi configurare ML Kit con il numero e le dimensioni ("forma") degli array utilizzati dal tuo modello.
Se non conosci la forma e il tipo di dati di input e output del modello, puoi utilizzare l'interprete Python di TensorFlow Lite per ispezionarlo. Ad esempio:
import tensorflow as tf interpreter = tf.lite.Interpreter(model_path="my_model.tflite") interpreter.allocate_tensors() # Print input shape and type print(interpreter.get_input_details()[0]['shape']) # Example: [1 224 224 3] print(interpreter.get_input_details()[0]['dtype']) # Example: <class 'numpy.float32'> # Print output shape and type print(interpreter.get_output_details()[0]['shape']) # Example: [1 1000] print(interpreter.get_output_details()[0]['dtype']) # Example: <class 'numpy.float32'>
Dopo aver determinato il formato dell'input e dell'output del modello, configura l'interprete del modello della tua app creando un oggetto ModelInputOutputOptions
.
Ad esempio, un modello di classificazione delle immagini a virgola mobile potrebbe prendere come input un array di valori Float
di Nx224x224x3, che rappresenta un batch di N immagini a tre canali (RGB) di 224 x 224 e produrre come output un elenco di 1000 valori Float
, ciascuno dei quali rappresenta la probabilità che l'immagine appartenga a una delle 1000 categorie previste dal modello.
Per un modello di questo tipo, devi configurare l'input e l'output dell'interprete del modello come mostrato di seguito:
Swift
let ioOptions = ModelInputOutputOptions() do { try ioOptions.setInputFormat(index: 0, type: .float32, dimensions: [1, 224, 224, 3]) try ioOptions.setOutputFormat(index: 0, type: .float32, dimensions: [1, 1000]) } catch let error as NSError { print("Failed to set input or output format with error: \(error.localizedDescription)") }
Objective-C
FIRModelInputOutputOptions *ioOptions = [[FIRModelInputOutputOptions alloc] init]; NSError *error; [ioOptions setInputFormatForIndex:0 type:FIRModelElementTypeFloat32 dimensions:@[@1, @224, @224, @3] error:&error]; if (error != nil) { return; } [ioOptions setOutputFormatForIndex:0 type:FIRModelElementTypeFloat32 dimensions:@[@1, @1000] error:&error]; if (error != nil) { return; }
Eseguire l'inferenza sui dati di input
Infine, per eseguire l'inferenza utilizzando il modello, recupera i dati di input, esegui eventuali trasformazioni sui dati che potrebbero essere necessarie per il modello e crea un oggetto Data
contenente i dati.
Ad esempio, se il tuo modello elabora immagini e ha dimensioni di input di valori di tipo [BATCH_SIZE, 224, 224, 3]
a virgola mobile, potresti dover scalare i valori di colore dell'immagine a un intervallo a virgola mobile come nell'esempio seguente:
Swift
let image: CGImage = // Your input image guard let context = CGContext( data: nil, width: image.width, height: image.height, bitsPerComponent: 8, bytesPerRow: image.width * 4, space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue ) else { return false } context.draw(image, in: CGRect(x: 0, y: 0, width: image.width, height: image.height)) guard let imageData = context.data else { return false } let inputs = ModelInputs() var inputData = Data() do { for row in 0 ..< 224 { for col in 0 ..< 224 { let offset = 4 * (col * context.width + row) // (Ignore offset 0, the unused alpha channel) let red = imageData.load(fromByteOffset: offset+1, as: UInt8.self) let green = imageData.load(fromByteOffset: offset+2, as: UInt8.self) let blue = imageData.load(fromByteOffset: offset+3, as: UInt8.self) // Normalize channel values to [0.0, 1.0]. This requirement varies // by model. For example, some models might require values to be // normalized to the range [-1.0, 1.0] instead, and others might // require fixed-point values or the original bytes. var normalizedRed = Float32(red) / 255.0 var normalizedGreen = Float32(green) / 255.0 var normalizedBlue = Float32(blue) / 255.0 // Append normalized values to Data object in RGB order. let elementSize = MemoryLayout.size(ofValue: normalizedRed) var bytes = [UInt8](repeating: 0, count: elementSize) memcpy(&bytes, &normalizedRed, elementSize) inputData.append(&bytes, count: elementSize) memcpy(&bytes, &normalizedGreen, elementSize) inputData.append(&bytes, count: elementSize) memcpy(&ammp;bytes, &normalizedBlue, elementSize) inputData.append(&bytes, count: elementSize) } } try inputs.addInput(inputData) } catch let error { print("Failed to add input: \(error)") }
Objective-C
CGImageRef image = // Your input image long imageWidth = CGImageGetWidth(image); long imageHeight = CGImageGetHeight(image); CGContextRef context = CGBitmapContextCreate(nil, imageWidth, imageHeight, 8, imageWidth * 4, CGColorSpaceCreateDeviceRGB(), kCGImageAlphaNoneSkipFirst); CGContextDrawImage(context, CGRectMake(0, 0, imageWidth, imageHeight), image); UInt8 *imageData = CGBitmapContextGetData(context); FIRModelInputs *inputs = [[FIRModelInputs alloc] init]; NSMutableData *inputData = [[NSMutableData alloc] initWithCapacity:0]; for (int row = 0; row < 224; row++) { for (int col = 0; col < 224; col++) { long offset = 4 * (col * imageWidth + row); // Normalize channel values to [0.0, 1.0]. This requirement varies // by model. For example, some models might require values to be // normalized to the range [-1.0, 1.0] instead, and others might // require fixed-point values or the original bytes. // (Ignore offset 0, the unused alpha channel) Float32 red = imageData[offset+1] / 255.0f; Float32 green = imageData[offset+2] / 255.0f; Float32 blue = imageData[offset+3] / 255.0f; [inputData appendBytes:&red length:sizeof(red)]; [inputData appendBytes:&green length:sizeof(green)]; [inputData appendBytes:&blue length:sizeof(blue)]; } } [inputs addInput:inputData error:&error]; if (error != nil) { return nil; }
Dopo aver preparato l'input del modello (e dopo aver confermato che il modello è disponibile), passa le opzioni di input e input/output al metodo run(inputs:options:completion:)
del tuo interprete del modello.
Swift
interpreter.run(inputs: inputs, options: ioOptions) { outputs, error in guard error == nil, let outputs = outputs else { return } // Process outputs // ... }
Objective-C
[interpreter runWithInputs:inputs options:ioOptions completion:^(FIRModelOutputs * _Nullable outputs, NSError * _Nullable error) { if (error != nil || outputs == nil) { return; } // Process outputs // ... }];
Puoi ottenere l'output chiamando il metodo output(index:)
dell'oggetto che viene restituito. Ad esempio:
Swift
// Get first and only output of inference with a batch size of 1 let output = try? outputs.output(index: 0) as? [[NSNumber]] let probabilities = output??[0]
Objective-C
// Get first and only output of inference with a batch size of 1 NSError *outputError; NSArray *probabilites = [outputs outputAtIndex:0 error:&outputError][0];
La modalità di utilizzo dell'output dipende dal modello in uso.
Ad esempio, se stai eseguendo una classificazione, come passaggio successivo potresti mappare gli indici del risultato alle etichette che rappresentano. Supponiamo di avere un file di testo con stringhe di etichetta per ciascuna delle categorie del modello. Puoi mappare le stringhe di etichetta alle probabilità di output seguendo un procedimento simile al seguente:
Swift
guard let labelPath = Bundle.main.path(forResource: "retrained_labels", ofType: "txt") else { return } let fileContents = try? String(contentsOfFile: labelPath) guard let labels = fileContents?.components(separatedBy: "\n") else { return } for i in 0 ..< labels.count { if let probability = probabilities?[i] { print("\(labels[i]): \(probability)") } }
Objective-C
NSError *labelReadError = nil; NSString *labelPath = [NSBundle.mainBundle pathForResource:@"retrained_labels" ofType:@"txt"]; NSString *fileContents = [NSString stringWithContentsOfFile:labelPath encoding:NSUTF8StringEncoding error:&labelReadError]; if (labelReadError != nil || fileContents == NULL) { return; } NSArray<NSString *> *labels = [fileContents componentsSeparatedByString:@"\n"]; for (int i = 0; i < labels.count; i++) { NSString *label = labels[i]; NSNumber *probability = probabilites[i]; NSLog(@"%@: %f", label, probability.floatValue); }
Appendice: Sicurezza del modello
Indipendentemente da come rendi disponibili i tuoi modelli TensorFlow Lite per ML Kit, ML Kit li memorizza nel formato protobuf serializzato standard nello spazio di archiviazione locale.
In teoria, ciò significa che chiunque può copiare il tuo modello. Tuttavia, in pratica, la maggior parte dei modelli è così specifica per l'applicazione e offuscata dalle ottimizzazioni che il rischio è simile a quello dei concorrenti che smontano e riutilizzano il codice. Tuttavia, devi essere consapevole di questo rischio prima di utilizzare un modello personalizzato nella tua app.