# 01_1_tensors.ipynb
# Markdown:
# Знакомство с `torch.Tensor`

__Автор задач: Блохин Н.В. (NVBlokhin@fa.ru)__

Материалы:
* Deep Learning with PyTorch (2020) Авторы: Eli Stevens, Luca Antiga, Thomas Viehmann
* https://pytorch.org/docs/stable/torch.html

# Markdown:
## Задачи для совместного разбора
# Markdown:
1\. Создайте тензор и исследуйте его основные характеристики
import torch as th
th.__version__
t = th.tensor([0, 2, 3, 4, 5])
t
t.dtype
t_f = th.Tensor([0, 2, 3, 4, 5])
t_f.dtype
th.tensor, th.Tensor
t.shape
t3 = th.rand(5, 6, 7)
t3.shape
t3.device
# Markdown:
2\. Создайте трехмерный тензор и рассмотрите основные способы индексирования по нему
t = th.rand(5, 6, 7)
t[0, [0, 1], 0]
t[0, [0, 1], :]
t[0, [0, 1]]
t[0, [0, 1], 3:5]
len(t), t.size(0), t.shape[0]
mask = [False, False, True, True, True]

t[mask].shape
t[::2, 1:3, 1:3]
t[::2, 1:3, [1,3]]
# Markdown:
3\. Создайте тензор (4х4) и модифицируйте следующим образом: ко всем четным столбцам прибавьте 1, из нечетных вычтите 1.
t = th.arange(16).view(4, 4)
t
x = th.tensor([-1, 1, -1, 1])
t.shape, x.shape
t + x
# x :
# [[-1, 1, -1, 1],
#  [-1, 1, -1, 1],
#  [-1, 1, -1, 1],
#  [-1, 1, -1, 1],]
# Markdown:
4\. Обсудите совместимость `torch` с `numpy` и `sklearn`
t.shape
th.sin(t)
import numpy as np

np.sin(t)
np.linalg.norm(t)
import torch as th
# t = th.rand(10, 5, device="cuda")
# t.device
th.sin(t).shape
import numpy as np
np.sin(t)
# Markdown:
## Задачи для самостоятельного решения
# Markdown:
<p class="task" id="1"></p>

1\. Создайте двумерный тензор размера (100000, 10), заполненный нулями. Используя прихотливое индексирование, поставьте в каждой строке тензора ровно одну единицу в случайно выбранном столбце. Рассчитайте и выведите на экран вероятности $p_i$ того, что для случайно выбранной строки в столбце $i$ будет стоять единица.

- [x] Проверено на семинаре
t = th.zeros(100000, 10)
t[th.arange(t.shape[0]), th.randint(0, t.shape[1], size=[t.shape[0]])] = 1
t[:10, :]
p = t.mean(dim=0)
print(p, p.sum())
# Markdown:
<p class="task" id="2"></p>

2\. При помощи прихотливого индексирования для двумерного тензора размерности (10, 10), состоящего из случайных целых чисел в пределах от 0 до 10, получите тензор элементов, находящихся сразу над  побочной диагональю.

- [x] Проверено на семинаре
t = th.randint(0, 10, (10, 10))
t
t[th.arange(8, -1, -1, dtype=th.int), th.arange(0, 9, dtype=th.int)]
# Markdown:
<p class="task" id="3"></p>

3\. Создайте двумерный тензор $t$ размерности (5, 5), состоящий из случайных чисел в пределах от 0 до 100. Обнулить все значения в массиве, расположенные вне квадрата размера 3х3 вокруг максимального элемента. Если максимумов несколько, обнулите элементы около любого из них.

- [x] Проверено на семинаре
t = th.randint(0, 100, (5, 5))
t_copy = t.clone()
print(f't max is {t.max()}')
t
d0, d1 = th.where(t == t.max())
d0, d1 = d0[0].item(), d1[0].item()
mask = th.zeros(t.shape, dtype=th.bool)
mask[max(d0 - 1, 0):min(d0 + 2, t.shape[0]), max(d1 - 1, 0):min(d1 + 2, t.shape[1])] = True
t_copy[~mask] = 0
t_copy
# Markdown:
<p class="task" id="4"></p>

4\. Создайте трехмерный массив размерности (2, 5, 5) на основе решения задачи 3 (объедините исходный и результирущий тензор вдоль нулевой оси). Сохраните полученный трехмерный тензор в файл `tensor.pt`. Загрузите полученный тензор и покажите, что все элементы двух тензоров совпадают.

- [x] Проверено на семинаре
import os
t_2_dim =  th.stack([t, t_copy], dim=0)
t_2_dim
t_2_dim.shape
th.save(t_2_dim, 'tensor.pt')
tensor = th.load('tensor.pt')
os.system('rm tensor.pt')
tensor
th.equal(t_2_dim, tensor)
# Markdown:
<p class="task" id="5"></p>

5\. Создайте четырехмерный массив `t` размерности (2, 3, 5, 5), заполненный случайными целыми числами от 1 до 10 (сами значения должны быть представлены типом float32). Рассчитайте среднее значение для каждого двумерного тензора `t[i, j, :, :]`. Представьте результат в виде трехмерного тензора размера (2, 3, 1).

- [x] Проверено на семинаре
t = th.randint(1, 10, size=(2, 3, 5, 5), dtype=th.float32)
t.shape
t_mean = t.mean(dim=(2, 3), keepdim=True)
print(f'shape is {t_mean.shape}')
t_mean
# Markdown:
<p class="task" id="6"></p>

6\. Создайте одномерный тензор размера `N=100_000_000`, заполненный числами из экспоненциального распредления с параметром $\lambda=5$. Рассчитайте значения для построения гистограммы при помощи пакета `torch`. Визуализируйте гистограмму. Проверьте возможность использования GPU. При наличии GPU перенесите созданный тензор в память GPU, повторите вычисления. Сравните время расчетом с и без использования GPU.

- [x] Проверено на семинаре
import matplotlib.pyplot as plt
import seaborn as sns
N, l = 100_000_000, 5
exp = th.distributions.Exponential(rate=l**-1)
t = exp.sample((100000000,))
t
th_hist = t.histogram()
sns.barplot(th_hist.hist)
plt.xticks(ticks=np.arange(0, 101, 10))
plt.show()
device = th.device('cuda' if th.cuda.is_available() else 'cpu')
device
t.to('cpu')
%timeit t.histogram()
t.to(device)
%timeit t.histogram()
...
# Markdown:
<p class="task" id="7"></p>

7\. Создайте четырехмерный тензор размера (10, 6, 6, 3), заполненный случайными целыми числами от 0 до 255. Считая, что данный тензор представляет собой батч из 10 картинок размера 6х6 в формате RGB, измените тензор следующим образом. Для оттенков красного обнулите все столбцы, кроме первых двух; для оттенков зеленого обнулите третий и четвертый столбцы; для оттенков синего обнулите пятый и шестой столбцы. Для выполнения задания используйте механизм распространения.

- [ ] Проверено на семинаре
t = th.randint(0, 256, (10, 6, 6, 3), dtype=th.uint8)
t.shape
batch_size, height, width, channels = t.shape
fig, axes = plt.subplots(1, batch_size, figsize=(15, 15))
for i in range(batch_size):
    axes[i].imshow(t[i].numpy())
    axes[i].axis('off')
plt.show()
# для красного канала
t[:, :, :, 0] *= th.tensor([[1, 1, 0, 0, 0, 0]])

# для зеленого канала
t[:, :, :, 1] *= th.tensor([[1, 1, 0, 0, 1, 1]])

# для синего канала
t[:, :, :, 2] *= th.tensor([[1, 1, 1, 1, 0, 0]])
batch_size, height, width, channels = t.shape
fig, axes = plt.subplots(1, batch_size, figsize=(15, 15))
for i in range(batch_size):
    axes[i].imshow(t[i].numpy())
    axes[i].axis('off')
plt.show()



# 01_2_data.ipynb
# Markdown:
# Подготовка данных для обучения моделей

__Автор задач: Блохин Н.В. (NVBlokhin@fa.ru)__

Материалы:
* https://pytorch.org/docs/stable/data.html
* https://pytorch.org/tutorials/beginner/data_loading_tutorial.html
* Deep Learning with PyTorch (2020) Авторы: Eli Stevens, Luca Antiga, Thomas Viehmann

# Markdown:
## Задачи для совместного разбора
# Markdown:
1. Создайте синтетический датасет для задачи регрессии и представьте его в виде `torch.utils.data.Dataset`
import torch as th
from torch.utils.data import Dataset
from sklearn.datasets import make_regression
X, y = make_regression(n_samples=1000, n_features=10)
X.shape, y.shape, type(X)
from typing import Callable

class RegressionDataset(Dataset):
    def __init__(self, transform: Callable | None = None, **kwargs):
        super().__init__()
        self.X, self.y = make_regression(**kwargs)
        self.transform = transform

    def custom_method(self):
        ...

    def __getitem__(self, idx):
        x = self.X[idx]
        if self.transform is not None:
            x = self.transform(x)
        y = self.y[idx]

        return x, y

    def __len__(self):
        return len(self.X)
def f(x):
    return x
f(5)
class MyCallable:
    def __call__(self, x):
        return x
c = MyCallable()
c(5)
dataset = RegressionDataset(n_samples=1000, n_features=10)
dataset[0]
import numpy as np

def my_transformer(x: np.ndarray) -> np.ndarray:
    return 1000 * x
class MyCallable:
    def __init__(self, coef: int) -> None:
        self._coef = coef

    def __call__(self, x: np.ndarray) -> np.ndarray:
        return self._coef * x
dataset = RegressionDataset(
        transform=my_transformer,
        n_samples=1000,
        n_features=10
)
dataset[0]
dataset = RegressionDataset(
        transform=MyCallable(coef=10000),
        n_samples=1000,
        n_features=10
)
dataset[0]
dataset[:2]
from torch.utils.data import random_split
train, val, test = random_split(dataset, lengths=[0.7, 0.15, 0.15])
train[0]
len(train)
from torch.utils.data import DataLoader
loader = DataLoader(train, 64, )

it = iter(loader)
x, y = next(it)
x.shape, y.shape
for x, y in loader:
    print(x.shape, y.shape)
    # break
700 % 64
# Markdown:
## Задачи для самостоятельного решения
# Markdown:
<p class="task" id="1"></p>

