August 2017

Band 32, Nummer 8

Test Run: DNN-E/A (Deep Neural Network) mit C#

Von James McCaffrey

James McCaffreyViele Fortschritte in Machine Learning (Treffen von Vorhersagen mithilfe von Daten) wurden in der letzten Zeit unter Verwendung von DNNs (Deep Neural Networks) umgesetzt. Beispiele sind die Spracherkennung in Cortana von Microsoft und Siri von Apple sowie die Bilderkennung, die selbstfahrende Automobile ermöglicht.

Der Begriff „Deep Neural Network“ (DNN) ist ein allgemeiner Begriff, und es gibt mehrere spezielle Variationen, z. B. Recurrent Neural Networks (RNNs) und Convolutional Neural Networks (CNNs). Die grundlegendste Form eines DNN (mit der ich mich in diesem Artikel befasse) trägt keinen besonderen Namen. Ich werde sie also einfach als ein DNN bezeichnen.

In diesem Artikel stelle ich Ihnen DNNs vor, um ein konkretes Demoprogramm zum Experimentieren bereitzustellen, damit Sie die Literatur zu DNNs besser verstehen können. Ich nutze keinen Code, der direkt in einem Produktionssystem verwendet werden kann. Der Code kann jedoch wie weiter unten beschrieben erweitert werden, um ein solches System zu erstellen. Auch wenn Sie nicht planen, ein DNN zu implementieren, finden Sie vielleicht die Erläuterung seiner Funktionsweise trotzdem interessant.

Für die Erläuterung eines DNN sind visuelle Verfahren am besten geeignet. Sehen Sie sich Abbildung 1 an. Das Deep Network weist auf der linken Seite zwei Eingabeknoten mit den Werten (1,0, 2,0) auf. Auf der rechten Seite befinden sich drei Ausgabeknoten mit den Werten (0,3269, 0,3333, 0,3398). Sie können sich ein DNN als eine komplexe mathematische Funktion vorstellen, die normalerweise mindestens zwei numerische Eingabewerte annimmt und mindestens einen numerischen Ausgabewert zurückgibt.

Ein grundlegendes Deep Neural Network
Abbildung 1: Ein grundlegendes Deep Neural Network

Das gezeigte DNN könnte einem Problem entsprechen, bei dem das Ziel die Vorhersage der politischen Parteizugehörigkeit (Demokrat, Republikaner, andere Partei) einer Person basierend auf ihrem Alter und Einkommen ist und die Eingabewerte auf bestimmte Weise skaliert sind. Wenn ein Demokrat als (1,0,0), ein Republikaner als (0,1,0) und ein Anhänger einer anderen Partei als (0,0,1) codiert ist, sagt das DNN in Abbildung 1 „Anhänger einer anderen Partei“ für eine Person mit dem Alter 1,0 und dem Einkommen 2.0 vorher, weil der letzte Ausgabewert (0,3398) der größte Wert ist.

Ein reguläres neuronales Netzwerk verfügt über eine verborgene Schicht mit Verarbeitungsknoten. Ein DNN weist mindestens zwei verborgene Schichten auf und kann sehr schwierige Vorhersageprobleme verarbeiten. Spezialisierte Typen von DNNs wie RNNs und CNNs verfügen ebenfalls über mehrere Schichten von Verarbeitungsknoten, aber auch über viel kompliziertere Verbindungsarchitekturen.

Das DNN in Abbildung 1 weist drei verborgene Schichten von Verarbeitungsknoten auf. Die erste verborgene Schicht verfügt über vier Knoten, die zweite und die dritte verborgene Schicht über zwei Knoten. Jeder lange Pfeil, der von links nach rechts zeigt, stellt eine numerische Konstante dar, die als Gewichtung bezeichnet wird. Wenn die Knoten oben in der Abbildung auf der Basis von null mit [0] indiziert sind, weist die Gewichtung, die input[0] mit hidden[0][0] (Schicht 0, Knoten 0) verbindet, den Wert 0,01 auf. Die Gewichtung, die input[1] mit hidden[0][3] (Schicht 0, Knoten 3) verbindet, besitzt den Wert 0,08 usw. Es sind 26 Knoten-zu-Knoten-Gewichtungswerte vorhanden.

