<jupyter_start><jupyter_text>Нейросети и PyTorch (Часть 2) > 🚀 В этой практике нам понадобятся: `numpy==1.21.2, pandas==1.3.3, matplotlib==3.4.3, scikit-learn==0.24.2, torch==1.9.1` > 🚀 Установить вы их можете с помощью команды: `!pip install numpy==1.21.2, pandas==1.3.3, matplotlib==3.4.3, scikit-learn==0.24.2, torch==1.9.1`   Содержание * [Многослойные сети](Mnogoslojnye_seti)  * [Задание - обучение многослойной нейронной сети](Zadanie_-_obuchenie_mnogoslojnoj_nejronnoj_seti)* [Как оценить работу нейросети?](Kak_otsenit__rabotu_nejroseti?)* [Я выбираю нелинейность!](Ja_vybiraju_nelinejnost_!)  * [Задание - нелинейная сеть](Zadanie_-_nelinejnaja_set_)* [Нейросеть для классификации](Nejroset__dlja_klassifikatsii)* [Как сохранить модель?](Kak_sohranit__model_?)* [Результаты](Rezul_taty)* [Выводы - задание](Vyvody_-_zadanie)<jupyter_code># Импорт необходимых модулей 
import matplotlib
import matplotlib.pyplot as plt
import numpy as np
import random
import torch

# Настройки для визуализации
# Если используется темная тема - лучше текст сделать белым
TEXT_COLOR = 'black'

matplotlib.rcParams['figure.figsize'] = (15, 10)
matplotlib.rcParams['text.color'] = 'black'
matplotlib.rcParams['font.size'] = 14
matplotlib.rcParams['axes.labelcolor'] = TEXT_COLOR
matplotlib.rcParams['xtick.color'] = TEXT_COLOR
matplotlib.rcParams['ytick.color'] = TEXT_COLOR

# Зафиксируем состояние случайных чисел
RANDOM_STATE = 0
random.seed(RANDOM_STATE)
# Добавляется специфичнвя для torch фиксация сида
torch.manual_seed(RANDOM_STATE)
np.random.seed(RANDOM_STATE)<jupyter_output><empty_output><jupyter_text>На данный момент мы отлично справляемся с нейросетью, состоящей из одного нейрона! Настало время попробовать сделать многослойную сеть!<jupyter_code>from torch import nn<jupyter_output><empty_output><jupyter_text>Возьмём те же данные, что были в предыдущем ноутбуке для однослойной сети.<jupyter_code>n_points = 100

rng = np.random.default_rng(RANDOM_STATE)

X_data = 4*np.sort(rng.random((n_points, 1)), axis=0)+1

noize = 1*(rng.random((n_points, 1))-0.5)
real_W = [2, 0.7]
y_data_true = real_W[0] + real_W[1]*X_data
y_data_noized = y_data_true + noize
y_data = y_data_noized[:, 0]<jupyter_output><empty_output><jupyter_text>Многослойные сети Самый простой способ создания многослойной сети - это сделать два модуля слоя и выполнить один за другим!<jupyter_code>torch.manual_seed(RANDOM_STATE)

# Делаем два нейрона в первом слое
layer1 = nn.Linear(1, 2)
# Так как в предыдущем два нейрона, то здесь два входа
#   Чтобы получить один выход всей сети, то у последнего слоя выход должен быть 1
layer2 = nn.Linear(2, 1)

# Данные примера
X_sample = torch.tensor([[1], [2], [3]]).float()

# Исполняем один за другим
l1_data = layer1(X_sample)
y_pred_tnsr = layer2(l1_data)

