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()
Ż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
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 = ...
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))