# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .py
#       format_name: light
#       format_version: '1.5'
#       jupytext_version: 1.14.4
#   kernelspec:
#     display_name: Python 3
#     language: python
#     name: python3
# ---

# + [markdown] colab_type="text" id="uDI0ZLrS9jAX"
# <img src="https://s8.hostingkartinok.com/uploads/images/2018/08/308b49fcfbc619d629fe4604bceb67ac.jpg" width=500, height=450>
# <h3 style="text-align: center;"><b>Физтех-Школа Прикладной математики и информатики (ФПМИ) МФТИ</b></h3>

# + [markdown] colab_type="text" id="k0ygS84T9jAY"
# ---

# + [markdown] colab_type="text" id="HnjQZLuC9jAY"
# <h2 style="text-align: center;"><b>Перцептрон Розенблатта <br><br>(нейрон с пороговой функцией активации)</b></h2>

# + [markdown] colab_type="text" id="543-uGN-9jAZ"
# ---

# + [markdown] colab_type="text" id="1JBsLVMI9jAa"
# В данном ноутбуке Вам нужно будет:  
#
# - самостоятельно реализовать класс **`Perceptron()`** -- нейрон пороговой функцией активации
# - обучить и протестировать Ваш перцептрон на сгенерированных и реальных данных (файлы с реальными данными помещены в папку /data в этой же директории)
# - сравнить качество работы Вашего класса с классом из библиотеки `scikit-learn` (`sklearn.linear_model.Perceptron()`)

# + [markdown] colab_type="text" id="cOAHk8eO9jAb"
# <h2 style="text-align: center;"><b>Введение</b></h2>

# + [markdown] colab_type="text" id="bF22tUW79jAc"
# Почти любой алгоритм машинного обучения, решающий задачу *классификации* или *регрессии*, работает так:
#
# 1. (*стадия инициализации*) Задаются его **гиперпараметры**, то есть те величины, которые не "выучиваются" алгоритмом в процессе обучения самостоятельно 
# 2. (*стадия обучения*) Алгоритм запускается на данных, **обучаясь** на них и меняя свои **параметры** (не путать с *гипер*параметрами) каким-то определённым образом (например, с помощью *метода градиентного спуска* или *метода коррекции ошибки*), исходя из функции потерь (её называют *loss function*). Функция потерь, по сути, говорит, где и как ошибается модель
# 3.  (*стадия предсказания*) Модель готова, и теперь с помощью неё можно делать **предсказания** на новых объектах

# + colab={} colab_type="code" id="3hxoVvmN9jAd"
from matplotlib import pyplot as plt
from matplotlib.colors import ListedColormap  # тут лежат разные штуки для цветовой магии
import numpy as np
import pandas as pd


# + [markdown] colab_type="text" id="jHd4CZjS9jAg"
# <h2 style="text-align: center;"><b>Класс Perceptron</b></h2>

# + [markdown] colab_type="text" id="PObIs0OB9jAh"
# В даном разделе будет решаться задача **бинарной классификации** с помощью перцептрона:  
# - *Входные данные*: матрица $X$ размера $(n, m)$ и столбец $y$ из нулей и единиц размера $(n, 1)$. Строкам матрицы соответствуют объекты, столбцам - признаки (то есть строка $i$ есть набор признаков (*признаковое описание*) объекта $X_i$).
# - *Выходные данные*: столбец $\hat{y}$ из нулей и единиц размера $(n, 1)$ - предсказания алгоритма.

# + [markdown] colab_type="text" id="wkd_24Zr9jAi"
# Модель нейрона в биологии и в deep learning:  
#
# ![title](http://lamda.nju.edu.cn/weixs/project/CNNTricks/imgs/neuron.png)

# + [markdown] colab_type="text" id="TwRqMBVPcy0j"
# \**картинка из http://cs231n.github.io/neural-networks-1/*

