Neural nets, bases en C, part 5
..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.
- 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
Annexe, détail de fonctions utiles (ou pas)