Trainierte Modelle in mobilen Apps einsetzen

Zusätzliches Training

Abbildung 3 zeigt Himbeeren, wird aber als Erdbeere (strawberry) erkannt. Das liegt daran, dass das Inception-V3-Modell keine Trainingsdaten für Himbeeren enthält und die Erdbeere ihr ähnlich scheint. Um dieses Problem zu lösen, lässt sich entweder ein komplett neues Modell trainieren oder ein bestehendes ergänzen. Im letzten Fall können Entwickler mit relativ wenigen Bildern und in kurzer Zeit ein Modell trainieren statt mit einem kompletten Trainingslauf und unzähligen Bildern.

Die Himbeeren erkennt das System fälschlich als Erdbeeren (Abb. 3).

Das Inception-V3-Modell besteht aus einem Convolutional Neural Network (CNN) mit mehreren Schichten und Pools. Bei dem trainierten Modell enthalten die ersten Schichten die Strukturdaten der Bilder wie Geraden und Kurven. In den weiteren Schichten entstehen aus diesen Strukturen bestimmte Merkmale wie Augen. Die letzten Schichten definieren die zu erkennenden Objekte.

Beim Aufsetzen auf ein trainiertes Modell lassen sich die Struktur- und Merkmalsdaten wiederverwenden. Das sogenannte Transfer Learning trainiert lediglich die letzten Schichten mit den eigenen Bildern und passt die Objekterkennung an. Da das Vorgehen trotzdem Rechenleistung und Zeit in Anspruch nimmt, aber nicht unbedingt für das Verständnis des Artikels notwendig ist, geht es im folgenden Beispiel um ein einfaches XOR-Modell, dessen Trainingslauf nur wenige Sekunden benötigt. Dabei definieren Entwickler das Modell, trainieren und konvertieren es und binden es schließlich in die Anwendung ein.

Entweder oder

Die XOR-Funktion liefert beim Aufruf mit zwei Werten, die 0 oder 1 sein dürfen, 1 bei unterschiedlichen und 0 bei gleichen Eingabewerte zurück. Daraus entstehen übersichtliche Trainingsdaten, die nicht aus großen Datenmengen oder vielen Bildern bestehen müssen. Für die Beschreibung des Modells kommen Python und die Keras-Bibliothek zum Einsatz. Letztere bietet eine einfache Schnittstelle für die Modelldefinition, die im Gegensatz zu TensorFlow nicht so feingranular ist. Da Keras auf TensorFlow aufbaut, lässt sich das Modell einfach in ein TensorFlow-Lite-Modell umwandeln. Zudem existiert für CoreML ein Keras-Konverter.

training_data = np.array([[0,0], [0,1], [1,0], [1,1]])
target_data = np.array([ [0], [1], [1], [0]])

model = Sequential()
model.add(Dense(8, input_dim=2, activation=sigmoid))
model.add(Dense(1, activation=sigmoid))
model.compile(loss=mean_squared_error,
optimizer=SGD(lr=1.0))
model.fit(training_data, target_data, epochs=1000)
print model.predict(training_data)

Die Trainingsdaten bestehen aus einem NumPy-Array mit vier Werten aus jeweils zwei Eingabepärchen für die verschiedenen Eingabemöglichkeiten. Die Zieldaten (target_data) bestehen ebenfalls aus einem NumPy-Array mit den Ausgabewerten der XOR-Ergebnisse.

Das Modell ist sequenziell, also fortlaufend aufgebaut und besteht aus zwei Dense-Layern. Einer davon verbindet die Neuronen der Schicht mit allen Neuronen der folgenden Schicht. In der ersten Schicht des neuronalen Netzes besteht der Dense-Layer aus 8 Hidden-Layern und einer Eingabedimension (input_dim) von 2, um die beiden Eingabepärchen der Trainingsdaten abbilden zu können. Zum Aktivieren kommt die Sigmoid-Funktion ins Spiel. Die Aktivierungsfunktion dient vereinfacht ausgedrückt als Berechnungsfunktion der Gewichte des neuronalen Netzes mit dessen Ein- und Ausgabewerten.

