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

Seite 2: Modellentwicklung mit PyTorch

Inhaltsverzeichnis

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)