Dane treningowe

Ponieważ będziemy potrzebowali na czymś wytrenować naszą sieć neuronową skorzystamy z popularnego zbioru w Machine Learningu czyli MNIST. Zbiór ten zawiera ręcznie pisane cyfry od 0 do 9. Są to niewielkie obrazki o wielkości 28x28 pixeli.

Pobierzmy i załadujmy zbiór.


In [ ]:
# skorzystamy z gotowej funkcji do pobrania tego zbioru
from sklearn.datasets import fetch_mldata
mnist = fetch_mldata('MNIST original')

Zaimportujmy dodatkowe biblioteki do wyświetlania wykresów/obrazów oraz numpy który jest paczką do obliczeń na macierzach


In [ ]:
import matplotlib.pyplot as plt
import seaborn as sns  # można osobno doinstalować tą paczke (rysuje ładniejsze wykresy)

import numpy as np

# pozwala na rysowanie w notebooku (nie otwiera osobnego okna)
%matplotlib inline

Sprawdźmy ile jest przykładów w zbiorze


In [ ]:
print(mnist.data.shape)  # 28x28
print(mnist.target.shape)

Teraz wyświetlmy pare przykładowych obrazków ze zbioru


In [ ]:
for i in range(10):
    r = np.random.randint(0, len(mnist.data))
    plt.subplot(2, 5, i + 1)
    plt.axis('off')
    plt.title(mnist.target[r])
    plt.imshow(mnist.data[r].reshape((28, 28)))
plt.show()

Tworzenie sieci neuronowej

Żeby za bardzo nie komplikować sprawy stworzymy sieć o trzech warstwach. Na początku ustalmy ilość neuronów w każdej z warstw. Ponieważ wielkość obrazka to 28x28 pixeli potrzebujemy więc 784 neuronów wejściowych. W warstwie ukrytej możemy ustawić ilość na dowolną. Ponieważ mamy do wyboru 10 różnych cyfr tyle samo neuronów damy w warstwie wyjściowej.


In [ ]:
input_layer = 784
hidden_layer = ...
output_layer = 10

Kluczowym elementem sieci neuronowych są ich wagi na połączeniach między neuronami. Aktualnie po prostu wczytamy już wytrenowane wagi dla sieci.


In [ ]:
# wcztanie już wytrenowanych wag (parametrów)
import h5py
with h5py.File('weights.h5', 'r') as file:
    W1 = file['W1'][:]
    W2 = file['W2'][:]

In [ ]:
def sigmoid(x):
    pass

Obliczenia wykonywane przez sieć neuronową można rozrysować w postaci grafu obliczeniowego, gdzie każdy z wierzchołków reprezentuje jakąś operację na wejściach. Wykorzystywana przez nas sieć przedstawiona jest na grafie poniżej (@ to mnożenie macierzy):


In [ ]:
def forward_pass(x, w1, w2):
    # x - wejście sieci
    # w1 - parametry warstwy ukrytej
    # w2 - parametry warstwy wyjściowej
    pass

In [ ]:
# uruchomienie sieci i sprawdzenie jej działania
# użyj funkcji forward_pass dla kilku przykładów i zobacz czy sieć odpowiada poprawnie

Trenowanie sieci (Back-propagation)

Należy przygotować dane pod trenowanie sieci. Chodzi tu głównie o zakodowanie mnist.target w sposób 'one-hot encoding'. Czyli: $$y = \left[ \begin{matrix} 0 \\ 1 \\ 2 \\ \vdots \\ 8 \\ 9 \end{matrix} \right] \Longrightarrow \left[ \begin{matrix} 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 1 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\ \vdots & \vdots & \vdots & \vdots & \vdots & \vdots & \vdots & \vdots & \vdots & \vdots \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 0 & 1 \end{matrix} \right]$$

Uwaga: aktualnie wszystkie dane są posortowane względem odpowiedzi. Czyli wszystkie zera są na początku póżniej są jednyki, itd. Takie ustawienie może w znaczący sposób utrudnić trenowanie sieci. Dlatego należy dane na starcie "przetasować". Trzeba przy tym pamiętać, żeby wejścia dalej odpowiadały tym samym wyjściom.


