{
 "cells": [
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": "### Основы pytorch",
   "id": "b8550dcda0eb831d"
  },
  {
   "metadata": {},
   "cell_type": "code",
   "source": [
    "import pandas as pd\n",
    "import torch\n",
    "torch.__version__"
   ],
   "id": "680006de7376bb65",
   "outputs": [],
   "execution_count": null
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": "Убедитесь, что Вам удалось подключить cuda, это легко проверить по версии `torch`, которая отображается выше. Должно быть что-то вроде `2.11.0+cu128`, а не `2.11.0+cpu`. Данную лабораторную работу можно выполнять и на CPU, но в дальнейшем нам понадобится ускорение GPU",
   "id": "aaa0a276ade8e580"
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": "Теперь рассмотрим частые преобразования pytorch связанные с изображениями. В первую очередь загрузим датасет MNIST и возьмем первый элемент",
   "id": "bf442ab3c4dd50c3"
  },
  {
   "metadata": {},
   "cell_type": "code",
   "source": [
    "from torchvision import datasets, transforms\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "transform = transforms.Compose([\n",
    "    transforms.ToTensor()\n",
    "])\n",
    "\n",
    "mnist_dataset = datasets.MNIST(root=\"./data\", download=True, transform=transform)\n",
    "\n",
    "img, label = mnist_dataset[0]\n",
    "\n",
    "plt.imshow(img.squeeze().numpy(), cmap=\"gray\")  # что происходит тут пока магия, как раз с этим и разберемся\n",
    "plt.title(f\"Label: {label}\")\n",
    "plt.show()"
   ],
   "id": "ba233583ec0dbd5a",
   "outputs": [],
   "execution_count": null
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": "Посмотрим на размер и форму изображения-тензора",
   "id": "c91d92b341f6eb6e"
  },
  {
   "metadata": {},
   "cell_type": "code",
   "source": "img.shape",
   "id": "f07d56a575461f2c",
   "outputs": [],
   "execution_count": null
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": "Теперь давайте возьмем не одно изображение из датасета, а сразу 4 изображения. Сделать это можно через `.data[0:4]`. И снова посмотрим, какую размерность и форму имеет полученный тензор.",
   "id": "1478d3176bf78a31"
  },
  {
   "metadata": {},
   "cell_type": "code",
   "source": "batch = ... # место для кода",
   "id": "85a65ae1464945d0",
   "outputs": [],
   "execution_count": null
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": [
    "Мы имеем здесь тензор в формате `BHW`, то есть трехмерный тензор, где\n",
    "\n",
    "первая координата `B` обозначает номер изображения в батче (batch),\n",
    "\n",
    "вторая координата `H` обозначает номер строки, то есть координату по высоте (height),\n",
    "\n",
    "третья координата `W` обозначает номер столбца, то есть координату по ширине (width)."
   ],
   "id": "900f9b879b0bee2f"
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": "Теперь используем операцию `.squeeze()` на тензоре img и посмотрим, какая размерность теперь у полученного тензора. Что сделала эта операция? А если использовать `.unsqueeze()`?",
   "id": "b6ce8e691cc29d7a"
  },
  {
   "metadata": {},
   "cell_type": "code",
   "source": "",
   "id": "482a37dc7529a8c3",
   "outputs": [],
   "execution_count": null
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": "Теперь заметим, что для того, чтобы быть классическим изображением нашему тензору не хватает каналов. Возьмите батч величиной 4 из датасета, и преобразуйте его к виду `BCHW`, где `C` -- количество каналов изображения. В нашем случае давайте сделаем трехканальное изображение (все каналы дублируют друг друга). Можете использовать методы `.repeat()` и `.expand()`. В комментариях объясните, в чем разница.",
   "id": "469491bac6780bcf"
  },
  {
   "metadata": {},
   "cell_type": "code",
   "source": [
    "# место для кода\n",
    "print(batch.shape)"
   ],
   "id": "c879058873f12c48",
   "outputs": [],
   "execution_count": null
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": "На самом деле не всегда нам нужен формат `BCHW`. В отдельных случаях, особенно при работе с `opencv`, может понадобиться формат `HWC`. Возьмите первый элемент батча и преобразуйте его из `CHW` в `HWC`. Можете использовать `.permute()` и `.view()`. В чем разница? Снова опишите в комментариях",
   "id": "8a5f4bea86c0f6e6"
  },
  {
   "metadata": {},
   "cell_type": "code",
   "source": [
    "# место для кода\n",
    "print(batch.shape)"
   ],
   "id": "a856f3eb7cc8c03",
   "outputs": [],
   "execution_count": null
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": "Но и это еще не все! Иногда нам может понадобиться (для того же `opencv`) представить изображение не в `RGB` кодировке, а в `BGR`. То есть нам нужно поменять местами каналы в изображении. Давайте это проделаем и получим изображение в формате `HWC` в кодировка `BGR`.",
   "id": "839bf9d2c3099913"
  },
  {
   "metadata": {},
   "cell_type": "code",
   "source": [
    "img_bgr = ...\n",
    "print(img_bgr.shape)"
   ],
   "id": "cfb4509c7282c737",
   "outputs": [],
   "execution_count": null
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": "А еще иногда нужно растянуть изображение в один вектор. Сделать это можно с помощью `.flatten()`, `.reshape()`, `.view()`.",
   "id": "e8332c2d72a8fe19"
  },
  {
   "metadata": {},
   "cell_type": "code",
   "source": [
    "img_flattened = ...\n",
    "print(img_flattened.shape)"
   ],
   "id": "1228746fa1f8dedd",
   "outputs": [],
   "execution_count": null
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": "### Нелинейная классификация",
   "id": "87b9430ef35d600"
  },
  {
   "metadata": {},
   "cell_type": "code",
   "source": [
    "import numpy as np\n",
    "\n",
    "def make_circles(\n",
    "    n_per_class: int = 500,\n",
    "    r_inner: float = 1.0,\n",
    "    r_outer: float = 2.5,\n",
    "    noise_std: float = 0.25,\n",
    "    seed: int = 42,\n",
    ") -> tuple[np.ndarray, np.ndarray]:\n",
    "    \"\"\"\n",
    "    Генерирует два класса точек: внутренний круг (класс 0) и внешний круг (класс 1), каждый со своим радиусом и гауссовым шумом.\n",
    "\n",
    "    Parameters\n",
    "    ----------\n",
    "    n_per_class : Число точек на каждый класс.\n",
    "    r_inner :     Радиус внутреннего круга.\n",
    "    r_outer :     Радиус внешнего круга.\n",
    "    noise_std :   Стандартное отклонение аддитивного шума по каждой оси.\n",
    "    seed :        Зерно ГСЧ для воспроизводимости.\n",
    "\n",
    "    Returns\n",
    "    -------\n",
    "    X : np.ndarray, shape (2 * n_per_class, 2)\n",
    "        Координаты точек.\n",
    "    y : np.ndarray, shape (2 * n_per_class,)\n",
    "    \"\"\"\n",
    "    rng = np.random.default_rng(seed)\n",
    "\n",
    "    # углы равномерно на [0, 2\\pi)\n",
    "    theta = rng.uniform(0, 2 * np.pi, size=n_per_class)\n",
    "\n",
    "    # радиусы с шумом\n",
    "    r1 = r_inner + rng.normal(0, noise_std, size=n_per_class)\n",
    "    r2 = r_outer + rng.normal(0, noise_std, size=n_per_class)\n",
    "\n",
    "    # полярные → декартовы координаты\n",
    "    X1 = np.column_stack([r1 * np.cos(theta), r1 * np.sin(theta)])\n",
    "    X2 = np.column_stack([r2 * np.cos(theta), r2 * np.sin(theta)])\n",
    "\n",
    "    X = np.vstack([X1, X2])\n",
    "    y = np.array([0] * n_per_class + [1] * n_per_class)\n",
    "\n",
    "    # перемешиваем\n",
    "    perm = rng.permutation(len(X))\n",
    "    return X[perm], y[perm]"
   ],
   "id": "2e2335b3ca2a8274",
   "outputs": [],
   "execution_count": null
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": "Используйте функцию выше для генерации датасета на 1000 датапоинтов. И нарисуйте их с помощью `plt.scatter`. Можете",
   "id": "c8452c8042809bba"
  },
  {
   "metadata": {},
   "cell_type": "code",
   "source": "X, y = ...",
   "id": "a65dcd9f962f8036",
   "outputs": [],
   "execution_count": null
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": "Конвертируем теперь полученный массив в тензор (через torch.tensor). Разделять на тренировочную и валидационную выборку не нужно. Проверьте, что `X_tensor` и `y_tensor` имеют одинаковую размерность по первой оси",
   "id": "2b6e7b65e6d0012d"
  },
  {
   "metadata": {},
   "cell_type": "code",
   "source": [
    "X_tensor = ...\n",
    "y_tensor = ..."
   ],
   "id": "8071e24b91f57b61",
   "outputs": [],
   "execution_count": null
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": "Теперь определим полносвязную нейронную сеть. Не добавляйте слишком много слоев (два будет достаточно), в качестве промежуточных функций активации используйте `ReLU`, в качестве выходной -- `sigmiod`",
   "id": "f1bfd9d53329390b"
  },
  {
   "metadata": {},
   "cell_type": "code",
   "source": [
    "import torch.nn as nn\n",
    "\n",
    "\n",
    "class FCN(nn.Module):\n",
    "    def __init__(self, input_dim: int, hidden_dim: int, output_dim: int = 1):\n",
    "        super().__init__()\n",
    "        # место для кода\n",
    "\n",
    "    def forward(self, x):\n",
    "        return # место для кода\n",
    "\n",
    "model = ...\n",
    "print(model)"
   ],
   "id": "2789412173ca19e6",
   "outputs": [],
   "execution_count": null
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": "Определим функцию потерь (бинарную кросс энтропию), оптимизатор (SGD). А затем запустим обучение с `batch_size=32`.",
   "id": "7f22b2b0fb98c629"
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": "Вариант для CPU",
   "id": "c065d73ef2df188c"
  },
  {
   "metadata": {},
   "cell_type": "code",
   "source": [
    "criterion = nn.BCELoss()\n",
    "optimizer = torch.optim.SGD(model.parameters(), lr=1e-3)\n",
    "batch_size = 32\n",
    "\n",
    "for epoch in range(200):\n",
    "    model.train()\n",
    "    for i in range(0, X_tensor.shape[0], batch_size):\n",
    "        x_batch = X_tensor[i:i + batch_size]\n",
    "        y_batch = y_tensor[i:i + batch_size]\n",
    "        pred = model(x_batch)\n",
    "        loss = criterion(pred, y_batch)\n",
    "\n",
    "        optimizer.zero_grad()\n",
    "        loss.backward()\n",
    "        optimizer.step()\n",
    "\n",
    "    if (epoch + 1) % 50 == 0:\n",
    "        print(f\"Epoch {epoch+1:3d} | loss = {loss.item():.4f}\")"
   ],
   "id": "5dc847473eb15394",
   "outputs": [],
   "execution_count": null
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": "Вариант для GPU/TPU",
   "id": "b9acf5caa37248e8"
  },
  {
   "metadata": {},
   "cell_type": "code",
   "outputs": [],
   "execution_count": null,
   "source": [
    "device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n",
    "\n",
    "model = model.to(device)\n",
    "X_tensor = X_tensor.to(device)\n",
    "y_tensor = y_tensor.to(device)\n",
    "\n",
    "criterion = nn.BCELoss()\n",
    "optimizer = torch.optim.SGD(model.parameters(), lr=1e-3)\n",
    "batch_size = 32\n",
    "\n",
    "for epoch in range(200):\n",
    "    model.train()\n",
    "    for i in range(0, X_tensor.shape[0], batch_size):\n",
    "        x_batch = X_tensor[i:i + batch_size]\n",
    "        y_batch = y_tensor[i:i + batch_size]\n",
    "        pred = model(x_batch)\n",
    "        loss = criterion(pred, y_batch)\n",
    "\n",
    "        optimizer.zero_grad()\n",
    "        loss.backward()\n",
    "        optimizer.step()\n",
    "\n",
    "    if (epoch + 1) % 50 == 0:\n",
    "        print(f\"Epoch {epoch+1:3d} | loss = {loss.item():.4f}\")\n",
    "\n",
    "model = model.to('cpu')"
   ],
   "id": "c6399ddaeb0f837e"
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": "Разберите построчно, что происходит в предыдущих ячейках. А также сравните время выполнения, насколько быстрее происходит обучение на GPU/TPU",
   "id": "4b12136fc97c281c"
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": "Теперь давайте визуализируем результаты обучения. Для начала составим шкалы признаков с помощью `torch.linspace`. Не берите слишком большое количество шаговб `steps=100` будет достаточно. Какая размерность у полученных тензоров?",
   "id": "d058afef02bb916"
  },
  {
   "metadata": {},
   "cell_type": "code",
   "source": [
    "x1s = ...\n",
    "x2s = ..."
   ],
   "id": "2b3df7d59759cb88",
   "outputs": [],
   "execution_count": null
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": "Теперь постройте декартово произведение этих множеств с помощью `torch.meshgrid`. Какая размерность у него? Какая нужна нам для передачи в модель? Подсказка, итоговая размерность должна быть заканчиваться на 2.",
   "id": "d75fc7fd833deaea"
  },
  {
   "metadata": {},
   "cell_type": "code",
   "source": "x1x1, x2x2 = ...",
   "id": "5a646e10b6ac9ad0",
   "outputs": [],
   "execution_count": null
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": "Далее используем `torch.stack`. Какая теперь размерность у тензора? Используем еще `.reshape()` для того, чтобы привести тензор к нужному размеру.",
   "id": "fad81cf8cdeaf32f"
  },
  {
   "metadata": {},
   "cell_type": "code",
   "source": "grid = ...",
   "id": "66a080862c773c74",
   "outputs": [],
   "execution_count": null
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": "Теперь, когда у нас есть тестовый тензор. Посчитаем на нем вероятность принадлежности каждой пары к положительному классу. Делается это просто. Но для визуализации еще нужно преобразовать тензор вероятностей к размерности `(step, step)`",
   "id": "3fc0bc1da6da7d18"
  },
  {
   "metadata": {},
   "cell_type": "code",
   "source": "probs = ...",
   "id": "73b88bc9dc20de22",
   "outputs": [],
   "execution_count": null
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": "Далее, для того, чтобы визуализировать результаты, наши тензоры нужно преобразовать к `np` массивам.",
   "id": "2dc1546983e49aba"
  },
  {
   "metadata": {},
   "cell_type": "code",
   "source": [
    "x1x1_np = x1x1.numpy()\n",
    "x2x2_np = x2x2.numpy()\n",
    "probs_np = probs.detach().numpy()"
   ],
   "id": "958b003956ecb014",
   "outputs": [],
   "execution_count": null
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": "Теперь с помощью `plt` изобразите на одном полотне точки датасета, тепловую карту вероятностей и разделяющую поверхность (с пороговым значением вероятности 0.5)",
   "id": "15568b0eacc5099a"
  },
  {
   "metadata": {},
   "cell_type": "code",
   "source": "# место для кода",
   "id": "96b68547bdf596bd",
   "outputs": [],
   "execution_count": null
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": "### Самостоятельное решение задачи",
   "id": "6bc0f7ddf21ed0a"
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": "Проделайте тоже самое с датасетом из файла `lab1.csv`. Архитектуру нейросети оставьте полносвязной, но можете выбирать функции активации, количество слоев, количество нейронов в слоях. Для зачета необходимо добиться точности не менее `0.7`",
   "id": "a8a87ef0567c289c"
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2026-09-08T10:57:22.876599283Z",
     "start_time": "2026-09-08T10:57:22.226425662Z"
    }
   },
   "cell_type": "code",
   "source": [
    "import pandas as pd\n",
    "\n",
    "df = pd.read_csv(\"lab1.csv\")"
   ],
   "id": "ca0359fc3a41335a",
   "outputs": [],
   "execution_count": 2
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2026-09-08T10:58:26.942141381Z",
     "start_time": "2026-09-08T10:58:25.457156030Z"
    }
   },
   "cell_type": "code",
   "source": [
    "from sklearn.model_selection import train_test_split\n",
    "\n",
    "train_df, test_df = train_test_split(df, test_size=0.1, random_state=42)"
   ],
   "id": "9242284b0a13a36a",
   "outputs": [],
   "execution_count": 4
  },
  {
   "metadata": {},
   "cell_type": "code",
   "source": "",
   "id": "bdaa417954317cc",
   "outputs": [],
   "execution_count": null
  },
  {
   "metadata": {},
   "cell_type": "markdown",
   "source": "**Соревнование**. Куда же без него. Тот студент, кто сможет добиться наилучшей точности на `test_df` при наименьшем количестве параметров нейросети, получит дополнительные 5 баллов. Также 4 балла за второе место и 3 балла за третье. Метрика, по которой необходимо оценивать нейросеть ниже",
   "id": "82b2f2a2edbb67b7"
  },
  {
   "metadata": {},
   "cell_type": "code",
   "source": [
    "import math\n",
    "\n",
    "def score(acc, n_params):\n",
    "    \"\"\"\n",
    "    Метрика для оценки модели в конкурсе.\n",
    "\n",
    "    :param acc: Точность по всем классам.\n",
    "    :param n_params: Количество параметров нейросети.\n",
    "    \"\"\"\n",
    "    return acc - 0.01 * math.log1p(n_params)"
   ],
   "id": "32efee0ff7e0efef",
   "outputs": [],
   "execution_count": null
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 2
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython2",
   "version": "2.7.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