1\. Считайте файл `bank-full.csv` ([источник](https://www.kaggle.com/datasets/hariharanpavan/bank-marketing-dataset-analysis-classification)) в виде `pd.DataFrame`.

Опишите класс `BankDatasetBase`. Решение должно удовлетворять следующим критериям:

* класс наследуется от `torch.utils.data.Dataset`;
* при создании объекта в конструктор передается набор данных в виде `pd.DataFrame`;
* объекты класса имеют поля `X` и `y` с признаками и метками соответственно;
* класс реализует интерфейс последовательностей (`__getitem__` + `__len__`);
* `obj[i]` возвращает кортеж, содержащий `i`-ую строку из `obj.X` (серию) и `i`-ую строку из `obj.y` (строку).
        
Создайте объект класса `BankDatasetBase` и продемонстрируйте работоспособность.

- [ ] Проверено на семинаре
import pandas as pd
data = pd.read_csv('./../data/bank-full.csv')
data.head(3)
class BankDatasetBase(Dataset):
    def __init__(self, data: pd.DataFrame) -> None:
        super().__init__()
        self.data = data
        self.X, self.y = data.drop(columns='y'), data['y']

    def __getitem__(self, idx: int) -> tuple:
        x = self.X.iloc[idx]
        y = self.y.iloc[idx]
        return x, y

    def __len__(self) -> int:
        return self.X.shape[0]
bank_dataset = BankDatasetBase(data)
bank_dataset[0]
# Markdown:
<p class="task" id="2"></p>

2\. Опишите класс `BankDataset`. Решение должно удовлетворять всем критериям из предыдущего задания, а также:
* при создании объекта в конструктор может быть передан необязательные аргументы `transform` и `target_transform`;
* если аргумент `transform` был передан, то при получении `i`-го элемента, нужно вызвать `transform(x)` и вернуть полученный результат.
* если аргумент `target_transform` был передан, то при получении `i`-го элемента, нужно вызвать `target_transform(y)` и вернуть полученный результат.

Создайте объект класса `BankDataset` и продемонстрируйте работоспособность (без передачи `target_transform` и `transform`).

- [ ] Проверено на семинаре
class BankDataset(Dataset):
    def __init__(
        self,
        data: pd.DataFrame,
        transform: Callable | None = None,
        target_transform: Callable | None = None
    ) -> None:
        super().__init__()
        self.data = data
        self.X, self.y = data.drop(columns='y'), data['y']
        self.transform = transform
        self.target_transform = target_transform

    def __getitem__(self, idx: int) -> tuple:
        x = self.X.iloc[idx]
        y = self.y.iloc[idx]
        if self.transform is not None:
            x, y = self.transform(x, y)
        if self.target_transform is not None:
            x, y = self.target_transform(x, y)
        return x, y

    def __len__(self) -> int:
        return self.X.shape[0]
bank_dataset = BankDataset(data)
bank_dataset[0]
# Markdown:
<p class="task" id="3"></p>

3\. Опишите класс `OrdinalEncoderTransform`. Решение должно удовлетворять следующим критериям:

* при создании объекта в конструктор передаются названия нечисловых столбцов в датасете
* класс реализует интерфейс `Callable` (`__call__`); метод `__call__` имеет один параметр (признаки) и возвращает набор признаков, в котором нечисловые характеристики закодированы целыми числами;
* состояние объекта (индексы для кодирования) обновляется в момент очередного вызова `__call__` (т.е. все данные сразу никогда не передаются никакому методу объекта).
        
Продемонстрируйте работоспособность, создав объект `BankDataset` и передав при создании объект класса `OrdinalEncoderTransform`.

- [ ] Проверено на семинаре
category_columns = data.select_dtypes('object').columns.tolist()
category_columns
class Transform:
    def __init__(self, columns=[]):
        self.columns = columns
        self.encoders = {col: {} for col in columns}
class OrdinalEncoderTransform(Transform):
    def __init__(self, category_columns: list[str]) -> None:
        super().__init__(category_columns)

    def __call__(self, x_: pd.Series, y_: str) -> pd.Series:
        x = x_.copy()
        for col in self.columns:
            if col in x:
                if x[col] not in self.encoders[col]:
                    self.encoders[col][x[col]] = len(self.encoders[col])
                x.at[col] = self.encoders[col][x[col]]
        return x, y
transform = OrdinalEncoderTransform(category_columns)
bank_dataset = BankDataset(data, transform=transform)
x, y = bank_dataset[0]
x
x, y = bank_dataset[20]
x
# Markdown:
<p class="task" id="4"></p>

4\. Опишите класс `LabelEncoderTransform`. Решение должно удовлетворять следующим критериям:

* класс реализует интерфейс `Callable` (`__call__`); метод `__call__` имеет один параметр (строку) и возвращает целое число, соответствующее этой строке;
* состояние объекта (индексы для кодирования) обновляется в момент очередного вызова `__call__` (т.е. все данные сразу никогда не передаются никакому методу объекта).
        
Продемонстрируйте работоспособность, создав объект `BankDataset` и передав при создании объекта в качестве аргумента `target_transform` объект класса `LabelEncoderTransform`.

- [ ] Проверено на семинаре
class LabelEncoderTransform(Transform):
    def __init__(self) -> None:
        super().__init__()

    def __call__(self, x: pd.Series, y: str) -> int:
        if y not in self.encoders:
            self.encoders[y] = len(self.encoders)
        y = self.encoders[y]

        return x, y
transform = OrdinalEncoderTransform(category_columns)
label_encoder = LabelEncoderTransform()
bank_dataset = BankDataset(data, transform=transform, target_transform=label_encoder)
x, y = bank_dataset[0]
y
x, y = bank_dataset[83]
y
# Markdown:
<p class="task" id="5"></p>

5\. Опишите класс `ToTensor`.    Решение должно удовлетворять следующим критериям:
* класс реализует интерфейс `Callable` (`__call__`); метод `__call__` принимает на вход серию или фрейм и возвращает тензор.

Опишите класс `Compose`.    Решение должно удовлетворять следующим критериям:
* при создании объекта в конструктор передается список объектов `transforms`, каждый из которых имеет метод `__call__(x, y)`;
* класс реализует интерфейс `Callable` (`__call__`); метод `__call__` принимает имеет параметра (признаки и класс в числовом виде) и и возвращает кортеж, полученный путем последовательного вызова объектов из `transforms`.

Продемонстрируйте работоспособность, создав объект `BankDataset` и передав при создании преобразования `Compose` список из объектов LabelEncoderTransform и ToTensor.

- [ ] Проверено на семинаре
from typing import Any
class ToTensor:
    def __call__(self, X: pd.Series | int, y: Any) -> tuple:
        return th.tensor(X.values.astype(float) if isinstance(X, pd.Series) else X), \
            th.tensor(y)

class Compose(Transform):
    def __init__(self, transforms: list[Transform]) -> None:
        self.transforms = transforms

    def __call__(self, x: Any, y: Any) -> Any:
        for transform in self.transforms:
            x, y = transform(x, y)
        return x, y
composed_transform = Compose([OrdinalEncoderTransform(category_columns), LabelEncoderTransform(), ToTensor()])
bank_dataset = BankDataset(data, transform=composed_transform)
x, y = bank_dataset[0]
x, y
x, y = bank_dataset[83]
x, y
# Markdown:
<p class="task" id="6"></p>

6\. Разделите датасет из предыдущего задания на обучающую и тестовую выборку в соотношении 75% на 25%. Создайте объект `DataLoader` для получения пакетов размера 64, полученных из перемешанного обучающего датасета. Кастомизируйте `DataLoader` таким образом, чтобы пакет признаков был представлен в виде трехмерного тензора размера 64x2x8 (разделите 16 признаков на два тензора по 8). Получите один пакет и выведите на экран размерность тензоров пакета.

- [ ] Проверено на семинаре
train, test = random_split(bank_dataset, lengths=[0.75, 0.25])
train_loader = DataLoader(train, 64)

it = iter(train_loader)
x, y = next(it)
x.shape, y.shape
def collate_fn(batch):
    x, y = zip(*batch)
    x = th.stack(x).view(-1, 2, 8)
    y = th.tensor(y)
    return x, y
train_loader = DataLoader(train, 64, shuffle=True, collate_fn=collate_fn)
x, y = next(iter(train_loader))
print(x.shape, y.shape)


# 02_1_nn_derivatives.ipynb
# Markdown:
# Дифференцирование

__Автор задач: Блохин Н.В. (NVBlokhin@fa.ru)__

Материалы:
* https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.axhline.html#matplotlib.pyplot.axhline
* https://numpy.org/doc/stable/reference/generated/numpy.log1p.html#numpy.log1p
* https://docs.sympy.org/latest/tutorials/intro-tutorial/calculus.html
* https://en.wikipedia.org/wiki/Finite_difference
* https://pythonnumericalmethods.berkeley.edu/notebooks/chapter20.02-Finite-Difference-Approximating-Derivatives.html
* https://en.wikipedia.org/wiki/Gradient_descent
* https://pytorch.org/tutorials/beginner/blitz/autograd_tutorial.html
* https://zhang-yang.medium.com/the-gradient-argument-in-pytorchs-backward-function-explained-by-examples-68f266950c29
# Markdown:
## Задачи для совместного разбора
# Markdown:
1\. Дана функция $f(x) = x^2$. Найдите производную этой функции различными способами
def f(x: float) -> float:
  return x**2
def dfdx(x: float) -> float:
  return 2 * x
f(2), dfdx(2)
from typing import Callable


def dfdx_finite(f: Callable, x: float, h: float) -> float:
  return (f(x+h) - f(x)) / h
dfdx_finite(f, 2, 1e-5)
import torch as th
x = th.tensor(10.0, requires_grad=True)
x
def g(x: th.Tensor) -> th.Tensor:
  return x ** 2
y = g(x)
y
y.backward()
x.grad # dy/dx
type(x)
th.tensor
x = th.tensor(10.0, requires_grad=True)
y = f(x)
y.backward()
grad = x.grad

gamma = 0.01
x = x - gamma * grad
# x -= gamma * grad  # will throw error
x = th.tensor([10.0, 20.0, 30.0], requires_grad=True)
y = g(x)
# y = [x1**2, x2**2, x3**2]
try:
    y.backward()
except Exception as e:
    print(e)
x1, x2, x3 = x
J = th.tensor(
    [[2*x1, 0, 0],
    [0, 2*x2, 0],
    [0, 0, 2*x3],]
)
J
z = th.ones((3, 1))
J @ z
x = th.tensor([10.0, 20.0, 30.0], requires_grad=True)
y = g(x)
# y = [x1**2, x2**2, x3**2]
y.backward(th.ones((3, )))
x.grad
# Markdown:
## Задачи для самостоятельного решения
import torch as th
import numpy as np

import matplotlib.pyplot as plt
%matplotlib inline
# Markdown:
<p class="task" id="1"></p>

1\. Дана функция $f(x)$. Найдите (аналитически) производную данной функции $f'(x)$ и реализуйте две этих функции. Постройте в одной системе координат графики $f(x)$, $f'(x)$ и $g(x) = 0$ на отрезке [1, 10]. Изобразите графики различными цветами и включите сетку.

$$f(x) = \frac{sin(x)}{\ln(x) + 1}$$

- [ ] Проверено на семинаре
f = lambda x: np.sin(x) / (np.log(x) + 1)
dfdx = lambda x: (np.cos(x)*(np.log(x) + 1) - np.sin(x)/x) / (np.log(x) + 1)**2
g = lambda _: 0
lsp = np.linspace(1, 10)
plt.plot(lsp, [f(i) for i in lsp], color='b', label='f(x)')
plt.plot(lsp, [dfdx(i) for i in lsp], color='g', label='dfdx(x)')
plt.plot(lsp, [g(i) for i in lsp], color='r', label='g(x)')

plt.grid(True)
plt.legend()
plt.show()
# Markdown:
<p class="task" id="2"></p>

2\. Дана функция $f(x)$. Найдите (численно) производную данной функции $f'(x)$ на отрезке [1, 10]. Постройте в одной системе координат график $f(x)$, $f'(x)$ и $g(x) = 0$. Изобразите графики различными цветами и включите сетку.

$$f(x) = \frac{sin(x)}{\ln(x) + 1}$$

- [ ] Проверено на семинаре
f = lambda x: np.sin(x) / (np.log(x) + 1)
g = lambda _: 0
h = 0.001
lsp = np.linspace(1, 10)
plt.plot(lsp, [f(i) for i in lsp], color='b', label='f(x)')
plt.plot(lsp, [(f(x+h) - f(x)) / h for x in lsp], color='g', label='dfdx(x) числ.')
plt.plot(lsp, [g(i) for i in lsp], color='r', label='g(x)')

plt.grid(True)
plt.legend()
plt.show()
# Markdown:
<p class="task" id="3"></p>

3\. Найдите локальный минимум функции $f(x)$ при помощи метода градиентного спуска. В качестве начальной точки используйте $x_0 = 4$. Найдите локальный максимум этой же функции, используя в качестве начальной точки $x_0'=9$.

$$f(x) = \frac{sin(x)}{\ln(x) + 1}$$

- [ ] Проверено на семинаре
f = lambda x: np.sin(x) / (np.log(x) + 1)
dfdx = lambda x: (np.cos(x)*(np.log(x) + 1) - np.sin(x)/x) / (np.log(x) + 1)**2
def gradient(f, x0, lr=0.01, h=1e-6, tol=1e-6, max_iter=1000):
    x = x0
    for _ in range(max_iter):
        grad = (f(x + h) - f(x)) / h
        x_new = x - lr*grad
        if abs(x_new - x) < tol:
            break
        x = x_new
    return x
x_min = gradient(f, 4, 0.01, max_iter=5000)
f'Локальный минимум в точке {x_min}'
x_max = gradient(lambda x: -f(x), 9, 0.01, max_iter=5000)
f'Локальный максимум в точке {x_max}'
h = 0.001
lsp = np.linspace(1, 10)
plt.plot(lsp, [f(i) for i in lsp], color='b', label='f(x)')
plt.plot(lsp, [(f(x+h) - f(x)) / h for x in lsp], color='g', label='dfdx(x) числ.')
plt.plot(lsp, [g(i) for i in lsp], color='r', label='g(x)')
plt.scatter([x_min, x_max], [f(x_min), f(x_max)], color='#FF0FAF', label='min/max')

plt.grid(True)
plt.legend()
plt.show()
f = lambda x: th.sin(x) / (th.log(x) + 1)
def gradient(f, x, lr=0.01, epochs=1000):
    x = th.tensor(x, dtype=th.float32, requires_grad=True)
    optimizer = th.optim.SGD([x], lr=lr)
    for epoch in range(epochs):
        optimizer.zero_grad()
        y = f(x)
        y.backward()
        optimizer.step()
    return x.detach().item()
x_min = gradient(f, 4, 0.01)
print(f'Локальный минимум в точке {x_min}')
x_max = gradient(lambda x: -f(x), 9, 0.01)
f'Локальный максимум в точке {x_max}'
# Markdown:
<p class="task" id="4"></p>

4\. Дана функция $f(x)$. Найдите (используя возможности по автоматическому дифференцированию пакета `torch`) производную данной функции $f'(x)$ на отрезке [0, 10]. Постройте в одной системе координат график $f(x)$, $f'(x)$ и $g(x) = 0$ на полуинтервале (0, 10]. Изобразите графики различными цветами и включите сетку.

$$f(x) = \frac{sin(x)}{\ln(x) + 1}$$

- [ ] Проверено на семинаре
f = lambda x: th.sin(x) / (th.log(x) + 1)
x = th.linspace(0.5, 10, 5000, requires_grad=True)
y = f(x)
x, y
y.backward(th.ones((x.shape[0], )))
plt.plot(x.detach().numpy(), y.detach().numpy(), color='b', label='f(x)')
plt.plot(x.detach().numpy(), x.grad.detach().numpy(), color='g', label='dfdx(x) torch')
plt.plot(x.detach().numpy(), [0]*len(x), color='r', label='g(x)')

plt.grid(True)
plt.legend()
plt.show()
# Markdown:
<p class="task" id="5"></p>

5\. Дана функция $f(x)$. Найдите производную данной функции $f'(x)$ на отрезке [0, 10] при помощи формулы производной сложной функции. На этом же отрезке найдите, используя возможности по автоматическому дифференцированию пакета `torch`. Сравните результаты.

$$f(x) = sin(cos(x))$$

- [ ] Проверено на семинаре
def g(x: th.Tensor) -> th.Tensor:
    return th.sin(x)

def h(x: th.Tensor) -> th.Tensor:
    return th.cos(x)

def dfdg(x: th.Tensor) -> th.Tensor:
    return th.cos(x)

def dgdx(x: th.Tensor) -> th.Tensor:
    return -th.sin(x)

def dfdx(x: th.Tensor) -> th.Tensor:
    return dfdg(h(x)) * dgdx(x)
x = 0.5
out = -np.cos(np.cos(x)) * np.sin(x)
f'{out = :.4f}'
x = th.tensor(x, requires_grad=True)
out = dfdx(x)
f'{out = :.4f}'
def f(x: th.Tensor) -> th.Tensor:
    return th.sin(th.cos(x))
x = th.linspace(0.5, 10, 5000, requires_grad=True)
y = f(x)
y.backward(th.ones_like(x))
x.grad
plt.plot(x.detach().numpy(), y.detach().numpy(), color='b', label='f(x)')
plt.plot(x.detach().numpy(), dfdx(x).detach().numpy(), color='r', label='dfdx(x) hands')
plt.plot(x.detach().numpy(), x.grad.detach().numpy(), color='g', label='dfdx(x) torch')

plt.grid(True)
plt.legend()
plt.show()
out_hands = dfdx(x).detach().numpy()
out_torch = x.grad.detach().numpy()
(out_hands - out_torch).sum()


# 02_2_nn_forward.ipynb
# Markdown:
#  Forward pass

__Автор задач: Блохин Н.В. (NVBlokhin@fa.ru)__

Материалы:
* Deep Learning with PyTorch (2020) Авторы: Eli Stevens, Luca Antiga, Thomas Viehmann
* https://pytorch.org/docs/stable/generated/torch.matmul.html
* https://machinelearningmastery.com/choose-an-activation-function-for-deep-learning/
* https://machinelearningmastery.com/loss-and-loss-functions-for-training-deep-learning-neural-networks/
* https://kidger.site/thoughts/jaxtyping/
* https://github.com/patrick-kidger/torchtyping/tree/master
# Markdown:
## Задачи для совместного разбора
# !pip install torchtyping
from torchtyping import TensorType, patch_typeguard
from typeguard import typechecked
import torch as th

Scalar = TensorType[()]
patch_typeguard()
# Markdown:
1\. Используя операции над матрицами и векторами из библиотеки `torch`, реализуйте нейрон с заданными весами `weights` и `bias`. Пропустите вектор `inputs` через нейрон и выведите результат.
class Neuron:
    def __init__(self, n_features: int, bias: float) -> None:
        self.weights: TensorType["n_features"] = th.rand(n_features)
        self.bias: float = bias

    @typechecked
    def forward(self, inputs: TensorType["n_features"]) -> Scalar:
        return inputs @ self.weights + self.bias
import torch as th
inputs = th.tensor([1.0, 2.0, 3.0, 4.0])
neuron = Neuron(n_features=4, bias=0.0)
neuron.forward(inputs)
# Markdown:
2\. Используя операции над матрицами и векторами из библиотеки `torch`, реализуйте функцию активации ReLU:

![](https://wikimedia.org/api/rest_v1/media/math/render/svg/f4353f4e3e484130504049599d2e7b040793e1eb)

Создайте матрицу размера (4,3), заполненную числами из стандартного нормального распределения, и проверьте работоспособность функции активации.
class ReLU:
    @typechecked
    def forward(self, inputs: TensorType["n_features"]) -> TensorType["n_features"]:
        return th.clip(inputs, min=0)
inputs = th.tensor([1.0, -2.0, 3.0, -4.0])

act = ReLU()
act.forward(inputs)
# Markdown:
3\. Используя операции над матрицами и векторами из библиотеки `torch`, реализуйте функцию потерь MSE:

![](https://wikimedia.org/api/rest_v1/media/math/render/svg/e258221518869aa1c6561bb75b99476c4734108e)
где $Y_i$ - правильный ответ для примера $i$, $\hat{Y_i}$ - предсказание модели для примера $i$, $n$ - количество примеров в батче.
class MSELoss:
    @typechecked
    def forward(
        self,
        y_pred: TensorType["batch"],
        y_true: TensorType["batch"]
    ) -> Scalar:
        return ((y_pred - y_true)**2).mean()
y_pred = th.tensor([1.0, 3.0, 5.0])
y_true = th.tensor([2.0, 3.0, 4.0])
criterion = MSELoss()
loss = criterion.forward(y_pred, y_true)
loss
# Markdown:
## Задачи для самостоятельного решения
# Markdown:
### Cоздание полносвязных слоев
# Markdown:
<p class="task" id="1"></p>

1\. Используя операции над матрицами и векторами из библиотеки `torch`, реализуйте полносвязный слой из `n_neurons` нейронов с `n_features` весами у каждого нейрона (инициализируются из стандартного нормального распределения) и опциональным вектором смещения.

$$y = xW^T + b$$

Пропустите вектор `inputs` через слой и выведите результат. Результатом прогона сквозь слой должна быть матрица размера `batch_size` x `n_neurons`.

- [x] Проверено на семинаре
from typing import Any
class Linear:
    def __init__(self, n_neurons: int, n_features: int, bias: bool = False) -> None:
        self.n_neurons = n_neurons
        self.n_features = n_features

        fgen = th.randn if bias else th.zeros
        self.weights: TensorType["n_neurons", "n_features"] = th.randn((n_neurons, n_features))
        self.bias_: TensorType["n_neurons"] = fgen((self.n_neurons,))
    
    @typechecked()
    def forward(self, inputs: TensorType["batch", "feats"]) -> TensorType["batch", "n_neurons"]:
        return inputs @ self.weights.T + self.bias_
    
    def __call__(self, *args: Any, **kwds: Any) -> Any:
        return self.forward(*args, **kwds)
linear = Linear(2, 4, bias=False)
linear(th.randn((3, 4)))
linear = Linear(2, 4, bias=True)
linear(th.randn((3, 4)))
# Markdown:
<p class="task" id="2"></p>

2\. Используя решение предыдущей задачи, создайте 2 полносвязных слоя и пропустите тензор `inputs` последовательно через эти два слоя. Количество нейронов в первом слое выберите произвольно, количество нейронов во втором слое выберите так, чтобы результатом прогона являлась матрица `batch_size x 7`.

- [x] Проверено на семинаре
linear1 = Linear(10, 4, bias=False)
linear2 = Linear(7, linear1.n_neurons, bias=False)
out1 = linear1(th.randn((3, 4)))
out2 = linear2(out1)
out2
out1.shape
out2.shape
# Markdown:
### Создание функций активации
# Markdown:
<p class="task" id="3"></p>

3\. Используя операции над матрицами и векторами из библиотеки `torch`, реализуйте функцию активации softmax:

![](https://wikimedia.org/api/rest_v1/media/math/render/svg/6d7500d980c313da83e4117da701bf7c8f1982f5)

$$\overrightarrow{x} = (x_1, ..., x_J)$$

Создайте матрицу размера (4,3), заполненную числами из стандартного нормального распределения, и проверьте работоспособность функции активации. Строки матрицы трактовать как выходы линейного слоя некоторого классификатора для 4 различных примеров. Функция должна применяться переданной на вход матрице построчно.

- [x] Проверено на семинаре
class Softmax:
    def __init__(self) -> None:
        pass

    @typechecked()
    def forward(self, inputs: TensorType["batch", "feats"]) -> TensorType["batch", "feats"]:
        exp: TensorType["batch", "feats"] = th.exp(inputs)
        return exp / exp.sum(dim=1, keepdim=True)
    
    def __call__(self, *args: Any, **kwds: Any) -> Any:
        return self.forward(*args, **kwds)
inputs = th.randn((4, 3))
inputs
softmax = Softmax()
softmax(inputs)
# Markdown:
<p class="task" id="4"></p>

4 Используя операции над матрицами и векторами из библиотеки `torch`, реализуйте функцию активации ELU:

![](https://wikimedia.org/api/rest_v1/media/math/render/svg/eb23becd37c3602c4838e53f532163279192e4fd)

Создайте матрицу размера 4x3, заполненную числами из стандартного нормального распределения, и проверьте работоспособность функции активации.

- [x] Проверено на семинаре
class ELU:
    def __init__(self, alpha: float) -> None:
        self.alpha = alpha

    @typechecked()
    def forward(self, inputs: TensorType["batch", "feats"]) -> TensorType["batch", "feats"]:
        return th.where(inputs >= 0, inputs, self.alpha*(th.exp(inputs) - 1))
    
    def __call__(self, *args: Any, **kwds: Any) -> Any:
        return self.forward(*args, **kwds)
inputs = th.randn((4, 3))
inputs
elu = ELU(0.001)
elu(inputs)
# Markdown:
### Создание функции потерь
# Markdown:
<p class="task" id="5"></p>

5 Используя операции над матрицами и векторами из библиотеки `torch`, реализуйте функцию потерь CrossEntropyLoss:

$$y_i = (y_{i,1},...,y_{i,k})$$



$$ CrossEntropyLoss = \frac{1}{n}\sum_{i=1}^{n}{L_i}$$
где $y_i$ - вектор правильных ответов для примера $i$, $\hat{y_i}$ - вектор предсказаний модели для примера $i$; $k$ - количество классов, $n$ - количество примеров в батче.

Создайте полносвязный слой с 2 нейронами и прогнать через него батч `inputs`. Полученный результат пропустите через функцию активации Softmax. Посчитайте значение функции потерь, трактуя вектор `y` как вектор правильных ответов.

- [x] Проверено на семинаре
class CrossEntropyLoss:
    def __init__(self) -> None:
        pass

    @typechecked()
    def forward(self, y_pred: TensorType["batch", "classes", float], y_true: TensorType["batch", int]):
        return -(th.log(y_pred).gather(1, y_true.unsqueeze(1))).mean()
    
    def __call__(self, *args: Any, **kwds: Any) -> Any:
        return self.forward(*args, **kwds)
layer = Linear(2, 4, bias=True)
out = layer.forward(th.randn(10, 4))
out
softmax = Softmax()
y_pred = softmax.forward(out)
y_pred
loss = CrossEntropyLoss()
loss.forward(y_pred, y_true=th.randint(0, 2, (10,)))
# Markdown:
<p class="task" id="6"></p>

6 Модифицируйте MSE, добавив L2-регуляризацию.

$$MSE_R = MSE + \lambda\sum_{i=1}^{m}w_i^2$$

где $\lambda$ - коэффициент регуляризации; $w_i$ - веса модели.

- [x] Проверено на семинаре
from typing import Any


class MSERegularized:
    def __init__(self, lambda_: float) -> None:
        self.lambda_ = lambda_

    @typechecked
    def data_loss(
        self,
        y_pred: TensorType["batch"],
        y_true: TensorType["batch"],
    ) -> Scalar:
        return ((y_true - y_pred) ** 2).mean()

    @typechecked
    def reg_loss(self, weights: TensorType["batch", 1]) -> Scalar:
        return th.sum(weights ** 2)

    @typechecked()
    def forward(
        self,
        y_pred: TensorType["batch"],
        y_true: TensorType["batch"],
        weights: TensorType["batch", 1],
    ) -> Scalar:
        return self.data_loss(y_pred, y_true) + self.lambda_ * self.reg_loss(weights)

    def __call__(self, *args: Any, **kwds: Any) -> Any:
        return self.forward(*args, **kwds)
y_pred = th.randn(2)
y_true = th.randn(2)
weights = th.randn(2).unsqueeze(1)
y_true, y_pred, th.sum(weights**2)
loss_fn = MSERegularized(lambda_=0.001)
loss_fn(y_pred, y_true, weights)


# 02_3_nn_backprop.ipynb
# Markdown:
#  Обратное распространение ошибки

__Автор задач: Блохин Н.В. (NVBlokhin@fa.ru)__

Материалы:
* Deep Learning with PyTorch (2020) Авторы: Eli Stevens, Luca Antiga, Thomas Viehmann
* http://cs231n.stanford.edu/handouts/linear-backprop.pdf
* https://www.adityaagrawal.net/blog/deep_learning/bprop_fc
* https://en.wikipedia.org/wiki/Stochastic_gradient_descent
# !pip install torchtyping
from torchtyping import TensorType, patch_typeguard
from typeguard import typechecked
import torch as th

Scalar = TensorType[()]
patch_typeguard()
# Markdown:
## Задачи для совместного разбора
# Markdown:
1\. Реализуйте обратное распространение ошибки для модели нейрона с квадратичной функцией потерь при условии, что на вход нейрону поступает вектор `inputs`. Проверьте корректность вычисления градиентов, воспользовавшись возможностями по автоматическому дифференцированию `torch`.
class Neuron:
    def __init__(self, n_features: int, bias: float, requires_grad: bool = False) -> None:
        # <создать атрибуты объекта weights и bias>
        self.weights: TensorType["n_features"] = th.rand(n_features, requires_grad=requires_grad)
        self.bias: Scalar = th.tensor(bias, requires_grad=requires_grad)

    @typechecked
    def forward(self, inputs: TensorType["n_features"]) -> Scalar:
        return inputs @ self.weights + self.bias

    def backward(self, inputs: TensorType["n_features"], dnext: Scalar):
      self.dweights = dnext * inputs
      self.dbias = dnext
      self.dinputs = dnext * self.weights

class Loss:
    def forward(
        self,
        y_pred: Scalar,
        y_true: Scalar
    ) -> Scalar:
        return (y_pred - y_true)**2

    def backward(self, y_pred: Scalar, y_true: Scalar) -> Scalar:
      self.dinput = 2 * (y_pred - y_true)
neuron = Neuron(n_features=2, bias=0.0, requires_grad=True)
criterion = Loss()

inputs = th.tensor([2.0, 3.0])
y_true = th.tensor(10)

# forward pass
y_pred = neuron.forward(inputs)
loss = criterion.forward(y_pred, y_true)

# backprop
criterion.backward(y_pred, y_true)
neuron.backward(inputs, criterion.dinput)
loss
criterion.dinput
neuron.dweights
neuron.dbias
loss.backward()
neuron.weights.grad
neuron.bias.grad
loss
# Markdown:
2\. Настройте модель нейрона, используя метод стохастического градиентного спуска и собственную реализацию обратного распространения ошибки.
from sklearn.datasets import make_regression
import numpy as np

X, y, coef = make_regression(n_features=4, n_informative=4, coef=True, bias=0.5, random_state=42)
X = th.FloatTensor(X)
y = th.FloatTensor(y)
import numpy as np
neuron = Neuron(n_features=4, bias=0.0, requires_grad=False)
criterion = Loss()
lr = 0.1

for epoch in range(3):
    losses = []
    for x_i, y_i in zip(X, y):
        y_pred = neuron.forward(x_i)
        loss_i = criterion.forward(y_pred, y_i)
        losses.append(loss_i.item())

        criterion.backward(y_pred, y_i)
        neuron.backward(x_i, criterion.dinput)

        neuron.weights -= lr * neuron.dweights
        neuron.bias -= lr * neuron.dbias
    print(f"{epoch} {np.mean(losses)}")
neuron.weights
neuron.bias
coef
# Markdown:
## Задачи для самостоятельного решения
# Markdown:
<p class="task" id="1"></p>

1\. Реализуйте обратное распространение ошибки для модели нейрона с функцией потерь MSE при условии, что на вход нейрону поступает пакет (двумерный тензор) `inputs`. Проверьте корректность вычисления градиентов, воспользовавшись возможностями по автоматическому дифференцированию `torch`.

$$\mathbf{X} = \begin{bmatrix}
x_{10} & x_{11} & \ldots & x_{1m} \\
x_{20} & x_{21} & \ldots & x_{2m} \\
\vdots & \vdots & \ddots & \vdots \\
x_{k0} & x_{k1} & \ldots & x_{km} \\
\end{bmatrix}
\mathbf{Y} = \begin{bmatrix}
y_{1} \\
y_{2} \\
\vdots \\
y_{k} \\
\end{bmatrix}
\mathbf{W} = \begin{bmatrix}
w_{0} \\
w_{1} \\
\vdots \\
w_{m} \\
\end{bmatrix}$$

$$\hat{\mathbf{Y}} = \mathbf{X}\times \mathbf{W}$$

$$L = \frac{1}{k}\sum_{k}{(\hat{y_k}-y_k)^2}$$

$$\nabla_{\hat{\mathbf{Y}}} L=\begin{bmatrix}
\frac{\partial L}{\partial \hat{y_1}} \\
\frac{\partial L}{\partial \hat{y_2}} \\
\vdots \\
\frac{\partial L}{\partial \hat{y_k}} \\
\end{bmatrix} = \frac{2}{k}\begin{bmatrix}
\hat{y_1} - y_1 \\
\hat{y_2} - y_2 \\
\vdots \\
\hat{y_k} - y_k \\
\end{bmatrix}$$

$$\boldsymbol{\nabla_{\mathbf{W}} L = \mathbf{X}^T\nabla_{\hat{\mathbf{Y}}} L}$$

- [ ] Проверено на семинаре
class NeuronBatch:
    def __init__(self, n_features: int, seed: int | None = None, grad: bool = False, lr: float = 0.01) -> None:
        if seed is not None:
            th.manual_seed(seed)
        self.weights: TensorType['n_features_with_bias', 1] = th.rand(n_features+1, requires_grad=grad)
        self.lr = lr

    def add_ones_col(self, inputs: TensorType["batch", "n_features"]) -> TensorType["batch", "n_features_with_bias"]:
        return th.cat([th.ones((inputs.shape[0], 1)), inputs], dim=1)

    def forward(self, inputs: TensorType["batch", "n_features"]) -> TensorType["batch", 1]:
        inputs = self.add_ones_col(inputs)
        return inputs @ self.weights

    def backward(self, inputs: TensorType["batch", "n_features"], dnext: TensorType["batch", 1]) -> None:
        inputs = self.add_ones_col(inputs)
        self.dweights: TensorType["n_features_with_bias", 1] = inputs.T @ dnext
        self.weights: TensorType['n_features_with_bias', 1] = self.weights - self.lr * self.dweights
    
    def __call__(self, *args: th.Any, **kwds: th.Any) -> th.Any:
        return self.forward(*args, **kwds)
class MSELoss:
    def forward(self, y_pred: TensorType['batch', 1], y_true: TensorType['batch', 1]) -> Scalar:
        if y_true.dim() > 1:
            y_true = y_true.squeeze()
        return th.mean((y_pred - y_true)**2)

    def backward(self, y_pred: TensorType['batch', 1], y_true: TensorType['batch', 1]) -> None:
        if y_true.dim() > 1:
            y_true = y_true.squeeze()
        return 2*(y_pred - y_true) / y_true.shape[0]
    
    def __call__(self, *args: th.Any, **kwds: th.Any) -> th.Any:
        return self.forward(*args, **kwds)
# Markdown:
Обучение модели
neuron = NeuronBatch(n_features=2, grad=True)
criterion = MSELoss()

inputs = th.rand((20, 2))*10
y_true = inputs[:, 0].clone().detach()*10 - 55

for epoch in range(5000):
    y_pred = neuron.forward(inputs)
    loss = criterion.forward(y_pred, y_true)

    dinput = criterion.backward(y_pred, y_true)
    neuron.backward(inputs, dinput)

    if epoch % 500 == 0:
        print(f"Epoch {epoch}, Loss: {loss:.6f}")
loss, y_pred
# Markdown:
Проверка равенства градиентов
neuron = NeuronBatch(n_features=2, grad=True)
criterion = MSELoss()

y_pred = neuron.forward(inputs)
loss = criterion.forward(y_pred, y_true)

loss.backward()
grad_autograd = neuron.weights.grad.detach().clone()

dinput = criterion.backward(y_pred, y_true)
neuron.backward(inputs, dinput)
grad_manual = neuron.dweights.detach().clone()

_eq = th.allclose(grad_manual, grad_autograd, atol=1e-7)
print(f'Градиенты совпадают {_eq}')
# Markdown:
Построение аналогичной модели с помощью torch
from torch import nn

class ThNeuron(nn.Module):
    def __init__(self, n_features: int, lr: float = 0.01):
        super(ThNeuron, self).__init__()
        self.linear = nn.Linear(n_features, 1, bias=True)
        self.lr = lr

    def forward(self, inputs: th.Tensor) -> th.Tensor:
        return self.linear(inputs)
    
    def __call__(self, *args: th.Any, **kwds: th.Any) -> th.Any:
        return super().__call__(*args, **kwds)
neuron = NeuronBatch(n_features=2, seed=42, grad=True)
thneuron = ThNeuron(n_features=2, lr=0.01)

with th.no_grad():
    thneuron.linear.weight.copy_(neuron.weights[1:].t())
    thneuron.linear.bias.copy_(neuron.weights[0])

criterion_manual = MSELoss()
criterion_autograd = nn.MSELoss()

inputs = th.rand((10, 2)) * 10
y_true = inputs[:, 0].clone().detach() * 10 - 55
y_pred_manual = neuron.forward(inputs)
y_pred_autograd = thneuron(inputs).squeeze(1)
_eq = th.allclose(y_pred_manual, y_pred_autograd, atol=1e-7)
print(f'Предсказания одинаковы: {_eq}')
loss_manual = criterion_manual.forward(y_pred_manual, y_true)
loss_autograd = criterion_autograd(y_pred_autograd, y_true)
_eq = th.allclose(y_pred_manual, y_pred_autograd, atol=1e-7)
print(f'Ошибки одинаковы: {_eq}')
dinput = criterion_manual.backward(y_pred_manual, y_true)
neuron.backward(inputs, dinput)
grad_manual = neuron.dweights.detach()

loss_autograd.backward()
thneuron.linear.weight.grad, thneuron.linear.bias.grad
grad_autograd = th.cat([
    thneuron.linear.bias.grad.unsqueeze(0), 
    thneuron.linear.weight.grad.t().reshape(-1,1)
], dim=0).squeeze(1)
_eq = th.allclose(grad_manual, grad_autograd, atol=1e-7) 
print(f'Градиенты одинаковы: {_eq}')
# Markdown:
<p class="task" id="2"></p>

2\. Настройте модель нейрона, используя метод мини-пакетного градиентного спуска.

Используйте обратное распространение ошибки, реализованное самостоятельно. Выведите на экран полученные и правильные коэффициенты модели.

- [ ] Проверено на семинаре
from sklearn.datasets import make_regression
from torch.utils.data import DataLoader, TensorDataset

X, y, coef = make_regression(n_features=4, n_informative=4, coef=True, bias=0.5, random_state=42)
X = th.FloatTensor(X)
y = th.FloatTensor(y).reshape(-1, 1)
neuron = NeuronBatch(n_features=4, grad=True, lr=0.01)
criterion = MSELoss()
dataset = TensorDataset(X, y)
for epoch in range(1501):
    losses = []
    for X, y in DataLoader(dataset, batch_size=20, shuffle=True):
        y_pred = neuron.forward(X)
        loss_i = criterion.forward(y_pred, y)
        losses.append(loss_i.item())

        dinput = criterion.backward(y_pred, y)
        neuron.backward(X, dinput)
    
    if epoch % 250 == 0:
        print(f'Epoch {epoch}, Loss: {np.mean(losses):.6f}')
neuron_coef = neuron.weights.detach()
neuron_coef
_eq = th.allclose(
    th.tensor(
        np.concat([[0.5], coef]),
        dtype=th.float32
    ), neuron_coef, atol=1e-7,
)
print(f'Коэффициенты одинаковы: {_eq}')
# Markdown:
<p class="task" id="3"></p>

3\. Реализуйте обратное распространение ошибки для модели полносвязного слоя с функцией потерь MSE при условии, что на вход нейрону поступает пакет (двумерный тензор) `inputs`.  Проверьте корректность вычисления градиентов, воспользовавшись возможностями по автоматическому дифференцированию `torch`.

Обратите внимание, что вам потребуются оба градиента $ \boldsymbol{\nabla_{\mathbf{W}} L }$ и $\boldsymbol{\nabla_{\mathbf{X}} L}$ для распространения ошибки с несколькими слоями.

$$\mathbf{X} = \begin{bmatrix}
x_{10} & x_{11} & \ldots & x_{1m} \\
x_{20} & x_{21} & \ldots & x_{2m} \\
\vdots & \vdots & \ddots & \vdots \\
x_{k0} & x_{k1} & \ldots & x_{km} \\
\end{bmatrix}
\mathbf{Y} = \begin{bmatrix}
y_{1} \\
y_{2} \\
\vdots \\
y_{k} \\
\end{bmatrix}
\mathbf{W} = \begin{bmatrix}
w_{01} & w_{02} & \ldots & w_{0n} \\
w_{11} & w_{12} & \ldots & w_{1n} \\
\vdots & \vdots & \ddots & \vdots \\
w_{m1} & w_{m2} & \ldots & w_{mn} \\
\end{bmatrix}$$

$$\hat{\mathbf{Y}} = \mathbf{X}\times \mathbf{W}$$

$$\nabla_{\hat{\mathbf{Y}}} L = \begin{bmatrix}
\frac{\partial L}{\partial \hat{y_{11}}} & \ldots & \frac{\partial L}{\partial \hat{y_{1n}}} \\
\vdots & \vdots & \vdots \\
\frac{\partial L}{\partial \hat{y_{k1}}} & \ldots & \frac{\partial L}{\partial \hat{y_{kn}}} \\
\end{bmatrix}$$

$$\boldsymbol{\nabla_{\mathbf{W}} L = \mathbf{X}^T\times \nabla_{\hat{\mathbf{Y}}} L}$$
$$\boldsymbol{\nabla_{\mathbf{X}} L = \nabla_{\hat{\mathbf{Y}}} L\times \mathbf{W}^T}$$

- [ ] Проверено на семинаре
class Linear:
    def __init__(self, n_features: int, n_neurons: int, seed: int | None = None, grad: bool = False, lr: float = 0.01) -> None:
        if seed is not None:
            th.manual_seed(seed)
        self.weights: TensorType['n_features_with_bias', "n_neurons"] = th.rand((n_features+1, n_neurons), requires_grad=grad)
        self.lr = lr

    def add_ones_col(self, inputs: TensorType["batch", "n_features"]) -> TensorType["batch", "n_features_with_bias"]:
        return th.cat([th.ones((inputs.shape[0], 1)), inputs], dim=1)

    def forward(self, inputs: TensorType["batch", "n_features"]) -> TensorType["batch", "n_neurons"]:
        inputs = self.add_ones_col(inputs)
        return inputs @ self.weights

    def backward(self, inputs: TensorType["batch", "n_features"], dnext: TensorType["batch", "n_neurons"]):
        inputs = self.add_ones_col(inputs)
        self.dweights: TensorType["n_features_with_bias", "n_neurons"] = inputs.T @ dnext
        self.weights: TensorType['n_features_with_bias', "n_neurons"] = self.weights - self.lr * self.dweights
        self.dinputs = dnext @ self.weights.T
        return self.dinputs[:, 1:]
    
    def __call__(self, *args: th.Any, **kwds: th.Any) -> th.Any:
        return self.forward(*args, **kwds)
class MSELoss:
    def forward(self, y_pred: TensorType["batch", "n_neurons"], y_true: TensorType["batch", "n_neurons"]) -> Scalar:
        return th.mean((y_pred - y_true)**2)

    def backward(self, y_pred: TensorType["batch", "n_neurons"], y_true: TensorType["batch", "n_neurons"]) -> None:
        return 2*(y_pred - y_true) / y_true.shape[0]
    
    def __call__(self, *args: th.Any, **kwds: th.Any) -> th.Any:
        return self.forward(*args, **kwds)
# Markdown:
Проверка равенства градиентов
neuron = NeuronBatch(n_features=2, grad=True)
criterion = MSELoss()

y_pred = neuron.forward(inputs)
loss = criterion.forward(y_pred, y_true)

loss.backward()
grad_autograd = neuron.weights.grad.detach().clone()

dinput = criterion.backward(y_pred, y_true)
neuron.backward(inputs, dinput)
grad_manual = neuron.dweights.detach().clone()

_eq = th.allclose(grad_manual, grad_autograd, atol=1e-7)
print(f'Градиенты совпадают {_eq}')
# Markdown:
<p class="task" id="4"></p>

4\. Настройте полносвязный слой, используя метод пакетного градиентного спуска. Используйте обратное распространение ошибки, реализованное самостоятельно. Выведите на экран полученные и правильные коэффициенты модели.

- [ ] Проверено на семинаре
from sklearn.datasets import make_regression

X, y, coef = make_regression(n_features=4, n_informative=4, coef=True, bias=0.5, random_state=42)
X = th.FloatTensor(X)
y = th.FloatTensor(y).reshape(-1, 1)
neuron = Linear(n_features=4, n_neurons=1, grad=True)
criterion = MSELoss()
dataset = TensorDataset(X, y)
for epoch in range(1501):
    losses = []
    for X, y in DataLoader(dataset, batch_size=20, shuffle=True):
        y_pred = neuron.forward(X)
        loss_i = criterion.forward(y_pred, y)
        losses.append(loss_i.item())

        dinput = criterion.backward(y_pred, y)
        neuron.backward(X, dinput)
    
    if epoch % 250 == 0:
        print(f'Epoch {epoch}, Loss: {np.mean(losses):.6f}')
neuron_coef = neuron.weights.detach().squeeze(1)
neuron_coef
_eq = th.allclose(
    th.tensor(
        np.concat([[0.5], coef]),
        dtype=th.float32
    ), neuron_coef, atol=1e-7,
)
print(f'Коэффициенты одинаковы: {_eq}')
# Markdown:
<p class="task" id="5"></p>

5\. Используя решения предыдущих задач, создайте нейросеть и решите задачу регрессии. При наличии корректно реализованных методов `backward` у `Linear` и `MSE` вы можете обобщить процедуру распространения ошибки на любое количество слоев. Реализуйте и обучите модель, состояющую из двух полносвязных слоев:

1. Полносвязный слой с 10 нейронами;
2. Полносвязный слой с 1 нейроном;

Схематично процедура обратного распространения ошибки представлена на рис. ниже.

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


- [ ] Проверено на семинаре
# Markdown:
import matplotlib.pyplot as plt
import seaborn as sns
sns.set(style="whitegrid", palette="deep", font_scale=1.1)
th.manual_seed(42)
X = th.linspace(-1, 1, 100).view(-1, 1)
y = X.pow(2) + 0.2 * th.rand(X.size())
dataset = TensorDataset(X, y)
dataloader = DataLoader(dataset, batch_size=50, shuffle=True)
linear10 = Linear(n_features=1, n_neurons=10, grad=True, lr=0.01)
linear1 = Linear(n_features=10, n_neurons=1, grad=True, lr=0.01)
criterion = MSELoss()
for epoch in range(500):
    losses = []
    for X, y in dataloader:
        y_pred_10 = linear10(X)
        y_pred_1 = linear1(y_pred_10)
        loss_i = criterion.forward(y_pred_1, y)
        losses.append(loss_i.item())

        dinput = criterion.backward(y_pred_1, y)
        dinput = linear1.backward(y_pred_10, dinput)
        dinput = linear10.backward(X, dinput)
    
    if epoch % 100 == 0:
        print(f'Epoch {epoch}, Loss: {np.mean(losses):.6f}')
X_np = X.numpy().flatten()
y_np = y.numpy().flatten()

plt.figure(figsize=(10, 6))
plt.title('Без ReLU', fontsize=16)

with th.no_grad():
    y_pred = linear1(linear10(X))
y_pred_np = y_pred.numpy().flatten()

ix = np.argsort(X_np)
sns.scatterplot(x=X_np, y=y_np, label='Данные', s=60, edgecolor='w', alpha=0.7)
sns.scatterplot(x=X_np, y=y_pred_np, color='red', label='Предсказание модели', s=60, edgecolor='w', alpha=0.7)
sns.lineplot(x=X_np[ix], y=y_pred_np[ix], color='black', linestyle='--')

plt.xlabel('X', fontsize=14)
plt.ylabel('y', fontsize=14)

plt.legend(title='Легенда')
plt.tight_layout()
plt.show()
# Markdown:
<p class="task" id="6"></p>

6\. Модель из предыдущей задачи является линейной и не способна качественно предсказать искомую зависимость. Для того, чтобы сделать модель нелинейной, в нейронных сетях используются функции активации. Для того, чтобы встроить такую функцию в процесс обратного распространения ошибки, необходимо реализовать соответствующий слой с методами `forward` и `backward`.

$$
f(x) = \max(0, x)
$$

$$
\frac{\partial L}{\partial x} = \frac{\partial L}{\partial f}\frac{\partial f}{\partial x} = \frac{\partial L}{\partial f}
\begin{cases}
1 & \text{если } x \ge 0 \\
0 & \text{если } x <  0
\end{cases}
$$

Здесь $L$ - это функция (слой), следующая за ReLU в потоке вычислений.

Реализуйте и обучите модель, состояющую из двух полносвязных слоев, разделенных функцией активации ReLU:
1. Полносвязный слой с 10 нейронами
2. Активация ReLU
3. Полносвязный слой с 1 нейроном

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

- [ ] Проверено на семинаре
# Markdown:

class ReLU:
    def forward(self, inputs: TensorType["batch", "n_features"]) -> TensorType["batch", "n_features"]:
        return inputs.clip(min=0)

    def backward(
        self,
        inputs: TensorType["batch", "n_features"],
        dvalues: TensorType["batch", "n_features"]
    ) -> TensorType["batch", "n_features"]:
        return (inputs >= 0).float() * dvalues
    
    def __call__(self, *args: th.Any, **kwds: th.Any) -> th.Any:
        return self.forward(*args, **kwds)
th.manual_seed(42)
X = th.linspace(-1, 1, 100).view(-1, 1)
y = X.pow(2) + 0.2 * th.rand(X.size())
dataset = TensorDataset(X, y)
dataloader = DataLoader(dataset, batch_size=50, shuffle=True)
linear10 = Linear(n_features=1, n_neurons=10, grad=True, lr=0.01)
linear1 = Linear(n_features=10, n_neurons=1, grad=True, lr=0.01)
criterion = MSELoss()
relu = ReLU()
saved_predictions = {}
save_epochs = [0, 100, 250, 500, 1000, 2500, 4950, 7950]
for epoch in range(8000):
    losses = []
    for X_d, y_d in dataloader:
        y_pred_10 = linear10(X_d)
        y_pred_10_relu = relu.forward(y_pred_10)
        y_pred_1 = linear1(y_pred_10_relu)
        loss_i = criterion.forward(y_pred_1, y_d)
        losses.append(loss_i.item())

        dinput = criterion.backward(y_pred_1, y_d)
        dinput = linear1.backward(y_pred_10_relu, dinput)
        dinput = relu.backward(y_pred_10, dinput)
        dinput = linear10.backward(X_d, dinput)

    if epoch % 500 == 0:
        print(f'Epoch {epoch}, Loss: {np.mean(losses):.6f}')
    
    if epoch in save_epochs:
        with th.no_grad():
            y_pred = linear1(relu(linear10(X)))
            saved_predictions[epoch] = y_pred.numpy().flatten()
X_np = X.numpy().flatten()
y_np = y.numpy().flatten()

cols = 2
rows = (len(save_epochs) + 1) // cols

plt.figure(figsize=(15, rows * 4))
plt.suptitle('Предсказания модели на различных эпохах', fontsize=20)

for idx, epoch in enumerate(save_epochs):
    plt.subplot(rows, cols, idx + 1)
    y_pred_np = saved_predictions[epoch]
    
    ix = np.argsort(X_np)
    sns.scatterplot(x=X_np, y=y_np, label='Данные', s=60, edgecolor='w', alpha=0.7)
    sns.scatterplot(x=X_np, y=y_pred_np, color='red', label='Предсказание модели', s=60, edgecolor='w', alpha=0.7)
    sns.lineplot(x=X_np[ix], y=y_pred_np[ix], color='black', linestyle='--')
    
    plt.xlabel('X', fontsize=12)
    plt.ylabel('y', fontsize=12)
    plt.title(f'Epoch {epoch}')
    plt.legend()

plt.tight_layout()
plt.show()


# 02_4_torch_nn_regression.ipynb
# Markdown:
#  Решение задачи регрессии при помощи пакета `torch`. Метрики.

__Автор задач: Блохин Н.В. (NVBlokhin@fa.ru)__

Материалы:
* Deep Learning with PyTorch (2020) Авторы: Eli Stevens, Luca Antiga, Thomas Viehmann
* https://pytorch.org/docs/stable/nn.html
* https://pytorch.org/docs/stable/optim.html
* https://github.com/Lightning-AI/torchmetrics
* https://pytorch.org/docs/stable/generated/torch.no_grad.html
* https://pytorch-lightning.readthedocs.io/en/2.1.2/pytorch/ecosystem/metrics.html#torchmetrics
# Markdown:
## Задачи для совместного разбора
from torchtyping import TensorType, patch_typeguard
from typeguard import typechecked
import torch as th

Scalar = TensorType[()]
patch_typeguard()
th.cuda.is_available()
# Markdown:
1\. Используя реализацию из `torch.nn`, решите задачу регрессии. Для расчета градиентов воспользуйтесь возможностями по автоматическому дифференцированию `torch`. В качестве функции потерь используйте собственную реализацию MSE. Для настройки весов реализуйте пакетный градиентный спуск с использованием `torch.optim.SGD`.
from sklearn.datasets import make_regression
import torch as th

X, y, coef = make_regression(n_features=4, n_informative=4, coef=True, bias=0.5, random_state=42)
X = th.FloatTensor(X)
y = th.FloatTensor(y)
import torch.nn as nn

class SyntRegressionModel(nn.Module):
    def __init__(self, n_inputs: int, n_hidden: int) -> None:
        super().__init__()
        self.fc1 = nn.Linear(in_features=n_inputs, out_features=n_hidden)
        self.fc2 = nn.Linear(in_features=n_hidden, out_features=1)
        self.relu = nn.ReLU()

    def forward(self, X: th.Tensor) -> th.Tensor:
        out = self.fc1(X)
        out = self.relu(out)
        out = self.relu(out)
        out = self.fc2(out)
        return out
model = SyntRegressionModel(n_inputs=4, n_hidden=2)
# y_pred = model.forward(X)
y_pred = model(X)
model.fc1.weight
y_pred.shape
n_inputs = 4
n_hidden = 1
model = nn.Sequential(
    nn.Linear(in_features=n_inputs, out_features=n_hidden),
    nn.ReLU(),
    nn.Linear(in_features=n_hidden, out_features=1),
)
y_pred = model(X)
criterion = nn.MSELoss()

loss = criterion(y_pred.flatten(), y)
loss
loss.backward()
import torch.optim as optim

optimizer = optim.SGD(model.parameters(), lr=0.01)
optimizer.step()
optimizer.zero_grad()
n_inputs = 4
n_hidden = 1

model = nn.Sequential(
    nn.Linear(in_features=n_inputs, out_features=n_hidden),
    nn.ReLU(),
    nn.Linear(in_features=n_hidden, out_features=1),
)
criterion = nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=0.01)

for _ in range(100):
    # forward pass
    y_pred = model(X)
    loss = criterion(y_pred.flatten(), y)

    # backprop
    loss.backward()

    # gradient descend
    optimizer.step()
    optimizer.zero_grad()
loss
from torch.utils.data import TensorDataset, DataLoader

dataset = TensorDataset(X, y)
loader = DataLoader(dataset, batch_size=16, shuffle=True)

n_inputs = 4
n_hidden = 1

model = nn.Sequential(
    nn.Linear(in_features=n_inputs, out_features=1),
    # nn.ReLU(),
    # nn.Linear(in_features=n_hidden, out_features=1),
)
criterion = nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=0.005)

epoch_losses = []
for epoch in range(100):
    epoch_loss = 0
    for X_batch, y_batch in loader:
        # forward pass
        y_pred = model(X_batch)
        loss = criterion(y_pred.flatten(), y_batch)
        epoch_loss += loss
        # backprop
        loss.backward()

        # gradient descend
        optimizer.step()
        optimizer.zero_grad()
    
    if epoch % 10 == 0:
        epoch_loss = epoch_loss / len(loader)
        epoch_losses.append( epoch_loss.item())
        print('epoch:', epoch, '; loss: ', epoch_loss.item())
import matplotlib.pyplot as plt

plt.plot(epoch_losses)
plt.show()
def train(model, dataset, loader, n_epochs, criterion, optimizer, print_every):
    model.train()
    pass
@th.no_grad()
def eval(model):
    model.eval()
with th.no_grad():
    y_pred = model(X)
    print(y_pred.flatten())
with th.no_grad():
  y_pred = model(X)
from sklearn.metrics import r2_score
r2_score(y, y_pred.flatten())
import torchmetrics as M
dataset = TensorDataset(X, y)
loader = DataLoader(dataset, batch_size=16, shuffle=True)

n_inputs = 4
n_hidden = 1

model = nn.Sequential(
    nn.Linear(in_features=n_inputs, out_features=1),
    # nn.ReLU(),
    # nn.Linear(in_features=n_hidden, out_features=1),
)
criterion = nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=0.005)

epoch_losses = []
for epoch in range(100):
    epoch_loss = 0
    r2_metric = M.R2Score()
    for X_batch, y_batch in loader:
        # forward pass
        y_pred = model(X_batch)
        loss = criterion(y_pred.flatten(), y_batch)
        epoch_loss += loss
        r2_metric.update(y_pred.flatten(), y_batch)
        # backprop
        loss.backward()

        # gradient descend
        optimizer.step()
        optimizer.zero_grad()
    
    if epoch % 10 == 0:
        epoch_loss = epoch_loss / len(loader)
        epoch_losses.append( epoch_loss.item())
        r2_epoch = r2_metric.compute()
        print('epoch:', epoch, '| loss:', epoch_loss.item(), '| r2:', r2_epoch.item())
# Markdown:
## Задачи для самостоятельного решения
# Markdown:
<p class="task" id="1"></p>

1\. Используя реализацию полносвязного слоя из `torch.nn` решите задачу регрессии. В качестве функции потерь используйте реализацию MSE из `torch.nn`. Для настройки весов реализуйте мини-пакетный градиентный спуск с использованием `torch.optim.SGD`. Для создания модели опишите класс `SineModel`.

Предлагаемая архитектура нейронной сети:
1. Полносвязный слой с 100 нейронами
2. Активация ReLU
3. Полносвязный слой с 1 нейроном

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

Выведите график изменения значения функции потерь в процессе обучения. Логику расчета значения функции потерь на уровне эпохи реализуйте самостоятельно.

- [ ] Проверено на семинаре
import torch.optim as optim
import torch.nn as nn
import numpy as np

import matplotlib.pyplot as plt
import seaborn as sns
X = th.linspace(0, 1, 100).view(-1, 1)
y = th.sin(2 * th.pi * X) + 0.1 * th.rand(X.size())
dataset = TensorDataset(X, y)
loader = DataLoader(dataset, batch_size=20, shuffle=True)
class SineModel(nn.Module):
    def __init__(self, n_features: int, n_hidden: int, n_out: int) -> None:
        super(SineModel, self).__init__()
        self.fc1 = nn.Linear(in_features=n_features, out_features=n_hidden)
        self.fc2 = nn.Linear(in_features=n_hidden, out_features=n_out)
        self.relu = nn.ReLU()

    def forward(self, X: TensorType["batch", "n_features"]) -> TensorType["batch", 1]:
        out = self.fc1(X)
        out = self.relu(out)
        out = self.fc2(out)
        return out
model = SineModel(1, 100, 1)
optimizer = optim.SGD(model.parameters(), lr=0.01)
criterion = nn.MSELoss()
def train(model, dataset, loader, n_epochs, criterion, optimizer, save_epochs, print_every=1000):
    saved_loss = {}
    saved_predictions = {}
    for epoch in range(n_epochs):
        loss_arr = []
        for X_batch, y_batch in loader:
            model.train()
            y_pred = model(X_batch)

            loss = criterion(y_pred, y_batch)
            loss_arr.append(loss.item())

            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
        
        if epoch % 50 == 0:
            saved_loss[epoch] = np.mean(loss_arr)
        
        if epoch % print_every == 0:
            print(f'Epoch:{epoch:^6}| Loss: {np.mean(loss_arr):.4f}')
        
        if epoch in save_epochs:
            with th.no_grad():
                y_pred = model(dataset[:][0])
                if y_pred.device.type != 'cpu':
                    y_pred = y_pred.cpu()
                saved_predictions[epoch] = y_pred.numpy().flatten()
    
    return saved_loss, saved_predictions
def plot(X, y, save_epochs, saved_predictions, cols=2):
    X_np = X.numpy().flatten()
    y_np = y.numpy().flatten()
    rows = (len(save_epochs) + 1) // cols

    plt.figure(figsize=(15, rows * 4))
    plt.suptitle('Предсказания модели на различных эпохах', fontsize=20)

    for idx, epoch in enumerate(save_epochs):
        plt.subplot(rows, cols, idx + 1)
        y_pred_np = saved_predictions[epoch]
        
        ix = np.argsort(X_np)
        sns.scatterplot(x=X_np, y=y_np, label='Данные', s=60, edgecolor='w', alpha=0.7)
        sns.scatterplot(x=X_np, y=y_pred_np, color='red', label='Предсказание модели', s=60, edgecolor='w', alpha=0.7)
        sns.lineplot(x=X_np[ix], y=y_pred_np[ix], color='black', linestyle='--')
        
        plt.xlabel('X', fontsize=12)
        plt.ylabel('y', fontsize=12)
        plt.title(f'Epoch {epoch}')
        plt.legend()

    plt.tight_layout()
    plt.show()

def plot_loss(saved_loss, title='Loss/Epoch'):
    plt.title(title)
    sns.lineplot(x=saved_loss.keys(), y=saved_loss.values(), color='black', linestyle='--')
    plt.xlabel('Epoch')
    plt.ylabel('Loss')
    plt.grid(True)
    plt.tight_layout()
    plt.show()
save_epochs = [0, 100, 250, 500, 1000, 2500, 4950, 7950]
saved_loss, saved_predictions = train(model, dataset, loader, 8001, criterion, optimizer, save_epochs)
plot(X, y, save_epochs, saved_predictions)
plot_loss(saved_loss)
# Markdown:
<p class="task" id="2"></p>

2\. Повторите решение задачи 1, изменив модель. Для создания модели создайте объект класса `nn.Sequential`.

Предлагаемая архитектура нейронной сети:
1. Полносвязный слой с 50 нейронами
2. Активация Tanh
3. Полносвязный слой с 1 нейроном

- [ ] Проверено на семинаре
X = th.linspace(0, 1, 100).view(-1, 1)
y = th.sin(2 * th.pi * X) + 0.1 * th.rand(X.size())
dataset = TensorDataset(X, y)
loader = DataLoader(dataset, batch_size=20, shuffle=True)
model = nn.Sequential(
    nn.Linear(1, 50),
    nn.Tanh(),
    nn.Linear(50, 1),
)
optimizer = optim.SGD(model.parameters(), lr=0.01)
criterion = nn.MSELoss()
saved_loss, saved_predictions = train(model, dataset, loader, 8001, criterion, optimizer, save_epochs)
plot(X, y, save_epochs, saved_predictions)
plot_loss(saved_loss)
# Markdown:
<p class="task" id="3"></p>

3\. Используя реализацию полносвязного слоя из `torch.nn`, решите задачу регрессии. В качестве функции потерь используйте реализацию MSE из `torch.nn`. Для настройки весов реализуйте мини-пакетный градиентный спуск с использованием `torch.optim.SGD`. Перенесите вычисления на GPU и сравните время обучения с и без использования GPU. Решение должно корректно работать в случае отсутствия GPU без дополнительных изменений в коде.

- [ ] Проверено на семинаре
from sklearn.datasets import make_regression
import torch as th

X, y, coef = make_regression(
    n_samples=10000,
    n_features=10,
    n_informative=6,
    coef=True,
    bias=0.5,
    random_state=42
)
X = th.FloatTensor(X)
y = th.FloatTensor(y).reshape(-1, 1)
dataset = TensorDataset(X, y)
loader = DataLoader(dataset, batch_size=2500, shuffle=True)
model = nn.Sequential(
    nn.Linear(10, 50),
    nn.Tanh(),
    nn.Linear(50, 1),
).to('cpu')
optimizer = optim.SGD(model.parameters(), lr=0.01)
criterion = nn.MSELoss()
print('Запуск с CPU:')
%timeit -n 1 -r 1 saved_loss, saved_predictions = train(model, dataset, loader, 2501, criterion, optimizer, save_epochs, print_every=500)
plot_loss(saved_loss)
device = th.device('cuda' if th.cuda.is_available() else 'cpu')
device, th.cuda.device_count()
model = nn.Sequential(
    nn.Linear(10, 50),
    nn.Tanh(),
    nn.Linear(50, 1),
).to(device)
optimizer = optim.SGD(model.parameters(), lr=0.01)
criterion = nn.MSELoss().to(device)
dataset = TensorDataset(X.to(device), y.to(device))
loader = DataLoader(dataset, batch_size=2500, shuffle=True)
print(f'Запуск с {str(device).upper()}:')
%timeit -n 1 -r 1 saved_loss, saved_predictions = train(model, dataset, loader, 2501, criterion, optimizer, save_epochs, print_every=500)
plot_loss(saved_loss)
# Markdown:
<p class="task" id="4"></p>

4\. Повторите решение задач 1-2, используя для расчета значения функции потерь за эпоху метрику `MeanMetric` из пакета `torchmetrics`. Добавьте в цикл обучения расчет метрики $R^2$ (воспользуйтесь реализацией из `torchmetrics`). Выведите на экран график изменения значения функции потерь и метрики $R^2$ по эпохам в процессе обучения.
X = th.linspace(0, 1, 100).view(-1, 1)
y = th.sin(2 * th.pi * X) + 0.1 * th.rand(X.size())
dataset = TensorDataset(X, y)
loader = DataLoader(dataset, batch_size=20, shuffle=True)
def train_m(model, dataset, loader, n_epochs, criterion, optimizer, save_epochs, print_every=1000):
    saved_loss = {}
    saved_predictions = {}
    saved_r2 = {}
    for epoch in range(n_epochs):
        loss_arr = []
        r2_metric = M.R2Score()
        for X_batch, y_batch in loader:
            model.train()
            y_pred = model(X_batch)

            loss = criterion(y_pred, y_batch)
            loss_arr.append(loss.item())
            r2_metric.update(y_pred, y_batch)

            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
        
        if epoch % 50 == 0:
            saved_loss[epoch] = np.mean(loss_arr)
            saved_r2[epoch] = r2_metric.compute().item()
        
        if epoch % print_every == 0:
            print(f'Epoch:{epoch:^6}| Loss: {np.mean(loss_arr):.4f} | R2: {r2_metric.compute().item():.4f}')
        
        if epoch in save_epochs:
            with th.no_grad():
                y_pred = model(dataset[:][0])
                saved_predictions[epoch] = y_pred.numpy().flatten()
    
    return saved_loss, saved_predictions, saved_r2
model = nn.Sequential(
    nn.Linear(1, 50),
    nn.ReLU(),
    nn.Linear(50, 1),
)
optimizer = optim.SGD(model.parameters(), lr=0.01)
criterion = nn.MSELoss()
saved_loss, saved_predictions, saved_r2 = train_m(model, dataset, loader, 8001, criterion, optimizer, save_epochs)
plot_loss(saved_r2, 'R2 - Loss/Epoch')
plot_loss(saved_loss, 'MSE - Loss/Epoch')
# Markdown:
<p class="task" id="5"></p>

5\. Повторите решение задач 1-2, изменив функцию потерь. Обучите модель, используя три функции потерь: `MSELoss`, `L1Loss` и `HuberLoss` - и выведите на одном графике динамику изменения метрики $R^2$ по эпохам для каждой модели в процессе обучения. Добавьте подписи полученных кривых.

- [ ] Проверено на семинаре
for loss in (nn.MSELoss, nn.L1Loss, nn.HuberLoss):
    model = nn.Sequential(
        nn.Linear(1, 50),
        nn.ReLU(),
        nn.Linear(50, 1),
    )
    optimizer = optim.SGD(model.parameters(), lr=0.01)
    criterion = loss()

    print(f'Запуск с {loss.__name__}:\n')
    saved_loss, saved_predictions, saved_r2 = train_m(model, dataset, loader, 8001, criterion, optimizer, save_epochs)
    plot_loss(saved_r2, f'R2 - Loss/Epoch - {loss.__name__}')
    print('\n', '-'*75, '\n', sep='')
# Markdown:
<p class="task" id="6"></p>

6\. Повторите решение задач 1-2, разделив датасет на обучающую и тестовую выборку в соотношении 80% на 20%. Обучите модель. Для тестовой выборки посчитайте и выведите на экран значения метрик:

- MAE;
- MAPE;
- MSE;
- MSLE;


- [ ] Проверено на семинаре
import torch.utils.data as data
X = th.linspace(0, 1, 100).view(-1, 1)
y = th.sin(2 * th.pi * X) + 0.1 * th.rand(X.size())
dataset = TensorDataset(X, y)
loader = DataLoader(dataset, batch_size=20, shuffle=True)
train_dataset, test_dataset = data.random_split(dataset, [0.8, 0.2])
model = nn.Sequential(
    nn.Linear(1, 50),
    nn.ReLU(),
    nn.Linear(50, 1),
)
optimizer = optim.SGD(model.parameters(), lr=0.01)
criterion = nn.MSELoss()
saved_loss, saved_predictions, saved_r2 = train_m(model, dataset, loader, 8001, criterion, optimizer, save_epochs)
y_pred = model(test_dataset[:][0])
print(' '+'_'*50)
for loss in (M.MeanAbsoluteError(), M.MeanAbsolutePercentageError(), M.MeanSquaredError(), M.MeanSquaredLogError()):
    print(f'| {loss.__class__.__name__:-^32}: {loss(y_pred, test_dataset[:][1]).item():-^14.4f} |')
print(' '+'‾'*50)


# 02_5_torch_nn_classification.ipynb
# Markdown:
#  Решение задачи классификации при помощи пакета `torch`.

__Автор задач: Блохин Н.В. (NVBlokhin@fa.ru)__

Материалы:
* Deep Learning with PyTorch (2020) Авторы: Eli Stevens, Luca Antiga, Thomas Viehmann
* https://pytorch.org/docs/stable/nn.html
* https://pytorch.org/docs/stable/optim.html
* https://lightning.ai/docs/torchmetrics/stable/
* https://pytorch.org/docs/stable/generated/torch.no_grad.html
* https://www.learnpytorch.io/02_pytorch_classification/
* https://pytorch.org/docs/stable/data.html#torch.utils.data.WeightedRandomSampler
* https://towardsdatascience.com/demystifying-pytorchs-weightedrandomsampler-by-example-a68aceccb45
* https://pytorch.org/docs/stable/generated/torch.nn.CrossEntropyLoss.html
* https://medium.com/@zergtant/use-weighted-loss-function-to-solve-imbalanced-data-classification-problems-749237f38b7
* https://pytorch.org/docs/stable/generated/torch.nn.BCELoss.html#torch.nn.BCELoss
* https://pytorch.org/docs/stable/generated/torch.nn.BCEWithLogitsLoss.html#torch.nn.BCEWithLogitsLoss52
# Markdown:
## Задачи для совместного разбора
# from torchtyping import TensorType, patch_typeguard
# from typeguard import typechecked
import torch as th

# Scalar = TensorType[()]
# patch_typeguard()
# Markdown:
1\. Обсудите подходы к решению задачи классификации на примере синтетического датасета.
num_samples = 1000
num_features = 10
num_classes = 3

X = th.randn(num_samples, num_features)
y = th.randint(0, num_classes, (num_samples, ))
import torch.nn as nn

class Classifier(nn.Module):
    def __init__(self, n_inputs: int, n_classes: int) -> None:
        super().__init__()
        self.fc1 = nn.Linear(n_inputs, n_classes)

    def forward(self, X: th.Tensor) -> th.Tensor:
        return self.fc1(X)
model = Classifier(num_features, num_classes)
preds = model(X)
preds.shape
preds[:5]
preds.argmax(dim=1)[:5]
y[:5]
criterion = nn.CrossEntropyLoss()
loss = criterion(preds, y)
num_samples = 1000
num_features = 10
num_classes = 2

X = th.randn(num_samples, num_features)
y = th.randint(0, num_classes, (num_samples, ))
import torch.nn as nn

class Classifier(nn.Module):
    def __init__(self, n_inputs: int, n_classes: int) -> None:
        super().__init__()
        self.fc1 = nn.Linear(n_inputs, n_classes)

    def forward(self, X: th.Tensor) -> th.Tensor:
        return self.fc1(X)
model = Classifier(num_features, 1)
preds = model(X)
preds.shape
preds.sigmoid()[:5]
(preds.sigmoid() >= 0.5)[:5]
y[:5]
criterion = nn.BCEWithLogitsLoss()
criterion(preds.flatten(), y.float())
# Markdown:
## Задачи для самостоятельного решения
# Markdown:
<p class="task" id="1"></p>

1\. Используя реализацию полносвязного слоя из `torch.nn`, решите задачу классификации. Разделите датасет на обучающую и тестовую выборку в соотношении 80% на 20%. В качестве функции потерь используйте реализацию `CrossEntropyLoss` из `torch.nn`. Для настройки весов реализуйте мини-пакетный градиентный спуск с использованием `torch.optim.SGD`.

Используйте модель, состоящую из двух слоев:
1. Полносвязный слой с 10 нейронами;
2. Полносвязный слой с 2 нейронами.

Выведите график изменения значения функции потерь в процессе обучения. Выведите на экран значения Accuracy, Precision, Recall и F1 для обучающего и тестового множества.

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

- [x] Проверено на семинаре
from torch.utils.data import random_split, TensorDataset, DataLoader
from sklearn.datasets import make_circles
from matplotlib import pyplot as plt
from torch import nn, optim
import torchmetrics as M
import seaborn as sns
import numpy as np

X, y = make_circles(n_samples=1000, noise=0.05, random_state=42)
X = th.FloatTensor(X)
y = th.LongTensor(y)
dataset = TensorDataset(X, y)
dataset_train, dataset_test = random_split(dataset, [0.8, 0.2])
dataloader_train, dataloader_test = \
    DataLoader(dataset_train, batch_size=125, shuffle=True), DataLoader(dataset_test, batch_size=125, shuffle=True)
model = nn.Sequential(
    nn.Linear(2, 10),
    # nn.ReLU(),
    nn.Linear(10, 2)
)
optimizer = optim.SGD(model.parameters(), lr=0.01)
criterion = nn.CrossEntropyLoss()
def train(model, criterion, optimizer, loader, n_epochs=10000, print_every=100):
    epoch_losses = []
    for epoch in range(n_epochs + 1):
        model.train()
        accum_losses = []
        for X_batch, y_batch in loader:
            y_pred = model(X_batch)
            loss = criterion(y_pred, y_batch)
            accum_losses.append(loss.item())

            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

        epoch_loss = np.mean(accum_losses)
        epoch_losses.append(epoch_loss)
        
        if epoch % print_every == 0:
            print(f'Epoch [{epoch}/{n_epochs}], Loss: {loss.item():.4f}')
    return epoch_losses
def plot(epoch_losses, window_size=50):
    smoothed_losses = np.convolve(epoch_losses, np.ones(window_size) / window_size, mode='valid')
    plt.figure(figsize=(10, 5))
    plt.plot(smoothed_losses, label=f'Скользящее среднее', linewidth=2)
    plt.title('График изменения значения функции потерь')
    plt.xlabel('Эпоха')
    plt.ylabel('Значение функции потерь')
    plt.grid(True)
    plt.legend()
    plt.show()
losses = train(model, criterion, optimizer, dataloader_train, 2500, 500)
plot(losses)
def get_metrics(model, dataset_train, dataset_test):
    y_pred_train = model(dataset_train[:][0]).argmax(dim=1)
    y_true_train = dataset_train[:][1]
    print('dataset_train:'.upper())
    for metric in [M.Accuracy('binary'), M.Precision('binary'), M.Recall('binary'), M.F1Score('binary')]:
        print(f'{metric.__class__.__name__}: {metric(y_pred_train, y_true_train).item():.4f}')
    
    y_pred_test = model(dataset_test[:][0]).argmax(dim=1)
    y_true_test = dataset_test[:][1]
    print('\ndataset_test:'.upper())
    for metric in [M.Accuracy('binary'), M.Precision('binary'), M.Recall('binary'), M.F1Score('binary')]:
        print(f'{metric.__class__.__name__}: {metric(y_pred_test, y_true_test).item():.4f}')
get_metrics(model, dataset_train, dataset_test)
y_pred = model(dataset[:][0]).argmax(dim=1)
y_pred.shape
plt.figure(figsize=(10, 5))
plt.scatter(dataset[:][0][:, 0], dataset[:][0][:, 1], c=y_pred, cmap='coolwarm', alpha=0.6)
plt.title('Облако точек с цветом, соответствующим предсказаниям модели')
plt.xlabel('X1')
plt.ylabel('X2')
plt.grid(True)
plt.show()
# Markdown:
<p class="task" id="2"></p>

2\. Повторите задачу 1, используя другую архитектуру нейронной сети.

1. Полносвязный слой с 10 нейронами;
2. Функция активации ReLU;
3. Полносвязный слой с 2 нейронами.

- [x] Проверено на семинаре
model = nn.Sequential(
    nn.Linear(2, 10),
    nn.ReLU(),
    nn.Linear(10, 2)
)
optimizer = optim.SGD(model.parameters(), lr=0.01)
criterion = nn.CrossEntropyLoss()
losses = train(model, criterion, optimizer, dataloader_train, 2500, 500)
plot(losses)
get_metrics(model, dataset_train, dataset_test)
y_pred = model(dataset[:][0]).argmax(dim=1)
y_pred.shape
plt.figure(figsize=(10, 5))
plt.scatter(dataset[:][0][:, 0], dataset[:][0][:, 1], c=y_pred, cmap='coolwarm', alpha=0.6)
plt.title('Облако точек с цветом, соответствующим предсказаниям модели')
plt.xlabel('X1')
plt.ylabel('X2')
plt.grid(True)
plt.show()
# Markdown:
<p class="task" id="3"></p>

3\. `CrossEntropyLoss` может быть использована для задачи классификации на любое количество классов. Для задачи бинарной классификации существуют специфические функции потерь. Решите задачу 2, используя `BCEWithLogitsLoss` в качестве функции потерь.

- [x] Проверено на семинаре
def train_alt(model, criterion, optimizer, loader, n_epochs=10000, print_every=100):
    epoch_losses = []
    for epoch in range(n_epochs + 1):
        model.train()
        accum_losses = []
        for X_batch, y_batch in loader:
            y_pred = model(X_batch)
            loss = criterion(y_pred, y_batch.unsqueeze(1).float())
            accum_losses.append(loss.item())

            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

        epoch_loss = np.mean(accum_losses)
        epoch_losses.append(epoch_loss)
        
        if epoch % print_every == 0:
            print(f'Epoch [{epoch}/{n_epochs}], Loss: {loss.item():.4f}')
    return epoch_losses
def get_metrics_alt(model, dataset_train, dataset_test, threshold=0.5):
    y_pred_train = (model(dataset_train[:][0]).sigmoid() > threshold).squeeze(1).int()
    y_true_train = dataset_train[:][1]
    print('dataset_train:'.upper())
    for metric in [M.Accuracy('binary'), M.Precision('binary'), M.Recall('binary'), M.F1Score('binary')]:
        print(f'{metric.__class__.__name__}: {metric(y_pred_train, y_true_train).item():.4f}')
    
    y_pred_test = (model(dataset_test[:][0]).sigmoid() > threshold).squeeze(1).int()
    y_true_test = dataset_test[:][1]
    print('\ndataset_test:'.upper())
    for metric in [M.Accuracy('binary'), M.Precision('binary'), M.Recall('binary'), M.F1Score('binary')]:
        print(f'{metric.__class__.__name__}: {metric(y_pred_test, y_true_test).item():.4f}')
model = nn.Sequential(
    nn.Linear(2, 10),
    nn.ReLU(),
    nn.Linear(10, 1)
)
optimizer = optim.SGD(model.parameters(), lr=0.1)
criterion = nn.BCEWithLogitsLoss()
losses = train_alt(model, criterion, optimizer, dataloader_train, 2500, 500)
plot(losses)
get_metrics_alt(model, dataset_train, dataset_test)
y_pred = model(dataset[:][0]).sigmoid() > 0.5
y_pred.shape
plt.figure(figsize=(10, 5))
plt.scatter(dataset[:][0][:, 0], dataset[:][0][:, 1], c=y_pred, cmap='coolwarm', alpha=0.6)
plt.title('Облако точек с цветом, соответствующим предсказаниям модели')
plt.xlabel('X1')
plt.ylabel('X2')
plt.grid(True)
plt.show()
# Markdown:
<p class="task" id="4"></p>

4\. На практике часто задача классификации является несбалансированной. В файлах каталога `imb_task` содержится несбалансированный набор данных. Обучите модель без учета несбалансированности классов (аналогично предыдущим заданиям, можно использовать любую подходящую функцию потерь). Повысьте качество модели (в смысле F1) путем модификации функции потерь (указания специального аргумента, позволяющего учесть несбалансированность классов).

- [x] Проверено на семинаре
X = th.load('./../data/imb_task/imb_X.th', weights_only=True).float()
y = th.load('./../data/imb_task/imb_y.th', weights_only=True).long()
dataset = TensorDataset(X, y)
dataset_train, dataset_test = random_split(dataset, [0.8, 0.2])
dataloader_train, dataloader_test = \
    DataLoader(dataset_train, batch_size=125, shuffle=True), \
    DataLoader(dataset_test, batch_size=125, shuffle=True)
plt.title('Распределение классов в целевой переменной (y)')
sns.histplot(y.tolist(), bins=2)
plt.xticks([0, 1])
plt.show()
model = nn.Sequential(
    nn.Linear(2, 10),
    nn.ReLU(),
    nn.Linear(10, 2)
)
optimizer = optim.SGD(model.parameters(), lr=0.01)
criterion = nn.CrossEntropyLoss()
losses = train(model, criterion, optimizer, dataloader_train, 2500, 500)
# plot(losses)
get_metrics(model, dataset_train, dataset_test)
y_pred = model(dataset[:][0]).argmax(dim=1)
y_pred.shape
plt.figure(figsize=(10, 5))
plt.scatter(dataset[:][0][:, 0], dataset[:][0][:, 1], c=y_pred, cmap='coolwarm', alpha=0.6)
plt.title('Облако точек с цветом, соответствующим предсказаниям модели')
plt.xlabel('X1')
plt.ylabel('X2')
plt.grid(True)
plt.show()
y_pos = y.count_nonzero() / y.shape[0]
y_neg = 1 - y_pos
y_pos, y_neg
model = nn.Sequential(
    nn.Linear(2, 10),
    nn.ReLU(),
    nn.Linear(10, 2)
)
optimizer = optim.SGD(model.parameters(), lr=0.01)
criterion = nn.CrossEntropyLoss(weight=th.tensor([y_pos, y_neg]))
losses = train(model, criterion, optimizer, dataloader_train, 2500, 500)
# plot(losses)
get_metrics(model, dataset_train, dataset_test)
y_pred = model(dataset[:][0]).argmax(dim=1)
y_pred.shape
plt.figure(figsize=(10, 5))
plt.scatter(dataset[:][0][:, 0], dataset[:][0][:, 1], c=y_pred, cmap='coolwarm', alpha=0.6)
plt.title('Облако точек с цветом, соответствующим предсказаниям модели')
plt.xlabel('X1')
plt.ylabel('X2')
plt.grid(True)
plt.show()
# Markdown:
<p class="task" id="5"></p>

5\. Повторите решение задачи 4, повысив качество модели за счет использования `WeightedRandomSampler` вместо модификации функции потерь.

- [x] Проверено на семинаре
from torch.utils.data import WeightedRandomSampler
train_labels = dataset_train[:][1]
class_counts = th.bincount(train_labels)
class_weights = 1.0 / class_counts.float()
train_sample_weights = class_weights[train_labels]
sampler = WeightedRandomSampler(weights=train_sample_weights, num_samples=len(train_sample_weights), replacement=True)
dataset = TensorDataset(X, y)
dataset_train, dataset_test = random_split(dataset, [0.8, 0.2])
dataloader_train, dataloader_test = \
    DataLoader(dataset_train, batch_size=125, sampler=sampler), \
    DataLoader(dataset_test, batch_size=125, sampler=sampler)
model = nn.Sequential(
    nn.Linear(2, 10),
    nn.ReLU(),
    nn.Linear(10, 2)
)
optimizer = optim.SGD(model.parameters(), lr=0.01)
criterion = nn.CrossEntropyLoss()
losses = train(model, criterion, optimizer, dataloader_train, 2500, 500)
# plot(losses)
get_metrics(model, dataset_train, dataset_test)
y_pred = model(dataset[:][0]).argmax(dim=1)
y_pred.shape
plt.figure(figsize=(10, 5))
plt.scatter(dataset[:][0][:, 0], dataset[:][0][:, 1], c=y_pred, cmap='coolwarm', alpha=0.6)
plt.title('Облако точек с цветом, соответствующим предсказаниям модели')
plt.xlabel('X1')
plt.ylabel('X2')
plt.grid(True)
plt.show()


# 03_1_monitoring.ipynb
# Markdown:
#  Мониторинг процесса обучения

__Автор задач: Блохин Н.В. (NVBlokhin@fa.ru)__

Материалы:
* Deep Learning with PyTorch (2020) Авторы: Eli Stevens, Luca Antiga, Thomas Viehmann
* https://pytorch.org/tutorials/recipes/recipes/tensorboard_with_pytorch.html
* https://docs.wandb.ai/quickstart
* https://docs.wandb.ai/guides/track/log/log-summary#docusaurus_skipToContent_fallback
* https://docs.wandb.ai/guides/track/log/log-models
* https://www.youtube.com/playlist?list=PLD80i8An1OEGajeVo15ohAQYF1Ttle0lk
# Markdown:
## Задачи для совместного разбора
import wandb

wandb.login()
# Markdown:
1\. Рассмотрите возможности пакета `wandb` по отслеживанию числовых значений, визуализации изображений и таблиц.
import torch as th

def train(num_epochs: int):
    for x in range(num_epochs):
        x = th.tensor(x)
        loss = th.exp(-x/num_epochs)
        r2 = th.randn(size=(1,))
        wandb.log({'train/loss': loss, 'train/r2': r2})
    wandb.run.summary['test/r2'] = 1
with wandb.init(
    project='seminar-tutorial',
    # name='run2',
    tags=['demo'],
    config={'num_epochs': 100}
):
    train(100)
import seaborn as sns
sns.get_dataset_names()
with wandb.init(
    project='seminar-tutorial',
    # name='run2',
    tags=['demo'],
    config={'num_epochs': 100}
):
    dataset = sns.load_dataset('penguins')
    img = sns.pairplot(dataset)
    wandb.log({'train/pairplot': wandb.Image(img.figure)})
# Markdown:
## Задачи для самостоятельного решения
# Markdown:
<p class='task' id='1'></p>

1\. Решите задачу регрессии, используя для мониторинга процесса обучения `wandb`.

Разделите набор данных на обучающее и тестовое множество. В процессе обучения отслеживайте динамику изменения значения функции потерь и метрики $R^2$ по эпохам. После завершения обучения рассчитайте значение метрик MSE, RMSE, MAE и MAPE и сохраните в виде summary данного запуска.

Обучите не менее трех моделей (с разной архитектурой или гиперпараметрами), отследите все запуски при помощи `wandb` и вставьте в текстовую ячейку скриншоты, демонстрирующие интерфейс `wandb` с графиками обучения. Для каждого запуска приложите также скриншот с описанием гиперпараметров модели и summary (страница overview).

- [ ] Проверено на семинаре
import torch as  th

X = th.linspace(0, 1, 100).view(-1, 1)
y = th.sin(2 * th.pi * X) + 0.1 * th.rand(X.size())
from torch.utils.data import TensorDataset, DataLoader, random_split

dataset = TensorDataset(X, y)
dataset_train, dataset_test = random_split(dataset, [0.8, 0.2])
train_X, train_y = dataset_train[:][0], dataset_train[:][1]
test_X, test_y = dataset_test[:][0], dataset_test[:][1]
dataloader_train = DataLoader(dataset_train, batch_size=16)
dataloader_test = DataLoader(dataset_test, batch_size=16)
from torch import nn, optim

model_1 = nn.Sequential(
    nn.Linear(1, 16),
    nn.ReLU(),
    nn.Linear(16, 1),
)
optimizer_1 = optim.AdamW(model_1.parameters(), lr=0.01)
critertion_1 = nn.MSELoss()

model_2 = nn.Sequential(
    nn.Linear(1, 32),
    nn.ReLU(),
    nn.Linear(32, 4),
    nn.ReLU(),
    nn.Linear(4, 1),
)
optimizer_2 = optim.AdamW(model_2.parameters(), lr=0.01)
critertion_2 = nn.MSELoss()

model_3 = nn.Sequential(
    nn.Linear(1, 128),
    nn.ReLU(),
    nn.Linear(128, 64),
    nn.ReLU(),
    nn.Linear(64, 32),
    nn.ReLU(),
    nn.Linear(32, 1),
)
optimizer_3 = optim.AdamW(model_3.parameters(), lr=0.01)
critertion_3 = nn.MSELoss()
from torchmetrics import MeanSquaredError, MeanAbsoluteError, MeanAbsolutePercentageError, R2Score

def train(model, optimizer, critertion, num_epochs, print_every=40):
    for epoch in range(1, num_epochs+1):
        model.train()
        for x_b, y_b in dataloader_train:
            y_pred = model(x_b)
            loss = critertion(y_pred, y_b)
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
        
        x, y = dataset_train[:][0], dataset_train[:][1]
        with th.no_grad():
            y_pred = model(x)
            loss = critertion(y_pred, y)
            r2 = R2Score()(y_pred, y)

        wandb.log({'train/loss': loss, 'train/r2': r2})
        if epoch % print_every == 0 or epoch == 1:
            print(f'Эпоха {epoch}/{num_epochs}, loss: {loss:.4f}, r2: {r2:.4f}')

    model.eval()
    x, y = dataset_test[:][0], dataset_test[:][1]
    y_pred = model(x)

    metrics = {
        'mse': MeanSquaredError(),
        'rmse': MeanSquaredError(squared=False),
        'mae': MeanAbsoluteError(),
        'mape': MeanAbsolutePercentageError(),
        'r2': R2Score()
    }

    for name, metric in metrics.items():
        wandb.summary[f'test/{name}'] = metric(y_pred, y).item()
config = {'num_epochs': 160, 'print_every': 40}
with wandb.init(
    project='seminar-tutorial',
    name='task-1, model-1',
    tags=['task-1'],
    config=config
):
    train(model_1, optimizer_1, critertion_1, **config)
config = {'num_epochs': 200, 'print_every': 50}
with wandb.init(
    project='seminar-tutorial',
    name='task-1, model-2',
    tags=['task-1'],
    config=config
):
    train(model_2, optimizer_2, critertion_2, **config)
config = {'num_epochs': 320, 'print_every': 80}
with wandb.init(
    project='seminar-tutorial',
    name='task-1, model-3',
    tags=['task-1'],
    config=config
):
    train(model_3, optimizer_3, critertion_3, **config)
# Markdown:
**Графики обучения**<br><br>
![Снимок экрана 2024-10-22 в 20.43.44.png](<attachment:Снимок экрана 2024-10-22 в 20.43.44.png>)

Описание | Скриншоты
---------|----------
**Модель 1:**<br>160 эпох + вывод каждые 40 эпох<br>`Linear(1, 16) -> ReLU() -> Linear(16, 1)` | ![Снимок экрана 2024-10-22 в 20.44.27.png](<attachment:Снимок экрана 2024-10-22 в 20.44.27.png>) ![Снимок экрана 2024-10-22 в 20.44.39.png](<attachment:Снимок экрана 2024-10-22 в 20.44.39.png>)
**Модель 2:**<br>200 эпох + вывод каждые 50 эпох<br>`Linear(1, 32) -> ReLU() -> Linear(32, 4) -> ReLU() -> Linear(4, 1)` | ![Снимок экрана 2024-10-22 в 20.45.28.png](<attachment:Снимок экрана 2024-10-22 в 20.45.28.png>) ![Снимок экрана 2024-10-22 в 20.45.50.png](<attachment:Снимок экрана 2024-10-22 в 20.45.50.png>)
**Модель 3:**<br>320 эпох + вывод каждые 80 эпох<br>`Linear(1, 128) -> ReLU() -> Linear(128, 64) -> ReLU() -> Linear(64, 32) -> ReLU() -> Linear(32, 1)` | ![Снимок экрана 2024-10-22 в 20.46.25.png](<attachment:Снимок экрана 2024-10-22 в 20.46.25.png>) ![Снимок экрана 2024-10-22 в 20.46.41.png](<attachment:Снимок экрана 2024-10-22 в 20.46.41.png>)
# Markdown:
<p class='task' id='2'></p>

2\. Решите задачу классификации, используя для мониторинга процесса обучения `wandb`.

Разделите набор данных на обучающее и тестовое множество. В процессе обучения отслеживайте динамику изменения значения функции потерь и метрики `Accuracy` по эпохам. После завершения обучения рассчитайте значение метрик Accuracy, Precision, Recall и F1 и сохраните в виде summary данного запуска.

Отследите все запуски при помощи `wandb` и вставьте в текстовую ячейку скриншоты, демонстрирующие интерфейс `wandb` с графиками обучения. Для каждого запуска приложите также скриншот с описанием гиперпараметров модели и summary (страница overview).


- [ ] Проверено на семинаре
from sklearn.datasets import make_circles
from torchmetrics import Accuracy, Precision, Recall, F1Score
import matplotlib.pyplot as plt

X, y = make_circles(n_samples=1000, noise=0.05, random_state=42)
X = th.FloatTensor(X)
y = th.LongTensor(y)
plt.scatter(X[:, 0], X[:, 1], c=y)
plt.show()
dataset = TensorDataset(X, y)
dataset_train, dataset_test = random_split(dataset, [0.8, 0.2])
dataloader_train = DataLoader(dataset_train, batch_size=16, shuffle=True)
dataloader_test = DataLoader(dataset_test, batch_size=16)
model_1 = nn.Sequential(
    nn.Linear(2, 16),
    nn.ReLU(),
    nn.Linear(16, 2),
)

model_2 = nn.Sequential(
    nn.Linear(2, 32),
    nn.ReLU(),
    nn.Linear(32, 4),
    nn.ReLU(),
    nn.Linear(4, 2),
)

model_3 = nn.Sequential(
    nn.Linear(2, 128),
    nn.ReLU(),
    nn.Linear(128, 64),
    nn.ReLU(),
    nn.Linear(64, 32),
    nn.ReLU(),
    nn.Linear(32, 2),
)
optimizer_1 = optim.AdamW(model_1.parameters(), lr=0.01)
criterion_1 = nn.CrossEntropyLoss()

optimizer_2 = optim.AdamW(model_2.parameters(), lr=0.01)
criterion_2 = nn.CrossEntropyLoss()

optimizer_3 = optim.AdamW(model_3.parameters(), lr=0.01)
criterion_3 = nn.CrossEntropyLoss()
def train(model, optimizer, criterion, num_epochs, print_every=40, model_name='model'):
    accuracy_metric = Accuracy(task='binary')
    
    for epoch in range(1, num_epochs + 1):
        model.train()
        for x_batch, y_batch in dataloader_train:
            y_pred = model(x_batch)
            loss = criterion(y_pred, y_batch)
            
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
        
        model.eval()
        with th.no_grad():
            X_train, y_train = dataset_train[:][0], dataset_train[:][1]
            y_pred_train = model(X_train)
            loss_train = criterion(y_pred_train, y_train)
            preds = th.argmax(y_pred_train, dim=1)
            acc = accuracy_metric(preds, y_train)
        
        wandb.log({'train/loss': loss_train.item(), 'train/accuracy': acc.item()})
        
        if epoch % print_every == 0 or epoch == 1:
            print(f'Epoch {epoch}/{num_epochs}, Loss: {loss_train:.4f}, Accuracy: {acc:.4f}')
    
    model.eval()
    with th.no_grad():
        X_test, y_test = dataset_test[:][0], dataset_test[:][1]
        y_pred_test = model(X_test)
        preds_test = th.argmax(y_pred_test, dim=1)
        
        accuracy = Accuracy(task='binary')(preds_test, y_test)
        precision = Precision(task='binary')(preds_test, y_test)
        recall = Recall(task='binary')(preds_test, y_test)
        f1 = F1Score(task='binary')(preds_test, y_test)
    
    wandb.summary['test/accuracy'] = accuracy.item()
    wandb.summary['test/precision'] = precision.item()
    wandb.summary['test/recall'] = recall.item()
    wandb.summary['test/f1'] = f1.item()
models = [model_1, model_2, model_3]
optimizers = [optimizer_1, optimizer_2, optimizer_3]
criterions = [criterion_1, criterion_2, criterion_3]
configs = [
    {'num_epochs': 160, 'print_every': 40, 'model_name': 'task-2, model-1'},
    {'num_epochs': 200, 'print_every': 50, 'model_name': 'task-2, model-2'},
    {'num_epochs': 320, 'print_every': 80, 'model_name': 'task-2, model-3'},
]
for model, optimizer, criterion, config in zip(models, optimizers, criterions, configs):
    wandb.init(
        project='seminar-tutorial',
        name=config['model_name'],
        tags=['task-2'],
        config=config
    )
    train(model, optimizer, criterion, **config)
    wandb.finish()
# Markdown:
**Графики обучения**<br><br>
![Снимок экрана 2024-10-25 в 12.33.03.png](<attachment:Снимок экрана 2024-10-25 в 12.33.03.png>)

Описание | Скриншоты
---------|----------
**Модель 1:**<br>160 эпох + вывод каждые 40 эпох<br>`Linear(1, 16) -> ReLU() -> Linear(16, 2)` | ![Снимок экрана 2024-10-25 в 12.36.26.png](<attachment:Снимок экрана 2024-10-25 в 12.36.26.png>) ![Снимок экрана 2024-10-25 в 12.36.46.png](<attachment:Снимок экрана 2024-10-25 в 12.36.46.png>)
**Модель 2:**<br>200 эпох + вывод каждые 50 эпох<br>`Linear(1, 32) -> ReLU() -> Linear(32, 4) -> ReLU() -> Linear(4, 2)` | ![Снимок экрана 2024-10-25 в 12.34.51.png](<attachment:Снимок экрана 2024-10-25 в 12.34.51.png>) ![Снимок экрана 2024-10-25 в 12.35.10.png](<attachment:Снимок экрана 2024-10-25 в 12.35.10.png>)
**Модель 3:**<br>320 эпох + вывод каждые 80 эпох<br>`Linear(1, 128) -> ReLU() -> Linear(128, 64) -> ReLU() -> Linear(64, 32) -> ReLU() -> Linear(32, 2)` | ![Снимок экрана 2024-10-25 в 12.34.04.png](<attachment:Снимок экрана 2024-10-25 в 12.34.04.png>) ![Снимок экрана 2024-10-25 в 12.34.24.png](<attachment:Снимок экрана 2024-10-25 в 12.34.24.png>)
# Markdown:
<p class='task' id='3'></p>

3\. Повторите задачу 2, вычислив и визуализировав матрицу несоответствий (для обучающей и тестовой выборки) тремя способами при помощи `wandb`:
* используя `torchmetrics` и представив данные в виде объекта `wandb.Table`;
* используя готовую функцию `wandb.plot.confusion_matrix`;
* построив тепловую карту при помощи `seaborn` и представив данные в виде объекта `wandb.Image`.

Вставьте в текстовую ячейку скриншоты, демонстрирующие интерфейс `wandb` со всеми нужными визуализациями.


- [ ] Проверено на семинаре
from torchmetrics import ConfusionMatrix
import pandas as pd
model_1 = nn.Sequential(
    nn.Linear(2, 16),
    nn.ReLU(),
    nn.Linear(16, 2),
)

model_2 = nn.Sequential(
    nn.Linear(2, 32),
    nn.ReLU(),
    nn.Linear(32, 4),
    nn.ReLU(),
    nn.Linear(4, 2),
)

model_3 = nn.Sequential(
    nn.Linear(2, 128),
    nn.ReLU(),
    nn.Linear(128, 64),
    nn.ReLU(),
    nn.Linear(64, 32),
    nn.ReLU(),
    nn.Linear(32, 2),
)
optimizer_1 = optim.AdamW(model_1.parameters(), lr=0.01)
criterion_1 = nn.CrossEntropyLoss()

optimizer_2 = optim.AdamW(model_2.parameters(), lr=0.01)
criterion_2 = nn.CrossEntropyLoss()

optimizer_3 = optim.AdamW(model_3.parameters(), lr=0.01)
criterion_3 = nn.CrossEntropyLoss()
def train(model, optimizer, criterion, num_epochs, print_every=40, model_name='model'):
    accuracy_metric = Accuracy(task='binary')
    for epoch in range(1, num_epochs + 1):
        model.train()
        for x_batch, y_batch in dataloader_train:
            y_pred = model(x_batch)
            loss = criterion(y_pred, y_batch)
            
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
        
        model.eval()
        with th.no_grad():
            X_train, y_train = dataset_train[:][0], dataset_train[:][1]
            y_pred_train = model(X_train)
            loss_train = criterion(y_pred_train, y_train)
            preds = th.argmax(y_pred_train, dim=1)
            acc = accuracy_metric(preds, y_train)
        
        wandb.log({
            'train/loss': loss_train.item(),
            'train/accuracy': acc.item(),
            'train/confusion_matrix': wandb.plot.confusion_matrix(
                probs=None,
                y_true=y_train.cpu().numpy(),
                preds=preds.cpu().numpy(),
                class_names=['Class 0', 'Class 1']
            )
        })
        
        if epoch % print_every == 0 or epoch == 1:
            print(f'Epoch {epoch}/{num_epochs}, Loss: {loss_train:.4f}, Accuracy: {acc:.4f}')
    
    model.eval()
    with th.no_grad():
        X_test, y_test = dataset_test[:][0], dataset_test[:][1]
        y_pred_test = model(X_test)
        preds_test = th.argmax(y_pred_test, dim=1)
        
        accuracy = Accuracy(task='binary')(preds_test, y_test)
        precision = Precision(task='binary')(preds_test, y_test)
        recall = Recall(task='binary')(preds_test, y_test)
        f1 = F1Score(task='binary')(preds_test, y_test)
        
        confusion_test = ConfusionMatrix(task='binary')(preds_test, y_test)
        conf_matrix_df = pd.DataFrame(
            confusion_test.cpu().numpy(), 
            index=['True 0', 'True 1'], 
            columns=['Pred 0', 'Pred 1']
        )
        table = wandb.Table(
            data=conf_matrix_df.values.tolist(),
            columns=conf_matrix_df.columns.tolist()
        )
        
        plt.figure(figsize=(6,5))
        sns.heatmap(conf_matrix_df, annot=True, fmt='d', cmap='Blues')
        plt.ylabel('Истина')
        plt.xlabel('Предсказание')
        plt.title('Confusion Matrix')
        heatmap_image = plt.gcf()
        plt.close()
        
    wandb.summary['test/accuracy'] = accuracy.item()
    wandb.summary['test/precision'] = precision.item()
    wandb.summary['test/recall'] = recall.item()
    wandb.summary['test/f1'] = f1.item()

    y_test_np = y_test.cpu().numpy()
    preds_test_np = preds_test.cpu().numpy()

    wandb.log({
        'test/confusion_matrix_table': table,
        'test/confusion_matrix_wandb_plot': wandb.plot.confusion_matrix(
            preds=preds_test_np,
            y_true=y_test_np,
            class_names=['Class 0', 'Class 1']
        ),
        'test/confusion_matrix_heatmap': wandb.Image(heatmap_image)
    })
models = [model_1, model_2, model_3]
optimizers = [optimizer_1, optimizer_2, optimizer_3]
criterions = [criterion_1, criterion_2, criterion_3]
configs = [
    {'num_epochs': 60, 'print_every': 20, 'model_name': 'task-3, model-1'},
    {'num_epochs': 40, 'print_every': 20, 'model_name': 'task-3, model-2'},
    {'num_epochs': 30, 'print_every': 15, 'model_name': 'task-3, model-3'},
]

for model, optimizer, criterion, config in zip(models, optimizers, criterions, configs):
    with wandb.init(
        project='seminar-tutorial',
        name=config['model_name'],
        tags=['task-3'],
        config=config
    ):
        train(model, optimizer, criterion, **config)
    wandb.finish()
# Markdown:
**Матрицы несоответствий**<br><br>
Описание | Скрин
-|-
Модель 1 | ![Снимок экрана 2024-10-27 в 15.02.23.png](<attachment:Снимок экрана 2024-10-27 в 15.02.23.png>)
Модели 1,2,3 | ![Снимок экрана 2024-10-27 в 15.03.29.png](<attachment:Снимок экрана 2024-10-27 в 15.03.29.png>)
# Markdown:
<p class='task' id='4'></p>

4\. Повторите задачу 2, обучив две модели: линейную и нелинейную. Для каждой из моделей сделайте прогноз (по всей выборке) и визуализируйте облако точек в виде `wandb.Image` (раскрасьте точки в цвета, соответствующие прогнозам модели).

Вставьте в текстовую ячейку скриншоты, демонстрирующие интерфейс `wandb` со всеми нужными визуализациями.


- [ ] Проверено на семинаре
lin = nn.Linear(2, 2)
nonlin = nn.Sequential(
    nn.Linear(2, 16),
    nn.ReLU(),
    nn.Linear(16, 2),
)
optimizer_linear = optim.AdamW(lin.parameters(), lr=0.01)
criterion_linear = nn.CrossEntropyLoss()

optimizer_nonlinear = optim.AdamW(nonlin.parameters(), lr=0.01)
criterion_nonlinear = nn.CrossEntropyLoss()
def train(model, optimizer, criterion, num_epochs, print_every=40, model_name='model'):
    accuracy_metric = Accuracy(task='binary')
    
    for epoch in range(1, num_epochs + 1):
        model.train()
        for x_batch, y_batch in dataloader_train:
            y_pred = model(x_batch)
            loss = criterion(y_pred, y_batch)
            
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
        
        model.eval()
        with th.no_grad():
            X_train, y_train = dataset_train[:][0], dataset_train[:][1]
            y_pred_train = model(X_train)
            loss_train = criterion(y_pred_train, y_train)
            preds_train = th.argmax(y_pred_train, dim=1)
            acc_train = accuracy_metric(preds_train, y_train)
        
        wandb.log({'train/loss': loss_train.item(), 'train/accuracy': acc_train.item()}, step=epoch)
        
        if epoch % print_every == 0 or epoch == 1:
            print(f'Epoch {epoch}/{num_epochs}, Loss: {loss_train:.4f}, Accuracy: {acc_train:.4f}')
    
    model.eval()
    with th.no_grad():
        X_test, y_test = dataset_test[:][0], dataset_test[:][1]
        y_pred_test = model(X_test)
        preds_test = th.argmax(y_pred_test, dim=1)
        
        accuracy = Accuracy(task='binary')(preds_test, y_test)
        precision = Precision(task='binary')(preds_test, y_test)
        recall = Recall(task='binary')(preds_test, y_test)
        f1 = F1Score(task='binary')(preds_test, y_test)
    
    wandb.summary['test/accuracy'] = accuracy.item()
    wandb.summary['test/precision'] = precision.item()
    wandb.summary['test/recall'] = recall.item()
    wandb.summary['test/f1'] = f1.item()
    
    with th.no_grad():
        X_all, y_all = dataset[:][0], dataset[:][1]
        y_pred_all = model(X_all)
        preds_all = th.argmax(y_pred_all, dim=1)
    
    plt.figure(figsize=(6, 6))
    plt.scatter(X_all[:, 0], X_all[:, 1], c=preds_all.cpu(), alpha=0.5)
    plt.title(model_name)
    plt.xlabel('x1')
    plt.ylabel('x2')
    plt.tight_layout()
    
    image = plt.gcf()
    plt.close()
    
    wandb.log({'predictions_scatter': wandb.Image(image)})
models = [lin, nonlin]
optimizers = [optimizer_linear, optimizer_nonlinear]
criterions = [criterion_linear, criterion_nonlinear]
configs = [
    {'num_epochs': 60, 'print_every': 20, 'model_name': 'task-4, model-linear'},
    {'num_epochs': 60, 'print_every': 20, 'model_name': 'task-4, model-nonlinear'},
]
for model, optimizer, criterion, config in zip(models, optimizers, criterions, configs):
    wandb.init(
        project='seminar-tutorial',
        name=config['model_name'],
        tags=['task-4'],
        config=config
    )
    train(model, optimizer, criterion, **config)
    wandb.finish()
# Markdown:
**Визуализация**<br><br>
Описание | Скрин
-|-
Предсказание | ![Снимок экрана 2024-10-27 в 15.11.12.png](<attachment:Снимок экрана 2024-10-27 в 15.11.12.png>)
Loss | ![Снимок экрана 2024-10-27 в 15.11.16.png](<attachment:Снимок экрана 2024-10-27 в 15.11.16.png>)
# Markdown:
<p class='task' id='5'></p>

5\. Повторите задачу 2, реализовав логику ранней остановки. Для этого разделите данные на три части: обучающую, валидационную и тестовую. Остановите процесс обучения, если целевая метрика (F1) на валидации не увеличивалась в течении последних $k$ ($k$ - гиперпараметр метода) эпох. В момент остановки выведите сообщение с текущим номером эпохи. Сохраните номер эпохи, на которой процесс обучения был прерван, в виде summary данного запуска.

Помимо отслеживания метрик на обучающей и тестовой выборке, также отслеживайте метрики на валидационной выборке в процессе обучения.

Постройте таблицу `wandb.Table`, в которой содержится информация о:
* признаках объекта;
* правильном ответе;
* прогнозе модели;
* принадлежности к обучающему, валидационному или тестовому множеству.

Визуализируйте данную таблицу при помощи `wandb`.

Вставьте в текстовую ячейку скриншоты, демонстрирующие интерфейс `wandb` со всеми нужными визуализациями.

- [ ] Проверено на семинаре
dataset = TensorDataset(X, y)
dataset_train, temp_subset = random_split(dataset, [0.7, 0.3])
dataset_val, dataset_test = random_split(temp_subset, [0.67, 0.33])

dataloader_train = DataLoader(dataset_train, batch_size=16, shuffle=True)
dataloader_val = DataLoader(dataset_val, batch_size=16)
dataloader_test = DataLoader(dataset_test, batch_size=16)
model_1 = nn.Sequential(
    nn.Linear(2, 16),
    nn.ReLU(),
    nn.Linear(16, 2),
)

model_2 = nn.Sequential(
    nn.Linear(2, 32),
    nn.ReLU(),
    nn.Linear(32, 4),
    nn.ReLU(),
    nn.Linear(4, 2),
)

model_3 = nn.Sequential(
    nn.Linear(2, 128),
    nn.ReLU(),
    nn.Linear(128, 64),
    nn.ReLU(),
    nn.Linear(64, 32),
    nn.ReLU(),
    nn.Linear(32, 2),
)
optimizer_1 = optim.AdamW(model_1.parameters(), lr=0.01)
criterion_1 = nn.CrossEntropyLoss()

optimizer_2 = optim.AdamW(model_2.parameters(), lr=0.01)
criterion_2 = nn.CrossEntropyLoss()

optimizer_3 = optim.AdamW(model_3.parameters(), lr=0.01)
criterion_3 = nn.CrossEntropyLoss()
def train(model, optimizer, criterion, num_epochs, patience, print_every=40, model_name='model'):
    best_f1 = 0
    epochs_no_improve = 0
    epoch_counter = 0

    best_model_state = model.state_dict()
    for epoch in range(1, num_epochs + 1):
        epoch_counter = epoch
        model.train()
        for x_batch, y_batch in dataloader_train:
            y_pred = model(x_batch)
            loss = criterion(y_pred, y_batch)
            
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
        
        model.eval()
        with th.no_grad():
            X_train, y_train = dataset_train[:][0], dataset_train[:][1]
            y_pred_train = model(X_train)
            loss_train = criterion(y_pred_train, y_train)
            preds_train = th.argmax(y_pred_train, dim=1)
            acc_train = Accuracy(task='binary')(preds_train, y_train)
            f1_train = F1Score(task='binary')(preds_train, y_train)
        
            X_val, y_val = dataset_val[:][0], dataset_val[:][1]
            y_pred_val = model(X_val)
            loss_val = criterion(y_pred_val, y_val)
            preds_val = th.argmax(y_pred_val, dim=1)
            acc_val = Accuracy(task='binary')(preds_val, y_val)
            f1_val = F1Score(task='binary')(preds_val, y_val)
        
            X_test, y_test = dataset_test[:][0], dataset_test[:][1]
            y_pred_test = model(X_test)
            loss_test = criterion(y_pred_test, y_test)
            preds_test = th.argmax(y_pred_test, dim=1)
            acc_test = Accuracy(task='binary')(preds_test, y_test)
            f1_test = F1Score(task='binary')(preds_test, y_test)
        
        wandb.log({
            'epoch': epoch,
            'train/loss': loss_train.item(),
            'train/accuracy': acc_train.item(),
            'train/f1': f1_train.item(),
            'val/loss': loss_val.item(),
            'val/accuracy': acc_val.item(),
            'val/f1': f1_val.item(),
            'test/loss': loss_test.item(),
            'test/accuracy': acc_test.item(),
            'test/f1': f1_test.item(),
        })
        
        if epoch % print_every == 0 or epoch == 1:
            print(f'Epoch {epoch}/{num_epochs}, Train Loss: {loss_train:.4f}, Train F1: {f1_train:.4f}, Val Loss: {loss_val:.4f}, Val F1: {f1_val:.4f}')
        
        if f1_val > best_f1:
            best_f1 = f1_val
            epochs_no_improve = 0
            best_model_state = model.state_dict()
        else:
            epochs_no_improve += 1
            if epochs_no_improve >= patience:
                print(f'Ранняя остановка на эпохе: {epoch}')
                break
    
    model.eval()
    with th.no_grad():
        X_test, y_test = dataset_test[:][0], dataset_test[:][1]
        y_pred_test = model(X_test)
        preds_test = th.argmax(y_pred_test, dim=1)
        
        accuracy_test = Accuracy(task='binary')(preds_test, y_test)
        precision_test = Precision(task='binary')(preds_test, y_test)
        recall_test = Recall(task='binary')(preds_test, y_test)
        f1_test = F1Score(task='binary')(preds_test, y_test)
    
    wandb.summary['test/accuracy'] = accuracy_test.item()
    wandb.summary['test/precision'] = precision_test.item()
    wandb.summary['test/recall'] = recall_test.item()
    wandb.summary['test/f1'] = f1_test.item()
    wandb.summary['early_stop_epoch'] = epoch_counter
    
    X_all, y_all = dataset[:][0], dataset[:][1]
    model.eval()
    with th.no_grad():
        y_pred_all = model(X_all)
        preds_all = th.argmax(y_pred_all, dim=1)
    
    data = []
    for i in range(len(dataset)):
        features = X_all[i].cpu().numpy()
        label = y_all[i].item()
        pred = preds_all[i].item()
        if i in dataset_train.indices:
            set_membership = 'train'
        elif i in dataset_val.indices:
            set_membership = 'validation'
        elif i in dataset_test.indices:
            set_membership = 'test'
        data.append([features[0], features[1], label, pred, set_membership])
    
    table = wandb.Table(columns=['x1', 'x2', 'label', 'pred', 'subset'])
    for row in data:
        table.add_data(*row)
    
    wandb.log({'data_table': table})
    return best_model_state
models = [model_1, model_2, model_3]
optimizers = [optimizer_1, optimizer_2, optimizer_3]
criterions = [criterion_1, criterion_2, criterion_3]
configs = [
    {'num_epochs': 60, 'print_every': 20, 'patience': 20, 'model_name': 'task-5, model-1'},
    {'num_epochs': 40, 'print_every': 20, 'patience': 20, 'model_name': 'task-5, model-2'},
    {'num_epochs': 30, 'print_every': 15, 'patience': 20, 'model_name': 'task-5, model-3'},
]
for model, optimizer, criterion, config in zip(models, optimizers, criterions, configs):
    wandb.init(
        project='seminar-tutorial',
        name=config['model_name'],
        tags=['task-5'],
        config=config
    )
    train(model, optimizer, criterion, **config)
    wandb.finish()
# Markdown:
Описание | Скрин
-|-
Таблица | ![Снимок экрана 2024-10-27 в 15.46.15.png](<attachment:Снимок экрана 2024-10-27 в 15.46.15.png>)
Тренировка | ![Снимок экрана 2024-10-27 в 15.48.30.png](<attachment:Снимок экрана 2024-10-27 в 15.48.30.png>)
Валидация | ![Снимок экрана 2024-10-27 в 15.47.21.png](<attachment:Снимок экрана 2024-10-27 в 15.47.21.png>)
Оценка | ![Снимок экрана 2024-10-27 в 15.47.09.png](<attachment:Снимок экрана 2024-10-27 в 15.47.09.png>)


# 03_2_init_optim_dropout_batchnorm.ipynb
# Markdown:
#  Инициализация весов нейронных сетей. Способы регуляризации нейронных сетей. Продвинутые алгоритмы градиентного спуска.

__Автор задач: Блохин Н.В. (NVBlokhin@fa.ru)__

Материалы:
* Deep Learning with PyTorch (2020) Авторы: Eli Stevens, Luca Antiga, Thomas Viehmann
* https://pytorch.org/docs/stable/nn.init.html
* https://adityassrana.github.io/blog/theory/2020/08/26/Weight-Init.html
* https://machinelearningmastery.com/dropout-for-regularizing-deep-neural-networks/
* https://machinelearningmastery.com/batch-normalization-for-training-of-deep-neural-networks/
* https://pytorch.org/docs/stable/optim.html
* https://seaborn.pydata.org/examples/errorband_lineplots.html
# Markdown:
## Задачи для совместного разбора
# Markdown:
1\. Инициализируйте веса полносвязного слоя единицами, а смещения - нулями.
import torch as th
import torch.nn as nn
fc = nn.Linear(in_features=5, out_features=3)
fc.weight
nn.init.ones_(fc.weight)
fc.weight
# Markdown:
2\. Изучите, как работает слой `nn.Dropout` в режиме обучения модели и в режиме использования модели.
model = nn.Sequential(
    # ...
    nn.Dropout(p=0.5)
)
X = th.randn(1, 5)
X
model(X) # scale: 1/(1-p)
model.eval()
model(X)
model.train()
# Markdown:
3\. Изучите, как работает слой `nn.BatchNorm1d` в режиме обучения модели и в режиме использования модели.
X = th.randn(100, 5)
X[:10]
X.mean(dim=0), X.std(dim=0), X.var(dim=0)
bn = nn.BatchNorm1d(num_features=5)
y = bn(X)
y.mean(dim=0), y.std(dim=0), y.var(dim=0)
bn.weight, bn.bias
bn.running_mean
bn.running_var
model.eval()
y = bn(X)
y.mean(dim=0), y.std(dim=0), y.var(dim=0)
# Markdown:
## Задачи для самостоятельного решения
# Markdown:
<p class="task" id="1"></p>

1\. Расширьте класс `torch.nn.Linear`, описав класс `InitializedLinear` и добавив возможность инициализировать веса слоя при помощи функций из пакета `torch.nn.init` (инициализацию bias оставьте по умолчанию). Обратите внимание, что данные функции имеют дополнительные параметры. Данные параметры должны передаваться в момент создания объекта класса `InitializedLinear`.

Пример создания слоя:
```
InitializedLinear(n_features, n_hidden, init_f=nn.init.uniform_, init_args={"a": 0.0, "b": 1.0})
```

- [ ] Проверено на семинаре
import torch.nn as nn
from typing import Callable

class InitializedLinear(nn.Linear):
    def __init__(self, n_features: int, n_hidden: int, init_f: Callable, init_args: dict) -> None:
        super().__init__(n_features, n_hidden)
        init_f(self.weight, **init_args)
lin = InitializedLinear(2, 1, init_f=nn.init.uniform_, init_args={"a": 0.0, "b": 1.0})
lin.weight
# Markdown:
<p class="task" id="2"></p>

2\. Решите задачу регрессии несколько раз, изменяя способ инициализации весов. Рассмотрите следующие варианты:
* `nn.init.uniform_`
* `nn.init.normal_`
* `nn.init.constant_`
* `nn.xavier_uniform_`
* `nn.kaiming_uniform_`
* `nn.xavier_normal_`
* `nn.kaiming_normal_`

Визуализируйте график изменения значений MSE с ходом эпох. Дайте кривым, соответствующие разным способам инициализации, различные цвета и добавьте подписи. Обратите внимание, что от запуска к запуску результаты могут отличаться. Чтобы решить эту проблему, обучайте каждую модель несколько раз и визуализируйте доверительный интервал (можно воспользоваться `seaborn.lineplot`).



- [ ] Проверено на семинаре
import torch as th
X = th.linspace(0, 1, 100).view(-1, 1)
y = th.sin(2 * th.pi * X) + 0.1 * th.rand(X.size())
from torch.utils.data import TensorDataset, DataLoader

dataset = TensorDataset(X, y)
loader = DataLoader(dataset, batch_size=16)
methods = (
    (nn.init.uniform_, {'a': 0.0, 'b': 1.0},),
    (nn.init.normal_, {'mean': 0.0, 'std': 1.0}),
    (nn.init.constant_, {'val': 1.0}),
    (nn.init.xavier_uniform_, {'gain': 1.0}),
    (nn.init.kaiming_uniform_, {'a': 0.0, 'mode': 'fan_in', 'nonlinearity': 'leaky_relu'}),
    (nn.init.xavier_normal_, {'gain': 1.0}),
    (nn.init.kaiming_normal_, {'a': 0.0, 'mode': 'fan_in', 'nonlinearity': 'leaky_relu'})
)
runs = 10
epochs = 3
from torch import optim
import numpy as np

final_losses = []
for method, kwargs in methods:
    run_losses = []
    for run in range(runs):
        model = nn.Sequential(
            InitializedLinear(n_features=1, n_hidden=100, init_f=method, init_args=kwargs),
            nn.ReLU(),
            InitializedLinear(n_features=100, n_hidden=1, init_f=method, init_args=kwargs),
        )

        criterion = nn.MSELoss()
        optimizer = optim.SGD(model.parameters())

        model.train()
        epoch_losses = []
        for epoch in range(epochs):
            epoch_loss = 0
            for X_batch, y_batch in loader:
                y_pred = model(X_batch)
                loss = criterion(y_pred, y_batch)
                epoch_loss += loss.item()
                optimizer.zero_grad()
                loss.backward()
                optimizer.step()
            epoch_losses.append(epoch_loss / len(loader))
        run_losses.append(epoch_losses)

    final_losses.append(run_losses)
import matplotlib.pyplot as plt
import seaborn as sns

epochs_range = range(1, epochs + 1)
plt.figure(figsize=(10, 6))
final_losses_np = np.array(final_losses)
for i, (method, kwargs) in enumerate(methods):
    mean = final_losses_np[i].mean(axis=0)
    std = final_losses_np[i].std(axis=0)
    sns.lineplot(x=epochs_range, y=mean, label=f'Метод {method.__name__}', marker='o')
    plt.fill_between(epochs_range, mean - std, mean + std, alpha=0.3)

plt.xlabel('Epoch')
plt.ylabel('Loss')
plt.title('График Ошибок')
plt.legend()
plt.grid(True)
plt.show()
# Markdown:
<p class="task" id="3"></p>

3\. Исследуйте, как добавление дропаута влияет на процесс обучения модели. Решите задачу регрессии несколько раз, изменяя значения вероятности дропаута $p$ от 0 до 0.8. В качестве модели рассмотрите нейронную сеть с одним скрытым слоем.

Визуализируйте график изменения значений $R^2$ в зависимости от вероятности дропаута $p$ на обучающей и тестовой выборке. Визуализируйте на отдельном графике зависимости разности между $R^2$ на обучающей выборки и $R^2$ на тестовой выборке.



- [ ] Проверено на семинаре
from sklearn.datasets import make_regression
from torch.utils.data import random_split
import torch as th

th.manual_seed(42)
X, y, coef = make_regression(
    n_samples=100,
    n_features=50,
    n_informative=20,
    noise=2,
    coef=True,
    random_state=42,

)
X = th.FloatTensor(X)
y = th.FloatTensor(y).reshape(-1, 1)
dataset = TensorDataset(X, y)
dataset_train, dataset_test = random_split(TensorDataset(X, y), [0.8, 0.2])
dataloader_train = DataLoader(dataset_train, batch_size=16)
dataloader_test = DataLoader(dataset_test, batch_size=16)
from torchmetrics import R2Score

epoch_max = 20
p_list = th.linspace(0, 0.8, 10)

loss_list = []
for p in p_list:
    model = nn.Sequential(
        nn.Linear(in_features=50, out_features=32),
        nn.ReLU(),
        nn.Dropout(p=p),
        nn.Linear(in_features=32, out_features=1)
    )
    criterion = nn.MSELoss()
    optimizer = optim.SGD(model.parameters())

    for epoch in range(epoch_max):
        model.train()
        for X_batch, y_batch in dataloader_train:
            y_pred = model(X_batch)
            loss = criterion(y_pred, y_batch)
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
        
    model.eval()
    with th.no_grad():
        loss = 0
        y_pred_train = model(dataset_train[:][0])
        loss_train = R2Score()(y_pred_train, dataset_train[:][1]).item()

        y_pred_test = model(dataset_test[:][0])
        loss_test = R2Score()(y_pred_test, dataset_test[:][1]).item()

        loss_list.append((p, loss_train, loss_test))
import pandas as pd

df = pd.DataFrame(loss_list, columns=['p', 'train', 'test'])

plt.figure(figsize=(10, 6))
plt.plot(df['p'], df['train'], label='Train R', marker='o')
plt.plot(df['p'], df['test'], label='Test R', marker='o')

plt.xlabel('Dropout Probability')
plt.ylabel('R2 Score')
plt.title('R2 Score')
plt.legend()
plt.grid(True)
plt.show()
plt.figure(figsize=(10, 6))
plt.plot(df['p'], abs(df['train'] - df['test']), marker='o')
plt.xlabel('Dropout Probability')
plt.ylabel('R2 Score')
plt.title('Разница r2 train/test')
plt.grid(True)
plt.show()
# Markdown:
<p class="task" id="4"></p>

4\. Решите задачу регрессии с и без использования пакетной нормализации. Покажите, как меняется результат обучения моделей при различных значениях скорости обучения (0.001, 0.01, 0.1) за одно и то же количество эпох.

Визуализируйте график изменения значений $R^2$ в зависимости от эпохи при различных значениях скорости обучения с и без использования пакетной нормализации.



- [ ] Проверено на семинаре
from sklearn.datasets import load_diabetes

X, y = load_diabetes(return_X_y=True)
X = th.FloatTensor(X)
y = th.FloatTensor(y).reshape(-1, 1)
y = (y - y.mean()) / y.std()
dataset = TensorDataset(X, y)
loader = DataLoader(dataset, batch_size=16)
lr_range = [0.001, 0.01, 0.1]
iter_list = [(i, 0) for i in lr_range] + [(i, 1) for i in lr_range]
epoch_max = 250

data_without_norm = []
for lr, norm in iter_list:
    if norm:
        model = nn.Sequential(
            nn.Linear(10, 4),
            nn.BatchNorm1d(num_features=4),
            nn.ReLU(),
            nn.Linear(4, 4),
            nn.BatchNorm1d(num_features=4),
            nn.ReLU(),
            nn.Linear(4, 1),
        )
    else:
        model = nn.Sequential(
            nn.Linear(10, 4),
            nn.ReLU(),
            nn.Linear(4, 4),
            nn.ReLU(),
            nn.Linear(4, 1),
        )
    criterion = nn.MSELoss()
    optimizer = optim.SGD(model.parameters(), lr=lr)

    r2 = []
    for epoch in range(epoch_max):
        r2_metric = R2Score()
        for X_batch, y_batch in loader:
            y_pred = model(X_batch)
            loss = criterion(y_pred.flatten(), y_batch.flatten())
            r2_metric.update(y_pred.flatten(), y_batch.flatten())
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
        r2.append(r2_metric.compute().item())

    data_without_norm.append((lr, norm, r2))
plt.title('R2 Score')
for item in data_without_norm:
    plt.plot(item[2], label=f'{item[0]} – {'без нормализации' if item[1] == 0 else 'с нормализацией'}')
plt.grid(True)
plt.legend()
plt.ylabel('R2')
plt.xlabel('Epoch')
plt.ylim(bottom=0)
plt.show()
# Markdown:
<p class="task" id="5"></p>

5\. Решите задачу регрессии c использованием различных алгоритмов градиентного спуска. Покажите, как меняется результат обучения моделей при использовании различных алгоритмов (Adam, Adagrad, RMSProp, SGD) за одно и то же количество эпох с одной и той же скоростью обучения. Используйте модель с архитектурой, аналогичной модели из предыдущей задачи.

Визуализируйте график изменения значений MAPE в зависимости от эпохи при использовании различных алгоритмов градиентного спуска.

- [ ] Проверено на семинаре
def load_boston():
    import pandas as pd
    import numpy as np

    data_url = "http://lib.stat.cmu.edu/datasets/boston"
    raw_df = pd.read_csv(data_url, sep=r'\s+', skiprows=22, header=None)
    data = np.hstack([raw_df.values[::2, :], raw_df.values[1::2, :2]])
    target = raw_df.values[1::2, 2]
    return data, target
X, y = load_boston()
X = th.FloatTensor(X)
y = th.FloatTensor(y).reshape(-1, 1)
dataset = TensorDataset(X, y)
loader = DataLoader(dataset, batch_size=10)
optimizers = [optim.Adam, optim.Adagrad, optim.RMSprop, optim.SGD]
from torchmetrics import MeanAbsolutePercentageError

epoch_max = 40

results = []
data = []
for optimizer_ in optimizers:
    model = nn.Sequential(
        nn.Linear(13, 4),
        nn.BatchNorm1d(num_features=4),
        nn.ReLU(),
        nn.Linear(4, 4),
        nn.BatchNorm1d(num_features=4),
        nn.ReLU(),
        nn.Linear(4, 1),
    )
    criterion = nn.MSELoss()
    optimizer = optimizer_(model.parameters(), lr=0.01)

    mape = []
    for epoch in range(epoch_max):
        mape_metric = MeanAbsolutePercentageError()
        for X_batch, y_batch in loader:
            y_pred = model(X_batch)
            loss = criterion(y_pred.flatten(), y_batch.flatten())
            mape_metric.update(y_pred.flatten(), y_batch.flatten())
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
        mape.append(mape_metric.compute().item())

    data.append((optimizer_.__name__, mape))
plt.title('MAPE Error')
for item in data:
    plt.plot(item[1], label=f'{item[0]}')
plt.grid(True)
plt.legend()
plt.ylabel('MAPE')
plt.xlabel('Epoch')
plt.show()


# 03_3_lightning.ipynb
# Markdown:
#  PyTorch Lightning

__Автор задач: Блохин Н.В. (NVBlokhin@fa.ru)__

Материалы:
* https://lightning.ai/docs/pytorch/stable/starter/introduction.html
* https://lightning.ai/docs/pytorch/stable/levels/core_skills.html
* https://lightning.ai/docs/pytorch/stable/api/lightning.pytorch.core.LightningModule.html#lightning.pytorch.core.LightningModule.log
* https://lightning.ai/docs/pytorch/stable/extensions/logging.html
* https://lightning.ai/docs/pytorch/stable/common/progress_bar.html
* https://lightning.ai/docs/pytorch/stable/common/early_stopping.html
* https://lightning.ai/docs/pytorch/1.6.3/api/pytorch_lightning.utilities.model_summary.html#pytorch_lightning.utilities.model_summary.ModelSummary
* https://torchmetrics.readthedocs.io/en/stable/pages/lightning.html
* https://pytorch-lightning.readthedocs.io/en/2.1.2/pytorch/
* https://www.youtube.com/watch?v=XbIN9LaQycQ&list=PLhhyoLH6IjfyL740PTuXef4TstxAK6nGP
* https://pytorch-lightning.readthedocs.io/en/2.1.2/pytorch/data/datamodule.html
# Markdown:
## Задачи для совместного разбора
# Markdown:
1\. Создайте датасет для классификации и обучите модель при помощи PyTorch Lightning.
!pip install pytorch_lightning
!pip install torchmetrics
import torch
from torch import nn
from torch.utils.data import DataLoader, Dataset
from typing import Any, List, Optional, Union
import pytorch_lightning as pl

class MyLightningModule(pl.LightningModule):
    """
    Класс модуля PyTorch Lightning.

    Этот класс определяет структуру модели, шаги обучения, валидации и тестирования,
    а также настройки оптимизатора для обучения.
    """

    def __init__(self):
        """Здесь определяется архитектура модели и инициализируются все необходимые слои."""
        super().__init__()

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """
        Определяет прямой проход модели.

        Args:
            x (torch.Tensor): Входной тензор.

        Returns:
            torch.Tensor: Выходной тензор модели.
        """

    def training_step(self, batch: Any, batch_idx: int) -> torch.Tensor:
        """
        Выполняет один шаг обучения.

        Args:
            batch (Any): Батч данных для обучения.
            batch_idx (int): Индекс текущего батча.

        Returns:
            torch.Tensor: Значение функции потерь для этого шага.
        """

    def validation_step(self, batch: Any, batch_idx: int) -> None:
        """
        Выполняет один шаг валидации.

        Args:
            batch (Any): Батч данных для валидации.
            batch_idx (int): Индекс текущего батча.
        """

    def test_step(self, batch: Any, batch_idx: int) -> None:
        """
        Выполняет один шаг тестирования.

        Args:
            batch (Any): Батч данных для тестирования.
            batch_idx (int): Индекс текущего батча.
        """

    def configure_optimizers(self) -> torch.optim.Optimizer:
        """
        Настраивает оптимизатор для обучения модели.

        Returns:
            torch.optim.Optimizer: Настроенный оптимизатор.
        """
import torch  as th
import torch.nn
from torch.utils.data import TensorDataset
from torchmetrics import Accuracy
num_samples, num_features = 1000, 10

train_size = int(num_samples * 0.8)
X = th.randn(num_samples, num_features)
y = th.randint(0, 2, (num_samples, ))

dataset_train = TensorDataset(X[:train_size], y[:train_size])
dataset_val = TensorDataset(X[train_size:], y[train_size:])
import torch
from torch import nn
from torch.utils.data import DataLoader, Dataset
from typing import Any, List, Optional, Union
import pytorch_lightning as pl

class MyLightningModule(pl.LightningModule):
    """
    Класс модуля PyTorch Lightning.

    Этот класс определяет структуру модели, шаги обучения, валидации и тестирования,
    а также настройки оптимизатора для обучения.
    """

    def __init__(self, n_inputs: int, n_hidden: int, n_out: int) -> None:
        super().__init__()
        self.model = nn.Sequential(
            nn.Linear(n_inputs, n_hidden),
            nn.ReLU(),
            nn.Linear(n_hidden, n_out),
        )

        self.criterion = nn.CrossEntropyLoss()
        self.accuracy = Accuracy(task="binary")


    def forward(self, x: torch.Tensor) -> torch.Tensor:
        return self.model(x)


    def training_step(self, batch: Any, batch_idx: int) -> torch.Tensor:
        x, y = batch
        logits = self(x) # self.forward(x)
        loss = self.criterion(logits, y)

        # доп. метрики
        preds = logits.argmax(dim=1)
        acc = self.accuracy(preds, y)

        # logging
        self.log("train_loss", loss, prog_bar=True)
        self.log("train_acc", acc, prog_bar=True)

        return loss

    def validation_step(self, batch: Any, batch_idx: int) -> None:
        x, y = batch
        logits = self(x) # self.forward(x)

        # доп. метрики
        preds = logits.argmax(dim=1)
        acc = self.accuracy(preds, y)
        self.log("val_acc", acc, prog_bar=True)


    def test_step(self, batch: Any, batch_idx: int) -> None:
        x, y = batch
        logits = self(x) # self.forward(x)

        # доп. метрики
        preds = logits.argmax(dim=1)
        acc = self.accuracy(preds, y)
        self.log("test_acc", acc, prog_bar=True)

    def configure_optimizers(self) -> torch.optim.Optimizer:
        return th.optim.Adam(self.parameters(), lr=0.001)
from torch.utils.data import DataLoader

class MyDataModule(pl.LightningDataModule):
    def __init__(self, num_samples: int, num_features: int, batch_size: int):
        super().__init__()
        self.num_samples = num_samples
        self.num_features = num_features
        self.batch_size = batch_size

    def setup(self, stage: Optional[str] = None) -> None:
        """
        Настраивает данные для использования на каждом этапе (обучение/валидация/тестирование).

        Args:
            stage (Optional[str]): Этап, на котором вызывается метод ("fit" или "test").
        """
        train_size = int(self.num_samples * 0.8)
        X = th.randn(self.num_samples, self.num_features)
        y = th.randint(0, 2, (self.num_samples, ))

        self.dataset_train = TensorDataset(X[:train_size], y[:train_size])
        self.dataset_val = TensorDataset(X[train_size:], y[train_size:])

    def train_dataloader(self) -> DataLoader:
        return DataLoader(self.dataset_train, batch_size=self.batch_size, shuffle=True)

    def val_dataloader(self) -> DataLoader:
        return DataLoader(self.dataset_val, batch_size=self.batch_size, shuffle=False)
model = MyLightningModule(10, 5, 2)
data_module = MyDataModule(1000, 10, 32)

trainer = pl.Trainer(
    max_epochs=10,
    log_every_n_steps=1,
)
trainer.fit(model, data_module)
class MyDataModule(pl.LightningDataModule):
    """
    Класс модуля данных PyTorch Lightning.

    Этот класс отвечает за загрузку, подготовку и предоставление данных
    для обучения, валидации и тестирования модели.
    """

    def __init__(self):
        """
        Инициализирует модуль данных.

        Args:
            data_dir (str): Путь к директории с данными.
        """
        super().__init__()

    def prepare_data(self) -> None:
        """
        Подготавливает данные для использования.

        Здесь можно выполнить загрузку данных или другие подготовительные операции.
        """
        pass

    def setup(self, stage: Optional[str] = None) -> None:
        """
        Настраивает данные для использования на каждом этапе (обучение/валидация/тестирование).

        Args:
            stage (Optional[str]): Этап, на котором вызывается метод ("fit" или "test").
        """

    def train_dataloader(self) -> DataLoader:
        """
        Возвращает DataLoader для обучающих данных.

        Returns:
            DataLoader: DataLoader с обучающими данными.
        """

    def val_dataloader(self) -> DataLoader:
        """
        Возвращает DataLoader для данных валидации.

        Returns:
            DataLoader: DataLoader с данными валидации.
        """


    def test_dataloader(self) -> DataLoader:
        """
        Возвращает DataLoader для тестовых данных.

        Returns:
            DataLoader: DataLoader с тестовыми данными.
        """
# Markdown:
## Задачи для самостоятельного решения
# Markdown:
<p class="task" id="1"></p>

1\. Загрузите набор данных из файла `Walmart.csv`. Выполните следующую процедуру предобработки:
- замените цены `Weekly_Sales` на логарифм цены;
- удалите столбец с датами;
- закодируйте столбцы `Store` и `Holiday_Flag` при помощи `TargetEncoder` (см. пакет [category_encoders](https://contrib.scikit-learn.org/category_encoders/));
- после кодирование выполните стандартизацию признаков;
- разбейте выборку на обучающее, валидационное и тестовое множество.

Все преобразования допускает делать при помощи `numpy`, `pandas` и `sklearn`.

- [ ] Проверено на семинаре
# Markdown:
<p class="task" id="2"></p>

2\. В ячейках ниже представлен шаблонный код для обучения модели. В данной версии все реализовано "с нуля": обучение, метрики, визуализация, логирование, логика ранней остановки.

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

- [ ] Проверено на семинаре
import torch
import torch.nn as nn
import torch.optim as optim
import numpy as np
import matplotlib.pyplot as plt
def r2_score(y_true, y_pred):
    total_sum_squares = torch.sum((y_true - y_true.mean())**2)
    residual_sum_squares = torch.sum((y_true - y_pred)**2)
    r2 = 1 - (residual_sum_squares / total_sum_squares)
    return r2

def mape_score(y_true, y_pred):
    return torch.mean(torch.abs((y_true - y_pred) / y_true)) * 100
class RegressionModel(nn.Module):
    def __init__(self, n_inputs, h_hidden):
        super().__init__()
        self.fc1 = nn.Linear(n_inputs, h_hidden)
        self.fc2 = nn.Linear(h_hidden, 1)

    def forward(self, x):
        out = self.fc1(x)
        out = out.relu()
        out = self.fc2(out)
        return out
class EarlyStopping:
    def __init__(self, patience=7, min_delta=0):
        self.patience = patience
        self.min_delta = min_delta
        self.counter = 0
        self.best_loss = None
        self.early_stop = False

    def __call__(self, val_loss):
        if self.best_loss is None:
            self.best_loss = val_loss
        elif val_loss > self.best_loss - self.min_delta:
            self.counter += 1
            if self.counter >= self.patience:
                self.early_stop = True
        else:
            self.best_loss = val_loss
            self.counter = 0
def train_model(model, train_loader, val_loader, criterion, optimizer, num_epochs, early_stopping):
    train_losses = []
    val_losses = []
    train_r2s = []
    val_r2s = []
    train_mapes = []
    val_mapes = []

    for epoch in range(num_epochs):
        model.train()
        train_loss = 0.0
        train_r2 = 0.0
        train_mape = 0.0
        for inputs, targets in train_loader:
            optimizer.zero_grad()
            outputs = model(inputs).flatten()
            loss = criterion(outputs, targets)
            loss.backward()
            optimizer.step()
            train_loss += loss.item()
            train_r2 += r2_score(targets, outputs).item()
            train_mape += mape_score(targets, outputs).item()

        train_loss /= len(train_loader)
        train_r2 /= len(train_loader)
        train_mape /= len(train_loader)

        train_losses.append(train_loss)
        train_r2s.append(train_r2)
        train_mapes.append(train_mape)

        model.eval()
        val_loss = 0.0
        val_r2 = 0.0
        val_mape = 0.0
        with torch.no_grad():
            for inputs, targets in val_loader:
                outputs = model(inputs).flatten()
                loss = criterion(outputs, targets)
                val_loss += loss.item()
                val_r2 += r2_score(targets, outputs).item()
                val_mape += mape_score(targets, outputs).item()

        val_loss /= len(val_loader)
        val_r2 /= len(val_loader)
        val_mape /= len(val_loader)

        val_losses.append(val_loss)
        val_r2s.append(val_r2)
        val_mapes.append(val_mape)

        print(f"Epoch {epoch+1}/{num_epochs}, Train Loss: {train_loss:.4f}, Val Loss: {val_loss:.4f}")

        # здесь должна быть логика ранней остановки

    return train_losses, val_losses, train_r2s, val_r2s, train_mapes, val_mapes

batch_size = 32
learning_rate = 0.01
patience = 5
num_epochs = 100
# Markdown:
<p class="task" id="3"></p>

3\. Перепишите логику обучения модели, используя `pytorch_lightning`. Для расчета метрик $R^2$ и MAPE используйте `torchmetrics`. Ранняя остановка в данном задании не требуется. После завершения обучения посчитайте значения метрик на тестовом множестве.

В процессе обучения настройки progressbar так, что:
* для каждого батча во время обучения рассчитывается значение функции потерь и метрик, по завершению эпохи показатели усредняются;
* для каждого батча во время валидации рассчитывается значение функции потерь и метрик, по завершению эпохи показатели усредняются.

- [ ] Проверено на семинаре
# Markdown:
<p class="task" id="4"></p>

4\. Повторите задачу 3, добавив логику ранней остановки, используя callback `pytorch_lightning`. Если значение функции потерь на валидационном множестве не улучшалось в течении 5 эпох, происходит ранняя остановка.

- [ ] Проверено на семинаре
# Markdown:
<p class="task" id="5"></p>

5\. Повторите задачу 4, оформив набор данных в виде `pytorch_lightning.LightningDataModule`. Всю логику по созданию датасета (преобразования признаков, разбиение и т.д.) запакуйте в метод `setup`.

- [ ] Проверено на семинаре
# Markdown:
<p class="task" id="6"></p>

6\. Повторите задачу 5, добавив логирование при помощи `wandb`.

Вставьте в текстовую ячейку скриншоты, демонстрирующие интерфейс `wandb` со всеми нужными визуализациями.

- [ ] Проверено на семинаре


# 04_1_cnn_image_classification.ipynb
# Markdown:
#  Классификация изображений с помощью сверточных нейронных сетей

__Автор задач: Блохин Н.В. (NVBlokhin@fa.ru)__

Материалы:
* Deep Learning with PyTorch (2020) Авторы: Eli Stevens, Luca Antiga, Thomas Viehmann
* https://pytorch.org/docs/stable/nn.html#convolution-layers
* https://pytorch.org/vision/0.16/transforms.html#v2-api-reference-recommended
* https://pytorch.org/tutorials/beginner/blitz/cifar10_tutorial.html
* https://pytorch.org/vision/main/generated/torchvision.datasets.ImageFolder.html
* https://kozodoi.me/blog/20210308/compute-image-stats
* https://matplotlib.org/stable/api/_as_gen/matplotlib.pyplot.matshow.html
# Markdown:
## Задачи для совместного разбора
# Markdown:
1\. Рассмотрите основные возможности по созданию датасетов из `torchvision` и примеры работы основных слоев для создания сверточных нейронных сетей для анализа изображений.
import torch as th
import torchvision
import torchvision.transforms.v2 as T
trainset = torchvision.datasets.CIFAR10(
    root='./cifar10',
    download=True,
    train=True,
)
trainset[0][0]
transform = T.Compose([
    T.ToTensor(),
    T.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]),
])

trainset = torchvision.datasets.CIFAR10(
    root='./cifar10',
    download=True,
    train=True,
    transform=transform
)
trainset[0][0].shape
# Markdown:
2\. Реализуйте типовую архитектуру CNN для классификации изображений.
import torch.nn as nn

class CNN(nn.Module):
    def __init__(self):
        super().__init__()

        self.conv_block1 = nn.Sequential(
            nn.Conv2d(
                in_channels=3,
                out_channels=6,
                kernel_size=3,
            ),
            nn.ReLU(),
            nn.MaxPool2d(2, 2),
        )

        self.conv_block2 = nn.Sequential(
            nn.Conv2d(
                in_channels=6,
                out_channels=2,
                kernel_size=3,
            ),
            nn.ReLU(),
            nn.MaxPool2d(2, 2),
        )
        self.classifier = nn.Linear(72, 10)


    def forward(self, X):
        out = self.conv_block1(X)
        out = self.conv_block2(out)

        out = out.flatten(start_dim=1)
        out = self.classifier(out)

        return out
from torch.utils.data import DataLoader

loader = DataLoader(trainset, batch_size=16)
X, y = next(iter(loader))
X.shape
model = CNN()
out = model(X)
out.shape
conv1 = nn.Conv2d(3, 6, (5, 7))
conv1.weight.shape
# Markdown:
## Задачи для самостоятельного решения
# Markdown:
<p class='task' id='1'></p>

1\. Создайте датасет `CatBreeds` на основе данных из архива `cat_breeds_4.zip`. Используя преобразования `torchvision`, приведите картинки к размеру 300х300 и нормализуйте значения интенсивности пикселей (рассчитайте статистику для нормализации отдельно). Выведите на экран количество картинок в датасете,  размер одной картинки, количество уникальных классов. Разбейте датасет на обучающее и тестовое множество в соотношении 80 на 20%.

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

- [ ] Проверено на семинаре
from torchvision.datasets import ImageFolder
from torch.utils.data import DataLoader, random_split
import numpy as np
dataset_path = './../data/cat_breeds_4'

transform_initial = T.Compose([
    T.Resize((300, 300)),
    T.ToTensor()
])

cat_breeds_dataset = ImageFolder(root=dataset_path, transform=transform_initial)
loader = DataLoader(cat_breeds_dataset, batch_size=64, shuffle=False)

mean = 0.0
std = 0.0
total_images = 0

for images, _ in loader:
    batch_samples = images.size(0)
    images = images.view(batch_samples, images.size(1), -1)
    mean += images.mean(2).sum(0)
    std += images.std(2).sum(0)
    total_images += batch_samples

mean /= total_images
std /= total_images

print(f'Среднее значение: {mean}')
print(f'Стандартное отклонение: {std}')
transform_normalized = T.Compose([
    T.Resize((300, 300)),
    T.ToTensor(),
    T.Normalize(mean=mean, std=std)
])

cat_breeds_normalized = ImageFolder(root=dataset_path, transform=transform_normalized)

print(f'Количество изображений в датасете: {len(cat_breeds_normalized)}')
print(f'Размер одной картинки: {cat_breeds_normalized[0][0].shape}')
print(f'Количество уникальных классов: {len(cat_breeds_normalized.classes)}')

train_dataset, test_dataset = random_split(cat_breeds_normalized, [0.8, 0.2])
train_dataloader = DataLoader(train_dataset, batch_size=24, shuffle=True)
test_dataloader = DataLoader(test_dataset, batch_size=24, shuffle=False)
print(f'Размер обучающего множества: {len(train_dataset)}')
print(f'Размер тестового множества: {len(test_dataset)}')
# Markdown:
<p class='task' id='2'></p>

2\. Решите задачу классификации на основе датасета из предыдущего задания, не используя сверточные слои. Постройте график изменения значения функции потерь на обучающем множестве в зависимости от номера эпохи, графики изменения метрики accuracy на обучающем и тестовом множестве в зависимости от эпохи. Выведите на экран итоговое значение метрики accuracy на обучающем и тестовом множестве. Выведите на экран количество параметров модели.

- [ ] Проверено на семинаре
from torch import optim

model = nn.Sequential(
    nn.Linear(np.prod(cat_breeds_normalized[0][0].shape), 8),
    nn.ReLU(),
    nn.Linear(8, len(cat_breeds_normalized.classes)),
)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=1e-3)
loss_train = []
loss_test = []
accuracy_train = []
accuracy_test = []
import torchmetrics as tm
from tqdm import tqdm

ecpochs = 2
run_data = []
metric = tm.Accuracy(
    task='multiclass',
    num_classes=len(cat_breeds_normalized.classes)
)
for epoch in range(ecpochs):
    model.train()
    y_pred_epoch_train = []
    y_true_epoch_train = []

    y_pred_epoch_test = []
    y_true_epoch_test = []

    loss_epoch_train = []
    loss_epoch_test = []
    
    for X_batch, y_batch in tqdm(train_dataloader, desc=f'Эпоха {epoch} – обучение'):
        y_pred = model(X_batch.view(X_batch.size(0), -1))
        
        loss = criterion(y_pred, y_batch)
        
        y_pred_epoch_train.extend(y_pred.detach().argmax(dim=1).numpy().tolist())
        y_true_epoch_train.extend(y_batch.detach().numpy().tolist())
        loss_epoch_train.append(loss.detach().numpy().item())
        
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
    
    model.eval()
    with th.no_grad():
        for X_batch, y_batch in tqdm(test_dataloader, desc=f'Эпоха {epoch} – тестирование'):
            y_pred = model(X_batch.view(X_batch.size(0), -1))
            loss = criterion(y_pred, y_batch)
            y_pred_epoch_test.extend(y_pred.detach().argmax(dim=1).numpy().tolist())
            y_true_epoch_test.extend(y_batch.detach().numpy().tolist())
            loss_epoch_test.append(loss.detach().numpy().item())
    
    accuracy_train = metric(th.tensor(y_pred_epoch_train), th.tensor(y_true_epoch_train)).item()
    metric.reset()
    accuracy_test = metric(th.tensor(y_pred_epoch_test), th.tensor(y_true_epoch_test)).item()
    metric.reset()
    loss_train = th.mean(th.tensor(loss_epoch_train)).item()
    loss_test = th.mean(th.tensor(loss_epoch_test)).item()
    run_data.append((epoch, loss_train, loss_test, accuracy_train, accuracy_test))
    
    print(f'Epoch {epoch} | Train loss: {loss_train:.4f} | Test loss: {loss_test:.4f}\n\t| Train accuracy: {accuracy_train:.4f} | Test Accuracy: {accuracy_test:.4f}')
import pandas as pd

data = pd.DataFrame(
    run_data,
    columns=[
        'epoch', 'train_loss', 'test_loss',
        'train_accuracy', 'test_accuracy'
    ]
)
data
import seaborn as sns
import matplotlib.pyplot as plt

fig, axs = plt.subplots(2, 1, figsize=(12, 12))
sns.lineplot(ax=axs[0], x=data['epoch'], y=data['train_loss'], label='Потери на обучении', marker='o')
sns.lineplot(ax=axs[0], x=data['epoch'], y=data['test_loss'], label='Потери на тестировании', marker='o')
axs[0].set_title('График потерь', fontsize=16)
axs[0].set_xlabel('Эпоха', fontsize=14)
axs[0].set_ylabel('Потери', fontsize=14)
axs[0].legend(fontsize=12)
axs[0].tick_params(axis='both', which='major', labelsize=12)

sns.lineplot(ax=axs[1], x=data['epoch'], y=data['train_accuracy'], label='Точность на обучении', marker='o')
sns.lineplot(ax=axs[1], x=data['epoch'], y=data['test_accuracy'], label='Точность на тестировании', marker='o')
axs[1].set_title('График точности', fontsize=16)
axs[1].set_xlabel('Эпоха', fontsize=14)
axs[1].set_ylabel('Точность', fontsize=14)
axs[1].legend(fontsize=12)
axs[1].tick_params(axis='both', which='major', labelsize=12)

plt.tight_layout()
plt.show()
print(f'Финальная точность на обучающем множестве: {accuracy_train:.4f}')
print(f'Финальная точность на тестовом множестве: {accuracy_test:.4f}')
num_params = sum(p.numel() for p in model.parameters())
print(f'Количество параметров в модели: {num_params}')
# Markdown:
<p class='task' id='3'></p>

3\. Напишите функцию, которая выбирает несколько изображений из переданного набора данных и выводит их на экран в виде сетки с указанием над ними названия правильного класса и класса, предсказанного моделью. Воспользовавшись данной функцией, выведите прогнозы итоговой модели из предыдущей задачи по 6 случайным картинкам.

```
def show_examples(model, dataset, k=6):
    pass
```

- [ ] Проверено на семинаре
import random

def show_examples(model, dataset, k=6, view=True):
    indices = random.sample(range(len(dataset)), k)
    images, labels = zip(*[dataset[i] for i in indices])
    images = th.stack(images)
    labels = th.tensor(labels)

    model.eval()
    with th.no_grad():
        images2 = images.view(images.size(0), -1) if view else images
        outputs = model(images2)
        preds = outputs.argmax(dim=1)

    fig, axes = plt.subplots(2, 3, figsize=(15, 10))
    axes = axes.flatten()
    for i, ax in enumerate(axes):
        ax.imshow(images[i].permute(1, 2, 0).numpy())
        ax.set_title(f'True: {dataset.classes[labels[i]]}\nPred: {dataset.classes[preds[i]]}')
        ax.axis('off')
    plt.tight_layout()
    plt.show()
show_examples(model, cat_breeds_normalized)
# Markdown:
<p class='task' id='4'></p>

4\. Решите задачу классификации на основе датасета из первого задания, используя сверточные слои. Постройте график изменения значения функции потерь на обучающем множестве в зависимости от номера эпохи, графики изменения метрики accuracy на обучающем и тестовом множестве в зависимости от эпохи. Выведите на экран итоговое значение метрики accuracy на обучающем и тестовом множестве. Выведите на экран количество параметров модели. Воспользовавшись функцией из предыдущего задания, выведите прогнозы итоговой модели по 6 случайным картинкам.

Сохраните веса обученной модели на диск.

- [ ] Проверено на семинаре
model = nn.Sequential(
    nn.Conv2d(3, 16, 3, padding=1),
    nn.ReLU(),
    nn.MaxPool2d(2, 2),

    nn.Conv2d(16, 32, 3, padding=1),
    nn.ReLU(),
    nn.MaxPool2d(2, 2),

    nn.Conv2d(32, 64, 3, padding=1),
    nn.ReLU(),
    nn.MaxPool2d(2, 2),

    nn.Flatten(),
    nn.Linear(64 * 37 * 37, 128),
    nn.ReLU(),
    nn.Linear(128, len(cat_breeds_normalized.classes)),
)
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=1e-3)
loss_train = []
loss_test = []
accuracy_train = []
accuracy_test = []
ecpochs = 4
run_data = []
metric = tm.Accuracy(
    task='multiclass',
    num_classes=len(cat_breeds_normalized.classes)
)
for epoch in range(ecpochs):
    model.train()
    y_pred_epoch_train = []
    y_true_epoch_train = []

    y_pred_epoch_test = []
    y_true_epoch_test = []

    loss_epoch_train = []
    loss_epoch_test = []
    
    for X_batch, y_batch in tqdm(train_dataloader, desc=f'Эпоха {epoch} – обучение'):
        y_pred = model(X_batch)
        
        loss = criterion(y_pred, y_batch)
        
        y_pred_epoch_train.extend(y_pred.detach().argmax(dim=1).numpy().tolist())
        y_true_epoch_train.extend(y_batch.detach().numpy().tolist())
        loss_epoch_train.append(loss.detach().numpy().item())
        
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
    
    model.eval()
    with th.no_grad():
        for X_batch, y_batch in tqdm(test_dataloader, desc=f'Эпоха {epoch} – тестирование'):
            y_pred = model(X_batch)
            loss = criterion(y_pred, y_batch)
            y_pred_epoch_test.extend(y_pred.detach().argmax(dim=1).numpy().tolist())
            y_true_epoch_test.extend(y_batch.detach().numpy().tolist())
            loss_epoch_test.append(loss.detach().numpy().item())
    
    accuracy_train = metric(th.tensor(y_pred_epoch_train), th.tensor(y_true_epoch_train)).item()
    metric.reset()
    accuracy_test = metric(th.tensor(y_pred_epoch_test), th.tensor(y_true_epoch_test)).item()
    metric.reset()
    loss_train = th.mean(th.tensor(loss_epoch_train)).item()
    loss_test = th.mean(th.tensor(loss_epoch_test)).item()
    run_data.append((epoch, loss_train, loss_test, accuracy_train, accuracy_test))
    
    print(f'Epoch {epoch} | Train loss: {loss_train:.4f} | Test loss: {loss_test:.4f}\n\t| Train accuracy: {accuracy_train:.4f} | Test Accuracy: {accuracy_test:.4f}')
import pandas as pd

data = pd.DataFrame(
    run_data,
    columns=[
        'epoch', 'train_loss', 'test_loss',
        'train_accuracy', 'test_accuracy'
    ]
)
data
import seaborn as sns
import matplotlib.pyplot as plt

fig, axs = plt.subplots(2, 1, figsize=(12, 12))
sns.lineplot(ax=axs[0], x=data['epoch'], y=data['train_loss'], label='Потери на обучении', marker='o')
sns.lineplot(ax=axs[0], x=data['epoch'], y=data['test_loss'], label='Потери на тестировании', marker='o')
axs[0].set_title('График потерь', fontsize=16)
axs[0].set_xlabel('Эпоха', fontsize=14)
axs[0].set_ylabel('Потери', fontsize=14)
axs[0].legend(fontsize=12)
axs[0].tick_params(axis='both', which='major', labelsize=12)

sns.lineplot(ax=axs[1], x=data['epoch'], y=data['train_accuracy'], label='Точность на обучении', marker='o')
sns.lineplot(ax=axs[1], x=data['epoch'], y=data['test_accuracy'], label='Точность на тестировании', marker='o')
axs[1].set_title('График точности', fontsize=16)
axs[1].set_xlabel('Эпоха', fontsize=14)
axs[1].set_ylabel('Точность', fontsize=14)
axs[1].legend(fontsize=12)
axs[1].tick_params(axis='both', which='major', labelsize=12)

plt.tight_layout()
plt.show()
print(f'Финальная точность на обучающем множестве: {accuracy_train:.4f}')
print(f'Финальная точность на тестовом множестве: {accuracy_test:.4f}')
num_params = sum(p.numel() for p in model.parameters())
print(f'Количество параметров в модели: {num_params}')
th.save(model.state_dict(), 'model_cnn.pt')
show_examples(model, cat_breeds_normalized, view=False)
# Markdown:
<p class='task' id='5'></p>

5\. Проанализируйте обученную в предыдущей задаче модель, исследовав обученные ядра сверточных слоев. Выберите одно изображение из тестового набора данных и пропустите через первый сверточный слой модели. Визуализируйте полученные карты признаков.

- [ ] Проверено на семинаре
def visualize_feature_maps(model, image):
    model.eval()

    with th.no_grad():
        feature_maps = model[0](image.unsqueeze(0))
    
    feature_maps = feature_maps.squeeze(0).numpy()
    num_feature_maps = feature_maps.shape[0]
    
    plt.figure(figsize=(15, 15))
    rows = 4
    cols = (num_feature_maps + rows - 1) // rows  # Calculate the number of columns needed
    for i in range(num_feature_maps):
        plt.subplot(rows, cols, i + 1)
        plt.imshow(feature_maps[i], cmap='viridis')
        plt.axis('off')
        plt.title(f'Feature Map {i + 1}')
    plt.show()
random_index = np.random.randint(len(test_dataset))
random_image, _ = test_dataset[random_index]
visualize_feature_maps(model, random_image)


# 04_2_cnn_pretrained.ipynb
# Markdown:
#  Использование предобученных моделей для классификации изображений

__Автор задач: Блохин Н.В. (NVBlokhin@fa.ru)__

Материалы:
* Deep Learning with PyTorch (2020) Авторы: Eli Stevens, Luca Antiga, Thomas Viehmann
* https://pytorch.org/vision/0.16/transforms.html#v2-api-reference-recommended
* https://pytorch.org/vision/main/generated/torchvision.datasets.ImageFolder.html
* https://pytorch.org/vision/stable/models.html
* https://albumentations.ai/docs/getting_started/image_augmentation/
* https://www.neurotec.uni-bremen.de/drupal/node/30
# Markdown:
## Задачи для совместного разбора
# Markdown:
1\. Загрузите предобученную модель из `torchvision`. Познакомьтесь с ее архитектурой. Заморозьте веса нескольких слоев.
import torchvision.models as models
model = models.efficientnet_b1(
    weights=models.EfficientNet_B1_Weights.IMAGENET1K_V2
)
model
list(model.parameters())[0].requires_grad_(False)
list(model.named_parameters())
ts = models.EfficientNet_B1_Weights.IMAGENET1K_V1.transforms()
ts
import torch as th

images = th.randint(0, 255, size=(16, 3, 500, 500))
images.shape
ts(images).shape
# Markdown:
## Задачи для самостоятельного решения
# Markdown:
<p class="task" id="1"></p>

1\. Используя реализацию из `torchvision`, cоздайте модель `vgg16` и загрузите предобученные веса `IMAGENET1K_V1`. Выведите на экран структуру модели, количество слоев и количество настраиваемых (`requires_grad==True`) параметров модели.

- [x] Проверено на семинаре
model = models.vgg16(
    weights=models.VGG16_Weights.IMAGENET1K_V1
)
model
num_layers = len(list(model.parameters()))
print(f'Количество слоёв: {num_layers}')
num_params_requires_grad = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f'Количество параметров, требующих обновления: {num_params_requires_grad}')
# Markdown:
<p class="task" id="2"></p>

