# ДЗ №2 - обучение модели трехслойного перцептрона методом градиентного спуска

## Задача 1
В качестве теоретического задания в этом ДЗ предлагается провести вывод функции ошибки для задачи регрессии в предположении, что целевая переменная подчиняется распределению Лапласа. Также предлагается воспользоваться байесовским выводом и в том же предположении относительно распределения целевой переменной вывести форму функции потерь с условием лапласовского априорного распределения параметров модели.

Решение задачи 1

Пусть целевая переменная имеет распределение Лапласа, то есть ее функция плотности задается, как $\rho(x|\mu, \sigma) = 	{\displaystyle {\frac {1}{2\sigma}}\exp \left(-{\frac {|x-\mu |}{\sigma}}\right)}$, где $\mu \in R_{+}$  и $\sigma \in R$ -  некоторые параметры. Также имеет место быть постановка предположения, что $y_{true} = y_{pred}(x, w) + \epsilon$, где $\epsilon$ - ошибка, имеющая распределение $Laplace(\mu_{\epsilon}, \sigma_{\epsilon})$, то тогда y_{true} также случайная величина, но имеющая распределение с параметрами $\mu = \mu_{\epsilon} + y_{pred}$ и $\sigma = \sigma_{\epsilon}$.

Пусть формально у нас есть какая-то выборка данных $Y$, которая состоит из некоторого набора элементов $\{y_i\}_{i=1}^n$, и, предполагая, что данные у нас независимо и одинаково распределенные с распределением Лапласа, зависящие от некоторого, возможно многомерного, параметра $\theta$, можно вывести оптимальную оценку этого параметра. Ниже будет представлен вывод оценки через **оценку максимального правдоподобия** и **максимизацию апостериорного распределения**.

## Оценка максимального правдоподобия
Данный подход оценивает параметр $\theta$ через следующую цепочку рассуждений:
$$
\theta_{MLE} =
argmax(\rho(Y|\theta)) =
argmax(\rho(y_1, y_2...y_n|\theta)) =
argmax(\prod\limits_{i=1}^n\rho(y_i|\theta)) =
argmax(\sum\limits_{i=1}^n\log(\rho(y_i|\theta))),
$$
Соответственно, если $Y$ порождены распределением Лапласа, то при подстановке соответствующей функции плотности получим:

