EihiS

December 16, 2016

Neural nets, bases en C, part 8 , généralisation

Filed under: linux, neuron — Tags: , , , — admin @ 11:41 am

La fonction backWARD_CASCADE() est transformée afin d’être utilisable dans un réseau qui serait constitué de plusieurs couches, ou d’autres configurations moins conventionelles :

L’existante :

void RESO_backWARD_CASCADE(reso* n,float reverse_error)
{
	uint32_t k;
	float xb;
	xb=tanh(1.0-(n->neuron*n->neuron));
	xb*=reverse_error;
	n->error=xb;	//
	for(k=0;k<n->synapses;k++)
	{
		n->part[k]=(fabs(n->w[k])/n->wtot);//
		n->ward[k]=n->error*n->w[k];//
		n->back[k]=n->error*n->in[k];//
	}
	n->epoch++;
}

Un neurone d’une couche ‘cachée’ peut avoir plusieurs connections vers des entrées de la couche suivante :

ou, plus simplement représenté :

Il est nécéssaire de modifier la fonction pour cumuler, pour chaque neurone d’une couche ‘N’ du reseau (autre que la dernière couche , ‘de sortie ‘), l’erreur des neurones auquels il est connecté, et ceci dans la proportion à laquelle il aura participé à l’erreur de ces neurones.

on modifie donc dabord la structure du reseau, pour rendre ‘xb’ élément de la structure :

//
typedef struct {
	uint32_t synapses;
	float* in;			// forward compute inputs
	float* back;		// backward computed inputs
	float* ward;
	float* w;			// forward/backward weight for each inputs
	float* part;
	float neuron;			// forward computed output, backward compute input
	float error;
	float wtot;			// total input weights
	//
	float xb;
	//
	uint64_t epoch;
	uint64_t internal;
} reso;

Puis la fonction backWARD_CASCADE() ,dupliquée et modifiée devient :

void RESO_backWARD_CASCADE_ADDERROR(reso* n,float reverse_error)
{
	uint32_t k;
	n->xb=tanh(1.0-(n->neuron*n->neuron));
	n->xb*=reverse_error;
	n->error+=n->xb;	// on cumule maintenant la part d'erreur entrante
	for(k=0;k<n->synapses;k++)
	{
		n->part[k]=(fabs(n->w[k])/n->wtot);//
		n->ward[k]=n->xb*n->w[k];//
		n->back[k]+=n->xb*n->in[k];// on cumule les backs[] générés pour chaque entrées
	}
	n->epoch++;
}

Ensuite,une fonction pour préparer (initialiser) error et les back[] avant :

//
void RESO_prepare_CASCADE(reso* n)
{
	uint32_t k;
	for(k=0;k<n->synapses;k++) { n->back[k]=0.0;}
	n->error=0.0;
}

Avant de tester avec la nouvelle mouture de programme qui suit, quelques détails sur des ajouts :

  • ajout de macros pour modifier facilement le mode d’activation des neurones ( tanh , etc )
  • mise en #define des LR

Il y précisement 2 macros pour la fonction d’activation :

#define _OUTPUT_TRANSFORM(x) tanh(x)
ou bien :
#define _OUTPUT_TRANSFORM(x) (x>0.0)? tanh(x):0.0

La deuxième est dérivée de reLU , pour rectifier Linear Unit (google pour plus d’informations), limitée a +1.0 par le tanh, ce qui aurait pu etre fait par une autre méthode.

On définit 2 sorties , la sortie z_net et la sortie y_net attendues:

#define _REPON_Z ((ang>3.0)&&(ang<6.0))? +1.0:0.0 /* expected formula, Z output */
#define _REPON_Y (ang>3.0)?  0.0:1.0 /* expected formula, Y output */

Je vous invite à tester le programme avec l’une et l’autre de fonctions pour l’activation

#include "../common/classics.h"
#include "../common/neuron/RESO_library.c"

int main(int argc, char **argv)
{
	// le reseau S (layer1)
	reso stnet;
	reso* snet=&stnet;			// pointeur vers la structure
	//
	// le reseau T (layer1)
	reso ttnet;
	reso* tnet=&ttnet;			// pointeur vers la structure
	//
	// le reseau Y (layer 2)
	reso ytnet;
	reso* y_net=&ytnet;			// pointeur vers la structure
	// le reseau Z (layer 2)
	reso ztnet;
	reso* z_net=&ztnet;			// pointeur vers la structure
	//
	//float s_expected,t_expected,u_expected;				// utile pour stocker ce qu'on attend en reponse
	float expected_z,expected_y;// commode
	float RMSerror,RMScompute;
	float ang;					// pour un angle, sinus/cosinus
	//
	uint32_t i;		// une variable d'utilité..
	//
	//
	RESO_init(snet,3);// on initialise le reseau S, avec 3 entrées
	RESO_init(tnet,3);// on initialise le reseau T, avec 3 entrées
	//RESO_init(unet,3);// on initialise le reseau U, avec 3 entrées
	//
	RESO_init(y_net,3);// on initialise le reseau Z, avec 3 entrées
	RESO_init(z_net,3);// on initialise le reseau Z, avec 3 entrées
	//
	snet->w[0]=0.5;
	snet->w[1]=0.39;
	snet->w[2]=0.41;
	//
	tnet->w[0]=0.5;
	tnet->w[1]=0.45;
	tnet->w[2]=0.43;
	//
	//
	z_net->w[0]=0.49;
	z_net->w[1]=0.40;
	z_net->w[2]=0.5;
	//
	y_net->w[0]=0.48;
	y_net->w[1]=0.42;
	y_net->w[2]=0.5;
	//
	RMSerror=0.0;
	RMScompute=0.0;
	//
	#define _OUTPUT_TRANSFORM(x) tanh(x)
	//#define _OUTPUT_TRANSFORM(x) (x>0.0)? tanh(x):0.0
	//
	#define _LR_OUT		0.01
	#define _LR_HIDDEN	0.01
	//
	// on définit ici pour les essais, les 3 entrées, la sortie attendue par formule
	//
	#define _INA	1.0 /* un bias */
	#define _INB 	NN_frand_ab(0.0,2.0) /* inutilisé */
	#define _INC	ang /* la variable d'entrée */
	//
	#define _REPON_Z ((ang>3.0)&&(ang<6.0))? +1.0:0.0 /* expected formula, Z output */
	#define _REPON_Y (ang>3.0)?  0.0:1.0 /* expected formula, Y output */
	//
	//
	ang=0.0;
	//
	while((RMScompute>0.05)||(z_net->epoch<200))// jusqu'a RMS erreur total < 0.01
	{
		//
		snet->in[0]=_INA; //
		snet->in[1]=_INB;
		snet->in[2]=_INC;

		tnet->in[0]=_INA;
		tnet->in[1]=_INB;
		tnet->in[2]=_INC;
		//
		//
		expected_z=_REPON_Z;
		expected_y=_REPON_Y;
		//
		//	CALCUL : FORWARD
		RESO_forward(snet);
		RESO_forward(tnet);
		//
		snet->neuron=_OUTPUT_TRANSFORM(snet->neuron);
		tnet->neuron=_OUTPUT_TRANSFORM(tnet->neuron);
		// on transmet les 2 sorties de snet,tnet ver z_net, neuron de sortie, qui a 3 entrées :
		z_net->in[0]=snet->neuron;
		z_net->in[1]=tnet->neuron;
		z_net->in[2]=_INA;// entrée BIAS pour ce neurone, est INA pour les 2 neurones de la couche précédente
		//
		y_net->in[0]=snet->neuron;
		y_net->in[1]=tnet->neuron;
		y_net->in[2]=_INA;// entrée BIAS pour ce neurone, est INA pour les 2 neurones de la couche précédente
		// on calcule ce dernier noeud:
		RESO_forward(z_net);
		z_net->neuron=_OUTPUT_TRANSFORM(z_net->neuron);
		RESO_forward(y_net);
		y_net->neuron=_OUTPUT_TRANSFORM(y_net->neuron);
		//
		if(z_net->epoch%10000==0)
		{
			printf("\n\nepoch[%6.6lu]\nZNET\t e(%+5.5f) , RMS{%8.8f}\n",z_net->epoch,z_net->error,RMScompute);
			printf("\t\t wtot[%+3.3f] --",z_net->wtot);	// au passage, le poids total des entrées a ce cycle
			for(i=0;i<z_net->synapses;i++) { printf("(w=%+5.5f / ward=%+7.7f]",z_net->w[i],z_net->ward[i]); }
			//
			// affiche snet valeurs
				printf("\nSNET\t e(%+5.5f) out=[%5.5f]\t",snet->error,snet->neuron);
				printf("wtot[%+3.3f] --",snet->wtot);	//
				for(i=0;i<snet->synapses;i++) { printf("(%+3.3f)",snet->w[i]); }
			// affiche tnet valeurs
				printf("\nTNET\t e(%+5.5f) out=[%5.5f]\t",tnet->error,tnet->neuron);
				printf("wtot[%+3.3f] --",tnet->wtot);	//
				for(i=0;i<tnet->synapses;i++) { printf("(%+3.3f)",tnet->w[i]); }
		}
		//  CALCUL : BACKWARD pour Z_NET ( pour obtenir l'erreur de la sortie n->neuron )
		//  expected contient la valeur attendue, fournie a la fonction
		RESO_backWARD(z_net,expected_z);
		RESO_backWARD(y_net,expected_y);
		//
		//
		RESO_prepare_CASCADE(snet);// remise a zéro du cumul d'erreur pour ce neurone
		RESO_prepare_CASCADE(tnet);// idem
		// on reporte LES ERREURS sur les reseaux précédents S,T , et ce ,
		// pour chaque liaison du neurone vers la couche suivante
		RESO_backWARD_CASCADE_ADDERROR(snet,y_net->ward[0]);// dans quelle 'proportion' snet a participé à l'erreur de znet,A
		RESO_backWARD_CASCADE_ADDERROR(snet,z_net->ward[0]);//idem pour znet, A
		// meme chose pour tnet
		RESO_backWARD_CASCADE_ADDERROR(tnet,y_net->ward[1]);// tnet, pour ynet entrée B
		RESO_backWARD_CASCADE_ADDERROR(tnet,z_net->ward[1]);//tnet pour znet B
		// cumul RMS pour controle convergence
		RMSerror+=(fabs(z_net->error)+fabs(y_net->error))/2.0; // basé sur une moyenne des erreurs RMS des 2 sorties
		RMScompute=(RMSerror/(float)z_net->epoch);
		// appliquer correction, inverse : derniere couche vers premiere
		RESO_apply(z_net,_LR_OUT);
		RESO_apply(y_net,_LR_OUT);
		//
		RESO_apply(snet,_LR_HIDDEN);//
		RESO_apply(tnet,_LR_HIDDEN);//
		// .. valeur entre 0/10.0 pour le prochain cycle
		ang=NN_frand_ab(0.0,10.0);
		//
	}
	// seuil RMS ok, affiche les infos finales poids/ part dans le résultat
	printf("\n\tZ_NET Final Parts  :");
	for(i=0;i<z_net->synapses;i++) { printf("(%+3.3f%c)",z_net->part[i]*100.0,'%'); }
	printf("\n\tZ_NET Final Weights:");
	for(i=0;i<z_net->synapses;i++) { printf("(%+3.3f )",z_net->w[i]);}
	printf("\n");
	printf("\n\tY_NET Final Parts  :");
	for(i=0;i<y_net->synapses;i++) { printf("(%+3.3f%c)",y_net->part[i]*100.0,'%'); }
	printf("\n\tY_NET Final Weights:");
	for(i=0;i<y_net->synapses;i++) { printf("(%+3.3f )",y_net->w[i]);}
	printf("\n");
//
	// infos snet
	printf("\n\tSNET Final Parts  :");
	for(i=0;i<snet->synapses;i++) { printf("(%+3.3f%c)",snet->part[i]*100.0,'%'); }
	printf("\n\tSNET Final Weights:");
	for(i=0;i<snet->synapses;i++) { printf("(%+3.3f )",snet->w[i]);}
	printf("\n");
	// infos tnet
	printf("\n\tTNET Final Parts  :");
	for(i=0;i<tnet->synapses;i++) { printf("(%+3.3f%c)",tnet->part[i]*100.0,'%'); }
	printf("\n\tTNET Final Weights:");
	for(i=0;i<tnet->synapses;i++) { printf("(%+3.3f )",tnet->w[i]);}
	printf("\n");
	// 'test run'
	z_net->internal=0;
	ang=0.00;
	while(z_net->internal<100)
	{
		//
		snet->in[0]=_INA; // diffuses constant 5.0
		snet->in[1]=_INB;
		snet->in[2]=ang;
		//
		tnet->in[0]=_INA;
		tnet->in[1]=_INB;
		tnet->in[2]=ang;
		//
		expected_z=_REPON_Z;
		expected_y=_REPON_Y;
		//	CALCUL : FORWARD
		RESO_forward(snet);
		RESO_forward(tnet);
		//
		snet->neuron=_OUTPUT_TRANSFORM(snet->neuron);
		tnet->neuron=_OUTPUT_TRANSFORM(tnet->neuron);
		//unet->neuron=tanh(unet->neuron);
		// on transmet les 3 sorties de snet,tnet,unet ver z_net, neuron de sorties, qui a 3 entrées :
		z_net->in[0]=snet->neuron;
		z_net->in[1]=tnet->neuron;
		z_net->in[2]=_INA;
		//
		y_net->in[0]=snet->neuron;
		y_net->in[1]=tnet->neuron;
		y_net->in[2]=_INA;
		// on calcule ce dernier noeud:
		RESO_forward(z_net);
		z_net->neuron=_OUTPUT_TRANSFORM(z_net->neuron);
		RESO_forward(y_net);
		y_net->neuron=_OUTPUT_TRANSFORM(y_net->neuron);
		// output version human readable
		#define _OUT_TEXT 	"cycle[%lu] B[%f] B[%f] A[%f] EXP_Z[%f] OUT_Z[%f] || EXP_Y[%f] OUT_Y[%f]\n"
		// ou pour export CSV :
		//#define _OUT_TEXT	"%lu,%f,%f,%f,%f,%f,%f,%f\n"
		printf(_OUT_TEXT,
			z_net->internal,
			_INC,
			_INB,
			_INA,
			_REPON_Z,
			z_net->neuron,
			_REPON_Y,
			y_net->neuron);
		//
		ang+=0.10;
	}
	//
	// todo : malloc releases
}

