<jupyter_start><jupyter_text>Семинар 1 "Полносвязные нейронные сети" ФИО: <NAME> В этом задании вам предстоит реализовать проход "вперед" для полносвязной нейронной сети. В дальнейшем мы реализуем процедуру обучения и научим сеть распознавать рукописные цифры.На первой лекции мы познакомились с тем, что такое нейронные сети и изучили три слоя — линейный, сигмоида и SoftMax. Из этих слоев можно составлять глубокие архитектуры и обучать их при помощи градиентного спуска. Чтобы конструировать сложные архитектуры, можно реализовать каждый тип слоя как отдельный "кирпичик" и затем собирать полную архитектуру как конструктор. Это мы и попробуем сделать на первом и втором семинарах. Каждый тип слоя мы будем реализовывать при помощи класса, который будет поддерживать три функции: forward, которая будет применять функцию, реализуемую слоем, к входной матрице и backward, которая будет вычислять градиенты и step, которая будет обновлять веса. Чтобы не применять функцию к каждому объекту в отдельности, мы будем подавать на вход слою матрицу размера (N, d), где N — количество объектов, а d — размерность каждого объекта.  Функция forward будет вычислять по $x$ значение $y$, backward — по $\frac{\partial L}{\partial y}$ вычислять $\frac{\partial L}{\partial x}$ и обновлять внутри себя $\frac{\partial L}{\partial w}$.Важным требованием к реализации является векторизация всех слоев: все операции должны быть сведены к матричным, не должно быть циклов. Это значительно уменьшает временные затраты.<jupyter_code>import matplotlib.pyplot as plt
import numpy as np
%matplotlib inline<jupyter_output><empty_output><jupyter_text>Часть 1: Линейный слой Приведем пример вычисления градиентов для линейного слоя: $y = xW^T$, $x \in \mathbb{R}^{K \times n}$, $y \in \mathbb{R}^{K \times n}$, $W \in \mathbb{R}^{m \times n}$, где $K$ — число объектов.Рассмотрим $L$ как функцию от выходов нейронной сети: $L = L(y_{11}, y_{12}, \dots)$$$y_i = (Wx)_i = \sum_j W_{i, j}x_j$$$$\frac{\partial L}{\partial x_{ij}} = \sum_{kt} \frac{\partial L}{\partial y_{kt}}\frac{\partial y_{kt}}{\partial x_{ij}} = \sum_{kt} \frac{\partial L}{\partial y_{kt}}\frac{\partial \sum_z x_{kz}w_{zt}}{\partial x_{ij}}= \sum_{t} \frac{\partial L}{\partial y_{it}}\frac{\partial w_{jt}}{\partial x_{ij}}$$$$\frac{\partial{L}}{\partial x} = \frac{\partial{L}}{\partial y}W$$<jupyter_code>class Linear:
    def __init__(self, input_size, output_size):
        '''
        Creates weights and biases for linear layer.
        Dimention of inputs is *input_size*, of output: *output_size*.
        '''
        #### YOUR CODE HERE
        #### Create weights, initialize them with samples from N(0, 0.1).
        
        self.W = np.random.randn(input_size, output_size) * 0.1
        self.b = np.zeros(output_size)
        
        pass
    
    def forward(self, X):
        '''
        Passes objects through this layer.
        X is np.array of size (N, input_size).
        Returns output of size (N, output_size).
        Hint: You may need to store X for backward pass
        '''
        #### YOUR CODE HERE
        #### Apply layer to input
        
        self.X = X
        self.Y = X @ self.W + self.b
        
        return self.Y
    
    def backward(self, dLdy):
        '''
        1. Compute dLdw and dLdx.
        2. Store dLdw for step() call
        3. Return dLdx
        '''
        #### YOUR CODE HERE
        
        self.dLdW = self.X.T @ dLdy
        self.dLdb = dLdy.sum(0)
        self.dLdx = dLdy @ self.W.T
        return self.dLdx
    
    def step(self, learning_rate):
        '''
        1. Apply gradient dLdw to network:
        w <- w - l * dLdw
        '''
        #### YOUR CODE HERE
        self.W = self.W - learning_rate * self.dLdW
        self.b = self.b - learning_rate * self.dLdb