# Смотрим на предсказания
y_pred_tnsr<jupyter_output><empty_output><jupyter_text>В целом, можно было бы на этом оставить всё как есть, но современные сети, как правило, содержат очень много слоёв и было бы сложно выполнять их через циклы и ещё хранить промежуточные значения.> 🔥 Мы сейчас разберём, как оптимизировать работу со многими слоями, но важно помнить, что при необходимости исследования можно сеть разобрать послойно и получить значения с любого из слоев.Первый способ объединения модулей в один является использвание [`nn.Sequential`](https://pytorch.org/docs/stable/generated/torch.nn.Sequential.html). Принцип работы с ним в том, что он объединяет операции в последовательность:<jupyter_code>seq_module = nn.Sequential(layer1, layer2)
print(seq_module)

y_pred_tnsr = seq_module(X_sample)
y_pred_tnsr<jupyter_output><empty_output><jupyter_text>Другим способом является написание класса модели, который наследуется от `nn.Module`. Посмотрим, как это делается:<jupyter_code>class MyLinearModel(nn.Module):
    def __init__(self):
        # На этой строке вызывается конструктор класса, от которого наследуемся
        #   Она нужна, чтобы корректно настроить класс
        super().__init__()
        torch.manual_seed(RANDOM_STATE)

        self.layer1 = nn.Linear(1, 2)
        self.layer2 = nn.Linear(2, 1)
    
    # Метод, который нужно написать, чтобы вызов работал! Всегда должен называться forward!
    def forward(self, X):
        l1_data = self.layer1(X)
        y_pred = self.layer2(l1_data)

        return y_pred
model = MyLinearModel()
# При отображении показываются все слои внутри модели
print(model)

# Вот именно в этот момент происходит вызов метода forward() класса
y_pred_tnsr = model(X_sample)
y_pred_tnsr<jupyter_output><empty_output><jupyter_text>Таким образом создаётся класс, который содержит все необходимые слои (можно даже использовать `nn.Sequential` и другие вспомогательные классы внутри) и в нём пишется метод `forward()`, в котором прописываются действия со слоями. Потом объект этого класса можно просто вызывать и получать результат метода `forward()`! Отличная инкапсуляция!  Задание - обучение многослойной нейронной сетиСамое время обучить модель и понять, лучше или хуже она работает, чем однослойная с одним нейроном. Используйте для этого функции, полученные из предыдущего ноутбука<jupyter_code>def predict(model, X):
    return model(X)
def fit_model(model, optim, loss_op, X: np.ndarray, y: np.ndarray, n_iter: int):
    """
        model:
            Модель для обучения
        optim:
            Модуль оптимизатора
        loss_op:
            Модуль функции потерь
        X:
            Матрица данных
        y:
            Вектор ground-truth значений
        n_iter:
            Количество итераций обучения (эпох)
    """

    # Вставьте сюда код из предыдущего ноутбука =) 
    loss_values = []

    return loss_values        
# TODO - обучите многослойную сеть и отобразите историю обучения 
#   и предсказания обученной модели (на графике с данными)
model = MyLinearModel()
def plot_model(X, y_pred, y_true):
    plt.scatter(X, y_true, label='Данные')
    plt.plot(X, y_pred, 'k--', label='Предсказание модели')
    plt.ylabel('$Y$')
    plt.xlabel('$X$')
    plt.grid()
    plt.legend()
    plt.show()
def preprocess_vector(x: list):
    # Вставьте сюда код из предыдущего ноутбука =) 
    return None
# TEST

_test_tnsr = preprocess_vector([3.5])
_test_pred = model(_test_tnsr)

np.testing.assert_array_almost_equal(_test_pred.detach(), [[4.5]], decimal=1)
assert loss_history[-1] < 0.22<jupyter_output><empty_output><jupyter_text>> ⚠️ Подумайте над идеей того, что модель теперь состоит из двух слоёв и суммарно трёх нейронов, но при этом характер предсказания (прямая линия) не изменился. Таким образом, нейронная сеть из слоёв с линейной активацией не даст ничего, кроме линейной модели!  Как оценить работу нейросети?Нейросеть = ещё один вид модели машинного обучения. При том, что мы решали задачу регрессии, то и все метрики, которые мы использовали для моделей регрессии, применимы здесь!Аналогично, сейчас рассмотрим задачу классификации, но и там такие же принципы для оценки.<jupyter_code># TODO - напишите функцию оценки работы модели по метрике R2 
#   (не забудьте импорт нужной функции из sklearn)

def evaluate_r2(model, X, y):
    return r2_value
# TEST

r2_value = evaluate_r2(model, X_data, y_data)
print(f'R2: {r2_value}')

np.testing.assert_almost_equal(r2_value, 0.83848, decimal=5)<jupyter_output><empty_output><jupyter_text>> ⚠️ Не забывайте, что мы учимся работать с фреймворком - полученная оценка на обучающей выборке не имеет большого смысла (просто пример), при реальной разработке оценивать работу модели нужно как всегда на выделенной выборке (например, тестовой)  Я выбираю нелинейность! Для работы с данными, в которых нам нужно определить линейные зависимости, достаточно нейросети без активации (или просто нейрона). Теперь, во-первых, давайте начнём работать с данными, которые имеют явную нелинейность, а во-вторых, посмотрим, как добавить слою функцию активации, чтобы добавить нейросети свойство нелинейности.Вот так будут выглядеть наши данные:<jupyter_code>rng = np.random.default_rng(RANDOM_STATE)

X_data = np.linspace(2, 10, 100)[:, None]
y_data = 1/X_data[:,0]*5 + rng.standard_normal(size=X_data.shape[0])/7 + 2
# y_data = (-1)*X_data[:,0]**2+(10)*X_data[:,0] + np.random.normal(size=X_data.shape[0]) + 5

# Посмотрим на данные
plt.scatter(X_data[:,0], y_data)
plt.grid(True)
plt.xlabel('$x$')
plt.ylabel('$y$')
plt.show()<jupyter_output><empty_output><jupyter_text>Отлично, линейная модель тут уже вряд ли справится, нам нужно научиться добавлять слоям нелинейную активацию!Давайте начнём с одного слоя и сделаем ему функцию активации:<jupyter_code># Создаем слой и задаём свой вес
layer = nn.Linear(1, 1, bias=False)
layer.weight.data.fill_(10)
# Создаем модуль сигмоиды
activation_func = nn.Sigmoid()

# Данные для примера
X_sample = torch.tensor([[-10], [0], [10]]).float()
print(f'Input: {X_sample}')

# Исполняем вычисления результатов слоя
layer_result = layer(X_sample)
print(f'Layer result: {layer_result}')

# Исполняем модуль сигмоиды
act_result = activation_func(layer_result)
print(f'Activation result: {act_result}')<jupyter_output><empty_output><jupyter_text>Вот таким несложным образом можно добавить в нейросеть нелинейность. Можно создать модуль и исполнять его.Другим более предпочтительным способом является не создание модуля, а просто исполнение функции сигмоиды:<jupyter_code>layer_result = layer(X_sample)
act_result = torch.sigmoid(layer_result)

print(f'Input: {X_sample}')
print(f'Layer result: {layer_result}')
print(f'Activation result: {act_result}')<jupyter_output><empty_output><jupyter_text>Мы видим аналогичный результат как по значениям, так и по функции `grad_fn`. То есть при написании класса модели можно прямо в методе `forward()` вызывать функцию сигмоиды (или другой функции активации).   Задание - нелинейная сетьРеализуйте код двуслойной сети по архитектуре `[2, 1]`:- 2 нейрона в скрытом слое;- 1 нейрон в выходной слое.Скрытый слой должен иметь сигмоиду в качестве активации, выходной слой должен иметь линейную активацию (без активации). После написания обучите модель и посмотрите на предсказания модели:<jupyter_code># TODO - реализуйте модель нейронной сети с нелинейностью, 
#   обучите и оцените модель

class NonlinearNeuralNetwork(nn.Module):
    def __init__(self):
        super().__init__()
        torch.manual_seed(RANDOM_STATE)
        # И здесь надо создать слои

    def forward(self, X):
        return y_pred
# TEST

_test_model = NonlinearNeuralNetwork()
_test_tnsr = preprocess_vector([10])
_test_pred = _test_model(_test_tnsr)

np.testing.assert_array_almost_equal(_test_pred.shape, (1, 1))
np.testing.assert_array_almost_equal(_test_pred.detach(), [[0.0949331]], decimal=4)
model = NonlinearNeuralNetwork()
optimizer = torch.optim.SGD(
    params=model.parameters(),
    lr=0.1
)
loss_op = nn.MSELoss()

loss_history = fit_model(
    model=model,
    optim=optimizer,
    loss_op=loss_op,
    X=X_data,
    y=y_data,
    n_iter=1000
)

plt.plot(loss_history)
plt.grid()
plt.show()

X_tnsr = torch.tensor(X_data).float()
y_pred_tnsr = model(X_tnsr)
y_pred = y_pred_tnsr.detach().numpy()

plot_model(X_data, y_pred, y_data)<jupyter_output><empty_output><jupyter_text>Отлично! По результатам обучения видно, что модель с нелинейностью может иметь нелинейный характер и описывать нелинейные зависимости. Можете самостоятельно оценить работу по численным показателям и кроссвалидацией постараться сделать модель ещё точнее!  Нейросеть для классификации Думаю, и так понятно, что нейросеть не ограничивается только задачей регрессии, поэтому мы зацепим ещё и работу модели для задачи классификации! Создадим данные для задачи классификации:<jupyter_code>from sklearn.datasets import make_moons

X_data, y_data = make_moons(
    n_samples=1000,
    noise=.1,
    random_state=RANDOM_STATE
)

pnts_scatter = plt.scatter(X_data[:, 0], X_data[:, 1], marker='o', c=y_data, s=50, edgecolor='k')
plt.xlabel('$x_1$')
plt.ylabel('$x_2$')
plt.grid(True)
plt.legend(handles=pnts_scatter.legend_elements()[0], labels=['0', '1'])
plt.show()<jupyter_output><empty_output><jupyter_text>Отлично! Данные есть, теперь можно переходить к написанию модели и обучению!<jupyter_code>class ClassificationNN(nn.Module):
    def __init__(self):
        super().__init__()
        torch.manual_seed(RANDOM_STATE)
        # Два признака - два входа
        self.layer1 = nn.Linear(2, 2)
        # Выход - степень уверенности присвоения классу 1 (так как задача бинарной классификации)
        self.layer2 = nn.Linear(2, 1)
    
    def forward(self, x):
        out = torch.sigmoid(self.layer1(x))
        out = self.layer2(out)
        y_prob = torch.sigmoid(out)
        return y_prob<jupyter_output><empty_output><jupyter_text>Небольшая модель сделана! Обратите внимание, что у нас выход - это уже не чистое значение линейного слоя, а функция сигмоиды! Так как задача состоит в бинарной классификации, то нам достаточно сказать степень уверенности и задаться порогом, как обычно.Теперь напишем цикл обучения, чтобы обучить модель, при этом для классификации нам нужна другая функция потерь - воспользуемся [`nn.BCELoss()`](https://pytorch.org/docs/stable/generated/torch.nn.BCELoss.html):<jupyter_code># TODO - напишите цикл обучения модели
# - Создайте модель
# - Задайте оптимизатор (SGD)
# - Создайте модуль функции потерь
# - Запустите обучение через fit_model() - видали, мы даже функцию не переписывали!
# - Отобразите историю обучения
from sklearn.metrics import classification_report

def show_classification_report(model, X, y):
    X_tnsr = torch.tensor(X).float()
    y_pred_prob = model(X_tnsr).detach().numpy()

    # Вектор степеней уверенности превращаем в бинарный вектор по порогу принятия решения
    y_pred = y_pred_prob > 0.5

    rep = classification_report(y, y_pred)
    print(rep)

show_classification_report(model, X_data, y_data)<jupyter_output><empty_output><jupyter_text>Теперь ещё напишем метод визуализации данных, чтобы посмотреть на пространство принятия решений:<jupyter_code>def plot_2d_decision_boundary(model, X, y):
    x1_vals = np.linspace(X[:,0].min()-0.5, X[:,0].max()+0.5, 100)
    x2_vals = np.linspace(X[:,1].min()-0.5, X[:,1].max()+0.5, 100)
    xx1, xx2 = np.meshgrid(x1_vals, x2_vals)

    X_tnsr = torch.tensor(np.c_[xx1.ravel(), xx2.ravel()]).float()
    y_pred = model(X_tnsr).detach()
    y_pred = y_pred.reshape(xx1.shape)

    plt.contourf(xx1, xx2, y_pred)
    pnts_scatter = plt.scatter(X[:, 0], X[:, 1], c=y, s=30, edgecolor='k')
    plt.xlabel("$x_1$")
    plt.ylabel("$x_2$")
    plt.grid(True)
    plt.legend(handles=pnts_scatter.legend_elements()[0], labels=['0', '1'])
    plt.show()

plot_2d_decision_boundary(model, X_data, y_data)<jupyter_output><empty_output><jupyter_text>Хммм, явный клиничейский случай недообучения! Модель не может разделить столь нелинейные данные - давайте добавим модели сложности: три слоя и больше нейронов в слое!<jupyter_code>class ClassificationNNv2(nn.Module):
    def __init__(self):
        super().__init__()
        HIDDEN = 20

        torch.manual_seed(RANDOM_STATE)
        self.layers = nn.Sequential(
            # Обратите внимание, нелинейности могут быть и между слоями!
            nn.Linear(2, HIDDEN),
            nn.Sigmoid(),
            nn.Linear(HIDDEN, HIDDEN),
            nn.Sigmoid(),
            nn.Linear(HIDDEN, 1),
        )
    
    def forward(self, x):
        return torch.sigmoid(self.layers(x))
# TODO - обучите модель снова и посмотрите на показатели (убедитесь, что лучше не стало...)<jupyter_output><empty_output><jupyter_text>Теперь давайте попробуем поменять функции активации слоев (сигмоида выхода нужна для превращения сырых показателей logits в степень уверенности - её не трогайте) с сигмоиды на гиперболический тангенс (в английском назван tanh) - [torch.nn.Tanh()](https://pytorch.org/docs/stable/generated/torch.nn.Tanh.html). Оцените, насколько изменился характер предсказания модели!<jupyter_code># TODO - замените функцию активации слоев и обучите модель, сделайте выводы
class ClassificationNNv3(nn.Module):
    

    def forward(self, x):
        return torch.sigmoid(self.layers(x))
# TEST

_test_model = ClassificationNNv3()
_test_tnsr = preprocess_vector([0.1, -0.1])
_test_pred = _test_model(_test_tnsr)

np.testing.assert_array_almost_equal(_test_pred.shape, (1, 1))
np.testing.assert_array_almost_equal(_test_pred.detach(), [[0.5198]], decimal=4)
# TODO - а теперь обучите последнюю версию (v3) модели и посмотрите, как все поменялось, напишите выводы<jupyter_output><empty_output><jupyter_text>Как сохранить модель?Один из актуальных вопросов - а как мне сохранить модель, чтобы сохранить результаты обучения? Мне же не придется обучать модель каждый раз заново? Ответ, конечно нет! И для этого PyTorch имеет очень простой функционал!<jupyter_code># Задаём путь, по которому хотим сохранить параметры модели
SAVE_PATH = 'my_model.pth'
# Вызываем функцию сохранения
# Сохраняем параметры модели!
torch.save(model.state_dict(), SAVE_PATH)<jupyter_output><empty_output><jupyter_text>После сохранения в файловой системе должен появиться файл с названием модели! Вот так можно в файл перенести параметры. А как загрузить их?<jupyter_code>loaded_state_dict = torch.load(SAVE_PATH)

model = ClassificationNNv3()
model.load_state_dict(loaded_state_dict)

plot_2d_decision_boundary(model, X_data, y_data)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.09973419623252051, 'fraction_numerical': 0.01438807350052005, 'mean_word_length': 5.135058489897199, 'pattern_counts': {'":': 0, '<': 82, '<?xml version=': 0, '>': 87, 'https://': 3, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '20', 'dir': 'jupyter-structured-clean-dedup', 'id': '65160', 'license': 'CC0-1.0', 'n_tokens_mistral': 7128, 'n_tokens_neox': 7871, 'n_words': 1570, 'path': 'notebooks/42_NeuralNetworks.ipynb', 'repo_name': 'MarinaKupetskova/ml_edu', 'size': '17306'}