Uczenie maszynowe i sztuczne sieci neuronowe/Ćwiczenia 7
Spis treści
Wstęp
Celem tych ćwiczeń jest zapoznanie się z funkcjonalnością PyBrain wspierającymi klasyfikację za pomocą sieci neuronowych.
Zbiór uczący
PyBrain posiada klasę do obsługi zbiorów uczących przeznaczonych do klasyfikacji: ClassificationDataSet. Jest ona dostarczana przez moduł pybrain.datasets.classification.
Podstawowa składnia to:
DS = ClassificationDataSet(inputdim, nb_classes=2, class_labels=['Fish','Chips'])
tzn. musimy zdefiniować:
- rozmiar wektorów wejściowych inputdim
- liczba klas nb_classes
- opcjonalnie możemy podać nazwy klas: class_labels
Wymiar docelowy ma być 1. Celem są etykiety klasy zaczynające się od zera. Jeśli z jakiegoś powodu nie wiemy wcześniej, ile będzie klas, możliwe jest ustawienie tej informacji później, używając metod assignClasses() lub calculateStatistics() tak jak w poniższym przykładzie:
DS = ClassificationDataSet(2, class_labels=['sredni', 'duzy', 'maly'])
DS.appendLinked([ 0.1, 0.5 ] , [0])
DS.appendLinked([ 1.2, 1.2 ] , [1])
DS.appendLinked([ 1.4, 1.6 ] , [1])
DS.appendLinked([ 1.6, 1.8 ] , [1])
DS.appendLinked([ 0.10, 0.80 ] , [2])
DS.appendLinked([ 0.20, 0.90 ] , [2])
print DS.calculateStatistics()
print DS.classHist
print DS.nClasses
print DS.getClass(1)
print DS.getField('target').transpose()
Problem klasyfikacji jest zazwyczaj łatwiejszy do rozwiązania jeśli w warstwie wyjściowej umieścimy tyle neuronów ile jest klas i docelowe klasy są kodowane jako wysoki stan jednego z neuronów wyjściowych. Nawiązuje to trochę do rozwiązań jakie wyprowadziliśmy na wykładzie dla regresji wielorakiej (softmax). ClassificationDataSet posiada metodę _convertToOneOfMany pozwalającą na automatyczne przekodowanie klas z numeracji [math]{0,1,\dots,k-1}[/math] na kodowanie zer i jedynek na odpowiednich wyjściach.
DS._convertToOneOfMany(bounds=[0, 1])
print DS.getField('target')
[[1 0 0]
[0 1 0]
[0 1 0]
[0 1 0]
[0 0 1]
[0 0 1]]
print DS.getField('class').transpose()
[[0 1 1 1 2 2]]
>>> DS._convertToClassNb()
>>> print DS.getField('target').transpose()
[[0 1 1 1 2 2]]
Klasyfikacja
W tym przykładzie pokażemy jak przy pomocy sieci neuronowej zbudować klasyfikator. Zadanie będzie polegało na zaliczaniu punktów do jednego z trzech typów. Dane będą pochodzić z trzech rozkładów normalnych dwuwymiarowych o różnych parametrach.
Najpierw przygotowyjemy grafikę do pracy w trybie interaktywnym
# -*- coding: utf-8 -*-
import matplotlib
matplotlib.use('TkAgg')
Następnie musimy zaimportować z bibliotek potrzebne klasy i definicje:
from pybrain.datasets import ClassificationDataSet
from pybrain.utilities import percentError
from pybrain.tools.shortcuts import buildNetwork
from pybrain.supervised.trainers import BackpropTrainer
from pybrain.structure.modules import SoftmaxLayer
from pylab import ion, ioff, figure, draw, contourf, clf, show, hold, plot
from scipy import diag, arange, meshgrid, where
from numpy.random import multivariate_normal
Teraz przygotujemy zestawy danych. Tablica mu zawiera wektory średnich dla każdego z trzech rozkładów, tablica cov zawiera macierze kowariancji dla tych rozkładów:
mu = [(-1,0),(2,4),(3,1)]
cov = [diag([1,1]), diag([0.5,1.2]), diag([1.5,0.7])]
alldata = ClassificationDataSet(2, 1, nb_classes=3)
for n in xrange(400):
for klasa in range(3):
input = multivariate_normal(mu[klasa],cov[klasa])
alldata.addSample(input, [klasa])
Teraz dzielimy ten zbiór danych uczących na część treningową i testową. Podzielimy go w proporcji 3/4 danych uczących i 1/4 testowych. Można oczywiście od razu wygenerować dwa osobne zbiory tak jak to robiliśmy na poprzednich zajęciach. Tu jednak zaprezentujemy narzędzie wspomagające ten proces dostępne w PyBrain.
tstdata, trndata = alldata.splitWithProportion( 0.25 )
Warto przekodować te dane tak aby jedna klasa była reprezentowana przez jeden neuron wyjściowy (porównaj: regresja softmax).
trndata._convertToOneOfMany( )
tstdata._convertToOneOfMany( )
Możemy wypisać trochę informacji o naszym zbiorze danych:
print "Ilość przykładów treningowych: ", len(trndata)
print "Rozmiary wejścia i wyjścia: ", trndata.indim, trndata.outdim
print "Pierwszy przykład (wejście , wyjście, klasa):"
print trndata['input'][0], trndata['target'][0], trndata['class'][0]
Teraz wytworzymy sieć. Skorzystamy ze skrótu buildNetwork. Rozmiar wejścia i wyjścia muszą się zgadzać z rozmiarami danych wejściowych i wyjściowych, odpowiednio. Do klasyfikacji najlepiej w warstwie wyjściowej umieścić warstwę typu softmax.
Proszę poeksperymentować z ilością i typem neuronów w warstwie ukrytej. Również ilość warstw ukrytych można zmieniać podając dodatkowe liczby pomiędzy parametrami określającymi rozmiar wejścia i wyjścia. Jako punkt startu zastosujemy 5 domyślnych (sigmoidalnych) neuronów w warstwie ukrytej:
fnn = buildNetwork( trndata.indim, 5, trndata.outdim, outclass=SoftmaxLayer )
Przygotowujemy trenera w standardowy sposób:
trainer = BackpropTrainer( fnn, dataset=trndata, momentum=0.1, verbose=True, weightdecay=0.01)
Tu przygotowujemy siatkę punktów, które będziemy stosować do zilustrowania podziału przestrzeni wejściowej na obszary należące do poszczególnych klas. Funkcja meshgrid pobiera na wejście wektor dla x i y, a zwraca tablicę dwuwymiarową reprezentującą siatkę o brzegach x i y. Punkty siatki pakujemy do obiektu typu ClassificationDataSet, aby było je łatwo przepuszczać przez sieć. Ponieważ musimy dodawać do tego obiektu pojedyncze punkty a nie całe tablice to trzeba tablcę spłaszczyć metodą ravel (równoważnie można by zaimplementować tworzenie tego obiektu w dwóch pętlach przebiegających odpowiednio x i y ). Pomimo, że nie interesuje nas teraz klasa tych punktów coś musimy wpisać (np. 0) i przekodować ten zestaw danych na typ OneOfMany, żeby można było łatwo skorzystać z funkcji przepuszczających zbiory danych przez sieć.
ticks = arange(-3.,6.,0.2)
X, Y = meshgrid(ticks, ticks)
# need column vectors in dataset, not arrays
griddata = ClassificationDataSet(2,1, nb_classes=3)
for i in xrange(X.size):
griddata.addSample([X.ravel()[i],Y.ravel()[i]], [0])
griddata._convertToOneOfMany()
Teraz przystępujemy do uczenia sieci (zapuścimy uczenie na 20 epok), przy czym po każdym kroku będziemy podglądać aktualny stan sieci, więc w metodzie trainEpochs podajemy 1 krok.
for i in range(20):
trainer.trainEpochs( 1 )
Sprawdzamy działanie sieci na zbiorze uczącym i na testowym. Skorzystamy z metody trenera testOnClassData() do policzenia aktualnej klasyfikacji dla zbioru uczącego (domyślnie) i dla danych testowych. Funkcja percentError oblicza procentową rozbieżność pomiędzy swoimi argumentami. Wyniki wypisujemy na konsoli.
trnresult = percentError( trainer.testOnClassData(), trndata['class'] )
tstresult = percentError( trainer.testOnClassData(dataset=tstdata ), tstdata['class'] )
print "krok: %4d" % trainer.totalepochs, \
" błąd na zbiorze uczącym: %5.2f%%" % trnresult, \
" błąd na zbiorze testowym: %5.2f%%" % tstresult
Teraz przygotujemy się do ilustrowania działania sieci graficznie. Przepuścimy przez sieć zbiór danych zawierających siatkę punktów. Dla każdego punktu otrzymamy w zmiennej out aktywność neuronów warstwy wyjściowej.
out = fnn.activateOnDataset(griddata)
Za pomocą metody argmax() pobieramy indeks neuronu, który miał największą aktywność.
out = out.argmax(axis=1) # the highest output activation gives the class
Dopasowujemy kształt wyjścia do kształtu wejść.
out = out.reshape(X.shape)
Teraz możemy wykonać rysunek.
figure(1)
ioff() # wyłączamy tryb interaktywny grafiki
clf() # czyścimy rysunek
hold(True) # włączamy opcję dopisywania do bieżącego rysunku
for c in [0,1,2]: # iterujemy się przez możliwe klasy
here, _ = where(tstdata['class']==c) # wybieramy indeksy punktów testowych należących do klasy c
plot(tstdata['input'][here,0],tstdata['input'][here,1],'o') # rysujemy kółkami punkty testowe należące do klasy c
if out.max()!=out.min(): # safety check against flat field
contourf(X, Y, out) # przy pomocy zapełnionych konturów rysujemy wynik klasyfikacji punktów siatki, daje nam to ilustrację obszarów na jakie sieć aktualnie dzieli przestrzeń wejściową
ion() # przełączamy grafikę w tryb interaktywny
draw() # odświeżamy rysunek
Po zakończeniu uczenia czekamy aż użytkownik zamknie ostatni obrazek.
ioff()
show()
Polecenia dodatkowe
- Proszę zbadać powtarzalność granic separacji
- Proszę zbadać klasyfikację punktu odległego od zbioru uczącego:
out = fnn.activate((100, 100))
print out
- Proszę zbadać zależność separacji i kształty powierzchni rozgraniczających w zależności od:
- liczby neuronów w warstwie ukrytej
- współczynnika weightdecay w trenerze
- Proszę sprawdzić działanie klasyfikatora dla innych konfiguracji klas wejściowych, np: łącząc kilka rozkładów normalnych o różnych parametrach w jedną klasę