Portabilität für Deep-Learning-Modelle mit ONNX

Mit Open-Source-Tools wie TensorFlow oder PyTorch lassen sich Machine-Learning-Modelle relativ einfach erstellen und trainieren, aber das Übertragen in den produktiven Betrieb oder zu anderen Tools ist alles andere als trivial. Abhilfe soll das ONNX-Format zum Austausch von Modellen schaffen.

In Pocket speichern vorlesen Druckansicht 2 Kommentare lesen
Portabilität für Deep-Learning-Modelle mit ONNX
Lesezeit: 16 Min.
Von
  • Marcel Kurovski
Inhaltsverzeichnis

Andrew Ng, der zu den bekanntesten Köpfen im Bereich Künstliche Intelligenz (KI) zählt, hat den Satz "KI ist die neue Elektrizität" geprägt. Er ruft regelmäßig zu einer KI-getriebenen Gesellschaft auf, wie bei seinem Auftritt bei der AI Frontiers Konferenz 2017. Doch trotz zahlreicher wissenschaftlicher Durchbrüche, begeisternder Proof-of-Concept-Präsentation und vor allem ständig neuer Frameworks für Machine Learning (ML) bleiben Themen wie die einfache Überführung der Modelle in den Produktivbetrieb und die Interoperabilität zwischen Frameworks bisher auf der Strecke.

Analog zu der Elektrizität, die ihren Weg aus dem Labor in Milliarden Haushalte, Büros und Fabriken gefunden hat, lassen sich ML-Anwendungen schnell und einfach einem größerem Publikum zugänglich machen. Der praktische Weg vom Prototyping zum produktiven Einsatz eines Modells ist nicht an ein spezielles Framework gebunden. Stattdessen lassen sich die Stärken unterschiedlicher Frameworks kombinieren.

An der Stelle kommt das Format Open Neural Network eXchange (ONNX) zum Einsatz, das das Exportieren trainierter und untrainierter Modelle aus einem Framework in eine standardisierte Darstellung ermöglicht, um sie anschließend in einem anderen Framework wiederzuverwenden. Zur Veranschaulichung soll ein Praxisbeispiel zum Erkennen von Ziffern und Buchstaben in Bildern dienen, das in folgenden vier Schritten den Weg von der Exploration bis zur Produktion durchläuft:

  • Datenexploration
  • Modellentwicklung
  • Modellübersetzung
  • Modell-Deployment

Als Grundlage dient der EMNIST Datensatz. Das prototypische Modell entsteht in PyTorch. Auf den Export des Modells in eine generische Repräsentation über ONNX folgt dessen Import in TensorFlow und das Abspeichern in ein serialisiertes Dateiformat. Für das abschließende Deployment kommt GraphPipe zum Einsatz, das einen Modellserver mit TensorFlow Serving erzeugt, der als Antwort auf REST-Anfragen Klassifikationen der Testbilder liefert.

Der gesamte Code ist im GitHub-Repository des Autors verfügbar.

Übersetzung von PyTorch zu TensorFlow mit ONNX (Abb. 1)

Data Scientists schauen typischerweise zu Beginn jedes Projekts zunächst in ihre Daten und versuchen, eine grobe Vorstellung von Struktur, Qualität und Umfang zu erhalten. Der MNIST-Datensatz gilt dafür als eine Art "Hello World" des Deep Learning und ist Grundlage vieler Tutorials. Weniger bekannt, aber anspruchsvoller ist hingegen der Extended-MNIST-Datensatz (EMNIST). Die Erweiterung umfasst neben handschriftlichen Ziffern zusätzlich Klein- und Großbuchstaben des lateinischen Alphabets. Somit wächst die Zahl der Klassen von zehn auf 62 (10 + 2 * 26). Das Auseinanderhalten ist damit deutlich schwieriger, aber auch realistischer. Das Modell muss zwischen "0", "O" und "o" unterscheiden, was nicht nur bei Handschriften eine echte Herausforderung sein kann.

Das US National Institute of Standards and Technology (NIST) stellt den Datensatz bereit, der insgesamt 814.255 Graufarben-Bilder im Format 28x28 umfasst. Er bietet eine Aufteilung in Trainings- und Testdatensatz im Verhältnis 6:1. Wer mehr Informationen sucht, findet sie in der ausführlichen Beschreibung.

EMNIST Beispielbilder für 8, a, N (Abb. 2)
Mehr Infos

ML-Konferenz M³ London