# + [markdown] colab_type="text" id="82qIny-49jAi"
# Чтобы понять, как мы будем обновлять параметры модели (веса), нужно знать, какую функцию потерь мы оптимизируем (находим минимум). В данном случае мы решаем задачу бинарной классификации (2 класса: 1 или 0), возьмём в качестве функции потерь среднеквадратичную ошибку:  
#
# $$Loss(w, x) = \frac{1}{2n}\sum_{i=1}^{n} (\hat{y_i} - y_i)^2 = \frac{1}{2n}\sum_{i=1}^{n} (f(w \cdot X_i) - y_i)^2$$  
#
# Здесь $w \cdot X_i$ - скалярное произведение, а $f(w \cdot X_i)$ - пороговая функция:  
#
# $$
# f(z) =
# \begin{cases}
# 1, &\text{если } w \cdot X_i > 0 \\
# 0, &\text{если } w \cdot X_i \le 0
# \end{cases}
# $$  
#
# **Примечание:** В формуле предполагается, что $b$ - свободный член - является частью вектора весов: $w_0$. Тогда, если к $X$ приписать слева единичный столбец, в скалярном произведении $b$ будет именно как свободный член (лучше распишите это -- станет понятнее). При реализации класса `Perceptron()` $b$ нужно считать отдельно (чтобы было нагляднее).

# + [markdown] colab_type="text" id="6wGYvpsv9jAj"
# ** Реализуйте функцию потерь $Loss$: **

# + colab={} colab_type="code" id="KIMPzh0B9jAk"
def Loss(y_pred, y):
    return # Ваш код здесь


# + [markdown] colab_type="text" id="5QrUrljB9jAn"
# Поскольку у *пороговой функции* не существует производной (вы её график видели? Выглядит он, конечно, простым, но производная таких не любит), то мы не можем использовать градиентный спуск, ведь:  
#
#
#
# $$ \frac{\partial Loss}{\partial w} = \frac{1}{n} X^T\left(f(w \cdot X) - y\right)f'(w \cdot X)$$  
#
# где $f^{'}(w \cdot X)$ - в точке 0 посчитать не получится. Но ведь хочется как-то обновлять веса, иначе как обучить алгоритм отличать груши от яблок?  
#
# Поэтому предлагается обновлять так:   
#
# $$w^{j+1} = w^{j} - \alpha\Delta{w^{j}}$$ 
#
# где:  
#
# $$\Delta{w} = \frac{1}{n}X^T(\hat{y} - y) = \frac{1}{n}X^T(f(w^j \cdot X) - y)$$  
#
# (не забудьте, что при $w_0 = b$ признак $x_0$ = 1), где $w \cdot X$ - матричное произведение столбца весов $w$ на матрицу объектов-признаков $X$, а индекс $j$ -- номер итерации градиентного спуска.
#
# Это правило является неким частным случаем градиентного спуска для данного случая (*[правило Хебба](https://ru.wikipedia.org/wiki/%D0%94%D0%B5%D0%BB%D1%8C%D1%82%D0%B0-%D0%BF%D1%80%D0%B0%D0%B2%D0%B8%D0%BB%D0%BE)*, *[метод коррекции ошибки](https://ru.wikipedia.org/wiki/%D0%9C%D0%B5%D1%82%D0%BE%D0%B4_%D0%BA%D0%BE%D1%80%D1%80%D0%B5%D0%BA%D1%86%D0%B8%D0%B8_%D0%BE%D1%88%D0%B8%D0%B1%D0%BA%D0%B8)*).

# + [markdown] colab_type="text" id="Zrm01BR69jAo"
# Теперь, вооружившись всеми формулами и силой духа, нужно написать свой класс **`Perceptron()`**. Уже есть код класса и немного кода реализации. По-максимуму используйте **Numpy** при реализации, т.к. будет проверяться и скорость работы Вашего алгоритма.  
#
# *Примечание*: В коде ниже `y_pred` - это $\hat{y}$ из формул выше

