programowanie w C++ dla OWK Sztuczne neurony - programowanie w C++ Opracowanie: dr hab. Mirosław R. Dudek, prof. UZ 1
Streszczenie Dyskusja o stworzeniu sztucznego mózgu zawsze powoduje duże emocje. Pierwszy model sztucznego neuronu stworzony został w 1943 przez W. McCullocha i W. Pitsa. Dużo nadziei na szybkie zagospodarowanie dziedziny sztucznej inteligencji stworzyła koncepcja perceptronu stworzona przez Franka Rosenblatta. Jednak po napisaniu w 1969 roku przez Minsky ego i Paperta książki w której pokazali oni ograniczenia modelu perceptronu zainteresowanie zbudowaniem sztucznego mózgu spadło prawie do zera. Przyczyny tego stanu rzeczy były również ekonomiczne ponieważ po opublikowaniu książki Minsky ego i Paperta przestano finansować granty badawcze dotyczące sieci neuronowych. Dopiero w latach 80. dwudziestego wieku nastąpił renesans tematyki kiedy okazało się że jednak warstwowy perceptron może wykonywać pewne operacje logiczne zanegowane przez Minsky ego i Paperta pod warunkiem właściwego trenowania takiej sieci neuronowej. W naszym krótkim opracowaniu nawiążemy do jednego z modeli rekurencyjnych sieci neuronowych lub inaczej sieci ze sprzężeniem zwrotnym, mianowicie jednowarstwowej sieci neuronowej Hopfielda wprowadzonej w 1982 roku. Model ten ma korzenie w modelu fizycznym szkła spinowego i istotnie wpłynął na postęp w dziedzinie sztucznej inteligencji. Dużą zaletą sieci Hopfielda jest jej prostota. W szczególności po nauczeniu takiej sieci wzorców podanie na jej wejście zniekształconego obrazu wzorca powoduje zrekonstruowanie takiego obrazu do jednego z z nauczonych wzorców. To zagadnienie będzie celem materiałów pomocniczych. Kod programu w języku C++ oparty jest na prostym przykładzie Pawła Górnickiego, Sieci neuronowe, Wiedza i Życie, kwiecień 1995. Model Hopfielda Załóżmy że mamy siatkę kwadratową L L w której węzły oznaczają neurony. Na rysunku poniżej mamy 16 neuronów i w tym przypadku L = 4. Zakładamy że każdy neuron i = 0, 1, 2, 3, 15 może być w dwóch stanach s = 1 - stan aktywny i s = 1 stan nieaktywny. Stan każdego neuronu zależy od stanów innych neuronów z którymi jest on połączony. Takie połączena pomiędzy neuronami nazywać będziemy synapsami. Synapsy mogą osłabiać aktywność neuronu lub pobudzać ją. W naszym modelu siłę tego pobudzania lub hamowania pomiędzy neuronem i i neuronem j oznaczać będziemy wagą w ij. W modelu Hopfielda wagi te są symetryczne tj. w ij = w ji. Rysunek 1: Siatka N N neuronów (N = 4) które połączone są synapsami każdy z każdym. Dla przejrzystości rysunku narysowane są tylko niektóre połączenia pomiędzy neuronami. Umówimy się że stan s i neuronu i = 0, 1, 2,..., N zależy od wartości parametru S i będącego sumą iloczynów wag synaps łączących i ty neuron z neuronami j i, gdzie j = 0, 1,..., N, tj. 2
S i = N j=0,j i w ij s j. (1) Jeśli wartość S i > 0 to neuron i przełącza się do stanu aktywnego s i = 1. Jeśli był aktywny to pozostaje w tym stanie. W przeciwnym razie neuron staje się nieaktywny (s i = 1) lub pozostaje nieaktywny jeśli wcześniej był nieaktywny. W działaniu sztucznej sieci neuronowej wyróżniamy dwa etapy. Etap pierwszy to uczenie się wg reguły Hebba. Jest to etap przygotowawczy do rozpoznawania sygnałów wejściowych podawanych na neurony. Etap drugi to rozpoznawanie obrazów podawanych na wejście również w przypadku gdy są trochę zmienione. W kodzie programu zamieszczonego poniżej Każdy neuron jest obiektem z klasy neuron i mamy N N neuronów zapamiętamych w kontenerze typu vector. 1 Ponieważ kontenery są liniowe tj. obiekty w nich ułożone są liniowo jeden obiekt za drugim to przyjęliśmy indeksowanie obiektów tak jak na rysunku 1, tzn. jest indeks n = 0, 1, 2,..., N 2 1. Wtedy element (i, j) sieci kwadratowej, gdzie i = 0, 1,..., N 1, j = 0, 1,..., N 1 można wyliczyć za pomocą indeksu n z następujących równań: i = n/n (2) j = n%n (3) W przypadku programu na sieć neuronową, zamieszczonym poniżej, kontener do umieszczania w nim obiektów z klasy neuron nosi nazwę Neur. Podobnie, w tablicy jednowymiarowej zapamiętane są synapsy, które są obiektami z klasy synapsa. Synapsy indeksowane są indeksem n = 0, 1, 2,..., N 4 1, który uwzględnia wszystkie możliwe połączenia pomiędzy neuronami (i, j) z (k, l) gdzie i, j, k, l = 0, 1,..., N 1. W programie poniżej, etap I to nauka wzorców, tutaj obrazów podanych w pliku tekstowym w postaci gwiazdek. Nauka polega na tym że początkowo dopóki obrazy nie są wczytane w kontenerze Synap obiekty reprezentujące synapsy wartości synaps są wyzerowane. Po wczytaniu obrazu każdy piksel obrazu odpowiada jednemu neuronowi i w przypadku połaczenia dwóch pikseli zapalonych(bit 1) lub zgaszonych(bit 0) wartość synapsy zwiększa się o wartość 1. W przeciwnym przypadku zmniejsza się o 1. Po wczytaniu paru obrazów wartości synaps (wag) łączących rózne neurony będą miały wartości zarówno dodatnie i ujemne i będą miały pamięć o wszystkich wczytanych obrazach. Etap II to rozpoznawanie zniekształconego obrazu który podany jest na wejście neuronów przy pomocy wyuczonych wag synaptycznych. Kod programu podany jest z objasnieniami które powinny ułatwić zrozumienie jego działania. Polecenie kompilacji: g++ -o neurony.exe neurony.cc 1 Kontenery tego typu zostały omówione w materiale dla OWK (Otwarte Warsztaty Komputerowe) pt. Programowanie w C++ z użyciem kontenerów - parę prostych przykładów programów i są dostępne poprzez adres http://owk.if.uz.zgora.pl/tutorial/programowanie-w-c-z-u-yciem-kontener-w-par-przyk-ad-w-program-w jako plik pdf lab2 cz4.pdf. 3
Format polecenia:./neurony.exe PlikWEObrazek1 PlikWEObrazek2 PlikWE3 gdzie w tym przykładzie synapsy uczone są na podstawie tylko dwóch obrazków w pliku PlikWE- Obrazek1 i PlikWEObrazek2. Natomiast zniekształcony obraz to PlikWE3. Do tego materiału załączone są przykładowe pliki tekstowe z obrazkami. Są to: obraz1.txt, obraz2.txt i obraz3.txt a odpowiednie polecenie wykonania progamu ma postać:./neurony.exe obraz1.txt obraz2.txt obraz3.txt Po uruchomieniu otrzymamy wydrukowane na ekranie kolejne etapy rozpoznania rysunku w pliku obraz3.txt tj. przyporządkowanie go do obraz1.txt lub obraz2.txt. Wczytywane obrazki są narysowane w trybie tekstowym. 4
Rysunek 2: Pierwszy fragment wydruku na ekranie komputera. 5
Rysunek 3: Drugi fragment wydruku na ekranie komputera. Rozpoznanie zniekształconego obrazka nastapiło w kroku piątym. 6
Teraz przedstawimy kod programu neurony.cc. 1 // j e s t to wersja programu otwarta do m o d y f i k a c j i 2 // napisana jako m a t e r i a l pomocniczy d l a grup projektowych OWK. 3 // Tutaj wszystko j e s t w jednym p l i k u ( t j. c z e s c uczenia s i e s i e c i neuronowej 4 // i c z e s c rozpoznawania obrazu ). 5 //Program ma za zadanie pokazac t y l k o czy s i e c neuronowa d z i a l a 6 7 #include <iostream> 8 #include <s t r i n g > 9 #include <vector > 10 #include <c s t d l i b > 11 #include <fstream> 12 13 using namespace std ; 14 15 16 17 class neuron { 18 private : 19 int stan ; // aktywny : stan =1; nieaktywny : stan= 1 20 int sygnal ; 21 double x, y ; 22 double progsygnalu ; 23 public : 24 void WpiszStan ( int s ){ stan=s ; } 25 void WpiszPolozenia ( double xx, double yy ){ x=xx ; y=yy ; } 26 int PodajStan ( ) { return stan ; } 27 // double PodajX (){ return x ; } 28 // double PodajY (){ return y ; } 29 30 } ; 31 32 33 class synapsa { 34 private : 35 int s i l a b o n d ; 36 public : 37 void ResetSynapsy ( ) { s i l a b o n d =0;} 38 void WprowadzWartoscSynapsy ( int s ){ s i l a b o n d+=s ; } 39 int PodajWartoscSynapsy ( ) { return s i l a b o n d ; } 40 } ; 41 42 43 typedef std : : vector <neuron > Komorki ; 44 Komorki Neur ; 45 typedef std : : vector <synapsa > Polaczenia ; 46 Polaczenia Synap ; 47 48 49 void AlokujNeurony ( Komorki &Neur, int LLx, int LLy){ 50 int L=LLx LLy ; 51 52 for ( int k=0; k<l ; k++) Neur. push back (new neuron ) ; 53 } 54 55 void AlokujSynapsy ( P o l a c z e n i a &Synap, int LLx, int LLy){ 7
56 int LL=LLx LLy ; 57 58 for ( int k=0; k<ll ; k++) Synap. push back (new synapsa ) ; 59 } 60 61 void ResetujNeurony ( Komorki &Neur ){ 62 cout<<"reset neuronow... "<<endl ; 63 int L=Neur. s i z e ( ) ; 64 for ( int k=0;k<l ; k++) Neur [ k] >WpiszStan ( 1); 65 } 66 67 void ResetujSynapsy ( P o l a c z e nia &Synap ){ 68 cout<<"reset synaps... "<<endl ; 69 70 int L=Synap. s i z e ( ) ; 71 for ( int k=0;k<l ; k++) Synap [ k] >ResetSynapsy ( ) ; 72 } 73 74 void WczytajParametryObrazka ( char nazwapliku, int &Lx, int &Ly){ 75 fstream WE; 76 77 WE. open ( nazwapliku, i o s : : in ) ; 78 i f (WE. good()== f a l s e ){ 79 cout<<" Nie moge otworzyc pliku "<<nazwapliku<<e n d l ; 80 cout<<" Wychodze do systemu :("<<e n d l ; 81 e x i t ( 1 ) ; 82 } 83 84 s t r i n g l i n i a ; 85 int dlug ; 86 Lx=0,Ly=0; // w a r t o s c i poczatkowe 87 // k t o r e zostana w y l i c z o n e z p l i k u z obrazkiem 88 89 while (1){ 90 g e t l i n e (WE, l i n i a ) ; 91 i f (WE. e o f ( ) ) break ; 92 93 dlug=l i n i a. s i z e ( ) ; 94 i f (Lx<dlug )Lx=dlug ; // l x przyjmuje wartosc maksymalna 95 // parametru d l u g 96 Ly++; // numeruje k o l e j n e l i n i e az do w a r t o s c i maksymalnej 97 } 98 99 100 WE. c l o s e ( ) ; 101 } 102 103 void WczytajObrazek ( Komorki &Neur, char nazwapliku ){ 104 fstream WE; 105 106 WE. open ( nazwapliku, i o s : : in ) ; 107 i f (WE. good()== f a l s e ){ 108 cout<<" Nie moge otworzyc pliku "<<nazwapliku<<e n d l ; 109 cout<<" Wychodze do systemu :("<<e n d l ; 110 e x i t ( 1 ) ; 111 } 8
112 113 s t r i n g l i n i a ; 114 int dlug ; 115 int LICZNIK=0; 116 char znak ; 117 118 while (1){ 119 g e t l i n e (WE, l i n i a ) ; 120 i f (WE. e o f ( ) ) break ; 121 122 dlug=l i n i a. s i z e ( ) ; 123 for ( int k=0;k<dlug ; k++){ 124 znak=l i n i a [ k ] ; 125 i f ( znak== * ){ 126 Neur [ LICZNIK] >WpiszStan ( 1 ) ; 127 } 128 129 LICZNIK++; 130 131 } 132 } 133 134 WE. c l o s e ( ) ; 135 136 } 137 138 void PokazStanyNeuronow ( Komorki &Neur, int Lx, int Ly){ 139 int j ; 140 int L=Neur. s i z e ( ) ; 141 142 for ( int k=0;k<l ; k++){ 143 j=k%lx ; 144 i f ( Neur [ k] >PodajStan ()==1) cout<<"+" ; 145 else cout<<" " ; 146 i f ( j==lx 1) cout<<endl ; 147 } 148 149 } 150 151 void WczytajSynapsy ( Komorki &Neur, Polaczenia &Synap, int Lx, int Ly){ 152 int polacz =0; 153 int LL=Lx Ly ; 154 int i j ; 155 156 for ( int i =0; i<ll ; i ++){ 157 for ( int j =0; j<ll ; j ++){ 158 i f ( i==j ) continue ; 159 i j=i LL+j ; 160 i f ( Neur [ i ] >PodajStan()== Neur [ j ] >PodajStan ( ) ) 161 Synap [ i j ] >WprowadzWartoscSynapsy ( 1 ) ; 162 else Synap [ i j ] >WprowadzWartoscSynapsy ( 1); 163 } 164 } 165 } 166 167 9
168 void WczytajDowolnyObrazek ( Komorki &Neur, char nazwapliku, int Lx, int Ly){ 169 fstream WE; 170 171 WE. open ( nazwapliku, i o s : : in ) ; 172 i f (WE. good()== f a l s e ){ 173 cout<<" Nie moge otworzyc pliku "<<nazwapliku<<e n d l ; 174 cout<<" Wychodze do systemu :("<<e n d l ; 175 e x i t ( 1 ) ; 176 } 177 178 s t r i n g l i n i a ; 179 int dlug ; 180 int LICZNIK=0; 181 char znak ; 182 int n u m e r l i n i i =0; 183 184 while (1){ 185 g e t l i n e (WE, l i n i a ) ; 186 i f (WE. e o f ( ) ) break ; 187 188 dlug=l i n i a. s i z e ( ) ; 189 190 i f ( dlug>lx){ 191 cout<<" Wczytany obrazek jest obciety w linii "<<Lx Ly/LICZNIK<<e n d l ; 192 } 193 194 for ( int k=0;k<dlug ; k++){ 195 i f ( k>=lx) continue ; // o b c i e c i e h o r y z o n t a l n e 196 znak=l i n i a [ k ] ; 197 i f ( znak== * ){ 198 Neur [ LICZNIK] >WpiszStan ( 1 ) ; 199 } 200 201 LICZNIK++; 202 203 } 204 n u m e r l i n i i ++; 205 i f ( n u m e r l i n i i >=Ly) break ; // o b c i e c i e w p i o n i e 206 } 207 208 WE. c l o s e ( ) ; 209 210 } 211 212 213 int Rozpoznaj ( Komorki &Neur, P o laczenia &Synap, int Lx, int Ly){ 214 215 int LL=Lx Ly ; 216 int i j ; 217 int i r a n d ; 218 int i t e r u j =0; 219 int s i l a ; // p o t e n c j a l aktywujacy neuron 220 221 for ( int i =0; i<ll ; i ++){ 222 s i l a =0; // zerujemy d l a kazdego neuronu i 223 i r a n d =(int ) (LL ( rand ()/(1.0+RAND MAX) ) ) ; 10
224 225 for ( int j =0; j<ll ; j ++){ // wyliczamy s i l e od p o z o s t a l y c h neuronow 226 i f ( i r a n d==j ) continue ; 227 i j=i r a n d LL+j ; 228 s i l a+=neur [ j ] >PodajStan ( ) Synap [ i j ] >PodajWartoscSynapsy ( ) ; 229 } 230 i f ( s i l a <0) { 231 i f ( Neur [ i r a n d ] >PodajStan ()==1) i t e r u j =1; 232 Neur [ i r a n d ] >WpiszStan ( 1); 233 } 234 else { 235 i f ( Neur [ i r a n d ] >PodajStan()== 1) i t e r u j =1; 236 Neur [ i r a n d ] >WpiszStan ( 1 ) ; 237 } 238 } 239 240 241 242 return i t e r u j ; 243 } 244 245 246 int main ( int argc, char argv ){ 247 i f ( argc!= 4){ 248 cout<<" Poprawny format polecenia: "<<e n d l ; 249 cout<<argv [0] <<" PlikWEObrazek1 PlikWEObrazek2 PlikWE3"<<e n d l ; 250 return 0 ; 251 } 252 253 / 254 ETAP I : NAUKA WZORCOW 255 / 256 cout<<endl<<endl<<" Siec neuronowa uczy sie wzorcow..."<<e n d l ; 257 258 int Lx, Ly ; // rozmiar s i a t k i kwadratowej Lx x Ly wezlow 259 int Lx temp, Ly temp ; 260 261 // p i e r w szy obrazek 262 WczytajParametryObrazka ( argv [ 1 ], Lx, Ly ) ; 263 Lx temp=lx ; Ly temp=ly ; 264 265 // d r u g i obrazek 266 WczytajParametryObrazka ( argv [ 2 ], Lx, Ly ) ; 267 i f ( Lx temp!=lx Ly temp!=ly){ 268 cout<<" Rozne rozmiary siatki pikseli"<<e n d l ; 269 cout<<" Wychodze do systemu..."<<e n d l ; 270 e x i t ( 1 ) ; 271 } 272 273 274 cout<<" Odczytane wartosci Lx="<<Lx<<"\t"<<" Ly="<<Ly<<e n d l ; 275 // alokujemy kontener Neur z neuronami 276 AlokujNeurony ( Neur, Lx, Ly ) ; // s i a t k a LxLy neuronow w kontenerze ; 277 278 // alokujemy synapsy 279 AlokujSynapsy ( Synap, Lx Ly, Lx Ly ) ; // kontener z ( LxLy )ˆ2 synapsami 11
280 281 282 283 // Reset neuronow i synaps przed wczytaniem 1 obrazka 284 ResetujNeurony ( Neur ) ; 285 ResetujSynapsy ( Synap ) ; 286 287 WczytajObrazek ( Neur, argv [ 1 ] ) ; // wczytanie 1 obrazka 288 WczytajSynapsy ( Neur, Synap, Lx, Ly ) ; 289 cout<<"obrazek 1:"<<endl ; 290 PokazStanyNeuronow ( Neur, Lx, Ly ) ; 291 292 ResetujNeurony ( Neur ) ; // resetujemy neurony przed wczytaniem 2 obrazka 293 WczytajObrazek ( Neur, argv [ 2 ] ) ; // wczytanie 2 obrazka 294 WczytajSynapsy ( Neur, Synap, Lx, Ly ) ; // u z u p e l n i e n i e synaps drugim obrazkiem 295 cout<<"obrazek 2:"<<endl ; 296 PokazStanyNeuronow ( Neur, Lx, Ly ) ; 297 298 / 299 ETAP I I : Czytanie sygnalu 300 / 301 //uwaga c a l a pamiec obrazkow j e s t w synapsach 302 //Wczytujemy t e r a z j a k i s inny obrazek k t o r y t r o c h e r o z n i s i e od jednego z wzorcow 303 304 ResetujNeurony ( Neur ) ; // resetujemy neurony przed wczytaniem obrazka 305 WczytajDowolnyObrazek ( Neur, argv [ 3 ], Lx, Ly ) ; // wczytujemy obraz3 306 cout<<" Obrazek przeznaczony do rozpoznania:"<<e n d l ; 307 PokazStanyNeuronow ( Neur, Lx, Ly ) ; 308 / 309 ETAPU I I I : Rozpoznawanie sygnalu 310 / 311 int i t e r u j =1; 312 int krok =1; 313 while ( i t e r u j ){ 314 i t e r u j=rozpoznaj ( Neur, Synap, Lx, Ly ) ; 315 316 cout<<"--------------------------------------------"<<endl ; 317 cout<<" Rozpoznanie - krok "<<krok<<e n d l ; 318 319 PokazStanyNeuronow ( Neur, Lx, Ly ) ; 320 krok++; 321 } 322 323 324 return 0 ; 325 } 12