Vom 30. September bis zum 2. Oktober findet in London die M³ 2019 statt. Für die von heise Developer und The Register ausgerichteten Entwicklerkonferenz zu Machine Learning gilt noch bis zum 22. Juli der Frühbucherrabatt mit günstigeren Ticketpreisen.

Ungleichmäßige Klassenhäufigkeiten sind ein erwartetes Problem in dem Datensatz. Die Hälfte der Bilder zeigt Ziffern und nur jeweils ein Viertel Groß- und Kleinbuchstaben. Innerhalb der Buchstabengruppen schwanken die Anteile der einzelnen Buchstaben zwischen ein und fünfzehn Prozent – ebenso wie ihre Verwendungshäufigkeit in der Sprache stark streut. Nichtsdestotrotz sind die Verteilungen zwischen Trainings- und Testdatensatz ähnlich. Eine ungleichmäßige Klassenhäufigkeit kann die Klassifikationsgüte negativ beeinflussen, aber als Gegenmittel existieren viele Methoden wie Downsampling, Upsampling, oder synthetisches Upsampling.

Auf die Betrachtung des Datensatzes folgt die eigentliche Aufgabe: Ziel ist es, ein Graustufenbild des Formats 28x28 einer Ziffer oder eines Buchstabens korrekt zu klassifizieren. Die vorhergesagte Klasse sollte im Idealfall immer der tatsächlichen entsprechen. Das Quantifizieren der Zielerreichung erfolgt anhand der Klassifikationsgüte auf den Testdaten, die im Idealfall 100 Prozent beträgt. Dazu dient das sogenannte Fitting eines geeigneten Modells auf die Trainingsdaten: Es gilt, eine Parametrisierung zu finden, die den Zusammenhang zwischen Pixelwerten (Input) und Klassenlabel (Output) mit maximaler Klassifikationsgüte beschreibt. Dabei haben sich tiefe neuronale Netze als nützlich erwiesen, insbesondere Convolutional Neural Networks (CNN), die räumliche Zusammenhänge beispielsweise in Bildern gut antizipieren.

Die Modellerstellung ist typischerweise ein iterativer Prozess, bei dem ein Modell entwickelt, auf Daten trainiert und anschließend evaluiert wird, um Hypothesen für eine Verbesserung des Modells bilden zu können, die in die nächste Iteration einfließen. CRISP-DM beschreibt dieses Vorgehen, das eine Anpassung der Datenvorverarbeitung umfasst. Ein Modell mit zufriedenstellender Güte wandert in die Produktion. Das ist exemplarisch zu sehen, denn neben der reinen Genauigkeit von Vorhersagemodellen spielen andere Kriterien wie Interpretierbarkeit oder Robustheit eine zunehmende Rolle.

Für die Modellentwicklung kommt PyTorch zum Einsatz. Das von Facebook initiierte Deep-Learning-Framework ist seit August 2016 Open-Source-Software und trägt beim Schreiben dieses Artikels die Versionsnummer 1.1. PyTorch ist neben Googles TensorFlow einer der Platzhirsche unter den Deep-Learning-Frameworks. Dank der einfachen und intuitiven Bedienung, dynamischer Graphen, Integration mit Bibliotheken wie NumPy und der nativen Unterstützung von ONNX ist es gleichermaßen zugänglich für Anfänger und attraktiv für Fortgeschrittene. Zudem erfreut sich PyTorch mit mittlerweile über 18.000 Commits und über 1000 Beitragenden einer sehr aktiven Entwicklergemeinschaft.

Die Architektur eines künstlichen neuronalen Netzes lässt sich mit wenigen Codezeilen beschreiben. Einige weitere bilden die Routine für den Trainingsprozess. Der Einfachheit halber kommen im Folgenden zwei Modelle zum Tragen:

  • LinearImgClassifier ist ein flaches neuronales Netz, das nur aus einer Eingabe- und einer Ausgabeschicht besteht und somit eine Linearkombination der Eingabedaten erzeugt und
  • DNNImgClassifier ist ein tiefes neuronales Netz, das zusätzlich zwei Hidden Layer, also weitere Schichten zwischen der Ein- und der Ausgabeschicht besitzt.

Die Größe der Eingabeschicht entspricht der Anzahl der Pixel und ist demnach 28² = 784. Die Ausgabeschicht umfasst 62 Werte, also die Anzahl der unterschiedlichen Klassen. Zusätzliche Hidden Layer steigern die Komplexität des Modells, befähigen es nichtlineare Zusammenhänge zwischen Input und Output zu entdecken und erreichen häufig bessere Ergebnisse – jedoch mit der Gefahr des Overfitting. Das bedeutet, dass sich das Modell zu sehr an die Trainingsdaten anpasst statt zu generalisieren.

