C++ Neuronales Netzwerk zur XOR Lösung

Andreasb.

Lt. Junior Grade
Registriert
Jan. 2011
Beiträge
269
Hallo Leute,

ich habe meine ersten Versuche mit neuronalen Netzen gestartet und wollte dazu ein Programm schreiben, welches das XOR Problem lösen soll. (Also für Input 0|0 und 1|1 soll der Output 0 sein und für 0|1 und 1|0 soll der Output 1 sein.)

Ich verwende ein neuronales Netz mit 3 Neuronen. 2 im 0. Layer und 1 Neuron im 1. (Output-) Layer.
Als Aktivierungsfunktion verwende ich die sigmoide Funktion sig(z) = 1/(1+e^-z).
Als Loss Funktion verwende ich die Root-Mean Squared Error Funktion.

Leider habe ich das Problem, dass der Output meines Programms immer gegen 1 konvergiert und dadurch eine Accuracy von ≈ 50% erreicht wird.

Ich habe schon sehr lange versucht den Fehler zu finden, aber leider finde ich den Fehler nicht. Ich vermute, aber dass es sich eher um einen Denk-/Verständnisfehler handelt als um einen Programmierfehler, weil ich Programm und Funktionen mehrfach abgeglichen habe und ich den Programmierstil möglichst schlicht gehalten habe. (Kein besonders schöner Stil, weil ich nur versucht habe dieses eine Problem mit einem hardcoded Netzwerk zu lösen)

Ich würde mich sehr freuen, wenn jemand eine Idee hat woran das Problem liegen könnte oder jemand einen Tipp hat welche Dinge ich überprüfen sollte.


Hier mein Programm:
Code:
#include <iostream>
#include <stdlib.h>
#include <math.h>
#include <time.h>

float sigmoid(float z);
void computeTeta(int numberNeuron);

/* Daten der 3 Neuronen
0. f input 1
1. f input 2
2. f output
3. weight input 1
4. weight input 2
5. bias
6. layer
*/
float neuron[3][8] = {{0,0,0,1,1,1,0},{0,0,0,1,1,1,0},{0,0,0,1,1,1,1}};
float alpha = 0.1; //Lernrate
float teta[3] = {0,0,0};

//berechnet Summe der gewichteten Inputs für das übergebene Neuron
float computeY(int numberNeuron){
	return (neuron[numberNeuron][3]*neuron[numberNeuron][0]) + (neuron[numberNeuron][4]*neuron[numberNeuron][1]) + neuron[numberNeuron][5];
}

//berechnet den Output f für das übergebene Neuron
void computeF(int numberNeuron){
	float y;
	y = computeY(numberNeuron);
	float f = sigmoid(y);
	neuron[numberNeuron][2] = f;
}

//gradienten Schritt zur Aktualisierung der Gewichtungen und des Bias für das übergebene Neuron
void gradientStep(int numberNeuron){
	computeTeta(numberNeuron);
	float weight1 = neuron[numberNeuron][3];
	float weight2 = neuron[numberNeuron][4];
	float bias = neuron[numberNeuron][5];

	weight1 = weight1 - alpha*teta[numberNeuron]*neuron[numberNeuron][0];
	weight2 = weight2 - alpha*teta[numberNeuron]*neuron[numberNeuron][1];
	bias = bias - alpha*teta[numberNeuron];
	
	neuron[numberNeuron][3] = weight1;
	neuron[numberNeuron][4] = weight2;
	neuron[numberNeuron][5] = bias;
}

//berechnet den relativen Fehler abhängig vom Layer in dem sich das übergebene Neuron befindet
void computeTeta(int numberNeuron){
	float f = neuron[numberNeuron][2];
	if(neuron[numberNeuron][6] == 1){ //Berechnung für Hidden-Layer
		float y = computeY(numberNeuron);
		teta[numberNeuron] = -(y-f)*f*(1-f);
	} else { //Berechnung für Output-Layer
		teta[numberNeuron] = f*(1-f)*(teta[2]*neuron[2][3]+teta[2]*neuron[2][4]);
	}
}

//Führt die gradienten Schritte für die 3 Neuronen durch
void backpropagation(){
	gradientStep(2);
	gradientStep(1);
	gradientStep(0);
}

// Sigmoide Aktivierungs-Funktion um kontinuierliche Werte zu erhalten
float sigmoid(float z){
	float value = 1 + pow(exp(1), -z);
	return 1/value;
}