# + colab={"base_uri": "https://localhost:8080/", "height": 132} colab_type="code" id="rLBDN2G89jAo" outputId="a9b7d64e-e7fe-4ce4-ee46-608991580501" executionInfo={"status": "error", "timestamp": 1539420667947, "user_tz": -300, "elapsed": 761, "user": {"displayName": "\u0413\u0440\u0438\u0433\u043e\u0440\u0438\u0439 \u041b\u0435\u043b\u0435\u0439\u0442\u043d\u0435\u0440", "photoUrl": "", "userId": "07179937308049589303"}}
class Perceptron:
    def __init__(self, w=None, b=0):
        """
        :param: w -- вектор весов
        :param: b -- смещение
        """
        # Пока что мы не знаем размер матрицы X, а значит не знаем, сколько будет весов
        self.w = w
        self.b = b
        
    def activate(self, x):
        return x > 0
        
    def forward_pass(self, X):
        """
        Эта функция рассчитывает ответ перцептрона при предъявлении набора объектов
        :param: X -- матрица объектов размера (n, m), каждая строка - отдельный объект
        :return: вектор размера (n, 1) из нулей и единиц с ответами перцептрона 
        """
        n = X.shape[0]
        y_pred = np.zeros((n, 1))  # y_pred(icted) - предсказанные классы
        # Ваш код здесь
        return y_pred
    
    def backward_pass(self, X, y, y_pred, learning_rate=0.005):
        """
        Обновляет значения весов перцептрона в соответствие с этим объектом
        :param: X -- матрица объектов размера (n, m)
                y -- вектор правильных ответов размера (n, 1)
                learning_rate - "скорость обучения" (символ alpha в формулах выше)
        В этом методе ничего возвращать не нужно, только правильно поменять веса
        с помощью градиентного спуска.
        """
        # Ваш код здесь
    
    def fit(self, X, y, num_epochs=300):
        """
        Спускаемся в минимум
        :param: X -- матрица объектов размера (n, m)
                y -- вектор правильных ответов размера (n, 1)
                num_epochs -- количество итераций обучения
        :return: Loss_values -- вектор значений функции потерь
        """
        self.w = np.zeros((X.shape[1], 1))  # столбец (m, 1)
        self.b = 0  # смещение (свободный член)
        losses = []  # значения функции потерь на различных итерациях обновления весов
        
        for i in range(num_epochs):
            # Ваш код здесь

        return losses


# + [markdown] colab_type="text" id="XlWXLoHQ9jAr"
# Класс готов. Посмотрим, правильно ли ведёт себя Ваш перцептрон. Далее идут несколько ячеек с тестовым кодом, Вам нужно просто запустить их и проверить, чтобы результаты запуска совпадали с соответствующими числами из таблиц:

# + [markdown] colab_type="text" id="GnrccB6H9jAs"
# **Проверка forward_pass():**

# + colab={"base_uri": "https://localhost:8080/", "height": 238} colab_type="code" id="9LZgkcHv9jAt" outputId="dc10527e-b3ab-4065-ff8a-20bb5df39b4e" executionInfo={"status": "error", "timestamp": 1539420677714, "user_tz": -300, "elapsed": 616, "user": {"displayName": "\u0413\u0440\u0438\u0433\u043e\u0440\u0438\u0439 \u041b\u0435\u043b\u0435\u0439\u0442\u043d\u0435\u0440", "photoUrl": "", "userId": "07179937308049589303"}}
w = np.array([1., 2.]).reshape(2, 1)
b = 2.
X = np.array([[1., 2., -1.], [3., 4., -3.2]])

perceptron = Perceptron(w, b)
y_pred = perceptron.forward_pass(X.T)
print ("y_pred = " + str(y_pred))

# + [markdown] colab_type="text" id="RlPOE9ia9jAv"
# |Должно быть||
# |------|-------|
# |**y_pred**|[1, 1, 0]|

# + [markdown] colab_type="text" id="1rgBqV9D9jAv"
# **Проверка backward_pass():**

# + colab={} colab_type="code" id="9RkAnK0P9jAw"
y = np.array([1, 0, 1]).reshape(3, 1)

# + colab={"base_uri": "https://localhost:8080/", "height": 221} colab_type="code" id="Be7OJp8c9jA1" outputId="bc1d3643-d2f1-4024-dea3-d98cc9c50ff1" executionInfo={"status": "error", "timestamp": 1539420682215, "user_tz": -300, "elapsed": 619, "user": {"displayName": "\u0413\u0440\u0438\u0433\u043e\u0440\u0438\u0439 \u041b\u0435\u043b\u0435\u0439\u0442\u043d\u0435\u0440", "photoUrl": "", "userId": "07179937308049589303"}}
perceptron.backward_pass(X.T, y, y_pred)