Jeder der drei verborgenen und der acht Ausgabeknoten verfügt über einen kleinen Pfeil, der eine numerische Konstante darstellt, die als „Bias“ bezeichnet wird. hidden[2][0] besitzt z. B. einen Biaswert von 0,33 und output[1] einen Biaswert von 0,36. Nicht alle Gewichtungen und Biaswerte sind in der Abbildung beschriftet. Da die Werte aber sequenziell zwischen 0,01 und 0,37 liegen, können Sie den Wert einer nicht beschrifteten Gewichtung oder eines Biaswerts auf einfache Weise ermitteln.

In den folgenden Abschnitten werde ich erläutern, wie der Eingabe-/Ausgabemechanismus des DNN funktioniert, und außerdem zeigen, wie er implementiert wird. Das Demoprogramm wurde mit C# programmiert, aber es sollte keine Probleme bereiten, ein Refactoring des Codes in eine andere Sprache (z. B. Python oder JavaScript) 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

Die Zielrichtung dieses Artikels lässt sich gut anhand des Screenshots des Demoprogramms aus Abbildung 2 nachvollziehen. Das Demoprogramm entspricht dem in Abbildung 1 gezeigten DNN und verdeutlicht den Eingabe-/Ausgabemechanismus, indem es die Werte der 13 Knoten im Netzwerk anzeigt. Der Democode, der die Ausgabe generiert hat, beginnt mit dem in Abbildung 3 gezeigten Code.

Grundlegendes Deep Neural Network: Demoausführung
Abbildung 2: Grundlegendes Deep Neural Network: Demoausführung

Abbildung 3: Anfang des die Ausgabe generierenden Codes

