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)