EihiS

December 11, 2016

Neural nets, deep learning, bases en C - part 2

Filed under: linux, neuron — Tags: , , , , , , — admin @ 9:32 am

Si vous avez testé différentes combinaisons d’entrées / sortie attendue, vous aurez remarqué que certaines ne convergent pas vers une solution.

Pour ceux qui débuteraient, c’est ici qu’il faut aborder la notion de BIAS , tout d’abord.

Si on pose les conditions suivantes en entrée du réseau :

#define _INA	0.0 // était 3.0 sur la version d'origine
#define _INB	0.0+sin(ang) // inchangé
#define _INC	0.0+cos(ang) // inchangé
//
#define _REPON	1.0+2.0*cos(ang)+3*sin(ang) // inchangé

Si on execute le code, on voit que le reseau ne converge pas et ’stagne’ avec une erreur RMS de l’ordre 1.011,
après des époques dépassant les 1000000.

La modification de l’entrée A du reseau , passant de 3.0 a 0.0 , interdit au reseau de pouvoir générer la premiere partie de l’equation de sortie attendue, qui est une constant.

En effet, puisque la sortie (la valeur du neurone) est egale a : (inA*wA)+(inB*wB)+(inC*wC) , on peut voir que, quels  que soient wA,wB, ou wC (les poids) , il faut AU MOINS une des entrées qui ait une valeur autre que ZERO si l’on souhaite générer une sortie qui ait un ‘offset’ (valuer constante) non nul.

En mettant inA à zero, on a donc supprimé une entrée indispensable à n’importe quel reseau de ce type, qu’on appelle l’entrée de ‘BIAS’.

Cette entrée est la même qu’une entrée normale, mais sa valeur est fixée au démarrage de l’apprentissage. l’apprentissage (fonction BACKWARD + APPLY )  viendra modifier le poids de cette entrée, de manière à utiliser la valeur de ce BIAS comme nécéssaire.

On peut vérifier la chose en posant de nouveau ces conditions :

#define _INA	1.0
#define _INB	0.0+sin(ang)
#define _INC	0.0+cos(ang)
//
#define _REPON	1.0+2.0*cos(ang)+3*sin(ang)

le reseau converge de nouveau, avec au final les poids suivants :

Final Parts :
(+16.667%)(+50.000%)(+33.333%)
Final Weights:
(+1.000 )(+3.000 )(+2.000 )

la partie constant de la formule de sortie attendue est générée grace à inA*wA = (+1.0*+1.0) = +1.0

Mais  on peut aussi proposer en entrée :

#define _INA	-1.0
#define _INB	0.0+sin(ang)
#define _INC	0.0+cos(ang)
//
#define _REPON	1.0+2.0*cos(ang)+3*sin(ang)

On éxecute, s’attendant à ce que le poids final wA soit -1.0, de facon à obtenir le premier terme de la formule attendue en sortie ( qui est +1.0 =constante ) , avec inA*wA = (-1.0 * -1.0) = +1.0

Hors le réseau ne converge pas.
La répartition des poids montre que wA a été réduit à zero, et reste dans cet etat, interdisant au réseau de converger.

C’est là qu’il est temps de jeter un oeil sur la fonction :

void RESO_apply(reso* n,float rate)
{
	uint32_t k;
	for(k=0;k<n->synapses;k++)
	{
		n->w[k]+=n->back[k]*rate;//
	}
}

Dans celle-ci, on multiplie le learning rate par la valeur de ‘back[] ‘, qui est la valeur de l’entrée calculée pour le réseau en le parcourant à l’envers (sortie vers entrées)

hors back[ ] est définie dans la fonction RESO_back() par le calcul suivant :

n->back[k]=n->neuron*n->part[k]*n->in[k];
avec :
n->part[k]=(fabs(n->w[k])/n->wtot);
et:
n->error=expected - n->neuron;
n->neuron=n->error;

On réécrit la fonction executée pour chaque entrée :
Remplacant neuron par sa valeur, qui est expected - neuron , c’est a dire l’erreur :

n->back[k]=n->neuron*n->part[k]*n->in[k];

peut s’écrire : back[k] = (expected-neuron) * ( FABS( w[k] ) / wtot ) * in [k] ;

le ‘problème‘ vient du terme w[k] / wtot