using System;
namespace DeepNetInputOutput
{
  class DeepInputOutputProgram
  {
    static void Main(string[] args)
    {
      Console.WriteLine("Begin deep net IO demo");
      Console.WriteLine("Creating a 2-(4-2-2)-3 deep network");
      int numInput = 2;
      int[] numHidden = new int[] { 4, 2, 2 };
      int numOutput = 3;
      DeepNet dn = new DeepNet(numInput, numHidden, numOutput);

Beachten Sie, dass das Demoprogramn nur reines C# ohne Namespaces (mit Ausnahme von „System“) verwendet. Das DNN wird durch Übergeben der Anzahl der Knoten in jeder Schicht an einen vom Programm definierten DeepNet-Klassenkonstruktor erstellt. 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.

Die Werte der 26 Gewichtungen und der 11 Bias werden wie folgt festgelegt:

int nw = DeepNet.NumWeights(numInput, numHidden, numOutput);
Console.WriteLine("Setting weights and biases to 0.01 to " +
  (nw/100.0).ToString("F2") );
double[] wts = new double[nw];
for (int i = 0; i < wts.Length; ++i)
  wts[i] = (i + 1) * 0.01; 
dn.SetWeights(wts);

Die Gesamtzahl der Gewichtungen und Bias wird mithilfe einer Methode „NumWeights“ einer statischen Klasse berechnet. Wenn Sie noch einmal einen Blick auf Abbildung 1 werfen, erkennen Sie, dass die Anzahl der Gewichtungen (2*4) + (4*2) + (2*2) + (2*3) = 8 + 8 + 4 + 6 = 26 beträgt, weil jeder Knoten mit allen Knoten in der Schicht auf der rechten Seite verbunden ist. Da ein Biaswert für jeden verborgenen und jeden Ausgabeknoten vorhanden ist, beträgt die Gesamtzahl der Biaswerte 4 + 2 + 2 + 3 = 11.

Ein Array mit dem Namen „wts“ wird mit 37 Zellen instanziiert. Anschließend werden die Werte auf 0,01 bis 0,37 festgelegt. Diese Werte werden mithilfe der SetWeights-Methode in das DeepNet-Objekt eingefügt. In einem realistischen DNN, das keine Demo darstellt, würden die Werte der Gewichtungen und Bias mithilfe einer Datenmenge ermittelt, die über bekannte Eingabewerte und bekannte, richtige Ausgabewerte verfügt. Dies wird als „Trainieren des Netzwerks“ bezeichnet. Der gängigste Trainingsalgorithmus wird als „Backpropagation“ bezeichnet.

Die Methode „Main“ des Demoprogramms wird wie folgt abgeschlossen:

...
    Console.WriteLine("Computing output for [1.0, 2.0] ");
    double[] xValues = new double[] { 1.0, 2.0 };
    dn.ComputeOutputs(xValues);
    dn.Dump(false); 
    Console.WriteLine("End demo");
    Console.ReadLine();
  } // Main
} // Class Program

Die Methode „ComputeOutputs“ nimmt ein Array mit Eingabewerten an und verwendet dann den Eingabe-/Ausgabemechanismus, den ich gleich erläutern werde, um die Werte der Ausgabeknoten zu berechnen und zu speichern. Die Hilfsmethode „Dump“ zeigt die Werte der 13 Knoten an, und das Argument „false“ bewirkt, dass die Werte der 37 Gewichtungen und Bias nicht angezeigt werden.

Der Eingabe-/Ausgabemechanismus

Der Eingabe-/Ausgabemechanismus für ein DNN lässt sich am besten anhand eines konkreten Beispiels zeigen. Der erste Schritt besteht im Verwenden der Werte in den Eingabeknoten zum Berechnen der Werte der Knoten in der ersten verborgenen Schicht. Der oberste verborgene Knoten in der ersten verborgenen Schicht weist den folgenden Wert auf:
tanh( (1,0)(0,01) + (2,0)(0,05) + 0,27 ) =
tanh(0,38) = 0,3627

In Worten: „Berechne die Summe der Produkte jedes Eingabeknotens und der zugehörigen Gewichtung, addiere den Biaswert und ermittle dann den hyperbolischen Tangens der Summe“. Der hyperbolische Tangens (abgekürzt tanh) wird als die „Aktivierungsfunktion“ bezeichnet. Die tanh-Funktion akzeptiert einen beliebigen Wert von minus unendlich bis plus unendlich und gibt einen Wert im Bereich von -1,0 bis +1,0 zurück. Wichtige alternative Aktivierungsfunktionen sind z. B. logistische Sigmoid- und ReLU-Funktionen (Rectified Linear Unit), die aber in diesem Artikel nicht behandelt werden.

Die Werte der Knoten in den restlichen verborgenen Schichten werden auf genau die gleiche Weise berechnet. Für hidden[1][0] ergibt sich beispielsweise der folgende Wert:
tanh( (0,3627)(0,09) + (0,3969)(0,11) + (0,4301)(0,13) + (0,4621)(0,15) + 0,31 ) =
tanh(0,5115) = 0,4711
Für hidden[2][0] gilt Folgendes:
tanh( (0,4711)(0,17) + (0,4915)(0,19) + 0,33 ) =
tanh(0,5035) = 0,4649

Die Werte der Ausgabeknoten werden mithilfe einer anderen Aktivierungsfunktion berechnet, die als „Softmax“ bezeichnet wird. Der vorbereitende Schritt vor der Aktivierung zum Ermitteln der Summe der Produkte und der Gewichtung ist identisch:
output[0] vor der Aktivierung =
(0,4649)(0,21) + (0,4801)(0,24) + 0,35 =
0,5628
output[1] vor der Aktivierung =
(0,4649)(0,22) + (0,4801)(0,25) + 0,36 =
0,5823
output[2] vor der Aktivierung =
(0,4649)(0,23) + (0,4801)(0,26) + 0,37 =
0,6017

Softmax der drei beliebigen Werte (x, y, z):
softmax(x) = e^x / (e^x + e^y + e^z)
softmax(y) = e^y / (e^x + e^y + e^z)
softmax(z) = e^z / (e^x + e^y + e^z)

Dabei ist „e“ die Eulersche Zahl, die gerundet dem Wert 2,718282 entspricht. Für das DNN in Abbildung 1 ergeben sich demnach die folgenden finalen Ausgabewerte:

output[0] = e^0,5628 / (e^0,5628 + e^0,5823 + e^0,6017) = 0,3269
output[1] = e^0,5823 / (e^0,5628 + e^0,5823 + e^0,6017) = 0,3333
output[2] = e^0,6017 / (e^0,5628 + e^0,5823 + e^0,6017) = 0,3398

Der Zweck der Softmax-Aktivierungsfunktion besteht darin, die Summe 1,0 für die Ausgabewerte zu erzwingen, damit sie als Wahrscheinlichkeiten interpretiert und einem kategorischen Wert zugeordnet werden können. Weil der dritte Ausgabewert der größte Wert ist, ist der kategorische Wert, der als (0,0,1) codiert wurde, die vorhergesagte Kategorie für die Eingaben = (1,0, 2,0).

Implementieren einer DeepNet-Klasse

Um das Demoprogramm zu erstellen, habe ich Visual Studio gestartet, die Vorlage für die C#-Konsolenanwendung ausgewählt und diese „DeepNetInputOutput“ genannt. Ich habe Visual Studio 2015 verwendet. Das Demoprogramm weist aber keine nennenswerten .NET-Abhängigkeiten auf, sodass jede Version von Visual Studio geeignet sein sollte.

Nach dem Laden des Vorlagencodes habe ich im Fenster des Projektmappen-Explorers mit der rechten Maustaste auf die Datei „Program.cs“ geklickt, die Datei in „DeepNetInputOutputProgram.cs“ umbenannt und Visual Studio die automatische Umbenennung der Klasse „Program“ gestattet. Oben im Editor-Fenster habe alle überflüssigen using-Anweisungen außer der Anweisung gelöscht, die auf den Namespace „System“ verweist.

Ich habe das Demo-DNN als eine Klasse namens „DeepNet“ implementiert. Die Klassendefinition beginnt folgendermaßen:

public class DeepNet
{
  public static Random rnd; 
  public int nInput; 
  public int[] nHidden; 
  public int nOutput; 
  public int nLayers; 
...

Alle Klassenmember werden aus Gründen der Einfachheit mit öffentlichem Bereich deklariert. Der statische Random-Objektmember namens „rnd“ wird von der DeepNet-Klasse zum Initialisieren von Gewichtungen und Biaswerten mit kleinen Zufallswerten verwendet (die anschließend mit den Werten 0,01 bis 0,37 überschrieben werden). Die Member „nInput“ und „nOuput“ stellen die Anzahl der Eingabe- und Ausgabeknoten dar. Der Arraymember „hHidden“ enthält die Anzahl der Knoten in jeder verborgenen Schicht. Die Anzahl der verborgenen Schichten wird daher durch die Length-Eigenschaft des Arrays angegeben, die zweckmäßigerweise im Member „nLayers“ gespeichert wird. Die Klassendefinition wird folgendermaßen fortgesetzt:

public double[] iNodes;
public double [][] hNodes;
public double[] oNodes;

Für die Implementierung eines DNN bestehen zahlreiche Auswahlmöglichkeiten bezüglich des Entwurfs. Die Arraymember „iNodes“ und „oNodes“ enthalten wie erwartet die Eingabe- und Ausgabewerte. Der Member „hNodes“ des verzweigten Arrays enthält die Werte des verborgenen Knotens. Ein alternativer Entwurf besteht im Speichern aller Knoten in einer verzweigten Arraystruktur „nnNodes“. Dabei ist im Demoprogramm „nnNodes[0]“ ein Array aus Eingabeknotenwerten und „nnNodes[4]“ ein Array aus Ausgabeknotenwerten.

Die Gewichtungen zwischen den Knoten werden mithilfe der folgenden Datenstrukturen gespeichert:

public double[][] ihWeights; 
public double[][][] hhWeights;
public double[][] hoWeights;

Der Member „ihWeights“ ist eine Matrix im Stil eines verzweigten Arrays, die die Gewichtungen der Eingaben in die erste verborgene Schicht speichert. Der Member „hoWeights“ ist eine Matrix im Stil eines verzweigten Arrays, die die Gewichtungen für das Verbinden der Knoten der letzten verborgenen Schicht mit den Ausgabeknoten speichert. Der Member „hhWeights“ ist ein Array, in dem jede Zelle auf eine verzweigte Arraymatrix zeigt, die die Gewichtungen innerhalb der verborgenen Schicht speichert. „hhWeights[0][3][1]“ enthält z. B. die Gewichtungen für das Verbinden des verborgenen Knotens [3] in der verborgenen Schicht [0] mit dem verborgenen Knoten [1] in der verborgenen Schicht [0+1]. Diese Datenstrukturen bilden das Herzstück des DNN-Eingabe-/Ausgabemechanismus und sind ein wenig kompliziert. Abbildung 4 zeigt das Konzept dieser Datenstrukturen.

Datenstrukturen für Gewichtungen und Bias
Abbildung 4: Datenstrukturen für Gewichtungen und Bias

Die letzten beiden Klassenmember enthalten die Bias des verborgenen Knotens und die Bias des Ausgabeknotens:

public double[][] hBiases;
public double[] oBiases;

Genau wie bei den anderen Softwaresystemen, mit denen ich arbeite, sind auch für DNNs zahlreiche alternative Datenstrukturentwürfe denkbar. Sie müssen aber beim Schreiben von Eingabe-/Ausgabecode unbedingt ein Konzept für diese Datenstrukturen berücksichtigen.

Berechnen der Anzahl von Gewichtungen und Biaswerten

Zum Festlegen der Gewichtungen und Biaswerte müssen Sie wissen, wie viele Gewichtungen und Biaswerte vorhanden sind. Das Demoprogramm implementiert die statische Methode „NumWeights“ zum Berechnen und Zurückgeben dieser Anzahl. Erinnern Sie sich daran, dass das 2-(4-2-2)-3-Demonetzwerk (2*4) + (4*2) + (2*2) + (2*3) = 26 Gewichtungen und 4 + 2 + 2 + 3 = 11 Biaswerte aufweist. Der folgende Schlüsselcode in der Methode „NumWeights“ berechnet die Gewichtungen für die Eingabe in die verborgene Schicht, die Werte innerhalb der verborgenen Schicht und die Ausgabe aus der verborgenen Schicht:

int ihWts = numInput * numHidden[0];
int hhWts = 0;
for (int j = 0; j < numHidden.Length - 1; ++j) {
  int rows = numHidden[j];
  int cols = numHidden[j + 1];
  hhWts += rows * cols;
}
int hoWts = numHidden[numHidden.Length - 1] * numOutput;

Anstatt wie mit der Methode „NumWeights“ die Gesamtzahl der Gewichtungen und Biaswerte zurückzugeben, können Sie ggf. auch die Anzahl der Gewichtungen und Biaswerte separat in einem aus zwei Zellen bestehenden Integerarray zurückgeben.

Festlegen von Gewichtungen und Biaswerten

Ein DNN, das kein Demo-DNN ist, initialisiert normalerweise alle Gewichtungen und Biaswerte mit kleinen Zufallswerten. Das Demoprogramm legt die 26 Gewichtungen mithilfe der Klassenmethode „SetWeights“ auf die Werte 0,01 bis 0,26 und die Biaswerte auf 0,27 bis 0,37 fest. Die Definition beginnt folgendermaßen:

public void SetWeights(double[] wts)
{
  int nw = NumWeights(this.nInput, this.nHidden, this.nOutput);
  if (wts.Length != nw)
    throw new Exception("Bad wts[] length in SetWeights()");
  int ptr = 0;
...

Der Eingabeparameter „wts“ enthält die Werte für die Gewichtungen und Biaswerte und weist die richtige Länge auf. Die Variable „ptr“ zeigt in das Array „wts“. Das Demoprogramm führt nur wenige Überprüfungen auf Fehler aus, um die Hauptaspekte so klar wie möglich zu zeigen. Die Gewichtungen für die Eingabe in die erste verborgene Schicht werden folgendermaßen festgelegt:

for (int i = 0; i < nInput; ++i) 
  for (int j = 0; j < hNodes[0].Length; ++j) 
    ihWeights[i][j] = wts[ptr++];

Im nächsten Schritt werden die Gewichtungen innerhalb der verborgenen Schicht festgelegt:

for (int h = 0; h < nLayers - 1; ++h) 
  for (int j = 0; j < nHidden[h]; ++j)  // From 
    for (int jj = 0; jj < nHidden[h+1]; ++jj)  // To 
      hhWeights[h][j][jj] = wts[ptr++];

Wenn Sie mit dem Arbeiten mit mehrdimensionalen Arrays nicht vertraut sind, kann die Indizierung recht schwierig sein. Ein Diagramm der Gewichtungs- und Biasdatenstrukturen ist dabei unbedingt erforderlich (für mich sowieso). Die letzten Gewichtungen für die Ausgabe aus der verborgenen Schicht werden wie folgt festgelegt:

int hi = this.nLayers - 1;
for (int j = 0; j < this.nHidden[hi]; ++j)
  for (int k = 0; k < this.nOutput; ++k)
    hoWeights[j][k] = wts[ptr++];

Dieser Code nutzt die Tatsache, dass der Index der letzten verborgenen Schicht „nLayers-1“ ist, wenn „nLayers“ verborgen sind (3 im Demoprogramm). Die Methode „SetWeights“ wird abgeschlossen, indem die Biaswerte für den verborgenen Knoten und den Ausgabeknoten festgelegt werden:

... 
  for (int h = 0; h < nLayers; ++h) 
    for (int j = 0; j < this.nHidden[h]; ++j)
      hBiases[h][j] = wts[ptr++];

  for (int k = 0; k < nOutput; ++k)
    oBiases[k] = wts[ptr++];
}

Berechnen der Ausgabewerte

Die Definition der Klassenmethode „ComputeOutputs“ beginnt wie folgt:

public double[] ComputeOutputs(double[] xValues)
{
  for (int i = 0; i < nInput; ++i) 
    iNodes[i] = xValues[i];
...

Die Eingabewerte befinden sich im Arrayparameter „xValues“. Der Klassenmember „nInput“ enthält die Anzahl der Eingabeknoten und wird im Klassenkonstruktor festgelegt. Die ersten nInput v-Werte in „xValues“ werden in die Eingabeknoten kopiert, sodass davon ausgegangen werden kann, dass „xValues“ mindestens nInput Werte in den ersten Zellen aufweist. Im nächsten Schritt werden die aktuellen Werte in den verborgenen und Ausgabeknoten auf null festgelegt:

for (int h = 0; h < nLayers; ++h)
  for (int j = 0; j < nHidden[h]; ++j)
    hNodes[h][j] = 0.0;
 
for (int k = 0; k < nOutput; ++k)
  oNodes[k] = 0.0;

Die Idee dabei ist, dass der Ausdruck für die Summe der Produkte direkt in den verborgenen und Ausgabeknoten akkumuliert wird. Diese Knoten müssen daher für jeden Methodenaufruf explizit auf 0,0 zurückgesetzt werden. Eine Alternative besteht im Deklarieren und Verwenden von lokalen Arrays mit Namen wie „hSums[][]“ und „oSums[]“. Im nächsten Schritt werden die Werte der Knoten in der ersten verborgenen Schicht berechnet:

for (int j = 0; j < nHidden[0]; ++j) {
  for (int i = 0; i < nInput; ++i)
    hNodes[0][j] += ihWeights[i][j] * iNodes[i];
  hNodes[0][j] += hBiases[0][j];  // Add the bias
  hNodes[0][j] = Math.Tanh(hNodes[0][j]);  // Activation
}

Der Code ist größtenteils eine 1:1-Zuordnung des weiter oben beschriebenen Mechanismus. Die integrierte Funktion „Math.Tanh“ wird für die Aktivierung des verborgenen Knotens verwendet. Wie ich bereits erwähnt habe, sind wichtige Alternativen die logistische Sigmoidfunktion und ReLUs (Rectified Linear Unit), die ich in einem zukünftigen Artikel erläutern werde. Nun werden die verbleibenden Knoten der verborgenen Schicht berechnet:

for (int h = 1; h < nLayers; ++h) {
  for (int j = 0; j < nHidden[h]; ++j) {
    for (int jj = 0; jj < nHidden[h-1]; ++jj) 
      hNodes[h][j] += hhWeights[h-1][jj][j] * hNodes[h-1][jj];
    hNodes[h][j] += hBiases[h][j];
    hNodes[h][j] = Math.Tanh(hNodes[h][j]);
  }
}

Dies ist hauptsächlich aufgrund der erforderlichen Arraymehrfachindizes der schwierigste Teil des Demoprogramms. Im nächsten Schritt werden die Summen der Produkte vor der Aktivierung für die Ausgabeknoten berechnet:

for (int k = 0; k < nOutput; ++k) {
  for (int j = 0; j < nHidden[nLayers - 1]; ++j)
    oNodes[k] += hoWeights[j][k] * hNodes[nLayers - 1][j];
   oNodes[k] += oBiases[k];  // Add bias 
}

Die Methode „ComputeOutputs“ wird abgeschlossen, indem die Softmax-Aktivierungsfunktion angewendet wird und die berechneten Ausgabewerte in einem separaten Array zurückgegeben werden:

...     
  double[] retResult = Softmax(oNodes); 
  for (int k = 0; k < nOutput; ++k)
    oNodes[k] = retResult[k];
  return retResult; 
}

Die Softmax-Methode ist eine statische Hilfsmethode. Details hierzu finden Sie im begleitenden Codedownload. Beachten Sie Folgendes: Weil für die Softmax-Aktivierung alle Werte erforderlich sind, die aktiviert werden (im Nennerausdruck), ist es effizienter, alle Softmax-Werte gleichzeitig und nicht separat zu berechnen. Die finalen Ausgabewerte werden in den Ausgabeknoten gespeichert und auch separat für die Vereinfachung von Aufrufen zurückgegeben.

Zusammenfassung

In den letzten Jahren wurden bei DNNs (Deep Neural Networks) enorme Forschungsaktivitäten ausgeführt und zahlreiche Durchbrüche erzielt. Spezialisierte DNNs wie etwa Convolutional Neural Networks, Recurrent Neural Networks, neuronale LSTM-Netzwerke und Residual Neural Networks sind außerordentlich leistungsfähig, aber auch sehr komplex. Meiner Meinung nach müssen Sie unbedingt verstehen, wie grundlegende DNNs funktionieren, um die komplexen Varianten begreifen zu können.

In einem zukünftigen Artikel werde ich ausführlich beschreiben, wie der Backpropagation-Algorithmus (wahrscheinlich der bekannteste und wichtigste Algorithmus in Machine Learning) zum Trainieren eines grundlegenden DNN verwendet wird. Backpropagation (oder zumindest eine Unterform davon) wird auch zum Trainieren der meisten DNN-Varianten verwendet. In diesen Erläuterungen stelle ich das Konzept des verschwindenden Gradienten vor, das seinerseits den Entwurf und die Motivation zahlreicher DNNs erklärt, die zurzeit für sehr komplexe Vorhersagesysteme verwendet werden.


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: Li Deng, Pingjun Hu, Po-Sen Huang, Kirk Li, Alan Liu, Ricky Loynd, Baochen Sun, Henrik Turbell.


Diesen Artikel im MSDN Magazine-Forum diskutieren