Creierul uman model de calculatoare? Rețele neuronale cu propagarea înapoi a eroriiLucian Brâncovean Încă de la începutul cercetărilor în domeniul inteligenței artificiale s-au conturat două direcții principale: modelul logico-simbolic și modelul conexionist. O nouă tendință în acest domeniu este reprezentată de algoritmii genetici și de calculul evolutiv.
IntroducereModelul logico-simbolic se bazează pe reprezentarea cunoștințelor unui sistem în mod simbolic și pe utilizarea unor metode logice în scopul deducerii altora noi. Exemple bine cunoscute în acest sens sunt programele de șah și sistemele expert. Modelul conexionist se bazează pe o structură asemănătoare creierului uman alcătuită din elemente interconectate analog neuronilor, creând conceptul de rețele neuronale artificiale. În acest caz informația memorată în sistem nu este reprezentată în mod clar, simbolic, ci ea se află distribuită în mod difuz în toate elementele rețelei. Marele avantaj al acestui model este capacitatea sa de a învăța din exemple și de a aplica regulile învățate pe cazuri noi. În mod normal, pentru rezolvarea unei clase de probleme cu ajutorul calculatorului se utilizează o metodă clară, un algoritm alcătuit din etape și decizii bine definite, executate într-o anumită succesiune. Unele clase de probleme necesită însă algoritmi de o asemenea complexitate încât construirea lor se poate dovedi o sarcină deosebit de dificilă, uneori imposibilă. În cazurile în care aceste probleme sunt totuși abordabile prin metode obișnuite, ele cer mari resurse de timp și putere de calcul. Metoda neconvențională a calculului neuronal ar putea face aceste cazuri nu doar abordabile, ci uneori deosebit de simple în raport cu algoritmii complicați existenți și folosiți în mod curent. Etapa de construcție a modelului și a algoritmului corespunzător problemei se reduce la antrenarea rețelei pe o mulțime considerabilă de exemple. Domeniul rețelelor neuronale este însă un domeniu vast. Primul model de neuron a apărut în 1943 din colaborarea unui matematician cu un neurofiziolog. De atunci, au apărut mai multe moduri de aplicare a paradigmei conexioniste ca: perceptronul multistrat, mașina Boltzmann, rețele asociative, rețele neuro-fuzzy, propagarea înapoi a erorii etc. Aceasta din urmă - cunoscută și sub numele de regula delta generalizată - este în acest moment cea mai importantă și mai utilizată metodă pentru instruirea rețelelor neuronale. De studiul ei ne vom ocupa în continuare. În general unele categorii specifice de probleme sunt potrivite pentru această abordare.
Exemple: - recunoașterea modelelor în imagini; - recunoașterea caracterelor; - recunoașterea vorbirii; - în general, clasificarea seturilor de date de intrare.
Ideea - asemănarea cu creierulIdeea inițială a pornit de la asemănarea cu sistemul nervos uman. Acesta este alcătuit dintr-o multitudine de celule specializate, numite neuroni, interconectate, unele având rol de culegere a informației, altele de transmitere a comenzilor către alte mecanisme, iar majoritatea cu scopul de a prelucra și memora informația, într-un mod ce ne rămâne încă puțin cunoscut. Toate aceste celule comunică prin impulsuri nervoase transmise prin conexiunile lor. Memorarea informației pare a fi realizată prin modificarea conductibilității acestor căi, în sensul creșterii ei în momentul trecerii repetate a unor semnale pe același traseu.
Structura neuronului
Elementul principal al unei rețele este neuronul. El se constituie ca o unitate de prelucrare a informației în formă numerică, având numeroase intrări și o singură ieșire, a cărei valoare este însă transmisă în mai multe direcții, spre alți neuroni. Un neuron este descris de o funcție de activare care ne indică valoarea ieșirii, ca funcție de o valoare ce reprezintă suma intrărilor sale. Această funcție este aceeași pentru toți neuronii din rețea și poate avea diferite forme. O primă formă des întâlnită este funcția prag ce are doar două valori posibile, de obicei 0 și 1, după cum argumentul este sau nu, mai mare decât o anumită valoare fixă, numită prag. O altă variantă este funcția identică, ce are ca ieșire chiar valoarea de intrare (f(x)=x). Se mai întâlnește și o combinație a acestora de forma
Însă, în cazul de față cea mai productivă se dovedește a fi o funcție de forma .
Straturile și ponderileNeuronii din rețea sunt grupați în straturi. Orice neuron este conectat la intrare cu ieșirile tuturor neuronilor din stratul precedent, iar ieșirea sa cu intrările tuturor neuronilor din stratul următor. Într-un același strat neuronii nu sunt conectați între ei. În acest mod propagarea informației prin rețea se face unidirecțional, dinspre intrări spre ieșiri. Există două straturi cu destinație specială: cel de intrare - primul în ordinea arătată mai sus - și cel de ieșire - ultimul în acest caz. Numărul neuronilor de intrare este de obicei numărul de date ale problemei, fiecare primind la intrare unul dintre acești parametri. Numărul neuronilor de ieșire este egal cu numărul claselor în care pot fi separate datele de intrare. Reprezentantul clasei în care este încadrat setul de date prezentat la intrare va avea ieșirea 1, iar ceilalți 0. Conexiunile dintre straturi sunt caracterizate fiecare de o pondere, un număr real cu care se înmulțește ieșirea unui neuron înainte de a fi prezentată la intrarea următorului neuron. Din aceste ponderi constă puterea întregii rețele - prin ajustarea lor se realizează memorarea informației.
Modul de funcționareSă urmărim acum o etapă de funcționare. Rețelei i se prezintă un set de date de intrare. Fiecare dintre valori este trimisă la intrarea unuia din neuronii din stratul de intrare. Aceștia nu fac efectiv nici o prelucrare, valorile sunt trimise mai departe spre următorul strat, prin conexiuni, ajustându-se cu ponderile respective. Fiecare neuron din stratul ascuns efectuează următorul calcul: se adună toate intrările sale; rezultatului i se aplică funcția de activare a neuronului, funcția sigmoidală în cazul acesta; valoarea rezultată este trimisă mai departe prin ponderi la neuronii stratului următor. Această operațiune se repetă pentru toate straturile rămase, valorile rezultate la ieșirea ultimilor neuroni fiind chiar ieșirile rețelei. Vă veți întreba, și pe bună dreptate, ce determină acest mecanism să producă rezultatele așteptate. Răspunsul este: antrenamentul. Să vedem cum se antrenează o astfel de rețea. La început i se prezintă rețelei un set de date și se calculează ieșirile corespunzătoare. Diferența dintre rezultatul obținut și cel dorit reprezintă eroarea. Această diferență se determină pentru fiecare neuron final și apoi se trimite înapoi prin rețea sub forma unui semnal de eroare. Acesta se calculează cu formula SE=DS(ACT)*(IR - ID), unde IR este ieșirea realizată prin calcul, ID este ieșirea dorită, ACT este activarea acestui neuron în calculul ieșirii, DS(x) este derivata funcției de activare a neuronului. În continuare, semnalul de eroare se propagă înapoi prin rețea în mod asemănător cu informația, dar în sens invers. Pentru un neuron dintr-un strat ascuns SE este suma semnalelor de eroare ale neuronilor din stratul următor, fiecare înmulțită cu ponderea conexiunii prin care se transmite înapoi această valoare. În final, ponderea fiecărei conexiuni se ajustează cu valoarea AJ=CI*SE*S(ACT), unde CI este rata de instruire, SE este semnalul de eroare ACT este activarea și S(x) este funcția de activare a neuronului. În acest fel întreaga rețea va fi modificată pentru a produce un răspuns mai apropiat de cel dorit. Procedeul se repetă pentru toate seturile de date prezente ca exemple pentru problema în cauză. Eroarea apărută la fiecare set este memorată și se calculează o eroare medie. Întregul proces este repetat până când eroarea medie atinge o valoare destul de scăzută.
ImplementareaProblema rezolvată de programul prezentat aici este relativ simplă. Programul trebuie să determine în care parte față de prima bisectoare se găsește un punct. Intrarea este alcătuită din coordonatele carteziene ale punctului, iar ieșirea dintr-o valoare reală - 0 pentru o parte, 1 pentru cealaltă. Datorită modului de funcționare a rețelei ieșirea va avea valori intermediare, apropierea lor de 1 sau 0 fiind și în funcție de eroarea maximă acceptată la antrenarea rețelei. Datele de intrare sunt formate din 6 puncte aflate de o parte și de alta a primei bisectoare, având coordonate întregi din mulțimea {1,2,3}. Problema poate părea prea simplă, dar este potrivită pentru testarea rețelei în condițiile în care interpretarea se face foarte ușor, iar timpii de antrenament sunt reduși. Se poate observa ușor influența parametrului CI (rata de instruire) și a erorii maxime asupra timpului de antrenament. Se poate experimenta cu diferite numere de neuroni în stratul ascuns.
Listing: NEURONI.CPP #include <stdio.h> #include <conio.h> #include <math.h> #include <stdlib.h> #include <values.h> #define NI 2 // nr. neuroni de intrare #define NA 2 // nr. neuroni ascunsi #define NF 1 // nr. neuroni de iesire (finali) #define CI (0.3) // constanta rata de instruire #define NS 4 // nr. seturi de date #define EMAX 0.005 // eroarea maxima acceptabila float pa[NI][NA]; // ponderile intre NI si NA float pb[NA][NF]; // ponderile intre NA si NF float intra[NI][NS]= {{1,2,1,2},{1,1,2,2}}; // vectorii de intrare float ies[NF][NS]= {{1,1,0,0}}; // vectorii de iesire float error; // eroarea medie globala float h[NA]; // activarile stratului ascuns float i[NA]; // iesirile stratului ascuns float y[NF]; // activarile stratului de iesire float o[NF]; // iesirile retelei float sef[NF]; // semnal de eroare pt stratul de iesire float sea[NA]; // semnal de eroare pt stratul ascuns float errloc[NS]; // eroarea locala a fiecarui set de date int k,ja,jb; // al catelea set de date unsigned long int tmp;// al catelea antrenament float s(float in) { return (1/(1+exp(-in))); // functie sigmoidala de activare a neuronului } float ds(float in) // derivata functiei sigmoidale { return (exp(in)/((exp(in)+1)*(exp(in)+1))); } void InitNet(void) { randomize(); int i,j; for(i=0;i<NI;i++) for(j=0;j<NA;j++) pa[i][j]=( (rand() % 30) + 1.0 )/100; // valori mici,diferite de zero for(i=0;i<NA;i++) for(j=0;j<NF;j++) pb[i][j]=( (rand() % 30) + 1.0 )/100; } void PrintNet(void) { int i,j; for(i=0;i<NI;i++) { for(j=0;j<NA;j++) printf( "%f | ",pa[i][j] ); printf("\n"); } printf("\n"); for(i=0;i<NA;i++) { for(j=0;j<NF;j++) printf( "%f | ",pb[i][j] ); printf("\n"); } printf("\n"); } void ActivNeuroniAscunsi(void) { for (ja=0;ja<NA;ja++) // pt fiecare neuron ascuns { h[ja]=0; for (jb=0;jb<NI;jb++) // se calculeaza activarea { h[ja]+=(intra[jb][k])*(pa[jb][ja]); } i[ja]=s(h[ja]); // se calcueaza iesirea } }
void ActivNeuroniIesire(void) { for (ja=0;ja<NF;ja++) // pt fiecare neuron de iesire { y[ja]=0; for (jb=0;jb<NA;jb++) // se calculeaza activarea { y[ja]+=(i[jb])*(pb[jb][ja]); } o[ja]=s(y[ja]); // se calcueaza iesirea } }
void SemnalEroareIesire(void) { for (ja=0;ja<NF;ja++)// pt fiec. neuron final sef[ja] = ds( y[ja] ) * (ies[ja][k] - o[ja]); }
void SemnalEroareAscuns(void) { for (ja=0;ja<NA;ja++) // pt fiecare neuron ascuns { sea[ja]=0; for (jb=0;jb<NF;jb++) sea[ja]=sef[jb]*pb[ja][jb]; sea[ja]*=ds(h[ja]); } }
void ActualPonderiIesire(void) { for (ja=0;ja<NA;ja++) // pt fiecare pondere din stratul b for (jb=0;jb<NF;jb++) // se actualizeaza conformsemnalului de eroare pb[ja][jb]+=CI*sef[jb]*s(h[ja]); }
void ActualPonderiAscuns(void) { for (ja=0;ja<NI;ja++) // pt fiecare pondere din stratul a se // actualizeaza conform semnalului de eroare for (jb=0;jb<NA;jb++) pa[ja][jb]+=CI*sea[jb]*s(intra[ja][k]); }
void CalculEroare(void) { errloc[k]=0; // eroarea pt setul de date #k pt fiecare // neuron final semisuma patratelor for (ja=0;ja<NF;ja++) errloc[k]+=pow((ies[ja][k]-o[ja]),2); errloc[k]/=2; error+=errloc[k]; // eroarea pt acest ciclu } void TrainNet(void) { tmp=0; do //un ciclu de antrenament { error=0; for(k=0;k<NS;k++) // pentru fiecare vector de date { ActivNeuroniAscunsi(); // calculeaza activarea si iesirea // stratului de neuroni ascunsi ActivNeuroniIesire(); // calculeaza activarea si iesirea // stratului de neuroni finali SemnalEroareIesire(); // se calculeaza semnalul de eroare SemnalEroareAscuns(); ActualPonderiIesire(); // se actualizeaza ponderile cu ActualPonderiAscuns(); // semnalul de eroare CalculEroare(); // se calculeaza eroarea } printf("%lu error : %.10f\n",++tmp,error); } while (EMAX <= error); //pana cand eroarea este destul de mica } void TryNet(void) //utilizarea retelei { printf(" Trying Net ... \n"); intra[0][0]=2; // vectorii de intrare intra[1][0]=1; // pentru testele finale intra[0][1]=1; intra[1][1]=2; intra[0][2]=1; intra[1][2]=1; intra[0][3]=2; intra[1][3]=2; for (k=0;k<NS;k++) { ActivNeuroniAscunsi(); // calculeaza activarea si iesirea // stratului de neuroni ascunsi ActivNeuroniIesire(); // calculeaza activarea si iesirea // stratului de neuroni finali for (ja=0;ja<NF;ja++) // pt fiecare neuron de iesire printf("setul %d iesirea %d=%f \n",k,ja,o[ja]); }; } void main(void) { clrscr(); InitNet(); PrintNet(); getch(); TrainNet(); TryNet(); PrintNet(); getch(); }
Programul folosește o rețea cu doi neuroni de intrare (ascunși), și unul de ieșire. Se afișează ponderile inițiale create aleator, se așteaptă apăsarea unei taste, apoi se începe antrenarea rețelei. Eroarea la fiecare pas este afișată pe ecran. Acest proces poate dura o vreme, în funcție de calculatorul folosit. La sfârșit se afișează ponderile rețelei și se testează rețeaua cu o serie de puncte.
Posibile îmbunătățiriProgramul suportă, evident, numeroase îmbunătățiri. Totuși, el este prezentat în această formă în scopuri didactice. O primă idee ar fi modificarea numărului de straturi ascunse. Pentru rezolvarea unor probleme dificile este uneori necesar ca rețeaua să conțină două sau chiar mai multe straturi ascunse. Procedeul de actualizare a ponderilor corespunzătoare acestora este analog cu cel folosit pentru unicul strat ascuns din program iar implementarea acestuia nu ridică absolut nici o problemă. În cazul de față, datorită problemei alese, timpii de antrenament sunt reduși și nu constituie o problemă, la fel seturile de date. Pentru probleme grele și structuri ale rețelei mai complexe - mulți neuroni pe strat, mai multe straturi - timpul ar putea deveni o problemă. În acest caz apare nevoia de a salva/încărca informațiile din rețea (ponderile). În antrenarea rețelei pot apărea situații de minim local, când eroarea nu este minimul realizabil, dar orice modificare în configurația ponderilor duce la o eroare mai mare. Aici orice modificare a rețelei va face ca modificarea următoare să fie în direcția opusă, revenindu-se la situația inițială. Pentru evitarea acestui gen de situații se poate efectua o modificare a tehnicii de antrenament bazată pe ideea de a ține cont la ajustarea unei ponderi de ajustarea precedentă aplicată aceleiași ponderi. În acest fel se evită cazurile în care rețeaua se "învârte" în jurul aceluiași punct.
AplicațiiAplicabilitatea rețelelor neuronale este deosebit de largă datorită proprietăților specifice acestora - pot fi aproximator universal și au capacitatea de a învăța din exemple. Domeniile în care ar putea fi folosite sunt diverse, de la inginerie, informatică, medicină, economie, până la domenii umaniste ca sociologie, lingvistică sau psihologie. Aplicații practice, aflate deja în studiu sunt: recunoașterea formelor plane sau chiar tridimensionale, aici apărând o proprietate interesantă - invarianța la rotație; recunoașterea scrisului, a amprentelor digitale și a vorbirii; recunoașterea unor situații patologice în funcționarea inimii sau a creierului uman, prin analizarea semnalelor EKG respectiv EEG; interpretarea automată a imaginilor sonarului și teledetecția; predicția macroeconomică și a piețelor financiare; eliminarea zgomotului și a interferențelor în telecomunicații.
BibliografieD. Dumitrescu, Hariton Costin - Rețele neuronale, teorie și aplicații.
Lucian Brâncovean este elev în clasa a XI-a la Colegiul "Gheorghe Lazăr" din Sibiu. Poate fi contactat prin intermediul redacției [cuprins] |