$$\theta_{MLE}= argmax(-n\log (2\sigma)-\frac{1}{\sigma}\sum_{t=1}^N |x_t-\mu - y^i_{pred}(\theta)|),$$ а так как экстремум ищется по $\theta$, то данная задача эквивалентна следующей (при условии также, что $\mu = 0$, а $\sigma = 1$:
$$\theta_{MLE} = argmin(\sum\limits_{i=1}^n|y_i - y^i_{pred}|),$$
что и является в чистом виде $MAE$.

## Оценка апостериорного максимума
Теперь помимо выше сделанных предположений о $Y$, также предположим, что $\theta \sim Laplace(\mu_{\theta}, \sigma_{\theta})$ (что есть априорным распределением).
Тогда, опираясь на метод максимизации апостериорного распределения будем искать $\theta_{MAP}$, как решение задачи:
$$\theta_{MAP} = argmax(\rho(\theta | Y)),$$
что в силу формулы Байеса  принимает вид:
$$\theta_{MAP} =
argmax(\rho(\theta | Y)) =
argmax(\frac{\rho(Y | \theta) \rho(\theta)}{  \rho(Y) }) =
argmax(\rho(Y | \theta) \rho(\theta)) =
argmax(log(\rho(Y | \theta)) + log(\rho(\theta))) =
argmax(\sum\limits_{i=1}^n log(\rho(y_i | \theta) + log(\rho(\theta)))
$$
Если брать во внимание сделанные предположения о $\theta$ и $Y$:
$$\theta_{MAP} =
argmax( -n\log (2\sigma)-\frac{1}{\sigma}\sum_{t=1}^N |x_t-\mu - y^i_{pred}(\theta)| -n\log (2\sigma_{\theta}) - \sum\limits_{i=1}^n |\theta_{i}|)
$$
что и при соответствующих парематрах модели (среднего и дисперсии), дает следующую задачу:
$$\theta_{MLE} = argmin(\sum\limits_{i=1}^n|y_i - y^i_{pred}| + \sum\limits_{i=1}^n |\theta_{i}|),$$
а это ничто иное как $MAE$ с $L^1$ - регуляризацией.

## Задача 2
В этом ДЗ предлагается реализовать модель трехслойного перцептрона и обучение этой модели методом градиентного спуска.

На этот раз предлагается работать с реальными данными. Данные представляют из себя набор рукописных цифр. Это изображения размером 28х28. Каждому изображению поставлен в соответствие класс - арабская цифра. Задача модели - определить цифру, соответствующую произвольному изображению из тестового набора данных.

Так же, как и в ДЗ №1, предлагается реализовать функцию потерь и саму модель перцептрона в манере, схожей с построением модулей фреймворка pytorch.

В решении ожидается наличие следующих ключевых составляющих:

- (текст) формулировка задачи +
- (текст) формулировка признакового описания объектов +
- (текст, формулы) формулировка модели многослойного перцептрона +
- (текст, формулы) формулировка функции ошибки +
- (текст, формулы) формулировка меры качества модели +
- (текст, код и диаграммы) исследование исходных данных: распределение признаков и другие действия, дающие понимание о характере исходных  данных +
- (текст, код, диаграммы) фильтрация признаков (при необходимости), порождение признаков (при необходимости) +
- (формулы, код, результаты, коммментарии) обучение модели методом градиентного спуска +
- (код, результаты, комментарии) оценка качества модели на валидационной выборке +

#### Код решения:
(можно использовать предлагаемые шаблоны)
- формулировка модели трехслойного перцептрона. Имеется в виду только один скрытый слой; +
- формулировка функции ошибки; +
- формулировка метрики (метрик); +
- формулировка цикла оптимизации параметров. +

#### Визуализация в решении:
- распределение признаков; +
- распределение целевой переменной; +
- отдельные экземпляры выборки в виде изображений; +
- эволюция функции ошибки по ходу обучения; +
- эволюция метрики(метрик) по ходу обучения +

#### Выводы
- вывод о достаточности или избыточности данных для оценки параметров модели
- вывод о соотношении выразительности модели и ее обобщающей способности (наблюдаются ли явления переобучения или недообучения).

Примечание:

Реализация перцептрона и других составляющих исследования может быть написана только с использованием библиотеки Numpy или scipy. Решения с использованием библиотек автоматического вычисления градиентов не засчитываются.

### Исходные данные

Исходные данные можно скачать [по этой ссылке](https://www.dropbox.com/s/y6ar7i7mb6fvoed/mnist.npz). Набор данных MNIST поставляется в различных вариантах. В варианте, доступном по приведенной ссылке, чтение исходных данных может быть выполнено следующим образом:

```
import numpy as np
mnist = np.load('mnist.npz')
x_train = mnist['x_train']
y_train = mnist['y_train']
x_test = mnist['x_test']
y_test = mnist['y_test']
```

### Многослойный перцептрон

Напомним, мы задаем мнолослойный перцептрон как сложную функцию, в которой используются линейные операции и поточечные нелинейные преобразования. Если входные данные (признаковое описание объекта) заданы вектором $x$, то функция перцептрона с одним скрытым слоем выглядит следующим образом:
$$
F(x) = \Psi\left(\phi\left( {x}\cdot\theta_1 + b_1 \right)\cdot\theta_2 + b_2\right),
$$
где $x$ имеется в виду без дополнительного единичного признака; $\phi$ - функция активации скрытого слоя; $\Psi$ - функция активации выходного слоя перцептрона.

Напомним также, что в задаче жесткой многоклассовой классификации на $K$ классов допустим вариант формулировки модели, такой что:
- количество признаков целевой переменной совпадает с количеством классов $K$;
- в качестве функции активации $\Psi$ может использоваться `softmax`:
$$
\Psi(h_i) = \frac{e^{h_i}}{\sum_{j=1}^{K}{e^{h_j}}}
$$
- в качестве функции потерь может использоваться перекрестная энтропия в многоклассовом варианте (приведено в записи для одного объекта):
$$
{\mathscr{L}}\left(\hat{y},y\right) = -\sum_{j=1}^{K}{y_j*ln\left(\hat{y}_j\right)},
$$
где $\hat{y}=F(x)$

В своем решении вы никак не ограничены в выборе функций активации $\phi$ или $\Psi$. Однако есть некоторые устоявшиеся практики применения функций `ReLU, sigmoid, tanh`.

### Особенности реализации функции `softmax`
Несложно заметить, что как в числителе, так и в знаменателе функции `softmax` стоит экспонента некоторого числа. При этом следует понимать, что разрядность чисел с плавающей точкой `float32`, `float64` и даже `float128` не бесконечны. Свойства экспоненты таковы, что, например, для переполнения разрядности чисел `float64` (максимум  $\sim1.78*10^{308}$) достаточно показателя, превышающего 710, что совсем немного. Поэтому в случае практической реализации функции `softmax` имеет смысл предусмотреть случаи, когда аргументы экспоненты велики или, наоборот, слишком малы.

В этом ДЗ кроме прочих заданий вам нужно реализовать вычислительно стабильную версию `softmax`.

### Особенности вычисления градиента функции потерь

В этом домашнем задании, также как и в ДЗ№1 предлагается реализовывать функцию потерь и отдельные вычислительные блоки перцептрона наследующими `Differentiable` для общности восприятия этих модулей как дифференцируемых по своим аргументам. По желанию можно вычислить градиент функции потерь по параметрам модели вручную (и далее реализовать его в коде), однако предпочитаемым способом будет реализация градиента каждого из вычислительных блоков по аргументу в методе `backward()` и использование этого результата в обобщенном виде, без упрощения. Этот вариант вычисления градиента функции потерь по параметрам модели называется "backpropagation" ("метод обратного распространения ошибки" или "метод обратной волны" у разных авторов).

Нелишним будет напомнить, что в некоторых случаях для вычисления компоненты градиента необходимо хранить значения, полученные на этапе вычисления функции $F(x)$. В вашем решении это может быть устроено по-разному. Но для тех, кто хочет придерживаться предложенного шаблона, введен атрибут `state` класса `Differentiable`.

Решение задачи 2

# Теоретические сведения

## Общая постановка задачи
В данном задании требуется реализовать модель *трехслойного перцептрона*. В этом случае подразумевается, что первым слоем будет слой входных данных, затем один скрытый, который переходит в слой с ответом нашей модели.

Задача связана с *классификацией* картинок из датасета **MNIST**. Требуется настроить алгоритм, который будет оптимально каждой картинке 28х28 ставить в соответствие некоторое число, которое на ней изображено.

В общем виде, многослойный перцептрон можно определить следующим образом:

$$
\begin{equation}
    \begin{cases}
    h_1 = \psi_1(\theta_1^T \cdot \phi_1(x))
    \\
    h_2 = \psi_2(\theta_2^T  \cdot \phi_2(h_1))
    \\
    \vdots
    \\
    h_k = \psi_k(\theta_k^T \cdot \phi_k(h_{k-1}))
    \\
    y = \psi_{k+1}(\theta_{k+1}^T \cdot \phi_{k+1}(h_k))
    \end{cases}
\end{equation}
$$

где $x$ - объект (в нашем случае, это картинка с числом), $\phi_i$ - basic function (фактически, некоторые преобразования над данными, например, нормировка), $\theta_i$ - матрица параметров, по которым будет происходить оптимизация, $\psi_i$ - activation function (какая-то нелинейная функция, для того чтобы $y$ задававал в итоге нелинейное отображение), $y$ - предсказание целевой метки (то есть для данной задачи - это предположительное число на картинке)

Либо же в более человеческом виде:
$$
y = y(h_k(h_{k-1}(h_{k-2}(...h_1(x)))))
$$

То есть многослойный перцептрон - это всего лишь сложная функция.

В нашей задаче:
- функция имеет вид: $F(x) = \Psi\left(\phi\left( {x}\cdot\theta_1 + b_1 \right)\cdot\theta_2 + b_2\right)$

- $X$ - это множество векторов из $R^{784}$, то есть это "плоское" представление матриц 28х28 (то есть каждая строка этой матрицы конкатенируется с предыдущей и в итоге получается вектор размера 784).

- $Y$ - one-hot вектор длиной 10, где единица стоит на месте с координатой истинной метки, а предсказанием алгоритма является вектор из вероятнотстей принадлежности каждому клаассу. Далее определим, каким образом получить эти самые вероятности

Очень упрощенно, скорее с точностью до схемы, но не до чисел, нашу модель можно изобразить следующим образом (хотя реализация ниже позволяет использовать любое количество слоев):

## Функция активации на выходном слое и функция потерь
`SoftMax` - обобщение сигмоида на многоклассовую классификацию. Так как хочется интерпретировать ответ алгоритма как вероятность, то `SoftMax` с этим отлично справляется, так как определяется следующим образом:
$$
y_{pred} =
\begin{bmatrix}
P(t=0 | \mathbf{z}) \\
\vdots \\
P(t=9 | \mathbf{z}) \\
\end{bmatrix}
= \begin{bmatrix}
\varsigma(\mathbf{z})_0 \\
\vdots \\
\varsigma(\mathbf{z})_9 \\
\end{bmatrix}
= \frac{1}{\sum_{d=0}^9 e^{z_d}}
\begin{bmatrix}
e^{z_0} \\
\vdots \\
e^{z_9} \\
\end{bmatrix}
$$,
где $\varsigma(z)_c  := \frac{e^{z_c}}{\sum_{d=0}^9 e^{z_d}}$

Также *hack* на тему того, как избегать переполнения float64, да и других типов:  `Softmax(x) == Softmax(x - const)`

Нам потребуется найти производную `Cross-entropy` по аргументу `Softmax`, это можно сделать следующим образом:
$$
\begin{split}
\frac{\partial \xi}{\partial z_i} & = - \sum_{j=1}^C \frac{\partial t_j \log(y_j)}{\partial z_i}{}
= - \sum_{j=1}^C t_j \frac{\partial \log(y_j)}{\partial z_i}
= - \sum_{j=1}^C t_j \frac{1}{y_j} \frac{\partial y_j}{\partial z_i} \\
& = - \frac{t_i}{y_i} \frac{\partial y_i}{\partial z_i} - \sum_{j \neq i}^C \frac{t_j}{y_j} \frac{\partial y_j}{\partial z_i}
= - \frac{t_i}{y_i} y_i (1-y_i) - \sum_{j \neq i}^C \frac{t_j}{y_j} (-y_j y_i) \\
& = - t_i + t_i y_i + \sum_{j \neq i}^C t_j y_i = - t_i + \sum_{j = 1}^C t_j y_i
= -t_i + y_i \sum_{j = 1}^C t_j \\
& = y_i - t_i
\end{split}
$$

Также на *backward-проходе* нам предстоит уметь вычислять производную `Softmax`-a по параметрам. Формально, это можно сделать следюущим образом:
$$
\begin{split}
\text{если} \; i = j :& \frac{\partial y_i}{\partial z_i} = \frac{\partial \frac{e^{z_i}}{\Sigma_{0-9}}}{\partial z_i} = \frac{e^{z_i}\Sigma_{0-9} - e^{z_i}e^{z_i}}{\Sigma_{0-9}^2} = \frac{e^{z_i}}{\Sigma_{0-9}}\frac{\Sigma_{0-9} - e^{z_i}}{\Sigma_{0-9}} = \frac{e^{z_i}}{\Sigma_{0-9}}(1-\frac{e^{z_i}}{\Sigma_{0-9}}) =  y_i (1 - y_i)\\
\text{если} \; i \neq j :& \frac{\partial y_i}{\partial z_j} = \frac{\partial \frac{e^{z_i}}{\Sigma_{0-9}}}{\partial z_j} = \frac{0 - e^{z_i}e^{z_j}}{\Sigma_{0-9}^2} = -\frac{e^{z_i}}{\Sigma_{0-9}} \frac{e^{z_j}}{\Sigma_{0-9}} = -y_i y_j
\end{split}
$$,
где $\Sigma_{0-9} := \sum\limits_{d=0}^{9} e^{z_{d}}$

Так как ответ алгоритма воспринимается как верооятность, то логичным будет максимизировать правдоподобие. По аналогии с тем, как выводится `logloss` для бинарной классификации, можно вывести общий вид этой функции потерь, который и будет именоваться, как `Cross-entropy`:
$$
L(Y,Y_{pred}) = \sum_{i=1}^n l(\mathbf{y}_i,\mathbf{y}^{pred}_i) = -\sum_{i=1}^n \sum_{d=0}^{9} y_{id} \cdot \log( y^{pred}_{id})
$$,
где $n$ - размер минибатча.

## Описание метода оптимизации
В данной реализации полносвязной нейронной сети будем использовать градиентную оптимизацию, а именно `Nesterov momentum`. Это модификация стохастического градиентного спуска, которая формально реализуется следующим образом:

$$
\begin{equation}
    \begin{cases}
    m_t = m_{t - 1} \cdot \beta + \nabla_{\theta} L(\theta_{t-1} + m_{t - 1} \cdot \beta) \cdot (1 - \beta)
    \\
    \theta_t = \theta_{t-1} - \alpha \cdot m_t
    \end{cases}
\end{equation}
$$

Для шага оптимизации  надо знать значение импульса для каждого параметра, а как следствие уметь посчитать градиент по параметру. Для этого используют `back propagation` - метод вычисления градиента лосса по параметрам нейронной сети.

Для начала заметим, что параметрами в нашем перцептроне являются $\theta_2, \theta_1, b_2, b_1$. Таким образом, нам надо научиться оптимально считать $\frac{\partial L}{\partial \theta_2}, \frac{\partial L}{\partial \theta_1}, \frac{\partial L}{\partial b_2}, \frac{\partial L}{\partial b_1}$, чтобы делать шаг градиентного спуска для каждого из параметров.

Введем следующие обозначения:
$$
a_2 = \Psi\left(\phi\left( {x}\cdot\theta_1 + b_1 \right)\cdot\theta_2 + b_2\right)
\\
o_2 = \phi\left( {x}\cdot\theta_1 + b_1 \right)\cdot\theta_2 + b_2
\\
a_1 = \phi\left( {x}\cdot\theta_1 + b_1 \right)
\\
o_1 = {x}\cdot\theta_1 + b_1
$$
Причем, в таких обозначениях, $a_i$ - по сути input на $i+1$ слой, а o_i - output на i линейном слое.

Теперь остается заметить, что:
$$
\begin{equation}
\frac{\partial L}{\partial b_2} = \frac{\partial L}{\partial a_2} \frac{\partial a_2}{\partial o_2} \frac{\partial o_2}{\partial b_2} = o_2 - y
\\
\frac{\partial L}{\partial \theta_2} = \frac{\partial L}{\partial a_2} \frac{\partial a_2}{\partial o_2} \frac{\partial o_2}{\partial \theta_2} = (o_2 - y) \cdot a_1
\\
\frac{\partial L}{\partial b_1} = \frac{\partial L}{\partial o_2} \frac{\partial o_2}{\partial a_1} \frac{\partial a_1}{\partial o_1} \frac{\partial o_1}{\partial b_1} = \frac{\partial L}{\partial o_2} \cdot \theta_2 \cdot \phi(o_1) \cdot (1 - \phi(o_1))
\\
\frac{\partial L}{\partial \theta_1} = \frac{\partial L}{\partial o_2} \frac{\partial o_2}{\partial a_1} \frac{\partial a_1}{\partial o_1} \frac{\partial o_1}{\partial \theta_1} = \frac{\partial L}{\partial o_2} \cdot \theta_2 \cdot \phi(o_1) \cdot (1 - \phi(o_1)) \cdot X
\end{equation}
$$
где $\phi(x)$ - сигмоид (для наглядности производной).

Какой мы можем сделать из этого всего вывод? А такой, что на `forward-pass` следует кэшировать входные значения на каждом слое функции, а для того, чтобы подсчитать градиент по параметрам на слое $t$, надо также знать $\frac{\partial L}{\partial o_{t+1}},$ для того чтобы не пересчитывать по 100 раз (а лишь один раз) производные на "верхних слоях"

```python
# Подгрузка основнвных модулей
import numpy as np
import pandas as pd

from abc import ABC, abstractmethod
from tqdm import tqdm
from itertools import chain

%matplotlib inline
import matplotlib.pyplot as plt
import seaborn as sns
plt.style.use('ggplot')

from warnings import simplefilter
simplefilter('ignore')
```

```python
# Подгрузка данных с разделением на обучаующую и тестовую выборки
mnist = np.load('./mnist.npz')

x_train = mnist['x_train']
y_train = mnist['y_train']

x_test = mnist['x_test']
y_test = mnist['y_test']
```

# Исследовательский анализ данных

## Визуализация данных
Для начала посмотрим, как буквально выглядят данные

```python
NUM = 5

fig, ax = plt.subplots(NUM, NUM, figsize=(10, 10))
plt.subplots_adjust(left=-0.2, bottom=-0.3)

for i in range(NUM**2):
    rand_num = np.random.randint(0, x_train.shape[0])

    ax[i // NUM, i % NUM].imshow(x_train[rand_num],)
    ax[i // NUM, i % NUM].set_title(f'IMAGE LABEL: {y_train[rand_num]}')
    ax[i // NUM, i % NUM].set_xticks([])
    ax[i // NUM, i % NUM].set_yticks([])
    ax[i // NUM, i % NUM].grid(False)
```

## Предобработка данных

```python
print(f"shape тренировочной выборки: {x_train.shape}")
print(f"shape тестовой выборки: {x_test.shape}")
print(f"shape целевой метки на тренировке: {y_train.shape}")
print(f"shape целевой метки на тесте: {y_test.shape}")
```

    shape тренировочной выборки: (60000, 28, 28)
    shape тестовой выборки: (10000, 28, 28)
    shape целевой метки на тренировке: (60000,)
    shape целевой метки на тесте: (10000,)

Как можно заметить, данные у нас представлены в виде 70000 матриц 28х28, причем значения в этих матрицах лежат в промежутке [0, 255].
Так как по условию задачи требуется реализовать полносвязную нейронную сеть, то на входной слой следует подавать вектор признаков, который является плоским представлением матриц 28х28 (то есть каждая строка этой матрицы конкатенируется с предыдущей и в итоге получатеся вектор размера 784).

Также, так как мы решаем задачу многоклассовой классификации, оптимизируя Cross-Entropy как функцию потерь (и используя Softmax, который возвращает вектор из вероятностей), то целевая метка должна быть не числом, а One-Hot вектором.

Вдобавок, так как оптимизация градиентная, то нормализуем данные на отрезок [0, 1] по формуле $x := \frac{x - x_{min}}{x_{max} - x_{min}}$

```python
def make_normal(data):
    assert data.ndim == 2

    min_value = data.min(axis=0)
    max_value = data.max(axis=0)

    return (data - min_value) / (max_value - min_value + 1e-100)

def make_flat(data):
    flat_data = data.reshape(data.shape[0], -1)

    assert flat_data.shape[1] == 784
    return flat_data

def make_one_hot(y, length=10):
    assert len(y.shape) == 1

    one_hot_y = np.empty(shape=(y.shape[0], length))

    for ind, obj_label in enumerate(y):
        one_hot_y[ind] = np.array([1 if obj_label == i else 0 for i in range(length)])

    return one_hot_y

def make_train_test(X, y, train_size=0.8):
    perm = np.random.permutation(range(len(X)))
    train_indx = perm[:int(train_size * len(X))]
    test_indx = perm[int(train_size * len(X)):]

    return (
        X[train_indx], X[test_indx],
        y[train_indx], y[test_indx],
    )
```

```python
# flat для x
x_train_flat = make_flat(x_train)
x_test_flat = make_flat(x_test)

# one_hot для y
y_train_one_hot = make_one_hot(y_train)
y_test_one_hot = make_one_hot(y_test)

# здесь можно не париться о data leak'е, так как max = 255, min = 0 (почти наверное)
x_train = make_normal(x_train)
x_test = make_normal(x_test)
```

## Табличная предобработка

Созадим `DataFrame` для train и test выборок, чтобы в дальнейшем произвести манипуляции непосредственно на обеих выборках

```python
columns_train = chain((f"pixel_{i}" for i in range(1, x_train.shape[1] + 1)), ('target',))

df_train = pd.DataFrame(
    np.concatenate((x_train, y_train[:, np.newaxis]), axis=1),
    columns=columns_train,
)
df_train.info()
```

    <class 'pandas.core.frame.DataFrame'>
    RangeIndex: 60000 entries, 0 to 59999
    Columns: 785 entries, pixel_1 to target
    dtypes: float64(785)
    memory usage: 359.3 MB

```python
columns_test = chain((f"pixel_{i}" for i in range(1, x_test.shape[1] + 1)), ('target',))

df_test = pd.DataFrame(
    np.concatenate((x_test, y_test[:, np.newaxis]), axis=1),
    columns=columns_test
)
df_test.info()
```

    <class 'pandas.core.frame.DataFrame'>
    RangeIndex: 10000 entries, 0 to 9999
    Columns: 785 entries, pixel_1 to target
    dtypes: float64(785)
    memory usage: 59.9 MB

```python
print(f'количество признаков с пустыми значениями на train: {(df_train.isna().sum() > 0).sum()}')
print(f'количество признаков с пустыми значениями на test: {(df_test.isna().sum() > 0).sum()}')
```

    количество признаков с пустыми значениями на train: 0
    количество признаков с пустыми значениями на test: 0

### Удаление фиктивных признаков
Посмотрим, есть ли среди признаков такие, что они имеют одно и то же значение на всех объектах. Соответственно, такие признаки мы будем искать на train, а затем удалим ровно их же на test, чтобы не произошло data leak'a

```python
def remove_constant_pix(dataframe):
    new = dataframe.copy()
    deleted = []

    for column in dataframe:
        if dataframe[column].min() == dataframe[column].max():
            deleted.append(column)
            new.drop(column, axis=1, inplace=True)

    return new, deleted
```

```python
df_train_wo_constant, train_feature_deleted = remove_constant_pix(df_train)
```

```python
df_train_wo_constant.shape
```

    (60000, 718)

```python
df_test_wo_constant = df_test.drop(train_feature_deleted, axis=1)
```

```python
df_test_wo_constant.shape
```

    (10000, 718)

## Финальная предобработка
Наконец, разобьем данные на три группы:
- train - данные для обучения
- validataion - данные для оценки работы модели во время обучения
- test - "скрытые" от нас данные, для самой финальной оценки

```python
prepared_train = df_train_wo_constant.drop('target', axis=1)
X_tr, X_val, y_tr, y_val = make_train_test(prepared_train.values, y_train_one_hot, train_size=0.8)

X_test = df_test_wo_constant.drop('target', axis=1)
y_tset = y_test_one_hot
```

```python
print(X_tr.shape)
print(X_val.shape)
print(X_test.shape)
```

    (48000, 717)
    (12000, 717)
    (10000, 717)

# Реализация модели
## Описание основных классов
Далее будут реализованы следующие классы:
- `Module` - абстрактный базовый класс для всех модулей нейронной сети.

- `Loss` - абстрактный базовый класс для всех функционалов ошибки.

- `CrossEntropy` - класс для описания функционала ошибки, а именно CrossEntropy.

- `Linear` - класс, описывающий линейную часть модели.

- `ReLu` - класс для ReLu функции активации.

- `Softmax` - класс для функции активации Softmax (описывался выше)

- `Sequential` - класс для аггрегации слоев сети. По сути можно рассматривать как некоторый контейнер с архитектурой нейронной сети.

Более подробное описание можно увидеть в документации к классам.

```python
class Module(ABC):
    def __init__(self):
        self.input_grad = None  # необходим для хранения градиента по input на данном модуле
        self.input = None  # необходим для кэширования input на forward-pass
        self.output = None # необходим для кэширования output на forward-pass
        self.has_parameters = False  # требуется поставить True у линейного слоя
        self.train = True  # отображает, находится ли модель в состоянии тренировки или в состоянии теста

    @abstractmethod
    def forward(self, input_):
        '''
        Абстрактный метод,
        который возвращает значение соответствующего модуля по input_

        Args:
            input_: входные данные для этого модуля
        '''
        pass

    @abstractmethod
    def backward(self, output_grad):
        '''
        Абстрактный метод,
        который возвращает значение градиента Loss'а
        по input на этого модуля.

        Args:
            output_grad: градиент Loss'а по output этого модуля
        '''
        pass

    def train_(self):
        '''
        Переводит модуль в режим тренировки
        '''
        self.train = True

    def evaluate_(self):
        '''
        Переводит модуль в режим вычисления на валидационных данных
        '''
        self.train = False

    def __repr__(self):
        return "Module"
```

```python
class Loss(ABC):
    def __init__(self):
        self.input_grad = None  # необходим для хранения градиента по input на данном модуле
        self.input = None  # необходим для сохранения input на forward-pass
        self.train = True  # отображает, находится ли модель в состоянии тренировки или в состоянии теста

    @abstractmethod
    def forward(self, y_pred, y_true):
        '''
        Абстрактный метод,
        который вычисляет значение лосса по истинным значениям
        и предсказанным моделью

        Args:
            y_pred: предсказанные значения
            y_true: истинные значения
        '''
        pass

    @abstractmethod
    def backward(self, y_true):
        '''
        Абстрактный метод, который должен возващать
        градиент лосса по входу, то есть про предсказанным значениям

        Args:
            y_pred: предсказанные значения
            y_true: истинные значения
        '''
        pass

    def train_(self):
        '''
        Переводит лосс в режим треинровки
        '''
        self.train = True

    def evaluate_(self):
        '''
        Переводит лосс в режим вычисления на валидационных данных без кэшироваиня
        '''
        self.train = False

    def __repr__(self):
        '''
        Просто для красивого print'а
        '''
        return "Loss"
```

```python
class CrossEntropy(Loss):
    # Реализация Cross-entropy
    def __init__(self):
        super(CrossEntropy, self).__init__()

    def __call__(self, y_pred, y_true):
        return self.forward(y_pred, y_true)

    def forward(self, y_pred, y_true):
        assert y_pred.ndim == y_true.ndim == 2, "предсказывается матрица, у которой каждая строка - предсказание для одного объекта"

        if self.train:  # следует кэшировать только на тренировке
            self.input = y_pred  # для консинстентности стиля, сохраним y_pred в self.input

        log_pred = np.log(y_pred)
        entropy = np.sum(y_true * log_pred, axis=1)
        avg_entropy = entropy.mean()

        return np.negative(avg_entropy)

    def backward(self, y_true):
        assert y_true.shape == self.input.shape, "self.input и y_pred должны быть одноразмерны"

        self.input_grad = np.negative(np.divide(y_true, self.input))

        return self.input_grad

    def __repr__(self):
        # просто для красивого printing'а
        return "Cross-Entropy"

```

```python
class Linear(Module):
    def __init__(self, in_features, out_features, fit_intercept=True):
        super(Linear, self).__init__()
        self.has_parameters = True
        self.fit_intercept = fit_intercept

        self.in_features = in_features
        self.out_features = out_features

        # вроде бы нормальный способ инициализации весов для такой маленькой сетки (refers to LeNet)
        stdv = 1./np.sqrt(in_features)
        self.W = np.random.uniform(-stdv, stdv, size=(out_features, in_features))
        self.grad_W = np.zeros_like(self.W)
        self.momentum_W = np.zeros_like(self.W)

        if fit_intercept:  # если у линейного слоя есть bias:
            self.b = np.random.uniform(-stdv, stdv, size=(out_features))
            self.grad_b = np.zeros_like(self.b)
            self.momentum_b = np.zeros_like(self.b)

    def __call__(self, input_):
        return self.forward(input_)

    def forward(self, input_):
        assert input_.ndim == 2, "input_ линейной модели должен быть размерности 2"

        if self.train:  # следует кэшировать только на тренировке
            self.input = input_
            self.output = self.input @ self.W.T + self.b

        assert self.output.shape == (self.input.shape[0], self.W.shape[0]), "линейная модель должна возврващать матрицу" \
                                                                            "размера (n_samples x out_features)"
        return input_ @ self.W.T + self.b

    def backward(self, output_grad):
        assert output_grad.ndim == 2

        output_grad_tr = output_grad[:, np.newaxis]
        input_tr = self.input[:, np.newaxis]
        W_tr = np.repeat(self.W[None], repeats=output_grad.shape[0], axis=0)

        input_grad_tr = output_grad_tr @ W_tr

        self.input_grad = np.squeeze(input_grad_tr)

        grad_W_wo_sum = output_grad_tr.reshape((self.input.shape[0], output_grad.shape[1], 1)) @ input_tr

        self.grad_W = np.sum(grad_W_wo_sum, axis=0)
        self.grad_b = output_grad

        assert self.input_grad.ndim == 2 and self.grad_W.ndim == 2 and self.grad_W.shape == self.W.shape
        return self.input_grad

    def zero_grad(self):
        self.grad_W.fill(0)
        self.grad_b.fill(0)

    @property
    def W_(self):
        '''
        Getter для матрицы весов линейного слоя
        '''
        return self.W

    @W_.setter
    def W_(self, new_W):
        '''
        Setter для матрицы весов линейного слоя
        '''
        self.W = new_W

    @property
    def b_(self):
        '''
        Getter для 'bias'a линейного слоя
        '''
        return self.b

    @b_.setter
    def b_(self, new_b):
        '''
        Setter для 'bias'a линейного слоя
        '''
        self.b = new_b

    @property
    def grad_W_(self):
        '''
        Getter для градиента лосса по матрице весов.
        '''
        return self.grad_W

    @property
    def grad_b_(self):
        '''
        Getter для градиента лосса по 'bias'у
        '''
        return self.grad_b

    def __repr__(self):
        # для красивого printing'а
        return f"Linear: {self.in_features} -> {self.out_features}"

```

```python
class ReLu(Module):
    def __init__(self):
        super(ReLu, self).__init__()

    def __call__(self, X):
        return self.forward(X)

    def forward(self, input_):
        assert input_.ndim == 2, "input_ReLu должен быть размерности 2"

        if self.train:  # следует кэшировать только на тренировке
            self.input = input_

        return np.maximum(input_, 0)

    def backward(self, output_grad):
        self.input_grad = np.multiply(output_grad, self.input > 0)

        return self.input_grad

    def __repr__(self):
        # для красивого printing'а
        return "ReLu"

```

```python
class Softmax(Module):
    def __init__(self):
        super(Softmax, self).__init__()

    def __call__(self, input_):
        return self.forward(input_)

    def forward(self, input_):
        input_transformed = np.exp(input_ - np.max(input_, axis=1)[:, np.newaxis])

        if self.train:  # следует кэшировать только на тренировке
            self.input = input_
            self.output = np.divide(input_transformed, np.sum(input_transformed, axis=1)[:, np.newaxis])

        return np.divide(input_transformed, np.sum(input_transformed, axis=1)[:, np.newaxis])

    @staticmethod
    def jacobian(x):
        '''
        Возвращает якобиан Softmax'a, который выражается через сам Softmax.
        '''
        x = x.reshape(-1,1)
        return np.diagflat(x) - np.dot(x, x.T)

    def backward(self, output_grad):
        output = self.forward(self.input)[:, np.newaxis]
        jacob = np.squeeze(np.apply_along_axis(Softmax.jacobian, axis=2, arr=output))

        output_grad_tr = output_grad[:, np.newaxis]
        self.input_grad = np.squeeze(output_grad_tr @ jacob)

        return self.input_grad

    def __repr__(self):
        return "Softmax"

```

```python
class OptimizationModeError(Exception):
    def __init__(self):
        super(OptimizationModeError, self).__init__

class Sequential(Module):
    def __init__(self, optimization_mode='Momentum'):
        super(Sequential, self).__init__()
        self.modules = []
        self.optimization_mode = optimization_mode

    def __call__(self, input_):
        return self.forward(input_)

    def add(self, module):
        '''
        Метод для добавления слоя

        Args:
            module: слой, который требуется добавить
        '''
        self.modules.append(module)

    def forward(self, input_):
        output = input_
        for module in self.modules:
            output = module.forward(output)

        if self.train:  # следует кэшировать только на тренировке
            self.input = input_
            self.output = output

        return output

    def backward(self, output_grad):
        '''
        Делаем backward на каждом слое нейронной сети

        Args:
            output_grad: градиент лосса по input'у лосса
        '''

        self.grad_input = output_grad
        for i in range(len(self.modules) - 1, 0, -1):
            self.grad_input = self.modules[i].backward(self.grad_input)

        self.grad_input = self.modules[0].backward(self.grad_input)

        return self.grad_input

    def zero_grad(self):
        '''
        Обнуление градиентов по параметрам
        '''
        for module in self.modules:
            if module.has_parameters:
                module.zero_grad()

    @property
    def gradients(self):
        '''
        Getter для листа параметров
        '''
        return [[module.W_, module.b_] for module in self.modules if module.has_parameters]

    @property
    def parameters(self):
        '''
        Getter для листа градиентов
        '''
        return [[module.grad_W_, module.grad_b_] for module in self.modules if module.has_parameters]

    def train_mode(self):
        self.train_()
        for module in self.modules:
            module.train_()

    def evaluate_mode(self):
        self.evaluate_()
        for module in self.modules:
            module.evaluate_()

    def optimization(self, learning_rate, batch_size, beta=0.9):
        '''
        Метод, реализующий шаг оптимизации.
        Поддерживваются vanilla SGD и SGD + Momentum

        Args:
            learning_rate: скорость обучения

        '''
        if self.optimization_mode == 'SGD':
            for module in self.modules:
                if module.has_parameters:
                    module_W = module.W_
                    module_grad_W = module.grad_W_
                    module.W_ = module_W - learning_rate * module_grad_W / batch_size

                    if module.fit_intercept:
                        module_b = module.b_
                        module_grad_b = module.grad_b_
                        module.b_ = module_b - learning_rate * module_grad_b / batch_size

        elif self.optimization_mode == 'Momentum':
            for module in self.modules:
                if module.has_parameters:
                    module_W = module.W_
                    module_grad_W = module.grad_W_

                    module.momentum_W = beta * module.momentum_W + (1 - beta) * module_grad_W
                    module.W_ = module_W - learning_rate * module_grad_W / batch_size

                    if module.fit_intercept:
                        module_b = module.b_
                        module_grad_b = module.grad_b_

                        module.momentum_b = beta * module.momentum_b + (1 - beta) * module_grad_b
                        module.b_ = module_b - learning_rate * module.momentum_b / batch_size

                    else:
                        raise OptimizationModeError('Unavailable Optimization')

    def __repr__(self):
        return ''.join([str(module) + '\n\t' for module in self.modules ])
```

# Создание и инициализация модели

```python
INPUT = X_tr.shape[1]
HIDDEN = 128
OUTPUT = y_tr.shape[1]
```

```python
loss = CrossEntropy()

model = Sequential()
model.add(Linear(INPUT, HIDDEN))
model.add(ReLu())
model.add(Linear(HIDDEN, OUTPUT))
model.add(Softmax())

print(f"\tАрхитектура модели полносвязной нейронной сети:\n \n\t{model}")
```

    	Архитектура модели полносвязной нейронной сети:

    	Linear: 717 -> 128
    	ReLu
    	Linear: 128 -> 10
    	Softmax

# Обучение модели

```python
BATCH_SIZE = 128

DATASET = (
    ('train', (X_tr, y_tr)),
    ('eval', (X_val, y_val)),
)
```

```python
def generate_batches(X, y, batch_size):
    '''
    Генератор батчей.

    Args:
        X: матрица объектов
        y: вектор целевых меток
        batch_size: размер батча
    '''
    assert len(X) == len(y)

    np.random.seed(42)
    X = np.array(X)
    y = np.array(y)
    perm = np.random.permutation(len(X))
    full_batch_quantity = len(perm) // batch_size

    perm = list(perm)
    for i in range(full_batch_quantity):

        batch_inds = []
        batch_inds.extend(perm[(i*batch_size):((i+1)*batch_size)])

        assert len(batch_inds) == batch_size
        yield (X[batch_inds], y[batch_inds])
next(generate_batches(X_tr, y_tr, 100))[0].shape
```

    (100, 717)

```python
def train_loop(dataset, model, loss, batch_size, learning_rate=1e-2, epochs=100):
    '''
    Цикл обучения модели.

    Args:
        dataset: объелиненеие тренировочной и валидационной выборок.
        model: обучаемая модель
        loss: минимизируемый функционал потерь
        batch_size: размер батча
        learning_rate: коэффициент скорости обучения
        epochs: количетсво эпох для обучения

    '''
    loss_history_train = []
    loss_history_eval = []

    accuracy_history_train = []
    accuracy_history_eval = []

    epoch_loss_train = []
    epoch_loss_eval = []

    pbar = tqdm(total=epochs)
    for epoch in range(epochs):

        # каждую эпоху будем пробегать и по train, и по test
        for data_type, data in dataset:
            X = data[0]
            y = data[1]

            if data_type == 'train':
                # переходим в режим тренировки
                model.train_mode()

                epoch_correct_train = 0
                epoch_all_train = 0
                for batch_X, batch_y in generate_batches(X, y, batch_size):

                    # обнуление градиента
                    model.zero_grad()

                    # forward-pass
                    batch_y_pred = model(batch_X)

                    # преобразоваиня для вычисления accuracy
                    max_proba = np.argmax(batch_y_pred, axis=1)
                    one_hot_pred = make_one_hot(max_proba)
                    epoch_correct_train += (one_hot_pred == batch_y).all(axis=1).sum()
                    epoch_all_train += len(batch_y)

                    # вычисление лосса
                    loss_value_train = loss(batch_y_pred, batch_y)
                    epoch_loss_train.append(loss_value_train)

                    # backward-pass
                    loss_grad = loss.backward(batch_y)
                    model.backward(loss_grad)

                    # шаг оптимизации (дефолтно - Momentum)
                    model.optimization(learning_rate, batch_size)

            elif data_type == 'eval':
                # переходим в режим оценки
                model.evaluate_mode()

                epoch_correct_eval = 0
                epoch_all_eval = 0
                for batch_X, batch_y in generate_batches(X, y, batch_size):

                    # forward-pass без кэша
                    batch_y_pred = model(batch_X)

                    # преобразоваиня для вычисления accuracy
                    max_proba = np.argmax(batch_y_pred, axis=1)
                    one_hot_pred = make_one_hot(max_proba)
                    epoch_correct_eval += (one_hot_pred == batch_y).all(axis=1).sum()
                    epoch_all_eval += len(batch_y)

                    # вычисление лосса
                    loss_value_eval = loss(batch_y_pred, batch_y)
                    epoch_loss_eval.append(loss_value_eval)

        mean_loss_train = np.mean(epoch_loss_train)
        mean_loss_eval = np.mean(epoch_loss_eval)
        accuracy_train = epoch_correct_train / epoch_all_train
        accuracy_eval = epoch_correct_eval / epoch_all_eval

        # обновление progress bar'а
        pbar.update(1)
        pbar.set_postfix({'cross-entropy': mean_loss_train,
                          'accuracy': accuracy_train})

        accuracy_history_train.append(epoch_correct_train / epoch_all_train)  # добавляем accuracy на одной эпохе
        loss_history_train.append(mean_loss_train)  # добавляем среднее за эпоху в историю
        epoch_loss_train.clear()

        accuracy_history_eval.append(accuracy_eval)  # добавляем accuracy на одной эпохе
        loss_history_eval.append(mean_loss_eval)  # добавляем среднее за эпоху в историю
        epoch_loss_eval.clear()

    return loss_history_train, accuracy_history_train, loss_history_eval, accuracy_history_eval
```

```python
loss_history_train, accuracy_history_train, \
loss_history_eval, accuracy_history_eval = train_loop(DATASET, model, loss, epochs=100, batch_size=BATCH_SIZE)
```

      0%|          | 0/100 [00:00<?, ?it/s]

      1%|          | 1/100 [00:20<33:22, 20.23s/it]

      1%|          | 1/100 [00:20<33:22, 20.23s/it, cross-entropy=1.81, accuracy=0.628]

      2%|▏         | 2/100 [00:40<32:50, 20.10s/it, cross-entropy=1.81, accuracy=0.628]

      2%|▏         | 2/100 [00:40<32:50, 20.10s/it, cross-entropy=0.911, accuracy=0.82]

      3%|▎         | 3/100 [00:59<32:22, 20.02s/it, cross-entropy=0.911, accuracy=0.82]

      3%|▎         | 3/100 [00:59<32:22, 20.02s/it, cross-entropy=0.61, accuracy=0.857]

      4%|▍         | 4/100 [01:19<31:49, 19.89s/it, cross-entropy=0.61, accuracy=0.857]

      4%|▍         | 4/100 [01:19<31:49, 19.89s/it, cross-entropy=0.499, accuracy=0.875]

      5%|▌         | 5/100 [01:39<31:20, 19.80s/it, cross-entropy=0.499, accuracy=0.875]

      5%|▌         | 5/100 [01:39<31:20, 19.80s/it, cross-entropy=0.442, accuracy=0.884]

      6%|▌         | 6/100 [01:58<30:53, 19.72s/it, cross-entropy=0.442, accuracy=0.884]

      6%|▌         | 6/100 [01:58<30:53, 19.72s/it, cross-entropy=0.407, accuracy=0.891]

      7%|▋         | 7/100 [02:18<30:33, 19.71s/it, cross-entropy=0.407, accuracy=0.891]

      7%|▋         | 7/100 [02:18<30:33, 19.71s/it, cross-entropy=0.383, accuracy=0.895]

      8%|▊         | 8/100 [02:38<30:22, 19.81s/it, cross-entropy=0.383, accuracy=0.895]

      8%|▊         | 8/100 [02:38<30:22, 19.81s/it, cross-entropy=0.366, accuracy=0.9]

      9%|▉         | 9/100 [02:58<30:03, 19.82s/it, cross-entropy=0.366, accuracy=0.9]

      9%|▉         | 9/100 [02:58<30:03, 19.82s/it, cross-entropy=0.352, accuracy=0.903]

     10%|█         | 10/100 [03:18<30:01, 20.02s/it, cross-entropy=0.352, accuracy=0.903]

     10%|█         | 10/100 [03:18<30:01, 20.02s/it, cross-entropy=0.34, accuracy=0.906]

     11%|█         | 11/100 [03:39<29:52, 20.14s/it, cross-entropy=0.34, accuracy=0.906]

     11%|█         | 11/100 [03:39<29:52, 20.14s/it, cross-entropy=0.33, accuracy=0.908]

     12%|█▏        | 12/100 [03:59<29:30, 20.12s/it, cross-entropy=0.33, accuracy=0.908]

     12%|█▏        | 12/100 [03:59<29:30, 20.12s/it, cross-entropy=0.321, accuracy=0.91]

     13%|█▎        | 13/100 [04:18<29:03, 20.04s/it, cross-entropy=0.321, accuracy=0.91]

     13%|█▎        | 13/100 [04:18<29:03, 20.04s/it, cross-entropy=0.314, accuracy=0.912]

     14%|█▍        | 14/100 [04:38<28:39, 20.00s/it, cross-entropy=0.314, accuracy=0.912]

     14%|█▍        | 14/100 [04:38<28:39, 20.00s/it, cross-entropy=0.307, accuracy=0.914]

     15%|█▌        | 15/100 [04:58<28:17, 19.97s/it, cross-entropy=0.307, accuracy=0.914]

     15%|█▌        | 15/100 [04:58<28:17, 19.97s/it, cross-entropy=0.3, accuracy=0.916]

     16%|█▌        | 16/100 [05:19<28:04, 20.05s/it, cross-entropy=0.3, accuracy=0.916]

     16%|█▌        | 16/100 [05:19<28:04, 20.05s/it, cross-entropy=0.294, accuracy=0.918]

     17%|█▋        | 17/100 [05:39<27:42, 20.04s/it, cross-entropy=0.294, accuracy=0.918]

     17%|█▋        | 17/100 [05:39<27:42, 20.04s/it, cross-entropy=0.289, accuracy=0.919]

     18%|█▊        | 18/100 [05:59<27:24, 20.06s/it, cross-entropy=0.289, accuracy=0.919]

     18%|█▊        | 18/100 [05:59<27:24, 20.06s/it, cross-entropy=0.283, accuracy=0.92]

     19%|█▉        | 19/100 [06:19<27:22, 20.28s/it, cross-entropy=0.283, accuracy=0.92]

     19%|█▉        | 19/100 [06:19<27:22, 20.28s/it, cross-entropy=0.278, accuracy=0.922]

     20%|██        | 20/100 [06:39<26:46, 20.08s/it, cross-entropy=0.278, accuracy=0.922]

     20%|██        | 20/100 [06:39<26:46, 20.08s/it, cross-entropy=0.273, accuracy=0.924]

     21%|██        | 21/100 [06:59<26:30, 20.13s/it, cross-entropy=0.273, accuracy=0.924]

     21%|██        | 21/100 [06:59<26:30, 20.13s/it, cross-entropy=0.268, accuracy=0.925]

     22%|██▏       | 22/100 [07:19<26:10, 20.13s/it, cross-entropy=0.268, accuracy=0.925]

     22%|██▏       | 22/100 [07:19<26:10, 20.13s/it, cross-entropy=0.264, accuracy=0.926]

     23%|██▎       | 23/100 [07:39<25:46, 20.08s/it, cross-entropy=0.264, accuracy=0.926]

     23%|██▎       | 23/100 [07:39<25:46, 20.08s/it, cross-entropy=0.259, accuracy=0.927]

     24%|██▍       | 24/100 [07:59<25:26, 20.09s/it, cross-entropy=0.259, accuracy=0.927]

     24%|██▍       | 24/100 [07:59<25:26, 20.09s/it, cross-entropy=0.255, accuracy=0.928]

     25%|██▌       | 25/100 [08:19<25:01, 20.02s/it, cross-entropy=0.255, accuracy=0.928]

     25%|██▌       | 25/100 [08:19<25:01, 20.02s/it, cross-entropy=0.251, accuracy=0.93]

     26%|██▌       | 26/100 [08:39<24:38, 19.97s/it, cross-entropy=0.251, accuracy=0.93]

     26%|██▌       | 26/100 [08:39<24:38, 19.97s/it, cross-entropy=0.247, accuracy=0.931]

     27%|██▋       | 27/100 [08:59<24:15, 19.94s/it, cross-entropy=0.247, accuracy=0.931]

     27%|██▋       | 27/100 [08:59<24:15, 19.94s/it, cross-entropy=0.243, accuracy=0.932]

     28%|██▊       | 28/100 [09:19<23:58, 19.98s/it, cross-entropy=0.243, accuracy=0.932]

     28%|██▊       | 28/100 [09:19<23:58, 19.98s/it, cross-entropy=0.24, accuracy=0.933]

     29%|██▉       | 29/100 [09:39<23:40, 20.01s/it, cross-entropy=0.24, accuracy=0.933]

     29%|██▉       | 29/100 [09:39<23:40, 20.01s/it, cross-entropy=0.236, accuracy=0.934]

     30%|███       | 30/100 [09:59<23:18, 19.98s/it, cross-entropy=0.236, accuracy=0.934]

     30%|███       | 30/100 [09:59<23:18, 19.98s/it, cross-entropy=0.233, accuracy=0.935]

     31%|███       | 31/100 [10:19<22:59, 19.99s/it, cross-entropy=0.233, accuracy=0.935]

     31%|███       | 31/100 [10:19<22:59, 19.99s/it, cross-entropy=0.229, accuracy=0.936]

     32%|███▏      | 32/100 [10:39<22:40, 20.00s/it, cross-entropy=0.229, accuracy=0.936]

     32%|███▏      | 32/100 [10:39<22:40, 20.00s/it, cross-entropy=0.226, accuracy=0.937]

     33%|███▎      | 33/100 [10:59<22:16, 19.95s/it, cross-entropy=0.226, accuracy=0.937]

     33%|███▎      | 33/100 [10:59<22:16, 19.95s/it, cross-entropy=0.223, accuracy=0.938]

     34%|███▍      | 34/100 [11:19<21:54, 19.91s/it, cross-entropy=0.223, accuracy=0.938]

     34%|███▍      | 34/100 [11:19<21:54, 19.91s/it, cross-entropy=0.22, accuracy=0.939]

     35%|███▌      | 35/100 [11:39<21:31, 19.88s/it, cross-entropy=0.22, accuracy=0.939]

     35%|███▌      | 35/100 [11:39<21:31, 19.88s/it, cross-entropy=0.217, accuracy=0.94]

     36%|███▌      | 36/100 [11:58<21:10, 19.85s/it, cross-entropy=0.217, accuracy=0.94]

     36%|███▌      | 36/100 [11:58<21:10, 19.85s/it, cross-entropy=0.214, accuracy=0.941]

     37%|███▋      | 37/100 [12:19<20:57, 19.96s/it, cross-entropy=0.214, accuracy=0.941]

     37%|███▋      | 37/100 [12:19<20:57, 19.96s/it, cross-entropy=0.211, accuracy=0.942]

     38%|███▊      | 38/100 [12:39<20:51, 20.19s/it, cross-entropy=0.211, accuracy=0.942]

     38%|███▊      | 38/100 [12:39<20:51, 20.19s/it, cross-entropy=0.208, accuracy=0.942]

     39%|███▉      | 39/100 [13:00<20:36, 20.27s/it, cross-entropy=0.208, accuracy=0.942]

     39%|███▉      | 39/100 [13:00<20:36, 20.27s/it, cross-entropy=0.206, accuracy=0.943]

     40%|████      | 40/100 [13:20<20:12, 20.22s/it, cross-entropy=0.206, accuracy=0.943]

     40%|████      | 40/100 [13:20<20:12, 20.22s/it, cross-entropy=0.203, accuracy=0.944]

     41%|████      | 41/100 [13:40<19:54, 20.25s/it, cross-entropy=0.203, accuracy=0.944]

     41%|████      | 41/100 [13:40<19:54, 20.25s/it, cross-entropy=0.2, accuracy=0.944]

     42%|████▏     | 42/100 [14:00<19:22, 20.05s/it, cross-entropy=0.2, accuracy=0.944]

     42%|████▏     | 42/100 [14:00<19:22, 20.05s/it, cross-entropy=0.198, accuracy=0.945]

     43%|████▎     | 43/100 [14:20<18:56, 19.94s/it, cross-entropy=0.198, accuracy=0.945]

     43%|████▎     | 43/100 [14:20<18:56, 19.94s/it, cross-entropy=0.195, accuracy=0.946]

     44%|████▍     | 44/100 [14:39<18:32, 19.86s/it, cross-entropy=0.195, accuracy=0.946]

     44%|████▍     | 44/100 [14:39<18:32, 19.86s/it, cross-entropy=0.193, accuracy=0.947]

     45%|████▌     | 45/100 [14:59<18:08, 19.79s/it, cross-entropy=0.193, accuracy=0.947]

     45%|████▌     | 45/100 [14:59<18:08, 19.79s/it, cross-entropy=0.191, accuracy=0.947]

     46%|████▌     | 46/100 [15:19<17:49, 19.81s/it, cross-entropy=0.191, accuracy=0.947]

     46%|████▌     | 46/100 [15:19<17:49, 19.81s/it, cross-entropy=0.188, accuracy=0.948]

     47%|████▋     | 47/100 [15:40<17:46, 20.12s/it, cross-entropy=0.188, accuracy=0.948]

     47%|████▋     | 47/100 [15:40<17:46, 20.12s/it, cross-entropy=0.186, accuracy=0.949]

     48%|████▊     | 48/100 [15:59<17:23, 20.08s/it, cross-entropy=0.186, accuracy=0.949]

     48%|████▊     | 48/100 [15:59<17:23, 20.08s/it, cross-entropy=0.184, accuracy=0.949]

     49%|████▉     | 49/100 [16:19<17:00, 20.01s/it, cross-entropy=0.184, accuracy=0.949]

     49%|████▉     | 49/100 [16:19<17:00, 20.01s/it, cross-entropy=0.182, accuracy=0.95]

     50%|█████     | 50/100 [16:39<16:40, 20.00s/it, cross-entropy=0.182, accuracy=0.95]

     50%|█████     | 50/100 [16:39<16:40, 20.00s/it, cross-entropy=0.18, accuracy=0.95]

     51%|█████     | 51/100 [17:00<16:22, 20.05s/it, cross-entropy=0.18, accuracy=0.95]

     51%|█████     | 51/100 [17:00<16:22, 20.05s/it, cross-entropy=0.178, accuracy=0.95]

     52%|█████▏    | 52/100 [17:20<16:15, 20.31s/it, cross-entropy=0.178, accuracy=0.95]

     52%|█████▏    | 52/100 [17:20<16:15, 20.31s/it, cross-entropy=0.176, accuracy=0.951]

     53%|█████▎    | 53/100 [17:41<15:52, 20.28s/it, cross-entropy=0.176, accuracy=0.951]

     53%|█████▎    | 53/100 [17:41<15:52, 20.28s/it, cross-entropy=0.174, accuracy=0.952]

     54%|█████▍    | 54/100 [18:01<15:31, 20.24s/it, cross-entropy=0.174, accuracy=0.952]

     54%|█████▍    | 54/100 [18:01<15:31, 20.24s/it, cross-entropy=0.172, accuracy=0.952]

     55%|█████▌    | 55/100 [18:21<15:08, 20.18s/it, cross-entropy=0.172, accuracy=0.952]

     55%|█████▌    | 55/100 [18:21<15:08, 20.18s/it, cross-entropy=0.17, accuracy=0.952]

     56%|█████▌    | 56/100 [18:41<14:44, 20.10s/it, cross-entropy=0.17, accuracy=0.952]

     56%|█████▌    | 56/100 [18:41<14:44, 20.10s/it, cross-entropy=0.168, accuracy=0.953]

     57%|█████▋    | 57/100 [19:01<14:28, 20.19s/it, cross-entropy=0.168, accuracy=0.953]

     57%|█████▋    | 57/100 [19:01<14:28, 20.19s/it, cross-entropy=0.166, accuracy=0.954]

     58%|█████▊    | 58/100 [19:22<14:14, 20.35s/it, cross-entropy=0.166, accuracy=0.954]

     58%|█████▊    | 58/100 [19:22<14:14, 20.35s/it, cross-entropy=0.164, accuracy=0.954]

     59%|█████▉    | 59/100 [19:42<13:52, 20.31s/it, cross-entropy=0.164, accuracy=0.954]

     59%|█████▉    | 59/100 [19:42<13:52, 20.31s/it, cross-entropy=0.162, accuracy=0.955]

     60%|██████    | 60/100 [20:02<13:25, 20.15s/it, cross-entropy=0.162, accuracy=0.955]

     60%|██████    | 60/100 [20:02<13:25, 20.15s/it, cross-entropy=0.161, accuracy=0.956]

     61%|██████    | 61/100 [20:22<13:06, 20.16s/it, cross-entropy=0.161, accuracy=0.956]

     61%|██████    | 61/100 [20:22<13:06, 20.16s/it, cross-entropy=0.159, accuracy=0.956]

     62%|██████▏   | 62/100 [20:42<12:46, 20.17s/it, cross-entropy=0.159, accuracy=0.956]

     62%|██████▏   | 62/100 [20:42<12:46, 20.17s/it, cross-entropy=0.157, accuracy=0.957]

     63%|██████▎   | 63/100 [21:02<12:26, 20.17s/it, cross-entropy=0.157, accuracy=0.957]

     63%|██████▎   | 63/100 [21:02<12:26, 20.17s/it, cross-entropy=0.156, accuracy=0.957]

     64%|██████▍   | 64/100 [21:22<11:59, 19.98s/it, cross-entropy=0.156, accuracy=0.957]

     64%|██████▍   | 64/100 [21:22<11:59, 19.98s/it, cross-entropy=0.154, accuracy=0.958]

     65%|██████▌   | 65/100 [21:42<11:38, 19.96s/it, cross-entropy=0.154, accuracy=0.958]

     65%|██████▌   | 65/100 [21:42<11:38, 19.96s/it, cross-entropy=0.152, accuracy=0.958]

     66%|██████▌   | 66/100 [22:02<11:19, 19.99s/it, cross-entropy=0.152, accuracy=0.958]

     66%|██████▌   | 66/100 [22:02<11:19, 19.99s/it, cross-entropy=0.151, accuracy=0.958]

     67%|██████▋   | 67/100 [22:23<11:05, 20.18s/it, cross-entropy=0.151, accuracy=0.958]

     67%|██████▋   | 67/100 [22:23<11:05, 20.18s/it, cross-entropy=0.149, accuracy=0.959]

     68%|██████▊   | 68/100 [22:42<10:41, 20.05s/it, cross-entropy=0.149, accuracy=0.959]

     68%|██████▊   | 68/100 [22:42<10:41, 20.05s/it, cross-entropy=0.148, accuracy=0.959]

     69%|██████▉   | 69/100 [23:03<10:24, 20.16s/it, cross-entropy=0.148, accuracy=0.959]

     69%|██████▉   | 69/100 [23:03<10:24, 20.16s/it, cross-entropy=0.146, accuracy=0.96]

     70%|███████   | 70/100 [23:23<10:02, 20.09s/it, cross-entropy=0.146, accuracy=0.96]

     70%|███████   | 70/100 [23:23<10:02, 20.09s/it, cross-entropy=0.145, accuracy=0.96]

     71%|███████   | 71/100 [23:43<09:42, 20.07s/it, cross-entropy=0.145, accuracy=0.96]

     71%|███████   | 71/100 [23:43<09:42, 20.07s/it, cross-entropy=0.144, accuracy=0.961]

     72%|███████▏  | 72/100 [24:03<09:21, 20.07s/it, cross-entropy=0.144, accuracy=0.961]

     72%|███████▏  | 72/100 [24:03<09:21, 20.07s/it, cross-entropy=0.142, accuracy=0.961]

     73%|███████▎  | 73/100 [24:23<09:02, 20.09s/it, cross-entropy=0.142, accuracy=0.961]

     73%|███████▎  | 73/100 [24:23<09:02, 20.09s/it, cross-entropy=0.141, accuracy=0.961]

     74%|███████▍  | 74/100 [24:43<08:40, 20.04s/it, cross-entropy=0.141, accuracy=0.961]

     74%|███████▍  | 74/100 [24:43<08:40, 20.04s/it, cross-entropy=0.14, accuracy=0.962]

     75%|███████▌  | 75/100 [25:03<08:21, 20.04s/it, cross-entropy=0.14, accuracy=0.962]

     75%|███████▌  | 75/100 [25:03<08:21, 20.04s/it, cross-entropy=0.138, accuracy=0.962]

     76%|███████▌  | 76/100 [25:23<08:01, 20.04s/it, cross-entropy=0.138, accuracy=0.962]

     76%|███████▌  | 76/100 [25:23<08:01, 20.04s/it, cross-entropy=0.137, accuracy=0.962]

     77%|███████▋  | 77/100 [25:43<07:42, 20.10s/it, cross-entropy=0.137, accuracy=0.962]

     77%|███████▋  | 77/100 [25:43<07:42, 20.10s/it, cross-entropy=0.136, accuracy=0.963]

     78%|███████▊  | 78/100 [26:03<07:22, 20.11s/it, cross-entropy=0.136, accuracy=0.963]

     78%|███████▊  | 78/100 [26:03<07:22, 20.11s/it, cross-entropy=0.134, accuracy=0.963]

     79%|███████▉  | 79/100 [26:23<07:01, 20.05s/it, cross-entropy=0.134, accuracy=0.963]

     79%|███████▉  | 79/100 [26:23<07:01, 20.05s/it, cross-entropy=0.133, accuracy=0.964]

     80%|████████  | 80/100 [26:43<06:40, 20.04s/it, cross-entropy=0.133, accuracy=0.964]

     80%|████████  | 80/100 [26:43<06:40, 20.04s/it, cross-entropy=0.132, accuracy=0.964]

     81%|████████  | 81/100 [27:03<06:20, 20.02s/it, cross-entropy=0.132, accuracy=0.964]

     81%|████████  | 81/100 [27:03<06:20, 20.02s/it, cross-entropy=0.131, accuracy=0.964]

     82%|████████▏ | 82/100 [27:23<06:02, 20.11s/it, cross-entropy=0.131, accuracy=0.964]

     82%|████████▏ | 82/100 [27:23<06:02, 20.11s/it, cross-entropy=0.13, accuracy=0.965]

     83%|████████▎ | 83/100 [27:44<05:41, 20.10s/it, cross-entropy=0.13, accuracy=0.965]

     83%|████████▎ | 83/100 [27:44<05:41, 20.10s/it, cross-entropy=0.128, accuracy=0.965]

     84%|████████▍ | 84/100 [28:04<05:22, 20.16s/it, cross-entropy=0.128, accuracy=0.965]

     84%|████████▍ | 84/100 [28:04<05:22, 20.16s/it, cross-entropy=0.127, accuracy=0.965]

     85%|████████▌ | 85/100 [28:24<05:00, 20.05s/it, cross-entropy=0.127, accuracy=0.965]

     85%|████████▌ | 85/100 [28:24<05:00, 20.05s/it, cross-entropy=0.126, accuracy=0.965]

     86%|████████▌ | 86/100 [28:44<04:43, 20.25s/it, cross-entropy=0.126, accuracy=0.965]

     86%|████████▌ | 86/100 [28:44<04:43, 20.25s/it, cross-entropy=0.125, accuracy=0.966]

     87%|████████▋ | 87/100 [29:05<04:23, 20.29s/it, cross-entropy=0.125, accuracy=0.966]

     87%|████████▋ | 87/100 [29:05<04:23, 20.29s/it, cross-entropy=0.124, accuracy=0.966]

     88%|████████▊ | 88/100 [29:25<04:03, 20.26s/it, cross-entropy=0.124, accuracy=0.966]

     88%|████████▊ | 88/100 [29:25<04:03, 20.26s/it, cross-entropy=0.123, accuracy=0.966]

     89%|████████▉ | 89/100 [29:45<03:42, 20.21s/it, cross-entropy=0.123, accuracy=0.966]

     89%|████████▉ | 89/100 [29:45<03:42, 20.21s/it, cross-entropy=0.122, accuracy=0.967]

     90%|█████████ | 90/100 [30:05<03:21, 20.16s/it, cross-entropy=0.122, accuracy=0.967]

     90%|█████████ | 90/100 [30:05<03:21, 20.16s/it, cross-entropy=0.121, accuracy=0.967]

     91%|█████████ | 91/100 [30:25<03:01, 20.13s/it, cross-entropy=0.121, accuracy=0.967]

     91%|█████████ | 91/100 [30:25<03:01, 20.13s/it, cross-entropy=0.12, accuracy=0.967]

     92%|█████████▏| 92/100 [30:45<02:41, 20.13s/it, cross-entropy=0.12, accuracy=0.967]

     92%|█████████▏| 92/100 [30:45<02:41, 20.13s/it, cross-entropy=0.119, accuracy=0.968]

     93%|█████████▎| 93/100 [31:05<02:20, 20.14s/it, cross-entropy=0.119, accuracy=0.968]

     93%|█████████▎| 93/100 [31:05<02:20, 20.14s/it, cross-entropy=0.118, accuracy=0.968]

     94%|█████████▍| 94/100 [31:26<02:02, 20.36s/it, cross-entropy=0.118, accuracy=0.968]

     94%|█████████▍| 94/100 [31:26<02:02, 20.36s/it, cross-entropy=0.117, accuracy=0.968]

     95%|█████████▌| 95/100 [31:47<01:42, 20.46s/it, cross-entropy=0.117, accuracy=0.968]

     95%|█████████▌| 95/100 [31:47<01:42, 20.46s/it, cross-entropy=0.116, accuracy=0.968]

     96%|█████████▌| 96/100 [32:08<01:22, 20.57s/it, cross-entropy=0.116, accuracy=0.968]

     96%|█████████▌| 96/100 [32:08<01:22, 20.57s/it, cross-entropy=0.115, accuracy=0.969]

     97%|█████████▋| 97/100 [32:28<01:01, 20.58s/it, cross-entropy=0.115, accuracy=0.969]

     97%|█████████▋| 97/100 [32:28<01:01, 20.58s/it, cross-entropy=0.114, accuracy=0.969]

     98%|█████████▊| 98/100 [32:48<00:40, 20.33s/it, cross-entropy=0.114, accuracy=0.969]

     98%|█████████▊| 98/100 [32:48<00:40, 20.33s/it, cross-entropy=0.113, accuracy=0.969]

     99%|█████████▉| 99/100 [33:08<00:20, 20.25s/it, cross-entropy=0.113, accuracy=0.969]

     99%|█████████▉| 99/100 [33:08<00:20, 20.25s/it, cross-entropy=0.112, accuracy=0.969]

    100%|██████████| 100/100 [33:28<00:00, 20.24s/it, cross-entropy=0.112, accuracy=0.969]

    100%|██████████| 100/100 [33:28<00:00, 20.24s/it, cross-entropy=0.112, accuracy=0.97]

Изобразим на одном графике значение лосса и метрики для тренировочной и валидационной выборок.

```python
fig, ax = plt.subplots(1, 2, figsize=(16, 5));

ax[0].plot(loss_history_train, label='train')
ax[0].plot(loss_history_eval, label='test')
ax[0].set_xlabel('Epoch')
ax[0].set_ylabel('Cross-Entropy loss')
ax[0].set_title('Cross-entropy loss')
ax[0].legend()

ax[1].plot(accuracy_history_train, label='train')
ax[1].plot(accuracy_history_eval, label='test')
ax[1].set_xlabel('Epoch')
ax[1].set_ylabel('Accuracy score')
ax[1].set_title('Accuracy metrics')
ax[1].legend();
```

Видим, что переобучения не наблюдается

# Оценка качества
Наконец, воспользуясь обученном моделью, сделаем `predict` на данных, которые она никогда не видела (здесь можно придумать интерпретацию, что это данные уже непосредственно увиденные моделью на продакшне).

```python
# переходим в режим оценивания без кэширования
model.evaluate_mode()

correct = 0
y_pred = []
for X_batch, y_batch in generate_batches(X_test.values, y_test_one_hot, batch_size=128):
    y_batch_pred = model(X_batch)
    y_pred_one_hot = make_one_hot(np.argmax(y_batch_pred, axis=1))
    y_pred.extend(y_pred_one_hot)

    correct += (y_pred_one_hot == y_batch).all(axis=1).sum()

print(f"Финальный accuracy на test: {correct / len(y_test)}")
```

    Финальный accuracy на test: 0.9596

# Вывод

В данной работе была реализована модель, задача которой научиться классфицировать датасет `MNIST`. В плане оптимизации вычислений, использовался `back propagation`, а также `SGD+Momentum`.

Как итог, видно, что модель со своей задачей справилась, ведь об этом свидетельствует значение метрики. Переобучения не было, но это скорее связано с тем, что модель вообще неглубокая, потому что ни каких явных попыток регуляризации я не предприминмал по ходу обучения.

Также я почти уверен, что данные избыточны, поскольку помимо константных пискелей (которые я благополучно удалил на этапе предобработки), есть также и те, тональность которых совсем чуть-чуть менялась. Наверное, в таком случае было бы хорошо использовать более продвинутые методы, вроде `PCA`.


# Statistical Independence

The word “independence” generaly means free from external control or influence, but it also has a lot of connotations in US culture, as it probably does throughout the world. We will apply the concept of independence to many random phenomena, and the implication of independence is generally the same as the definition above: phenomena that are independent cannot influence each other.

In fact, we have already been applying the concept of independence throughout this book when we assume that the outcome of a coin flip, die roll, or simulation does not depend on the values seen in other trials of the same type of experiment. However, now we have the mathematical tools to define the concept of independence precisely.

## Conditional probabilities and independence

Based on the discussion above, try to answer the following question about what independence should mean for conditional probabilities. (Don't worry if you don't intuitively know the answer -- you can keep trying if you don't get it right at first!)

```python
from jupyterquiz import display_quiz
git_path="https://raw.githubusercontent.com/jmshea/Foundations-of-Data-Science-with-Python/main/questions/"

#display_quiz("../questions/si-conditional.json")
display_quiz(git_path + "si-conditional.json")

```

Click the “+” sign to reveal the answer and discussion -->

```{toggle}

If $B$ is independent of $A$, then knowledge of $A$ occurring should not change the probability of $B$ occurring. I.e., if we are *given* that $A$ occurred, then the conditional probability of $B$ occurring should equal the unconditional probability:

$$
P(B|A) = P(B)
$$

Let's see the implications of this by substituting the formula for $P(B|A)$ from the definition:

<!--
\begin{align}\frac{P(A \cap B)}{P(A)} &= P(B) \\
\Rightarrow P(A \cap B) &= P(A)P(B)
\end{align}
-->
$$
\frac{P(A \cap B)}{P(A)} &= P(B) \\
\Rightarrow P(A \cap B) &= P(A)P(B)
$$ (p-b-given-a)

Now we might ask: if $B$ is independent of $A$, does that imply that $A$ is independent of $B$?  Let's assume that {eq}`p-b-given-a` holds and apply the result to the definition for $P(A|B$), assuming that $P(B)>0$:

\begin{align*}
P(A|B) & =\frac{ P(A \cap B) } {P(B) } \\
& = \frac{ P(A) P( B) } {P(B) } \\
& = P(A)
\end{align*}

So if $P(B|A) = P(B)$, then $P(A|B)=P(A)$.

```

## Formal definition of statistically independent events

A simple definition for conditional probability of events that satisfies all the forms of independence discussed above and that can deal with events with probability zero is as follows:

```{panels}
:column: col-9

DEFINITION
^^^
statistically independent (two events)
: Given a probability space $S, \mathcal{F}, P$ and two events $A\in \mathcal{F}$ and $B \in \mathcal{F}$, $A$ and $B$ are {\it statistically independent} if and only if (iff)

$$
P(A \cap B) = P(A)P(B).
$$
```

If the context is clear, we will often just write “independent” instead of “statistically independent” or write *s.i.*, which is a commonly used abbreviation.

````{note}

Please take time to study the definition of *statistically independent* carefully. In particular, note the following:
* **Events** can be statistically independent or not
* Probabilities **are not** something that are statistically independent or not
* The “if and only if” statement means that the definition applies in both directions:
    * If events $A$ and $B$ are statistically independent, then the probability of the intersection of the events factors as the product of the individual events, $P(A \cap B) = P(A)P(B)$.
    * If we have events $A$ and $B$ for which $P(A \cap B) = P(A)P(B)$, then $A$ and $B$ are statistically independent.
````

## When can we assume independence?

Statistical independence is often assumed for many types of events. However, it is important to be careful when applying such a strong assumption because events can be coupled in ways that are subtle. For example, consider the Magician's Coin example. Many people assume that the event of getting Heads on the second flip of the chosen coin will be independent of the outcome of the first flip of the coin. However, we have seen that this assumption is wrong! So, when can we assume that events will be independent?

**Events can be assumed to be statistically independent if they arise from completely separate random phenomena.**

In the case of the Magician's Coin, this assumption is violated in a subtle way. If we knew that the two-headed coin was in use, then we would know the results completely. What is subtle is the fact that observing the outcome of the first flip may give some information about which coin is in use (although we won't be able to show this for observing heads on the first flip until Chapter 6).

Examples that are assumed to result from separate random phenomena are extensive:
* **Devices to generate randomness in games:** Independence can usually be assume dfor different flips of a fair coin, rolls of a fair die, or card hards drawn from shuffled decks.
* **Failures of different devices in systems:** mechanical and electrical devices fail ar random, and the failures at different devices are often assumed to be independent; examples include light bulbs in a building or computers in a lab.
* **Characteristics of people unrelated to any grouping of those people:** for example, for a group of people at a meeting, having a March birthday would generally be independent events across any two people.

Let's apply this concept to find a simpler way to solve a problem that was introduced in {doc}`../04-probability1/axiomatic-prob`:

**Example**

**(Take 3)** A fair six-sided die is rolled twice.  What is the probability that either of the rolls is a value less than 3?

As before, let $E_i$ be the event that the top face on roll $i$ is less than 3, for $i=1,2$.

We assume that different different rolls of the die are independent, so $E_1$ and $E_2$ are independent.

As in {doc}`../04/probability1/corollaries`, we can use Corollary 5 of the Axioms of Probability to write

$$
P(E_1 \cup E_2) = P(E_1) + P(E_2) - P(E_1 \cap E_2)
$$

Before, we had to enumerate $E_1 \cap E_2$ over the sample space for the combine roll of the dice to determine $P(E_1 \cap E_2)$.  Now, we can just apply statistical independence to write $P(E_1 \cap E_2) = P(E_1)P(E_2)$, yielding

\begin{align*}
P(E_1 \cup E_2) &= P(E_1) + P(E_2) - P(E_1)P(E_2) \\
&= \frac{1}{3}  + \frac{1}{3} - \left(\frac{1}{3}\right)\left(\frac{1}{3} \right) \\
&= \frac 5 9 .
\end{align*}

**Exercises**

Answer these questions to practice this form of statistical independence:

```python
#display_quiz("../questions/si1.json")
display_quiz(git_path + "si1.json")
```

 If $A$ and $B$ are s.i. events, then the following pairs of events are also s.i.:
* $A$ and $\overline{B}$
* $\overline{A}$ and $B$
* $\overline{A}$ and $\overline{B}$

I.e., if the probability of an event $A$ occurring does not depend on whether some event $B$ occurs, then it cannot depend on whether the event $B$ does not occur.  This probably matches your intuition. However, we should verify it. Let's check the first example. We need to evaluate $P(A \cap \overline{B})$ to see if it factors as $P(A)P(\overline{B})$. Referring to the Venn diagram below, we can see that $A$ consists of the union of the mutually exclusive parts,  $A \cap B$ and $A \cap \overline{B}$. So we can write $P\left(A \cap \overline{B} \right)= P(A) - P(A \cap B)$.

Then by utilizing the fact that $A$ and $B$ are s.i., we have
\begin{align}
P\left(A \cap \overline{B} \right) &= P(A) - P(A \cap B) \\
 &= P(A) - P(A) P(B) \\
 &= P(A) \left[ 1- P\left(B\right) \right] \\
 &= P(A) P\left( \overline{B} \right)
\end{align}

So, if $A$ and $B$ are s.i., so are $A$ and $\overline{B}$. The other expressions follow through similar manipulation. This is important because we often use this fact to simplify solving problems. We start with a simple example to demonstrate the basic technique:

**Example**

**(Take 4)** A fair six-sided die is rolled twice.  What is the probability that either of the rolls is a value less than 3?

As before, let $E_i$ be the event that the top face on roll $i$ is less than 3, for $i=1,2$, and $E_1$ and $E_2$ are s.i. then
\begin{align}
P(E_1 \cup E_2) &= 1 - P\left(\overline{E_1 \cup E_2}\right) \\
&= 1 - P\left( \overline{E_1} \cap \overline{E_2} \right) \\
&= 1 - P\left( \overline{E_1} \right) P\left( \overline{E_2} \right) \\
&= 1 - \left[ 1 - P\left( {E_1} \right)\right]
\left[ 1- P\left( {E_2} \right) \right]\\
&= 1- \left[ 1 - \left( \frac 2 6 \right) \right] \left[ 1 - \left( \frac 2 6 \right) \right] \\
&= \frac 5 9
\end{align}
Of course for this simple example, it is easiest to directly compute $P\left(\overline{E_1} \right)$, but the full approach shown here is a template that is encountered often when dealing with unions of s.i. events.

To see the power of this method, we first need to define s.i. for more than two events:

````{panels}
:column: col-9

DEFINITION
^^^
statistically independent (for any number of events)
: Given a probability space $S, \mathcal{F}, P$, a collection of events $E_0, E_1, \ldots E_{n-1}$ in $\mathcal{F}$ are {\it statistically independent} if and only if (iff)

\begin{align}
P(E_i \cap E_j) &= P(E_i) P(E_j), ~~ \forall i \ne j \\
P(E_i \cap E_j \cap E_k) &= P(E_i) P(E_j) P(E_k), ~~ \forall i \ne j \ne k \\
\ldots
P(E_0 \cap E_1 \cap \ldots \cap E_{n-1}) &= P(E_0) P(E_1) \cdots P(E_{n-1}), \\
\end{align}
````

It is not sufficient to just check that the probability of every pair of events factors as the product of the probabilities of the individual events. That defines a weaker form of independence:

````{panels}
:column: col-9

DEFINITION
^^^
pairwise statistically independent
: Given a probability space $S, \mathcal{F}, P$, a collection of events $E_0, E_1, \ldots E_{n-1}$ in $\mathcal{F}$ are {\it pairwise statistically independent} if and only if (iff)

\begin{align}
P(E_i \cap E_j) &= P(E_i) P(E_j), ~~ \forall i \ne j
\end{align}
````

We want to use complements to convert the unions to intersections and the resulting general form looks like
\begin{align}
P\left( \bigcup_i E_i \right) &=
1- \prod_i \left[ 1- P\left( E_i \right) \right].
\end{align}
It may be helpful to interpret this as follows: The complement of any of a collection of events occurring is that none of those events occurs; thus the probability that any of a collection of events occurs is one minus the probability that none of those events occurs.

Compare the simplicity of this approach to the form for directly solving for the probability of unions of events (Corrolary 7 from {doc}`../04-probability1/corollaries`):

\begin{eqnarray*}
P\left( \bigcup_{k=1}^{n} A_k \right) &=&
\sum_{k=1}^{n} P\left(A_j\right)
    -\sum_{j<k} P \left( A_j \cap A_k \right) + \cdots \\
    && +
    (-1)^{(n+1) } P\left(A_1 \cap A_2 \cap \cdots \cap A_n \right)
\end{eqnarray*}

Now apply this approach to solve the following practice problems:

```python
from jupyterquiz import display_quiz
git_path="https://raw.githubusercontent.com/jmshea/Foundations-of-Data-Science-with-Python/main/"

#display_quiz("quiz/si-unions.json")
display_quiz(git_path + "06-conditional-prob/quiz/si-unions.json")
```

## Relating Statistical Independent and Mutually Exclusive Events

```python
git_path1="https://raw.githubusercontent.com/jmshea/Foundations-of-Data-Science-with-Python/main/06-conditional-prob/quiz/"

#display_quiz("quiz/si-me.json")
display_quiz(git_path1 + "si-me.json")

```

Click the “+” sign to reveal the discussion -->

```{toggle}

Suppose $A$ and $B$ are events that are both mutually exclusive and statistically independent.

Since $A$ and $B$ are m.e., $A \cap B = \emptyset$, which further implies $P(A \cap B) = P(\emptyset) =0$.

Since $A$ and $B$ are s.i., $P(A \cap B) = P(A) P(B)$.

Combining these, we have that $P(A \cap B) = P(A)P(B) = 0$, which can only occur if either or both of $P(A)=0$ or $P(B)=0$.

Thus, events **cannot be both statistically independent and mutually exclusive unless at least one of the events has probability zero**.

To gain some further insight into this, consider further the m.e. condition, $A \cap B = \emptyset$. This condition implies that if $A$ occurs, then $B$ cannot have occurred, and vice versa. Thus, knowing that either $A$ or $B$ occurred provides a lot of information about the other event. Thus, $A$ and $B$ cannot be independent if they are m.e., except in the special case already identified.

```

## Terminology Review

Use the flashcards below to help you review the terminology introduced in this section.

```python
from jupytercards import display_flashcards

#display_flashcards('flashcards/'+'independence.json')

github='https://raw.githubusercontent.com/jmshea/Foundations-of-Data-Science-with-Python/main/'
github+='06-conditional-prob/flashcards/'
display_flashcards(github+'independence.json')

```

```python

```


```python
import matplotlib.pyplot as plt
import numpy as np
from mpl_toolkits.mplot3d import Axes3D
import sympy as sy
sy.init_printing()
```

# Definition of Linear Independence

If all $c$'s are zero, the set of vectors $\{v_1, v_2,...,v_p\}$ is said to be linearly independent, if the equation

$$c_{1} {v}_{1}+c_{2} {v}_{2}+\cdots+c_{p} {v}_{p}=\mathbf{0}$$

holds.

However if any of $c$'s are nonzero, the set of vectors is linearly dependent.

##  Example 1

Determine if ${v}_1, {v}_2, {v}_3\$ are linearly independent.
$${v}_{1}=\left[\begin{array}{l}
1 \\
2 \\
3
\end{array}\right]^T,
{v}_{2}=\left[\begin{array}{l}
4 \\
5 \\
6
\end{array}\right]^T, \text { and } {v}_{3}=\left[\begin{array}{l}
2 \\
1 \\
0
\end{array}\right]^T$$

The common way of testing linear combination is to

```python
A = sy.Matrix([[1,4,2,0],
               [2,5,1,0],
               [3,6,0,0]])
A.rref()
```

The solution shows that $x_3$ is a free variable, and naturally it could be nonzero, therefore the set is linearly dependent.

##  Example 2

Consider a matrix $A$， determine columns of $A$ are linearly independent.

$$
A=\left[\begin{array}{rrr}
0 & 1 & 4 \\
1 & 2 & -1 \\
5 & 8 & 0
\end{array}\right]
$$

Solve the system via augmented matrix.

```python
A = sy.Matrix([[0,1,4,0],[1,2,-1,0],[5,8,0,0]])
A.rref()
```

$Ax=0$ has only trivial solution, so the columns of $A$ are linearly independent.

Linear independence is closly connected with linear combination, in next section we visualize the linear independence.

#  Visualization of Linear Independence

Let's take a look an exmaple in $\mathbb{R}^2$, show $(3, 2)^T$, $(-9, -6)^T$, $(6, 4)^T$ are linearly dependent.

```python
fig, ax = plt.subplots(figsize = (8, 8))
#######################Arrows#######################
arrows = np.array([[[0,0,3,2]],
                   [[0,0,-9,-6]],
                   [[0,0,6,4]]])
colors = ['r','b','g']
for i in range(arrows.shape[0]):
    X,Y,U,V = zip(*arrows[i,:,:])
    ax.arrow(X[0], Y[0], U[0],V[0], color = colors[i], width = .18,
             length_includes_head = True,
             head_width = .3, # default: 3*width
             head_length = .6,
             overhang = .4, zorder = -i)

ax.scatter(0, 0, ec = 'red', fc = 'black', zorder = 5)
ax.text(6, 4, '$(6, 4)$')
ax.text(-9, -6.5, '$(-9, -6)$')
ax.text(3, 2, '$(3, 2)$')

ax.grid(True)
ax.set_title('Linear Dependence Visualization')
ax.axis([-10, 10, -10, 10])
ax.set_xlabel('x-axis', size = 18)
ax.set_ylabel('y-axis', size = 18)
plt.show()
```

Simply put, if one vector is the scalar multiple of the other, then these two vectors are linearly dependent.

Next, we visualize linear independence in $\mathbb{R}^3$. There are three vectors $(1,-2,1)^T$, $(2,1,2)^T$, $(-1,2,3)^T$, plot them in $\mathbb{R}^3$.

The standard procedure is to write down the span of first two vectors, which is a plane. Then we check if the third vector is in the plane or not. If not, this set of vectors is linearly independent.

$$
\left[
\begin{matrix}
x\\
y\\
z
\end{matrix}
\right]=
s\left[
\begin{matrix}
1\\
-2\\
1
\end{matrix}
\right]+
t\left[
\begin{matrix}
2\\
1\\
2
\end{matrix}
\right]=
\left[
\begin{matrix}
s+2t\\
-2s+t\\
s+2t
\end{matrix}
\right]
$$

```python
%matplotlib notebook

fig = plt.figure(figsize = (8,8))
ax = fig.add_subplot(projection='3d')

s = np.linspace(-1, 1, 10)
t = np.linspace(-1, 1, 10)
S, T = np.meshgrid(s, t)

X = S+2*T
Y = -2*S+T
Z = S+2*T
ax.plot_wireframe(X, Y, Z, linewidth = 1.5, color = 'k', alpha = .6)

vec = np.array([[[0, 0, 0, 1, -2, 1]],
               [[0, 0, 0, 2, 1, 2]],
               [[0, 0, 0, -1, 2, 3]]])
colors = ['r','b','g']
for i in range(vec.shape[0]):
    X, Y, Z, U, V, W = zip(*vec[i,:,:])
    ax.quiver(X, Y, Z, U, V, W, length=1, normalize=False, color = colors[i],
              arrow_length_ratio = .08, pivot = 'tail',
              linestyles = 'solid',linewidths = 3, alpha = .6)

ax.set_title('Linear Independence Visualization')

ax.set_xlabel('x-axis', size = 18)
ax.set_ylabel('y-axis', size = 18)
ax.set_zlabel('z-axis', size = 18)

ax.view_init(elev=20., azim=120)

```

    <IPython.core.display.Javascript object>

Pan around the image, we can see that the green vector is not in the plane spanned by red and blue vector, thus they are linearly independent.

 A Side Note for Linear Dependence

Let $S = \{{v}_1,{v}_2,{v}_3, ..., {v}_n\}$ be a set of vectors in $\mathbb{R}^m$, if $n>m$, then $S$ is linearly dependent.

It means the there must be at least one vector in the vector space spanned by the rest of vectors. For instance, a matrix $A$ which has 4 columns and 3 rows, then its columns are linear dependent.

Also if $S = \{{v}_1,{v}_2,{v}_3, ..., {v}_n\}$ contains a zero vector, then the set is linearly dependent as well.

```python

```


在此练习中，我们将使用logistic回归和神经网络来识别手写数字（0到9）。

# 多类分类

我们将扩展我们在练习2中写的logistic回归的实现，并将其应用于一对多的分类（不止两个类别）。

```python
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy.io import loadmat
```

## Dataset
首先，加载数据集。这里的数据为MATLAB的格式，所以要使用SciPy.io的loadmat函数。

```python
def load_data(path):
    data = loadmat(path)
    X = data['X']
    y = data['y']
    return X,y
```

```python
X, y = load_data('ex3data1.mat')
print(np.unique(y))  # 看下有几类标签)
X.shape, y.shape
```

    [ 1  2  3  4  5  6  7  8  9 10]

    ((5000, 400), (5000, 1))

其中有5000个训练样本，每个样本是20\*20像素的数字的灰度图像。每个像素代表一个浮点数，表示该位置的灰度强度。20×20的像素网格被展开成一个400维的向量。在我们的数据矩阵X中，每一个样本都变成了一行，这给了我们一个5000×400矩阵X，每一行都是一个手写数字图像的训练样本。

第一个任务是将我们的逻辑回归实现修改为完全向量化（即没有“for”循环）。这是因为向量化代码除了简洁外，还能够利用线性代数优化，并且通常比迭代代码快得多。但是，如果从练习2中看到我们的代价函数已经完全向量化实现了，所以我们可以在这里重复使用相同的实现。

## 1.2 Visualizing the data

```python
def plot_an_image(X):
    """
    随机打印一个数字
    """
    pick_one = np.random.randint(0, 5000)
    image = X[pick_one, :]
    fig, ax = plt.subplots(figsize=(1, 1))
    ax.matshow(image.reshape((20, 20)), cmap='gray_r')
    plt.xticks([])  # 去除刻度，美观
    plt.yticks([])
    plt.show()
    print('this should be {}'.format(y[pick_one]))
```

```python
plot_an_image(X)
```

```python
def plot_100_image(X):
    """
    随机画100个数字
    """
    sample_idx = np.random.choice(np.arange(X.shape[0]), 100)  # 随机选100个样本
    sample_images = X[sample_idx, :]  # (100,400)

    fig, ax_array = plt.subplots(nrows=10, ncols=10, sharey=True, sharex=True, figsize=(8, 8))

    for row in range(10):
        for column in range(10):
            ax_array[row, column].matshow(sample_images[10 * row + column].reshape((20, 20)),
                                   cmap='gray_r')
    plt.xticks([])
    plt.yticks([])
    plt.show()
```

```python
plot_100_image(X)
```

## 1.3 Vectorizing Logistic Regression

我们将使用多个one-vs-all(一对多)logistic回归模型来构建一个多类分类器。由于有10个类，需要训练10个独立的分类器。为了提高训练效率，重要的是**向量化**。在本节中，我们将实现一个不使用任何for循环的向量化的logistic回归版本。

首先准备下数据。

### 1.3.1 Vectorizing the cost function

首先写出向量化的代价函数。回想正则化的logistic回归的代价函数是：
$$ J\left( \theta  \right)=\frac{1}{m}\sum\limits_{i=1}^{m}{[-{{y}^{(i)}}\log \left( {{h}_{\theta }}\left( {{x}^{(i)}} \right) \right)-\left( 1-{{y}^{(i)}} \right)\log \left( 1-{{h}_{\theta }}\left( {{x}^{(i)}} \right) \right)]}+\frac{\lambda}{2m}\sum^n_{j=1}\theta^2_j $$

首先我们对每个样本 $i$ 要计算$h_{\theta}(x^{(i)})$，$h_{\theta}(x^{(i)})=g(\theta^Tx^{(i)})$，$g(z)=\frac{1}{1+e^{-z}}$ sigmoid函数。

事实上我们可以对所有的样本用矩阵乘法来快速的计算。让我们如下来定义 $X$ 和 $\theta$ ：

然后通过计算矩阵积 $X\theta$ ，我们可以得到：

在最后一个等式中，我们用到了一个定理，如果 $a$ 和 $b$ 都是向量，那么 $a^Tb=b^Ta$，这样我们就可以用一行代码计算出所有的样本。

```python
def sigmoid(z):
    return 1 / (1 + np.exp(-z))
```

```python
def regularized_cost(theta, X, y, l):
    """
    don't penalize theta_0
    args:
        X: feature matrix, (m, n+1) # 插入了x0=1
        y: target vector, (m, )
        l: lambda constant for regularization
    """
    thetaReg = theta[1:]
    first = (-y*np.log(sigmoid(X@theta))) + (y-1)*np.log(1-sigmoid(X@theta))
    reg = (thetaReg@thetaReg)*l / (2*len(X))
    return np.mean(first) + reg
```

### 1.3.2 Vectorizing the gradient
回顾正则化logistic回归代价函数的梯度下降法如下表示，因为不惩罚theta_0，所以分为两种情况：
\begin{align}
  & Repeat\text{ }until\text{ }convergence\text{ }\!\!\{\!\!\text{ } \\
 & \text{     }{{\theta }_{0}}:={{\theta }_{0}}-a\frac{1}{m}\sum\limits_{i=1}^{m}{[{{h}_{\theta }}\left( {{x}^{(i)}} \right)-{{y}^{(i)}}]x_{_{0}}^{(i)}} \\
 & \text{     }{{\theta }_{j}}:={{\theta }_{j}}-a\frac{1}{m}\sum\limits_{i=1}^{m}{[{{h}_{\theta }}\left( {{x}^{(i)}} \right)-{{y}^{(i)}}]x_{j}^{(i)}}+\frac{\lambda }{m}{{\theta }_{j}} \\
 & \text{          }\!\!\}\!\!\text{ } \\
 & Repeat \\
\end{align}

所以其中的梯度表示如下：

```python
def regularized_gradient(theta, X, y, l):
    """
    don't penalize theta_0
    args:
        l: lambda constant
    return:
        a vector of gradient
    """
    thetaReg = theta[1:]
    first = (1 / len(X)) * X.T @ (sigmoid(X @ theta) - y)
    # 这里人为插入一维0，使得对theta_0不惩罚，方便计算
    reg = np.concatenate([np.array([0]), (l / len(X)) * thetaReg])
    return first + reg
```

## 1.4 One-vs-all Classification

这部分我们将实现一对多分类通过训练多个正则化logistic回归分类器，每个对应数据集中K类中的一个。

对于这个任务，我们有10个可能的类，并且由于logistic回归只能一次在2个类之间进行分类，每个分类器在“类别 i”和“不是 i”之间决定。 我们将把分类器训练包含在一个函数中，该函数计算10个分类器中的每个分类器的最终权重，并将权重返回shape为(k, (n+1))数组，其中 n 是参数数量。

```python
from scipy.optimize import minimize

def one_vs_all(X, y, l, K):
    """generalized logistic regression
    args:
        X: feature matrix, (m, n+1) # with incercept x0=1
        y: target vector, (m, )
        l: lambda constant for regularization
        K: numbel of labels
    return: trained parameters
    """
    all_theta = np.zeros((K, X.shape[1]))  # (10, 401)

    for i in range(1, K+1):
        theta = np.zeros(X.shape[1])
        y_i = np.array([1 if label == i else 0 for label in y])
#         y_i = np.reshape(y_i, (-1, 1))  # 重新转化为列向量

        ret = minimize(fun=regularized_cost, x0=theta, args=(X, y_i, l), method='TNC',
                        jac=regularized_gradient, options={'disp': True})
        all_theta[i-1,:] = ret.x

    return all_theta
```

这里需要注意的几点：首先，我们为X添加了一列常数项 1 ，以计算截距项（常数项）。 其次，我们将y从类标签转换为每个分类器的二进制值（要么是类i，要么不是类i）。 最后，我们使用SciPy的较新优化API来最小化每个分类器的代价函数。 如果指定的话，API将采用目标函数，初始参数集，优化方法和jacobian（渐变）函数。 然后将优化程序找到的参数分配给参数数组。

实现向量化代码的一个更具挑战性的部分是正确地写入所有的矩阵，保证维度正确。

```python
def predict_all(X, all_theta):
    # compute the class probability for each class on each training instance
    h = sigmoid(X @ all_theta.T)  # 注意的这里的all_theta需要转置
    # create array of the index with the maximum probability
    # Returns the indices of the maximum values along an axis.
    h_argmax = np.argmax(h, axis=1)
    # because our array was zero-indexed we need to add one for the true label prediction
    h_argmax = h_argmax + 1

    return h_argmax
```

这里的`h`共5000行，10列，每行代表一个样本，每列是预测对应数字的概率。我们取概率最大对应的index加1就是我们分类器最终预测出来的类别。返回的`h_argmax`是一个array，包含5000个样本对应的预测值。

```python
raw_X, raw_y = load_data('ex3data1.mat')
X = np.insert(raw_X, 0, 0, axis=1) # (5000, 401)
y = raw_y.flatten()  # 这里消除了一个维度，方便后面的计算 or .reshape(-1) （5000，）

all_theta = one_vs_all(X, y, 1, 10)
all_theta  # 每一行是一个分类器的一组参数
```

    array([[ 0.00000000e+00,  0.00000000e+00,  0.00000000e+00, ...,
             1.31141206e-03, -1.16971030e-09,  0.00000000e+00],
           [ 0.00000000e+00,  0.00000000e+00,  0.00000000e+00, ...,
             3.28206468e-03, -3.75794187e-04,  0.00000000e+00],
           [ 0.00000000e+00,  0.00000000e+00,  0.00000000e+00, ...,
            -1.63821882e-05, -3.16577807e-07,  0.00000000e+00],
           ...,
           [ 0.00000000e+00,  0.00000000e+00,  0.00000000e+00, ...,
            -1.37694354e-03,  1.43370120e-04,  0.00000000e+00],
           [ 0.00000000e+00,  0.00000000e+00,  0.00000000e+00, ...,
            -2.83089562e-03,  2.13921244e-04,  0.00000000e+00],
           [ 0.00000000e+00,  0.00000000e+00,  0.00000000e+00, ...,
            -1.36585836e-04,  7.89392474e-06,  0.00000000e+00]])

```python
y_pred = predict_all(X, all_theta)
accuracy = np.mean(y_pred == y)
print ('accuracy = {0}%'.format(accuracy * 100))
```

    accuracy = 94.12%

```python
def predict(x, theta):
    prob = sigmoid(x @ theta)
    return (prob >= 0.5).astype(int)

y_pred = predict(X, all_theta[1])
print('Accuracy={}'.format(np.mean((y==1) == y_pred)))
```

    Accuracy=0.8102

**Tips: python中 true就是1，1就是true，false就是0，0就是false**

```python
y_pred == y
```

    array([False, False, False, ..., False, False, False])

# 2 Neural Networks
上面使用了多类logistic回归，然而logistic回归不能形成更复杂的假设，因为它只是一个线性分类器。

接下来我们用神经网络来尝试下，神经网络可以实现非常复杂的非线性的模型。我们将利用已经训练好了的权重进行预测。

```python
def load_weight(path):
    data = loadmat(path)
    return data['Theta1'], data['Theta2']
```

```python
theta1, theta2 = load_weight('ex3weights.mat')
theta1.shape, theta2.shape
```

    ((25, 401), (10, 26))

 因此在数据加载函数中，原始数据做了转置，然而，转置的数据与给定的参数不兼容，因为这些参数是由原始数据训练的。 所以为了应用给定的参数，我需要使用原始数据（不转置）

```python
X, y = load_data('ex3data1.mat')
y = y.flatten()
X = np.insert(X, 0, values=np.ones(X.shape[0]), axis=1)  # intercept
X.shape, y.shape
```

    ((5000, 401), (5000,))

```python
a1 = X
z2 = a1 @ theta1.T
z2.shape
```

    (5000, 25)

```python
z2 = np.insert(z2, 0, 1, axis=1)
```

```python
a2 = sigmoid(z2)
a2.shape
```

    (5000, 26)

```python
z3 = a2 @ theta2.T
z3.shape
```

    (5000, 10)

```python
a3 = sigmoid(z3)
a3.shape
```

    (5000, 10)

```python
y_pred = np.argmax(a3, axis=1) + 1
accuracy = np.mean(y_pred == y)
print ('accuracy = {0}%'.format(accuracy * 100))
```

    accuracy = 97.52%

虽然人工神经网络是非常强大的模型，但训练数据的准确性并不能完美预测实际数据，在这里很容易过拟合。