print ("w = " + str(perceptron.w))
print ("b = " + str(perceptron.b))

# + [markdown] colab_type="text" id="nNThF8MT9jA4"
# |Должно быть||
# |-|-|
# |**w**| [[ 0.995], [1.988]] |
# |**b**| 2.0 |

# + [markdown] colab_type="text" id="EDjsmpZp9jA5"
# Посмотрим, как меняется функция потерь в течение процесса обучения на реальных данных - датасет "Яблоки и Груши":

# + colab={"base_uri": "https://localhost:8080/", "height": 878} colab_type="code" id="aPzhL2L99jA5" outputId="ad70b9a6-f5a3-4c7a-cf5b-4adc7c725f22" executionInfo={"status": "error", "timestamp": 1539420685187, "user_tz": -300, "elapsed": 711, "user": {"displayName": "\u0413\u0440\u0438\u0433\u043e\u0440\u0438\u0439 \u041b\u0435\u043b\u0435\u0439\u0442\u043d\u0435\u0440", "photoUrl": "", "userId": "07179937308049589303"}}
data = pd.read_csv("./data/apples_pears.csv")

# + colab={} colab_type="code" id="q7cWGg5S9jA7"
data.head()

# + colab={} colab_type="code" id="V6T8WK2w9jA-"
plt.figure(figsize=(10, 8))
plt.scatter(data.iloc[:, 0], data.iloc[:, 1], c=data['target'], cmap='rainbow')
plt.title('Яблоки и груши', fontsize=15)
plt.xlabel('симметричность', fontsize=14)
plt.ylabel('желтизна', fontsize=14)
plt.show();

# + [markdown] colab_type="text" id="JYSpUvQM9jBE"
# **Вопрос:** Какой класс соответствует яблокам (какого они цвета на графике)?

# + [markdown] colab_type="text" id="MO0fW5R29jBF"
# **Ответ:** <Ваш ответ>

# + [markdown] colab_type="text" id="X6m0IAdu9jBF"
# Обозначим, что здесь признаки, а что - классы:

# + colab={} colab_type="code" id="ARYN13Io9jBG"
X = data.iloc[:,:2].values  # матрица объекты-признаки
y = data['target'].values.reshape((-1, 1))  # классы (столбец из нулей и единиц)

# + [markdown] colab_type="text" id="MRCQeKtH9jBI"
# **Вывод функции потерь**  
# Функция потерь должна убывать и в итоге стать близкой к 0

# + colab={} colab_type="code" id="sIR0g6mQ9jBJ"
# %%time
perceptron = # Ваш код здесь
losses = # Ваш код здесь

plt.figure(figsize=(10, 8))
plt.plot(losses)
plt.title('Функция потерь', fontsize=15)
plt.xlabel('номер итерации', fontsize=14)
plt.ylabel('$Loss(\hat{y}, y)$', fontsize=14)
plt.show()

# + [markdown] colab_type="text" id="gnzSyV_j9jBN"
# Посмотрим, как перцептрон классифицировал объекты из выборки:

# + colab={} colab_type="code" id="bNhsJbuY9jBO"
plt.figure(figsize=(10, 8))
plt.scatter(data.iloc[:, 0], data.iloc[:, 1], c=perceptron.forward_pass(X).ravel(), cmap='spring')
plt.title('Яблоки и груши', fontsize=15)
plt.xlabel('симметричность', fontsize=14)
plt.ylabel('желтизна', fontsize=14)
plt.show();

# + [markdown] colab_type="text" id="aE8iMuT39jBQ"
# <h3 style="text-align: center;"><b>Предсказание пола по голосу</b></h3>