lin = Linear(10, 2)
X = np.ones((5, 10))
y = lin.forward(X)
dLdy = np.ones((5, 2))
lin.backward(dLdy).shape, lin.dLdW.shape, lin.dLdb.shape<jupyter_output><empty_output><jupyter_text>Часть 2: Численный градиент Реализуйте функцию проверки численного градиента. Для этого для каждой переменной, по которой считается градиент, надо вычислить численный градиент: $f'(x) \approx \frac{f(x+\epsilon)-f(x-\epsilon)}{2\epsilon}$. Функция должна возвращать максимальное абсолютное отклонение аналитического градиента от численного. В качестве $\epsilon$ рекомендуется взять $10^{-6}$. При правильной реализации максимальное отличие будет иметь порядок $10^{-8}-10^{-6}$.<jupyter_code>def check_gradient(func, X, gradient):
    '''
    Computes numerical gradient and compares it with analytcal.
    func: callable, function of which gradient we are interested. Example call: func(X)
    X: np.array of size (n x m)
    gradient: np.array of size (n x m)
    Returns: maximum absolute deviation between numerical gradient and analytical.
    '''
    #### YOUR CODE HERE
    numerical_gradient = np.zeros(X.shape)
    eps = 1e-6
    for i in range(X.shape[0]):
        for j in range(X.shape[1]):
            X[i, j] += eps
            y1 = func(X)
            X[i, j] -= 2 * eps
            y2 = func(X)
            X[i, j] += eps
            numerical_gradient[i, j] = (y1 - y2) / 2 / eps
    
    print (np.abs(numerical_gradient - gradient).max())<jupyter_output><empty_output><jupyter_text>Протестируйте линейный слой при помощи реализованной функции check_gradient: $\frac{\partial L}{\partial x}$ и $\frac{\partial L}{\partial w}$.<jupyter_code>#### YOUR CODE HERE
func = lambda x: (x ** 2).sum()
np.random.seed(777)
x = np.random.rand(10, 20)
gradient = 2 * x
check_gradient(func, x, gradient)
lin = Linear(10, 2)
X = np.ones((5, 10))
y = lin.forward(X)
func = lambda x: lin.forward(x).sum()
dLdy = np.ones((5, 2))
gradient = lin.backward(dLdy)
check_gradient(func, X, gradient)
lin = Linear(10, 2)
X = np.ones((5, 10))
y = lin.forward(X)
def func(W):
    W_old = lin.W
    lin.W = W
    y = lin.forward(X).sum()
    lin.W = W_old
    return y

dLdy = np.ones((5, 2))
W = lin.W
lin.backward(dLdy)
gradient = lin.dLdW
check_gradient(func, W, gradient)<jupyter_output>1.4377832258105627e-10<jupyter_text>Часть 3: Сигмоида<jupyter_code>class Sigmoid:
    def __init__(self):
        pass
    
    def forward(self, X):
        '''
        Passes objects through this layer.
        X is np.array of size (N, d)
        '''
        #### YOUR CODE HERE
        #### Apply layer to input
        self.s = 1. / (1 + np.exp(-X))
        return self.s
    
    def backward(self, dLdy):
        '''
        1. Compute dLdx = dLdy * dydx.
        2. Return dLdx
        '''
        #### YOUR CODE HERE
        return self.s * (1 - self.s) * dLdy
    
    def step(self, learning_rate):
        pass<jupyter_output><empty_output><jupyter_text>Протестируйте слой при помощи реализованной функции check_gradient: $\frac{\partial L}{\partial x}$.<jupyter_code>#### YOUR CODE HERE
X = np.ones((5, 10))
s = Sigmoid()
def func(X):
    return s.forward(X).sum()