void trainNeurons(){
	int cycles = 10000;	//Anzahl der Training-Zyklen
	for(int i = 0; i<cycles; i++){
		int in1 = rand() % 2;	//Zufällige Input Werte
		int in2 = rand() % 2;
		neuron[0][0] = in1;	//Setzt Inputs der Neuronen im 1. Layer
		neuron[0][1] = in2;
		neuron[1][0] = in1;
		neuron[1][1] = in2;
		computeF(0);		//Berechnet Outputs der Neuronen im 1. Layer
		computeF(1);
		neuron[2][0] = neuron[0][2];  //Setzt Inputs des Output-Neurons
		neuron[2][1] = neuron[1][2];
		computeF(2);				//Berechnet Output des Output-Neurons
		if(((in1 == 0 && in2 == 0)||(in1 == 1 && in2 == 1)) && neuron[2][2] >= 0.5){
			backpropagation();
		} else if(((in1 == 0 && in2 == 1)||(in1 == 1 && in2 == 0)) && neuron[2][2] < 0.5){
			backpropagation();
		}
	}
}

void testNeurons(){
	int count = 0;
	int cycles = 1000;
	for(int i = 0; i<cycles; i++){
		int in1 = rand() % 2;
		int in2 = rand() % 2;
		neuron[0][0] = in1;
		neuron[0][1] = in2;
		neuron[1][0] = in1;
		neuron[1][1] = in2;
		computeF(0);
		computeF(1);
		neuron[2][0] = neuron[0][2];
		neuron[2][1] = neuron[1][2];
		computeF(2);
		if(((in1 == -1 && in2 == 0)||(in1 == 1 && in2 == 1)) && neuron[2][2] < 0.5){
			count++;
		} else if(((in1 == 0 && in2 == 1)||(in1 == 1 && in2 == 0)) && neuron[2][2] >= 0.5){
			count++;
		}
	}
	std::cout << "Accuracy: " << (float)count/(float)cycles << std::endl;
}


int main(){
	srand(time(NULL));
	trainNeurons();
	testNeurons();
	return 0;
}
 
Ich bin der Meinung, dass du das Problem nicht mit einem Netz mit nur 3 Neuronen lösen kannst. Das Lernen ist quasi das Rausfinden von richtigen Schwellenwerten für die einzelnen Neuronen. Die Eingänge liefern bei dir eh immer 0 oder 1 und das kommt sofort beim Ausgang an und dieser braucht min. 1 um zu schalten.
Probier mal das Netz aufzuzeichnen. Vielleicht wird es dadurch etwas klarer.

Edit:
Welche Schwellenwerte bekommst du eigentlich nach dem Lernen raus?
 
Zuletzt bearbeitet:
Eigentlich sollte es doch mit 3 Neuronen funktionieren. Wenn ich mir das XOR-Problem Graphisch vorstelle, benötige ich 2 Geraden um eine Separierung im Koordinatensystem vorzunehmen. Die beiden Neuronen im 0. Layer müssten dabei diese beiden Separierungsgeraden "darstellen" und das 3. Neuron erkennt dann in welchem Bereich sich die Eingabe befindet.

So habe ich das zumindest bei verschiedenen Erklärungen verstanden.

Habe mein Programm auch mal mit dem OR Problem getestet und auch da wird immer die 1 als Ergebnis propagiert.
Einen richtigen Threshold gibt es nach der Erklärung, nach der ich gearbeitet habe nicht. Da werden nur kontinuirliche Funktionen verwendet und bei einer Ausgabe >0.5 wird das als 1 und bei einer Ausgabe <0.5 als 0 interpretiert.
 
Zuletzt bearbeitet:
Ist schon ne Weile her, dass ich mich damit beschäftigt habe, aber bist du sicher, dass du Backpropagation nur betreiben sollstest, wenn das Ergebnis nicht passt? Wenn der Output für 1^0=0,6 ist hast du immer noch nen Fehler von 0,4.
 
Zuletzt bearbeitet:
Stimmt, da hast du recht. Backprop sollte auch bei korrekten Ergebnissen ausgeführt werden. Danke für den Hinweis :)

Habe außerdem noch einen Fehler gefunden: beim Berechnen des Fehlerwertes Teta habe ich die Propagation nicht mit dem Soll-Ergebniss verglichen, sondern wieder mit der Propagation selbst. In den Formelangaben wurde für beides die gleiche Variable genutzt, aber wenn ich die Abweichung zur Propagation selbst bestimme ist es ja klar, dass die Funktion gegen 1 konvergiert, weil sich der Algorithmus immer weiter selbst bestätigen möchte. Dann habe ich die Weights und Bias Werte noch zufällig initialisiert, damit die Fälle seltener werden in denen der Gradientenabstieg ewig in eine Richtung weiter geht.