2\. Создайте датасет `CatBreeds` на основе данных из архива `cat_breeds_4.zip`. Разбейте датасет на обучающее и тестовое множество в соотношении 80 на 20%.

К обучающему датасету примените следующее преобразование: приведите картинки к размеру 256x256, затем обрежьте по центру с размером 224х224, затем переведите изображения в тензор и нормализуйте значения интенсивности пикселей (`mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225)`).

К тестовому датасету примените преобразование `VGG16_Weights.IMAGENET1K_V1.transforms`.

- [x] Проверено на семинаре
import torchvision.transforms.v2 as T
from torch.utils.data import DataLoader, random_split
from torchvision.datasets import ImageFolder
dataset_path = './../data/cat_breeds_4'
cat_breeds_dataset = ImageFolder(root=dataset_path)
dataset_train, dataset_test = random_split(cat_breeds_dataset, [.8, .2])

transform = T.Compose([
    T.Resize((256, 256)),
    T.CenterCrop((224, 224)),
    T.ToDtype(th.float32),
    T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])

dataset_train.dataset.transform = transform
dataset_test.dataset.transform = models.VGG16_Weights.IMAGENET1K_V1.transforms()
dataset_train[0][0].shape, dataset_test[0][0].shape
dataloader_train = DataLoader(dataset_train, batch_size=8, shuffle=True)
dataloader_test = DataLoader(dataset_test, batch_size=8, shuffle=False)
# Markdown:
<p class="task" id="3"></p>

