Rozpoznawanie pisma ręcznego przy pomocy sieci neuronowej

Transkrypt

Rozpoznawanie pisma ręcznego przy pomocy sieci neuronowej
Rozpoznawanie pisma ręcznego przy pomocy sieci
neuronowej Kohonena
Automatyczne rozpoznawanie pisma ręcznego jest problemem bardzo trudnym i od wielu lat
nie do końca jeszcze rozwiązanym. Złożoność tego zagadnienia potęguje przede wszystkim
fakt, iż dany znak może być napisany różnymi charakterami pisma, więc litery za każdym
razem wyglądają nieco inaczej. Łączy je jednak pewne podobieństwo do znaku określanego
jako wzorzec. W niniejszym artykule przedstawię próbę rozwiązania tego problemu przy
pomocy sieci neuronowej Kohonena.
Sieć Kohonena
W 1982 roku fiński uczony Teuvo Kohonen zaproponował sieć neuronową liniową, w której
występuje czynnik konkurencji, a uczenie odbywa się bez nadzoru. Rysunek 1 przedstawia
schemat budowy omawianej sieci.
Rysunek 1. Topologia sieci Kohonena.
Informacja, którą chcemy poddać przetwarzaniu, podawana jest na wejście sieci. Następnie
sygnały te ulegają przemnożeniu przez współczynniki wagowe - inne dla każdego neuronu.
Na wyjściu otrzymujemy zestaw neuronów, których pobudzenie uzależnione jest od stopnia
zgodności sygnału wejściowego z wewnętrznym wzorcem sieci. Sygnał wyjściowy danego
neuronu jest więc tym silniejszy, im bardziej jego zestaw wartości wagowych przypomina
wartości sygnału wejściowego. W sieci Kohonena liczy się tylko zwycięski neuron, czyli ten
o największej wartości wyjściowej. Jego uznaje się za reprezentanta danej klasy zjawiska (np.
5 neuron rozpoznaje literę A).
Cały proces uczenia sieci polega na takim doborze wartości wag neuronów, aby sieć, jako
całość, prawidłowo realizowała postawione przed nią zadania. Na początku uczenia wszystkie
wagi inicjalizowane są w sposób losowy. Następnie na wejścia sieci podawane są sygnały
reprezentujące daną klasę badanego zjawiska, po czym sprawdza się, który neuron ma
największe pobudzenie. Neuron ten uznaje się za zwycięzcę i jego, wraz z neuronami
należącymi do jego sąsiedztwa, poddaje się procesowi uczenia, czyli modyfikuje się ich
zestawy wartości wagowych. Najsilniej zmieniane są wagi zwycięskiego neuronu, natomiast
wagi neuronów sąsiednich zmieniane są słabiej. Rozmiar sąsiedztwa określa projektant sieci.
Nie może być to wartość zbyt duża, a w szczególnym przypadku może przyjmować wartość
zerową. Warto w tym miejscu zauważyć, że nawet słaba początkowa skłonność jakiegoś
neuronu do rozpoznawania danej klasy obiektów może zostać wychwycona i wzmocniona
podczas procesu uczenia. Zatem wagi zwycięskiego neuronu aktualizuje się w taki sposób,
aby reagował on na przedstawioną mu prezentację jeszcze silniej. Umacnia on w ten sposób
swoją zwycięską pozycję. Proces ten zachodzi do momentu, aż kolejne prezentacje nie
powodują zmian wartości wagowych w znaczący sposób. Uznaje się wtedy, że neuron
osiągnął punkt stabilny. Wagi te modyfikuje się w taki sposób, aby po każdej kolejnej
prezentacji zestaw wartości wagowych lekko przesunąć w kierunku zestawu wartości
wejściowych. Efekt ten można uzyskać poprzez odjęcie od wektora wejściowego wektora
wag, a następnie część tej różnicy dodać do aktualnego wektora wag. Definicję tą ilustruje
poniższy wzór.
wi+1 = wi + (x - wi)
gdzie:
wi+1 - wektor wag w (i+1)-ej (tj. bieżącej) prezentacji,
wi - wektor wag w i-tej (tj. poprzedniej) prezentacji,
x - wektor wartości wejściowych,
 - współczynnik proporcjonalności, zwykle mniejszy od 1.