Jetzt komme ich auf eine Accuracy von um die 75%. Für das XOR-Problem sollte aber mehr drin sein, oder?
Edit: Habe den Fehler gefunden. Beim Vergleichen der Ergebnisse hatte ich vom rumprobieren mit den Werten noch einen Zahlendreher drin. Jetzt komme ich auf ca. 100% Accuracy.
 
Zuletzt bearbeitet:
Das ist der korrigierte Code:

Code:
#include <iostream>
#include <stdlib.h>
#include <math.h>
#include <time.h>
#include <random>

float sigmoid(float z);
void computeTeta(int numberNeuron);

/* Daten der 3 Neuronen
0. f input 1
1. f input 2
2. f output
3. weight input 1
4. weight input 2
5. bias
6. layer
*/
float neuron[3][7] = {{0,0,0,1,1,1,0},{0,0,0,1,1,1,0},{0,0,0,1,1,1,1}};
float alpha = 0.1; //Lernrate
float teta[3] = {0,0,0};
float label;

//berechnet Summe der gewichteten Inputs für das übergebene Neuron
float computeY(int numberNeuron){
	return (neuron[numberNeuron][3]*neuron[numberNeuron][0]) + (neuron[numberNeuron][4]*neuron[numberNeuron][1]) + neuron[numberNeuron][5];
}

//berechnet den Output f für das übergebene Neuron
void computeF(int numberNeuron){
	float y;
	y = computeY(numberNeuron);
	float f = sigmoid(y);
	neuron[numberNeuron][2] = f;
}

//gradienten Schritt zur Aktualisierung der Gewichtungen und des Bias für das übergebene Neuron
void gradientStep(int numberNeuron){
	computeTeta(numberNeuron);
	float weight1 = neuron[numberNeuron][3];
	float weight2 = neuron[numberNeuron][4];
	float bias = neuron[numberNeuron][5];

	weight1 = weight1 - alpha*teta[numberNeuron]*neuron[numberNeuron][0];
	weight2 = weight2 - alpha*teta[numberNeuron]*neuron[numberNeuron][1];
	bias = bias - alpha*teta[numberNeuron];
	
	neuron[numberNeuron][3] = weight1;
	neuron[numberNeuron][4] = weight2;
	neuron[numberNeuron][5] = bias;
}

//berechnet den relativen Fehler abhängig vom Layer in dem sich das übergebene Neuron befindet
void computeTeta(int numberNeuron){
	float f = neuron[numberNeuron][2];
	if(neuron[numberNeuron][6] == 1){ //Berechnung für Ouput-Layer
		teta[numberNeuron] = -(label-f)*f*(1-f);
	} else { //Berechnung für Hidden-Layer
		teta[numberNeuron] = f*(1-f)*(teta[2]*neuron[2][numberNeuron+3]);
	}
}

//Führt die gradienten Schritte für die 3 Neuronen durch
void backpropagation(){
	gradientStep(2);
	gradientStep(1);
	gradientStep(0);
}

// Sigmoide Aktivierungs-Funktion um kontinuierliche Werte zu erhalten
float sigmoid(float z){
	float value = 1 + exp(-z);
	return 1/value;
}


void trainNeurons(){
	int cycles = 100000;		//Anzahl der Training-Zyklen
	for(int i = 0; i<cycles; i++){
		int in1 = rand() % 2;	//Zufällige Input Werte
		int in2 = rand() % 2;
	
		label = (float)(in1^in2);
		neuron[0][0] = in1;		//Setzt Inputs der Neuronen im 0. Layer
		neuron[0][1] = in2;
		neuron[1][0] = in1;
		neuron[1][1] = in2;
		computeF(0);			//Berechnet Outputs der Neuronen im 0. Layer
		computeF(1);
		neuron[2][0] = neuron[0][2];	//Setzt Inputs des Output-Neurons
		neuron[2][1] = neuron[1][2];
		computeF(2);					//Berechnet Output des Output-Neurons
		backpropagation();
		
		#ifdef FULL_DEBUG
			if(i > cycles-5){
			for(int j = 0; j<3; j++){
				std::cout << "\n\n Cycle: " << i << std::endl;
				std::cout << "\nNeuron: " << j << "\nf input 1: " << neuron[j][0] << " f input 2: " << neuron[j][1] << std::endl;
				std::cout << "weight input 1: " << neuron[j][3] << " weight input 2: " << neuron[j][4] << std::endl;
				std::cout << "Bias: " << neuron[j][5] << "\n" << std::endl;
			}
			std::cout << "Propagation: " << neuron[2][2] << std::endl;
			}
		#endif
	}
	
}