Der zweite Dense-Layer besteht aus einem Output-Layer und ebenfalls der Sigmoid-Funktion zum Aktivieren. Die Eingabedimension ergibt sich aus der ersten Schicht – im konkreten Fall durch die 8 Hidden-Layer. Die Hidden-Layer der ersten Schicht sind ebenfalls Output-Layer, da sie sich aber zwischen dem ersten und zweiten Dense-Layer befinden, handelt es sich dabei um versteckte (hidden) Schichten.

Die compile-Methode konfiguriert das Modell für das Training und definiert die Loss-Funktion und den Optimizer. Während des Trainings des neuronalen Netzes berechnet die Loss-Funktion den Wert zwischen dem aktuell berechneten und dem erwarteten Wert. Dessen Minimierung ist die Aufgabe des Optimizers. Das Beispiel verwendet das Stochastic-Gradient-Descent-Verfahren (SGD) mit einer Lernrate von 1,0.

Nach dem Festlegen des Modells startet die fit-Methode das Training mit den entsprechenden Trainings- und Zieldaten. Der epochs-Parameter legt die Anzahl der Durchläufe fest, wobei ein kompletter Datendurchlauf einer Epoche entspricht. Je höher der Wert ist, desto länger läuft der Trainingsprozess, was jedoch nicht zwingend eine Verbesserung zur Folge hat. Die Ergebnisse lassen sich beispielsweise mit dem TensorBoard visuell überprüfen.

Anhand der Beispieldaten führt die predict-Methode eine Vorhersage aus. Bei dem vorhandenen Modell und der Anzahl von 2000 Epochen ergeben sich für die beiden erwarteten 0er-Ergebnisse Werte unterhalb von 0,1. Für die erwarteten 1er-Ergebnisse liegen die Werte oberhalb von 0,9. Nach dem Runden entsprechen die Werte den Zieldaten. Ein gleichbleibender Wert existiert jedoch nicht, da sich die Ausgabewerte mit jedem Trainingslauf ändern. Grund hierfür sind die zufällig initialisierten Werte, mit denen das Modell das Training startet und die Berechnung durchführt.

Speichern der Ergebnisse

Nachdem das Training abgeschlossen ist und die predict-Methode akzeptable Werte liefert, ist es an der Zeit, das Ergebnis festzuhalten und zu speichern. Zunächst geschieht das für das TensorFlow-Lite-Modell. Der TensorFlow Lite Optimizing Converter (TOCO) benötigt ein eingefrorenes Modell. Die from_session-Methode kann das eigentlich selbst erledigen, aber das Vorgehen funktionierte beim Erstellen dieses Artikels noch nicht korrekt.

Ein Modell ist mit verschiedenen Variablen aufgebaut, die der Trainingsprozess mit Werten füllt. Dazu gehören auch die des berechneten Modells. Die Methode convert_variables_to_constants wandelt die Variablen in Konstanten um und friert gleichzeitig das Modell ein. Hierfür benötigt sie die Session, die Graph-Definition, die Ausgabenamen des Models und noch eine Variablenliste, in diesem Fall aus den globalen TensorFlow-Variablen.

freeze_var_names = 
list(set(v.op.name for v in tf.global_variables()))
output_names = [model.outputs[0].name.split(":")[0]]
frozen_graph =
convert_variables_to_constants(sess,
sess.graph.as_graph_def(),
output_names,
freeze_var_names)
fgraph = tf.Graph()
with fgraph.as_default():
tf.import_graph_def(frozen_graph, name="")
fsess = tf.Session(graph=fgraph)
toco = tf.contrib.lite.TocoConverter
converter = toco.from_session(fsess,
model.inputs,
model.outputs)
tflite_model = converter.convert()
open("xor.tflite", "wb").write(tflite_model)