dLdy = np.ones((5, 10))
s.forward(X)
gradient = s.backward(dLdy)
check_gradient(func, X, gradient)<jupyter_output>1.1730462157188981e-09<jupyter_text>Часть 4: Функция потерь Как мы увидели на семинаре, вычисление производной для связки SoftMax + Negative log-likelihood проще чем для этих двух слоев по отдельности. Поэтому мы реализуем их как один класс. Важное замечание: на проходе "вперед" важно воспользоваться трюком log-sum-exp, чтобы не столкнуться с численными неустойчивостями.<jupyter_code>np.random.seed(777)
a = np.random.rand(10, 4)
b = np.random.randint(0, 4, 10)
class NLLLoss:
    def __init__(self):
        '''
        Applies Softmax operation to inputs and computes NLL loss
        '''
        pass
    
    def forward(self, X, y):
        '''
        Passes objects through this layer.
        X is np.array of size (N, C), where C is the number of classes
        y is np.array of size (N), contains correct labels
        '''
        self.p = np.exp(X)
        self.p /= self.p.sum(1, keepdims=True)
        self.y = np.zeros((X.shape[0], X.shape[1]))
        self.y[np.arange(X.shape[0]), y] = 1
        return - (np.log(self.p) * self.y).sum(1).mean(0)
    
    def backward(self):
        '''
        Note that here dLdy = 1 since L = y
        1. Compute dLdx
        2. Return dLdx
        '''
        return (self.p - self.y) / self.y.shape[0]<jupyter_output><empty_output><jupyter_text>Протестируйте слой при помощи реализованной функции check_gradient: $\frac{\partial L}{\partial x}$.<jupyter_code>#### YOUR CODE HERE
np.random.seed(777)
X = np.random.rand(15, 4)
y = np.random.randint(0, 4, 15)
loss = NLLLoss()
loss.forward(X, y)
func = lambda x: loss.forward(x, y)
check_gradient(func, X, loss.backward())<jupyter_output>1.8305948423780194e-10<jupyter_text>Часть 5, нейронная сеть Теперь, когда у нас есть "кирпичики", мы можем написать класс, который будет собирать всю сеть вместе<jupyter_code>class NeuralNetwork:
    def __init__(self, modules):
        '''
        Constructs network with *modules* as its layers
        '''
        self.modules = modules
    
    def forward(self, X):
        y = X
        for i in range(len(self.modules)):
            y = self.modules[i].forward(y)
        return y
    
    def backward(self, dLdy):
        '''
        dLdy here is a gradient from loss function
        '''
        for i in range(len(self.modules))[::-1]:
            dLdy = self.modules[i].backward(dLdy)
    
    def step(self, learning_rate):
        for i in range(len(self.modules)):
            self.modules[i].step(learning_rate)<jupyter_output><empty_output><jupyter_text>Часть 6, обучение на простых данных<jupyter_code>data = np.load('data.npz')
X, y = data['arr_0'], data['arr_1']
plt.scatter(X[:, 0], X[:, 1], c=y)
plt.axis('equal')
plt.show()<jupyter_output><empty_output><jupyter_text>Обучите архитектуру вида 2 -> 10 -> 10 -> 3:* Linear(2, 10)* Sigmoid()* Linear(10, 10)* Sigmoid()* Linear(10, 3)В качестве функции потерь используйте NLLLoss.1. Создайте сеть, в цикле запускайте forward, backward, step (используйте learning rate 0.005). 2. Нарисуйте график сходимости (величина NLL после каждого обновления).3. Нарисуйте разделяющую поверхность4. Попробуйте подобрать темп обучения. Как меняется сходимость?<jupyter_code>arc = [Linear(2, 3), Sigmoid(), Linear(3, 3)]
network = NeuralNetwork(arc)
loss_func = NLLLoss()
losses = []
for i in range(16000):
    prediction = network.forward(X)
    loss = loss_func.forward(prediction, y)
    losses.append(loss)
    dLdy = loss_func.backward()
    network.backward(dLdy)
    network.step(0.1)