Zum Aktivieren der Neuronen in den Hidden Layer dient für das Beispiel die ELU-Funktion, die schnell zu besseren Ergebnissen konvergiert. Durch das Aktivieren der Werte in der letzten Schicht durch die Softmax-Funktion, ergibt sich eine Wahrscheinlichkeitsverteilung über die unterschiedlichen Klassen. Aus dieser lässt sich diejenige mit der höchsten Wahrscheinlichkeit als passende Vorhersage auswählen. Beide Modelle sind als Python-Klassen implementiert, die von torch.nn.Module erben, in ihrem Konstruktor die Vernetzung der Schichten definieren und jeweils eine forward-Funktion definieren, die die Rechenschritte zur Propagation von Eingabedaten beschreibt und letztlich die Klassenwahrscheinlichkeiten als Tensor zurückgibt:

import torch
import torch.nn.functional as F

class LinearImgClassifier(torch.nn.Module):
def __init__(self, n_input_feat, n_classes):
super(LinearImgClassifier, self).__init__()
self.linear = torch.nn.Linear(n_input_feat,
n_classes)

def forward(self, x):
y_pred = F.softmax(self.linear(x), dim=1)
return y_pred

class DNNImgClassifier(torch.nn.Module):
def __init__(self, n_input_feat, n_hidden_1,
n_hidden_2, n_classes):
super(DNNImgClassifier, self).__init__()
self.linear_1 = torch.nn.Linear(n_input_feat,
n_hidden_1)
self.linear_2 = torch.nn.Linear(n_hidden_1, n_hidden_2)
self.linear_3 = torch.nn.Linear(n_hidden_2, n_classes)

def forward(self, x):
logit_1 = self.linear_1(x)
activ_1 = F.elu(logit_1)
logit_2 = self.linear_2(activ_1)
activ_2 = F.elu(logit_2)
logit_3 = self.linear_3(activ_2)
y_pred = F.softmax(logit_3, dim=1)
return y_pred

Das "Deep" in Deep Learning steht übrigens nicht für die Zahl der Schichten. Details finden sich im Standardwerk "Deep Learning" von Goodfellow et al.. Die Autoren beschreiben, dass vielmehr die Zusammensetzung gelernter Konzepte und dadurch erlaubte Komplexität des Representation Learning für Tiefe steht als lediglich die Anzahl der Hidden Layer, wie leider oftmals falsch angenommen wird.

Nun folgt ein für die Bilderkennung durchaus üblicher Prozess: Das Training beider Modelle erfolgt für jeweils fünf Epochen über Mini-Batch Gradient Descent. Anschließend greift der Adam Optimizer mit dem Minimieren der mittleren Kreuzentropie, die eine differenzierbare Quantifizierung des Klassifikationsfehlers darstellt. Zuvor gilt es, die Inputdaten in ein für neuronale Netze geeignetes Format umzuwandeln. Dazu dient das Normieren der Pixelwerte (Integer von 0 bis 255) auf das Intervall [0, 1]. Wer tiefer in die Vorgehensweise bei der Bilderkennung mit tiefen neuronalen Netzen einsteigen möchte, findet eine ausführliche und unterhaltsame Beschreibung in dem Video "Tensorflow and deep learning - without a PhD" von Martin Görner:

Martin Görner gibt einen umfassenden Überblick zu Machine Learning.
dnn_model = DNNImgClassifier(n_input_feat, 
n_hidden_1,
n_hidden_2,
n_classes)
criterion = torch.nn.CrossEntropyLoss(reduction='mean')
optimizer = torch.optim.Adam(dnn_model.parameters(),
lr=0.0003)

for epoch in range(n_epochs):
for batch in range(n_batches):
idxs = torch.randint(0,
n_train,
size=(batch_size,),
dtype=torch.long)

x_batch, y_batch = x_train_tensor_flatten[idxs],
y_train_tensor[idxs]

optimizer.zero_grad()

# 1. Forward Pass
y_pred = dnn_model(x_batch)

# 2. Loss Calculation
train_loss = criterion(y_pred, y_batch.flatten())

# 3. Backward Pass
train_loss.backward()

# 4. Parameter Update
optimizer.step()