Aby opis algorytmu uczenia sieci był pełny, należy omówić jeszcze jeden bardzo ważny
proces. Zauważmy, że podczas uczenia sieci może dojść do bardzo niekorzystnego zjawiska
polegającego na tym, że niektóre neurony mogą wykazywać nadaktywność, czyli będą
zwyciężać dla więcej niż jednego zestawu wejściowego. Inne zaś neurony mogą żadnej
rywalizacji nie wygrać. Powodowałoby to nadmierne przeciążanie neuronów uczących się, co
w konsekwencji prowadziłoby do złej pracy sieci. Dlatego opracowano mechanizm nazwany
forsowaniem zwycięzcy, który rozwiązuje ten problem. Najogólniej polega on na wyszukaniu
wśród neuronów, które jeszcze nie zwyciężyły w żadnym zestawie uczącym, neuronu o
największej aktywacji. Ten właśnie neuron poddaje się uczeniu.
Trzeba dodać, że powyższy algorytm uczenia sieci Kohonena przyniesie prawidłowe
rezultaty, jeśli wartości wejściowe oraz wagi będą znormalizowane. Wartości wejściowe
muszą więc należeć do przedziału [-1, 1] oraz długość wektora wejściowego musi być taka
sama - najczęściej równa 1. Wektory wag, podobnie jak wejścia, muszą zostać zredukowane
do jednostkowej długości.
Realizacja rozpoznawania pisma ręcznego
Aby obraz litery można było przetworzyć w sieci neuronowej, musimy najpierw zamienić go
na postać binarną. Rysunek 2 przedstawia sposób w jaki się to dokonuje.
Rysunek 2. Binaryzacja obrazu litery.
Na obraz litery (Rysunek 2a) nałożona jest siatka prostokątna (Rysunek 2b). Następnie pola,
przez które przechodzi kreska litery, są zaczerniane, a pozostałe pola pozostają białe. W ten
sposób powstaje obraz binarny (Rysunek 2c). Taki obraz możemy wprowadzić na wejście
sieci neuronowej. Jeśli sieć neuronowa ma tyle neuronów wejściowych ile pikseli posiada
binarny obraz litery, wtedy każdemu pikselowi możemy przypisać wartość liczbową i podać
ją na poszczególne neurony wejściowe sieci.
W naszym przykładowym projekcie, który jest ilustracją omawianego tematu, przyjmujemy,
że rozmiar siatki dzielącej literę na poszczególne piksele, wynosi 5 pikseli szerokości i 7
pikseli wysokości. Daje to łącznie 35 pikseli zapisanego w postaci binarnej znaku. Liczba ta
wyznacza ilość neuronów wejściowych sieci Kohonena, którą użyjemy w przykładzie. Z kolei
ilość neuronów wyjściowych ustalamy na 26, ponieważ chcemy, aby tyle właśnie liter sieć
rozpoznawała. Będą to wszystkie litery alfabetu łacińskiego. Każdy neuron wyjściowy będzie
reprezentował jedną literę. Jego pobudzenie oznaczać będzie rozpoznanie odpowiadającej mu
litery. Natomiast to, który neuron będzie odpowiadał za jaką literę, sieć ustali sama podczas
procesu uczenia.
Dla zilustrowania omawianych w artykule zagadnień stworzyłem program działający w
systemie Windows. Program napisany jest w języku C/C++ i wykorzystuje funkcje API
Windows. Do plików źródłowych dołączony jest również plik projektu dla kompilatora MS
Visual C++ oraz skompilowana wersja programu pismo. Źródła programu można także
skompilować wykorzystując jeden z darmowych kompilatorów dla platformy Windows (np.
Borland C++ 5.5).
Opis programu pismo
Program pismo składa się z jednego okna. Możemy go zobaczyć na rysunku 3.
Rysunek 3. Okno główne programu pismo.
Podczas uruchamiania programu wczytywany jest plik data.txt, zawierający zbiór uczący dla
sieci neuronowej. Składają się na niego wzorce poszczególnych liter. Użytkownik ma
możliwość modyfikacji tego zbioru. Może on w tym celu zaznaczyć przycisk Wzorce, a
następnie wybrać na liście żądaną literę. Następnie, w górnej części okna programu, na oknietablicy narysować literę. Po naciśnięciu na przycisk Zapisz wzorzec zaznaczonej na liście
litery zostanie uaktualniony według znaku narysowanego przez użytkownika. Wybierając
opcję Rozpoznawanie przechodzimy do trybu rozpoznawania pisma. Ważną czynnością, którą
musimy wykonać na poczatku, jest uczenie sieci. Dokonujemy tego naciskając na przycisk
Trenuj sieć. Po tej czynności możemy narysować literę na okienku-tablicy. Po wciśnięciu
przycisku Rozpoznaj otrzymamy wynik przetwarzania sieci Kohonena. Pod okienkiem, gdzie
rysujemy literę, widoczny jest napis informujący nas, jaką literę program rozpoznał.
Binaryzacja obrazu litery
Wspomnieliśmy już, że narysowaną literę należy zamienić na obraz binarny. Dokonujemy
tego przy pomocy kodu z Listingu 1.
Listing 1. Binaryzacja obrazu litery
for(i=0; i<cxClient; i++)
{
if(!IsVLineClear(hdc, i))
{
downSampleLeft = i;
break;
}
}
for(i=cxClient; i>0; i--)
{
if(!IsVLineClear(hdc, i))
{
downSampleRight = i;
break;
}
}
for(i=0; i<cyClient; i++)
{
if(!IsHLineClear(hdc, i))
{
downSampleTop = i;
break;
}
}
for(i=cxClient; i>0; i--)
{
if(!IsHLineClear(hdc, i))
{
downSampleBottom = i;
break;
}
}
// próbkowanie
widthRect = (downSampleRight - downSampleLeft)/DOWNSAMPLE_WIDTH;
heightRect = (downSampleBottom - downSampleTop)/DOWNSAMPLE_HEIGHT;
idx=0;
for(i=0; i<DOWNSAMPLE_HEIGHT; i++)
{
for(j=0; j<DOWNSAMPLE_WIDTH; j++)
{
rect.left = downSampleLeft + j*widthRect;
rect.right = rect.left + widthRect;
rect.top = downSampleTop + i*heightRect;
rect.bottom = rect.top + heightRect;
if(IsRectClear(hdc, rect))
{
sample[idx++] = MIN_INPUT;
}
else
{
sample[idx++] = MAX_INPUT;
}
}
}
Najpierw ustalamy granice w jakich znajduje się badana przez nas litera. Lewą, prawą , górną
oraz dolną granicę określają zmienne downSampleLeft, downSampleRight,
downSampleTop, downSampleBottom. Mając określony obszar w jakim znajduje się
litera, dzielimy go na 5 kolumn i 7 rzędów. Otrzymujemy w ten sposób 35 prostokątów, w
których badamy, czy przechodzi przez nie fragment litery. Jeśli przez dany prostokąt litera
przechodzi, wtedy uznajemy go za czarny "piksel" i odpowiadającemu mu neuronowi
nadajemy wartość wejściową 0,5. W przeciwnym przypadku neuronowi wejściowemu
przypisujemy wartość -0,5. Listing 2 obrazuje, w jaki sposób zaimplementowane są funkcje:
IsHLineClear, IsVLineClear, IsRectClear. Wszystkie one przy pomocy
funkcji Windows GetPixel sprawdzają kolor pojedynczego piksela. Jeśli przynajmniej
jeden piksel jest czarny, wtedy funkcje zwracają FALSE – w przeciwnym przypadku TRUE.
Listing 2. Implementacja funkcji IsHLineClear, IsVLineClear, IsRectClear
BOOL IsHLineClear(HDC hdc, int line)
{
int j;
for(j=0; j<cxClient; j++)
{
if(GetPixel(hdc, j, line) == 0)
{
return FALSE;
}
}
return TRUE;
}
BOOL IsVLineClear(HDC hdc, int line)
{
int j;
for(j=0; j<cyClient; j++)
{
if(GetPixel(hdc, line, j) == 0)
{
return FALSE;
}
}
return TRUE;
}
BOOL IsRectClear(HDC hdc, RECT rect)
{
int i, j;
for(i=rect.left; i<rect.right; i++)
{
for(j=rect.top; j<rect.bottom; j++)
{
if(GetPixel(hdc, i, j) == 0)
{
return FALSE;
}
}
}
return TRUE;
}
Rozpoznawanie litery
Wiemy już, że aby zapewnić sieci Kohonena prawidłową pracę, należy jej dane wejściowe
poddać normalizacji. Dokonujemy tego w sposób przedstawiony na listingu 3.
Listing 3. Normalizacja danych wejściowych
void Kohonen::NormalizeInput(double *input, double *normfac)
{
double length;
length = VectorLength(NUMBER_INPUT, input);
if(length < 1.e-30)
length = 1.e-30 ;
*normfac = 1.0 / sqrt(length);
}
double Kohonen::VectorLength (int n, double *vec )
{
double sum = 0.0;
for (int i=0;i<n;i++ )
sum += vec[i] * vec[i];
return sum;
}
Głównym celem procedury normalizacyjnej NormalizeInput jest wyliczenie wartości
normfac używanej w dalszych obliczeniach. Funkcja VectorLength oblicza kwadrat
długości wektora.
Następnie możemy przyjrzeć się jak sieć wylicza zwycięski neuron podczas procesu
rozpoznawania pisma. Funkcję Winner realizującą to zadanie możemy prześledzić na
listingu 4.
Listing 4. Funkcja obliczająca zwycięski neuron
int Kohonen::Winner(double *input, double *normfac)
{
int i, win=0;
double biggest;
double *optr;
NormalizeInput(input, normfac);
biggest = -1.E30;
for (i=0; i<NUMBER_OUTPUT; i++)
{
optr = outputWeights[i];
output[i] = dotProduct (NUMBER_INPUT, input, optr ) * (*normfac);
output[i] = 0.5 * (output[i] + 1.0) ;
if ( output[i] > biggest
{
biggest = output[i] ;
win = i ;
}
if ( output[i] > 1.0 )
output[i] = 1.0;
if ( output[i] < 0.0 )
output[i] = 0.0;
}
return win ;
}
Każdą wartość wyjściową output[i] wyliczamy przy pomocy funkcji dotProduct.
Niezbędnymi parametrami są: wektor danych wejściowych, zestaw wag łączących neurony
wejściowe z obliczanym i-tym neuronem wyjściowym oraz wartość normalizacyjna. Funkcja
ta realizuje mnożenie wartości wejściowych przez odpowiednie wagi. Wykorzystywane tutaj
zestawy wag sieci wyznaczane są w trakcie procesu uczenia.
Uczenie sieci
Główną procedurę uczącą, stanowiącą najważniejszy element prawidłowego funkcjonowania
sieci neuronowej Kohonena, możemy prześledzić na listingu 5.
Listing 5. Procedura uczenia sieci Kohonena
BOOL Kohonen::Learn(TrainingSet *tptr)
{
(...)
nwts = NUMBER_INPUT * NUMBER_OUTPUT;< BR >
Initialize();
best_err = 1.e30;
rate = LEARN_RATE;
// główna pętla ucząca
n_retry = 0;
for (iter=0; ; iter++)
{
EvaluateErrors(tptr, rate, won, &bigerr, correc);
s_neterr = bigerr ;
if (s_neterr < best_err)
{
best_err = s_neterr ;
CopyWeights(bestnet, this);
}
winners = 0;
for (i=0; i<NUMBER_OUTPUT; i++ )
{
if (won[i] != 0)
winners++;
}
if (bigerr < QUIT_ERROR)
break ;
if ((winners < NUMBER_OUTPUT) && (winners < tptr->ntrain))
{
ForceWin(tptr, won);
continue;
}
AdjustWeights(rate, won, &bigcorr, correc);
if (bigcorr < 1.e-5)
{
if (++n_retry > RETRIES)
break;
Initialize();
iter = -1;
rate = LEARN_RATE;
continue;
}
if (rate > 0.01)
rate *= REDUCTION;
}
(...)
}
Jest ona stosunkowo długa, dlatego Listing 5 zawiera jej najważniejsze fragmenty. W funkcji
Initialize wszystkie wagi przyjmują wartości losowe po czym są normalizowane.
Następnie w pętli dokonywane jest uczenie sieci. Pierwszą z funkcji jest
EvaluateErrors. Jest ona miarą dostosowania aktualnych wag neuronu wyjściowego do
rozpoznawania wzorca zawartego w danych wejściowych. Jeśli wagi te nie są jeszcze
dostosowane, wtedy zmienna bigerr osiąga duże wartości. Z chwilą, gdy wagi w trakcie
uczenia zmieniają się i są coraz bardziej dostosowane do prawidłowego rozpoznawania
wzorca, zmienna bigerr zmniejsza się. Gdy osiągnie wartość mniejszą niż QUIT_ERROR
uczenie danego neuronu uznajemy za zakończone. Następnie zliczamy ilość nauczonych już
neuronów, po czym wywołujemy funkcję ForceWin. Wykonuje ona kolejno następujące
czynności:



wybiera wzorzec wejściowy, który nie ma jeszcze "swojego" neuronu wyjściowego,
dla wzorca z poprzedniego punktu wybierany jest neuron o największej aktywności ale jest to neuron wybrany spośród tych, które dotychczas jeszcze nie zwyciężyły dla
żadnego przypadku uczącego,
dla neuronu z poprzedniego punktu modyfikowane są jego wagi.
Końcowym etapem uczenia jest normalizacja wag, która wpływa dodatnio na proces
rozpoznawania wzorców. Realizowana jest ona poprzez funkcję AdjustWeights. Do
pełnego przestudiowania i analizy kodu realizującego działanie sieci neuronowej Kohonena
odsyłam Czytelnika do kodów źródłowych programu pismo .
Podsumowanie
Zawarte w artykule kody źródłowe, przede wszystkim pliki Kohonen.h i Kohonen.cpp, są
uniwersalne. Można je zastosować nie tylko do rozpoznawania znaków graficznych, ale także
do wielu innych problemów (rozpoznawanie mowy, inne problemy klasyfikacji). Myślę że
analiza kodu dołączonego do artykułu pozwoli lepiej zagłębić się w tajniki sieci neuronowej
Kohonena.

Podobne dokumenty