Um den eingefrorenen Graphen (frozen_graph) zu verwenden, erzeugt der Code zunächst eine neue Graph-Instanz und importiert ihn dann. Zusätzlich erstellt er eine neue Session mit der erstellten Instanz. Die from_session-Methode erzeugt anhand der neuen Session des Modells und der Input- und Output-Werte einen converter. Die convert-Methode wandelt die Session mit dem Graphen und den trainierten Werten in Binärdaten um, die eine einfache Schreiboperation als "xor.tflite"-Datei speichert. Künftig soll es auch möglich sein, dass der TensorFlow-Lite-Konverter (toco) das Keras-Modell direkt speichert.

Die CoreML-Tools haben bereits mehrere Konverter für verschiedene Machine-Learning-Frameworks, darunter auch Keras. Zusätzlich können auf dem CoreML-Tools-Framework weitere Konverter wie der TensorFlow-Konverter tfcoreml aufbauen. Im Gegensatz zum Keras-Konverter bietet er derzeit jedoch nicht die Option, die Namen der Input- und Output-Werte festzulegen, sondern er erzeugt die Namen anhand des internen Modellaufbaus.

from coremltools.converters.keras import convert

coreml = convert(model,
input_names = 'input',
output_names = ['result'])
coreml.input_description['input'] = '2-dim input array'
coreml.output_description['result'] = 'XOR result'
coreml.short_description = 'XOR model'
coreml.save("xor.mlmodel")

In diesem Codeausschnitt benötigt der convert-Aufruf des Keras-Konverters das Modell. Optional lassen sich die Namen der Input- und Output-Werte angeben. Xcode erzeugt anhand dieser Namen den entsprechenden Code. Zusätzlich beschreiben die description-Properties diese Werte, die Xcode auf einer Übersichtsseite des CoreML-Modells anzeigt. Die save-Methode speichert schließlich das CoreML-Modell.

Eine XOR-App

Nach dem Speichern der Modelle lassen sie sich in die jeweiligen Anwendungen integrieren. Sowohl die Android- als auch die iOS-Anwendung besteht aus vier Textfeldern, die das Ergebnis der vier möglichen Berechnungen anzeigen. Die Ergebnisse sind nicht gerundet, und sollte bei der Konvertierung kein Fehler aufgetreten sein, zeigen sie dieselben Werte an wie die Ausgabe der model.predict-Methode.

protected void onCreate(Bundle savedInstanceState) {
// ...
tflite = new Interpreter(loadModelFile("xor.tflite"));

result00.setText(prediction(0, 0));
result01.setText(prediction(0, 1));
result10.setText(prediction(1, 0));
result11.setText(prediction(1, 1));
}

private String prediction(int a, int b) {
float[][] in = new float[][]{{a, b}};
float[][] out = new float[][]{{0}};
tflite.run(in, out);
return String.valueOf(out[0][0]);
}

Wie beim Inception-Beispiel lädt der Java-Code für Android mit der loadModelFile-Hilfsmethode die TensorFlow-Lite-Modell-Datei, um sie der Interpreter-Instanz zu übergeben. Für alle vier Ergebnisse berechnet die prediction-Methode den Wert. Hierfür benötigt die run-Methode der Interpreter-Instanz ein float-Array mit zwei Eingabewerten, die als Parameter der prediction-Methode übergeben werden. Ebenso benötigt die run-Methode ein zweidimensionales float-Array für die Ausgabe. Nach dem Aufruf befindet sich das Ergebnis, das die prediction-Methode als String zurückgibt, an der Position [0][0]. Die jeweiligen Ergebnisfelder der Oberfläche ersetzen den Text mit dem Ergebniswert.

Android-XOR-Beispiel
let model = xor()

override func viewDidLoad() {
super.viewDidLoad()

out00.text = "\(predict(0, 0))"
out01.text = "\(predict(0, 1))"
out10.text = "\(predict(1, 0))"
out11.text = "\(predict(1, 1))"
}

