Uczenie maszynowe i sztuczne sieci neuronowe/Ćwiczenia 7: Różnice pomiędzy wersjami

Z Brain-wiki
 
(Nie pokazano 8 pośrednich wersji utworzonych przez tego samego użytkownika)
Linia 1: Linia 1:
  
 
=Wstęp=
 
=Wstęp=
−
Celem tych ćwiczeń jest zapoznanie się z funkcjonalnością PyBrain wspierającymi klasyfikację za pomocą sieci neuronowych.  
+
Celem tych ćwiczeń jest zapoznanie się z klasyfikacją za pomocą sieci neuronowych.
  
 +
== Importy ==
 +
<source lang = python>
 +
# -*- coding: utf-8 -*-
 +
import matplotlib
 +
%matplotlib notebook
 +
#matplotlib.use('TkAgg')
 +
import numpy as np
 +
import pylab as py
 +
from scipy import diag, arange, meshgrid, where
 +
from numpy.random import multivariate_normal
 +
 +
</source>
 
=Zbiór uczący =
 
=Zbiór uczący =
−
PyBrain posiada klasę do obsługi zbiorów uczących przeznaczonych do klasyfikacji:
+
Funkcja generująca zbiór uczący:
−
<tt>ClassificationDataSet</tt>. Jest ona dostarczana przez moduł <tt>pybrain.datasets.classification</tt>.
 
  
−
Podstawowa składnia to:
+
<source lang = python>
−
<source lang =python>
+
def gen(ile):
−
DS = ClassificationDataSet(inputdim, nb_classes=2, class_labels=['Fish','Chips'])
+
    mu = [(-1,0),(2,4),(3,1)]
 +
    cov = [diag([1,1]), diag([0.5,1.2]), diag([1.5,0.7])]
 +
    X = np.zeros((ile*3, 2)) # miejsce na dane wejściowe
 +
    Y = np.zeros((ile*3,1),dtype = int) # miejsce na dane wyjściowe
 +
    for klasa in range(3):
 +
        X[klasa*ile:(klasa+1)*ile] = multivariate_normal(mu[klasa],cov[klasa],ile)
 +
        Y[klasa*ile:(klasa+1)*ile] = klasa
 +
    return (X,Y)
 +
       
 
</source>
 
</source>
−
tzn. musimy zdefiniować:
 
−
* rozmiar wektorów wejściowych <tt>inputdim</tt>
 
−
* liczba klas <tt>nb_classes</tt>
 
−
* opcjonalnie możemy podać nazwy klas: <tt>class_labels</tt>
 
−
 
−
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 <tt>assignClasses()</tt> lub <tt>calculateStatistics()</tt> tak jak w poniższym przykładzie:
 
−
<source lang = "py">
 