# + [markdown] colab_type="text" id="hQHD1i1W9jBR"
# В этой задаче нужно сравнить качество работы Вашего перцептрона и алгоритма из библиотеки `sklearn` на датасете с сайта [Kaggle](https://www.kaggle.com) - [Gender Recognition by Voice](https://www.kaggle.com/primaryobjects/voicegender). В данном датасете в качестве признаков выступают различные звуковые характеристики голоса, а в качестве классов - пол (мужчина/женщина). Подробнее о самих признаках можно почитать [на странице датасета](https://www.kaggle.com/primaryobjects/voicegender) (на английском). Нашей целью пока что является просто протестировать на этих данных два алгоритма.

# + [markdown] colab_type="text" id="2duv7On99jBR"
# **! Обратите внимание на имя функции из sklearn - skPerceptron** (это сделано, чтобы не совпадало с именем вашего класса)

# + colab={} colab_type="code" id="YaLaxBHR9jBS"
import pandas as pd
from sklearn.linear_model import Perceptron as skPerceptron
from sklearn.metrics import accuracy_score

# + colab={} colab_type="code" id="IaNjHU7Q9jBU"
data_path = './data/voice.csv'
data = pd.read_csv(data_path)
data['label'] = data['label'].apply(lambda x: 1 if x == 'male' else 0)

# + colab={} colab_type="code" id="eU1EZFzM9jBW"
data.head()

# + colab={} colab_type="code" id="QCSK3sfX9jBY"
# Чтобы перемешать данные. Изначально там сначала идут все мужчины, потом все женщины
data = data.sample(frac=1)

# + colab={} colab_type="code" id="VKY1jHT79jBZ"
X_train = data.iloc[:int(len(data)*0.7), :-1]  # матрица объекты-признаки
y_train = data.iloc[:int(len(data)*0.7), -1]  # истинные значения пола (мужчина/женщина)

X_test = data.iloc[int(len(data)*0.7):, :-1]  # матрица объекты-признаки
y_test = data.iloc[int(len(data)*0.7):, -1]  # истинные значения пола (мужчина/женщина)

# + [markdown] colab_type="text" id="DDsWavYZ9jBe"
# Тут нужно натренировать Ваш перцептрон и перцептрон из `sklearn` на этих данных:

# + colab={} colab_type="code" id="4Z8Rh-bu9jBf"
# Ваш код здесь

# + [markdown] colab_type="text" id="3qsolz149jBh"
# Сравним доли правильных ответов (на тестовых данных):

# + colab={} colab_type="code" id="l6R3cXLO9jBi"
print('Точность (доля правильных ответов, из 100%) моего перцептрона: {:d}'.format(accuracy_score(<Ваш код здесь>) * 100))
print('Точность (доля правильных ответов) перцептрона из sklearn: {:.1f} %'.format(accuracy_score(<Ваш код здесь>) * 100))

# + [markdown] colab_type="text" id="CSasTfW09jBj"
# **Вопрос:** Хорошее ли качество показывает перцептрон? Как Вы думаете, почему? Можете писать любые мысли на этот счёт.

# + [markdown] colab_type="text" id="uWaJ-92j9jBj"
# **Ответ:**<Ваш ответ>

# + [markdown] colab_type="text" id="yyX2G7VvdvMC"
# ### Важно

# + [markdown] colab_type="text" id="sTZFLUqFdv57"
# Стоит понимать, что перцептрон сам по себе не используется в приложениях. Мы продемонстрровали его вам, чтобы вы знали, с чего всё начиналось. На самом деле это просто один нейрон с пороговой функцией активации, который не используется в многослойных нейросетях и каких-либо прикладных задачах, но всё же является хорошим учебным примером, помогающим понять то, как обновляются веса в соответствие с ошибками и перейти к рассмотрению более полезных моделей (нейронов с другими функциями активации).

# + [markdown] colab_type="text" id="MC0FNrfq9jBl"
# <h2 style="text-align: center;"><b>Полезные ссылки</b></h2>

# + [markdown] colab_type="text" id="AZLMhq149jBl"
# 1). Lecture Notes Стэнфордского университета: http://cs231n.github.io/neural-networks-1/  
# 2). [Википедия про перцептрон](https://ru.wikipedia.org/wiki/%D0%9F%D0%B5%D1%80%D1%86%D0%B5%D0%BF%D1%82%D1%80%D0%BE%D0%BD)

StarcoderdataJupyterScriptsDedupFiltered