Trainierte Modelle in mobilen Apps einsetzen

Beispiel Objekterkennung

Für das erste Beispiel beschränkt sich der Artikel auf ein bereits trainiertes Modell zur Erkennung von Objekten in Bilder, konkret das Inception-V3-Modell. Es erkennt 1000 Objekte wie Bäume, Tiere, Menschen und Fahrzeuge. Es ist mit etwas weniger als 100 MByte relativ klein und hat eine geringe Fehlerrate. Das Modell listet das richtige Objekt mit einer Wahrscheinlichkeit von 96,54 Prozent innerhalb der Top-5-Vorschläge.

Sowohl für CoreML als auch TensorFlow Lite existieren Inception-V3-Modelle. Der Vorteil eines bereits trainierten Modells liegt darin, dass Entwickler keine zusätzlichen Trainingsdaten benötigen und vor allem nicht die Zeit und Rechenkapazität aufwenden müssen, um ein solches Modell zu trainieren. Zudem ist es nicht notwendig, das dahinterliegende Modell und dessen internen Aufbau zu verstehen.

Die Beispielanwendungen verlangen weder ein Android-Gerät noch ein iPhone, da sie innerhalb eines Emulators oder Simulators laufen können. Die Android-Beispiele benutzen Android Studio. Für die iOS-Beispiele ist ein Apple-Gerät notwendig, auf dem Xcode läuft.

Die Beispiel-Apps für iOS und Android sind gleich aufgebaut. Im oberen Teil der Anwendung befinden sich drei Bilder zur Auswahl. Beim Anklicken eines Bildes erscheint es in einer vergrößerten Darstellung. Die Vorhersageberechnung verwendet das ausgewählte, und die Anwendung zeigt das Ergebnis des am besten erkannten Objekts mit dem Prozentwert der Übereinstimmung und dem Namen des Objekts an. Um die Anwendung einfach zu halten, verwendet sie vordefinierte Bilder und benötigt keinen zusätzlichen Programm-Code für eine Kamerasteuerung und Bildumwandlung. Zudem bleiben die Ergebnisse durch die vordefinierten Bilder immer konstant. Außerdem muss sich niemand auf die Suche nach einem echten Elefanten machen.

Inception V3 und CoreML

Das Inception-V3-Modell findet sich auf der Apple-Seite zum Download. Es lässt sich per Drag-and-drop in Xcode einfügen, woraufhin die Entwicklungsumgebung eine Klasse erstellt, die alle notwendigen Schnittstellen enthält, um das Modell zu laden, die Ein- und Ausgabe-Typen und die Prediction-Methode, um eine entsprechende Vorhersage zu treffen:

override func viewDidLoad() {
super.viewDidLoad()
let model = try? VNCoreMLModel(for: Inceptionv3().model)
let request =
VNCoreMLRequest(model: model!,
completionHandler: resHandler)
requests.append(request)
}

@IBAction func predict(_ sender: UIButton) {
selectedImage.image = sender.currentImage
let image = sender.currentImage!.cgImage!
let handler = VNImageRequestHandler(cgImage: image,
options: [:])
try? handler.perform(requests)
}

func resHandler(request: VNRequest, _: Error?) {
let results = request.results
as! [VNClassificationObservation]
let percent = Int(results[0].confidence * 100)
let identifier = results[0].identifier
resultLabel.text = "\(percent)% \(identifier)"
}

Der Aufbau der Anwendung erfolgt in relativ wenigen Schritten, bei denen man sich keine Gedanken um die Bildkonvertierung machen muss. CoreML nutzt hierfür das Vision Framework, um die Bilder automatisch in das Zielformat des Modells zu konvertieren.

Die viewDidLoad-Methode lädt über die VNCoreMLModel-Klasse das Modell. Die VNCoreMLRequest-Klasse erzeugt einen Request mit diesem Modell und einer completionHandler-Methode als Parameter – in diesem Fall der resHandler-Methode. Ein Array sammelt die Request-Instanzen, von denen es im Beispiel lediglich eine gibt. Es ist durchaus möglich, mehrere Requests von unterschiedlichen Modellen aufzunehmen. Der Klickvorgang beim Auswählen eines Bildes ist als IBAction mit der predict-Methode verknüpft. Diese setzt das ausgewählte Bild und erzeugt einen Handler vom Typ VNImageRequestHandler mit der Auswahl. Die perform-Methode des Handlers erhält das Array mit den Request-Instanzen, die wiederum die resHandler-Methode enthält. Deren Aufruf erfolgt nach dem Abschluss der Objekterkennung, und sie passt den Text des resultLabel-Feldes an, indem sie das Ergebnis des ersten erkannten Objekts verwendet. Xcode erzeugt für die Ergebnisse der Inception-V3-Klasse entsprechende Properties, um direkt auf den Prozentwert der Übereinstimmung (confidence) und dem Identifier zuzugreifen, der in diesem Fall der Name des gefundenen Objekts ist.

Inception V3 und TensorFlow Lite

Das Inception-V3-Modell für TensorFlow Lite befindet sich auf GitHub. Folgendes Listing benötigt die Dateien "inception_v3.tflite" und "labels.txt" im assets-Verzeichnis des Android-Projekts:

protected void onCreate(Bundle savedInstanceState) {
// ...
reader = new BufferedReader(... "labels.txt")));
// ...
labels.add(line);

// ...
tflite =
new Interpreter(loadModelFile("inception_v3.tflite"));

button1.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
predict(context, R.drawable.image1);
}
});
button2.setOnClickListener(new View.OnClickListener() {
// ...
});
button3.setOnClickListener(new View.OnClickListener() {
// ...
});
}

private void predict(Context context, int imgId) {
// ...
Bitmap bitmap =
BitmapFactory.decodeResource(..., imgId, ...);
image.setImageBitmap(bitmap);
// ...
ByteBuffer imgBuf = convertBitmapToByteBuffer(bitmap);
float[][] labelProb = new float[1][labels.size()];
tflite.run(imgBuf, labelProb);

String label = "";
Float percent = -1.0f;
for(int i = 0; i < labels.size(); i++) {
if (labelProb[0] [i] > percent) {
percent = labelProb[0][i];
label = labels.get(i);
}
}
percent *= 100;
result.setText(percent.intValue() + "% " + label);
}

private ByteBuffer convertBitmapToByteBuffer(Bitmap bitmap) {
// ...
bitmap = Bitmap.createScaledBitmap(...);
// ...
imgData.putFloat((val >> 16) & 0xFF);
imgData.putFloat((val >> 8) & 0xFF);
imgData.putFloat(val & 0xFF);
// ...
return imgData;
}

private MappedByteBuffer loadModelFile(String modelFile) {
// ...
}

Das Laden des TensorFlow-Lite-Modells "inception_v3.tflite" erfolgt mit der loadModelFile-Hilfsmethode, die die Modelldatei einliest und als MappedByteBuffer zurückliefert. Basierend darauf erzeugt die Interpreter-Klasse eine TensorFlow-Lite-Instanz. Letztere beinhaltet eine run-Methode, die als Parameter ein input- und ein output-Objekt benötigt. Hierdurch ist die Methode generisch, jedoch müssen sich Entwickler um die entsprechenden Ein- und Ausgabetypen kümmern.

Wie schon im iOS-Beispiel wählen Anwender ein Bild auf der Oberfläche aus, wodurch der onClickListener die predict-Methode mit der Bild-ID aufruft. Diese lädt das Bild und zeigt es groß an. Außerdem passt die convertBitmapToByteBuffer-Hilfsmethode das Bild auf die richtige Größe an und konvertiert die Bilddaten in einen ByteBuffer, der als input-Wert für die run-Methode dient. Für die output-Werte kommt die von der onCreate-Methode eingelesene "labels.txt"-Datei ins Spiel. Das Programm legt entsprechend der Label-Anzahl ein Array an und befüllt es über die run-Methode mit den entsprechenden Werten. Die Methode ermittelt für jeden Label-Eintrag die Wahrscheinlichkeit zum entsprechenden Objekt und legt die Werte in das labelProb-Array ab. Im Anschluss daran sucht die predict-Methode den Wert sowie das Label mit der höchsten Wahrscheinlichkeit und zeigt diese mit Prozentwert und Namen auf der Oberfläche an.

Die Android-Anwendung ist insbesondere durch das Laden der Modelldatei und die Bildkonvertierung mit den Bit-Shift-Operatoren aufwendig und im Listing nur angedeutet. Das könnte sich möglicherweise in der Zukunft ändern.

Vor dem Start der Anwendung ist in der "build.gradle"-Datei die Abhängigkeit zur TensorFlow-Lite-Bibliothek einzutragen und die aaptOptions, damit die "tflite"- und "lite"-Dateien nicht komprimiert eingelesen werden und einen Fehler verursachen:

aaptOptions {
noCompress "tflite"
noCompress "lite"
}
// ...
implementation 'org.TensorFlow:TensorFlow-lite:+'