void testNeurons(){
	int count = 0;
	int cycles = 10000;
	for(int i = 0; i<cycles; i++){
		int in1 = rand() % 2;
		int in2 = rand() % 2;
		neuron[0][0] = in1;
		neuron[0][1] = in2;
		neuron[1][0] = in1;
		neuron[1][1] = in2;
		computeF(0);
		computeF(1);
		neuron[2][0] = neuron[0][2];
		neuron[2][1] = neuron[1][2];
		computeF(2);
		if(((in1 == 0 && in2 == 0)||(in1 == 1 && in2 == 1)) && neuron[2][2] < 0.5){
			count++;
		} else if(((in1 == 0 && in2 == 1)||(in1 == 1 && in2 == 0)) && neuron[2][2] >= 0.5){
			count++;
		}
	}
	std::cout << "Accurancy: " << (float)count/(float)cycles << std::endl;
}

//Zufällige Initialisierung der Gewichtungen
void initialiseWeights(){
	std::random_device rand;
	std::mt19937 gen(rand());
	std::normal_distribution<float> d(0, 0.1);
	neuron[0][3] = d(gen); neuron[0][4] = d(gen); 
	neuron[1][3] = d(gen); neuron[1][4] = d(gen); 
	neuron[2][3] = d(gen); neuron[2][4] = d(gen); 
	neuron[0][5] = d(gen);
	neuron[1][5] = d(gen);
	neuron[2][5] = d(gen);
}

int main(){
	initialiseWeights();
	srand(time(NULL));
	trainNeurons();
	testNeurons();
	return 0;
}

Im wesentlichen habe ich nur den Fehler bei computeTeta() korrigiert und die zufällige Initialisierung der Gewichtungen hinzugefügt.
 
Was mir am Code noch auffällt.
Verwende die C++ Header der C Bibliotheken.
Statt
Code:
#include <stdlib.h>
#include <math.h>
#include <time.h>
besser
Code:
#include <cstdlib>
#include <cmath>
#include <ctime>

Keine C-Style Casts!
Statt
Code:
label = (float)(in1^in2);
besser
Code:
label = static_cast<float>(in1^in2);

Deklaration und Zuweisung am besten immer in einem Schriit durchführen.
Statt
Code:
void computeF(int numberNeuron){
	float y;
	y = computeY(numberNeuron);
	float f = sigmoid(y);
	neuron[numberNeuron][2] = f;
}
besser
Code:
void computeF(int numberNeuron){
	float y = computeY(numberNeuron);
	float f = sigmoid(y);
	neuron[numberNeuron][2] = f;
}

Dies hier geht auch besser.
Code:
void gradientStep(int numberNeuron){
	computeTeta(numberNeuron);
	float weight1 = neuron[numberNeuron][3];
	float weight2 = neuron[numberNeuron][4];
	float bias = neuron[numberNeuron][5];
 
	weight1 = weight1 - alpha*teta[numberNeuron]*neuron[numberNeuron][0];
	weight2 = weight2 - alpha*teta[numberNeuron]*neuron[numberNeuron][1];
	bias = bias - alpha*teta[numberNeuron];
	
	neuron[numberNeuron][3] = weight1;
	neuron[numberNeuron][4] = weight2;
	neuron[numberNeuron][5] = bias;
}
so...
void gradientStep(int numberNeuron){
computeTeta(numberNeuron);
float weight1 = neuron[numberNeuron][3] - (alpha * teta[numberNeuron] * neuron[numberNeuron][0]);
float weight2 = neuron[numberNeuron][4] - (alpha * teta[numberNeuron] * neuron[numberNeuron][1]);
float bias = neuron[numberNeuron][5] - (alpha * teta[numberNeuron]);
neuron[numberNeuron][3] = weight1;
neuron[numberNeuron][4] = weight2;
neuron[numberNeuron][5] = bias;
}
oder so...
void gradientStep(int numberNeuron){
computeTeta(numberNeuron);
//weight1
neuron[numberNeuron][3] = neuron[numberNeuron][3] - (alpha * teta[numberNeuron] * neuron[numberNeuron][0]);
//weight2
neuron[numberNeuron][4] = neuron[numberNeuron][4] - (alpha * teta[numberNeuron] * neuron[numberNeuron][1]);
//bias
neuron[numberNeuron][5] = neuron[numberNeuron][5] - (alpha * teta[numberNeuron]);
}
 
Zurück
Oben