|
Z teorią obliczeń sieciowych, dziedziną dość
jeszcze młodą, wiązane są nadzieje na rozwikłanie problemów,
dla których zadowalające rozwiązania algorytmiczne nie
istnieją lub nie są znane. Dotyczy to w szczególności takich
zagadnień, jak analiza i przetwarzanie sygnałów, rozpoznawnie
obrazów i mowy, problemy optymalizacji. W tym artykule
przedstawimy jeden z prostszych typów sieci neuronowych i jego
zastosowanie w najprostszym programie OCR.
Choć terminy "sztuczna inteligencja" czy
"klasyfikacja wzorców" mogą sprawiać wrażenie bardzo
skomplikowanych, zaprogramowanie prostej (lecz użytecznej)
sieci neuronowej nie jest trudne nawet dla początkującego
programisty. Oczywiście, nim przejdziemy do zbudowania
pierwszej sieci, musimy wyjaśnić kilka podstawowych pojęć. Już
sam termin "sieć neuronowa" sugeruje obecność w strukturze
sieci komórek zbliżonych funkcjonowaniem do komórek
biologicznego układu nerwowego. Oczywiście w stosunku do
biologicznej rzeczywistości przyjmuje się wiele uproszczeń.
Model neuronu
Najprostszy model jednostki neuronowej
(neuronu) ' przedstawiony pierwotnie przez McCullocha i
Pittsa, a rozszerzony przez innych badaczy ' składa się z
ustalonej liczby wejść i jednego wyjścia. Model taki, podobnie
jak prawdziwa komórka nerwowa, poprzez wejścia otrzymuje z
otoczenia pewien sygnał, który przetwarza i na wyjściu
generuje odpowiedź. Wartość sygnału wyjściowego zależy od
pobudzenia, które jest sumą ważoną sygnałów wejściowych.
Obecność wag pozwala przypisać poszczególnym wejściom różne
znaczenie, a ich modyfikacja jest podstawą procesu uczenia się
sieci.
Zwykle od wartości pobudzenia odejmuje się
pewną wartość progową (dla uproszczenia próg możemy traktować
jako dodatkowe wejście o wadze 1,0) i tak otrzymaną wartość
przetwarza się przy użyciu funkcji progowej (zwanej też
funkcją przejścia lub funkcją aktywacji). Zadaniem tej funkcji
jest normalizacja poziomu sygnału, najczęściej w przedziale
<0, 1> lub <-1, 1>. Funkcja przejścia jest z
reguły ciągła, by umożliwić uczenie sieci często stosowaną
metodą propagacji wstecznej.
Najczęściej wykorzystywane funkcje aktywacji
to m.in.:
- funkcja sigmoidalna
f(x) = 1/(1+exp(-x)),
- tangens hiperboliczny
f(x) = tanh(x),
- funkcja Gaussa
f(x) = exp(-x*x).
Struktura sieci
Oczywiście pojedyncza jednostka sieci nie
jest jeszcze zdolna do wykonania użytecznych obliczeń. Potęga
sieci leży w strukturze połączeń między jednostkami. Często
spotykana i szczególnie prosta w implementacji jest sieć o
strukturze warstwowej (multi-layered network), w której
połączone są neurony kolejnych warstw, a sygnał przechodzi od
wejścia do wyjścia sieci, poruszając się tylko w jednym
kierunku (określa się to terminem feedforward). W praktyce nie
stosuje się sieci mającej więcej niż trzy warstwy. Najczęściej
przyjmuje się pełną strukturę połączeń między kolejnymi
warstwami (każdy z każdym), gdyż brak połączenia można
modelować ustawieniem odpowiedniej wagi na zero.
Symulator sieci w działaniu
Sieci warstwowe stosuje się do klasyfikacji
wzorców, tzn. wszędzie tam, gdzie każdemu sygnałowi
wejściowemu chcemy przypisać określony stan wyjść. Niejako
przy okazji uwidaczniają się tutaj największe zalety sieci
neuronowych: sieć nie tylko jest do pewnego stopnia odporna na
szum (tj. zakłócenia), ale często potrafi także sensownie
ekstrapolować, prawidłowo klasyfikując niespotkane wcześniej
wzorce.
Jak działa sieć? Wektor wejściowy
przekazywany jest do pierwszej z warstw i dla każdej jednostki
tej warstwy obliczany jest sygnał wyjściowy przekazywany
dalej. Algorytm ten powtarza się dla kolejnych warstw aż do
uzyskania sygnału na wyjściu sieci. By ten wyjściowy sygnał
odpowiadał naszym potrzebom, niezbędny jest trening sieci.
Proces uczenia
Kluczem do "nauczenia" sieci rozwiązywania
danego problemu jest odpowiedni dobór wag wejściowych. Dobór
ten można zrealizować na wiele sposobów. W przypadku tzw.
"uczenia z nauczycielem" (gdy znana jest poprawna odpowiedź)
powszechnie stosowany jest algorytm wstecznej propagacji
błędu.
W metodzie tej, jak wskazuje jej nazwa, błąd
obliczony dla warstwy wyjściowej jest przekazywany wstecz, w
głąb sieci, aż do wejścia. Wagi każdorazowo modyfikuje się
tak, by minimalizować wyznaczony błąd. W każdej iteracji
procesu nauczania można wyodrębnić następujące etapy:
1. Określamy błąd jednostki j warstwy
wyjściowej jako:
Err[j] = X'[j] * (CEL[j] X[j])
gdzie:
CEL [j] pożądana wartość na wyjściu j;
X[j] wartość otrzymana;
X'[j] pierwsza pochodna funkcji
przejścia w punkcie X[j].
2. Określamy błąd dla jednostki j w warstwie
ukrytej l wg wzoru:
Err[l, j] = X'[l, j] * SUMk(Err[l + 1, k] * WAGA[l + 1, k, j])
gdzie:
k ' zmienia się od 1 do liczby jednostek
warstwy (l+1);
Err[l, j] błąd przypisany jednostce j
warstwy l;
X'[l, j] pierwsza pochodna funkcji
przejścia w punkcie X[l, j];
WAGA[l, k, j] waga wejścia j jednostki
k warstwy l.
3. Modyfikujemy wagi wejściowe każdego
neuronu:
WAGA[l, j] = WAGA[l, j] + eta * SUMk(Err[l, j] * X[l 1, k]) + alfa * dWAGA[l, j],
gdzie:
eta czynnik (stała) uczenia;
alfa czynnik tłumienia;
dWAGA zmiana wagi w czasie.
Proces uczenia sieci polega na cyklicznej
prezentacji zestawu par wzorzec'odpowiedź i każdorazowej
modyfikacji wag aż do uzyskania pożądanej precyzji. Należy
zawsze pamiętać, że konkretna sieć może nie być w stanie
nauczyć się prawidłowego rozpoznawania wszystkich wzorców. Dla
efektywności uczenia niezwykle istotny jest odpowiedni dobór
współczynników eta i alfa. Przy wyższej wartości eta sieć uczy
się szybciej, ale mniej dokładnie. Przy niższej wartości eta
sieć uczy się wolniej, ale bardziej skutecznie. Wartość alfa
odpowiada za wytłumienie niepożądanych oscylacji. Odpowiedni
dobór alfa zwykle znacznie przyspiesza uczenie.
Program przykładowy
Zastosowanie sieci neuronowych przedstawimy
na przykładzie prostego programu OCR (kody źródłowe
zamieszczamy na naszej stronie WWW). Choć nie rozpoznaje on
ciągłego tekstu, a jedynie pojedyncze znaki (symbole), stanowi
na pewno ciekawą ilustrację przedstawionych zagadnień. Po
zapoznaniu się z interfejsem klasy NeuralNetwork czytelnicy
będą mogli wykorzystać ją do klasyfikacji wzorców wejściowych
innych niż znaki.
W programie
napisanym w C++ wyodrębniłem dla przejrzystości trzy moduły:
klasy Neuron,
NeuralLayer
oraz NeuralNetwork.
Klasa Neuron jest implementacją
przedstawionego wyżej modelu jednostki neuronowej. Jej
konstruktor przyjmuje jeden parametr typu całkowitego
określający liczbę wejść. Najważniejszą metodą jest
propagate() obliczająca wartość sygnału wyjściowego. W
zależności od wartości zmiennej activationFunction ,
stosowaną funkcją aktywacji jest funkcja liniowa lub
sigmoidalna.
Klasa NeuralLayer przedstawia warstwę
sieci. Konstruktor obiektu tej klasy wymaga określenia liczby
jednostek oraz liczby wejść każdej z jednostek. Podobnie jak w
klasie Neuron, za przetwarzanie sygnału odpowiada
metoda propagate(). Oprócz tego klasa ma kilka metod
ułatwiających operacje na jednostkach i umożliwiających dostęp
do tych jednostek.
Klasą bezpośrednio stosowaną "na zewnątrz"
jest NeuralNetwork, będąca reprezentacją sieci
neuronowej jako wektora warstw. Prywatne metody
computeError() i adjustWeights() są
odpowiedzialne za wyznaczenie i wsteczną propagację błędu oraz
za modyfikację wag. Przy konstrukcji sieci określamy jedynie
liczbę wejść, a kolejne warstwy dodajemy metodą
addLayer(). Wektor wejściowy oraz pożądany stan wyjść
ustalają metody setInput(double*) oraz setTarget
(double*). Odpowiedź sieci uzyskamy, wywołując metodę
getOutput(double*).
Moduł OCR.cpp
realizuje postawione przed nim zadanie w możliwie najprostszy
sposób: uczy sieć zestawu wzorców (cyfr arabskich) wczytanych
z pliku pattern.dat,
a następnie testuje ją na podobnym, lecz pełnym szumów
zestawie z pliku test.dat.
Wynik analizy każdego wzorca prezentowany jest na ekranie.
Zachęcam do porównania zawartości obu plików z danymi i ich
modyfikacji.
Wynik dzialania programu przykładowego
|