Uczenie maszynowe i sztuczne sieci neuronowe/Ćwiczenia 7

Z Brain-wiki

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ę