Uczenie maszynowe i sztuczne sieci neuronowe/Ćwiczenia 7

Z Brain-wiki

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ę