{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "monthly-marriage",
   "metadata": {},
   "source": [
    "# Обучение нейросетей: Базовые приемы"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "tender-private",
   "metadata": {},
   "outputs": [],
   "source": [
    "!curl -O 'https://courses.cv-gml.ru/storage/seminars/nn-training-basics/dataset.zip'"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "speaking-frank",
   "metadata": {},
   "outputs": [],
   "source": [
    "!unzip -qo dataset.zip"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "electric-fellow",
   "metadata": {},
   "outputs": [],
   "source": [
    "import glob\n",
    "import os\n",
    "import random\n",
    "\n",
    "import matplotlib.pyplot as plt\n",
    "import numpy as np\n",
    "import PIL.Image\n",
    "import torch\n",
    "import torch.nn.functional as F\n",
    "from torch import nn\n",
    "from tqdm.auto import tqdm\n",
    "\n",
    "# Increase these if figures appear small\n",
    "plt.rcParams[\"figure.figsize\"] = fx, fy = (14.08, 6.40)\n",
    "\n",
    "# Not using `bfloat16` matrix multiplication for consistency\n",
    "# You might get better performance without much precision loss\n",
    "# by setting this to \"medium\" on some devices\n",
    "torch.set_float32_matmul_precision(\"high\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "residential-confidentiality",
   "metadata": {},
   "outputs": [],
   "source": [
    "def show_images(images, titles=[]):\n",
    "    num = len(images)\n",
    "    fig, axs = plt.subplots(nrows=1, ncols=num, squeeze=True, layout=\"constrained\")\n",
    "    axs = [axs] if num <= 1 else axs\n",
    "    for i in range(num):\n",
    "        ax = axs[i]\n",
    "        ax.imshow(images[i])\n",
    "        ax.axis(\"off\")\n",
    "        if titles != []:\n",
    "            ax.set_title(titles[i])\n",
    "\n",
    "    plt.show(fig)\n",
    "    plt.close(fig)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "de082173",
   "metadata": {},
   "source": [
    "## Обучение простого классификатора на PyTorch"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "hearing-beijing",
   "metadata": {},
   "outputs": [],
   "source": [
    "CLASSES = {\"cloudy\": 0, \"foggy\": 1, \"rainy\": 2, \"shine\": 3, \"sunrise\": 4}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "understanding-pastor",
   "metadata": {},
   "outputs": [],
   "source": [
    "weather_images = []\n",
    "titles = []\n",
    "for weather in CLASSES.keys():\n",
    "    sample = sorted(glob.glob(f\"./dataset/{weather}/*\")).pop(0)\n",
    "    weather_images.append(PIL.Image.open(sample))\n",
    "    titles.append(weather)\n",
    "show_images(weather_images, titles)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7ff69c7b",
   "metadata": {},
   "source": [
    "На прошлом семинаре мы работали с микро-выборкой из 100 синтетических сэмплов и обучали нейросеть **сразу на всей этой выборке**. В большинстве случаев на практике, обучающие выборки содержат значительно больше 100 эталонных экземпляров. Как следствие, обучение с использованием всех данных может не помещаться в память ускорителей (VRAM).\n",
    "\n",
    "В связи с этим, на практике обычно используется метод [стохастического градиентного спуска](https://en.wikipedia.org/wiki/Stochastic_gradient_descent). В данной модификации, истинные значения градиентов на каждом шаге приближаются используя только подмножество данных из датасета. Это подмножество называется пакетом, мини-батчем или батчем (batch).\n",
    "\n",
    "При этом, у нас может появится желание формировать каждый батч по определенному принципу (например каждый раз создавать батч из слуачйно выбранных объектов выборки), автоматически предобрабатывать наши данные перед обучением и многое другое.\n",
    "\n",
    "В худшем случае, может быть что сами данные (или даже просто мета-данные) целиком не помещаются в оперативную память (RAM) и приходится организовывать динамическую загрузку данных с диска."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "fa68bcf4",
   "metadata": {},
   "outputs": [],
   "source": [
    "NETWORK_SIZE = (64, 64)\n",
    "BATCH_SIZE = 16\n",
    "\n",
    "if torch.cuda.is_available():\n",
    "    DEVICE = torch.device(\"cuda\")\n",
    "    print(\"Using the GPU 😊\")\n",
    "else:\n",
    "    DEVICE = torch.device(\"cpu\")\n",
    "    print(\"Using the CPU 😞\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "991225b1",
   "metadata": {},
   "source": [
    "Для таких нужд PyTorch предоставляет нам модуль `torch.utils.data`.\n",
    "\n",
    "**`torch.utils.data.Dataset`** - интерфейс, который позволяет энкапсулировать логику загрузки и подготовки экземпляров выборки. При наследовании от класса `Dataset` необходимо реализовать два метода: `__len__` возвращающий количество экземпляров в датасете и `__getitem__` загружающий и подготавливающий один экземпляр по его индексу.\n",
    "\n",
    "Давайте реализуем свой подкласс `Dataset` для картинок с погодой. Также, давайте сразу заложим в данный класс функционал для разбиения всех имеющихся у нас данных на обучающее (train) и валидационное (valid) подмножества."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "prospective-folks",
   "metadata": {},
   "outputs": [],
   "source": [
    "from torch.utils import data\n",
    "\n",
    "\n",
    "class MyCustomDataset(data.Dataset):\n",
    "    def __init__(\n",
    "        self,\n",
    "        mode,\n",
    "        transform,\n",
    "        root_dir=\"./dataset\",\n",
    "        train_fraction=0.8,\n",
    "        split_seed=42,\n",
    "    ):\n",
    "        # We can't store all the images in memory at the same time,\n",
    "        # because sometimes we have to work with very large datasets\n",
    "        # so we will only store data paths.\n",
    "\n",
    "        paths = []\n",
    "        labels = []\n",
    "        rng = random.Random(split_seed)\n",
    "        for cls_name, cls_idx in CLASSES.items():\n",
    "\n",
    "            # Make sure that train and validation splits\n",
    "            # use the same (random) order of samples\n",
    "            cls_paths = sorted(glob.glob(f\"{root_dir}/{cls_name}/*\"))\n",
    "            split = int(train_fraction * len(cls_paths))\n",
    "            rng.shuffle(cls_paths)\n",
    "\n",
    "            if mode == \"train\":\n",
    "                cls_paths = cls_paths[:split]\n",
    "            elif mode == \"valid\":\n",
    "                cls_paths = cls_paths[split:]\n",
    "            else:\n",
    "                raise RuntimeError(f\"Invalid mode: {mode!r}\")\n",
    "\n",
    "            paths.extend(cls_paths)\n",
    "            labels.extend(cls_idx for _ in range(len(cls_paths)))\n",
    "\n",
    "        self._transform = transform\n",
    "        self._len = len(paths)\n",
    "        self._paths = paths\n",
    "        self._labels = np.array(labels)\n",
    "        assert self._labels.shape == (self._len,)\n",
    "\n",
    "    def __len__(self):\n",
    "        return len(self._paths)\n",
    "\n",
    "    def __getitem__(self, index):\n",
    "        img_path = self._paths[index]\n",
    "        label = self._labels[index]\n",
    "\n",
    "        # Read the image from disk\n",
    "        image = np.array(PIL.Image.open(img_path).convert(\"RGB\"))\n",
    "\n",
    "        # Apply the desired transformations\n",
    "        image = self._transform(image)\n",
    "\n",
    "        return image, label"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7a1e785a",
   "metadata": {},
   "source": [
    "Чтобы не реализовывать самостоятельно код для предобработки изображений, будем использовать модуль `transforms.v2` из библиотеки `torchvision`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6f2c82e1",
   "metadata": {},
   "outputs": [],
   "source": [
    "import torchvision.transforms.v2 as T\n",
    "\n",
    "IMAGENET_MEAN = [0.485, 0.456, 0.406]\n",
    "IMAGENET_STD = [0.229, 0.224, 0.225]\n",
    "DEFAULT_TRANSFORM = T.Compose(\n",
    "    [\n",
    "        T.ToImage(),\n",
    "        T.ToDtype(torch.float32, scale=True),\n",
    "        T.Resize(size=NETWORK_SIZE),\n",
    "        T.Normalize(mean=IMAGENET_MEAN, std=IMAGENET_STD),\n",
    "    ]\n",
    ")\n",
    "\n",
    "# Prepare the data\n",
    "ds_train = MyCustomDataset(mode=\"train\", transform=DEFAULT_TRANSFORM)\n",
    "ds_valid = MyCustomDataset(mode=\"valid\", transform=DEFAULT_TRANSFORM)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "inner-atlantic",
   "metadata": {},
   "source": [
    "Имея экземпляр `Dataset` класса можно обернуть его в **`DataLoader`** - уже реализованный в PyTorch класс, который достает сэмплы из указанного `Dataset`а и подготавливает из них батчи. Этот класс также реализует в себе часто используемый функционал включая перемешивание, параллельную загрузку в несколько процессов (`num_workers`) и предзагрузку данных (`prefetch`).\n",
    "\n",
    "Один полный проход по всему датасету обычно<sup>†</sup> называется **эпохой**.\n",
    "\n",
    "<br/>\n",
    "<div style=\"font-size: 0.75em\">\n",
    "\n",
    "†: На практике, слово \"эпоха\" не имеет строгого определения и может использоваться для обозначения слегка разных периодов времени (проход по датасету, интервал между валидациями, интервал между понижением темпа обучения, итп).\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "48cf7709",
   "metadata": {},
   "outputs": [],
   "source": [
    "dl_train = data.DataLoader(\n",
    "    ds_train,\n",
    "    batch_size=BATCH_SIZE,\n",
    "    shuffle=True,\n",
    "    drop_last=True,\n",
    "    num_workers=os.cpu_count(),\n",
    ")\n",
    "dl_valid = data.DataLoader(\n",
    "    ds_valid,\n",
    "    batch_size=BATCH_SIZE,\n",
    "    shuffle=False,\n",
    "    drop_last=False,\n",
    "    num_workers=os.cpu_count(),\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "01aab127",
   "metadata": {},
   "source": [
    "Будем использовать простую сверточную архитектуру"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "harmful-participation",
   "metadata": {},
   "outputs": [],
   "source": [
    "class MyModel(nn.Sequential):\n",
    "    def __init__(self, num_classes):\n",
    "        super().__init__()\n",
    "\n",
    "        self.conv1 = nn.Conv2d(3, 6, kernel_size=5)\n",
    "        self.relu1 = nn.ReLU()\n",
    "        self.pool1 = nn.MaxPool2d(kernel_size=2)\n",
    "\n",
    "        self.conv2 = nn.Conv2d(6, 16, kernel_size=3)\n",
    "        self.relu2 = nn.ReLU()\n",
    "        self.pool2 = nn.MaxPool2d(kernel_size=2)\n",
    "\n",
    "        self.conv3 = nn.Conv2d(16, 5, kernel_size=3, padding=1)\n",
    "        self.relu3 = nn.ReLU()\n",
    "        self.pool3 = nn.MaxPool2d(kernel_size=2)\n",
    "\n",
    "        # (N, 5, 7, 7) -> (N, 5 * 7 * 7 == 245)\n",
    "        self.flatten = nn.Flatten(start_dim=1, end_dim=-1)\n",
    "\n",
    "        self.fc1 = nn.Linear(5 * 7 * 7, 120)\n",
    "        self.relu4 = nn.ReLU()\n",
    "\n",
    "        self.fc2 = nn.Linear(120, 84)\n",
    "        self.relu5 = nn.ReLU()\n",
    "\n",
    "        self.fc3 = nn.Linear(84, num_classes)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3ad66eee",
   "metadata": {},
   "source": [
    "#### Дизайн / выбор архитектуры\n",
    "\n",
    "<table style=\"font-size: 2rem;\"><thead>\n",
    "  <tr>\n",
    "    <td style=\"border-right: solid 1px;\"></td>\n",
    "    <th style=\"text-align: center;\">inference (production)</th>\n",
    "    <th style=\"text-align: center;\">training (development)</th>\n",
    "  </tr></thead>\n",
    "<tbody>\n",
    "  <tr>\n",
    "    <th style=\"border-right: solid 1px;\">скорость работы<br/>(в FLOPs, FPS или seconds/sample)</th>\n",
    "    <td style=\"text-align: center;\">на целевом девайсе, с использованием<br/>inference-specific оптимизаций</td>\n",
    "    <td style=\"text-align: center;\">на девайсе для обучения, время<br/>необходимое на проведение экспериментов</td>\n",
    "  </tr>\n",
    "  <tr>\n",
    "    <th style=\"border-right: solid 1px;\">количество параметров<br/>(в штуках или байтах)</th>\n",
    "    <td style=\"text-align: center;\">размер приложения на диске,<br/>скорость загрузки на девайс</td>\n",
    "    <td style=\"text-align: center;\">репрезентативная способность модели,<br/>склонность к переобучению</td>\n",
    "  </tr>\n",
    "  <tr>\n",
    "    <th style=\"border-right: solid 1px;\">пиковое потребление памяти<br/>(в байтах или % от максимума)</th>\n",
    "    <td style=\"text-align: center;\">возможность \"уместиться\" на целевом девайсе,<br/>возможность параллельной обработки</td>\n",
    "    <td style=\"text-align: center;\">максимальный размер батча,<br/>требования к девайсу для обучения</td>\n",
    "  </tr>\n",
    "</tbody>\n",
    "</table>\n",
    "\n",
    "Все 6 пунктов - важны. Каждый из этих пунктов нетривиально зависит от выбора гиперпараметров и элементов дизайна архитектуры. В частности, обратите внимание, что все 6 пунктов в общем случае независимы и не обязаны коррелировать друг с другом (то есть они могут увеличиваться/уменьшаться независимо друг от друга). "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5d1431b5",
   "metadata": {},
   "source": [
    "---"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ba8c6d51",
   "metadata": {},
   "source": [
    "Тривиальный цикл для обучения на PyTorch (см. прошлый семинар)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "876d13dc",
   "metadata": {},
   "outputs": [],
   "source": [
    "def init_adam_simple(parameters):\n",
    "    optimizer = torch.optim.Adam(parameters, lr=1e-3)\n",
    "\n",
    "    def _on_step():\n",
    "        optimizer.step()\n",
    "        optimizer.zero_grad()\n",
    "\n",
    "    def _on_epoch():\n",
    "        # No-Op for now\n",
    "        pass\n",
    "\n",
    "    return _on_step, _on_epoch\n",
    "\n",
    "\n",
    "def simple_train_step(model, loss_fn, x_batch, y_batch):\n",
    "    p_batch = model(x_batch)\n",
    "    loss = loss_fn(p_batch, y_batch)\n",
    "    return loss\n",
    "\n",
    "\n",
    "def train(\n",
    "    num_epochs,\n",
    "    dl_train,\n",
    "    dl_valid,\n",
    "    loss_cls=torch.nn.CrossEntropyLoss,\n",
    "    configure_optimizer=init_adam_simple,\n",
    "    train_step=simple_train_step,\n",
    "):\n",
    "    # Prepare model and optimization stuff\n",
    "    model = MyModel(num_classes=len(CLASSES)).to(DEVICE)\n",
    "    loss_fn = loss_cls().to(DEVICE)\n",
    "    on_step, on_epoch = configure_optimizer(model.parameters())\n",
    "\n",
    "    # The main loop\n",
    "    for e in range(num_epochs):\n",
    "\n",
    "        # Train for a single epoch\n",
    "        model = model.train()\n",
    "        train_loss = []\n",
    "        progress_train = tqdm(\n",
    "            total=len(dl_train),\n",
    "            desc=f\"Epoch {e}\",\n",
    "            leave=False,\n",
    "        )\n",
    "        for x_batch, y_batch in dl_train:\n",
    "            x_batch = x_batch.to(DEVICE)\n",
    "            y_batch = y_batch.to(DEVICE)\n",
    "\n",
    "            loss = train_step(model, loss_fn, x_batch, y_batch)\n",
    "            train_loss.append(loss.detach())\n",
    "            loss.backward()\n",
    "\n",
    "            # Do stuff once per batch/step\n",
    "            on_step()\n",
    "            progress_train.update()\n",
    "\n",
    "        # Do stuff once per epoch\n",
    "        on_epoch()\n",
    "        progress_train.close()\n",
    "\n",
    "        train_loss = torch.stack(train_loss).mean()\n",
    "        print(\n",
    "            f\"Epoch {e},\",\n",
    "            f\"train_loss: {train_loss.item():.8f}\",\n",
    "        )\n",
    "\n",
    "        # Measure metrics on validation\n",
    "        model = model.eval()\n",
    "        valid_accs = []\n",
    "        progress_valid = tqdm(\n",
    "            total=len(dl_valid),\n",
    "            desc=f\"Epoch {e}\",\n",
    "            leave=False,\n",
    "        )\n",
    "        for x_batch, y_batch in dl_valid:\n",
    "            x_batch = x_batch.to(DEVICE)\n",
    "            y_batch = y_batch.to(DEVICE)\n",
    "\n",
    "            with torch.no_grad():\n",
    "                p_batch = model(x_batch)\n",
    "\n",
    "            matches = p_batch.argmax(axis=-1) == y_batch\n",
    "            valid_accs.append(matches)\n",
    "\n",
    "            progress_valid.update()\n",
    "        progress_valid.close()\n",
    "\n",
    "        valid_accs = torch.concat(valid_accs).float().mean()\n",
    "        print(\n",
    "            f\"Epoch {e},\",\n",
    "            f\"valid_accs: {valid_accs.item():.8f}\",\n",
    "        )\n",
    "\n",
    "    return model"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6061bf9e",
   "metadata": {},
   "outputs": [],
   "source": [
    "model = train(num_epochs=3, dl_train=dl_train, dl_valid=dl_valid)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4adce297",
   "metadata": {},
   "source": [
    "Сохранение и загрузка параметров"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "62b55b06",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Get all of the current model parameters\n",
    "sd = model.state_dict()\n",
    "print(list(sd.keys()))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4aaa4b70",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(sd[\"conv1.bias\"])\n",
    "print()\n",
    "print(model.conv1.bias)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "86cccca6",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Save the parameters to disk\n",
    "torch.save(sd, \"model.pt\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7190a84f",
   "metadata": {},
   "outputs": [],
   "source": [
    "# This is WRONG!\n",
    "if False:\n",
    "    torch.save(model, \"dont_do_this.pt\")\n",
    "\n",
    "# This \"kind of\" works, but it pickles the whole MyModel\n",
    "# object itself, instead of only saving the parameters.\n",
    "# This is almost never what you actually want."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "12f7ff7b",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Re-create the model object from scratch\n",
    "model = MyModel(num_classes=len(CLASSES))\n",
    "\n",
    "# Load the saved parameters\n",
    "#\n",
    "# WARNING!\n",
    "# By default, torch.load uses pickle which\n",
    "# can cause arbitrary code execution.\n",
    "#\n",
    "# NEVER use torch.load without explicitly setting\n",
    "# weights_only=True. Alternatively, consider using\n",
    "# safetensors when loading parameters from\n",
    "# unknown/untrusted sources.\n",
    "sd = torch.load(\n",
    "    \"model.pt\",\n",
    "    map_location=DEVICE,\n",
    "    weights_only=True,\n",
    ")\n",
    "model.load_state_dict(sd);"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "110338a9",
   "metadata": {},
   "source": [
    "# Расписание learning rate\n",
    "\n",
    "До данного момента времени, при создании оптимизатора мы заранее выбирали константное значение $\\alpha$ задающее длину шага / темп обучения в методах оптимизации.\n",
    "\n",
    "В теории, существуют теоремы доказывающие сходимость градиентных методов оптимизации с константным шагом. Например, для SGD: [1](http://www.machinelearning.ru/wiki/index.php?title=Метод_градиентного_спуска#.D0.A1.D1.85.D0.BE.D0.B4.D0.B8.D0.BC.D0.BE.D1.81.D1.82.D1.8C_.D0.B3.D1.80.D0.B0.D0.B4.D0.B8.D0.B5.D0.BD.D1.82.D0.BD.D0.BE.D0.B3.D0.BE_.D1.81.D0.BF.D1.83.D1.81.D0.BA.D0.B0_.D1.81_.D0.BF.D0.BE.D1.81.D1.82.D0.BE.D1.8F.D0.BD.D0.BD.D1.8B.D0.BC_.D1.88.D0.B0.D0.B3.D0.BE.D0.BC), [2](https://contest.yandex.ru/tracks/ml/optimization-in-machine-learning/sgd-convergence).\n",
    "\n",
    "Однако, данные теоремы верны только при выполнении определенных глобальных условий относительно гладкости и выпуклости оптимизируемой функции и конкретного выбора параметра $\\alpha$. Чем \"хуже\" оптимизируемая функция, тем меньше требуется $\\alpha$ для обеспечения сходимости. С другой стороны, чем меньше $\\alpha$, тем больше шагов потребуется выполнить для достижения минимума.\n",
    "\n",
    "Таким образом, мы получаем конфликт: с одной стороны мы хотим выбрать $\\alpha$ побольше, чтобы делать большие шаги и быстро \"дойти\" до минимума; с другой стороны при слишком больших значениях $\\alpha$ методы оптимизации могут плохо сходиться или даже расходиться.\n",
    "\n",
    "В связи с этим, достаточно часто константное значение $\\alpha$ заменяют на так называемое \"расписание\" $\\alpha\\left(step\\right)$ изменяющееся в процессе обучения. В классическом машинном обучении достаточно часто используют следующее правило:\n",
    "\n",
    "  $$\\alpha(step) = \\alpha_0\\ {\\cdot}\\ \\frac{1}{1 + \\gamma{\\cdot}step}$$\n",
    "\n",
    "Т.к. при $step \\to \\infty$ значение $\\frac{1}{step} \\to 0$, то начиная с какого-то шага значение $\\alpha \\sim \\frac{1}{step}$ станет достаточно маленьким, чтобы обеспечить сходимость. При этом, известно что $\\sum \\frac{1}{step} \\to \\infty$ и следовательно данный алгоритм оптимизации в теории может \"дойти\" до любой точки пространства.\n",
    "\n",
    "Данные свойства - достаточно красивые в теории, но в контексте современного глубокого машинного обучения не очень актуальны. На практике, $\\sum \\frac{1}{step} \\sim ln\\left(step\\right)$ растест слишком медленно. С другой стороны, для любого расписания $\\alpha\\left(step\\right) \\to 0$, даже если $\\sum \\alpha\\left(step\\right)$ ограничена сверху, мы всегда можем оценить / подбрать такое адекватное значение $\\alpha\\left(0\\right)$, чтобы $\\sum \\alpha\\left(step\\right)$ было \"достаточно большим\"."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4e18161d",
   "metadata": {},
   "source": [
    "## Расписания lr в современном ML\n",
    "\n",
    "Для изменения темпа обучения по расписанию есть модуль `torch.optim.lr_scheduler`. Для использования данного модуля нужно обернуть наш `optimizer` в один из подклассов `torch.optim.lr_scheduler.LRScheduler` и затем периодически (в зависимости от желаемого расписания) вызывать `lr_scheduler.step()`:\n",
    "\n",
    "- либо в конце каждой эпохи\n",
    "\n",
    "<!-- -->\n",
    "\n",
    "- либо после каждого шага оптимизатора (`optimizer.step()`)\n",
    "\n",
    "Давайте рассмотрим некоторые часто используемые в современном ML виды расписаний."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ca13fd14",
   "metadata": {},
   "outputs": [],
   "source": [
    "lr_history = []\n",
    "lr_history_plots = {}\n",
    "\n",
    "\n",
    "def show_lr_plots(focus):\n",
    "    for k, v in lr_history_plots.items():\n",
    "        alpha = 1.0 if k == focus else 0.3\n",
    "        plt.plot(v, label=k, alpha=alpha)\n",
    "    plt.legend()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5bfa47a7",
   "metadata": {},
   "source": [
    "#### Smooth exponential decay\n",
    "\n",
    "$$\\alpha(step) = \\alpha_0\\ {\\cdot}\\ {\\gamma}^{step} \\qquad (0 < \\gamma < 1)$$"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4b08c429",
   "metadata": {},
   "outputs": [],
   "source": [
    "def init_adam_with_batch_exponential(parameters):\n",
    "    optimizer = torch.optim.Adam(parameters, lr=1e-3)\n",
    "    lr_scheduler = torch.optim.lr_scheduler.ExponentialLR(\n",
    "        optimizer,\n",
    "        # multiply the LR by 0.995 every step\n",
    "        gamma=0.995,\n",
    "    )\n",
    "\n",
    "    def _on_step():\n",
    "        # For plots\n",
    "        lr_history.append(lr_scheduler.get_last_lr())\n",
    "\n",
    "        optimizer.step()\n",
    "        optimizer.zero_grad()\n",
    "        # The LR scheduler must be called\n",
    "        # performed **after** the optimizer\n",
    "        lr_scheduler.step()\n",
    "\n",
    "    def _on_epoch():\n",
    "        pass\n",
    "\n",
    "    return _on_step, _on_epoch"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b5a13812",
   "metadata": {},
   "outputs": [],
   "source": [
    "lr_history.clear()\n",
    "model = train(\n",
    "    num_epochs=6,\n",
    "    dl_train=dl_train,\n",
    "    dl_valid=dl_valid,\n",
    "    configure_optimizer=init_adam_with_batch_exponential,\n",
    ")\n",
    "lr_history_plots[\"batch_exponential\"] = np.array(lr_history).squeeze(-1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b3a9eb24",
   "metadata": {},
   "outputs": [],
   "source": [
    "show_lr_plots(\"batch_exponential\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8c45890f",
   "metadata": {},
   "source": [
    "#### Stepwise exponential decay\n",
    "\n",
    "$$\\alpha(step) = \\alpha_0\\ {\\cdot}\\ {\\gamma}^{\\left\\lfloor\\frac{step}{interval}\\right\\rfloor} \\qquad (\\text{e.g. }\\ \\gamma = 0.1,\\ \\ interval \\propto epoch)$$"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "29ff418a",
   "metadata": {},
   "outputs": [],
   "source": [
    "def init_adam_with_epoch_stepwise(parameters):\n",
    "    optimizer = torch.optim.Adam(parameters, lr=1e-3)\n",
    "    lr_scheduler = torch.optim.lr_scheduler.StepLR(\n",
    "        optimizer,\n",
    "        # multiply the LR by 0.1 every 2 epochs\n",
    "        gamma=0.1,\n",
    "        step_size=2,\n",
    "    )\n",
    "\n",
    "    def _on_step():\n",
    "        lr_history.append(lr_scheduler.get_last_lr())\n",
    "        optimizer.step()\n",
    "        optimizer.zero_grad()\n",
    "\n",
    "    def _on_epoch():\n",
    "        # You can call the LR scheduler once per epoch\n",
    "        # or equivalently call it once per batch and set\n",
    "        #     step_size=2 * len(dl_train)\n",
    "        lr_scheduler.step()\n",
    "\n",
    "    return _on_step, _on_epoch"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "328be6de",
   "metadata": {},
   "outputs": [],
   "source": [
    "lr_history.clear()\n",
    "model = train(\n",
    "    num_epochs=6,\n",
    "    dl_train=dl_train,\n",
    "    dl_valid=dl_valid,\n",
    "    configure_optimizer=init_adam_with_epoch_stepwise,\n",
    ")\n",
    "lr_history_plots[\"epoch_stepwise\"] = np.array(lr_history).squeeze(-1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "46bc3c46",
   "metadata": {},
   "outputs": [],
   "source": [
    "show_lr_plots(\"epoch_stepwise\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e14778b5",
   "metadata": {},
   "source": [
    "### Cosine Annealing Learning Rate, Warm Restarts\n",
    "\n",
    "В [данной статье](https://arxiv.org/abs/1608.03983v5) предлагается две идеи:\n",
    "\n",
    "1) Cosine Annealing Learning Rate: понижать $\\alpha \\sim cos\\left(\\pi{\\cdot}\\frac{step}{max\\_step}\\right)$"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c70448b2",
   "metadata": {},
   "outputs": [],
   "source": [
    "def init_adam_with_cosine_annealing(parameters):\n",
    "    optimizer = torch.optim.Adam(parameters, lr=1e-3)\n",
    "    lr_scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(\n",
    "        optimizer,\n",
    "        # 6 epochs\n",
    "        T_max=6 * len(dl_train),\n",
    "    )\n",
    "\n",
    "    def _on_step():\n",
    "        lr_history.append(lr_scheduler.get_last_lr())\n",
    "        optimizer.step()\n",
    "        optimizer.zero_grad()\n",
    "        lr_scheduler.step()\n",
    "\n",
    "    def _on_epoch():\n",
    "        pass\n",
    "\n",
    "    return _on_step, _on_epoch"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e238294c",
   "metadata": {},
   "outputs": [],
   "source": [
    "lr_history.clear()\n",
    "model = train(\n",
    "    num_epochs=6,\n",
    "    dl_train=dl_train,\n",
    "    dl_valid=dl_valid,\n",
    "    configure_optimizer=init_adam_with_cosine_annealing,\n",
    ")\n",
    "lr_history_plots[\"cosine_annealing\"] = np.array(lr_history).squeeze(-1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "49fb7a47",
   "metadata": {},
   "outputs": [],
   "source": [
    "show_lr_plots(\"cosine_annealing\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5cf28128",
   "metadata": {},
   "source": [
    "2) Warm Restarts: несколько раз после конца одного \"спуска\" / расписания возвращаться к исходному значению $\\alpha$ и затем повторять спуск ещё раз (но уже начиная с \"warm\" / уже как-то обученных параметров из прошлого \"спуска\")."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0b999af4",
   "metadata": {},
   "outputs": [],
   "source": [
    "def init_adam_with_cosine_annealing_warm_restarts(parameters):\n",
    "    optimizer = torch.optim.Adam(parameters, lr=1e-3)\n",
    "    lr_scheduler = torch.optim.lr_scheduler.CosineAnnealingWarmRestarts(\n",
    "        optimizer,\n",
    "        # Warm Restart once every 2 epochs\n",
    "        T_0=2 * len(dl_train),\n",
    "    )\n",
    "\n",
    "    def _on_step():\n",
    "        lr_history.append(lr_scheduler.get_last_lr())\n",
    "        optimizer.step()\n",
    "        optimizer.zero_grad()\n",
    "        lr_scheduler.step()\n",
    "\n",
    "    def _on_epoch():\n",
    "        pass\n",
    "\n",
    "    return _on_step, _on_epoch"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6fc112a9",
   "metadata": {},
   "outputs": [],
   "source": [
    "lr_history.clear()\n",
    "model = train(\n",
    "    num_epochs=6,\n",
    "    dl_train=dl_train,\n",
    "    dl_valid=dl_valid,\n",
    "    configure_optimizer=init_adam_with_cosine_annealing_warm_restarts,\n",
    ")\n",
    "lr_history_plots[\"cosine_annealing_warm_restarts\"] = np.array(lr_history).squeeze(-1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1d305014",
   "metadata": {},
   "outputs": [],
   "source": [
    "show_lr_plots(\"cosine_annealing_warm_restarts\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bbd94535",
   "metadata": {},
   "source": [
    "Также, идею warm restarts можно развить, сохраняя параметры в конце каждого из периодов обучения. Тогда, полученный набор моделей можно использовать как ансамбль или даже усреднять их веса для получения одной мета-модели. Данная техника называется Stochastic Weight Averaging и была предложена [в данной статье](https://arxiv.org/abs/1803.05407)."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "aa294735",
   "metadata": {},
   "source": [
    "---"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5f1bdbb9",
   "metadata": {},
   "source": [
    "# Методы регуляризации обучения"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "billion-lightweight",
   "metadata": {},
   "source": [
    "## Аугментация данных\n",
    "\n",
    "<img src=\"https://courses.cv-gml.ru/storage/seminars/nn-training-basics/albumentations_logo.png\" width=\"50%\">"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "three-devices",
   "metadata": {},
   "source": [
    "Albumentations - это Python библиотека для быстрого и гибкого аугментирования изображений. Albumentations эффективно реализует широкий спектр операций преобразования изображений и обеспечивает при этом лаконичный, но мощный интерфейс для различных задач компьютерного зрения, включая классификацию, сегментацию и детекцию."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "printable-geneva",
   "metadata": {},
   "source": [
    "### Пример аугментаций из Albumentations:"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "TODAY-cRN570",
   "metadata": {},
   "source": [
    "![](https://courses.cv-gml.ru/storage/seminars/nn-training-basics/albumentations_1.png)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "scheduled-pavilion",
   "metadata": {},
   "outputs": [],
   "source": [
    "import albumentations as A"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "universal-episode",
   "metadata": {},
   "outputs": [],
   "source": [
    "MyTransform = A.Compose(\n",
    "    [\n",
    "        A.RandomResizedCrop(size=(128, 128), p=0.3),\n",
    "        A.Rotate(limit=30),\n",
    "        A.HorizontalFlip(p=0.3),\n",
    "        A.RandomBrightnessContrast(brightness_limit=0.3, contrast_limit=0.3, p=0.5),\n",
    "    ]\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "beautiful-beginning",
   "metadata": {},
   "outputs": [],
   "source": [
    "show_images(weather_images, titles)\n",
    "for _ in range(4):\n",
    "    transformed = [\n",
    "        MyTransform(image=np.array(image))[\"image\"]\n",
    "        for image in weather_images\n",
    "        #\n",
    "    ]\n",
    "    show_images(transformed)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5e31bd3c",
   "metadata": {},
   "outputs": [],
   "source": [
    "MyTransform = A.Compose(\n",
    "    [\n",
    "        A.SmallestMaxSize(max_size=160),\n",
    "        A.Affine(translate_percent=0.05, scale=(1.0, 1.05), rotate=15, p=0.5),\n",
    "        A.RandomCrop(height=128, width=128),\n",
    "        A.RGBShift(r_shift_limit=15, g_shift_limit=15, b_shift_limit=15, p=0.5),\n",
    "        A.RandomBrightnessContrast(brightness_limit=0.3, contrast_limit=0.3, p=0.5),\n",
    "        A.HorizontalFlip(p=0.3),\n",
    "    ]\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "244885e8",
   "metadata": {},
   "outputs": [],
   "source": [
    "show_images(weather_images, titles)\n",
    "for _ in range(4):\n",
    "    transformed = [\n",
    "        MyTransform(image=np.array(image))[\"image\"]\n",
    "        for image in weather_images\n",
    "        #\n",
    "    ]\n",
    "    show_images(transformed)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "logical-output",
   "metadata": {},
   "source": [
    "#### Ковариантное преобразование изображений и меток"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "M5xzCPbnx_1h",
   "metadata": {},
   "source": [
    "![](https://courses.cv-gml.ru/storage/seminars/nn-training-basics/albumentations_2.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "confirmed-guard",
   "metadata": {},
   "source": [
    "------"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "EQrOZ7EKyXWo",
   "metadata": {},
   "source": [
    "Добавим аугментации в обучение"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e455d2ab",
   "metadata": {},
   "outputs": [],
   "source": [
    "import albumentations.pytorch.transforms\n",
    "\n",
    "augmentations = [\n",
    "    A.SmallestMaxSize(max_size=160),\n",
    "    A.Affine(translate_percent=0.05, scale=(1.0, 1.05), rotate=15, p=0.5),\n",
    "    A.RandomCrop(height=128, width=128),\n",
    "    A.RGBShift(r_shift_limit=15, g_shift_limit=15, b_shift_limit=15, p=0.5),\n",
    "    A.RandomBrightnessContrast(brightness_limit=0.3, contrast_limit=0.3, p=0.5),\n",
    "    A.HorizontalFlip(p=0.3),\n",
    "]\n",
    "# Don't forget to prepare the image data for consumption by PyTorch\n",
    "common_transforms = [\n",
    "    A.Resize(*NETWORK_SIZE),\n",
    "    A.ToFloat(max_value=255),\n",
    "    A.Normalize(max_pixel_value=1.0, mean=IMAGENET_MEAN, std=IMAGENET_STD),\n",
    "    A.pytorch.transforms.ToTensorV2(),\n",
    "]\n",
    "\n",
    "MyTrainTransform = A.Compose(augmentations + common_transforms)\n",
    "MyValidTransform = A.Compose(common_transforms)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "according-pharmacy",
   "metadata": {},
   "outputs": [],
   "source": [
    "def my_train_transform(image):\n",
    "    return MyTrainTransform(image=image)[\"image\"]\n",
    "\n",
    "\n",
    "def my_valid_transform(image):\n",
    "    return MyValidTransform(image=image)[\"image\"]\n",
    "\n",
    "\n",
    "ds_train_alb = MyCustomDataset(\n",
    "    mode=\"train\",\n",
    "    transform=my_train_transform,\n",
    ")\n",
    "ds_valid_alb = MyCustomDataset(\n",
    "    mode=\"valid\",\n",
    "    transform=my_valid_transform,\n",
    ")\n",
    "\n",
    "dl_train_alb = data.DataLoader(\n",
    "    ds_train_alb,\n",
    "    batch_size=BATCH_SIZE,\n",
    "    shuffle=True,\n",
    "    drop_last=True,\n",
    "    num_workers=os.cpu_count(),\n",
    ")\n",
    "dl_valid_alb = data.DataLoader(\n",
    "    ds_valid_alb,\n",
    "    batch_size=BATCH_SIZE,\n",
    "    shuffle=False,\n",
    "    drop_last=False,\n",
    "    num_workers=os.cpu_count(),\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "regulation-monitoring",
   "metadata": {},
   "outputs": [],
   "source": [
    "model = train(num_epochs=3, dl_train=dl_train_alb, dl_valid=dl_valid_alb)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dffad066",
   "metadata": {},
   "source": [
    "## Регуляризующие аугментации меток"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b1c7eef7",
   "metadata": {},
   "source": [
    "### MixUp\n",
    "\n",
    "Попробуем использовать более сложную аугментацию из статьи [MixUp: Beyond Empirical Risk Minimization](https://arxiv.org/abs/1710.09412).\n",
    "\n",
    "MixUp - взвешенное усреднение двух изображений с соответствующим изменением one-hot таргета. Брать пары изображений можно внутри одного батча."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c7f5a3da",
   "metadata": {},
   "source": [
    "![](https://courses.cv-gml.ru/storage/seminars/nn-training-advanced/mixup.png)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f927d548",
   "metadata": {},
   "outputs": [],
   "source": [
    "def mixup_data(x, y, alpha=1.0):\n",
    "    lam = np.random.beta(alpha, alpha)\n",
    "\n",
    "    batch_size = x.size()[0]\n",
    "    index = torch.randperm(batch_size, device=x.device)\n",
    "\n",
    "    mixed_x = lam * x + (1 - lam) * x[index, ...]\n",
    "    y_a, y_b = y, y[index]\n",
    "\n",
    "    return mixed_x, y_a, y_b, lam"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7a777d8d",
   "metadata": {},
   "outputs": [],
   "source": [
    "show_images(weather_images, titles)\n",
    "for _ in range(2):\n",
    "    transformed = [\n",
    "        MyTransform(image=np.array(image))[\"image\"]\n",
    "        for image in weather_images\n",
    "        #\n",
    "    ]\n",
    "    transformed = torch.from_numpy(np.array(transformed))\n",
    "    transformed, _, _, _ = mixup_data(transformed, torch.zeros(5))\n",
    "    transformed = transformed.numpy().round().clip(0, 255).astype(np.uint8)\n",
    "    show_images(transformed)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3c8d05ca",
   "metadata": {},
   "outputs": [],
   "source": [
    "def mixup_loss_fn(loss_fn, p, y_a, y_b, lam):\n",
    "    loss_a = loss_fn(p, y_a)\n",
    "    loss_b = loss_fn(p, y_b)\n",
    "\n",
    "    loss = lam * loss_a + (1 - lam) * loss_b\n",
    "\n",
    "    # Or alternatively for smooth data,\n",
    "    # you can keep the loss function and\n",
    "    # mix the labels in mixup_data instead\n",
    "    #\n",
    "    # mixed_y = lam * onehot(y_a) + (1-lam) * onehot(y_b)\n",
    "    # loss = loss_fn(p, mixed_y)\n",
    "    return loss\n",
    "\n",
    "\n",
    "# Mix-up is performed in TRAIN ONLY\n",
    "def mixup_train_step(model, loss_fn, x, y):\n",
    "    x_mixed, y_a, y_b, lam = mixup_data(x, y)\n",
    "\n",
    "    p = model(x_mixed)\n",
    "\n",
    "    return mixup_loss_fn(loss_fn, p, y_a, y_b, lam)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c0060177",
   "metadata": {},
   "outputs": [],
   "source": [
    "model = train(\n",
    "    num_epochs=3,\n",
    "    dl_train=dl_train_alb,\n",
    "    dl_valid=dl_valid_alb,\n",
    "    train_step=mixup_train_step,\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d8ab1498",
   "metadata": {},
   "source": [
    "Другие похожие способы аугментаций:\n",
    "* [CutMix](https://arxiv.org/abs/1905.04899)\n",
    "* [FMix](https://arxiv.org/abs/2002.12047)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "668a6291",
   "metadata": {},
   "source": [
    "### Label smoothing"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0324f30d",
   "metadata": {},
   "source": [
    "Модель машинного обучения называется \"откалиброванной\" (calibrated), если выдаваемая ею вероятность отражает ее качество. Пусть у нас есть 100 примеров, на каждый модель выдает вероятность 0.9. Тогда, если модель \"откалибрована\", то ровно 90 примеров должны быть классифицированы корректно.\n",
    "\n",
    "Предсказания \"откалиброванной\" модели легче интерпретируются, к такой модели проще подбирать пороги и легче добавлять ее в ансамбль.\n",
    "\n",
    "Чересчур \"уверенная\" модель (overconfident) склонна выдавать очень высокие скоры. Побороть этот эффект помогает label smoothing. Он может быть использован в связке с softmax+cross-enropy loss. Метод заключается в том, чтобы преобразовать таргет по следующей формуле:\n",
    "\n",
    "$$y_{ls} = (1 - α) \\cdot y_{hot}\\ +\\ α \\cdot\\frac{1}{K}$$\n",
    "\n",
    "$y_{hot}$ - one-hot представление таргета, $K$ - число классов, $α$ - гиперпараметр, задающий силу сглаживания (обычному представлению таргета соответствует $α$ = 0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e739f32e",
   "metadata": {},
   "outputs": [],
   "source": [
    "class LabelSmoothingCrossEntropyLoss(nn.Module):\n",
    "    def __init__(self, num_classes=len(CLASSES), smoothing=0.01):\n",
    "        assert 0 <= smoothing < 1\n",
    "\n",
    "        super().__init__()\n",
    "        self.smoothing = smoothing\n",
    "        self.num_classes = num_classes\n",
    "\n",
    "    def forward(self, p, y):\n",
    "\n",
    "        with torch.no_grad():\n",
    "            K = self.num_classes\n",
    "            alpha = self.smoothing\n",
    "            y_one_hot = F.one_hot(y, num_classes=K)\n",
    "\n",
    "            smooth_y = (1 - alpha) * y_one_hot + (alpha / K)\n",
    "\n",
    "        loss = F.cross_entropy(p, smooth_y)\n",
    "        return loss"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ef771e44",
   "metadata": {},
   "outputs": [],
   "source": [
    "model = train(\n",
    "    num_epochs=3,\n",
    "    dl_train=dl_train_alb,\n",
    "    dl_valid=dl_valid_alb,\n",
    "    loss_cls=LabelSmoothingCrossEntropyLoss,\n",
    ")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "burning-donna",
   "metadata": {},
   "source": [
    "---"
   ]
  }
 ],
 "metadata": {},
 "nbformat": 4,
 "nbformat_minor": 5
}