La sortie, en version TANH() * seuil atteint en 1100000 epochs

Le même , avec activation en mode ‘reLU’ , seuil atteint en 140000 epochs :

En descendant le seuil RMS a 0.01, et reLU en activation :

Dans cette situation , un résumé des parts de chaque élément :

Z_NET Final Parts  :(+45.641%)(+46.988%)(+7.372%)
Z_NET Final Weights:(+4.510 )(-4.643 )(-0.728 )

Y_NET Final Parts  :(+63.674%)(+6.939%)(+29.387%)
Y_NET Final Weights:(-5.336 )(-0.581 )(+2.463 )

SNET Final Parts  :(+74.550%)(+0.064%)(+25.386%)
SNET Final Weights:(-16.639 )(+0.014 )(+5.666 )

TNET Final Parts  :(+84.045%)(+0.552%)(+15.403%)
TNET Final Weights:(-8.278 )(-0.054 )(+1.517 )

SNET utilise A et C ( BIAS, et la variable ang ) : il détecte un des ’seuils’ ,celui a 3.0 ou celui a 6.0
TNET utilise A et C egalement : il detecte une des deux seuils , celui a 3.0 ou celui a 6.0
ZNET utilise majoritairement ses entrées A et B , qui sont les sorties de SNET et TNET, respectivement , pour générer la réponse attendue, ainsi que son entrée de BIAS (comparaison)
YNET, utilise majoritairement  son entrée A , qui est la sortie de SNET, et son BIAS , hors, comme on sait que ZNET et YNET , en sortie, doivent communément déclencher sur 3.0, on peut en conclure que SNET detecte le passage par 3.0 de la variable en ‘ang’ , puisqu’il est utilisé par YNET ET ZNET.