3\. Заморозьте все веса модели из предыдущего задания. Замените последний слой `Linear` классификатора на новый слой, соответствующий задаче. После изменения последнего слоя выведите на экран количество настраиваемых (`requires_grad==True`) параметров модели. Решите задачу, используя модель с замороженными весами и изменнным последним слоем.

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

- [x] Проверено на семинаре
import torch.nn as nn
import torch.optim as optim
import matplotlib.pyplot as plt
for param in model.parameters():
    param.requires_grad = False
num_classes = len(cat_breeds_dataset.classes)
model.classifier[6] = nn.Linear(model.classifier[6].in_features, num_classes)
num_params_requires_grad = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f'Количество параметров, требующих обновления: {num_params_requires_grad}')
criterion = nn.CrossEntropyLoss()
optimizer = optim.AdamW(model.classifier[6].parameters(), lr=5e-4)
import torchmetrics as tm
from tqdm import tqdm
device = th.device('cuda') if th.cuda.is_available() else th.device('cpu')
model.to(device)
epochs = 4
run_data = []
metric = tm.Accuracy(
    task='multiclass',
    num_classes=num_classes,
).to(device)
def train_model(model, dataloader_train, dataloader_test, criterion, optimizer, metric, device, epochs=5):
    run_data = []
    model.to(device)
    metric.to(device)
    for epoch in range(epochs):
        model.train()
        y_pred_epoch_train = []
        y_true_epoch_train = []
        y_pred_epoch_test = []
        y_true_epoch_test = []
        loss_epoch_train = []
        loss_epoch_test = []
        for X_batch, y_batch in tqdm(dataloader_train, desc=f'Epoch {epoch} – Training'):
            X_batch = X_batch.to(device)
            y_batch = y_batch.to(device)
            y_pred = model(X_batch)
            loss = criterion(y_pred, y_batch)
            y_pred_epoch_train.extend(y_pred.argmax(dim=1).detach().cpu().numpy().tolist())
            y_true_epoch_train.extend(y_batch.detach().cpu().numpy().tolist())
            loss_epoch_train.append(loss.detach().cpu().item())
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
        model.eval()
        with th.no_grad():
            for X_batch, y_batch in tqdm(dataloader_test, desc=f'Epoch {epoch} – Testing'):
                X_batch = X_batch.to(device)
                y_batch = y_batch.to(device)
                y_pred = model(X_batch)
                loss = criterion(y_pred, y_batch)
                y_pred_epoch_test.extend(y_pred.argmax(dim=1).detach().cpu().numpy().tolist())
                y_true_epoch_test.extend(y_batch.detach().cpu().numpy().tolist())
                loss_epoch_test.append(loss.detach().cpu().item())
        accuracy_train = metric(th.tensor(y_pred_epoch_train).to(device), th.tensor(y_true_epoch_train).to(device)).item()
        metric.reset()
        accuracy_test = metric(th.tensor(y_pred_epoch_test).to(device), th.tensor(y_true_epoch_test).to(device)).item()
        metric.reset()
        loss_train = th.mean(th.tensor(loss_epoch_train)).item()
        loss_test = th.mean(th.tensor(loss_epoch_test)).item()
        run_data.append((epoch, loss_train, loss_test, accuracy_train, accuracy_test))
        print(f'Epoch {epoch} | Train loss: {loss_train:.4f} | Test loss: {loss_test:.4f}\n'
              f'\t| Train accuracy: {accuracy_train:.4f} | Test Accuracy: {accuracy_test:.4f}')
    return run_data