In [ ]:
x_train = ...
y_train = ...

Na starcie parametry są zwyczajnie losowane. Wykorzystamy do tego funkcje np.random.rand(dim_1, dim_2, ..., dim_n) losuje ona liczby z przedziału $[0, 1)$ i zwraca tensor o podanych przez nas wymiarach.

Uwaga: Mimo, że funkcja zwraca liczby z przedziału $[0, 1)$ nasze startowe parametry powinny być z przedziału $(-0.01, 0.01)$


In [ ]:
W1 = ...
W2 = ...

Implementacja propagacji wstecznej

Podobnie jak przy optymalizowaniu funkcji, do wyliczenia gradientów wykorzystamy backprop. Graf obliczeniowy jest trochę bardziej skomplikowany. (@ oznacza mnożenie macierzy)

Do zaimplementowania funkcji back_prop(...) będziemy jeszcze potrzebować pochodnych dla naszych funkcji oraz funkcje straty.


In [ ]:
def loss_func(y_true, y_pred):
    # y_true - poprawna odpowiedź
    # y_pred - odpowiedź wyliczona przez sieć neuronową
    pass

In [ ]:
def sigmoid_derivative(x):
    # implementacja
    pass

In [ ]:
def loss_derivative(y_true, y_pred):
    # y_true - poprawna odpowiedź
    # y_pred - odpowiedź wyliczona przez sieć neuronową
    pass

In [ ]:
def back_prop(x, y, w1, w2):
    # x - wejście sieci
    # y - poprawne odpowiedzi
    # w1 - parametry warstwy ukrytej
    # w2 - parametry warstwy wyjściowej
    
    # zastąp linie pod spodem kodem z funkcji forward_pass
    # >>>
    ...
    # <<<

    ...
    
    return loss, dw1, dw2

Napiszemy jeszcze funkcje, która będzie wykonywała jeden krok optymalizacji dla podanych parametrów i ich gradientów o podanym kroku.


In [ ]:
def apply_gradients(w1, w2, dw1, dw2, learning_rate):
    # w1 - parametry warstwy ukrytej
    # w2 - parametry warstwy wyjściowej
    # dw1 - gradienty dla parametrów warstwy ukrytej
    # dw2 - gradienty dla parametrów warstwy wyjściowej
    # learning_rate - krok optymalizacji
    
    ...
    
    return w1, w2

Żeby móc lepiej ocenić postęp uczenia się sieci napiszemy funkcje, która będzie wyliczać jaki procent odpowiedzi udzielanych przez sieć neuronową jest poprawny.


In [ ]:
def accuracy(x, y, w1, w2):
    # x - wejście sieci
    # y - poprawne odpowiedzi
    # w1 - parametry warstwy ukrytej
    # w2 - parametry warstwy wyjściowej
    
    # hint: użyj funkcji forward_pass i np.argmax
    pass

W końcu możemy przejść do napisania głównej pętli uczącej.


In [ ]:
nb_epoch = 5  # ile razy będziemy iterować po danych treningowych
learning_rate = 0.001
batch_size = 16  # na jak wielu przykładach na raz będziemy trenować sieć

In [ ]:
losses = []
for epoch in range(nb_epoch):
    print('\nEpoch %d' % (epoch,))
    for i in range(0, len(x_train), batch_size):
        x_batch = ...
        y_batch = ...

        # wykonaj back_prop dla pojedynczego batch'a
        ...
        
        # zaktualizuj parametry
        ...
        
        losses.append(loss)
        print('\r[%5d/%5d] loss: %8.6f - accuracy: %10.6f' % (i + 1, len(x_train),
                  loss, accuracy(x_batch, y_batch, W1, W2)), end='')
        
plt.plot(losses)
plt.show()

In [ ]:
print('Dokładność dla całego zbioru:', accuracy(x_train, y_train, W1, W2))