Dans le cas d’un poids qui évolue vers une valeur positive croissante (avec w initial positif), tout ce passe bien.
Mais dans le cas d’une évolution décroissante, on va rapidement stagner, avec un terme w[k]/wtot qui va ressembler de plus en plus à zero (tendant vers zero pour etre précis) ,  divisé par wtot ( qui au passage ne doit pas etre egal a zero sous peine d’exception ‘DIVIDE BY ZERO’ du programme) , ce qui donne une valeur tendant vers zéro.

Hors ce terme w[k]/wtot est multiplié par in[k] et ‘error’
L’ensemble va donc tendre vers ZERO, et , de ce fait, plus le poids va s’approcher de zéro, moins il sera modifié par la fonction ‘APPLY’.

Dans le cas de l’exemple donné, on a inA=-1.0 , hors le terme constant de la formule attendue en sortie est +1.0+(…)

Le poids de inA devrait donc être -1.0, pour obtenir cette valeur constante :

inA * wA =  (-1.0* -1.0) = 1.0

Donc, puisque le poids wA au départ vaut +1.0 , il faut qu’il décroisse vers -1.0 : c’est ce qui se produit lors de l’apprentissage, et,à l’approche de 0.0, l’apprentissage se verrouille puisque le poids sera de moins en moins modifié, plus il tendra vers ZERO.
Le cap du ‘ZERO’ semble etre un point de blocage infranchissable avec la méthode actuelle de calcul et la réciproque ( poids initial négatif, et croissant vers le positif du fait de l’apprentissage), et aussi valable.
On va pour l’heure modifier la fonction, pour passer ce CAP , de la facon suivante :

 void RESO_apply(reso* n,float rate)
{
	uint32_t k;
	for(k=0;k<n->synapses;k++)
	{
		n->w[k]+=n->back[k]*rate;// inchangé
		if(fabs(n->w[k])<0.001) { n->w[k]=-n->w[k]; } // switch : évite le ZERO fatidique...
	}
}

On relance avec les même valeurs d’entrée que précédement, et miracle, le réseau converge, avec au final :

Final Parts :
(+25.000%)(+75.000%)(+50.000%)
Final Weights:
(-1.000 )(+3.000 )(+2.000 )

On essaye une modification sur une autre entrée par exemple inA, en inversant sa valuer de sin() mais pas la formule attendue en sortie :

#define _INA	-1.0
#define _INB	0.0-sin(ang) // inversée->  -sin(ang)
#define _INC	0.0+cos(ang)
//
#define _REPON	1.0+2.0*cos(ang)+3*sin(ang) // terme +3.0*sin(ang) ..

Le reseau converge, avec comme valeurs finales (ainsi que les 20 essais de vérification ) :

Final Parts :
(+16.667%)(+50.000%)(+33.333%)
Final Weights:
(-1.000 )(-3.000 )(+2.000 )

Cycle[000001] EXPECTED(-2.269685) , ACTUAL{-2.26969}
Cycle[000002] EXPECTED(+2.999692) , ACTUAL{2.99969}
Cycle[000003] EXPECTED(+0.855610) , ACTUAL{0.85561}
Cycle[000004] EXPECTED(-0.753175) , ACTUAL{-0.75318}
Cycle[000005] EXPECTED(+4.137586) , ACTUAL{4.13759}
Cycle[000006] EXPECTED(-2.603627) , ACTUAL{-2.60363}
Cycle[000007] EXPECTED(+4.014887) , ACTUAL{4.01489}
Cycle[000008] EXPECTED(-0.543690) , ACTUAL{-0.54369}
Cycle[000009] EXPECTED(+0.620656) , ACTUAL{0.62066}
Cycle[000010] EXPECTED(+3.191344) , ACTUAL{3.19134}
Cycle[000011] EXPECTED(-2.361939) , ACTUAL{-2.36194}
Cycle[000012] EXPECTED(+4.548495) , ACTUAL{4.54849}
Cycle[000013] EXPECTED(-1.696408) , ACTUAL{-1.69641}
Cycle[000014] EXPECTED(+2.055083) , ACTUAL{2.05508}
Cycle[000015] EXPECTED(+1.895064) , ACTUAL{1.89506}
Cycle[000016] EXPECTED(-1.583227) , ACTUAL{-1.58323}
Cycle[000017] EXPECTED(+4.515279) , ACTUAL{4.51528}
Cycle[000018] EXPECTED(-2.418411) , ACTUAL{-2.41841}
Cycle[000019] EXPECTED(+3.320974) , ACTUAL{3.32097}
Cycle[000020] EXPECTED(+0.455810) , ACTUAL{0.45581}
314159265358979323846264338327950288
419716939937510582097494459230781640
628620899862803482534211706798214808