Pour finir :

Même si le reseau et les fonctions sont, pour l’instant, plutot ‘lourdes’, vous avez les bases pour constituer des réseau personnalisés beaucoup plus compacts, et j’ai volontairement découpé les fonctions et phases de l’apprentissage, afin de clarifier chacunes des étapes.
On va optimiser et rendre synthétiques les choses dans les prochains posts

314159265358979323846264338327950288
419716939937510582097494459230781640
628620899862803482534211706798214808

December 15, 2016

Neural nets, bases en C , part 6

Filed under: linux, neuron — Tags: , , , , — admin @ 6:08 pm

De l’importance du ‘training set’

Le reseau décrit précédement aboutit a une erreur RMS en sortie inférieure a 0.05 au bout d’approximativement  cycles (epochs) d’apprentissage (epoch = 7.27Millions)

On modifie la façon dont ‘ang’ est générée, dans la boucle d’apprentissage :

de :

  • ang=NN_frand_ab(0.0,10);
en :
  • ang=(float) ((int32_t)NN_frand_ab(0.0,10.0));

On relance le programme :

  • -> il atteint le seuil RMS voulu en seulement 180000 epochs
On trace le graphe de sa sortie :
Le réseau a généralisé : la simplification du nombre d’élements dans le train-set accélère l’apprentissage, mais entraine aussi le réseau à faire une approximation des cas intermédiaires, non présents dans le train-set.
Au niveau du seuil du 3.0 en valeur d’entrée par exemple, le réseau n’a eu que deux exemples de cas en apprentissage :
  • pour 3.0 en entrée, sortie attendue = -1 ( le test est : +1 si strictement supérieur a 3.0)
  • pour 4.0 en entrée, sortie attendue = +1
  • On constate que la sortie OUTPUT bascule de -1.0 vers +1.0 à la moitié de ces 2 valeurs, 3.5 ( la fonction tanh ‘arrondi’ les angles aux extrèmes -1.0/+1.0
Peut-on faire apprendre le reseau encore plus vite ?
Il est temps de parler de la fonction d’activation :
  • pour l’heure c’est tanh()
on va changer l’attendu en sortie du reseau , de :
  • #define _REPON ((ang>3.0)&&(ang<8.0)) +1.0:-1.0
en :
  • #define _REPON ((ang>3.0)&&(ang<8.0))? +1.0:0.0
On relance le programme sans autre modifications :
Le seuil est atteint en environ 70000 ( l’affichage est fait en %10000 .. )
C’est deux fois moins de temps pour descendre vers le seuil RMS voulu.
Son graphe en sortie :
314159265358979323846264338327950288
419716939937510582097494459230781640
628620899862803482534211706798214808

Neural nets, bases en C, part 5

Filed under: linux, neuron — Tags: , , , , — admin @ 8:58 am

..Ou l’on arrive à l’ajout nécéssaire d’une couche supplémentaire de neurones :

Si on garde le réseau (plutot le neurone daillleurs ) , à 3 entrées, on constatera que tout tentative de le faire converger vers une solution est impossible, lorsqu’on pose par exemple, un sortie attendue de la forme  :

#define _REPON	((net->in[1]>3.0)&&(net->in[1]<8.0))? +1.0:-1.0

Il va falloir créer un vrai réseau, en connectant entre eux les neurones :

J’ai gardé volontairement les 3 entrées pour chaque neurone même si elles ne sont pas nécéssaire pour résoudre le problème, mais pour un aspect pratique afin de modifier facilement les entrées du réseau constitué a fins de tests.

La littérature sur le sujet de la backpropagation étant largement diffusée et ‘mathéifiée’ , on reviendra plus tard sur une portion de la nouvelle fonction ‘backWARD_CASCADE()‘ qui sera, pour l’instant:

void RESO_backWARD_CASCADE(reso* n,float reverse_error)
{
	uint32_t k;
	float xb;
	//
	xb=tanh(1.0-(n->neuron*n->neuron));
	xb*=reverse_error;
	n->error=xb;	//
	for(k=0;k<n->synapses;k++)
	{
		n->part[k]=(fabs(n->w[k])/n->wtot);//
		n->ward[k]=n->error*n->w[k];//
		n->back[k]=n->error*n->in[k];//
	}
	n->epoch++;
}

A la différence de backWARD() , on n’introduit plus une valeur attendue , mais (l’erreur*poids)=ward[] du neurone situé après celui qui est en cours de traitement ( et pour l’instant, on en aura qu’1 seul , celui de z_net )

On peut tester la configuration à trois neurones, avec le programme suivant :

#include "../common/classics.h"
#include "../common/neuron/RESO_library.c"

int main(int argc, char **argv)
{
	// le reseau S
	reso stnet;
	reso* snet=&stnet;			// pointeur vers la structure
	//
	// le reseau T
	reso ttnet;
	reso* tnet=&ttnet;			// pointeur vers la structure
	//
	// le reseau Z
	reso ztnet;
	reso* z_net=&ztnet;			// pointeur vers la structure
	//
	//float s_expected,t_expected,u_expected;				// utile pour stocker ce qu'on attend en reponse
	float expected;
	float RMSerror,RMScompute;
	float ang;					// pour un angle, sinus/cosinus
	//
	uint32_t i;		// une variable d'utilité..
	//
	//
	RESO_init(snet,3);// on initialise le reseau S, avec 3 entrées
	RESO_init(tnet,3);// on initialise le reseau T, avec 3 entrées
	//RESO_init(unet,3);// on initialise le reseau U, avec 3 entrées
	//
	RESO_init(z_net,3);// on initialise le reseau Z, avec 3 entrées
	//
	// mise a 0.5,0.4,0.3 de tous les poids , pour S T et Z cf POST
	snet->w[0]=0.5;
	snet->w[1]=0.5;
	snet->w[2]=0.5;
	//
	tnet->w[0]=0.4;
	tnet->w[1]=0.4;
	tnet->w[2]=0.4;
	//
	//
	z_net->w[0]=0.3;
	z_net->w[1]=0.3;
	z_net->w[2]=0.3;
	//
	RMSerror=0.0;
	RMScompute=0.0;
	//
	// on définit ici pour les essais, les 3 entrées, la sortie attendue par formule
	//
	#define _INA	5.0 /* un bias */
	#define _INB 	0.0 /* inutilisé */
	#define _INC	ang /* la variable d'entrée */
	//
	#define _REPON	((ang>3.0)&&(ang<8.0))? +1.0:-1.0 /* expected formula */
	//
	// _________--------________
	// 0        3       8        10
	//
	ang=0.0;
	//
	while((RMScompute>0.05)||(z_net->epoch<200))// jusqu'a RMS erreur total < 0.01
	{
		//
		snet->in[0]=_INA; //
		snet->in[1]=_INB;
		snet->in[2]=_INC;

		tnet->in[0]=_INA;
		tnet->in[1]=_INB;
		tnet->in[2]=_INC;
		//
		//
		expected=_REPON;
		//
		//	CALCUL : FORWARD
		RESO_forward(snet);
		RESO_forward(tnet);
		//
		snet->neuron=tanh(snet->neuron);
		tnet->neuron=tanh(tnet->neuron);
		// on transmet les 2 sorties de snet,tnet ver z_net, neuron de sortie, qui a 3 entrées :
		z_net->in[0]=snet->neuron;
		z_net->in[1]=tnet->neuron;
		z_net->in[2]=_INA;// entrée BIAS pour ce neurone, est INA pour les 2 neurones de la couche précédente
		// on calcule ce dernier noeud:
		RESO_forward(z_net);
		z_net->neuron=tanh(z_net->neuron);
		//
		if(z_net->epoch%10000==0)
		{
			printf("\n\nepoch[%6.6lu]\nZNET\t e(%+5.5f) , RMS{%8.8f}\n",z_net->epoch,z_net->error,RMScompute);
			printf("\t\t wtot[%+3.3f] --",z_net->wtot);	// au passage, le poids total des entrées a ce cycle
			for(i=0;i<z_net->synapses;i++) { printf("(w=%+5.5f / ward=%+7.7f]",z_net->w[i],z_net->ward[i]); }
			//
			// affiche snet valeurs
				printf("\nSNET\t e(%+5.5f) out=[%5.5f]\t",snet->error,snet->neuron);
				printf("wtot[%+3.3f] --",snet->wtot);	//
				for(i=0;i<snet->synapses;i++) { printf("(%+3.3f)",snet->w[i]); }
			// affiche tnet valeurs
				printf("\nTNET\t e(%+5.5f) out=[%5.5f]\t",tnet->error,tnet->neuron);
				printf("wtot[%+3.3f] --",tnet->wtot);	//
				for(i=0;i<tnet->synapses;i++) { printf("(%+3.3f)",tnet->w[i]); }
		}
		//  CALCUL : BACKWARD pour Z_NET ( pour obtenir l'erreur de la sortie n->neuron )
		//  expected contient la valeur attendue, fournie a la fonction
		RESO_backWARD(z_net,expected);
		//
		// on reporte ** l'ERREUR ** sur sur les reseaux précédents S,T
		RESO_backWARD_CASCADE(snet,z_net->ward[0]);// <- on transmet ward[0] de z_net
		RESO_backWARD_CASCADE(tnet,z_net->ward[1]);// <- idem pour ward[1]
		// cumul RMS pour controle convergence
		RMSerror+=fabs(z_net->error);
		RMScompute=(RMSerror/(float)z_net->epoch);
		// appliquer correction, inverse : derniere couche vers premiere
		RESO_apply(z_net,0.001);
		//
		RESO_apply(snet,0.1);//
		RESO_apply(tnet,0.1);//
		// .. valeur entre 0/10.0 pour le prochain cycle
		ang=NN_frand_ab(0.0,10.0);
	}
	// seuil RMS ok, affiche les infos finales poids/ part dans le résultat
	printf("\n\tZ_NET Final Parts  :");
	for(i=0;i<z_net->synapses;i++) { printf("(%+3.3f%c)",z_net->part[i]*100.0,'%'); }
	printf("\n\tZ_NET Final Weights:");
	for(i=0;i<z_net->synapses;i++) { printf("(%+3.3f )",z_net->w[i]);}
	printf("\n");
	// infos snet
	printf("\n\tSNET Final Parts  :");
	for(i=0;i<snet->synapses;i++) { printf("(%+3.3f%c)",snet->part[i]*100.0,'%'); }
	printf("\n\tSNET Final Weights:");
	for(i=0;i<snet->synapses;i++) { printf("(%+3.3f )",snet->w[i]);}
	printf("\n");
	// infos tnet
	printf("\n\tTNET Final Parts  :");
	for(i=0;i<tnet->synapses;i++) { printf("(%+3.3f%c)",tnet->part[i]*100.0,'%'); }
	printf("\n\tTNET Final Weights:");
	for(i=0;i<tnet->synapses;i++) { printf("(%+3.3f )",tnet->w[i]);}
	printf("\n");
	// 'test run'
	z_net->internal=0;
	ang=0.00;
	while(z_net->internal<100)
	{
		//
		snet->in[0]=_INA; // diffuses constant 5.0
		snet->in[1]=_INB;
		snet->in[2]=ang;
		//
		tnet->in[0]=_INA;
		tnet->in[1]=_INB;
		tnet->in[2]=ang;
		//
		expected=_REPON;
		//	CALCUL : FORWARD
		RESO_forward(snet);
		RESO_forward(tnet);
		//
		snet->neuron=tanh(snet->neuron);
		tnet->neuron=tanh(tnet->neuron);
		//unet->neuron=tanh(unet->neuron);
		// on transmet les 3 sorties de snet,tnet,unet ver z_net, neuron de sorties, qui a 3 entrées :
		z_net->in[0]=snet->neuron;
		z_net->in[1]=tnet->neuron;
		z_net->in[2]=_INA;
		// on calcule ce dernier noeud:
		RESO_forward(z_net);
		z_net->neuron=tanh(z_net->neuron);
		// output version human readable
		#define _OUT_TEXT 	"cycle[%lu] A[%f] B[%f] C[%f] EXP[%f] OUT[%f]\n"
		// ou pour export CSV :
		// #define _OUT_TEXT	"%lu,%f,%f,%f,%f,%f\n"
		printf(_OUT_TEXT,
			z_net->internal,
			_INA,
			_INB,
			_INC,
			_REPON,
			z_net->neuron);
		//
		ang+=0.10;
	}
	//
	// todo : malloc releases
}

L’apprentissage converge vers une solution, et on obtient en résultat (sur ma machine):

Z_NET Final Parts  :(+48.571%)(+42.593%)(+8.837%)
Z_NET Final Weights:(+2.709 )(+2.375 )(-0.493 )

SNET Final Parts  :(+37.337%)(+0.258%)(+62.405%)
SNET Final Weights:(-72.378 )(+0.500 )(+120.971 )

TNET Final Parts  :(+61.343%)(+0.139%)(+38.517%)
TNET Final Weights:(+176.048 )(+0.400 )(-110.540 )

Comme on peut le constater, l’entrée ‘B’ de snet, et tnet, qui est a 0.0 (cf. programme) , est quasiment complètement rejetée ( 0.258% et 0.139% de parts, respectivement ), ou , plus précisement, son poids n’a pas été changé, hors comparé aux poids des autres entrées, leur parts sont devenues minimes dans la résultat final.

  • snet et tnet ont utilisé les entrées A (bias) et C (la variable) en majorité
  • znet, à utilisé les 3 entrées : A qui est la sortie de snet, B qui est la sortie de tnet et C qui est son entrée BIAS, la même que A pour snet et tnet.
Plusieurs remarques s’imposent :
  • Les poids de départ ne sont plus fixés à la même valeur. La situation de départ du réseau constitué (les valeurs des poids) influe sur la rapidité de la convergence, et peut aboutir des echecs ( cf. littérature sur le sujet )
  • Les learning rates : faire des essais , cycles de comparaisons avec même situation (poids) de départ VS différents learning rates sur z_net et snet/tnet
Avant d’aborder une partie d’explication sur la fonction backWARD_CASCADE() , un graphique complet des entrées/sorties du réseau :

Annexe, détail de fonctions utiles (ou pas)

314159265358979323846264338327950288
419716939937510582097494459230781640
628620899862803482534211706798214808

December 14, 2016

Neural nets,bases en C part 4, la fonction TANH(y)

Filed under: linux, neuron — Tags: , , , , , — admin @ 10:12 am

Ou l’on arrive a la fonction TANH, nécéssaire dans le cas ou on s’attend à une sortie plus proche du ‘tout’ ou ‘rien’ logique que des valeurs linéaires précédentes :

Le programme de test suivant fait apprendre au réseau le déclenchement de la sortie à +1.0 si l’entrée 2 (_INB) est supérieure à +5.0, et -1.0 si elle est inférieure (ou égale) a +5.0 :

On ne change pas la structure du réseau, 3 entrées,une sortie. L’entrée inA est ‘inutilisée’, fixée à 0.01, inB est fixée entre 0.0 et +10.0 par un tirage aléatoire a chaque cycle, et inC est fixée a 2.5 fixe, servant d’entrée BIAS pour le réseau :

#include "../common/classics.h"
#include "../common/neuron/RESO_library.c"

int main(int argc, char **argv)
{
	// le reseau
	reso snet;
	reso* net=&snet;			// pointeur vers la structure
	//
	float expected;				// utile pour stocker ce qu'on attend en reponse
	float RMSerror,RMScompute;
	//
	uint32_t i;		// des variables d'utilité..
	//
	//
	RESO_init(net,3);// on initialise le reseau, avec 3 entrées
	//
	net->w[0]=1.0;
	net->w[1]=1.0;
	net->w[2]=1.0;
	//
	RMSerror=0.0;
	RMScompute=1.0;
	// on définit ici pour les essais, les 3 entrées, la sortie attendue par formule
	#define _INA	0.01
	#define _INB	NN_frand_ab(0.0,10.0)
	#define _INC	2.5
	//
	#define _REPON	(net->in[1]>5.0)? 1.0:-1.0
	//
	while((RMScompute>0.05)||(net->epoch<20))// jusqu'a RMS erreur total < 0.01
	{
		//
		net->in[0]=_INA;
		net->in[1]=_INB;
		net->in[2]=_INC;
		//
		expected=_REPON;
		//
		//	CALCUL : FORWARD
		//
		RESO_forward(net);
		// application tanh() a la sortie
		net->neuron=tanh(net->neuron);
		//
		//  CALCUL : BACKWARD ( pour obtenir l'erreur de la sortie n->neuron )
		//  expected contient la valeur attendue, fournie a la fonction
		RESO_backWARD(net,expected);
		// au passage, on fait un cumul d'erreur RMS pour superviser l'apprentissage du reseau
		RMSerror+=fabs(net->error);
		RMScompute=(RMSerror/(float)net->epoch);
		// epoque de l'apprentissage, erreur immédiate dela sortie, et erreur RMS total depuis le début
		printf("\nepoch[%6.6lu] e(%+6.6f) , RMS{%5.5f} ",net->epoch,net->error,RMScompute);
		printf("wtot[%+3.3f] --",net->wtot);	// au passage, le poids total des entrées a ce cycle
		// les 'n' entrées (ici 3) , leur part en % dans le résultat que fournit la sortie
		for(i=0;i<net->synapses;i++) { printf("(%+3.3f%c)",net->part[i]*100.0,'%'); }
		//
		RESO_apply(net,0.05);
	}
	// seuil RMS ok, affiche les infos finales poids/ part dans le résultat
	printf("\nFinal Parts :\n");
	for(i=0;i<net->synapses;i++) { printf("(%+3.3f%c)",net->part[i]*100.0,'%'); }
	printf("\nFinal Weights:\n");
	for(i=0;i<net->synapses;i++) { printf("(%+3.3f )",net->w[i]);}
	printf("\n");
	// on execute un cycle de calcul FORWARD uniquement, pour vérifier que
	// tout est OK :
	// 'test run'
	net->internal=0;
	while(net->internal<80)
	{
		net->in[0]=_INA;
		net->in[1]=_INB;
		net->in[2]=_INC;
		expected=_REPON;				// uniquement pour comparer l'attendu a la sortie du reseau
		RESO_forward(net);				// forward uniquement
		net->neuron=tanh(net->neuron);	// application tanh()
		printf("\nCycle[%6.6lu] EXPECTED(%+6.6f) , ACTUAL{%5.5f} ",net->internal,expected,net->neuron);
	}
	//
}

.. on utilise les fonctions :

  • RESO_init()
  • RESO_forward()
  • RESO_backWARD()
  • RESO_apply()
  • une fonction NN_frand_ab(min,max)
  • et la structure reso

Telles que : ( je les ai listées a nouveau ici avec les modifications des précédents posts intégrées )

float NN_frand_ab(float a, float b)
{
    return (float) (( rand()/(float)RAND_MAX ) * (b-a) + a);
}
//
typedef struct {
	uint32_t synapses;
	float* in;			// forward compute inputs
	float* back;		// backward computed inputs
	float* ward;
	float* w;			// forward/backward weight for each inputs
	float* part;
	float neuron;			// forward computed output, backward compute input
	float error;
	float wtot;			// total input weights
	//
	uint64_t epoch;
	uint64_t internal;
} reso;
void RESO_init(reso* n,uint32_t input_count)
{
	uint32_t k;
	float init_w=1.0;
	n->in=(float*) malloc(sizeof(float)*input_count);
	n->back=(float*) malloc(sizeof(float)*input_count);
	n->ward=(float*) malloc(sizeof(float)*input_count);
	n->w=(float*) malloc(sizeof(float)*input_count);
	n->part=(float*) malloc(sizeof(float)*input_count);
	//
	n->synapses=input_count;
	n->wtot=init_w*input_count;	//prepare startup total weights
	for(k=0;k<n->synapses;k++)
	{
		n->in[k]=0.0;
		n->w[k]=init_w;	// default, @1
		n->part[k]=n->w[k]/n->wtot;		// backed ready
	}
	n->neuron=0.0;
	n->error=0.0;
	//
	n->epoch=1;
	n->internal=1;

}
void RESO_forward(reso* n)
{
	uint32_t i;
	n->neuron=0.0;
	n->wtot=0.000001;// divide by zero ?
	for(i=0;i<n->synapses;i++)
	{
		n->neuron+=n->in[i]*n->w[i];
		n->wtot+=fabs(n->w[i]);//
	}
	n->internal++;
}
void RESO_backWARD(reso* n,float expected)
{
	uint32_t k;
	n->error=n->neuron-expected;	// actual - expected
	//
	for(k=0;k<n->synapses;k++)
	{
		n->part[k]=(fabs(n->w[k])/n->wtot);								// assume w always positive
		n->ward[k]=n->error*n->w[k];// erreur * poids
		n->back[k]=n->error*n->in[k];// erreur * input val
	}
	n->epoch++;
}
void RESO_apply(reso* n,float rate)
{
	uint32_t k;
	for(k=0;k<n->synapses;k++)
	{
		// commentée (voir plus tard ) : if(fabs(n->w[k])<0.001) n->w[k]=-n->w[k];
		n->w[k]-=n->back[k]*rate;//
	}
}

En suivant, un graphe de la sortie ‘neuron’, en fonction de l’entrée inB ( 0-10 ), et zoom (4.
125 à 6.0 )  - neuron et error utilisent l’axe Y gauche, ward et back, l’axe droite

..Avec le seuil RMS fixé a 0.05, on voit que le déclenchement de la sortie , passant de -1.0 a +1.0, se fait un peu avant 5 en entrée.

Si on descend le seuil RMS a 0.01 avant de terminer l’apprentissage, on obtient cette fois :

remarque : ne pas se fier a l’overshoot du neuron qui passe ‘au dela’ de 1.0 malgré la fonction tanh() . les courbes sont ‘lissées’ (spline) par le soft qui génère les graphes.

Sans smooth, et dans le range 4.9 - 5.1 , par pas de 0.025 :

La fonction tanh() sur la sortie permet de passer relativement vite d’un état a l’autre (-1.0 / +1.0 ), par une transition douce.

On peut essayer de modifer la partie du code :

commenté : //  net->neuron=tanh(net->neuron);
remplacé par : net->neuron=(net->neuron>0.0)? 1.0:-1.0;

Dans ces conditions, le réseau converge 2 fois plus vite , et son graphe devient :

.. radical..

On peut aussi essayer avec :

if(net->neuron>1.0) net->neuron=1.0;
if(net->neuron<-1.0) net->neuron=-1.0;

On obtient :

314159265358979323846264338327950288
419716939937510582097494459230781640
628620899862803482534211706798214808
Older Posts »

cat{ } { post_1009 } { } 2009-2015 EIhIS Powered by WordPress