func predict(_ a: Int, _ b: Int) -> NSNumber {
let input_data = try!
MLMultiArray(shape:[2],
dataType: MLMultiArrayDataType.float32)
input_data[0] = NSNumber(value: a)
input_data[1] = NSNumber(value: b)

let xor_input = xorInput(input: input_data)
let prediction = try! model.prediction(input: xor_input)
print(prediction.result.count)
return prediction.result[0]

Die gezeigte iOS-Anwendung hat den gleichen Aufbau wie die Android-App. Xcode generiert dynamisch für das CoreML-Modell Schnittstellen und den entsprechenden Code, sodass ein einfacher Aufruf die XOR-Instanz erzeugt. Die predict-Methode führt die Berechnung aus und erhält die entsprechenden Werte als Parameter, um sie in einem zweidimensionalen MultiArray einzutragen. Die prediction-Methode des CoreML-Modells benötigt als input-Parameter eine xorInput-Instanz mit dem zweidimensionalen MultiArray, um die Berechnung auszuführen. Das Ergebnis, das ebenfalls ein MultiArray ist, enthält den Wert im ersten Element. Diesen gibt die Methode zurück, damit das Textfeld das Ergebnis anzeigen kann.

Für iOS besteht ebenfalls die Möglichkeit, Anwendungen mit TensorFlow Lite zu entwickeln. Hierzu ist aber eine zusätzliche Abhängigkeit zu der TensorFlow-Lite-Bibliothek und die Integration der C++-Bibliothek notwendig. Das ist sowohl in Swift als auch in Objective-C möglich, jedoch aufwendiger als mit CoreML. Interessierte Leser finden beim TensorFlow-Lite-Projekt ein entsprechendes Objective-C-Beispiel.

Des Weiteren bietet das Keras-Modell Möglichkeiten für Experimente: Durch den einfachen Aufbau des Modells lassen sich die einzelnen Methoden und Parameter bequem anpassen. Ebenso ist das Training in wenigen Sekunden beendet. Somit lässt sich beispielsweise die Zahl der Hidden-Layer von 8 auf 16 oder 32 erhöhen oder die erste Aktivierungsmethode durch eine relu-Funktion (Rectified Linear Unit) austauschen, die im Gegensatz zum S-förmigen Graphen der Sigmoind-Funktion einen linearen Verlauf im positiven Wertebereich besitzt. Durch das Hinzufügen eines weiteren Dense-Layers mit 16 Hidden-Layer liefert das Training vergleichbare Ergebnisse bei einem gleichzeitig auf 100 reduzierten epochs-Parameter. Die Änderung wirkt sich jedoch auf die Größe der TensorFlow-Lite- beziehungsweise CoreML-Modelle aus.

Schlankheitskur mit Quantisierung

Das Reduzieren der sogenannten Weight-Werte zum Trainieren des Modells in einen anderen Wertebereich nennt sich Quantisierung (Quantization). Der Trainingsprozess speichert sie als 32-Bit-Float-Wert ab und benötigt somit 4 Byte pro Wert. Das TensorFlow-Lite-Konvertierungstool (toco) kann sie in 8-Bit-Werte umwandeln und reduziert so die Größe des Modells entsprechend. Meistens geht das zu Lasten der Objekterkennungsgenauigkeit. Ein Testlauf mit unterschiedlichen Testbildern kann die Veränderung aufzeigen.

Zudem müssen Entwickler die convertBitmapToByteBuffer-Methode aus dem Android-Beispiel entsprechend von Float- auf Byte-Werte anpassen. Hierzu existiert innerhalb von TensorFlow Lite ein Beispiel, das anstelle eines Inception-V3-Modells ein MobileNet-Modell mit einer 8-Bit-Quantisierung verwendet.

Auf der iOS-Seite bietet CoreML mit der Version 2 ab iOS 12 ebenfalls eine Unterstützung der Quantisierung an und kann die Werte neben 8 Bit auch auf 4, 2 und 1 Bit reduzieren. Das Anpassen des Programmcodes ist nicht erforderlich, jedoch setzt die Quantisierung mit dem CoreML-Tool eine macOS-Version 10.14 beziehungsweise iOS 12 auf dem Mobilgerät voraus.