© Shutterstock / Triff
Classic Games Reloaded – Teil 14

Intelligente Neuronen und dendritisches Lernen (II)


Das Problem des Handlungsreisenden lässt sich per maschinellem Lernen lösen. Diese anspruchsvolle Aufgabe bedarf allerdings einiger fortgeschrittener Techniken, die im vorigen Teil der Serie noch nicht zur Sprache gekommen sind. Daher ergänzen wir in diesem Artikel das bereits entwickelte Neuronenmodell, sodass es der neuen Herausforderung gewachsen ist.

Im letzten Artikel haben wir uns mit der Frage auseinandergesetzt, in welchem Umfang sich intelligente Verhaltensweisen unter Zuhilfenahme einzelner Neuronen simulieren lassen. In diesem Zusammenhang habe ich Ihnen ein verbessertes Neuronenmodell vorgestellt, das als eine Weiterentwicklung der sogenannten RBF-Neuronen angesehen werden kann. Zur Erinnerung: Im Unterschied zu den klassischen Aktivierungsfunktionen (TanH, ReLU, Sigmoid usw.) besitzen die zur Aktivierung verwendeten radialen Basisfunktionen eine Art von Gedächtnis, das einerseits durch die einzelnen Centroid-Werte c1, c2 usw. (Centroid, zu Deutsch: Schwerpunkt) und andererseits durch die zugehörigen Vorfaktoren repräsentiert wird. Ein Beispiel für eine RBF-Aktivierungsfunktion:

sumSquare = f1*(input1–c1)*(input1–c1) + f2*(input2–c2)*(input2–c2) + ...; NeuronOutput = exp(-sumSquare);

Zum Abschluss des letzten Artikels habe ich Ihnen demonstriert, dass sich bereits mit Hilfe eines einzelnen Neurons komplizierte Funktionsverläufe mit der sogenannten Polynomapproximation approximieren lassen, sofern das hierbei verwendete Neuronenmodell zusätzlich zum synaptischen Lernen auch das sogenannte dendritische Lernen unterstützt. Rekapitulieren wir: Als Dendriten (dendrites, zu Deutsch: zum Baum gehörend) werden die mehr oder weniger verzweigten Zellfortsätze bezeichnet, über welche die Reizaufnahme sowie die Filterung, Vor- und mitunter auch Weiterverarbeitung der eingehenden Signale erfolgt. Es handelt sich also um ein zusätzliches, lernfähiges Netzwerk (dendritisches Netz) innerhalb eines Netzwerks von Neuronen (neuronales Netz). An dieser Stelle werden wir uns nun zunehmend anspruchsvolleren Problemstellungen zuwenden und schauen, wie gut sich unser Neuronenmodell bei der Bewältigung dieser Aufgaben schlägt.

Logische Verknüpfungen, Entscheidungsfindung und regelbasierte KI-Routinen

Einer der Gründe, warum das Funktionsprinzip eines neuronalen Netzwerks so häufig am Beispiel diverser logischer Verknüpfungen veranschaulicht wird, besteht darin, dass man in diesem Zusammenhang auf sogenannte binäre Aktivierungsfunktionen zurückgreifen kann:

NeuronOutput = 0.0f; if (SumOfNeuronInputs > Threshold) NeuronOutput = 1.0f;

Unabhängig von der Art der jeweils betrachteten logischen Verknüpfung müssen die zu trainierenden neuronalen Netze die folgenden vier (zweidimensionalen) Eingabevektoren verarbeiten:

float InputArray1[2] = {0.0f, 0.0f}; float InputArray2[2] = {1.0f, 0.0f}; float InputArray3[2] = {0.0f, 1.0f}; float InputArray4[2] = {1.0f, 1.0f};

Für die Handhabung der einzelnen Trainingsdurchgänge würde sich die Verwendung einer einfachen for-Schleife anbieten. Diese müsste spätestens nach einer fest vorgegebenen Anzahl von Schleifendurchgängen (NumTrainingSessionsMax) abgebrochen werden, selbst wenn der Trainingsfehler (errorSum) noch oberhalb des minimal tolerierbaren Fehlers von beispielsweise 0.0001f liegen sollte:

for (int32_t j = 0; j < NumTrainingSessionsMax; j++) { epoch++; errorSum = 0.0f; // [Durchführung eines Trainingsdurchgangs] if (errorSum < 0.0001f) break; }

Es versteht sich von selbst, dass neuronale Netzwerke, die lediglich eine einzelne logische Verknüpfung erlernen können, allenfalls einen pädagogischen Wert besitzen und in gewisser Weise das Äquivalent zu einem klassischen „Hallo Welt“-Programm darstellen. Interessanterweise zeigen sich jedoch bereits bei solch einem einfachen Fallbeispiel die Limitationen und Nachteile von neuronalen Netzwerken, mit denen man im Zuge ihres Einsatzes umzugehen lernen muss. Da wäre zum einen die als „katastrophales Vergessen“ bezeichnete Eigenart, dass ein bereits trainiertes neuronales Netzwerk keine neuen Dinge mehr erlernen kann, ohne das zuvor antrainierte Wissen wieder zu vergessen. Wenn man ein neuronales Netz, das beispielsweise die Ausgabewerte der nachstehend gezeigten XOR-Verknüpfung reproduzieren kann, zu einem späteren Zeitpunkt mit den Ausgabewerten einer anderen logischen Verknüpfung erneut trainiert, geht sämtliches Wissen über die XOR-Verknüpfung wieder verloren.

  • XOR-Ausgabewerte (eXclusive OR, zu Deutsch: exklusives Oder):

    float DesiredOutput1 = 0.0f; // (Input1: 0, Input2: 0) float DesiredOutput2 = 1.0f; // (Input1: 1, Input2: 0) float DesiredOutput3 = 1.0f; // (Input1: 0, Input2: 1) float DesiredOutput4 = 0.0f; // (Input1: 1, Input2: 1)

Weiterhin zeigt es sich, dass ein neuronales Netzwerk bereits aus drei Schichten (einer Eingabeschicht, einer Schicht von verdeckten Neuronen sowie einer Ausgabeschicht) bestehen muss, um die XOR- oder die nachfolgenden XNOR-Ausgabewerte korrekt reproduzieren zu können.

  • XNOR-Ausgabewerte (exklusives Nicht/Oder):

    float DesiredOutput1 = 1.0f; // (Input1: 0, Input2: 0) float DesiredOutput2 = 0.0f; // (Input1: 1, Input2: 0) float DesiredOutput3 = 0.0f; // (Input1: 0, Input2: 1) float DesiredOutput4 = 1.0f; // (Input1: 1, Input2: 1)

Man verballert also einfach viel zu viel Rechenleistung (besonders, wenn man das Training des Netzes mit einbezieht), um so etwas Banales wie eine logische Verknüpfung zu simulieren. Auf der anderen Seite werden wir natürlich nicht daran gehindert, unser Netzwerk mit einem umfangreicheren Datensatz zu trainieren, wodurch wir es in die Lage versetzen würden, eine weitaus größere Anzahl von Wenn-dann-Entscheidungen treffen zu können, als dies bei einer einfachen logischen Verknüpfung (wenn Input1 == 1 und Input2 == 0, dann => 1 usw.) der Fall wäre. Mit unserem neuronalen Netz könnten wir dann beispielsweise ein sogenanntes regelbasiertes KI-System simulieren. Betrachten wir in diesem Zusammenhang einmal den nachfolgend gezeigten Trainingsdatensatz (aus Gründen der Einfachheit arbeiten wir weiterhin mit zweidimensionalen Inputvektoren) samt der zugehörigen Ausgabewerte:

  • Eingabevektoren:

    float InputArray1[2] = {0.0f, 0.0f}; float InputArray2[2] = {1.0f, 0.0f}; float InputArray3[2] = {0.0f, 1.0f}; float InputArray4[2] = {1.0f, 1.0f}; float InputArray5[2] = {0.5f, 1.0f}; float InputArray6[2] = {1.0f, 0.5f};
  • zugehörige Ausgabewerte:

    float DesiredOutputArray[ConstNumOfInputArrays] = { 0.005f, 0.105f, 0.205f, 0.305f, 0.405f, 0.505f };

Würde man nun die Ausgabewerte des neuronalen Netzwerks mit dem Faktor 10 multiplizieren, könnte man die jeweiligen Resultate (0, 1, 2, 3, 4 sowie 5) als Indexwerte interpretieren, mit denen sich unter Zuhilfenahme diverser Funktionszeiger- bzw. Methodenzeiger-Arrays verschiedene Funktionen aufrufen ließen:

typedef void(*pFunc)(...); // [...] pFunc BehaviorFunctionArray[3]; BehaviorFunctionArray[0] = Attack; BehaviorFunctionArray[1] = Retreat; BehaviorFunctionArray[2] = Exploration;

Wir können also die if-, else if-, else- bzw. switch-Konstrukte einer klassischen regelbasierten KI durch ein neuronales Netz ersetzen, sofern wir sicherstellen, dass auch vom Trainingsdatensatz abweichende Eingabewerte in einer weitestgehend vernünftigen Art und Weise interpretiert werden. Damit wir in diesem Zusammenhang möglichst unkompliziert auf die Trainingsdaten zugreifen können, speichern wir zunächst einmal die Adressen der für das Training verwendeten Inputvektoren innerhalb des nachstehend gezeigten Arrays von Zeigervariablen (pInputVectorPointerArray) ab:

float *pInputVectorPointerArray[ConstNumOfInputArrays]; pInputVectorPointerArray[0] = InputArray1; pInputVectorPointerArray[1] = InputArray2; // usw.

Codebeispiel und Listings zum Download

Achtung: Das Beispielprojekt und die Listings zu diesem Artikel finden Sie in unserem GitHub-Repository: https://github.com/EntwicklerMagazin/Entwickler-Magazin. Dort stehen auch die Codes zu den vorherigen Teilen der Serie bereit.

Der Autor stellt die Programmbeispiele außerdem unter www.graphics-and-physics-framework.spieleprogrammierung.net bereit.

Die Aufgabe, die es nun zu lösen gilt, besteht darin, für einen beliebigen Eingabevektor denjenigen Trainingsinputvektor zu identifizieren, der die größte Ähnlichkeit mit dem betreffenden Eingabevektor aufweist. So hat beispielsweise, wie unschwer zu erkennen ist, der nachfolgend gezeigte Eingabevektor TestInputArray1 die höchste Übereinstimmung mit dem eingangs vorgestellten InputArray4:

float TestInputArray1[2] = { 0.9f, 0.9f }; // sollte interpretiert werden als: float InputArray4[2] = { 1.0f, 1.0f };

Mit Hilfe der in Listing 1 vorgestellten Find_BestMatchedVectorFromList()-Funktion können wir die Speicheradresse eines zu einem Eingabevektor passenden Trainingsinputvektors wie nachfolgend gezeigt ermitteln und in einer Zeigervariablen hinterlegen. In unserem konkreten Beispiel würde es sich folgerichtig um die Adresse von InputArray4 handeln, die im pBestMatchedInputArray-Zeiger abgespeichert wird:

float *pBestMatchedInputArray = nullptr; Find_BestMatchedVectorFromList(&pBestMatchedInputArray, TestInputArray1, /*VectorSize:*/ 2, pInputVectorPointerArray, ConstNumOfInputArrays);

Das neuronale Netzwerk, das in dem hier vorgestellten Programmbeispiel (zu finden in der Datei mainDendriteLogicFunctionApproximation.cpp) zum Einsatz kommt, könnte nicht einfacher aufgebaut sein, da es gerade einmal aus zwei Instanzen der CNeuronV2-Klasse besteht. Das erste Neuron (PrecedingNeuron) dient zunächst zur Vorverarbeitung der Eingabewerte:

PrecedingNeuron.Init_Dendrite_Arrays(/*input:*/ 2 + /*bias:*/ 1 + /*transformedInput:*/ 10); PrecedingNeuron.Set_DendriticFunction( LogicFunctionApproximationDendriticFunction_2Inputs_To_10);

Anhand von Listing 2 können Sie die Arbeitsweise der für die Vorverarbeitung verantwortlichen dendritischen LogicFunctionApproximationDendriticFunction_2Inputs_To_10()-Funktion nachvollziehen. Im Zuge dieser Vorverarbeitung wird der zweidimensionale Inputvektor um einen zusätzlichen Bias-Wert ergänzt und in einen 10-dimensionalen Inputvektor transformiert (daher auch die Benennung: 2Inputs_To_10). Hierdurch ergibt sich eine größere Anzahl von Verknüpfungen (10 anstelle von 2) mit einem nachgeschalteten Neuron, was zur Folge hat, dass das besagte Neuron auch k...

Neugierig geworden? Wir haben diese Angebote für dich:

Angebote für Teams

Für Firmen haben wir individuelle Teamlizenzen. Wir erstellen Ihnen gerne ein passendes Angebot.

Das Library-Modell:
IP-Zugang

Das Company-Modell:
Domain-Zugang