Während des Trainings und im Anschluss hilft das Betrachten der Klassifikationsgüte beider Modelle auf einem repräsentativen Testdatensatz, um einzuschätzen, wie gut das Modell generalisiert beziehungsweise wie effektiv es seine Trainingserfahrung auf unbekannte Bilder überträgt. Abbildung 3 zeigt eine deutliche Überlegenheit des tieferen Modells mit etwa 77 % korrekt klassifizierten Zeichen.

Daher wandert im Folgenden das tiefe neuronale Netz in Produktion. An dieser Stelle ließe sich das Ergebnis durch zusätzliche Arbeitsschritte optimieren, beispielsweise durch ein oder mehrere kombinierte Regularisierungsansätze. Zudem ließen sich mit einem Convolutional Neural Network statt einem Deep Neural Network (DNN) räumliche Zusammenhänge in Bildern effektiver erkennen. Dies liegt zum einen an sogenannten Filtern, die die Nutzung gemeinsamer Parameter über das gesamte Bild erlauben und lokal einzelne Pixel verbinden. Zum anderen besitzen CNN-Architekturen meist deutlich mehr Schichten und spielen diese erhöhte Modellkomplexität bei der Klassifikation erfolgreich aus.

An der Stelle sei noch einmal auf obiges Video verwiesen. Da der Fokus dieses Artikels aber nicht auf der Verbesserung eines Ansatzes im Speziellen, sondern auf der gesamten Pipeline von der Exploration bis zum Deployment liegt, sind knapp drei von vier Richtigen gar nicht so schlecht.

Genauigkeit der Testdatenklassifikation (Abb. 3)

Das Modell hat nun eine brauchbare Qualität, aber die konkrete Beschreibung der Netzwerkarchitektur und dessen Parameter sind abhängig vom Trainingsframework, also von PyTorch. Das Modell ließe sich nun aus PyTorch in die Produktion übertragen. Der Schritt lässt sich jedoch komfortabler mit TensorFlow erledigen, das mit TensorFlow Serving ein dediziertes, performantes und ausgereiftes Werkzeug für das Deployment von Deep-Learning-Modellen bietet. Für den Übergang zwischen den beiden Frameworks kommt ONNX ins Spiel, das als Übersetzer zwischen unterschiedlichen Deep-Learning-Frameworks dient und damit eine Flexibilität bei der Wahl der Tools bietet, die es erlaubt, das Beste aus mehreren Welten zu nutzen.

Amazon, Facebook, Microsoft und über 20 weitere Unternehmen unterstützen ONNX, das erstmals im Dezember 2017 vorgestellt wurde. Es erlaubt den Export und Import trainierter und untrainierter Modelle aus beziehungsweise in diverse Frameworks wie Caffe2, Microsoft Cognitive Toolkit, MXNet, CoreML, TensorFlow und PyTorch. Manche Frameworks integrieren ONNX nativ, für andere existieren Konnektoren, also dedizierte Import- oder Exportmodule (siehe Abbildung unten). Eine Übersicht der Frameworks sowie Tutorials findet sich auf der ONNX-Site.

Die Tabelle zeigt, welche der gängigen Frameworks den Import aus beziehungsweise Export zu ONNX anbieten.

Praktischerweise bietet PyTorch den Modellexport mit der Funktion torch.onnx.export nativ, sodass keine weiteren Pakete erforderlich sind:

example_input = torch.randn(1, 784)


torch.onnx.export(dnn_model,
example_input,
"../models/dnn_model_pt.onnx",
input_names=["flattened_rescaled_img_28x28"]
+ ["weight_1", "bias_1",
"weight_2", "bias_2",
"weight_3", "bias_3"],
output_names=["softmax_probabilities"],
verbose=True)

Die Parameter sind die Modellinstanz dnn_model, ein beispielhafter Input example_input und der Pfad für die Exportdatei. Input, Parameter und Output dürfen dedizierte Namen erhalten. Da der Modellexport durch Tracing erfolgt, benötigt der Export einen Dummy für den Input. Das Netzwerk propagiert diesen und nutzt die aufgerufenen Funktionen zum Erzeugen des ONNX-Graphen.

Die Ausgabe besteht aus einer binären Protobuf-Datei, die die Architektur und Parametrisierung des trainierten Modells umfasst. Ein Zielframework kann das allgemein definierte Modell anschließend importieren. Für das Bereitstellen mit TensorFlow Serving ist zunächst der Import in TensorFlow notwendig, um anschließend eine TensorFlow-spezifische Modelldatei für das Deployment zu erzeugen.

