<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 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.
Step1:  Zaimportujmy dodatkowe biblioteki do wyświetlania wykresów/obrazów oraz numpy który jest paczką do obliczeń na macierzach
Step2:  Sprawdźmy ile jest przykładów w zbiorze
Step3:  Teraz wyświetlmy pare przykładowych obrazków ze zbioru
Step4:  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.
Step5:  Kluczowym elementem sieci neuronowych są ich wagi na połączeniach między neuronami. Aktualnie po prostu wczytamy już wytrenowane wagi dla sieci.
Step6:  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)
Step7:  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
Step8:  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
Step9:  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.
Step10:  Napiszemy jeszcze funkcje, która będzie wykonywała jeden krok optymalizacji dla podanych parametrów i ich gradientów o podanym kroku.
Step11:  Ż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.
Step12:  W końcu możemy przejść do napisania głównej pętli uczącej.
<ASSISTANT_TASK:>
Python Code:
# skorzystamy z gotowej funkcji do pobrania tego zbioru
from sklearn.datasets import fetch_mldata
mnist = fetch_mldata('MNIST original')
Explanation: 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.
End of explanation
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
Explanation: Zaimportujmy dodatkowe biblioteki do wyświetlania wykresów/obrazów oraz numpy który jest paczką do obliczeń na macierzach
End of explanation
print(mnist.data.shape)  # 28x28
print(mnist.target.shape)
Explanation: Sprawdźmy ile jest przykładów w zbiorze
End of explanation
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()
Explanation: Teraz wyświetlmy pare przykładowych obrazków ze zbioru
End of explanation
input_layer = 784
hidden_layer = 128
output_layer = 10
Explanation: 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.
End of explanation
# wcztanie już wytrenowanych wag (parametrów)
import h5py
with h5py.File('weights.h5', 'r') as file:
    W1 = file['W1'][:]
    W2 = file['W2'][:]
def sigmoid(x):
    return 1. / (1. + np.exp(-x))
Explanation: Kluczowym elementem sieci neuronowych są ich wagi na połączeniach między neuronami. Aktualnie po prostu wczytamy już wytrenowane wagi dla sieci.
End of explanation
def forward_pass(x, w1, w2):
    # x - wejście sieci
    # w1 - parametry warstwy ukrytej
    # w2 - parametry warstwy wyjściowej
    
    z1 = x @ w1
    h1 = sigmoid(z1)
    
    z2 = h1 @ w2
    h2 = sigmoid(z2)
    
    return h2
# uruchomienie sieci i sprawdzenie jej działania
# użyj funkcji forward_pass dla kilku przykładów i zobacz czy sieć odpowiada poprawnie
sample = np.random.choice(len(mnist.data), size=(10,), replace=False)
x_ex = mnist.data[sample]
y_ex = mnist.target[sample]
nn_ans = forward_pass(x_ex, W1, W2)
for i in range(10):
    plt.subplot(2, 5, i + 1)
    plt.axis('off')
    plt.title(np.argmax(nn_ans[i]))
    plt.imshow(x_ex[i].reshape((28, 28)))
plt.show()
Explanation: 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):
End of explanation
x_train = mnist.data
y_train = np.eye(10)[mnist.target.astype(np.int32)]
shuffle = np.random.permutation(len(x_train))
x_train = x_train[shuffle]
y_train = y_train[shuffle]
Explanation: 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.
End of explanation
W1 = 0.01 * (2. * np.random.rand(input_layer, hidden_layer) - 1.)
W2 = 0.01 * (2. * np.random.rand(hidden_layer, output_layer) - 1.)
Explanation: 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)$
End of explanation
def loss_func(y_true, y_pred):
    # y_true - poprawna odpowiedź
    # y_pred - odpowiedź wyliczona przez sieć neuronową
    
    return np.mean(np.square(y_true - y_pred))
def sigmoid_derivative(x):
    # implementacja
    return x * (1. - x)
def loss_derivative(y_true, y_pred):
    # y_true - poprawna odpowiedź
    # y_pred - odpowiedź wyliczona przez sieć neuronową
    
    return -2. * (y_true - y_pred) / len(y_pred)
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
    # >>>
    z1 = x @ w1
    h1 = sigmoid(z1)
    
    z2 = h1 @ w2
    h2 = sigmoid(z2)
    # <<<
    
    loss = loss_func(y, h2)
    
    # backprop: loss = loss_func(y, h2)
    dh2 = loss_derivative(y, h2)
    
    # backprop: h2 = sigmoid(z2)
    dz2 = sigmoid_derivative(h2) * dh2
    
    # backprop: z2 = h1 @ w2
    dh1 = dz2 @ w2.T
    dw2 = h1.T @ dz2
    
    # backprop: h1 = sigmoid(z1)
    dz1 = sigmoid_derivative(h1) * dh1
    
    # backprop: z1 = x @ w1
    dx = dz1 @ w1.T
    dw1 = x.T @ dz1
    
    return loss, dw1, dw2
Explanation: 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.
End of explanation
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
    
    w1 -= learning_rate * dw1
    w2 -= learning_rate * dw2
    
    return w1, w2
Explanation: Napiszemy jeszcze funkcje, która będzie wykonywała jeden krok optymalizacji dla podanych parametrów i ich gradientów o podanym kroku.
End of explanation
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
    pred = forward_pass(x, w1, w2)
    return np.sum(np.isclose(np.argmax(pred, axis=1), np.argmax(y, axis=1))) / len(x)
Explanation: Ż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.
End of explanation
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ć
losses = []
for epoch in range(nb_epoch):
    print('\nEpoch %d' % (epoch,))
    for i in range(0, len(x_train), batch_size):
        x_batch = x_train[i:i+batch_size]
        y_batch = y_train[i:i+batch_size]
        # wykonaj back_prop dla pojedynczego batch'a
        loss, dw1, dw2 = back_prop(x_batch, y_batch, W1, W2)
        
        # zaktualizuj parametry
        W1, W2 = apply_gradients(W1, W2, dw1, dw2, learning_rate)
        
        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()
print('Dokładność dla całego zbioru:', accuracy(x_train, y_train, W1, W2))
Explanation: W końcu możemy przejść do napisania głównej pętli uczącej.
End of explanation
<END_TASK>