−
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 [[Uczenie_maszynowe_i_sztuczne_sieci_neuronowe/Wykład_6#Regresja_softmax_jako_GLM | regresji wielorakiej (softmax)]].  
  
 +
<source lang = python>
 +
def convert_to_target(Y):
 +
    target = np.zeros((len(Y), len(np.unique(Y))))
 +
    for i in range(len(Y)):
 +
        target[i,Y[i,0]] = 1
 +
    return target
 
</source>
 
</source>
  
−
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 [[STAT:Uczenie_maszynowe_i_sztuczne_sieci_neuronowe/Wykład_6#Regresja_softmax_jako_GLM | regresji wielorakiej (softmax)]].  ClassificationDataSet  posiada metodę <tt>_convertToOneOfMany</tt> pozwalającą na automatyczne przekodowanie klas z numeracji <math>{0,1,\dots,k-1}</math> na kodowanie zer i jedynek na odpowiednich wyjściach.
+
Testujemy:
−
<source lang = "py">
+
<source lang = python>
−
DS._convertToOneOfMany(bounds=[0, 1])
+
X,Y = gen(5)
−
print DS.getField('target')
+
print(Y)
−
[[1 0 0]
+
py.scatter(X[:,0],X[:,1],c = Y,s = 10)
−
[0 1 0]
+
target = convert_to_target(Y)
−
[0 1 0]
+
print(target)
−
[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]]
 
 
</source>
 
</source>
  
 
=Klasyfikacja=
 
=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.
 
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.
 +
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:
  
−
Najpierw przygotowyjemy grafikę do pracy w trybie interaktywnym
+
<source lang = python>
−
<source lang = 'py'>
+
N_przykladow =37
−
# -*- coding: utf-8 -*-
+
X,  Y      = gen(N_przykladow) # przykłady do ciągu uczącego
−
import matplotlib
+
X_m, Y_m    = gen(N_przykladow) # przykłady do ciągu monitorującego
−
matplotlib.use('TkAgg')
+
py.figure()
 +
py.scatter(X[:,0],X[:,1],c = Y,s = 10)
 +
py.scatter(X_m[:,0],X_m[:,1],c = Y_m, s = 10, marker = 's')
 
</source>
 
</source>
  
−
Następnie musimy zaimportować z bibliotek potrzebne klasy i definicje:
+
Warto przekodować te dane tak aby jedna klasa była reprezentowana przez jeden neuron wyjściowy:  
−
<source lang = 'py'>
+
<source lang = python>
−
from pybrain.datasets            import ClassificationDataSet
+
target = convert_to_target(Y)
−
from pybrain.utilities          import percentError
+
target_m = convert_to_target(Y_m)
−
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
 
 
</source>
 
</source>
  
−
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:
 
  
−
<source lang = 'py'>
+
Teraz wytworzymy sieć.  Rozmiar wejścia i wyjścia muszą się zgadzać z rozmiarami danych wejściowych i wyjściowych, odpowiednio.
−
mu = [(-1,0),(2,4),(3,1)]
+
<source lang = python>
−
cov = [diag([1,1]), diag([0.5,1.2]), diag([1.5,0.7])]
+
class siec(object):
−
alldata = ClassificationDataSet(2, 1, nb_classes=3)
+
    def __init__(self, X, Y, N_hid=3):
−
for n in xrange(400):
+
        self.X = X
−
    for klasa in range(3):
+
        self.Y = Y
−
        input = multivariate_normal(mu[klasa],cov[klasa])
+
        self.N_wej = X.shape[1]
−
        alldata.addSample(input, [klasa])
+
        self.N_wyj = Y.shape[1]
 +
        self.N_hid = N_hid
 +
 +
        # inicjujemy połączenia
 +
        # wagi ułożone są tak, że w kolejnych wierszach są kolejne neurony
 +
        # a w kolumnach wagi od konkretnego neuronu
 +
        # to +1 jest wagą dla obciążenia
 +
        self.w_1 = (2*np.random.random((self.N_hid, self.N_wej+1)) - 1)/self.N_wej # pomiędzy warstwą pierwszą (wejściem) a warstwą ukrytą
 +
        self.w_2 = (2*np.random.random((self.N_wyj, self.N_hid+1)) - 1)/self.N_hid
 +
        self.dw1 = np.zeros((self.N_hid, self.N_wej+1))
 +
        self.dw2 = np.zeros((self.N_wyj, self.N_hid+1))
 +
 +
    def g1(self, x):
 +
        f = np.exp(-x)
 +
        f[f>1e8]=1e8
 +
        y = 1./(1+f)
 +
        return y 
 +
    def g1_prim(self, x):
 +
        y = x*(1-x)
 +
        return y
 +
    def g2(self, x):
 +
        y = x
 +
        return y 
 +
    def g2_prim(self, x):
 +
        y = 1
 +
        return y
 +
    def get_params(self):
 +
        return np.concatenate((self.w_1.reshape(-1), self.w_2.reshape(-1)))
 +
 +
    def predict(self, x):
 +
        # propagacja "w przód"
 +
        self.a_0 = np.vstack((1,x))  # z warstwy wejściowej (zerowej) wychodzi a_0
 +
        z_1 = np.dot( self.w_1, self.a_0 )# na warstwe 1 wchodzą iloczyny skalarne
 +
        self.a_1 = np.vstack((1,self.g1(z_1))) # dokładamy 1 i dostaję wyjście z warstwy 1
 +
        z_2 = np.dot( self.w_2, self.a_1 ) # na warstwe 3 wchodzą iloczyny skalarne
 +
        self.a_2 = self.g2(z_2)
 +
        return self.a_2
 +
   
 +
    def get_error(self,X,trget):
 +
        self.bl = 0
 +
        for i in range(0,self.X.shape[0]):
 +
            # weźmy przykład i-ty       
 +
            x = self.X[i,:].reshape(self.N_wej,1)
 +
            y = self.Y[i,:].reshape(self.N_wyj,1)
 +
            self.a_2 = self.predict(x)
 +
 +
            # delta
 +
            d_2 = (self.a_2 - y)*self.g2_prim(self.a_2)
 +
       
 +
            self.bl += np.dot(d_2.T,d_2)/self.X.shape[0]
 +
        return self.bl
 +
       
 +
    def fit_one_step(self, eta1,eta2):
 +
        self.bl = 0
 +
        D_1 = np.zeros((self.N_hid, self.N_wej+1))
 +
        D_2 = np.zeros((self.N_wyj, self.N_hid+1))
 +
        for i in range(0,self.X.shape[0]):
 +
            # weźmy przykład i-ty       
 +
            x = self.X[i,:].reshape(self.N_wej,1)
 +
            y = self.Y[i,:].reshape(self.N_wyj,1)
 +
            self.a_2 = self.predict(x)
 +
 +
            # propagacja "wstecz"
 +
            d_2 = (self.a_2 - y)*self.g2_prim(self.a_2)
 +
            d_1 = np.dot(self.w_2.T, d_2) * self.g1_prim(self.a_1)#z_2
 +
 +
            # akumulujemy poprawki
 +
            D_2 +=  np.dot( d_2, self.a_1.T)
 +
            D_1 +=  np.dot( d_1[1:], self.a_0.T)
 +
 +
            self.bl += np.dot(d_2.T,d_2)/self.X.shape[0]
 +
        # uaktualniamy wagi
 +
        self.w_1 -=  eta1*D_1 + eta2*self.dw1
 +
        self.w_2 -=  eta1*D_2+  eta2*self.dw2
 +
        self.dw1  =  eta1*D_1
 +
        self.dw2  =  eta1*D_2 
 +
        return self.bl
 +
   
 
</source>
 
</source>
  
−
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.
 
  
−
<source lang = 'py'>
+
 
−
tstdata, trndata = alldata.splitWithProportion( 0.25 )
+
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 3 domyślnych (sigmoidalnych) neuronów w warstwie  ukrytej:
 +
 
 +
<source lang = python>
 +
S = siec( X, target, N_hid= 3)
 
</source>
 
</source>
  
−
Warto przekodować te dane tak aby jedna klasa była reprezentowana przez jeden neuron wyjściowy (porównaj: regresja softmax).
+
Przygotowujemy parametry:
−
<source lang = 'py'>
+
<source lang = python>
−
trndata._convertToOneOfMany( )
+
# liczba epok uczenia
−
tstdata._convertToOneOfMany( )
+
N_epochs = 150
 +
eta1 = 0.005
 +
eta2 = 0.9
 
</source>
 
</source>
  
−
Możemy wypisać trochę informacji o naszym zbiorze danych:
 
−
<source lang = 'py'>
 
−
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]
 
−
</source>
 
−
Teraz wytworzymy sieć. Skorzystamy ze skrótu <tt>buildNetwork</tt>. 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:
+
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 <tt>trainEpochs</tt> podajemy 1 krok.  
 +
<source lang = python>
 +
# inicjuję tablice na ewolucje
 +
 
 +
err  = np.zeros(N_epochs) #tablica na błąd zbioru uczącego
 +
err_m = np.zeros(N_epochs) #tablica na błąd zbioru monitorującego
 +
wagi = np.zeros((N_epochs,len(S.get_params()))) #tablica na wagi
  
−
<source lang = 'py'>
 
−
fnn = buildNetwork( trndata.indim, 5, trndata.outdim, outclass=SoftmaxLayer )
 
−
</source>
 
  
−
Przygotowujemy trenera w standardowy sposób:
+
py.figure()
−
<source lang = 'py'>
+
for cykl in range(N_epochs):
−
trainer = BackpropTrainer( fnn, dataset=trndata, momentum=0.1, verbose=True, weightdecay=0.01)
+
    err[cykl] = S.fit_one_step(eta1,eta2) # wykonaj krok uczenia
−
</source>
+
    err_m[cykl] = S.get_error(X_m,target_m)# normalizuję aby uzyskać średni błąd kwadratowy
 +
    wagi[cykl,:] = S.get_params() #pobieram wagi do zapamiętania
 +
</source>  
  
 +
Narysujmy co działo się z błędem i wagami:
 +
<source lang = python>
 +
#  rysunki
 +
py.figure()
 +
py.subplot(2,1,1) # błędów
 +
py.semilogy(err,'b',label='zb. uczacy')
 +
py.semilogy(err_m,'r',label='zb. monitorujacy')
 +
py.title(u'błąd')
 +
py.legend()
 +
#py.ylim([0,1])
 +
py.subplot(2,1,2) #wag
 +
py.plot(wagi)
 +
py.title('wagi')
 +
py.ylim([-3,3])
 +
</source>
 +
==Rysunki dodatkowe==
 
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 <tt>meshgrid</tt> pobiera na wejście wektor dla x i y, a zwraca tablicę dwuwymiarową reprezentującą siatkę  o brzegach x i y.  
 
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 <tt>meshgrid</tt> 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 <tt>ClassificationDataSet</tt>, 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ą <tt>ravel</tt> (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 <tt>OneOfMany</tt>, żeby można było łatwo skorzystać z funkcji przepuszczających zbiory danych przez sieć.
+
 
−
<source lang = 'py'>
+
<source lang = python>
 
ticks = arange(-3.,6.,0.2)
 
ticks = arange(-3.,6.,0.2)
 
X, Y = meshgrid(ticks, ticks)
 
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() 
 
−
</source>
 
−
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 <tt>trainEpochs</tt> podajemy 1 krok.
 
−
<source lang = 'py'>
 
−
for i in range(20):
 
−
    trainer.trainEpochs( 1 )
 
 
</source>
 
</source>
  
−
Sprawdzamy działanie sieci na zbiorze uczącym i na testowym. Skorzystamy z metody trenera <tt>testOnClassData()</tt> do policzenia aktualnej klasyfikacji dla zbioru uczącego (domyślnie) i dla danych testowych. Funkcja <tt>percentError</tt> oblicza procentową rozbieżność pomiędzy swoimi argumentami.  Wyniki wypisujemy na konsoli.
 
−
<source lang = 'py'>
 
−
    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
 
−
</source>
 
  
 
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 <tt>out</tt> aktywność neuronów warstwy wyjściowej.  
 
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 <tt>out</tt> aktywność neuronów warstwy wyjściowej.  
−
<source lang = 'py'>
+
<source lang = python>
−
     out = fnn.activateOnDataset(griddata)
+
     out = ...
 
</source>
 
</source>
 
Za pomocą metody <tt>argmax()</tt> pobieramy indeks neuronu, który miał największą aktywność.  
 
Za pomocą metody <tt>argmax()</tt> pobieramy indeks neuronu, który miał największą aktywność.  
−
<source lang = 'py'>
+
<source lang = python>
 
     out = out.argmax(axis=1)  # the highest output activation gives the class
 
     out = out.argmax(axis=1)  # the highest output activation gives the class
 
</source>
 
</source>
 
Dopasowujemy kształt wyjścia do kształtu wejść.
 
Dopasowujemy kształt wyjścia do kształtu wejść.
−
<source lang = 'py'>
+
<source lang = python>
 
     out = out.reshape(X.shape)
 
     out = out.reshape(X.shape)
 
</source>
 
</source>
  
 
Teraz możemy wykonać rysunek.
 
Teraz możemy wykonać rysunek.
−
<source lang = 'py'>
+
<source lang = python>
−
     figure(1)
+
     py.figure()
−
    ioff()  # wyłączamy tryb interaktywny grafiki
+
 
−
     clf()  # czyścimy rysunek
+
      
 
     hold(True) # włączamy opcję dopisywania do bieżącego rysunku
 
     hold(True) # włączamy opcję dopisywania do bieżącego rysunku
 
     for c in [0,1,2]:  # iterujemy się przez możliwe klasy
 
     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
+
         here, _ = ...      # 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   
+
         ... # rysujemy kółkami punkty testowe należące do klasy c   
 
     if out.max()!=out.min():  # safety check against flat field
 
     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ą
 
         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
+
     py.show()
−
    draw()  # odświeżamy rysunek
 
−
</source>
 
−
 
 
−
 
 
−
Po zakończeniu uczenia czekamy aż użytkownik zamknie ostatni obrazek.
 
−
<source lang = 'py'>
 
−
ioff()
 
−
show()
 
 
</source>
 
</source>
  

Aktualna wersja na dzień 18:53, 6 cze 2017

Wstęp

Celem tych ćwiczeń jest zapoznanie się z klasyfikacją za pomocą sieci neuronowych.

Importy

# -*- coding: utf-8 -*-
import matplotlib
%matplotlib notebook 
#matplotlib.use('TkAgg')
import numpy as np
import pylab as py
from scipy import diag, arange, meshgrid, where
from numpy.random import multivariate_normal

Zbiór uczący

Funkcja generująca zbiór uczący:

def gen(ile):
    mu = [(-1,0),(2,4),(3,1)]
    cov = [diag([1,1]), diag([0.5,1.2]), diag([1.5,0.7])]
    X = np.zeros((ile*3, 2)) # miejsce na dane wejściowe
    Y = np.zeros((ile*3,1),dtype = int) # miejsce na dane wyjściowe
    for klasa in range(3):
        X[klasa*ile:(klasa+1)*ile] = multivariate_normal(mu[klasa],cov[klasa],ile)
        Y[klasa*ile:(klasa+1)*ile] = klasa
    return (X,Y)


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).

def convert_to_target(Y):
    target = np.zeros((len(Y), len(np.unique(Y))))
    for i in range(len(Y)):
        target[i,Y[i,0]] = 1
    return target

Testujemy:

X,Y = gen(5)
print(Y)
py.scatter(X[:,0],X[:,1],c = Y,s = 10)
target = convert_to_target(Y)
print(target)

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. 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:

N_przykladow =37
X,   Y      = gen(N_przykladow) # przykłady do ciągu uczącego
X_m, Y_m    = gen(N_przykladow) # przykłady do ciągu monitorującego
py.figure()
py.scatter(X[:,0],X[:,1],c = Y,s = 10)
py.scatter(X_m[:,0],X_m[:,1],c = Y_m, s = 10, marker = 's')

Warto przekodować te dane tak aby jedna klasa była reprezentowana przez jeden neuron wyjściowy:

target = convert_to_target(Y)
target_m = convert_to_target(Y_m)


Teraz wytworzymy sieć. Rozmiar wejścia i wyjścia muszą się zgadzać z rozmiarami danych wejściowych i wyjściowych, odpowiednio.

class siec(object):
    def __init__(self, X, Y, N_hid=3):
        self.X = X
        self.Y = Y
        self.N_wej = X.shape[1] 
        self.N_wyj = Y.shape[1]
        self.N_hid = N_hid
 
        # inicjujemy połączenia
        # wagi ułożone są tak, że w kolejnych wierszach są kolejne neurony 
        # a w kolumnach wagi od konkretnego neuronu 
        # to +1 jest wagą dla obciążenia
        self.w_1 = (2*np.random.random((self.N_hid, self.N_wej+1)) - 1)/self.N_wej # pomiędzy warstwą pierwszą (wejściem) a warstwą ukrytą
        self.w_2 = (2*np.random.random((self.N_wyj, self.N_hid+1)) - 1)/self.N_hid
        self.dw1 = np.zeros((self.N_hid, self.N_wej+1))
        self.dw2 = np.zeros((self.N_wyj, self.N_hid+1)) 
 
    def g1(self, x):
        f = np.exp(-x) 
        f[f>1e8]=1e8
        y = 1./(1+f)
        return y   
    def g1_prim(self, x):
        y = x*(1-x)
        return y
    def g2(self, x):
        y = x
        return y   
    def g2_prim(self, x):
        y = 1
        return y
    def get_params(self):
        return np.concatenate((self.w_1.reshape(-1), self.w_2.reshape(-1)))
 
    def predict(self, x):
        # propagacja "w przód"
        self.a_0 = np.vstack((1,x))  # z warstwy wejściowej (zerowej) wychodzi a_0
        z_1 = np.dot( self.w_1, self.a_0 )# na warstwe 1 wchodzą iloczyny skalarne 
        self.a_1 = np.vstack((1,self.g1(z_1))) # dokładamy 1 i dostaję wyjście z warstwy 1
        z_2 = np.dot( self.w_2, self.a_1 ) # na warstwe 3 wchodzą iloczyny skalarne 
        self.a_2 = self.g2(z_2)
        return self.a_2
    
    def get_error(self,X,trget):
        self.bl = 0
        for i in range(0,self.X.shape[0]):
            # weźmy przykład i-ty        
            x = self.X[i,:].reshape(self.N_wej,1)
            y = self.Y[i,:].reshape(self.N_wyj,1)
            self.a_2 = self.predict(x)
 
            # delta
            d_2 = (self.a_2 - y)*self.g2_prim(self.a_2)
         
            self.bl += np.dot(d_2.T,d_2)/self.X.shape[0]
        return self.bl
        
    def fit_one_step(self, eta1,eta2):
        self.bl = 0
        D_1 = np.zeros((self.N_hid, self.N_wej+1))
        D_2 = np.zeros((self.N_wyj, self.N_hid+1))
        for i in range(0,self.X.shape[0]):
            # weźmy przykład i-ty        
            x = self.X[i,:].reshape(self.N_wej,1)
            y = self.Y[i,:].reshape(self.N_wyj,1)
            self.a_2 = self.predict(x)
 
            # propagacja "wstecz"
            d_2 = (self.a_2 - y)*self.g2_prim(self.a_2)
            d_1 = np.dot(self.w_2.T, d_2) * self.g1_prim(self.a_1)#z_2
 
            # akumulujemy poprawki 
            D_2 +=  np.dot( d_2, self.a_1.T)
            D_1 +=  np.dot( d_1[1:], self.a_0.T)
 
            self.bl += np.dot(d_2.T,d_2)/self.X.shape[0]
        # uaktualniamy wagi
        self.w_1 -=  eta1*D_1 + eta2*self.dw1
        self.w_2 -=  eta1*D_2+  eta2*self.dw2
        self.dw1  =  eta1*D_1 
        self.dw2  =  eta1*D_2   
        return self.bl


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 3 domyślnych (sigmoidalnych) neuronów w warstwie ukrytej:

S = siec( X, target, N_hid= 3)

Przygotowujemy parametry:

# liczba epok uczenia
N_epochs = 150
eta1 = 0.005
eta2 = 0.9


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.

# inicjuję tablice na ewolucje

err  = np.zeros(N_epochs) #tablica na błąd zbioru uczącego
err_m  = np.zeros(N_epochs) #tablica na błąd zbioru monitorującego
wagi = np.zeros((N_epochs,len(S.get_params()))) #tablica na wagi


py.figure()
for cykl in range(N_epochs):
    err[cykl] = S.fit_one_step(eta1,eta2) # wykonaj krok uczenia
    err_m[cykl] = S.get_error(X_m,target_m)# normalizuję aby uzyskać średni błąd kwadratowy
    wagi[cykl,:] = S.get_params() #pobieram wagi do zapamiętania

Narysujmy co działo się z błędem i wagami:

#  rysunki
py.figure()
py.subplot(2,1,1) # błędów
py.semilogy(err,'b',label='zb. uczacy')
py.semilogy(err_m,'r',label='zb. monitorujacy')
py.title(u'błąd')
py.legend()
#py.ylim([0,1]) 
py.subplot(2,1,2) #wag
py.plot(wagi)
py.title('wagi')
py.ylim([-3,3])

Rysunki dodatkowe

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.

ticks = arange(-3.,6.,0.2)
X, Y = meshgrid(ticks, ticks)


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 = ...

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.

    py.figure()
   
    
    hold(True) # włączamy opcję dopisywania do bieżącego rysunku
    for c in [0,1,2]:  # iterujemy się przez możliwe klasy
        here, _ = ...       # wybieramy indeksy punktów testowych należących do klasy c
        ... # 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ą
    py.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ę