plt.plot(losses)
N = 50
x_grid = np.linspace(-0.2, 1.2, N)
y_grid = np.linspace(0, 1, N)
X_mesh, Y_mesh = np.meshgrid(x_grid, y_grid)
mesh = np.dstack((X_mesh, Y_mesh)).reshape(N * N, 2)
predictions = np.argmax(network.forward(mesh), 1)
plt.scatter(mesh[:, 0], mesh[:, 1], c=predictions, alpha=0.2)
plt.scatter(X[:, 0], X[:, 1], c=y)
plt.axis('equal')
plt.show()<jupyter_output><empty_output><jupyter_text>Отличная визуализация: http://playground.tensorflow.org/  Домашнее задание: * Подберите темп обучения. Как меняется сходимость? Нарисуйте график оптимального значения функции потерь для различных значений learning_rate* Решите поставленную выше задачу как задачу регрессии с MSE. Изменилась ли разделяющая поверхность?<jupyter_code>### YOUR CODE HERE:
import copy

def train_network(arc, loss_func, learning_rate, nsteps):
    netarc = copy.deepcopy(arc)
    network = NeuralNetwork(netarc)
    
    losses = []
    for i in range(nsteps):
        prediction = network.forward(X)
        loss = loss_func.forward(prediction, y)
        losses.append(loss)
        dLdy = loss_func.backward()
        network.backward(dLdy)
        network.step(learning_rate)

    return network, losses   
arc = [Linear(2, 3), Sigmoid(), Linear(3, 3)]
loss_func = NLLLoss()
learning_rates = np.logspace(-3, 1, num=5)
n_steps = 16000

for learning_rate in learning_rates:
    _, losses = train_network(arc, loss_func, learning_rate, n_steps)
    plt.plot(losses, label=str(learning_rate))
plt.legend()<jupyter_output><empty_output><jupyter_text>Видно, что при learning_rate = 1 обучается быстрее и стабильнее всего (при learning_rate = 10 обучается достаточно быстро, но очень нестабильно)<jupyter_code>learning_rate = 1
n_steps = 10000

network, _ = train_network(arc, loss_func, learning_rate, n_steps)        
predictions = np.argmax(network.forward(mesh), 1)

plt.scatter(mesh[:, 0], mesh[:, 1], c=predictions, alpha=0.2)
plt.scatter(X[:, 0], X[:, 1], c=y)
plt.axis('equal')
plt.title('learing_rate {}'.format(learning_rate))
plt.show()
class MSELoss:
    def __init__(self):
        pass
    
    def forward(self, X, y):
        self.shapeX = X.shape
        self.X = X.reshape(-1)
        self.y = y
        self.n_objects = y.size
        return ((self.X - self.y) ** 2).sum() * 0.5 / self.n_objects
    
    def backward(self):
        return (self.X - self.y).reshape(self.shapeX) / self.n_objects
arc = [Linear(2, 3), Sigmoid(), Linear(3, 1)]
loss_func = MSELoss()
learning_rates = np.logspace(-4, 0, num=5)
n_steps = 16000

for learning_rate in learning_rates:
    _, losses = train_network(arc, loss_func, learning_rate, n_steps)
    plt.plot(losses, label=str(learning_rate))
plt.legend()<jupyter_output><empty_output><jupyter_text>Наиболее стабильные и быстрые результаты получаются при learning_rate 0.1.<jupyter_code>learning_rate = 0.1
n_steps = 10000

network, _ = train_network(arc, loss_func, learning_rate, n_steps)        
predictions = np.round(network.forward(mesh).reshape(-1), 0)
predictions[predictions > 2] = 2
predictions[predictions < 0] = 0

plt.scatter(mesh[:, 0], mesh[:, 1], c=predictions, alpha=0.2)
plt.scatter(X[:, 0], X[:, 1], c=y)
plt.axis('equal')
plt.title('learing_rate {}'.format(learning_rate))
plt.show()<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.1301042571676803, 'fraction_numerical': 0.021285838401390096, 'mean_word_length': 3.3574132492113566, 'pattern_counts': {'":': 0, '<': 57, '<?xml version=': 0, '>': 59, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '14', 'dir': 'jupyter-structured-clean-dedup', 'id': '393828', 'license': 'MIT', 'n_tokens_mistral': 5401, 'n_tokens_neox': 5470, 'n_words': 1334, 'path': 'Dokukin_Practice task 1, Back propagation.ipynb', 'repo_name': 'sergy2710/spheremail_nn', 'size': '13834'}