December 10, 2016

Neural nets , deep learning, bases en C - part 1

Filed under: linux, neuron — Tags: , , , , , , — admin @ 7:15 pm

Les reseaux composés de plus d’une couche cachée sont plus difficiles que les autres à entrainer.
On trouve ça-et-la des articles sur le sujet , bourrés de maths, qui ont de quoi rebuter.

Les arXiv publiés sont nombreux..

Pour un réseau type “perceptron” à une couche ( N entrées vers une sortie unique) , partons sur la base suivante ( code C )
Attention: j’utilise des fonctions ‘maison’ qui peuvent différer des formes ‘canoniques’ si tant est qu’elles existent.
Je suis un codeur plus qu’un matheux. les programmes originaux sont souvent ‘mathéifiés’ par la suite, et non l’inverse.

Le calcul du reseau inverse, justement, est , si l’on pousse la reflection sur son mode de calcul, analogue aux phénomènes de mouvements oculaires rapides,et mouvements inconscients pendant les phases de sommeil, lors desquelles il est supposé (ou prouvé?) que le cerveau ‘renforce’ les liens neuronaux en ‘répétant’ des stimulis de la periode d’eveil. les vagues EEG mettent en evidence des mouvements de va-et-vient de l’avant a l’arrière du cortex des signaux electriques dans les reseaux de neurones cérébraux.

Les fonctions décrites ci-après agissent sur cette structure simple, qui décrit le reseau (désolé pour les ‘mix’ entre anglais,français. mes codes sont tjs commentés en anglais, j’ai étoffé en français pour cet post )  :

//
typedef struct {
	uint32_t synapses;// nombre d'entrées du neurone
	float* in;	// forward compute inputs
	float* back;	// backward computed inputs
	float* w;	// forward/backward weight for each inputs
	float* part;    // proportion rapportée a 1.0 pour chaque poids entrant
	float neuron;	// forward computed output, backward compute input
	float error;    // erreur en sortie
	float wtot;			// total input weights
	//
	uint64_t epoch;	// incrémenté a chaque function d'apprentissage appliquée (modification des poids)
	uint64_t internal;// incrémenté a chaque calcul de la sortie en fonction des entrées
} reso;

Une fonction qui initialise cette structure :

void RESO_init(reso* n,uint32_t input_count)
{
	uint32_t k;
	float init_w=1.0;// todo: use setup, default var instead
	n->in=(float*) malloc(sizeof(float)*input_count);
	n->back=(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...
		n->part[k]=n->w[k]/n->wtot;// back ready
	}
	n->neuron=0.0;
        n->error=0.0;
	//
	n->epoch=1;
	n->internal=1;
}

Maintenant, la fonction de calcul ‘FORWARD’ : Calcule la sortie en fonction des entrées

void RESO_forward(reso* n)
{
	uint32_t i;
	n->neuron=0.0;
	n->wtot=0.0;
	for(i=0;i<n->synapses;i++)
	{
	  n->neuron+=n->in[i]*n->w[i]; // somme des entrées*poids de cette entrée
	  n->wtot+=fabs(n->w[i]);      // cumule au passage les poids pour calculer le total des poids entrants
	}
	n->internal++;
}

On passe a deux fonctions essentielles dans l’apprentissage:

la fonction ‘BACKWARD’ qui effectue un calcul inverse (de la sortie vers les entrées), mais ne modifie aucun des poids du réseau :

//
void RESO_back(reso* n,float expected)
{
	uint32_t k;
	n->error=expected - n->neuron;	// AKA 'expected - what_i_got'
	//
	n->neuron=n->error;// prepare for back compute
	//
	for(k=0;k<n->synapses;k++)
	{
		n->part[k]=fabs(n->w[k])/n->wtot;	 // weight part always positive
		n->back[k]=n->neuron*n->part[k]*n->in[k];// back[] is the backward equivalent of in[]'s for the net
	}
	n->epoch++;
}

.. je reviendrai plus tard sur celle ci ainsi que sur la suivante : la fonction de modification du reseau ,qui s’execute après le calcul inverse (fonction précédente) :

void RESO_apply(reso* n,float rate)
{
	uint32_t k;
	for(k=0;k<n->synapses;k++)
	{
		n->w[k]+=n->back[k]*rate;
	}
}

En pratique, on peut tester tout ça avec un programme simple :

#include "../common/classics.h" // check for a post on the website about it
// ..les librairies classiques requises
#include "../common/neuron/RESO_library.c"
// c'est le nom que j'utilise
// pour le fichier ou sont les fonctions précédentes et la definition de la structure
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;
	float ang;					// pour un angle, sinus/cosinus cf while()

	//
	uint32_t i;		// des variables d'utilité..
	//
	//
	RESO_init(net,3);// on initialise le reseau, avec 3 entrées
	// tous les poids a 1.0 pour tester
	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,et la sortie attendue par formule
	#define _INA	3.0
	#define _INB	0.0+sin(ang)
	#define _INC	0.0+cos(ang)
	// on pond une formule qui utilise les 3 entrées, pour tester le reseau
	#define _REPON	1.0+2.0*cos(ang)+3.0*sin(ang)
	//
	ang=0.0;
	while((RMScompute>0.01)||(net->epoch<2))// jusqu'a RMS erreur total <= 0.01
	{
		//
		net->in[0]=_INA;
		net->in[1]=_INB;
		net->in[2]=_INC;
		//
		expected=_REPON;
		ang+=2.6;// incrém. pour le prochain cycle du while, 2.6 est choisi comme ça..
		// *** CALCUL : FORWARD
		RESO_forward(net);
		// *** CALCUL : BACKWARD ( pour obtenir l'erreur de la sortie n->neuron )
		//  expected contient la valeur attendue, fournie a la fonction
		RESO_back(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,'%'); }
		// final : on modifie le reseau, et rebouclage while()
		// on fournit la 'learning_rate' a la fonction ( vitesse d'apprentissage ) ,
		// c'est a dire en quelle quantité elle va venir modifier les poids.
		// le learning rate agit en dosage sur la modification.
		RESO_apply(net,0.05); // *** APPLIQUER modification aux poids ***

	}
	// si le seuil RMS est 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 , 20 fois :
	// 'test run'
	net->internal=0;
	while(net->internal<20)
	{
		net->in[0]=_INA;
		net->in[1]=_INB;
		net->in[2]=_INC;
		expected=_REPON;// uniquement pour afficher l'attendu VS la sortie du reseau
		ang+=2.6;//
		RESO_forward(net);
		printf("\nCycle[%6.6lu] EXPECTED(%+6.6f) , ACTUAL{%5.5f} ",net->internal,expected,net->neuron);

	}
// TODO : malloc cleanups !
 return 0;// :)
}

Le reseau, dans son état actuel,  converge , et c’est le but.

On constate que les parts de chacune des entrées est exactement proportionnées tel que nécéssaire , a savoir :

Final Parts :
(+6.250%)(+56.250%)(+37.500%)
Final Weights:
(+0.333 )(+3.000 )(+2.000 )
A) On a in[0]=3.0 , poids=0.33333 ce qui fait  3.0*0.33333333 = 1.0
B) On a in[1]=sin(angle)  , poids = 3.0 ce qui fait 3.0*sin(angle)
C) On a in[2]=cos(angle) , poids = 2.0  ce qui fait 2.0*cos(angle)
La valeur du neurone etant la somme des entrées * leur poids respectifs,
on a donc neuron = A+B+C = 1.0 + 3.0*sin(angle) + 2.0*cos(angle)
.. c’est ce qu’on voulait, ça tombe bien… (expected : 1.0+2.0*cos(ang)+3.0*sin(ang) )

Suite au prochain numéro. ( si vous avez VRAIMENT besoin d’aide, ou des remarques constructives, contact : admin(AT)eihis.com)

314159265358979323846264338327950288
419716939937510582097494459230781640
628620899862803482534211706798214808
« Newer Posts

cat{ 185 } { post_958 } { } 2009-2015 EIhIS Powered by WordPress