run_data = train_model(model, dataloader_train, dataloader_test, criterion, optimizer, metric, device, epochs=4)
import pandas as pd

data = pd.DataFrame(
    run_data,
    columns=[
        'epoch', 'train_loss', 'test_loss',
        'train_accuracy', 'test_accuracy'
    ]
)
data
import seaborn as sns
import matplotlib.pyplot as plt

fig, axs = plt.subplots(2, 1, figsize=(12, 12))
sns.lineplot(ax=axs[0], x=data['epoch'], y=data['train_loss'], label='Потери на обучении', marker='o')
sns.lineplot(ax=axs[0], x=data['epoch'], y=data['test_loss'], label='Потери на тестировании', marker='o')
axs[0].set_title('График потерь', fontsize=16)
axs[0].set_xlabel('Эпоха', fontsize=14)
axs[0].set_ylabel('Потери', fontsize=14)
axs[0].legend(fontsize=12)
axs[0].tick_params(axis='both', which='major', labelsize=12)
axs[0].grid(True)

sns.lineplot(ax=axs[1], x=data['epoch'], y=data['train_accuracy'], label='Точность на обучении', marker='o')
sns.lineplot(ax=axs[1], x=data['epoch'], y=data['test_accuracy'], label='Точность на тестировании', marker='o')
axs[1].set_title('График точности', fontsize=16)
axs[1].set_xlabel('Эпоха', fontsize=14)
axs[1].set_ylabel('Точность', fontsize=14)
axs[1].legend(fontsize=12)
axs[1].tick_params(axis='both', which='major', labelsize=12)
axs[1].grid(True)

plt.tight_layout()
plt.show()
print(f'Финальная точность на обучающем множестве: {data["train_accuracy"].to_list()[-1]:.4f}')
print(f'Финальная точность на тестовом множестве: {data["test_accuracy"].to_list()[-1]:.4f}')
num_params = sum(p.numel() for p in model.parameters())
print(f'Количество параметров в модели: {num_params}')
# Markdown:
<p class="task" id="4"></p>

4\. Повторите решение предыдущей задачи, заморозив все сверточные слои, кроме последнего (слои классификатора не замораживайте). Сравните качество полученного решения и решения из предыдущей задачи, а также время, затраченное на обучения моделей. Перед началом работы создайте модель заново.

- [x] Проверено на семинаре
model = models.vgg16(weights=models.VGG16_Weights.IMAGENET1K_V1)
for param in model.features[:-1].parameters():
    param.requires_grad = False
num_features = model.classifier[-1].in_features
model.classifier[-1] = nn.Linear(num_features, num_classes)
criterion = nn.CrossEntropyLoss()
optimizer = optim.AdamW(model.parameters(), lr=5e-4)
run_data = train_model(model, dataloader_train, dataloader_test, criterion, optimizer, metric, device, epochs=4)
import pandas as pd

data = pd.DataFrame(
    run_data,
    columns=[
        'epoch', 'train_loss', 'test_loss',
        'train_accuracy', 'test_accuracy'
    ]
)
data
import seaborn as sns
import matplotlib.pyplot as plt

fig, axs = plt.subplots(2, 1, figsize=(12, 12))
sns.lineplot(ax=axs[0], x=data['epoch'], y=data['train_loss'], label='Потери на обучении', marker='o')
sns.lineplot(ax=axs[0], x=data['epoch'], y=data['test_loss'], label='Потери на тестировании', marker='o')
axs[0].set_title('График потерь', fontsize=16)
axs[0].set_xlabel('Эпоха', fontsize=14)
axs[0].set_ylabel('Потери', fontsize=14)
axs[0].legend(fontsize=12)
axs[0].tick_params(axis='both', which='major', labelsize=12)
axs[0].grid(True)

sns.lineplot(ax=axs[1], x=data['epoch'], y=data['train_accuracy'], label='Точность на обучении', marker='o')
sns.lineplot(ax=axs[1], x=data['epoch'], y=data['test_accuracy'], label='Точность на тестировании', marker='o')
axs[1].set_title('График точности', fontsize=16)
axs[1].set_xlabel('Эпоха', fontsize=14)
axs[1].set_ylabel('Точность', fontsize=14)
axs[1].legend(fontsize=12)
axs[1].tick_params(axis='both', which='major', labelsize=12)
axs[1].grid(True)

plt.tight_layout()
plt.show()
print(f'Финальная точность на обучающем множестве: {data["train_accuracy"].to_list()[-1]:.4f}')
print(f'Финальная точность на тестовом множестве: {data["test_accuracy"].to_list()[-1]:.4f}')
num_params = sum(p.numel() for p in model.parameters())
print(f'Количество параметров в модели: {num_params}')
# Markdown:
<p class="task" id="5"></p>

5\. Повторите решение задачи 3, расширив обучающий набор данных при помощи преобразований из `torchvision`, изменяющих изображение (повороты, изменение интенсивности пикселей, обрезание и т.д.). При оценке модели на тестовой выборке данные преобразования применяться не должны. Решение о том, сколько и каких слоев модели будет обучаться, примите самостоятельно. Перед началом работы создайте модель заново.

- [ ] Проверено на семинаре
train_transforms = T.Compose([
    T.Resize((256, 256)),
    T.RandomRotation(degrees=90),
    T.CenterCrop((224, 224)),
    T.RandomResizedCrop((224, 224), scale=(0.8, 1.0)),
    T.ColorJitter(brightness=0.4, contrast=0.3, saturation=0.2, hue=0.1),
    T.ToTensor(),
    T.Normalize(mean=(0.485, 0.456, 0.406), std=(0.229, 0.224, 0.225))
])

test_transforms = models.VGG16_Weights.IMAGENET1K_V1.transforms()
from torch.utils.data import ConcatDataset
import copy
dataset = ImageFolder(root='./../data/cat_breeds_4')
train_dataset, test_dataset = random_split(dataset, [0.8, 0.2])

train_datasets = [copy.deepcopy(train_dataset) for _ in range(2)]
for i in train_datasets:
    i.dataset.transform = train_transforms
test_dataset.dataset.transform = test_transforms

extended_train_dataset = ConcatDataset(train_datasets)
dataloader_train = DataLoader(extended_train_dataset, batch_size=8, shuffle=True)
dataloader_test = DataLoader(test_dataset, batch_size=8, shuffle=False)
model = models.vgg16(weights=models.VGG16_Weights.IMAGENET1K_V1)
for param in model.features[:-1].parameters():
    param.requires_grad = False
num_features = model.classifier[-1].in_features
model.classifier[-1] = nn.Linear(num_features, num_classes)
criterion = nn.CrossEntropyLoss()
optimizer = optim.AdamW(model.classifier[-1].parameters(), lr=5e-4)
run_data = train_model(model, dataloader_train, dataloader_test, criterion, optimizer, metric, device, epochs=4)
data = pd.DataFrame(
    run_data,
    columns=[
        'epoch', 'train_loss', 'test_loss',
        'train_accuracy', 'test_accuracy'
    ]
)
data
import seaborn as sns
import matplotlib.pyplot as plt

fig, axs = plt.subplots(2, 1, figsize=(12, 12))
sns.lineplot(ax=axs[0], x=data['epoch'], y=data['train_loss'], label='Потери на обучении', marker='o')
sns.lineplot(ax=axs[0], x=data['epoch'], y=data['test_loss'], label='Потери на тестировании', marker='o')
axs[0].set_title('График потерь', fontsize=16)
axs[0].set_xlabel('Эпоха', fontsize=14)
axs[0].set_ylabel('Потери', fontsize=14)
axs[0].legend(fontsize=12)
axs[0].tick_params(axis='both', which='major', labelsize=12)
axs[0].grid(True)

sns.lineplot(ax=axs[1], x=data['epoch'], y=data['train_accuracy'], label='Точность на обучении', marker='o')
sns.lineplot(ax=axs[1], x=data['epoch'], y=data['test_accuracy'], label='Точность на тестировании', marker='o')
axs[1].set_title('График точности', fontsize=16)
axs[1].set_xlabel('Эпоха', fontsize=14)
axs[1].set_ylabel('Точность', fontsize=14)
axs[1].legend(fontsize=12)
axs[1].tick_params(axis='both', which='major', labelsize=12)
axs[1].grid(True)

plt.tight_layout()
plt.show()
print(f'Финальная точность на обучающем множестве: {data["train_accuracy"].to_list()[-1]:.4f}')
print(f'Финальная точность на тестовом множестве: {data["test_accuracy"].to_list()[-1]:.4f}')
num_params = sum(p.numel() for p in model.parameters())
print(f'Количество параметров в модели: {num_params}')


# 04_3_deploy_publishing.ipynb
# Markdown:
#  Разворачивание и публикация моделей. Streamlit.

__Автор задач: Блохин Н.В. (NVBlokhin@fa.ru)__

Материалы:
* https://docs.streamlit.io/get-started
* https://colab.research.google.com/github/mrm8488/shared_colab_notebooks/blob/master/Create_streamlit_app.ipynb#scrollTo=meJ36PefNftd
* https://www.youtube.com/playlist?list=PLtqF5YXg7GLmCvTswG32NqQypOuYkPRUE
* https://docs.streamlit.io/develop/api-reference/widgets/st.slider
* https://docs.streamlit.io/develop/api-reference/media/st.image
* https://docs.streamlit.io/develop/api-reference/widgets/st.file_uploader
* https://docs.streamlit.io/develop/api-reference/caching-and-state/st.session_state
# Markdown:
## Задачи для совместного разбора
# Markdown:
Запуск приложений для заданий ниже
> streamlit run ./app.py
# Markdown:
1\. Обсудите базовые возможности по созданию веб-приложения при помощи `streamlit` на примере построения графика функции $y=x^p, x\in[-x_{min}, x_{max}]$.
%%writefile app.py

import streamlit as st
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image

def plot(x_min, x_max, power):
    x = np.linspace(x_min, x_max, 200)
    y = x ** power
    plt.figure(figsize=(10, 5))
    plt.plot(x, y)
    return plt

def main():
    st.title('Пример визуализации')

    x_min = st.sidebar.slider('Минимум', min_value=-5, max_value=5)
    x_max = st.sidebar.slider('Максимум', min_value=-5, max_value=5)
    power = st.sidebar.slider('Степень', min_value=-5, max_value=5)

    fig = plot(x_min, x_max, power)
    st.pyplot(fig)

# def main():
#     img_file = st.file_uploader(
#         label='Изображение'
#     )

#     if img_file is not None:
#         img = Image.open(img_file)
#         st.image(img)

if __name__ == '__main__':
    main()
# Markdown:
2\. Обсудите способ загрузки изображений и хранения переменных в сессии пользователя.
# Markdown:
## Задачи для самостоятельного решения
# Markdown:
<p class='task' id='1'></p>

1\. Напишите функцию `load_model`, которая восстанавливает модель предсказания категорий животных на основе пути к файлу с весами этой модели и любой другой дополнительной информации, которая требуется для восстановления модели. Загрузите модель и выведите ее архитектуру на экран.

- [ ] Проверено на семинаре
from pathlib import Path
from torch import nn, optim
import torch as th

def load_model(weights_path: Path, *args, **kwargs) -> nn.Module:
    model = th.load(weights_path, weights_only=False, *args, **kwargs)
    return model
model = load_model('./classification_model.pth', map_location=th.device('cpu'))
model
# Markdown:
<p class='task' id='2'></p>

2\. Напишите функцию `preprocess_image`, которая принимает на вход изображение в виде `PIL.Image.Image` и предобрабатывает его таким образом, чтобы результат можно было пропустить через модель. Преобразования, применяемые к изображению, должны соответствовать тому, как данная модель была обучена.

Протестируйте работу функции, скачав картинку с котиком при помощи готовой функции `get_cat_image`.

- [ ] Проверено на семинаре
import requests
from PIL import Image
from io import BytesIO
import torchvision.models as models
import torchvision.transforms as transforms

def get_cat_image(url: str) -> Image.Image:
    response = requests.get(url)
    if response.status_code == 200:
        image = Image.open(BytesIO(response.content))
        return image
    else:
        return None

def preprocess_image(image: Image.Image) -> Image.Image:
    transform = models.VGG16_Weights.IMAGENET1K_V1.transforms()
    image_transformed = transform(image)
    return image_transformed

url = 'https://www.catster.com/wp-content/uploads/2023/11/Fawn-Sphynx_sophiecat_Shutterstock-800x592.jpg'
img = get_cat_image(url)
img_tr = preprocess_image(img)
from matplotlib import pyplot as plt
%matplotlib inline

to_pil = transforms.ToPILImage()
fig, ax = plt.subplots(nrows=1, ncols=2)
ax[0].imshow(img)
ax[1].imshow(to_pil(img_tr))
plt.show()
# Markdown:
<p class='task' id='3'></p>

3\. Напишите функцию `predict`, при помощи которой можно получить прогноз для изображения. Продемонстируйте работу функции.

- [ ] Проверено на семинаре
def predict(model, image):
    model.eval()
    with th.no_grad():
        image = image.unsqueeze(0)
        outputs = model(image)
        _, predicted = th.max(outputs, 1)
    return predicted.item()
predict(model, img_tr)
# Markdown:
<p class='task' id='4'></p>

4\. Реализуйте веб-приложение, которое позволяет загрузить изображение и получить прогноз для него при помощи обученной модели. На странице должны располагаться следующие визуальные элементы:
- кнопка для загрузки изображения;
- само изображение (после загрузки);
- кнопка для получения прогнозов;
- таблица с вероятностями каждого класса (после нажатия на кнопку): должны быть видны названия классов.

Продемонстрируйте работу, вставив в ячейку скриншоты, подтверждающие корректность решения.

В этом и следующем задании использование `streamlit` является опциональным. Если вы владеете любым другим инструментом для создания веб-приложения, вы можете использовать его.

- [ ] Проверено на семинаре
%%writefile app.py

import streamlit as st
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from PIL import Image
import torch as th
from torchvision import models
from pathlib import Path

def load_model(weights_path: Path, *args, **kwargs) -> th.nn.Module:
    model = th.load(weights_path, weights_only=False, *args, **kwargs)
    model.eval()
    return model

def preprocess_image(image: Image.Image) -> th.Tensor:
    transform = models.VGG16_Weights.IMAGENET1K_V1.transforms()
    image_transformed = transform(image)
    return image_transformed

def predict(model, image) -> (th.Tensor, th.Tensor):
    with th.no_grad():
        image = image.unsqueeze(0)
        outputs = model(image)
        probabilities = th.nn.functional.softmax(outputs, dim=1).squeeze()
    return probabilities, th.argmax(probabilities)

def main():
    st.title('Классификация Изображений')
    
    img_file = st.file_uploader(label='Загрузите изображение', type=['jpg', 'jpeg', 'png'])

    device = th.device('cuda' if th.cuda.is_available() else 'cpu')
    model = load_model(Path('./classification_model.pth'), map_location=device)
    class_names = ['American Shorthair', 'Persian', 'Russian Blue', 'Tiger']

    if img_file is not None:
        img = Image.open(img_file).convert('RGB')
        st.image(img, caption='Загруженное изображение', use_column_width=True)

        pred_button = st.button('Получить прогноз')

        if pred_button:
            img_pr = preprocess_image(img).to(device)
            probabilities, img_class = predict(model, img_pr)

            probs_np = probabilities.cpu().numpy()
            df = pd.DataFrame({
                'Класс': class_names,
                'Вероятность': probs_np
            })
            df_sorted = df.sort_values(by='Вероятность', ascending=False).reset_index(drop=True)
            st.dataframe(df_sorted)
            
            st.success(f'Предсказанный класс: {class_names[img_class]} с вероятностью {probs_np[img_class]:.4f}')

if __name__ == '__main__':
    main()
# Markdown:
![Снимок экрана 2024-11-06 в 23.56.43.png](<attachment:Снимок экрана 2024-11-06 в 23.56.43.png>)
# Markdown:
<p class='task' id='5'></p>

5\. Расширьте возможности приложения, добавив возможность отобразить информацию о топ-k наиболее вероятных классов в виде столбчатой диаграммы. Значение k должно выбираться при помощи визуального элемента 'слайдер'.

Продемонстрируйте работу, вставив в ячейку скриншоты, подтверждающие корректность решения.

- [ ] Проверено на семинаре
%%writefile app.py

import streamlit as st
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from PIL import Image
import torch as th
from torchvision import models
from pathlib import Path

def load_model(weights_path: Path, *args, **kwargs) -> th.nn.Module:
    model = th.load(weights_path, weights_only=False, *args, **kwargs)
    model.eval()
    return model

def preprocess_image(image: Image.Image) -> th.Tensor:
    transform = models.VGG16_Weights.IMAGENET1K_V1.transforms()
    image_transformed = transform(image)
    return image_transformed

def predict(model, image) -> (th.Tensor, th.Tensor):
    with th.no_grad():
        image = image.unsqueeze(0)
        outputs = model(image)
        probabilities = th.nn.functional.softmax(outputs, dim=1).squeeze()
    return probabilities, th.argmax(probabilities)

def main():
    st.title('Классификация Изображений')
    
    img_file = st.file_uploader(label='Загрузите изображение', type=['jpg', 'jpeg', 'png'])

    device = th.device('cuda' if th.cuda.is_available() else 'cpu')
    model = load_model(Path('./classification_model.pth'), map_location=device)
    class_names = ['American Shorthair', 'Persian', 'Russian Blue', 'Tiger']

    if img_file is not None:
        img = Image.open(img_file).convert('RGB')
        st.image(img, caption='Загруженное изображение', use_column_width=True)

        if 'probabilities' not in st.session_state:
            st.session_state.probabilities = None
            st.session_state.img_class = None

        pred_button = st.button('Получить прогноз')

        if pred_button:
            img_pr = preprocess_image(img).to(device)
            probabilities, img_class = predict(model, img_pr)
            st.session_state.probabilities = probabilities.cpu().numpy()
            st.session_state.img_class = img_class.item()

        if st.session_state.probabilities is not None:
            top_k = st.sidebar.slider('Выберите количество топ-классов (k)', min_value=1, max_value=len(class_names), value=len(class_names))
            
            probs_np = st.session_state.probabilities
            img_class = st.session_state.img_class

            df = pd.DataFrame({
                'Класс': class_names,
                'Вероятность': probs_np
            })
            df_sorted = df.sort_values(by='Вероятность', ascending=False).reset_index(drop=True)
            st.dataframe(df_sorted.head(top_k))
            
            st.success(f'Предсказанный класс: {class_names[img_class]} с вероятностью {probs_np[img_class]:.4f}')

            fig, ax = plt.subplots()
            sns.barplot(x=df_sorted['Класс'].head(top_k), y=df_sorted['Вероятность'].head(top_k), ax=ax)
            ax.set_xlabel('Вероятность')
            ax.set_title(f'Топ-{top_k} классов')
            st.pyplot(fig)

if __name__ == '__main__':
    main()