Der benötigte TensorFlow-Konnektor lässt sich über pip install onnx-tf installieren. Der Befehl onnx_tf.backend.prepare konvertiert das eingelesene ONNX-Modell in ein TensorFlow-Modell zum Speichern in einer Protobuf-Datei:

import onnx
from onnx_tf.backend import prepare
model_path = '../models/dnn_model_pt.onnx'

dnn_model_onnx = onnx.load(model_path)
dnn_model_tf = prepare(dnn_model_onnx, device='cpu')
dnn_model_tf.export_graph('../models/dnn_model_tf.pb')

Das eigentliche Deployment erfolgt mit GraphPipe, das unter anderem auf TensorFlow Serving aufsetzt. Dank dem Verwenden von Flatbuffers erreicht es deutliche Performancegewinne gegenüber der standardmäßigen TensorFlow-Serving-Implementierung.

Für das Bereitstellen des Modells in einem lokal verfügbaren Docker-Container existiert ein Docker-Image, das TensorFlow Serving enthält. Der Start erfolgt mit folgendem Befehl:

docker run -it --rm \
-v "$PWD/models:/models/" \
-p 9000:9000 \
sleepsonthefloor/graphpipe-tf:cpu \
--model=/models/dnn_model_tf.pb \
--listen=0.0.0.0:9000

Neben der in der vorletzten Zeile angegebenen TensorFlow-Modelldatei ist in der letzten Zeile der Port definiert, unter dem sich das Modell via REST abfragen lässt.

Die Ausgabe sollte in etwa folgendermaßen aussehen:

INFO[0000] Starting graphpipe-tf version 1.0.0.10.f235920 

...

INFO[0000] Using default inputs [flattened_rescaled_img_28x28:0]
INFO[0000] Using default outputs [Softmax:0]
INFO[0000] Listening on '0.0.0.0:9000'

Nach dem Deployment ist es Zeit, die Installation zu testen. Mit requests lassen sich REST-Aufrufe an den lokalen Server schicken. Im Folgenden dient dazu der über pip install graphpipe installierte GraphPipe-Client, der fünf Beispielbilder aus den Testdaten an den Server sendet:

from graphpipe import remote

n_test_instances = 5
n_test = x_test.shape[0]

for _ in range(n_test_instances):
idx = np.random.randint(n_test)
# flatten and normalize test image
x = x_test[idx].reshape(1, -1)/255
y = y_test[idx][0]
softmax_pred = remote.execute("http://127.0.0.1:9000", x)
pred_class = mapping[np.argmax(softmax_pred)]
true_class = mapping[y_test[idx][0]]
print("Predicted Label / True Label: {} == {} ? - {} !"
.format(pred_class, true_class,
(pred_class==true_class)))

Der Modellserver gibt die passenden Klassifikationen zurück:

Predicted Label / True Label: 5 == 5 ? - True !
Predicted Label / True Label: 8 == 8 ? - True !
Predicted Label / True Label: 9 == 9 ? - True !
Predicted Label / True Label: F == F ? - True !
Predicted Label / True Label: 3 == S ? - False !

Als zusätzliche Information liefert das Backend die Inferenz-Zeiten:

INFO[0379] Request for / took 194.5615ms
INFO[0379] Request for / took 1.4576ms
INFO[0379] Request for / took 1.5524ms
INFO[0379] Request for / took 4.4992ms
INFO[0379] Request for / took 6.5824ms

Der Weg von den ersten explorativen Schritten bis zu einem produktiven Modell ist dank ONNX relativ einfach umzusetzen. Das Beispiel zeigt die flexible Werkzeugwahl anhand der verbreiteten ML-Frameworks PyTorch und TensorFlow. ONNX und GraphPipe dienen als Meta-Frameworks, um zum einen die Portabilität der Modelle und zum anderen ein schnelles Deployment zu erreichen.

Damit schließt sich die Lücke zwischen Exploration und Produktion, und das Modell ist einfach zugänglich. Andrew Ng folgend fehlt aber noch ein gutes Stück Umsetzung und Fortschritt, bis Machine-Learning-Anwendungen so zugänglich wie Elektrizität sind.

Marcel Kurovski
ist Data Scientist für die Kölner inovex GmbH. Er hat sich auf Deep und Reinforcement Learning sowie deren Anwendung für Recommender-Systeme spezialisiert. Während seines Studiums des Wirtschaftsingenieurwesens hatte er einen Fokus auf Machine Learning, Simulation und Operations Research gesetzt und ist fasziniert von Künstlicher Intelligenz.
(rme)