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