# Markdown:
![Снимок экрана 2024-11-07 в 00.24.54.png](<attachment:Снимок экрана 2024-11-07 в 00.24.54.png>)


# 05_1_hf_transformers.ipynb
# Markdown:
#  🤗 Transformers

__Автор задач: Блохин Н.В. (NVBlokhin@fa.ru)__

Материалы:
* https://huggingface.co/docs/transformers/index
* https://huggingface.co/docs/transformers/main_classes/pipelines#transformers.pipeline.task
* https://huggingface.co/docs/transformers/preprocessing
* https://habr.com/ru/articles/704592/
* https://lightning.ai/docs/torchmetrics/stable/text/bleu_score.html
# Markdown:
## Задачи для совместного разбора
# Markdown:
1\. Обсудите основные возможности и экосистему пакета 🤗 Transformers на примере задачи поиска ответа на вопрос в тексте.
import warnings

warnings.filterwarnings('ignore')
text = '''The seminars on Deep Learning and Natural Language Processing were truly captivating,
providing a deep dive into the intricacies of these disciplines.
The wealth of knowledge and insights gained during the sessions was commendable.
However, it's disheartening to note the scarcity of homework assignments.
Anastasia, in particular, is quite concerned that the limited number of assignments might
fall short of even reaching 30. While the seminars were intellectually stimulating,
the desire for more hands-on practice through assignments remains strong,
as it is crucial for reinforcing the theoretical understanding acquired during the classes.'''
question1 = 'What would be the ideal number of homework assignments for Anastasia'
question2 = 'What are the shortcomings of the course?'
from transformers import pipeline
answerer = pipeline(
    'question-answering',
    model='distilbert/distilbert-base-uncased-distilled-squad'
)
answerer(question=question1, context=text)
from transformers import AutoModelForQuestionAnswering, AutoTokenizer
import torch
model_name = 'distilbert/distilbert-base-uncased-distilled-squad'
model = AutoModelForQuestionAnswering.from_pretrained(model_name)
tokenizer = AutoTokenizer.from_pretrained(model_name)
inputs = tokenizer(question1, text, return_tensors='pt')
inputs
with torch.no_grad():
    outputs = model(**inputs)
ans_start = outputs.start_logits.argmax()
ans_end = outputs.end_logits.argmax()
ans_end
token_ids = inputs['input_ids'][0][ans_start:ans_end+1]
tokens = tokenizer.convert_ids_to_tokens(token_ids)
tokenizer.convert_tokens_to_string(tokens)
# Markdown:
## Задачи для самостоятельного решения
# Markdown:
<p class='task' id='1'></p>

1\. Среди предобученных моделей найдите модель для перевода текста с русского языка на английский. Протестируйте данную модель на нескольких предложениях, используя `transformers.pipeline`. Выведите результаты работы в следующем виде:

```
sentence1_ru -> sentence1_en
sentence2_ru -> sentence2_en
```

Получите перевод для всех текстов из файла `RuBQ_2.0_test.json` и посчитайте BLEU-score.

- [ ] Проверено на семинаре
import json

with open('./../data/RuBQ_2.0_test.json', 'r') as f:
    data = json.load(f)
ix = 0
data[ix]['question_text'], data[ix]['question_eng']
from transformers import pipeline

pipe = pipeline('translation', model='Helsinki-NLP/opus-mt-ru-en', device='cpu')
import numpy as np

for i in np.random.randint(0, len(data), 3):
    answer = pipe(data[i]['question_text'])[0]['translation_text']
    print(f'{data[i]['question_text']} -> {answer}')
from tqdm import tqdm

translated_texts = [pipe(i['question_text'])[0]['translation_text'] for i in tqdm(data)]
english_texts = [i['question_eng'] for i in tqdm(data)]
translated_texts[:2]
from torchmetrics.text import BLEUScore

metric = BLEUScore()
score = metric(english_texts, [translated_texts])
print(f'BLEU Score: {score:.6f}')
# Markdown:
<p class='task' id='2'></p>

2\. Среди предобученных моделей найдите модель для классификации фотографий людей по полу.

Протестируйте данную модель на нескольких фотографиях, используя `AutoFeatureExtractor` и `AutoModelForImageClassification`.

Выведите на экран сетку 2х2 из изображений, где над каждым изображением добавлена подпись, содержащая прогноз модели и правильный ответ.

- [ ] Проверено на семинаре
from transformers import AutoFeatureExtractor, AutoModelForImageClassification

processor = AutoFeatureExtractor.from_pretrained('rizvandwiki/gender-classification')
model = AutoModelForImageClassification.from_pretrained('rizvandwiki/gender-classification')
id2label = {0: 'female', 1: 'male'}
label2id = {v: k for k, v in id2label.items()}
import requests as req
from PIL import Image

gigachad_link = 'https://upload.wikimedia.org/wikipedia/ru/9/94/Гигачад.jpg'
image = Image.open(req.get(gigachad_link, stream=True).raw).convert('RGB')
image
import torch as th

with th.no_grad():
    inputs = processor(image, return_tensors='pt')
    outputs = model(**inputs)
image_class_pred = outputs.logits.argmax().item()
id2label[image_class_pred]
from matplotlib import pyplot as plt

links = {
    'https://cdn-lfs.hf.co/repos/d1/97/d1977aabf1da449ea968dea0a790b061ab27d28947718c0ac00b56295feebf7f/7c5095faa4f6589241ecafb42e7ec0178776e510b6afe533e23d86e8dcf6b733?response-content-disposition=inline%3B+filename*%3DUTF-8%27%27female.jpg%3B+filename%3D%22female.jpg%22%3B&response-content-type=image%2Fjpeg&Expires=1731873049&Policy=eyJTdGF0ZW1lbnQiOlt7IkNvbmRpdGlvbiI6eyJEYXRlTGVzc1RoYW4iOnsiQVdTOkVwb2NoVGltZSI6MTczMTg3MzA0OX19LCJSZXNvdXJjZSI6Imh0dHBzOi8vY2RuLWxmcy5oZi5jby9yZXBvcy9kMS85Ny9kMTk3N2FhYmYxZGE0NDllYTk2OGRlYTBhNzkwYjA2MWFiMjdkMjg5NDc3MThjMGFjMDBiNTYyOTVmZWViZjdmLzdjNTA5NWZhYTRmNjU4OTI0MWVjYWZiNDJlN2VjMDE3ODc3NmU1MTBiNmFmZTUzM2UyM2Q4NmU4ZGNmNmI3MzM~cmVzcG9uc2UtY29udGVudC1kaXNwb3NpdGlvbj0qJnJlc3BvbnNlLWNvbnRlbnQtdHlwZT0qIn1dfQ__&Signature=KIohq33Fe595xoUtyDemzDBeiLvZdEgQ035Pa8CqXBZ7DrVuKpoC-c83xFdMw0suf8FVKGZpBEPDRPj7GosqLVB2zbmzYHbBBqSl8izfNTbXNngtI9O-COsnROh7Qy3BTls9LAn-Z4921idtF-Z122xn9QyxkAEYtfOxTxvJ5SWhBUxsRzK3ORa7NbETcA-51hUFT72YnlVdJ92LYrswnZkwiYVCZjhewu1MSqDaoptjebz2Dc~cDNEJZURC7wmw2BYw0mL-Ws2qMPuRh2VIMJc1Lv5el6x9dJThARh4oGC0cwhczgymeJqNAZLYVbe0ZIVzVg6JrtXluOiabYm5mg__&Key-Pair-Id=K3RPWS32NSSJCE': 0,
    'https://cdn-lfs.hf.co/repos/d1/97/d1977aabf1da449ea968dea0a790b061ab27d28947718c0ac00b56295feebf7f/802366c93c74e84e7eff1d6b676c2cd5c6a9e2c34907556959ebff7356996e52?response-content-disposition=inline%3B+filename*%3DUTF-8%27%27male.jpg%3B+filename%3D%22male.jpg%22%3B&response-content-type=image%2Fjpeg&Expires=1731873061&Policy=eyJTdGF0ZW1lbnQiOlt7IkNvbmRpdGlvbiI6eyJEYXRlTGVzc1RoYW4iOnsiQVdTOkVwb2NoVGltZSI6MTczMTg3MzA2MX19LCJSZXNvdXJjZSI6Imh0dHBzOi8vY2RuLWxmcy5oZi5jby9yZXBvcy9kMS85Ny9kMTk3N2FhYmYxZGE0NDllYTk2OGRlYTBhNzkwYjA2MWFiMjdkMjg5NDc3MThjMGFjMDBiNTYyOTVmZWViZjdmLzgwMjM2NmM5M2M3NGU4NGU3ZWZmMWQ2YjY3NmMyY2Q1YzZhOWUyYzM0OTA3NTU2OTU5ZWJmZjczNTY5OTZlNTI~cmVzcG9uc2UtY29udGVudC1kaXNwb3NpdGlvbj0qJnJlc3BvbnNlLWNvbnRlbnQtdHlwZT0qIn1dfQ__&Signature=haqsdEGKSjhVF8PrXvwiS8gIAvPtN5VI6z5OiqPAlTjgneZ0ZPCPisk6bwZqmXGCGcz5-alVpD0I5~xUQGi5D972QkYrLb0tAw-FVSmGMxBkJnPZ75kDARpzBlXo50D32LbHPqkK1qT4bReoearimHgwlP9xWI6g5TP5-AcI5pykfP3pmSJmnZuRAmdRKtlpBFkNlitqT9HiqT7VHv8DBrC45nYubUASJl~i-Bu3XoZ5a0crj2mu5zXHfQFZaVsuXtTKVj3Qn-PBkW86HpZWb5TB5HL4poofAFMEjjaNaj0zVaMPRybSzwyRNzwzDhx5zdg5deDnErNbRQFpD7n9lw__&Key-Pair-Id=K3RPWS32NSSJCE': 1,
    'https://cdn-lfs.hf.co/repos/d9/12/d9124b3249f10dbcd309d8f27dc5950d49d474f4c77efbcf206a0c723934ad06/9532c1eac299ee3a56b8af4a62d640523068de7f4b087cc12fed7b9ae484a840?response-content-disposition=inline%3B+filename*%3DUTF-8%27%27female.jpg%3B+filename%3D%22female.jpg%22%3B&response-content-type=image%2Fjpeg&Expires=1731873070&Policy=eyJTdGF0ZW1lbnQiOlt7IkNvbmRpdGlvbiI6eyJEYXRlTGVzc1RoYW4iOnsiQVdTOkVwb2NoVGltZSI6MTczMTg3MzA3MH19LCJSZXNvdXJjZSI6Imh0dHBzOi8vY2RuLWxmcy5oZi5jby9yZXBvcy9kOS8xMi9kOTEyNGIzMjQ5ZjEwZGJjZDMwOWQ4ZjI3ZGM1OTUwZDQ5ZDQ3NGY0Yzc3ZWZiY2YyMDZhMGM3MjM5MzRhZDA2Lzk1MzJjMWVhYzI5OWVlM2E1NmI4YWY0YTYyZDY0MDUyMzA2OGRlN2Y0YjA4N2NjMTJmZWQ3YjlhZTQ4NGE4NDA~cmVzcG9uc2UtY29udGVudC1kaXNwb3NpdGlvbj0qJnJlc3BvbnNlLWNvbnRlbnQtdHlwZT0qIn1dfQ__&Signature=G2p3XpEGmkmSzhk2DhdIPFr5uSyj9osow73ZkRi2xXKfLCMRqOVQ06RA6Gx3ZcJKPIS6fkq8Jt017fG14oDROd8UpiNM4NmfyeSMgly7eICqAiPBdVF5eHV4C-p6BuVI2GG4yOjJC~gm0t0ODBWVmbUkbqchyC~SRalpnaAPwfCnhMdW7eCWHKU1idbtBk-YoxKKvkAO3bakneS1CMmFQkQ4d33op4ocBYQnwCZZYm591tSrdolIrKJX6RmN914vRfRn5N3ZMnc1-HB~VUVYLB3VzL3UR~yBcKfT1Ddv9ziwb40CsCxTi0i1swHmPZk-Rv6Gcq5sQgJ~L5LJ1Y01Tw__&Key-Pair-Id=K3RPWS32NSSJCE': 0,
    'https://cdn-lfs.hf.co/repos/d9/12/d9124b3249f10dbcd309d8f27dc5950d49d474f4c77efbcf206a0c723934ad06/fd9bfbd3dec8e55196cabb5ce8303b2ebd08a5fcaf5d4198fef9d23ea9254cc2?response-content-disposition=inline%3B+filename*%3DUTF-8%27%27male.jpg%3B+filename%3D%22male.jpg%22%3B&response-content-type=image%2Fjpeg&Expires=1731873080&Policy=eyJTdGF0ZW1lbnQiOlt7IkNvbmRpdGlvbiI6eyJEYXRlTGVzc1RoYW4iOnsiQVdTOkVwb2NoVGltZSI6MTczMTg3MzA4MH19LCJSZXNvdXJjZSI6Imh0dHBzOi8vY2RuLWxmcy5oZi5jby9yZXBvcy9kOS8xMi9kOTEyNGIzMjQ5ZjEwZGJjZDMwOWQ4ZjI3ZGM1OTUwZDQ5ZDQ3NGY0Yzc3ZWZiY2YyMDZhMGM3MjM5MzRhZDA2L2ZkOWJmYmQzZGVjOGU1NTE5NmNhYmI1Y2U4MzAzYjJlYmQwOGE1ZmNhZjVkNDE5OGZlZjlkMjNlYTkyNTRjYzI~cmVzcG9uc2UtY29udGVudC1kaXNwb3NpdGlvbj0qJnJlc3BvbnNlLWNvbnRlbnQtdHlwZT0qIn1dfQ__&Signature=qPlDp4p3152FmEwLXM0X6FZcG8OShfBrKdx80hDDrx0WuMvnLJ0z6IdQGT4P57l0h0ZXxdKnwfSyBk04DYdcCqKnLY0tMcAy1tNEyjqfWugKRJ1RBdf67b4gHW~pLL67AD5o~Zrbx26TLWUvdWPtcWch1O0nJ21P2RpHQ~5gIba3eRXqmMW-Gzm31hpvvwLGe4BTCMHFehz9kUK8vj7PQk4bUu3qVOBRnODOs0Q1EmYm41Kc91ZiBviHj8IjX9GMkWImNnOKSbOYxN72Gm0v3ukfJUHaRCiBlT5vztUoUBoEedZYDNk7p0qnwFUjwDxdV6wdn4CulqDyppHp5LlnQQ__&Key-Pair-Id=K3RPWS32NSSJCE': 1
}

