September 2017
Band 32, Nummer 9
Test Run: Trainieren von DNNs (Deep Neural Networks)
Von James McCaffrey
Ein normales FNN (Feedforward Neural Network) weist eine Sammlung von Eingabeknoten, eine Sammlung verborgener Verarbeitungsknoten und eine Sammlung von Ausgabeknoten auf. Wenn Sie z. B. die politischen Ansichten einer Person (konservativ, gemäßigt, liberal) basierend auf ihrem Alter und Einkommen vorhersagen möchten, erstellen Sie ein FNN mit zwei Eingabeknoten, acht verborgenen Knoten und drei Ausgabeknoten. Die Anzahl der Eingabe- und Ausgabeknoten wird durch die Struktur Ihrer Daten bestimmt, die Anzahl der verborgenen Knoten ist jedoch ein frei wählbarer Parameter, den Sie durch Versuch und Irrtum ermitteln müssen.
Ein DNN (Deep Neural Network) weist mindestens zwei verborgene Ebenen auf. In diesem Artikel beschreibe ich, wie ein DNN mithilfe des Backpropagation-Algorithmus trainiert wird und erläutere das Problem des „verschwindenden Gradienten“. Nachdem Sie diesen Artikel gelesen haben, verfügen Sie über Code zum Experimentieren und über ein besseres Verständnis dafür, was hinter den Kulissen geschieht, wenn Sie eine neuronale Netzwerkbibliothek wie das Microsoft Cognitive Toolkit (CNTK) oder Google TensorFlow verwenden.
Betrachten Sie das in Abbildung 1 gezeigte DNN. Das DNN weist drei verborgene Ebenen auf, die jeweils über vier, zwei und zwei Knoten verfügen. Die Werte der Eingabeknoten sind 3,80 und 5,00. Sie stehen für das normalisierte Alter (38 Jahre) und Einkommen (50.000 US-Dollar pro Jahr) einer Person. Jeder der acht verborgenen Knoten weist einen Wert zwischen -1,0 und +1,0 auf, weil das DNN tanh-Aktivierung verwendet.
Abbildung 1: Ein Beispiel-DNN mit der Struktur 2-(4,2,2)-3
Die vorläufigen Ausgabeknoten weisen die Werte (-0,109, 3,015, 1,202) auf. Diese Werte werden mithilfe der Softmax-Funktion in die endgültigen Ausgabewerte (0,036, 0,829, 0,135) konvertiert. Die Aufgabe von Softmax besteht im Erzwingen, dass die Summe der Werte des Ausgabeknotens 1,0 ist, damit sie als Wahrscheinlichkeiten interpretiert werden können. Unter der Annahme, dass die Ausgabeknoten die Wahrscheinlichkeiten für „konservativ“, „gemäßigt“ und „liberal“ darstellen, wird für die Person in diesem Beispiel vorhergesagt, dass sie eine gemäßigte politische Einstellung besitzt.
In Abbildung 1 stellt jeder der Pfeile, die Knoten verbinden, eine numerische Konstante dar, die als „Gewichtung“ bezeichnet wird. Gewichtungswerte liegen normalerweise zwischen -10,0 und +10,0, können im Prinzip aber jeden beliebigen Wert aufweisen. Jeder der kleinen Pfeile, die auf die verborgenen Knoten und die Ausgabeknoten verweisen, ist eine numerische Konstante, die als „Bias“ bezeichnet wird. Die Werte der Gewichtungen und Bias bestimmen die Werte des Ausgabeknotens.
Der Vorgang des Ermittelns der Werte für die Gewichtungen und Bias wird als Trainieren des Netzwerks bezeichnet. Die Idee dahinter besteht im Verwenden einer großen Menge von Trainingsdaten, die bekannte Eingabewerte und bekannte richtige Ausgabewerte besitzen, und dem anschließenden Verwenden eines Optimierungsalgorithmus zum Ermitteln der Werte für die Gewichtungen und Bias, damit die Differenz zwischen berechneten Ausgabewerten und bekannten richtigen Ausgabewerten so gering wie möglich ist. Verschiedene Algorithmen können zum Trainieren eines DNN verwendet werden. Der am weitaus häufigsten verwendete Algorithmus ist aber der Backpropagation-Algorithmus.
Dieser Artikel setzt voraus, dass Sie über grundlegende Kenntnisse des E/A-Mechanismus neuronaler Netzwerke sowie über mindestens durchschnittliche Programmierkenntnisse verfügen. Das Demoprogramm wurde mit C# programmiert, aber es sollte keine Probleme bereiten, bei Bedarf ein Refactoring des Demoprogramms in eine andere Sprache (z. B. Python oder Java) auszuführen. Das Demoprogramm ist zu lang, um es in diesem Artikel im Ganzen darzustellen. Das vollständige Programm ist im begleitenden Codedownload enthalten.
Das Demoprogramm
Das Demoprogramm generiert im ersten Schritt 2.000 synthetische Datenelemente für das Training. Jedes Element weist vier Eingabewerte zwischen -4,0 und +4,0 auf, gefolgt von drei Ausgabewerten, die drei mögliche kategorische Werte darstellen: (1, 0, 0) oder (0, 1, 0) oder (0, 0, 1). Die folgende Abbildung zeigt das erste und das letzte Trainingselement:
[ 0] 2.24 1.91 2.52 2.41 0.00 1.00 0.00
...
[1999] 1.30 -2.41 -3.18 0.11 1.00 0.00 0.00
Hinter den Kulissen werden die Blinddaten durch Erstellen eines 4-(10,10,10)-3-DNN mit zufälligen Gewichtungen und Biaswerten generiert. Anschließend werden zufällige Eingabewerte in das Netzwerk eingespeist. Nachdem die Trainingsdaten generiert wurden, erstellt das Demoprogramm ein neues 4-(10,10,10)-3-DNN und trainiert dieses mithilfe des Backpropagation-Algorithmus. Während des Trainings werden der aktuelle mittlere quadratische Fehler und die Klassifizierungsgenauigkeit alle 200 Iterationen angezeigt.
Der Fehler nimmt wie erwartet langsam ab und die Genauigkeit langsam zu. Nachdem das Training abgeschlossen ist, wird eine endgültige Genauigkeit des DNN-Modells von 93,45 Prozent erreicht. Dies bedeutet, dass 0,9345 * 2.000 = 1.869 Elemente richtig und 131 Elemente falsch klassifiziert wurden. Der Democode, der die Ausgabe generiert, beginnt wie folgt:
using System;
namespace DeepNetTrain
{
class DeepNetTrainProgram {
static void Main(string[] args) {
Console.WriteLine("Begin deep net demo");
int numInput = 4;
int[] numHidden = new int[] { 10, 10, 10 };
int numOutput = 3;
...
Das Demoprogramm verwendet nur reines C# ohne Namespaces (mit Ausnahme von „System“). Zuerst wird das DNN vorbereitet, das die simulierten Trainingsdaten generieren soll. Die Anzahl der verborgenen Schichten (3) wird implizit als die Anzahl der Elemente im numHidden-Array übergeben. Ein alternativer Entwurf besteht im expliziten Übergeben der Anzahl der verborgenen Schichten. Im nächsten Schritt werden die Trainingsdaten mit der Hilfsmethode „MakeData“ generiert:
int numDataItems = 2000;
Console.WriteLine("Generating " + numDataItems +
" artificial training data items ");
double[][] trainData = MakeData(numDataItems,
numInput, numHidden, numOutput, 5);
Console.WriteLine("Done. Training data is: ");
ShowMatrix(trainData, 3, 2, true);
Der an „MakeData“ übergebene Wert 5 ist ein Startwert für ein zufälliges Objekt, damit die Demoausführungen reproduzierbar sind. Der Wert 5 wurde nur deshalb verwendet, weil er eine ansprechende Demo ergab. Der Aufruf der Hilfsmethode „ShowMatrix“ zeigt die ersten drei Zeilen sowie die letzte Zeile der generierten Daten mit zwei Dezimalstellen und Indizes (TRUE) an. Nun wird das DNN erstellt und das Training vorbereitet:
Console.WriteLine("Creating a 4-(10,10,10)-3 DNN");
DeepNet dn = new DeepNet(numInput, numHidden, numOutput);
int maxEpochs = 2000;
double learnRate = 0.001;
double momentum = 0.01;
Das Demoprogramm verwendet eine vom Programm definierte Klasse „DeepNet“. Der Backpropagation-Algorithmus ist iterativ. Daher muss eine maximale Anzahl von Iterationen (in diesem Fall 2.000) angegeben werden. Der Lernratenparameter steuert, in welchem Umfang die Gewichtungen und Biaswerte bei jeder Verarbeitung eines Trainingselements angepasst werden. Eine kleine Lernrate kann dazu führen, dass das Training zu langsam ist (Stunden, Tage oder noch länger), eine schnelle Lernrate hingegen kann zu heftig schwankenden Ergebnissen führen, die sich nie stabilisieren. Das Auswählen einer guten Lernrate ist von Versuch und Irrtum geprägt und stellt eine der Hauptherausforderungen beim Arbeiten mit DNNs dar. Der Eigendynamikfaktor gleicht ein wenig einer Hilfslernrate und beschleunigt normalerweise das Training, wenn eine kleine Lernrate verwendet wird.
Der Aufrufcode des Demoprogramms wird folgendermaßen abgeschlossen:
...
double[] wts = dn.Train(trainData, maxEpochs,
learnRate, momentum, 10);
Console.WriteLine("Training complete");
double trainError = dn.Error(trainData, false);
double trainAcc = dn.Accuracy(trainData, false);
Console.WriteLine("Final model MS error = " +
trainError.ToString("F4"));
Console.WriteLine("Final model accuracy = " +
trainAcc.ToString("F4"));
Console.WriteLine("End demo ");
}
Die Methode „Train“ verwendet den Backpropagation-Algorithmus, um Werte für die Gewichtungen und Bias zu ermitteln, damit die Differenz zwischen den berechneten Ausgabewerten und den richtigen Ausgabewerten so gering wie möglich ist. Die Werte der Gewichtungen und Bias werden von „Train“ zurückgegeben. Das an „Train“ übergebene Argument mit dem Wert 10 bedeutet, dass alle 2.000 / 10 = 200 Iterationen Statusmeldungen angezeigt werden. Die Überwachung des Status ist wichtig, weil beim Trainieren eines neuronalen Netzwerks unerwünschte Effekte auftreten können (und häufig auch tatsächlich auftreten).
Nachdem das Training abgeschlossen ist, werden der Fehler und die Genauigkeit des Modells abschließend berechnet und mithilfe der endgültigen Gewichtungen und Biaswerte angezeigt, die sich noch im DNN befinden. Die Gewichtungen und Bias könnten durch Ausführen der Anweisung „dnn.SetWeights(wts)“ explizit neu geladen werden. In diesem Fall ist das aber nicht erforderlich. Die an die Methoden „Error“ und „Accuracy“ übergebenen FALSE-Argumente bedeuten, dass keine Diagnosemeldungen angezeigt werden sollen.
DNN-Gradienten und -Gewichtungen
Jede Gewichtung und jeder Bias in einem DNN besitzt einen zugehörigen Gradientenwert. Ein Gradient ist ein abgeleiteter Wert der Fehlerfunktion und stellt nur einen Wert (z. B. -1,53) dar. Dabei informiert Sie das Vorzeichen des Gradienten, ob die zugehörige Gewichtung oder der Bias vergrößert oder verkleinert werden sollte, um Fehler zu verringern, und die Größenordnung des Gradienten ist proportional dazu, in welchem Ausmaß die Gewichtung oder der Biaswert geändert werden sollte. Angenommen, eine der Gewichtungen („w“) in einem DNN besitzt den Wert +4,36, und nach der Verarbeitung eines Trainingselements wird der Gradient für die Gewichtung („g“) als +2,50 berechnet. Wenn die Lernrate („lr“) auf 0,10 festgelegt ist, ergibt sich der folgende neue Gewichtungswert:
w = w + (lr * g)= 4.36 + (0.10 * 2.50)= 4.36 + 0.25= 4.61
Das Trainieren eines DNN besteht im Wesentlichen im Ermitteln des Gradienten für jede Gewichtung und jeden Biaswert. Es stellt sich heraus, dass das Berechnen der Gradienten für die Gewichtungen, die die Knoten der letzten verborgenen Schicht mit den Knoten der Ausgabeschicht verbinden, und der Gradienten für die Bias der Ausgabeknoten relativ einfach ist, obwohl der zugrunde liegende mathematische Ansatz außerordentlich schwierig ist. In Code ausgedrückt, besteht der erste Schritt im Berechnen der sogenannten Ausgabeknotensignale für jeden Ausgabeknoten:
for (int k = 0; k < nOutput; ++k) {
errorSignal = tValues[k] - oNodes[k];
derivative = (1 - oNodes[k]) * oNodes[k];
oSignals[k] = errorSignal * derivative;
}
Die lokale Variable „errorSignal“ ist die Differenz zwischen dem Zielwert (dem richtigen Knotenwert aus den Trainingsdaten) und dem berechneten Ausgabeknotenwert. Die Details können sehr kompliziert sein. Der Democode verwendet z. B. (target - output), einige Verweise hingegen (output - target). Dies wirkt sich darauf aus, ob die zugehörige Anweisung zum Aktualisieren der Gewichtungen beim Ändern der Gewichtungen Additionen oder Subtraktionen ausführen sollte.
Die lokale Variable „derivative“ ist ein abgeleiteter Wert (nicht der gleiche Wert wie der Gradient, der ebenfalls ein abgeleiteter Wert ist) der Ausgabeaktivierungsfunktion, die in diesem Fall die Softmax-Funktion ist. Wenn Sie also eine andere Funktion als Softmax verwenden, müssen Sie die Berechnung der lokalen Variable „derivative“ ändern.
Nachdem die Ausgabeknotensignale berechnet wurden, können diese zum Berechnen der Gradienten für die Gewichtungen von den verborgenen zu den Ausgabeknoten verwendet werden:
for (int j = 0; j < nHidden[lastLayer]; ++j) {
for (int k = 0; k < nOutput; ++k) {
hoGrads[j][k] = hNodes[lastLayer][j] * oSignals[k];
}
}
Der Gradient für eine Gewichtung, die einen verborgenen Knoten mit einem Ausgabeknoten verbindet, ist also der Wert des verborgenen Knotens, multipliziert mit dem Ausgabesignal des Ausgabeknotens. Nachdem der Gradient für die Gewichtung von einem verborgenen zu einem Ausgabeknoten berechnet wurde, kann die Gewichtung aktualisiert werden:
for (int j = 0; j < nHidden[lastLayer]; ++j) {
for (int k = 0; k < nOutput; ++k) {
double delta = hoGrads[j][k] * learnRate;
hoWeights[j][k] += delta;
hoWeights[j][k] += hoPrevWeightsDelta[j][k] * momentum;
hoPrevWeightsDelta[j][k] = delta;
}
}
Zuerst wird die Gewichtung um das Delta (den Wert des Gradienten, multipliziert mit der Lernrate) inkrementiert. Anschließend wird die Gewichtung um einen zusätzlichen Wert inkrementiert: das Produkt des vorherigen Deltas, multipliziert mit dem Eigendynamikfaktor. Beachten Sie, dass die Verwendung von Eigendynamik zwar optional ist, aber fast immer erfolgt, um die Trainingsgeschwindigkeit zu erhöhen.
Zusammenfassend lässt sich Folgendes sagen: Zum Aktualisieren einer Gewichtung von einem verborgenen zu einem Ausgabeknoten berechnen Sie ein Ausgabeknotensignal, das von der Differenz zwischen dem Zielwert und dem berechneten Wert abhängt, und den abgeleiteten Wert der Ausgabeknoten-Aktivierungsfunktion (normalerweise Softmax). Im nächsten Schritt verwenden Sie das Ausgabeknotensignal und den Wert des verborgenen Knotens, um den Gradienten zu berechnen. Anschließend verwenden Sie den Gradienten und die Lernrate zum Berechnen eines Deltas für die Gewichtung und aktualisieren dann die Gewichtung mithilfe des Deltas.
Leider ist das Berechnen der Gradienten für die Gewichtung von Eingabe- zu verborgenen Knoten und der Gewichtung zwischen verborgenen Knoten wesentlich komplizierter. Eine gründliche Darstellung dieses Vorgangs würde viele Seiten umfassen. Sie erhalten jedoch eine einigermaßen gute Vorstellung dieses Vorgangs, wenn Sie einen Teil des Codes untersuchen:
int lastLayer = nLayers - 1;
for (int j = 0; j < nHidden[lastLayer]; ++j) {
derivative = (1 + hNodes[lastLayer][j]) *
(1 - hNodes[lastLayer][j]); // For tanh
double sum = 0.0;
for (int k = 0; k < nOutput; ++k) {
sum += oSignals[k] * hoWeights[j][k];
}
hSignals[lastLayer][j] = derivative * sum;
}
Dieser Code berechnet die Signale für die letzten Knoten der verborgenen Schicht (für die Knoten, die unmittelbar vor den Ausgabeknoten liegen). Der abgeleitete Wert der lokalen Variablen ist der abgeleitete Wert der Aktivierungsfunktion der verborgenen Schicht (in diesem Fall tanh). Die verborgenen Signale hängen jedoch von einer Summe der Produkte ab, die die Ausgabeknotensignale einschließt. Dies führt zum Problem des „verschwindenden Gradienten“.
Das Problem des verschwindenden Gradienten
Wenn Sie den Backpropagation-Algorithmus zum Trainieren eines DNN verwenden, werden während des Trainings die Gradientenwerte, die mit den Gewichtungen zwischen verborgenen Knoten verbunden sind, schnell sehr klein oder sogar null. Wenn ein Gradientenwert null ist, ist der Gradient, multipliziert mit der Lernrate, ebenfalls null, und das Gewichtungsdelta ist null. Die Gewichtung ändert sich also nicht. Selbst wenn ein Gradient nicht gegen null geht, aber sehr klein wird, ist das Delta winzig, und das Training erfolgt nur noch im Schneckentempo.
Der Grund, warum Gradienten schnell gegen null gehen, sollte klar werden, wenn Sie den Democode gründlich untersuchen. Da die Werte der Ausgabeknoten erzwungenerweise Wahrscheinlichkeiten sind, liegen sie alle zwischen 0 und 1. Dies führt zu Ausgabeknotensignalen, die zwischen 0 und 1 liegen. Die Multiplikation beim Berechnen der Signale des verborgenen Knotens beinhaltet daher das wiederholte Multiplizieren mit Werten zwischen 0 und 1. Das Ergebnis sind immer kleiner werdende Gradienten. Beispiel: 0,5 * 0,5 * 0,5 * 0,5 = 0,0625. Außerdem führt die tanh-Aktivierungsfunktion der verborgenen Schicht zu einem weiteren durch Multiplizieren kleiner Dezimalwerte verursachten Problem.
Das Demoprogramm zeigt das Problem des verschwindenden Gradienten anhand des Gradienten, der der Gewichtung von Knoten 0 in der Eingabeschicht zu Knoten 0 in der ersten verborgenen Schicht zugeordnet ist. Der Gradient für diese Gewichtung wird schnell kleiner:
epoch = 200 gradient = -0.002536
epoch = 400 gradient = -0.000551
epoch = 600 gradient = -0.000141
epoch = 800 gradient = -0.159148
epoch = 1000 gradient = -0.000009
...
Der Gradient steigt vorübergehend bei Epoche 800 auf einen hohen Wert an, weil das Demoprogramm Gewichtungen und Bias nach der Verarbeitung jedes Trainingselements aktualisiert (dies wird als „stochastisches Training“ oder „Onlinetraining“ im Gegensatz zu „Batchtraining“ oder „Minibatchtraining“ bezeichnet), und rein zufällig hat das bei Epoche 800 verarbeitete Trainingselement zu einem Gradienten geführt, der größer als normal ist.
In den DNN-Frühzeiten vor ungefähr 25 oder 30 Jahren war das Problem des verschwindenden Gradienten ein K.-o.-Kriterium. Durch verbesserte Computeleistung wurde der verschwindende Gradient zu einem kleineren Problem, weil das Training eine kleine Verlangsamung verkraften konnte. Mit dem Aufkommen sehr tiefer Netzwerke mit Hunderten oder sogar Tausenden von verborgenen Schichten trat das Problem jedoch erneut an den Tag.
Zahlreiche Techniken wurden entwickelt, um dem Problem des verschwindenden Gradienten beizukommen. Ein Ansatz besteht im Verwenden der ReLU-Funktion (Rectified Linear Unit) anstelle der tanh-Funktion für die Aktivierung der verborgenen Schicht. Ein anderer Ansatz setzt verschiedene Lernraten für verschiedene Schichten ein: größere Raten für Schichten, die näher an der Eingabeschicht liegen. Außerdem stellt die Verwendung von GPUs für Deep Learning heutzutage den Standard dar. Eine radikaler Ansatz vermeidet jede Form von Backpropagation und setzt stattdessen einen Optimierungsalgorithmus ein, der keine Gradienten erfordert, z. B. Partikelschwarmoptimierung.
Zusammenfassung
Der Begriff „DNN“ (Deep Neural Network) bezieht sich meistens auf den in diesem Artikel beschriebenen Typ von Netzwerk: auf ein vollständig verbundenes Netzwerk mit mehreren verborgenen Schichten. Es gibt aber viele weitere Typen von DNNs. Convolutional Neural Networks eignen sich ausgezeichnet für die Bildklassifizierung. LSTM-Netzwerke (Long Short-Term Memory) sind außerordentlich leistungsfähig bei der Verarbeitung natürlicher Sprache. Die meisten Varianten von DNNs verwenden eine Form von Backpropagation und sind vom Problem des verschwindenden Gradienten betroffen.
Dr. James McCaffreyist in Redmond (Washington) für Microsoft Research tätig. Er hat an verschiedenen Microsoft-Produkten mitgearbeitet, unter anderem an Internet Explorer und Bing. Dr. McCaffrey erreichen Sie unter jamccaff@microsoft.com.
Unser Dank gilt den folgenden technischen Experten von Microsoft für die Durchsicht dieses Artikels: Chris Lee und Adith Swaminathan