fig, ax = plt.subplots(2, 2, figsize=(10, 10))
with th.no_grad():
    for i, (image_link, image_class) in enumerate(links.items()):
        image = Image.open(req.get(image_link, stream=True).raw)
        inputs = processor(image, return_tensors='pt')
        outputs = model(**inputs)
        image_class_pred = outputs.logits.argmax().item()

        ax[i//2, i%2].imshow(image)
        ax[i//2, i%2].set_title(f'Pred: {id2label[image_class_pred]}\nTrue: {id2label[image_class]}')
        ax[i//2, i%2].set_xticks([])
        ax[i//2, i%2].set_yticks([])
plt.tight_layout()
plt.show()
# Markdown:
<p class='task' id='3'></p>

3\. Среди предобученных моделей найдите модель для генерации аудио по тексту. Используя [данный сервис](https://geek-jokes.sameerkumar.website/api?format=json), получите текст случайной шутки. Сгенерируйте аудио с озвучкой данной шутки.

Для прослушивания полученного аудио воспользуйтесь встроенным виджетом `IPython.display.Audio`

- [ ] Проверено на семинаре
pipe = pipeline('text-to-speech', model='suno/bark-small')
result = req.get('https://geek-jokes.sameerkumar.website/api?format=json', json=True)
joke = result.json().get('joke', 'No joke :(')
joke
outputs = pipe(joke)
outputs
from IPython.display import Audio

audio = Audio(outputs.get('audio'), rate=outputs.get('sampling_rate'))
audio
# Markdown:
<p class='task' id='4'></p>

4\. Разработайте решение для поиска ответа на голосовой вопрос по тексту, используя готовые модели `transformers`. Решение должно включать себя следующие модели:
- модель распознавания текста из аудио;
- модель поиска ответа на вопрос в тексте;
- модель генерации аудио по тексту.

В качестве входных данных запишите небольшой аудиофрагмент в формате на русском языке. Для записи вы можете воспользоваться любым устройством: мобильным телефоном, веб-приложением (например, [этим](https://vocalremover.org/ru/voice-recorder)) и т.д.

В качестве контекста для поиска ответа используйте предложенный текст.

Продемонстируйте все промежуточные результаты, полученные в процессе работы конвейера моделей.

- [ ] Проверено на семинаре
text = '''
Машинное обучение — это раздел искусственного интеллекта, который позволяет компьютерным системам улучшать свою работу на основе опыта без явного программирования.
Основная идея заключается в том, что алгоритмы могут учиться на данных, выявляя закономерности и принимая решения с минимальным вмешательством человека.
Существует несколько типов машинного обучения: обучение с учителем, где алгоритм учится на размеченных данных; обучение без учителя, работающее с неразмеченными данными;
и обучение с подкреплением, где алгоритм учится через взаимодействие с окружающей средой.
'''
Audio('question.mp3')
asr_model = pipeline('automatic-speech-recognition', model='openai/whisper-tiny')
outputs = asr_model('question.mp3')
question_text = outputs.get('text', '').strip()
question_text
qa_model = pipeline('question-answering', model='AlexKay/xlm-roberta-large-qa-multilingual-finedtuned-ru')
outputs = qa_model(question=question_text, context=text)
question_answer = outputs.get('answer', '').strip()
question_answer
tts_model = pipeline('text-to-speech', model='suno/bark-small')
outputs = tts_model(question_answer)
audio = Audio(outputs.get('audio'), rate=outputs.get('sampling_rate'))
audio


# 05_2_hf_transformers_finetuning.ipynb
# Markdown:
#  🤗 Transformers Finetuning

__Автор задач: Блохин Н.В. (NVBlokhin@fa.ru)__

Материалы: 
* https://huggingface.co/docs/transformers/training
* https://huggingface.co/docs/datasets/main/en/repository_structure
* https://huggingface.co/docs/datasets/main/en/package_reference/loading_methods#datasets.load_dataset
* https://huggingface.co/docs/transformers/v4.35.2/en/training#prepare-a-dataset
* https://huggingface.co/docs/datasets/v2.0.0/en/image_process
* https://huggingface.co/docs/datasets/v3.0.1/en/package_reference/main_classes#datasets.Dataset.train_test_split
* https://huggingface.co/docs/datasets/process
* https://huggingface.co/docs/evaluate/index
* https://huggingface.co/docs/transformers/main_classes/trainer
* https://huggingface.co/docs/transformers/v4.35.2/en/main_classes/trainer#transformers.TrainingArguments
* https://albumentations.ai/docs/getting_started/image_augmentation/
* https://wandb.ai/ayush-thakur/huggingface/reports/Examples-of-Early-Stopping-in-HuggingFace-Transformers--Vmlldzo0MzE2MTM
* https://colab.research.google.com/github/wandb/examples/blob/master/colabs/huggingface/Optimize_Hugging_Face_models_with_Weights_&_Biases.ipynb#scrollTo=8k0XaprCT51P
# Markdown:
## Задачи для совместного разбора
# Markdown:
1\. Обсудите основные шаги по дообучению моделей из экосистемы 🤗 Transformers.
# Markdown:
## Задачи для самостоятельного решения
# Markdown:
<p class='task' id='1'></p>

1\. Создайте набор данных для обучения модели классификации пород кошек, используя пакет 🤗 Datasets. Разделите датасет на обучающее и тестовое множество в соотношении 80/20. 

К обучающему множеству примените следующие преобразования из пакета `albumentations`:
- Resize до 256х256;
- CenterCrop до размера 224х224;
- минимум одно преобразование, случайным образом изменяющее изображение.

К тестовому множеству примените аналогичное преобразование, но без добавления случайных изменений.

Создайте два `DataLoader` на основе обучающего и валидационного множества. Получите батч из обучающего множества и форму признаков и меток на экран. Признаки в батче должны быть уложены в четырехмерный тензор.

- [ ] Проверено на семинаре
from torchvision.datasets import ImageFolder
from torch.utils.data import DataLoader, random_split
import albumentations as A
import numpy as np
dataset_path = './../data/cat_breeds_4'
dataset = ImageFolder(dataset_path)
dataset_train, dataset_test = random_split(dataset, [0.8, 0.2])
al_train = A.Compose([
    A.Resize(256, 256),
    A.CenterCrop(224, 224),
    A.RandomRotate90(),
])

al_test = A.Compose([
    A.Resize(256, 256),
    A.CenterCrop(224, 224),
])
class AlbumentationsTransform:
    def __init__(self, augmentations):
        self.augment = augmentations

    def __call__(self, img):
        img = np.array(img)
        augmented = self.augment(image=img)
        img = augmented['image']
        return img
dataset_train.dataset.transform = AlbumentationsTransform(al_train)
dataset_test.dataset.transform = AlbumentationsTransform(al_test)
train_loader = DataLoader(dataset_train, batch_size=16, shuffle=True)
test_loader = DataLoader(dataset_test, batch_size=16, shuffle=False)
train_features, train_labels = next(iter(train_loader))
print(f'Форма признаков: {train_features.shape}')
print(f'Форма меток: {train_labels.shape}')
# Markdown:
<p class='task' id='2'></p>

2\. Создайте модель при помощи класса `AutoModelForImageClassification`, заменив голову модели в соответствии с решаемой задачей  классификации. Используя стандартный цикл обучения `torch` (или `pytorch_lightning`), настройте модель. Во время обучения выводите на экран значение функции потерь и значение F1 на обучающем множестве, а также F1 на валидационном множестве. 

- [ ] Проверено на семинаре
import torch as th
from transformers import AutoModelForImageClassification

device = th.device('cuda' if th.cuda.is_available() else 'cpu')
model = AutoModelForImageClassification.from_pretrained(
    'google/efficientnet-b0', ignore_mismatched_sizes=True, num_labels=4
).to(device)
from torch import nn, optim
from torchmetrics import F1Score

epochs = 7
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=5e-5)

train_f1_metric = F1Score(task='multiclass', num_classes=4).to(device)
val_f1_metric = F1Score(task='multiclass', num_classes=4).to(device)
from tqdm import tqdm

for epoch in tqdm(range(epochs), desc='train'):
    model.train()
    running_loss = 0.0
    train_f1_metric.reset()
    
    for images, labels in train_loader:
        images = images.permute(0, 3, 1, 2).float().to(device)
        labels = labels.to(device)

        optimizer.zero_grad()
        outputs = model(images)
        loss = criterion(outputs.logits, labels)
        loss.backward()
        optimizer.step()
        
        running_loss += loss.item()
        preds = th.argmax(outputs.logits, dim=1)
        train_f1_metric.update(preds, labels)
    train_f1 = train_f1_metric.compute().item()

    model.eval()
    val_f1_metric.reset()
    with th.no_grad():
        for images, labels in tqdm(test_loader, desc='val'):
            images = images.permute(0, 3, 1, 2).float().to(device)
            labels = labels.to(device)
            
            outputs = model(images)
            preds = th.argmax(outputs.logits, dim=1)
            val_f1_metric.update(preds, labels)
    
    val_f1 = val_f1_metric.compute().item()
    print(f'Epoch {epoch+1}/{epochs}, Loss: {running_loss/len(train_loader)}, Train F1: {train_f1}, Validation F1: {val_f1}')
# Markdown:
<p class='task' id='3'></p>

3\. Создайте модель при помощи класса `AutoModelForImageClassification`, заменив голову модели в соответствии решаемой задачей  классификации. Используя `transformers.Trainer`, настройте модель для решения задачи. Во время обучения выводите на экран значение функции потерь на обучающем и валидационном множества, а также F1 на валидационном множестве. 

Настройте `Trainer` таким образом, чтобы логирование процесса обучения осуществлялось при помощи `wandb`. Прикрепите скриншоты интерфейса `wandb` с результатами. 

- [ ] Проверено на семинаре
from transformers import TrainingArguments, Trainer
from torch.utils.data import Dataset
import evaluate
import wandb
wandb.login()
wandb.init(project='image-classification-project')
from transformers import AutoImageProcessor

processor = AutoImageProcessor.from_pretrained('google/efficientnet-b0')
from datasets import Dataset as HFDataset

def transform_dataset(dataset, processor):
    images = []
    labels = []
    for img, label in dataset:
        encoding = processor(img, return_tensors='pt')
        images.append(encoding['pixel_values'].squeeze())
        labels.append(label)
    return HFDataset.from_dict({'pixel_values': images, 'labels': labels})

train_dataset = transform_dataset(dataset_train, processor)
eval_dataset = transform_dataset(dataset_test, processor)
num_labels = len(dataset.classes)
label_names = dataset.classes
id2label = {str(i): label for i, label in enumerate(label_names)}
label2id = {label: i for i, label in enumerate(label_names)}

model = AutoModelForImageClassification.from_pretrained(
    'google/efficientnet-b0',
    num_labels=num_labels,
    id2label=id2label,
    label2id=label2id,
    ignore_mismatched_sizes=True
)
accuracy_metric = evaluate.load('accuracy')
f1_metric = evaluate.load('f1')

def compute_metrics(p):
    preds = np.argmax(p.predictions, axis=1)
    labels = p.label_ids
    accuracy = accuracy_metric.compute(predictions=preds, references=labels)['accuracy']
    f1 = f1_metric.compute(predictions=preds, references=labels, average='weighted')['f1']
    return {'accuracy': accuracy, 'f1': f1}

training_args = TrainingArguments(
    output_dir='./results',
    num_train_epochs=12,
    per_device_train_batch_size=16,
    per_device_eval_batch_size=16,
    save_strategy='epoch',
    eval_strategy='epoch',
    logging_strategy='steps',
    logging_steps=10,
    load_best_model_at_end=True,
    metric_for_best_model='f1',
    report_to='wandb',
)

trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=train_dataset,
    eval_dataset=eval_dataset,
    compute_metrics=compute_metrics,
)
trainer.train()
wandb.finish()
# Markdown:
![Screenshot 2024-11-28 at 15.59.05.png](<attachment:Screenshot 2024-11-28 at 15.59.05.png>)
![Screenshot 2024-11-28 at 15.59.18.png](<attachment:Screenshot 2024-11-28 at 15.59.18.png>)
![Screenshot 2024-11-28 at 15.59.25.png](<attachment:Screenshot 2024-11-28 at 15.59.25.png>)
# Markdown:
<p class='task' id='4'></p>

4\. Повторите решение задачи 3, настроив процедуру ранней остановки (используйте механизм callback для Trainer). Логика ранней остановки следующая: если метрика F1 не увеличивалась на валидационном множестве в течение 3 последних эпох, то процесс обучения останавливается.

- [ ] Проверено на семинаре
wandb.init(project='image-classification-project')
from transformers import EarlyStoppingCallback

early_stopping_callback = EarlyStoppingCallback(
    early_stopping_patience=3,
)

training_args = TrainingArguments(
    output_dir='./results',
    num_train_epochs=12,
    per_device_train_batch_size=16,
    per_device_eval_batch_size=16,
    save_strategy='epoch',
    eval_strategy='epoch',
    logging_strategy='steps',
    logging_steps=10,
    load_best_model_at_end=True,
    metric_for_best_model='f1',
    report_to='wandb',
)

trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=train_dataset,
    eval_dataset=eval_dataset,
    compute_metrics=compute_metrics,
    callbacks=[early_stopping_callback],
)
trainer.train()
wandb.finish()


# 06_1_object_detection.ipynb
# Markdown:
#  Обнаружение объектов

__Автор задач: Блохин Н.В. (NVBlokhin@fa.ru)__

Материалы:
* https://pytorch.org/tutorials/intermediate/torchvision_tutorial.html
* https://pyimagesearch.com/2021/11/01/training-an-object-detector-from-scratch-in-pytorch/
* https://pyimagesearch.com/2021/08/02/pytorch-object-detection-with-pre-trained-networks/
# Markdown:
## Задачи для совместного разбора
# Markdown:
1\. Рассмотрите простейшую архитектуру для решения задачи object detection и процесс настройки модели.
import torch as th
import torch.nn as nn
imgs = th.rand(size=(16, 3, 100, 100))
bboxes_true = th.rand(size=(16, 4))
labels_true = th.randint(0, 2, size=(16, ))
import matplotlib.pyplot as plt

plt.imshow(imgs[0].permute(1, 2, 0))
plt.show()
class Detector(nn.Module):
	def __init__(self):
		super().__init__()
		self.backbone = nn.Sequential(
			nn.Conv2d(3, 64, kernel_size=3),
			nn.ReLU(),
			nn.MaxPool2d(2)
		)

		self.regressor = nn.Sequential(
			nn.Linear(153664, 64),
			nn.ReLU(),
			nn.Linear(64, 4)
		)

		self.classifier = nn.Sequential(
			nn.Linear(153664, 64),
			nn.ReLU(),
			nn.Linear(64, 2)
		)

	def forward(self, X):
		features = self.backbone(X).flatten(start_dim=1)
		bbox = self.regressor(features)
		labels = self.classifier(features)

		return bbox, labels
model = Detector()
bboxes_pred, labels_pred = model(imgs)
bboxes_pred[0], labels_pred[0]
mse_criterion = nn.MSELoss()
ce_criterion = nn.CrossEntropyLoss()
loss = (
    0.5*mse_criterion(bboxes_pred, bboxes_true) +
    0.5*ce_criterion(labels_pred, labels_true)
)
# Markdown:
## Задачи для самостоятельного решения
# Markdown:
<p class='task' id='1'></p>

1\. Напишите функцию `parse_xml`, которая читает xml-файл с разметкой изображения из архива `animals.zip` и возвращает словарь, содержащий три ключа:
```
{
        'raw': # словарь с ключами xmin, ymin, xmax, ymax
        'scaled': # словарь с ключами xmin, ymin, xmax, ymax
        'obj_name': # строка
}
```
В этом словаре `row` - абсолютные значения координат вершин bounding box, а `scaled` - относительные (нормированные на ширину и высоту изображения). Примените функцию к файлу `cat.0.xml` и выведите результат на экран.


- [ ] Проверено на семинаре
import xml.etree.ElementTree as ET
from zipfile import ZipFile
def parse_xml(zip_path, xml_name):
    with ZipFile(zip_path) as z:
        with z.open(xml_name) as f:
            tree = ET.parse(f)
            root = tree.getroot()

            for obj in root.iter('object'):
                size = root.find('size')
                width = int(size.find('width').text)
                height = int(size.find('height').text)
                
                obj = root.find('object')
                obj_name = obj.find('name').text
                
                bbox = obj.find('bndbox')
                xmin = float(bbox.find('xmin').text)
                ymin = float(bbox.find('ymin').text)
                xmax = float(bbox.find('xmax').text)
                ymax = float(bbox.find('ymax').text)
                
                out = {
                    'raw': {
                        'xmin': xmin,
                        'ymin': ymin,
                        'xmax': xmax,
                        'ymax': ymax
                    },
                    'scaled': {
                        'xmin': xmin / width,
                        'ymin': ymin / height,
                        'xmax': xmax / width,
                        'ymax': ymax / height
                    },
                    'obj_name': obj_name,
                }

                return out
parse_xml('./../data/animals.zip', 'Asirra: cat vs dogs/cat.17.xml')
# Markdown:
<p class='task' id='2'></p>

2\. Опишите датасет `AnimalDetectionDataset` на основе архива `animals.zip`. Реализуйте `__getitem__` таким образом, чтобы он возвращал три элемента: тензор с изображением, словарь с координатами bounding box и метку объекта. Предусмотрите возможность передавать извне при создании датасета набор преобразований для изображений, преобразование для метки объекта (для кодирования) и флаг, показывающий, нужно ли возвращать исходные или нормированные координаты bounding box.

- [ ] Проверено на семинаре
from torch.utils.data import Dataset
from PIL import Image
import numpy as np
class AnimalDetectionDataset(Dataset):
    def __init__(self, zip_path, transforms=None, target_transform = None, return_scaled=True):
        self.zip_path = zip_path
        self.transforms = transforms
        self.target_transform = target_transform
        self.return_scaled = return_scaled
        
        with ZipFile(self.zip_path) as z:
            self.file_list = z.namelist()
        
        self.images_path = list(filter(lambda x: x.endswith('.jpg'), self.file_list))
        self.annotations_path = [f.replace('.jpg', '.xml') for f in self.images_path]
    
    def __len__(self):
        return len(self.images_path)

    def __getitem__(self, index):
        with ZipFile(self.zip_path) as z:
            with z.open(self.images_path[index]) as f:
                img = Image.open(f).convert('RGB')
                
            annotation = parse_xml(self.zip_path, self.annotations_path[index])
            
            if self.transforms:
                img = self.transforms(img)
        
            label = self.target_transform(annotation.get('obj_name')) \
                if self.target_transform else \
                annotation.get('obj_name')
            
            out = (
                th.from_numpy(np.array(img)),
                annotation.get('scaled') \
                    if self.return_scaled else \
                    annotation.get('raw'),
                label
            )
            
            return out
# Markdown:
<p class='task' id='3'></p>

3\. Создайте объект класса `AnimalDetectionDataset` без применения преобразований и со значением `return_scaled=False`. Напишите функцию `show_image_with_bounding_box` для визуализации изображения с добавлением на него bounding box и подписи объекта. Продемонстрируйте работу функцию на изображении собаки и кошки.

- [ ] Проверено на семинаре
dataset = AnimalDetectionDataset(
    './../data/animals.zip',
    transforms=None,  
    target_transform=None, 
    return_scaled=False,
)
dataset[0]
def show_image_with_bounding_box(row):
    img, bbox, label = row

    fig, ax = plt.subplots(1)
    ax.imshow(img)

    rect = plt.Rectangle(
        (bbox['xmin'], bbox['ymin']),
        bbox['xmax'] - bbox['xmin'],
        bbox['ymax'] - bbox['ymin'],
        linewidth=2, edgecolor='g', facecolor='none'
    )
    ax.add_patch(rect)

    plt.text(bbox['xmin'], bbox['ymin'] - 10, label, color='green', fontsize=12, weight='bold')

    plt.show()
show_image_with_bounding_box(dataset[0])
show_image_with_bounding_box(dataset[127])
# Markdown:
<p class='task' id='4'></p>

4\. Напишите модель для решения задачи выделения объектов. Реализуйте двухголовую сеть, одна голова которой предсказывает метку объекта (задача классификации), а вторая голова предсказывает 4 координаты вершин bounding box (задача регрессии). В качестве backbone используйте модель resnet50 из пакета `torchvision`.

- [ ] Проверено на семинаре
from torchvision.models import resnet50
class Detector(nn.Module):
    def __init__(self):
        super().__init__()
        self.backbone = resnet50(pretrained=True)
        self.backbone = nn.Sequential(*list(self.backbone.children())[:-2])
        
        self.regressor = nn.Sequential(
            nn.Linear(2048 * 7 * 7, 64),
            nn.ReLU(),
            nn.Linear(64, 4)
        )

        self.classifier = nn.Sequential(
            nn.Linear(2048 * 7 * 7, 64),
            nn.ReLU(),
            nn.Linear(64, 2)
        )

    def forward(self, X):
        features = self.backbone(X).flatten(start_dim=1)
        bbox = self.regressor(features)
        labels = self.classifier(features)

        return bbox, labels
# Markdown:
<p class='task' id='5'></p>

5\. Разбейте набор данных на обучающее и валидационное множество. Обучите модель, описанную в задаче 4. При создании датасета не забудьте указать преобразования, соответствующие модели ResNet.

Используйте сумму MSELoss (для расчета ошибки на задаче регрессии) и CrossEntropyLoss (для расчета ошибки на задачи классификации) для настройки весов модели. Для ускорения процесса обучения слои backbone можно заморозить. Во время обучения выводите на экран значения функции потерь на обучающем и валидационном множестве. Используя обученную модель, получите предсказания для изображения кошки и собаки и отрисуйте их. Выполните процедуру, обратную нормализации, чтобы корректно отобразить фотографии.

- [ ] Проверено на семинаре
from torchvision.transforms import Compose, Resize, ToTensor, Normalize
from torch.utils.data import random_split, DataLoader
from torch import optim
dataset = AnimalDetectionDataset(
    './../data/animals.zip',
    transforms=Compose([
        Resize((224, 224)),
        ToTensor(),
        Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
    ]),
    target_transform=lambda label: 0 if label == 'cat' else 1,
    return_scaled=True
)
dataset_train, dataset_val = random_split(
    dataset, [int(0.8*len(dataset)), len(dataset) - int(0.8*len(dataset))]
)
device = th.device('cuda' if th.cuda.is_available() else 'cpu')
model = Detector().to(device)
for param in model.backbone.parameters():
    param.requires_grad = True
train_loader = DataLoader(dataset_train, batch_size=4, shuffle=True, num_workers=4)
val_loader = DataLoader(dataset_val, batch_size=4, shuffle=False, num_workers=4)
optimizer = optim.AdamW(model.parameters(), lr=1e-5)
criterion_class = nn.CrossEntropyLoss()
criterion_bbox = nn.MSELoss()
num_epochs = 10
for epoch in range(num_epochs):
    model.train()
    train_loss = 0.0
    for batch_idx, (images, bboxes, labels) in enumerate(train_loader):
        images = images.to(device)
        bboxes_tensor = th.stack(
            [bboxes[key].to(th.float32) for key in ['xmin', 'ymin', 'xmax', 'ymax']], dim=1
        ).to(device)
        labels = labels.to(device)

        optimizer.zero_grad()
        pred_bboxes, pred_labels = model(images)
        
        loss_class = criterion_class(pred_labels, labels)
        loss_bbox = criterion_bbox(pred_bboxes, bboxes_tensor)
        loss = 0.5 * loss_class +  0.5 * loss_bbox

        loss.backward()
        optimizer.step()
        train_loss += loss.item()

    model.eval()
    val_loss = 0.0
    with th.no_grad():
        for batch_idx, (images, bboxes, labels) in enumerate(val_loader):
            images = images.to(device)
            bboxes_tensor = th.stack(
                [bboxes[key].to(th.float32) for key in ['xmin', 'ymin', 'xmax', 'ymax']], dim=1
            ).to(device)
            labels = labels.to(device)

            pred_bboxes, pred_labels = model(images)
            
            loss_class = criterion_class(pred_labels, labels)
            loss_bbox = criterion_bbox(pred_bboxes, bboxes_tensor)
            loss =  0.5 * loss_class +  0.5 * loss_bbox

            val_loss += loss.item()

    avg_train_loss = train_loss / len(train_loader)
    avg_val_loss = val_loss / len(val_loader)
    print(f'Epoch {epoch+1}/{num_epochs} - Train Loss: {avg_train_loss:.4f} - Val Loss: {avg_val_loss:.4f}')
def denormalize(image, mean, std):
    mean = th.tensor(mean).view(3, 1, 1)
    std = th.tensor(std).view(3, 1, 1)
    image = image * std + mean
    return image

def visualize_prediction(image, bbox, label, label_map, mean, std):
    image = denormalize(image, mean, std).permute(1, 2, 0).cpu().numpy()
    image = np.clip(image, 0, 1)

    xmin, ymin, xmax, ymax = bbox
    xmin *= image.shape[1]
    ymin *= image.shape[0]
    xmax *= image.shape[1]
    ymax *= image.shape[0]

    plt.figure(figsize=(6, 6))
    plt.imshow(image)
    plt.gca().add_patch(plt.Rectangle(
        (xmin, ymin), xmax - xmin, ymax - ymin,
        edgecolor='red', fill=False, linewidth=2
    ))
    plt.title(f'Predicted: {label_map[label]}')
    plt.axis('off')
    plt.show()
model.eval()
mean = [0.485, 0.456, 0.406]
std = [0.229, 0.224, 0.225]
indices = [107, 1007]

images, predicted_bboxes, predicted_labels = [], [], []
with th.no_grad():
    for idx in indices:
        image, true_bbox, true_label = dataset[idx]
        image_tensor = image.unsqueeze(0).to(device)
        
        pred_bboxes, pred_labels = model(image_tensor)
        
        pred_bbox = pred_bboxes[0].cpu().numpy()
        pred_label = pred_labels[0].argmax().item()

        images.append(image)
        predicted_bboxes.append(pred_bbox)
        predicted_labels.append(pred_label)

for image, predicted_bbox, predicted_label in zip(images, predicted_bboxes, predicted_labels):
    visualize_prediction(image, predicted_bbox, predicted_label, {0: 'cat', 1: 'dog'}, mean, std)
# Markdown:
<p class='task' id='6'></p>

6\. Найдите в сети несколько изображений котов и собак. Используя любой инструмент для разметки (например, [CVAT](https://www.cvat.ai/)), выделите котов и собак на изображениях. Вставьте скриншот экспортированного файла с разметкой. Используя полученные изображения, визуализируйте разметку и bounding boxes, полученные при помощи модели.

- [ ] Проверено на семинаре
# Markdown:
![Снимок экрана 2024-12-02 в 17.28.04.png](<attachment:Снимок экрана 2024-12-02 в 17.28.04.png>)
def parse_xml(zip_path, image_path, xml_name):
    with ZipFile(zip_path) as z:
        with z.open(xml_name) as f:
            tree = ET.parse(f)
            root = tree.getroot()

            image_annotation = root.find(f".//image[@name='{image_path.split('/')[-1]}']")

            width = int(image_annotation.attrib['width'])
            height = int(image_annotation.attrib['height'])

            box = image_annotation.find("box")
            xmin = float(box.attrib['xtl'])
            ymin = float(box.attrib['ytl'])
            xmax = float(box.attrib['xbr'])
            ymax = float(box.attrib['ybr'])
            label = box.attrib['label']

            out = {
                'raw': {
                    'xmin': xmin,
                    'ymin': ymin,
                    'xmax': xmax,
                    'ymax': ymax
                },
                'scaled': {
                    'xmin': xmin / width,
                    'ymin': ymin / height,
                    'xmax': xmax / width,
                    'ymax': ymax / height
                },
                'obj_name': label,
            }

            return out
class AnimalDetectionDataset(Dataset):
    def __init__(self, zip_path, transforms=None, target_transform = None, return_scaled=True):
        self.zip_path = zip_path
        self.transforms = transforms
        self.target_transform = target_transform
        self.return_scaled = return_scaled
        
        with ZipFile(self.zip_path) as z:
            self.file_list = z.namelist()
        
        self.images_path = list(filter(lambda x: x.endswith('.jpg'), self.file_list))
        self.annotations_path = 'cvat-annotations/annotations.xml'
    
    def __len__(self):
        return len(self.images_path)

    def __getitem__(self, index):
        with ZipFile(self.zip_path) as z:
            with z.open(self.images_path[index]) as f:
                img = Image.open(f).convert('RGB')
                
            annotation = parse_xml(self.zip_path, self.images_path[index], self.annotations_path)
            
            if self.transforms:
                img = self.transforms(img)
        
            label = self.target_transform(annotation.get('obj_name')) \
                if self.target_transform else \
                annotation.get('obj_name')
            
            out = (
                th.from_numpy(np.array(img)),
                annotation.get('scaled') \
                    if self.return_scaled else \
                    annotation.get('raw'),
                label
            )
            
            return out
dataset = AnimalDetectionDataset(
    './../data/cvat-annotations.zip',
    transforms=Compose([
        Resize((224, 224)),
        ToTensor(),
        Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
    ]),
    target_transform=lambda label: 0 if label == 'cat' else 1,
    return_scaled=True
)
def visualize_prediction(image, true_bbox, bbox, label, label_map, mean, std):
    image = denormalize(image, mean, std).permute(1, 2, 0).cpu().numpy()
    image = np.clip(image, 0, 1)

    xmin, ymin, xmax, ymax = bbox
    xmin *= image.shape[1]
    ymin *= image.shape[0]
    xmax *= image.shape[1]
    ymax *= image.shape[0]

    xmin_true, ymin_true, xmax_true, ymax_true = true_bbox.values()
    xmin_true *= image.shape[1]
    ymin_true *= image.shape[0]
    xmax_true *= image.shape[1]
    ymax_true *= image.shape[0]

    plt.figure(figsize=(6, 6))
    plt.imshow(image)
    plt.gca().add_patch(plt.Rectangle(
        (xmin, ymin), xmax - xmin, ymax - ymin,
        edgecolor='red', fill=False, linewidth=1, label='Predicted'
    ))
    plt.gca().add_patch(plt.Rectangle(
        (xmin_true, ymin_true), xmax_true - xmin_true, ymax_true - ymin_true,
        edgecolor='green', fill=False, linewidth=1, label='True'
    ))
    plt.title(f'Predicted: {label_map[label]}')
    plt.legend()
    plt.axis('off')
    plt.show()
images, true_bboxes, predicted_bboxes, predicted_labels = [], [], [], []
with th.no_grad():
    for idx in range(len(dataset)):
        image, true_bbox, true_label = dataset[idx]
        image_tensor = image.unsqueeze(0).to(device)
        
        pred_bboxes, pred_labels = model(image_tensor)
        
        pred_bbox = pred_bboxes[0].cpu().numpy()
        pred_label = pred_labels[0].argmax().item()

        images.append(image)
        true_bboxes.append(true_bbox)
        predicted_bboxes.append(pred_bbox)
        predicted_labels.append(pred_label)

for image, true_bbox, predicted_bbox, predicted_label in zip(images, true_bboxes, predicted_bboxes, predicted_labels):
    visualize_prediction(image, true_bbox, predicted_bbox, predicted_label, {0: 'cat', 1: 'dog'}, mean, std)


# 06_2_image_segmentation.ipynb
# Markdown:
#  Сегментация изображений

__Автор задач: Блохин Н.В. (NVBlokhin@fa.ru)__

Материалы:
* https://www.kaggle.com/datasets/rajkumarl/people-clothing-segmentation/data
* https://pyimagesearch.com/2021/11/08/u-net-training-image-segmentation-models-in-pytorch/
* https://amaarora.github.io/posts/2020-09-13-unet.html
* https://pytorch.org/docs/stable/generated/torch.nn.ConvTranspose2d.html
* https://medium.com/apache-mxnet/transposed-convolutions-explained-with-ms-excel-52d13030c7e8
* https://github.com/vdumoulin/conv_arithmetic/blob/master/README.md
* https://huggingface.co/docs/transformers/model_doc/segformer
* https://www.kaggle.com/code/damianpanek/segformerb0-people-clothing-segmentation
# Markdown:
## Задачи для совместного разбора
# Markdown:
1\. Обсудите постановку задачи сегментации изображений.
# Markdown:
2\. Рассмотрите пример работы слоя `ConvTranspose2d`.
import torch
import torch.nn as nn
img = torch.arange(0, 4).reshape(1, 1, 2, 2).float()
img
layer = nn.ConvTranspose2d(
    in_channels=1,
    out_channels=1,
    kernel_size=3,
    bias=False,
)

layer(img)
res = torch.zeros(4, 4)
w = layer.weight.squeeze()
x = img.squeeze()

res[:3, :3] += w * x[0, 0]
res[:3, 1:4] += w * x[0, 1]
res[1:4, :3] += w * x[1, 0]
res[1:4, 1:4] += w * x[1, 1]
res
# Markdown:
## Задачи для самостоятельного решения
# Markdown:
<p class='task' id='1'></p>

1\. Опишите датасет `ClothesSegmentationDataset`. Реализуйте `__getitem__` таким образом, чтобы он возвращал два элемента: тензор с изображением и тензор с маской. Маска должна быть представлена трехмерным тензором целых чисел. Предусмотрите возможность передавать извне при создании датасета набор преобразований для изображений и масок. Создайте объект датасета и выведите на экран форму и типы одного изображения и его маски.

- [ ] Проверено на семинаре
from torch.utils.data import Dataset
from torchvision.transforms import Compose, Normalize, Resize, ToTensor
from PIL import Image
import numpy as np
import torch as th
import kagglehub
from pathlib import Path

dataset_path = Path(kagglehub.dataset_download('rajkumarl/people-clothing-segmentation', force_download=False))
dataset_path
class ClothesSegmentationDataset(Dataset):
    def __init__(self, dataset_path, image_transform=None, mask_transform=None):
        self.dataset_path = dataset_path
        self.image_transform = image_transform
        self.mask_transform = mask_transform
        
        self.images_path = dataset_path / 'png_images' / 'IMAGES'
        self.masks_path = dataset_path / 'png_masks' / 'MASKS'

        self.images = list(self.images_path.iterdir())
        self.masks = [
            self.masks_path / i.name.replace('img', 'seg')
            for i in self.images
        ]
    
    def get_random_element(self):
        index = np.random.randint(0, len(self))
        return self.__getitem__(index)
    
    def __len__(self):
        return len(self.images)
    
    def __getitem__(self, index):
        image_path = self.images[index]
        mask_path = self.masks[index]

        image = Image.open(image_path).convert('RGB')
        mask = Image.open(mask_path)

        if self.image_transform:
            image = self.image_transform(image)
        else:
            image = ToTensor()(image)

        if self.mask_transform:
            mask = self.mask_transform(mask)
        
        mask = th.tensor(np.array(mask, dtype=np.int64))

        if mask.ndim == 2:
            mask = mask.unsqueeze(0)

        return image, mask
image_transform = Compose([
    Resize((256, 256)),
    ToTensor(),
    Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])
mask_transform = Compose([
    Resize((256, 256)),
])
dataset = ClothesSegmentationDataset(dataset_path, image_transform, mask_transform)
image, mask = dataset.get_random_element()
image.shape, mask.shape
# Markdown:
<p class='task' id='2'></p>

2\. Напишите функцию `show_image_with_mask`, которая выводит рядом два изображения: фотографию и маску. Продемонстрируйте работу функции, взяв один пример из созданного датасета.

- [ ] Проверено на семинаре
from matplotlib import pyplot as plt
def denormalize(image, mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]):
    mean = torch.tensor(mean).view(3, 1, 1)
    std = torch.tensor(std).view(3, 1, 1)
    return image * std + mean

def show_image_with_mask(image, mask, mask2=None, do_denormalize=True):
    if do_denormalize:
        image = denormalize(image)
    fig, ax = plt.subplots(1, 3 if mask2 is not None else 2, figsize=(10, 5))

    ax[0].imshow(image.permute(1, 2, 0), cmap='nipy_spectral')
    ax[0].set_title('Image')
    ax[0].axis('off')

    ax[1].imshow(mask.squeeze(), cmap='nipy_spectral')
    ax[1].set_title('Mask')
    ax[1].axis('off')

    if mask2 is not None:
        ax[2].imshow(mask2.squeeze(), cmap='nipy_spectral')
        ax[2].set_title('Model')
        ax[2].axis('off')

    fig.tight_layout()
    fig.show()
image, mask = dataset.get_random_element()
show_image_with_mask(image, mask)
# Markdown:
<p class='task' id='3'></p>

3\. Реализуйте архитектуру U-Net. Реализуйте модель таким образом, чтобы на выходе для каждого изображения получался тензор размера `n_classes x h x w`, где `n_classes` - количество уникальных значений в масках, а `h` и `w` - размер исходного изображения. Возьмите один пример из набора данных и пропустите его через сеть. Выведите форму полученного результата на экран.

- [ ] Проверено на семинаре
# Markdown:
![image.png](attachment:image.png)
from torch import nn, optim
class UNet(nn.Module):
    def __init__(self, in_channels, n_classes):
        super(UNet, self).__init__()

        def conv_block(in_ch, out_ch):
            block = nn.Sequential(
                nn.Conv2d(in_ch, out_ch, kernel_size=3, padding=1),
                nn.ReLU(inplace=True),
                nn.Conv2d(out_ch, out_ch, kernel_size=3, padding=1),
                nn.ReLU(inplace=True),
            )
            return block

        def up_conv_block(in_ch, out_ch):
            block = nn.ConvTranspose2d(in_ch, out_ch, kernel_size=2, stride=2)
            return block

        self.max_pool = nn.MaxPool2d(2)

        self.down1 = conv_block(in_channels, 64)
        self.down2 = conv_block(64, 128)
        self.down3 = conv_block(128, 256)
        self.down4 = conv_block(256, 512)
        self.down5 = conv_block(512, 1024)

        self.up1 = up_conv_block(1024, 512)
        self.up_conv1 = conv_block(1024, 512)
        self.up2 = up_conv_block(512, 256)
        self.up_conv2 = conv_block(512, 256)
        self.up3 = up_conv_block(256, 128)
        self.up_conv3 = conv_block(256, 128)
        self.up4 = up_conv_block(128, 64)
        self.up_conv4 = conv_block(128, 64)

        self.final_conv = nn.Conv2d(64, n_classes, kernel_size=1)

    def forward(self, x):
        x1 = self.down1(x)
        x2 = self.down2(self.max_pool(x1))
        x3 = self.down3(self.max_pool(x2))
        x4 = self.down4(self.max_pool(x3))
        x5 = self.down5(self.max_pool(x4))

        x4u = self.up1(x5)
        x4u = self.up_conv1(
            torch.cat([x4, x4u], dim=1)
        )

        x3u = self.up2(x4u)
        x3u = self.up_conv2(
            torch.cat([x3, x3u], dim=1)
        )

        x2u = self.up3(x3u)
        x2u = self.up_conv3(
            torch.cat([x2, x2u], dim=1)
        )

        x1u = self.up4(x2u)
        x1u= self.up_conv4(
            torch.cat([x1, x1u], dim=1)
        )

        output = self.final_conv(x1u)
        return output

    def __call__(self, *args, **kwargs):
        return self.forward(*args, **kwargs)
model = UNet(3, 59)
image, mask = dataset.get_random_element()
image = image.unsqueeze(0)
out = model(image)
image.shape, out.shape
# Markdown:
<p class='task' id='4'></p>

4\.  Разбейте набор данных на обучающее и валидационное множество. Обучите модель U-Net для сегментации изображения. Во время обучения выводите на экран значения функции потерь и точности прогнозов на обучающем и валидационном множестве. Обратите внимание, что выборка является несбалансированной. При расчете функции потерь примените любую известную вам технику для работы с несбалансированными выборками.

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

Используя обученную модель, получите предсказания для нескольких изображений и отрисуйте их.
- [ ] Проверено на семинаре
from torch.utils.data import DataLoader, random_split
from tqdm import tqdm
image_transform = Compose([
    Resize((256, 256)),
    ToTensor(),
    Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])
mask_transform = Compose([
    Resize((256, 256)),
])
dataset = ClothesSegmentationDataset(dataset_path, image_transform, mask_transform)
image, mask = dataset.get_random_element()
image.shape, mask.shape
device = th.device('cuda' if th.cuda.is_available() else 'cpu')
dataset_train, dataset_val = random_split(dataset, [0.8, 0.2])
loader_train = DataLoader(dataset_train, batch_size=4, shuffle=True)
loader_val = DataLoader(dataset_val, batch_size=4, shuffle=False)
class_counts = np.zeros(59)
for _, mask in dataset:
    unique, counts = np.unique(mask.numpy(), return_counts=True)
    class_counts[unique] += counts
class_weights = 1 / class_counts
class_weights = class_weights / class_weights.sum()
class_weights = th.tensor(class_weights, dtype=th.float32).to(device)
model = UNet(in_channels=3, n_classes=59).to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.AdamW(model.parameters(), lr=2e-4)
def calculate_iou(pred, target, num_classes):
    ious = []
    pred = pred.view(-1)
    target = target.view(-1)
    for cls in range(num_classes):
        pred_inds = pred == cls
        target_inds = target == cls
        intersection = (pred_inds & target_inds).sum().item()
        union = (pred_inds | target_inds).sum().item()
        if union == 0:
            ious.append(float('nan'))
        else:
            ious.append(intersection / union)
    return np.nanmean(ious)
num_epochs = 25
for epoch in range(num_epochs):
    model.train()
    train_loss = []
    train_iou = []

    for images, masks in tqdm(loader_train, desc=f'Epoch {epoch+1}/{num_epochs} Train'):
        images = images.to(device)
        masks = masks.to(device)

        optimizer.zero_grad()
        outputs = model(images)
        loss = criterion(outputs, masks.squeeze(1))
        iou = calculate_iou(outputs.argmax(dim=1), masks, 59)
        loss.backward()
        optimizer.step()

        train_loss.append(loss.item())
        train_iou.append(iou)

    model.eval()
    val_loss = []
    val_iou = []

    with torch.no_grad():
        for images, masks in tqdm(loader_val, desc=f'Epoch {epoch+1}/{num_epochs} Val'):
            images = images.to(device)
            masks = masks.to(device)

            outputs = model(images)
            loss = criterion(outputs, masks.squeeze(1))
            iou = calculate_iou(outputs.argmax(dim=1), masks, 59)
            val_loss.append(loss.item())
            val_iou.append(iou)

    print(f'Epoch {epoch+1}/{num_epochs}')
    print(f'Train Loss: {np.mean(train_loss):.4f}, Val Loss: {np.mean(val_loss):.4f}\n' \
          f'Train IoU: {np.nanmean(train_iou):.4f}, Val IoU: {np.nanmean(val_iou):.4f}')
for _ in range(3):
    image, mask = dataset.get_random_element()
    out = model(image.unsqueeze(0).to(device))
    show_image_with_mask(image.cpu(), mask, out[0].argmax(0).cpu())
# Markdown:
<p class='task' id='5'></p>

5\.  Обучите модуль `SegformerForSemanticSegmentation` из пакета `transformers` для сегментации изображения. Во время обучения выводите на экран значения функции потерь и точности прогнозов на обучающем и валидационном множестве. Для оптимизации используйте значение функции потерь, которое возвращает вам модель.

Используя обученную модель, получите предсказания для нескольких изображений и отрисуйте их.
- [ ] Проверено на семинаре
from transformers import SegformerForSemanticSegmentation, SegformerImageProcessor
dataset = ClothesSegmentationDataset(dataset_path)
image, mask = dataset.get_random_element()

dataset_train, dataset_val = random_split(dataset, [0.8, 0.2])
loader_train = DataLoader(dataset_train, batch_size=4, shuffle=True)
loader_val = DataLoader(dataset_val, batch_size=4, shuffle=False)
model = SegformerForSemanticSegmentation.from_pretrained(
    'nvidia/segformer-b0-finetuned-ade-512-512',
    num_labels=59, ignore_mismatched_sizes=True,
).to(device)
processor = SegformerImageProcessor()
optimizer = torch.optim.AdamW(model.parameters(), lr=5e-5)
num_epochs = 20
for epoch in range(num_epochs):
    model.train()
    train_loss = []

    for images, masks in tqdm(loader_train, desc=f'Epoch {epoch+1}/{num_epochs} Train'):
        images = images.to(device)
        inputs = processor(images, return_tensors='pt').to(device)
        masks = masks.squeeze(1).to(device)

        optimizer.zero_grad()
        outputs = model(**inputs, labels=masks)
        loss = outputs.loss
        loss.backward()
        optimizer.step()

        train_loss.append(loss.item())
    
    model.eval()
    val_loss = []

    with torch.no_grad():
        for images, masks in tqdm(loader_val, desc=f'Epoch {epoch+1}/{num_epochs} Val'):
            images = denormalize(images).to(device)
            inputs = processor(images, return_tensors='pt').to(device)
            masks = masks.squeeze(1).to(device)

            outputs = model(**inputs, labels=masks)
            val_loss.append(outputs.loss.item())

    print(f'Epoch {epoch+1}/{num_epochs}')
    print(f'Train Loss: {np.mean(train_loss):.4f}, Val Loss: {np.mean(val_loss):.4f}\n')
for _ in range(3):
    image, mask = dataset.get_random_element()
    image_m = image.clone().unsqueeze(0).to(device)
    inputs = processor(image_m, return_tensors='pt').to(device)
    out = model(**inputs).logits[0].argmax(0).cpu()
    show_image_with_mask(image, mask, out, do_denormalize=False)


# 07_1_autoencoders.ipynb
# Markdown:
#  Автоэнкодеры

__Автор задач: Блохин Н.В. (NVBlokhin@fa.ru)__

Материалы:
* https://www.eecs.qmul.ac.uk/~sgg/_ECS795P_/papers/WK07-8_PyTorch_Tutorial2.html
* https://www.youtube.com/watch?v=zp8clK9yCro
* https://medium.com/@rekalantar/variational-auto-encoder-vae-pytorch-tutorial-dce2d2fe0f5f
* https://towardsdatascience.com/conditional-variational-autoencoders-with-learnable-conditional-embeddings-e22ee5359a2a
* https://pytorch.org/vision/stable/auto_examples/others/plot_visualization_utils.html#sphx-glr-auto-examples-others-plot-visualization-utils-py
# Markdown:
## Задачи для совместного разбора
# Markdown:
1\. Обсудите основные шаги в обучении автокодировщиков.
# Markdown:
## Задачи для самостоятельного решения
# Markdown:
<p class="task" id="1"></p>

1\. Загрузите набор данных MNIST из пакета `torchvision` (данный набор уже разбит на обучающее и тестовое множество).

Создайте и обучите модель автокодировщика, используя только полносвязные слои и функции активации.

Кодировщик - это функция вида
$z = f_\theta(x)$
,где $\theta$ - это параметры кодировщика.

Декодировщик - это функция вида
$\hat{x} = g_\phi(z)$
,где $\phi$ - это параметры декодировщика.

В нашем случае оба компонента представляют собой нейронные сети. Скрытое представление, полученное после части-кодировщика, должно иметь размерность 2. Последним слоем части-декодеровщика сделайте сигмоиду.

В качестве функции потерь используйте `MSELoss` между исходным и восстановленным изображением $MSE(x, \hat{x})$.

Обратите внимание, что во время обучения метки классов не используются.


- [ ] Проверено на семинаре
# Markdown:
from torchvision.datasets import MNIST
from torchvision.transforms import Compose, ToTensor
from torch.utils.data import DataLoader
from torch import nn, optim
from tqdm import tqdm
import torch as th
import numpy as np
transform = Compose([ToTensor()])
train_dataset = MNIST(root='./../data/', train=True, download=True, transform=transform)
test_dataset = MNIST(root='./../data/', train=False, download=True, transform=transform)

train_loader = DataLoader(train_dataset, batch_size=8, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=8, shuffle=True)
len(train_dataset), len(test_dataset)
train_dataset[0][0].shape
class Autoencoder(nn.Module):
    def __init__(self):
        super(Autoencoder, self).__init__()
        self.encoder = nn.Sequential(
            nn.Linear(28*28, 128),
            nn.ReLU(),
            nn.Linear(128, 64),
            nn.ReLU(),
            nn.Linear(64, 2),
        )
        self.decoder = nn.Sequential(
            nn.Linear(2, 64),
            nn.ReLU(),
            nn.Linear(64, 128),
            nn.ReLU(),
            nn.Linear(128, 28*28),
            nn.Sigmoid(),
        )
    
    def forward(self, x):
        x = self.encoder(x)
        x = self.decoder(x)
        return x
device = th.device('cuda' if th.cuda.is_available() else 'cpu')
model = Autoencoder().to(device)
optimizer = optim.AdamW(model.parameters(), lr=2e-4)
critertion = nn.MSELoss()
num_epochs = 20
for epoch in range(num_epochs):
    model.train()
    loss_train = 0
    for images, classes, in tqdm(train_loader, desc=f'Epoch {epoch+1}/{num_epochs} Train'):
        images = images.flatten(start_dim=1).to(device)

        out = model(images)

        loss = critertion(images, out)
        loss_train += loss.item()

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
    
    model.eval()
    loss_val = 0
    with th.no_grad():
        for images, classes, in tqdm(test_loader, desc=f'Epoch {epoch+1}/{num_epochs} Val'):
            images = images.flatten(start_dim=1).to(device)
            
            out = model(images)

            loss = critertion(images, out)
            loss_val += loss.item()
    
    print(f'  - Train Loss: {loss_train / len(train_loader):.4f} Val Loss: {loss_val / len(test_loader):.4f}')
# Markdown:
<p class="task" id="2"></p>

2\. Получите один батч из тестового множества. Используя модель, обученную в предыдущем задании, получите скрытые представления для всех изображений из этого батча и визуализируйте на плоскости (они должны иметь размерность 2!). Раскрасьте точки в цвета, соответствующие меткам класса изображений (цифрам).

Возьмите одно изображение из тестового множества и пропустите его через обученный автокодировщик. Визуализируйте рядом (по горизонтали) два изображения: исходное и после восстановления автокодировщиком.


- [ ] Проверено на семинаре
import matplotlib.pyplot as plt
import seaborn as sns
images, classes = next(iter(test_loader))
images = images.flatten(start_dim=1).to(device)
encoder_out = model.encoder(images)
encoder_out
data_xy = encoder_out.cpu().detach().numpy()
classes_np = classes.cpu().numpy()
sns.scatterplot(x=data_xy[:, 0], y=data_xy[:, 1], hue=classes_np, palette='rainbow')
plt.grid(True)
plt.show()
image, label = test_dataset[0]
image_th = image.clone().to(device).flatten().unsqueeze(0)
out = model(image_th)
fig, ax = plt.subplots(1, 2)
for a in ax:
	a.set_xticks([])
	a.set_yticks([])
ax[0].set_title('True')
ax[1].set_title('Pred')
ax[0].imshow(image.permute(1, 2, 0), cmap='gray')
ax[1].imshow((out.cpu().detach().numpy().reshape(28, 28) * 255), cmap='gray')
fig.tight_layout()
plt.show()
# Markdown:
<p class="task" id="3"></p>

3\. Напишите функцию для генерации изображения на основе случайного шума. Функция должна генерировать случайный шум из стандартного нормального распределения и пропускать его через часть-декодировщик. Сгенерируйте несколько изображений и визуализируйте в виде сетки из картинок.

- [ ] Проверено на семинаре
def generate_images_from_noise(decoder):
    noise = th.randn(16, 2, device=device)
    generated_images = decoder(noise).cpu().detach().numpy().reshape(16, 28, 28)
    
    fig, axes = plt.subplots(4, 4, figsize=(8, 8))
    for i, ax in enumerate(axes.flatten()):
        ax.imshow(generated_images[i], cmap='gray')
        ax.axis('off')
    plt.tight_layout()
    plt.show()
generate_images_from_noise(model.decoder)
# Markdown:
<p class="task" id="4"></p>

4\. Создайте и обучите модель условного автокодировщика, используя только полносвязные слои и функции активации.

Отличие от предыдущего варианта заключается в том, что теперь функции кодировщика и декодировщика принимают на вход также метку класса:
$$z = f_\theta(x, c)$$
$$\hat{x} = g_\phi(z, c)$$

Таким образом, теперь во теперь время обучения метки классов используются. Задействуйте их следующим образом: представьте метки классов в виде one-hot кодировки и объедините с пикселями изображения (для этого адаптируйте размерность слоев).

Скрытое представление, полученное после части-кодировщика, должно иметь размерность 2. Последним слоем части-декодеровщика сделайте сигмоиду. В качестве функции потерь используйте `MSELoss` между исходным и восстановленным изображением $MSE(x, \hat{x})$.


- [ ] Проверено на семинаре
# Markdown:
class Autoencoder(nn.Module):
    def __init__(self):
        super(Autoencoder, self).__init__()
        self.encoder = nn.Sequential(
            nn.Linear(28*28+10, 128),
            nn.ReLU(),
            nn.Linear(128, 64),
            nn.ReLU(),
            nn.Linear(64, 2),
        )
        
        self.decoder = nn.Sequential(
            nn.Linear(2+10, 64),
            nn.ReLU(),
            nn.Linear(64, 128),
            nn.ReLU(),
            nn.Linear(128, 28*28),
            nn.Sigmoid(),
        )
    
    def forward(self, x):
        x = self.encoder(x)
        x = self.decoder(x)
        return x
model = Autoencoder().to(device)
optimizer = optim.AdamW(model.parameters(), lr=2e-4)
critertion = nn.MSELoss()
num_epochs = 20
for epoch in range(num_epochs):
    model.train()
    loss_train = 0
    for images, classes, in tqdm(train_loader, desc=f'Epoch {epoch+1}/{num_epochs} Train'):
        images = images.flatten(start_dim=1).to(device)

        batch_size = images.shape[0]
        class_onehot = th.zeros((batch_size, 10), device=device)
        class_onehot[th.arange(batch_size), classes] = 1

        inputs = th.cat([images, class_onehot], dim=1)
        encoder_out = model.encoder(inputs)
        inputs = th.cat([encoder_out, class_onehot], dim=1)
        decoder_out = model.decoder(inputs)

        loss = critertion(images, decoder_out)
        loss_train += loss.item()

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

    model.eval()
    loss_val = 0
    with th.no_grad():
        for images, classes, in tqdm(test_loader, desc=f'Epoch {epoch+1}/{num_epochs} Val'):
            images = images.flatten(start_dim=1).to(device)

            batch_size = images.shape[0]
            class_onehot = th.zeros((batch_size, 10), device=device)
            class_onehot[th.arange(batch_size), classes] = 1

            inputs = th.cat([images, class_onehot], dim=1)
            encoder_out = model.encoder(inputs)
            inputs = th.cat([encoder_out, class_onehot], dim=1)
            decoder_out = model.decoder(inputs)

            loss = critertion(images, decoder_out)
            loss_val += loss.item()
    
    print(f'  - Train Loss: {loss_train / len(train_loader):.4f} Val Loss: {loss_val / len(test_loader):.4f}')
# Markdown:
<p class="task" id="5"></p>

5\. Напишите функцию для генерации изображения на основе случайного шума. Функция должна генерировать случайный шум из стандартного нормального распределения и one-hot представление цифры. Далее объединенный вектор пропускается его через часть-декодировщик. Сгенерируйте несколько изображений и визуализируйте в виде сетки из картинок.

- [ ] Проверено на семинаре
def generate_images_from_noise(decoder, label):
    noise = th.randn(16, 2, device=device)
    class_onehot = th.zeros((16, 10), device=device)
    class_onehot[th.arange(16), label] = 1
    
    inputs = th.cat([noise, class_onehot], dim=1)
    generated_images = decoder(inputs).cpu().detach().numpy().reshape(16, 28, 28)
    
    fig, axes = plt.subplots(4, 4, figsize=(8, 8))
    for i, ax in enumerate(axes.flatten()):
        ax.imshow(generated_images[i], cmap='gray')
        ax.axis('off')
    plt.tight_layout()
    plt.show()
generate_images_from_noise(model.decoder, 5)
# Markdown:
<p class="task" id="6"></p>

6\. Создайте и обучите модель вариационного автокодировщика, используя только полносвязные слои и функции активации.

Кодировщик - это функция следующего вида:
$$q_\phi(z|x) = \mathcal{N}(\mu_\phi(x), \sigma_\phi^2(x))$$

Здесь $\phi$ - параметры кодировщика, а $\mu_\phi(x)$ и $\sigma_\phi^2(x)$ - это обучаемые функции (в нашем случае - полносвязные слои).

Чтобы иметь возможность обучить такую модель, используется т.н. reparametrization trick: на основе функций $\mu$ и $ \sigma$ считаем значение:

$$z = \mu_\phi(x) + \sigma_\phi(x) \odot \epsilon, \quad \epsilon \sim \mathcal{N}(0, I)$$

Декодировщик пытается восстановить исходное изображение из полученного вектора:

$$p_\theta(x|z) = f(z; \theta)$$

В качестве функции потерь обычно используется следующая:
$$\mathcal{L}_{total} = \mathcal{L}_{recon} + D_{KL}$$
$$\mathcal{L}_{recon} = -\sum_{i=1}^D [x_i \log \hat{x}_i + (1 - x_i) \log (1 - \hat{x}_i)]$$
$$D_{KL} = -\frac{1}{2} \sum_{j=1}^J (1 + \log \sigma_j^2 - \mu_j^2 - \sigma_j^2)$$


- [ ] Проверено на семинаре
# Markdown:
class VAE(nn.Module):
    def __init__(self):
        super(VAE, self).__init__()
        self.encoder = nn.Sequential(
            nn.Linear(28*28, 256),
            nn.ReLU(),
            nn.Linear(256, 128),
            nn.ReLU(),
            nn.Linear(128, 200),
        )

        self.linear_mu = nn.Linear(200, 20)
        self.linear_sigma = nn.Linear(200, 20)
        
        self.decoder = nn.Sequential(
            nn.Linear(20, 128),
            nn.ReLU(),
            nn.Linear(128, 256),
            nn.ReLU(),
            nn.Linear(256, 28*28),
            nn.Sigmoid(),
        )
    
    def reparameterize(self, mu, sigma):
        std = th.exp(0.5 * sigma)
        eps = th.randn_like(std)
        return mu + eps * std
    
    def forward(self, x):
        encoded = self.encoder(x)
        mu = self.linear_mu(encoded)
        logvar = self.linear_sigma(encoded)
        z = self.reparameterize(mu, logvar)
        decoded = self.decoder(z)
        return decoded, mu, logvar
def loss_function(recon_x, x, mu, logvar):
    L1 = nn.functional.binary_cross_entropy(recon_x, x, reduction='sum')
    L2 = -0.5 * th.sum(1 + logvar - mu.pow(2) - logvar.exp())
    return L1 + L2
model = VAE().to(device)
optimizer = optim.AdamW(model.parameters(), lr=2e-4)
num_epochs = 20
for epoch in range(num_epochs):
    model.train()
    loss_train = 0
    for images, classes, in tqdm(train_loader, desc=f'Epoch {epoch+1}/{num_epochs} Train'):
        images = images.flatten(start_dim=1).to(device)

        recon_batch, mu, logvar = model(images)
        loss = loss_function(recon_batch, images, mu, logvar)
        loss_train += loss.item()

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
    
    model.eval()
    loss_val = 0
    with th.no_grad():
        for images, classes, in tqdm(test_loader, desc=f'Epoch {epoch+1}/{num_epochs} Val'):
            images = images.flatten(start_dim=1).to(device)
            
            recon_batch, mu, logvar = model(images)
            loss = loss_function(recon_batch, images, mu, logvar)
            loss_val += loss.item()
    
    print(f'  - Train Loss: {loss_train / len(train_loader):.4f} Val Loss: {loss_val / len(test_loader):.4f}')
# Markdown:
<p class="task" id="7"></p>

7\. Напишите функцию для генерации изображения на основе случайного шума. Функция должна генерировать случайный шум из стандартного нормального распределения. Далее вектор пропускается его через часть-декодировщик. Сгенерируйте несколько изображений и визуализируйте в виде сетки из картинок.

- [ ] Проверено на семинаре
def generate_images_from_noise(decoder):
    noise = th.randn(16, 20).to(device)
    generated = decoder(noise).cpu().detach().numpy().reshape(-1, 28, 28)
    
    fig, axes = plt.subplots(int(np.sqrt(16)), int(np.sqrt(16)), figsize=(8, 8))
    for i, ax in enumerate(axes.flatten()):
        ax.imshow(generated[i], cmap='gray')
        ax.axis('off')
    plt.tight_layout()
    plt.show()
generate_images_from_noise(model.decoder)


# 07_2_gan.ipynb
# Markdown:
#  GAN

__Автор задач: Блохин Н.В. (NVBlokhin@fa.ru)__

Материалы:
* https://pytorch.org/tutorials/beginner/dcgan_faces_tutorial.html
* https://www.kaggle.com/datasets/splcher/animefacedataset
* https://github.com/eriklindernoren/PyTorch-GAN
# Markdown:
## Задачи для совместного разбора
# Markdown:
1\. Обсудите основные шаги в обучении GAN.
import torch.nn as nn
import torch

gen = nn.Sequential(
    nn.ConvTranspose2d(100, 8, 4, 2, 1),
    nn.Tanh()
)
noise = torch.randn(16, 100, 1, 1)
generated = gen(noise)
generated.shape
faked_labels = torch.zeros(16)
real = torch.randn(16, 8, 2, 2)
real_labels = torch.ones(16)
discriminator = nn.Sequential(
    #...
    nn.Flatten(start_dim=1),
    nn.LazyLinear(out_features=2)
)
discriminator(generated).shape
discriminator(real).shape
# Markdown:
## Задачи для самостоятельного решения
# Markdown:
<p class='task' id='1'></p>

1\. Создайте набор данных на основе датасета `animefacedataset`. Используя преобразования `torchvision`, приведите изображения к одному размеру и нормализуйте их. Выведите на экран несколько примеров изображений.

- [ ] Проверено на семинаре
import kagglehub
from torchvision.datasets import ImageFolder
from torchvision.transforms import Compose, Resize, CenterCrop, Normalize, ToTensor
from torchvision.utils import make_grid

import matplotlib.pyplot as plt
import seaborn as sns

from torch.utils.data import DataLoader
import torch as th
import numpy as np
path = kagglehub.dataset_download('splcher/animefacedataset')
print('Path to dataset files:', path)
image_size = 64
batch_size = 128
stats = (0.5, 0.5, 0.5), (0.5, 0.5, 0.5)

transform = Compose([
    Resize((64, 64)),
    CenterCrop((64, 64)),
    ToTensor(),
    Normalize(*stats)
])
dataset = ImageFolder(path, transform=transform)
loader = DataLoader(dataset, batch_size=batch_size, shuffle=True, num_workers=8, pin_memory=True)
dataset
def denorm(img_tensors):
    return img_tensors * stats[1][0] + stats[0][0]

def show_images(images, nmax=64):
    fig, ax = plt.subplots(figsize=(8, 8))
    ax.set_xticks([]); ax.set_yticks([])
    ax.imshow(make_grid(denorm(images.detach()[:nmax]), nrow=8).permute(1, 2, 0))

def show_batch(dl, nmax=64):
    images, _ = next(iter(dl))
    show_images(images, nmax)
show_batch(loader)
# Markdown:
<p class='task' id='2'></p>

2\. Реализуйте архитектуру `DCGAN` и обучите модель. Подберите гиперпараметры таким образом, чтобы получаемые изображения стали достаточного качественными (четкими и без существенных дефектов). Во время обучения сохраняйте примеры генерации изображений из случайного шума и сравните, как менялось качество получаемых изображений в процессе обучения.

- [ ] Проверено на семинаре
from torch import optim, nn
from torchvision.utils import save_image
from IPython.display import display, Image as IPImage
from tqdm.notebook import tqdm
from PIL import Image
import imageio
import os
device = th.device('cuda' if th.cuda.is_available() else 'cpu')
nc = 3
nz = 128
ngf = 64
ndf = 64
def weights_init(m):
    classname = m.__class__.__name__
    if classname.find('Conv') != -1:
        nn.init.normal_(m.weight.data, 0.0, 0.02)
    elif classname.find('BatchNorm') != -1:
        nn.init.normal_(m.weight.data, 1.0, 0.02)
        nn.init.constant_(m.bias.data, 0)
class Generator(nn.Module):
    def __init__(self):
        super(Generator, self).__init__()
        self.main = nn.Sequential(
            
            nn.ConvTranspose2d(nz, ngf * 8, 4, 1, 0, bias=False),
            nn.BatchNorm2d(ngf * 8),
            nn.ReLU(True),
            
            nn.ConvTranspose2d(ngf * 8, ngf * 4, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ngf * 4),
            nn.ReLU(True),
            
            nn.ConvTranspose2d(ngf * 4, ngf * 2, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ngf * 2),
            nn.ReLU(True),
            
            nn.ConvTranspose2d(ngf * 2, ngf, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ngf),
            nn.ReLU(True),
            
            nn.ConvTranspose2d(ngf, nc, 4, 2, 1, bias=False),
            nn.Tanh()
        )

    def forward(self, input):
        return self.main(input)
generator = Generator().to(device)
generator.apply(weights_init)
class Discriminator(nn.Module):
    def __init__(self):
        super(Discriminator, self).__init__()
        self.main = nn.Sequential(
            nn.Conv2d(nc, ndf, 4, 2, 1, bias=False),
            nn.LeakyReLU(0.2, inplace=True),
            
            nn.Conv2d(ndf, ndf * 2, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ndf * 2),
            nn.LeakyReLU(0.2, inplace=True),
            
            nn.Conv2d(ndf * 2, ndf * 4, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ndf * 4),
            nn.LeakyReLU(0.2, inplace=True),
            
            nn.Conv2d(ndf * 4, ndf * 8, 4, 2, 1, bias=False),
            nn.BatchNorm2d(ndf * 8),
            nn.LeakyReLU(0.2, inplace=True),
            
            nn.Conv2d(ndf * 8, 1, 4, 1, 0, bias=False),
            nn.Sigmoid()
        )

    def forward(self, input):
        return self.main(input)
discriminator = Discriminator().to(device)
discriminator.apply(weights_init)
xb = torch.randn(batch_size, nz, 1, 1, device=device)
fake_images = generator(xb)
show_images(fake_images.cpu())
sample_dir = 'generated_images'
os.makedirs(sample_dir, exist_ok=True)

def save_samples(index, latent_tensors):
    fake_images = generator(latent_tensors)
    fake_fname = 'generated-images-{0:0=4d}.png'.format(index)
    
    save_image(denorm(fake_images), os.path.join(sample_dir, fake_fname), nrow=8)
    print('Saving', fake_fname)
start_idx = 1
epochs = 250
lr = 2e-4

fixed_latent = torch.randn(64, nz, 1, 1, device=device)
losses_g, losses_d = [], []
real_scores, fake_scores = [], []

opt_d = optim.Adam(discriminator.parameters(), lr=lr, betas=(0.5, 0.999))
opt_g = optim.Adam(generator.parameters(), lr=lr, betas=(0.5, 0.999))
criterion = nn.BCELoss()
for epoch in range(epochs):
    for real_images, _ in tqdm(loader, desc=f'Epoch {epoch+1}/{epochs}'):
        
        discriminator.zero_grad()
        real_cpu = real_images.to(device)
        b_size = real_cpu.size(0)
        label = torch.full((b_size,), 1, dtype=torch.float, device=device)
        output = discriminator(real_cpu).view(-1)
        errD_real = criterion(output, label)
        errD_real.backward()
        D_x = output.mean().item()

        noise = torch.randn(b_size, nz, 1, 1, device=device)
        fake = generator(noise)
        label.fill_(0)
        output = discriminator(fake.detach()).view(-1)
        errD_fake = criterion(output, label)
        errD_fake.backward()
        D_G_z1 = output.mean().item()
        errD = errD_real + errD_fake
        opt_d.step()

        generator.zero_grad()
        label.fill_(1)
        output = discriminator(fake).view(-1)
        errG = criterion(output, label)
        errG.backward()
        D_G_z2 = output.mean().item()
        opt_g.step()
    
    print('Epoch [{}/{}], loss_g: {:.4f}, loss_d: {:.4f}'.format(epoch+1, epochs, errG.item(), errD.item()))

    save_samples(epoch + start_idx, fixed_latent)
torch.save(generator.state_dict(), 'generator_model.bin')
torch.save(discriminator.state_dict(), 'discriminator_model.bin')
def save_frames_as_video(filename, images_path):
    files = [os.path.join(sample_dir, f) for f in os.listdir(sample_dir) if images_path in f]
    files.sort()
    
    images = []
    for fname in files:
        img = Image.open(fname)
        images.append(img)
    
    imageio.mimsave(filename, images, duration=0.25, loop=0)
save_frames_as_video('output.gif', 'generated')
display(IPImage(filename='./output.gif'))
latent = torch.randn(batch_size, nz, 1, 1, device=device)
fake_images = generator(latent).cpu()
show_images(fake_images, nmax=2)
# Markdown:
<p class='task' id='3'></p>

3\. Создайте наборы данных на основе архива `summer2winter_yosemite.zip`. Используя преобразования `torchvision`, приведите изображения к одному размеру и нормализуйте их. Выведите на экран несколько примеров изображений, расположив изображения из одной пары рядом по горизонтали.

- [ ] Проверено на семинаре

# Markdown:
<p class='task' id='4'></p>

4\. Реализуйте архитектуру `CycleGAN` и обучите модель. Подберите гиперпараметры таким образом, чтобы получаемые изображения стали достаточного качественными (четкими и без существенных дефектов). Во время обучения сохраняйте примеры преобразования (в обе стороны) и  сравните, как менялось качество получаемых изображений в процессе обучения.

- [ ] Проверено на семинаре


# 08_1_gymnasium_env.ipynb
# Markdown:
# Введение в RL и пакет Gymnasium

__Автор задач: Блохин Н.В. (NVBlokhin@fa.ru)__

Материалы:
* https://gymnasium.farama.org/
* https://pypi.org/project/ufal.pybox2d/
* https://gymnasium.farama.org/tutorials/gymnasium_basics/environment_creation/
* https://gymnasium.farama.org/api/spaces/fundamental/
* https://gymnasium.farama.org/environments/toy_text/blackjack/
# Markdown:
## Задачи для совместного разбора
# Markdown:
1\. Рассмотрите пример создания окружения `gymnasium` и основные этапы взаимодействия с этим окружением.


import gymnasium as gym
env = gym.make('LunarLander-v3', render_mode='human')

def make_action(obs, env):
  # тут вы должны исопльзовать состояние
  return env.action_space.sample()

obs, _ = env.reset()
is_done = False
rewards = []
while not is_done:
  action = make_action(obs, env)
  obs, reward, terminated, truncated, info = env.step(action)

  is_done = terminated or truncated
  rewards.append(reward)

env.close()
sum(rewards)
# Markdown:
## Задачи для самостоятельного решения
# Markdown:
<p class="task" id="1"></p>

1\. Создайте окружение `Blackjack-v1`. Сыграйте `N=10000` игр, выбирая действие случайным образом. Посчитайте и выведите на экран долю выигранных игр.

- [ ] Проверено на семинаре
env = gym.make('Blackjack-v1')
N = 10000
wins = 0

for _ in range(N):
    obs, _ = env.reset()
    done = False
    while not done:
        action = env.action_space.sample()
        obs, reward, terminated, truncated, _ = env.step(action)
        done = terminated or truncated
    if reward == 1:
        wins += 1
win_ratio = wins / N
print(f'Доля выигранных игр: {win_ratio}')
# Markdown:
<p class="task" id="2"></p>

2\. Создайте окружение `Blackjack-v1`. Предложите стратегию, которая позволит, в среднем, выигрывать чаще, чем случайный выбор действия. Реализуйте эту стратегию и сыграйте `N=10000` игр, выбирая действие согласно этой стратегии. Посчитайте и выведите на экран долю выигранных игр.

- [ ] Проверено на семинаре
env = gym.make('Blackjack-v1')
N = 10000
wins = 0

def strategy(obs):
    player_sum, dealer_card, usable_ace = obs
    if player_sum < 16:
        return 1
    else:
        return 0

for _ in range(N):
    obs, _ = env.reset()
    done = False
    while not done:
        action = strategy(obs)
        obs, reward, terminated, truncated, _ = env.step(action)
        done = terminated or truncated
    if reward == 1:
        wins += 1
win_ratio = wins / N
print(f'Доля выигранных игр: {win_ratio}')
# Markdown:
<p class="task" id="3"></p>

3\. Создайте окружение для игры в крестики-нолики, реализовав интерфейс `gym.Env`. Решение должно удовлетворять следующим условиям:
* для создания пространства состояний используется `spaces.Box`;
* для создания пространства действий используется `spaces.MultiDiscrete`;
* игра прекращается, если:
    - нет возможности сделать ход;
    - игрок пытается отметить уже выбранную ячейку.
* после каждого хода игрок получает награду:
    - 0, если игра не закончена;
    - 1, если игрок выиграл;
    - -1, если игрок проиграл.
* стратегию выбора действия для второго игрока (машины) определите самостоятельно.

Стратегия поведения машины является частью окружения и должна быть реализована внутри него. Сделайте все соответствующие переменные и методы приватными (названия всех переменных начинаются с `__`), подчеркнув, что у пользователя не должно быть к ним доступа извне.

Сыграйте одну игру, выбирая действия случайным образом. Выведите на экран состояние окружения после каждого хода и итоговую награду пользователя за сессию.

- [ ] Проверено на семинаре
from gymnasium import spaces
import numpy as np
import random
class TicTacToeEnv(gym.Env):
    def __init__(self, draw=True):
        super(TicTacToeEnv, self).__init__()
        self.observation_space = spaces.Box(low=0, high=2, shape=(3, 3), dtype=np.int8)
        self.action_space = spaces.MultiDiscrete([3, 3])
        self.board = np.zeros((3, 3), dtype=np.int8)
        self.done = False
        self.draw = draw
        self.reward = 0
        self.info = {}
    
    def reset(self):
        self.board = np.zeros((3, 3), dtype=np.int8)
        self.done = False
        self.reward = 0
        self.info = {}
        return self.board.copy(), self.info
    
    def step(self, action):
        if self.done:
            raise ValueError('Игра уже завершена')
        
        user_row, user_col = action
        if self.board[user_row, user_col] != 0:
            self.done = True
            self.reward = -1
            return self.board.copy(), self.reward, self.done, False, self.info
        
        self.board[user_row, user_col] = 1
        if self.check_winner(1):
            self.done = True
            self.reward = 1
            return self.board.copy(), self.reward, self.done, False, self.info
        
        if self.is_board_full():
            self.done = True
            self.reward = -1
            return self.board.copy(), self.reward, self.done, False, self.info
        
        machine_move = self.get_machine_move()
        if machine_move is not None:
            machine_row, machine_col = machine_move
            self.board[machine_row, machine_col] = 2
            if self.check_winner(2):
                self.done = True
                self.reward = -1
                return self.board.copy(), self.reward, self.done, False, self.info
        
        if self.is_board_full():
            self.done = True
            self.reward = -1
            return self.board.copy(), self.reward, self.done, False, self.info
        
        self.reward = 0
        return self.board.copy(), self.reward, self.done, False, self.info
    
    def render(self, mode='human'):
        symbol_map = {0: ' ', 1: 'X', 2: 'O'}
        if self.draw:
            print('-'*13)
        for row in self.board:
            row_symbols = [symbol_map[cell] for cell in row]
            if self.draw:
                print(f'| {' | '.join(row_symbols)} |')
                print('-'*13)
    
    def check_winner(self, player):
        for i in range(3):
            if np.all(self.board[i, :] == player):
                return True
            if np.all(self.board[:, i] == player):
                return True
        if self.board[0, 0] == player and self.board[1, 1] == player and self.board[2, 2] == player:
            return True
        if self.board[0, 2] == player and self.board[1, 1] == player and self.board[2, 0] == player:
            return True
        return False
    
    def is_board_full(self):
        return not np.any(self.board == 0)
    
    def get_machine_move(self):
        available_actions = self.get_available_actions()
        if not available_actions:
            return None
        return random.choice(available_actions)
    
    def get_user_move_strategy(self):
        return self.action_space.sample()
    
    def get_available_actions(self):
        return list(zip(*np.where(self.board == 0)))
env = TicTacToeEnv()
observation, info = env.reset()
done = False
total_reward = 0
episode = 0
print('Начальное состояние:')
env.render(mode='human')
while not done:
    action = env.get_user_move_strategy()
    print(f'Ход пользователя: {action}')
    
    observation, reward, done, truncated, info = env.step(action)
    env.render()
    
    total_reward += reward
    episode += 1
    
    if done:
        if reward == 1:
            print('Пользователь выиграл!')
        elif reward == -1:
            print('Пользователь проиграл!')
        else:
            print('Ничья!')

print(f'Итоговая награда пользователя: {total_reward}')
# Markdown:
<p class="task" id="4"></p>

4\. Предложите стратегию (в виде алгоритма без использования методов машинного обучения), которая позволит, в среднем, выигрывать в крестики-нолики чаще, чем случайный выбор действия. Реализуйте эту стратегию и сыграйте игру, выбирая действия согласно этой стратегии. Выведите на экран состояние окружения после каждого хода и итоговую награду пользователя за сессию.

- [ ] Проверено на семинаре
from tqdm import tqdm
def get_user_move_strategy(self):
    for move in self.get_available_actions():
        row, col = move
        self.board[row, col] = 1
        if self.check_winner(1):
            self.board[row, col] = 0
            return (row, col)
        self.board[row, col] = 0

    for move in self.get_available_actions():
        row, col = move
        self.board[row, col] = 2
        if self.check_winner(2):
            self.board[row, col] = 0
            return (row, col)
        self.board[row, col] = 0

    if self.board[1, 1] == 0:
        return (1, 1)

    corners = [(0,0), (0,2), (2,0), (2,2)]
    available_corners = [corner for corner in corners if self.board[corner] == 0]
    if available_corners:
        return random.choice(available_corners)

    sides = [(0,1), (1,0), (1,2), (2,1)]
    available_sides = [side for side in sides if self.board[side] == 0]
    if available_sides:
        return random.choice(available_sides)

    return None
env = TicTacToeEnv()
env.get_user_move_strategy = get_user_move_strategy.__get__(env, TicTacToeEnv)
observation, info = env.reset()
done = False
total_reward = 0
episode = 0
while not done:
    action = env.get_user_move_strategy()
    print(f'Ход пользователя: {action}')
    
    observation, reward, done, truncated, info = env.step(action)
    env.render()
    
    total_reward += reward
    episode += 1
    
    if done:
        if reward == 1:
            print('Пользователь выиграл!')
        elif reward == -1:
            print('Пользователь проиграл!')
        else:
            print('Ничья!')

print(f'Итоговая награда пользователя: {total_reward}')
N = 10000
data = {-1: 0, 1: 0}
env = TicTacToeEnv(draw=False)
env.get_user_move_strategy = get_user_move_strategy.__get__(env, TicTacToeEnv)
for i in tqdm(range(N)):
    env.reset()

    observation, info = env.reset()
    done = False
    total_reward = 0
    episode = 0
    
    while not done:
        action = env.get_user_move_strategy()
        
        observation, reward, done, truncated, info = env.step(action)
        env.render()
        
        total_reward += reward
        episode += 1
        
        if done:
            data[reward] += 1
rename_map = {-1: 'проигрыш', 1: 'выигрыш'}
for k, v in data.items():
    print(f'Игры, закончившиеся с результатом {rename_map[k]}: {v} ({v/N}%)')
# Markdown:
<p class="task" id="5"></p>

5\. Создайте окружение `MountainCar-v0`. Проиграйте 10 эпизодов и сохраните на диск файл с записью каждого пятого эпизода. Для записи видео воспользуйтесь обёрткой `RecordVideo`. Вставьте скриншот, на котором видно, что файлы были созданы.

- [ ] Проверено на семинаре
from gymnasium.wrappers import RecordVideo
env = gym.make('MountainCar-v0', render_mode='rgb_array')
env = RecordVideo(env, video_folder='./trash', episode_trigger=lambda x: (x+1) % 5 == 0)
for episode in range(10):
    obs, _ = env.reset()
    done = False
    while not done:
        action = env.action_space.sample()
        obs, reward, done, truncated, info = env.step(action)
        done = done or truncated
env.close()
# Markdown:
![Снимок экрана 2024-12-12 в 10.54.23.png](<attachment:Снимок экрана 2024-12-12 в 10.54.23.png>)


# 08_2_q_learning.ipynb
# Markdown:
#  Q-learning

__Автор задач: Блохин Н.В. (NVBlokhin@fa.ru)__

Материалы:
* Саттон Р.	С.,	Барто Э. Дж. Обучение с подкреплением: Введение. 2-е изд.
* https://gymnasium.farama.org/tutorials/training_agents/blackjack_tutorial/
* https://en.wikipedia.org/wiki/Q-learning
* https://www.baeldung.com/cs/epsilon-greedy-q-learning
* https://pythonprogramming.net/q-learning-reinforcement-learning-python-tutorial/
* https://www.datacamp.com/tutorial/introduction-q-learning-beginner-tutorial
* https://rubikscode.net/2021/07/20/introduction-to-double-q-learning/
* https://gymnasium.farama.org/api/wrappers/misc_wrappers/#gymnasium.wrappers.RecordVideo
# Markdown:
## Задачи для совместного разбора
# Markdown:
1\. Рассмотрите понятие Q-функции, ее применение для формирования политики агента и способов ее создания.
import numpy as np

states = [0, 1, 2]
actions = [0, 1]

q_table = np.random.uniform(0, 100, size=(len(states), len(actions)))
current_state = 0
q_table[current_state]
q_table[current_state].argmax()
states = np.array([
    [0.5, 0.7],
    [1.2, 0.3],
    [-5.2, 0.1],
    [2.0, -3.0],
])

actions = [0, 1]
np.digitize(states[:, 0], bins=[-6, 0, 2])
np.digitize(states, bins=[-6, 0, 4])
import numpy as np

actions = [0, 1]

q_table = np.random.uniform(0, 100, size=(3, 3, len(actions)))
state = [1, 1]
q_table[1, 1]
# Markdown:
## Задачи для самостоятельного решения
# Markdown:
<p class="task" id="1"></p>

1\. Обучите агента для игры в блэкджек (окружение `Blackjack-v1`), используя алгоритм Q-learning. Для создания таблицы Q-функции выясните размеры пространства состояния игры и количество возможных действий игрока и выведите эти значения на экран. Во время обучения несколько раз вычислите статистику за `print_every` последних эпизодов: количество выигранных и проигранных сессий. После завершения обучения визуализируйте полученные данные. Изучите, как выглядит Q-функция (в каких состояниях игрок будет брать карту, в каких - нет). Cыграйте `N=10000` игр, применяя стратегию, выведенную из обученной Q-функции, посчитайте и выведите на экран долю выигранных игр.

Cтратегия для выбора действия:
$$a_{t+1}(s_t) = argmax_aQ(s_t, a)$$

Правило обновления Q-функции:

![q-learning](https://wikimedia.org/api/rest_v1/media/math/render/svg/d247db9eaad4bd343e7882ec546bf3847ebd36d8)

- [x] Проверено на семинаре
from dataclasses import dataclass

@dataclass
class Config:
    discount: float = 0.95
    lr: float = 0.005
    n_episodes: float = 100_000
    print_every: int = 5000
import matplotlib.pyplot as plt
import gymnasium as gym
from tqdm import tqdm

class Agent:
    def __init__(self, env: gym.Env, config: Config) -> None:
        self.env = env
        self.cfg = config
        self._create_q_table()

    def _create_q_table(self):
        # напишите код для создания таблицы Q-функции
        # для окружения Blackjack должен получиться массив 32x11x2x2
        
        self.q_table = np.zeros((32, 11, 2, 2))
        print(self.q_table.shape)

    def get_action(self, state: np.ndarray | tuple) -> int:
        # найдите и верните индекс максимума Q-функции для состояния state
        # обратите внимание, что максимумов может быть несколько
        
        player_sum, dealer_card, ace = state
        return np.argmax(self.q_table[player_sum, dealer_card, ace])

    def update_q_table(
        self,
        state: np.ndarray | tuple,
        new_state: np.ndarray | tuple,
        reward: float, action: int,
        done: bool
    ) -> None:
        # напишите код для обновления Q-функции согласно правилу выше
        # если эпизод закончен, то будущая награда равна 0
        
        p_sum, d_card, ace = state
        n_p_sum, n_d_card, n_ace = new_state
        
        if not done:
            max_future_q = np.max(self.q_table[n_p_sum, n_d_card, n_ace])
        else:
            max_future_q = 0

        self.q_table[p_sum, d_card, ace, action] = (1 - self.cfg.lr) * self.q_table[p_sum, d_card, ace, action] + \
            self.cfg.lr * (reward + self.cfg.discount * max_future_q)

    def run_episode(self) -> float:
        done = False
        state, info = self.env.reset()
        while not done:
            action = self.get_action(state)
            new_state, reward, terminated, truncated, info = self.env.step(action)
            done = terminated or truncated
            self.update_q_table(state, new_state, reward, action, done)
            state = new_state

            if done:
                return reward

    def train(self):
        # допишите код для сбора статистики

        ep_rewards, stats = [], []
        loop = tqdm(range(self.cfg.n_episodes))
        for ep in loop:
            reward = self.run_episode()
            ep_rewards.append(reward)

            if (ep + 1) % self.cfg.print_every == 0:
                win_rate = np.sum(np.array(ep_rewards[-self.cfg.print_every:]) > 0) / self.cfg.print_every
                loss_rate = np.sum(np.array(ep_rewards[-self.cfg.print_every:]) < 0) / self.cfg.print_every
                draw_rate = np.sum(np.array(ep_rewards[-self.cfg.print_every:]) == 0) / self.cfg.print_every
                stats.append((ep + 1, win_rate, loss_rate, draw_rate))

                loop.set_postfix_str(f'Episode {ep + 1}: Win rate {win_rate*100:.2f}, Loss rate {loss_rate*100:.2f}, Draw rate {draw_rate*100:.2f}')
                
        return ep_rewards, stats
env = gym.make('Blackjack-v1')
config = Config()
agent = Agent(env, config)
rewards, stats = agent.train()
stats
episodes, win_rates, loss_rate, draw_rate = zip(*stats)
plt.plot(episodes, win_rates, label='Победы')
plt.plot(episodes, loss_rate, label='Поражения')
plt.plot(episodes, draw_rate, label='Ничьи')
plt.title('Процент')
plt.xlabel('Эпизоды')
plt.ylabel('Доля')
plt.grid(True)
plt.legend()
plt.show()
N = 10000
wins = 0
loop = tqdm(range(N))
for i in loop:
    state, _ = env.reset()
    done = False

    while not done:
        action = agent.get_action(state)
        state, reward, terminated, truncated, _ = env.step(action)
        done = terminated or truncated

    if reward > 0:
        wins += 1
    
    loop.set_postfix_str(f'Победы {wins / (i + 1) * 100:.2f}%')

print(f'Процент побед: {wins / N * 100:.2f}%')
# Markdown:
<p class="task" id="2"></p>

2\. Повторите решение предыдущей задачи, используя алгоритм $\epsilon$-greedy Q-learning. Исследуйте, как гиперпараметры и способ инициализации значений Q-функции влияют на результат.

Cтратегия для выбора действия:
1. Сгенерировать число $p$ из $U(0, 1)$;
2. Если $p < \epsilon$, то выбрать действие случайным образом;
3. В противном случае $a_{t+1}(s_t) = argmax_aQ(s_t, a)$.

Правило обновления Q-функции:
![q-learning](https://wikimedia.org/api/rest_v1/media/math/render/svg/d247db9eaad4bd343e7882ec546bf3847ebd36d8)

- [x] Проверено на семинаре
from dataclasses import dataclass

@dataclass
class Config:
    discount: float = 0.95
    lr: float = 0.005
    n_episodes: float = 1_000_000
    epsilon: float = 1.0
    final_epsilon: float = 0.0
    print_every: int = 5000
class Agent:
    def __init__(self, env: gym.Env, config: Config, table_method='zeros') -> None:
        self.env = env
        self.cfg = config
        self._create_q_table(table_method)
        self.epsilon_decay = (config.epsilon - config.final_epsilon) / config.n_episodes

    def _create_q_table(self, table_method):
        # напишите код для создания таблицы Q-функции
        # для окружения Blackjack должен получиться массив 32x11x2x2
        
        match table_method:
            case 'zeros':
                self.q_table = np.zeros((32, 11, 2, 2))
            case 'uniform':
                self.q_table = np.random.uniform(0, 1, size=(32, 11, 2, 2))
            case 'normal':
                self.q_table = np.random.normal(0, 1, size=(32, 11, 2, 2))

    def get_action(self, state: np.ndarray | tuple) -> int:
        # найдите и верните индекс максимума Q-функции для состояния state
        # обратите внимание, что максимумов может быть несколько
        
        player_sum, dealer_card, ace = state
        p = np.random.uniform(0, 1)

        if p < self.cfg.epsilon:
            action = np.random.choice(self.env.action_space.n)
        else:
            action = np.argmax(self.q_table[player_sum, dealer_card, ace])
        
        return action

    def update_q_table(
        self,
        state: np.ndarray | tuple,
        new_state: np.ndarray | tuple,
        reward: float, action: int,
        done: bool
    ) -> None:
        # напишите код для обновления Q-функции согласно правилу выше
        # если эпизод закончен, то будущая награда равна 0
        
        p_sum, d_card, ace = state
        n_p_sum, n_d_card, n_ace = new_state

        if not done:
            max_future_q = np.max(self.q_table[n_p_sum, n_d_card, n_ace])
        else:
            max_future_q = 0

        self.q_table[p_sum, d_card, ace, action] = (1 - self.cfg.lr) * self.q_table[p_sum, d_card, ace, action] + \
            self.cfg.lr * (reward + self.cfg.discount * max_future_q)
    
    def update_epsilon(self):
        self.cfg.epsilon = max(self.cfg.final_epsilon, self.cfg.epsilon - self.epsilon_decay)

    def run_episode(self) -> float:
        done = False
        state, info = self.env.reset()
        while not done:
            action = self.get_action(state)
            new_state, reward, terminated, truncated, info = self.env.step(action)
            done = terminated or truncated
            self.update_q_table(state, new_state, reward, action, done)
            state = new_state
            self.update_epsilon()

            if done:
                return reward

    def train(self):
        # допишите код для сбора статистики

        ep_rewards, stats = [], []
        loop = tqdm(range(self.cfg.n_episodes))
        for ep in loop:
            reward = self.run_episode()
            ep_rewards.append(reward)

            if (ep + 1) % self.cfg.print_every == 0:
                win_rate = np.sum(np.array(ep_rewards[-self.cfg.print_every:]) > 0) / self.cfg.print_every
                loss_rate = np.sum(np.array(ep_rewards[-self.cfg.print_every:]) < 0) / self.cfg.print_every
                draw_rate = np.sum(np.array(ep_rewards[-self.cfg.print_every:]) == 0) / self.cfg.print_every
                stats.append((ep + 1, win_rate, loss_rate, draw_rate))

                loop.set_postfix_str(f'Episode {ep + 1}: Win rate {win_rate*100:.2f}, Loss rate {loss_rate*100:.2f}, Draw rate {draw_rate*100:.2f}')
                
        return ep_rewards, stats
def draw(stats, label):
    episodes, win_rates, loss_rate, draw_rate = zip(*stats)
    plt.plot(episodes, win_rates, label='Победы')
    plt.plot(episodes, loss_rate, label='Поражения')
    plt.plot(episodes, draw_rate, label='Ничьи')
    plt.title(f'Процент {label}')
    plt.xlabel('Эпизоды')
    plt.ylabel('Доля')
    plt.grid(True)
    plt.legend()
    plt.show()
env = gym.make('Blackjack-v1')
config = Config()
agent = Agent(env, config, table_method='uniform')
rewards, stats = agent.train()
draw(stats, label='uniform')
agent = Agent(env, config, table_method='normal')
rewards, stats = agent.train()
draw(stats, label='normal')
agent = Agent(env, config, table_method='zeros')
rewards, stats = agent.train()
draw(stats, label='zeros')
N = 10000
wins = 0
loop = tqdm(range(N))
for i in loop:
    state, _ = env.reset()
    done = False

    while not done:
        action = agent.get_action(state)
        state, reward, terminated, truncated, _ = env.step(action)
        done = terminated or truncated

    if reward > 0:
        wins += 1
    
    loop.set_postfix_str(f'Победы {wins / (i + 1) * 100:.2f}%')

print(f'Процент побед: {wins / N * 100:.2f}%')


# 08_3_dqn.ipynb
# Markdown:
#  DQN

__Автор задач: Блохин Н.В. (NVBlokhin@fa.ru)__

Материалы:
* Саттон Р.	С.,	Барто Э. Дж. Обучение с подкреплением: Введение. 2-е изд.
* https://en.wikipedia.org/wiki/Q-learning
* https://pythonprogramming.net/q-learning-reinforcement-learning-python-tutorial/
* https://pytorch.org/tutorials/intermediate/reinforcement_q_learning.html
* https://github.com/pylSER/Deep-Reinforcement-learning-Mountain-Car/tree/master
* https://valohai.com/blog/reinforcement-learning-tutorial-basic-deep-q-learning/
# Markdown:
## Задачи для совместного разбора
# Markdown:
1\. Обсудите основные отличия DQN от классических вариантов Q-learning.






# Markdown:
## Задачи для самостоятельного решения
# Markdown:
<p class="task" id="1"></p>

1\. Допишите класс `ReplayMemory` для хранения переходов между состояниями.

- [ ] Проверено на семинаре
from collections import namedtuple, deque
import random

Transition = namedtuple(
    'Transition',
    ('state', 'action', 'next_state', 'reward', 'done')
)

class ReplayMemory(object):
    def __init__(self, capacity):
        """capacity - максимальный размер хранилища"""
        self.capacity = capacity
        self.memory = deque(maxlen=capacity)
    
    def push(self, *args):
        """Сохраняет переход. При нехватке места в хранилище самые старые записи удаляются."""
        self.memory.append(Transition(*args))
    
    def sample(self, batch_size):
        """Возвращает batch_size случайно выбранных переходов"""
        return random.sample(self.memory, batch_size)
    
    def __len__(self):
        return len(self.memory)
# Markdown:
<p class="task" id="2"></p>

2\. Допишите класс `DQN` для моделирования Q-функции.

- [ ] Проверено на семинаре
import torch.nn as nn

class DQN(nn.Module):
    """Нейронная сеть для моделирования Q-функции."""
    def __init__(self, n_observations, n_actions):
        super(DQN, self).__init__()
        self.fc1 = nn.Linear(n_observations, 128)
        self.relu = nn.ReLU()
        self.fc2 = nn.Linear(128, 128)
        self.fc3 = nn.Linear(128, n_actions)

    def forward(self, x):
        """Для каждого состояния должны получать n_actions чисел."""
        x = self.relu(self.fc1(x))
        x = self.relu(self.fc2(x))
        return self.fc3(x)
# Markdown:
<p class="task" id="3"></p>

3\. Допишите классы `PolicyConfig` для настроек политики агента и `Policy` для реализации политики.

- [ ] Проверено на семинаре
from dataclasses import dataclass
import torch as th

@dataclass
class PolicyConfig:
    """Содержит настройки для Policy: размерность пространства наблюдений, кол-во действий,
    устройство, на котором будет располагаться модели; ε и т.д."""
    n_observations: int
    n_actions: int
    device: str = 'cpu'
    epsilon_start: float = 1.0
    epsilon_end: float = 0.05
    epsilon_decay: int = 5000
    gamma: float = 0.99
import numpy as np

class Policy:
    def __init__(self, policy_cfg: PolicyConfig):
        self.policy_network = DQN(policy_cfg.n_observations, policy_cfg.n_actions).to(policy_cfg.device)
        self.target_network = DQN(policy_cfg.n_observations, policy_cfg.n_actions).to(policy_cfg.device)
        self.sync_models()

        self.epsilon = policy_cfg.epsilon_start
        self.epsilon_start = policy_cfg.epsilon_start
        self.epsilon_end = policy_cfg.epsilon_end
        self.epsilon_decay = policy_cfg.epsilon_decay
        self.gamma = policy_cfg.gamma
        self.n_actions = policy_cfg.n_actions
        self.device = policy_cfg.device
        self.steps_done = 0

    def sync_models(self):
        self.target_network.load_state_dict(self.policy_network.state_dict())

    def get_best_action(self, state: th.Tensor) -> int:
        sample = random.random()
        self.steps_done += 1
        
        self.epsilon = max(
            self.epsilon_end,
            self.epsilon_start - (self.epsilon_start - self.epsilon_end) * self.steps_done / self.epsilon_decay
        )
        if sample < self.epsilon:
            return random.randrange(self.n_actions)
        else:
            with th.no_grad():
                return self.policy_network(state).argmax(dim=1).item()

    def save(self, path_policy: str, path_target: str):
        th.save(self.policy_network.state_dict(), path_policy)
        th.save(self.target_network.state_dict(), path_target)

    def load(self, path_policy: str, path_target: str):
        self.policy_network.load_state_dict(th.load(path_policy, map_location=self.device))
        self.target_network.load_state_dict(th.load(path_target, map_location=self.device))
# Markdown:
<p class="task" id="4"></p>

4\. Напишите функцию `plot_metrics`, которая будет использоваться для визуализации процесса обучения: суммарной награды за каждый эпизод и максимальное значение x-координаты машины за эпизод. Для реализации можете воспользоваться `wandb` или любым другим удобным инструментом.

- [ ] Проверено на семинаре
import matplotlib.pyplot as plt

def plot_metrics(episode_rewards, max_x_positions, window=100):
    plt.figure(figsize=(14,6))

    plt.subplot(1, 2, 1)
    rewards = np.array(episode_rewards)
    if len(rewards) >= window:
        rewards_smoothed = np.convolve(rewards, np.ones(window)/window, mode='valid')
        plt.plot(range(window-1, len(rewards)), rewards_smoothed, label=f'Награда (скользящее ср. {window})')
    else:
        plt.plot(rewards, label='Награда')
    plt.title('Награда за эпизод')
    plt.xlabel('Эпизод')
    plt.ylabel('Суммарная награда')
    plt.grid(True)
    plt.legend()

    plt.subplot(1, 2, 2)
    max_x = np.array(max_x_positions)
    if len(max_x) >= window:
        max_x_smoothed = np.convolve(max_x, np.ones(window)/window, mode='valid')
        plt.plot(range(window-1, len(max_x)), max_x_smoothed, label=f'Макс. X (скользящее ср. {window})', color='orange')
    else:
        plt.plot(max_x, label='Макс. X', color='orange')
    plt.title('Максимальное положение за эпизод')
    plt.xlabel('Эпизод')
    plt.ylabel('X координата')
    plt.grid(True)
    plt.legend()

    plt.tight_layout()
    plt.show()
# Markdown:
<p class="task" id="5"></p>

5\. Допишите классы `TrainConfig` для настроек обучения и `Trainer` для реализации процесса обучения.

- [ ] Проверено на семинаре
@dataclass
class TrainConfig:
    """Содержит настройки для процесса обучения: к-т дисконтирования, скорость обучения,
    количество эпизодов для обучения, размер батча и т.д."""
    gamma: float = 0.99
    learning_rate: float = 1e-3
    num_episodes: int = 2500
    batch_size: int = 64
    target_sync: int = 100
    memory_capacity: int = 10000
    max_steps_per_episode: int = 325
import gymnasium as gym
from torch import optim

class Trainer:
    def __init__(self, env: gym.Env, train_config: TrainConfig, policy: Policy):
        self.criterion = nn.MSELoss()
        self.optimizer = optim.Adam(policy.policy_network.parameters(), lr=train_config.learning_rate)

        self.memory = ReplayMemory(train_config.memory_capacity)
        self.env = env
        self.train_config = train_config
        self.policy = policy
        self.episode_rewards = []
        self.max_x_positions = []

    def train(self):
        for episode in range(self.train_config.num_episodes):
            state, _ = self.env.reset()

            state = th.tensor(state, dtype=th.float32, device=self.policy.device).unsqueeze(0)

            total_reward = 0.0
            max_x = -float('inf')

            state, total_reward, max_x = self.run_episode(state)

            self.episode_rewards.append(total_reward)
            self.max_x_positions.append(max_x)

            if episode % 250 == 0 and episode > 0:
                plot_metrics(self.episode_rewards, self.max_x_positions)

            if episode % self.train_config.target_sync == 0 and episode > 0:
                self.policy.sync_models()

            if max_x >= 0.5:
                print(f'Флаг на эпизоде {episode}!')
                self.policy.save('policy_network.pth', 'target_network.pth')

        plot_metrics(self.episode_rewards, self.max_x_positions)

    def run_episode(self, start_state: th.Tensor):
        state = start_state
        total_reward = 0.0
        max_x = -float('inf')

        for t in range(self.train_config.max_steps_per_episode):
            action = self.policy.get_best_action(state)

            next_state, reward, done, truncated, _ = self.env.step(action)

            if next_state[0] >= 0.5:
                reward += 100.0

            total_reward += reward
            max_x = max(max_x, next_state[0])

            next_state_tensor = th.tensor(next_state, dtype=th.float32, device=self.policy.device).unsqueeze(0)

            done_flag = done or truncated
            self.memory.push(state, action, next_state_tensor, reward, done_flag)

            state = next_state_tensor

            self.generate_batch_and_fit()
            
            if done_flag:
                break

        return state, total_reward, max_x

    def generate_batch_and_fit(self):
        if len(self.memory) < self.train_config.batch_size:
            return

        transitions = self.memory.sample(self.train_config.batch_size)
        batch = Transition(*zip(*transitions))

        state_batch = th.cat(batch.state)
        action_batch = th.tensor(batch.action, dtype=th.long, device=self.policy.device).unsqueeze(1)
        reward_batch = th.tensor(batch.reward, dtype=th.float32, device=self.policy.device).unsqueeze(1)
        next_state_batch = th.cat(batch.next_state)
        done_batch = th.tensor(batch.done, dtype=th.float32, device=self.policy.device).unsqueeze(1)

        current_q_values = self.policy.policy_network(state_batch).gather(1, action_batch)

        with th.no_grad():
            next_q_values = self.policy.target_network(next_state_batch).max(1)[0].unsqueeze(1)
        expected_q_values = reward_batch + (self.train_config.gamma * next_q_values * (1 - done_batch))

        self.fit_policy_network(current_q_values, expected_q_values)

    def fit_policy_network(self, current_q_values, expected_q_values):
        self.optimizer.zero_grad()
        loss = self.criterion(current_q_values, expected_q_values)
        loss.backward()
        self.optimizer.step()
# Markdown:
<p class="task" id="6"></p>

6\. Настройте модель для управления машиной в окружении `MountainCar-v0`. Для преобразования векторов состояний в тензоры используйте обертку `TransformObservation`. Выведите на экран график с информацией о процессе обучения. При необходимости вставьте скриншоты этих графиков.

- [ ] Проверено на семинаре
from gymnasium.wrappers import TransformObservation, RecordVideo
env = gym.make('MountainCar-v0', render_mode='rgb_array')
env = RecordVideo(env, episode_trigger=lambda x: x%25== 0, video_folder='videos')
env = TransformObservation(env, lambda obs: obs, observation_space=env.observation_space)
n_observations = env.observation_space.shape[0]
n_actions = env.action_space.n

policy_cfg = PolicyConfig(
    n_observations=n_observations,
    n_actions=n_actions,
    device='cpu',
    epsilon_start=1.0,
    epsilon_end=0.05,
    epsilon_decay=5000,
    gamma=0.99
)
policy = Policy(policy_cfg)

train_cfg = TrainConfig(
    gamma=0.99,
    learning_rate=1e-3,
    num_episodes=2500,
    batch_size=64,
    target_sync=100,
    memory_capacity=10000,
    max_steps_per_episode=325
)
trainer = Trainer(env, train_cfg, policy)
trainer.train()
env.close()
plot_metrics(trainer.episode_rewards, trainer.max_x_positions)
# Markdown:
<video controls src="rl-video-episode-2350.mp4" title="Title"></video>

