PyTorch: основные инструменты и структуры для создания нейросетей

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

1. Тензоры, устройства и основы автодифференцирования

Тензоры, устройства и основы автодифференцирования

Эта статья — старт курса: разберём базовые строительные блоки PyTorch, без которых невозможно уверенно писать нейросетевые модели. Вы научитесь:

  • Создавать и менять тензоры (главный тип данных PyTorch)
  • Управлять устройствами (CPU/GPU) и переносить данные между ними
  • Понимать автодифференцирование (autograd): откуда берутся градиенты и как PyTorch их считает
  • Официальная документация, на которую будем опираться:

  • PyTorch Tensors
  • torch (основные операции)
  • Autograd mechanics
  • CUDA semantics
  • Тензоры как основной тип данных

    Тензор — это обобщение вектора и матрицы на произвольное число измерений. В PyTorch тензор хранит:

  • Данные (числа)
  • Форму (shape) — размеры по осям
  • Тип данных (dtype) — например float32, int64
  • Устройство (device) — CPU или GPU
  • В нейросетях тензоры используются для входов, весов, выходов и градиентов.

    Создание тензоров

    Чаще всего тензоры создают из Python-структур или фабричными функциями.

    | Задача | Пример | Комментарий | |---|---|---| | Из списка | torch.tensor([1, 2, 3]) | Тип выводится автоматически | | Нули | torch.zeros(2, 3) | Форма (2, 3) | | Единицы | torch.ones(2, 3) | Удобно для масок и инициализаций | | Случайные значения | torch.randn(2, 3) | Нормальное распределение | | Диапазон | torch.arange(0, 10) | Аналог range, но тензор | | Как другой тензор | torch.zeros_like(x) | Сохраняет shape, dtype, device |

    Пример:

    Форма (shape) и размерности

    В PyTorch принято хранить батч данных первым измерением. Например:

  • Изображения: (batch, channels, height, width)
  • Тексты (последовательности): часто (batch, length, features)
  • Полезные операции:

  • x.ndim — число измерений
  • x.shape — форма
  • x.numel() — количество элементов
  • x.view(...) или x.reshape(...) — изменить форму (при совместимости)
  • Пример изменения формы:

    Тип данных (dtype)

    dtype влияет на:

  • Точность вычислений
  • Потребление памяти
  • Совместимость операций
  • Частые типы:

  • torch.float32 — стандарт для обучения
  • torch.float16 / torch.bfloat16 — ускорение на GPU (подробнее будет в темах про оптимизацию)
  • torch.int64 — часто для индексов и меток классов
  • Задать dtype можно при создании или преобразовать:

    Индексация и срезы

    Индексация похожа на NumPy:

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

    Broadcasting (автоматическое расширение размеров)

    Broadcasting позволяет выполнять операции над тензорами разных форм, если размеры совместимы. Упрощённая идея:

  • Если измерение равно 1, его можно «растянуть» под другое
  • Если измерений не хватает, слева мысленно добавляются 1
  • Пример: прибавим смещение (bias) к каждой строке:

    Операции in-place и почему с ними аккуратно

    Операции in-place меняют тензор на месте (обычно имеют суффикс _), например add_, relu_.

    Они экономят память, но могут мешать автодифференцированию, если PyTorch нужно сохранить промежуточные значения для градиентов.

    Пример:

    Устройства: CPU и GPU

    У каждого тензора есть device. Основные варианты:

  • cpu
  • cuda:0, cuda:1, ... — видеокарты NVIDIA (если доступно)
  • Проверка доступности GPU:

    Перенос тензоров на нужное устройство

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

    Также можно переносить и менять dtype одной командой:

    Практические советы по устройствам

  • Держите модель и данные на одном устройстве (иначе получите ошибку про Expected all tensors to be on the same device)
  • Перенос CPU→GPU — относительно дорогая операция; старайтесь не делать .to("cuda") внутри тесного цикла без необходимости
  • Для отладки проще начинать на CPU, потом переносить на GPU
  • Основы автодифференцирования (autograd)

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

    Идея:

  • Вы выполняете вычисления и получаете результат (например, loss)
  • PyTorch строит граф вычислений из операций над тензорами
  • Когда вы вызываете backward(), PyTorch проходит по графу в обратном направлении и вычисляет производные
  • !Иллюстрация того, как PyTorch строит граф и распространяет градиенты назад

    requires_grad и градиенты

    Если вы хотите, чтобы PyTorch считал градиенты по тензору, нужно включить флаг requires_grad=True.

    Что здесь происходит математически:

  • Мы задали , где — число, а — результат
  • Производная показывает, как меняется , если немного изменить
  • Для функции производная равна
  • При получаем , что и оказалось в x.grad
  • Важно: backward() требует, чтобы итог был скаляром (одним числом). Если результат — вектор/матрица, обычно сводят к скаляру через mean() или sum().

    Граф вычислений и grad_fn

    Тензоры, полученные в результате операций, обычно имеют поле grad_fn — ссылку на функцию/узел графа, который создал этот тензор.

    Накопление градиентов и обнуление

    Градиенты в PyTorch накапливаются в .grad. Если вы несколько раз вызовете backward(), градиенты суммируются.

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

    Ключевой факт на будущее:

  • .grad не сбрасывается автоматически
  • Отключение вычисления градиентов: no_grad и detach

    Иногда градиенты не нужны: например, при валидации модели или при подготовке данных.

    torch.no_grad() отключает построение графа внутри блока:

    detach() создаёт новый тензор, который разделяет те же данные, но не участвует в графе:

    Практическое различие:

  • no_grad удобен как режим выполнения кода
  • detach удобен, когда нужно «отрезать» конкретный тензор от графа
  • Частые ошибки новичков

  • Смешивание CPU и GPU тензоров в одной операции
  • Неожиданное накопление градиентов из-за отсутствия обнуления .grad
  • Использование in-place операций в местах, где autograd должен хранить значения для backward
  • Попытка вызвать backward() на не-скаляре без сведения к скаляру
  • Мини-чеклист перед тем как писать модель

  • Понимаю shape входов и выходов (и что такое батч)
  • Контролирую dtype (обычно float32 для входов и весов, int64 для индексов)
  • Все тензоры и модель на одном device
  • Понимаю, где нужны/не нужны градиенты (requires_grad, no_grad, detach)
  • В следующей статье логично перейти к nn.Module, слоям (nn.Linear, nn.Conv2d), контейнерам (nn.Sequential) и тому, как параметры модели связываются с autograd.

    2. Построение моделей через nn.Module и слои

    Построение моделей через nn.Module и слои

    В прошлой статье вы разобрали тензоры, устройства и autograd: как PyTorch строит граф вычислений и считает градиенты. Теперь перейдём к тому, как упаковывать вычисления в модель: из слоёв, с параметрами, удобным переносом на GPU и сохранением.

    Ключевая идея: в PyTorch модель почти всегда является объектом класса nn.Module, внутри которого лежат другие модули (слои), а вычисления задаются методом forward.

    Официальные источники:

  • torch.nn
  • torch.nn.Module
  • torch.nn.functional
  • Saving and loading models
  • Что такое nn.Module

    nn.Module — базовый класс для любой модели и любого слоя в PyTorch. Он решает несколько практических задач, без которых писать и обучать модели неудобно:

  • Регистрация параметров: всё, что является nn.Parameter и лежит как атрибут модуля, автоматически попадает в model.parameters().
  • Регистрация подмодулей: если вы присваиваете слой в self.something = nn.Linear(...), то он становится частью дерева модели.
  • Режимы train/eval: model.train() и model.eval() переключают поведение модулей вроде Dropout и BatchNorm.
  • Перенос на device и смена dtype: model.to(device) переносит все параметры и буферы.
  • Сериализация: model.state_dict() даёт словарь всех параметров и буферов, пригодный для сохранения.
  • Слои как готовые модули

    PyTorch предоставляет много готовых слоёв. Почти все они являются наследниками nn.Module.

    Пример: линейный слой nn.Linear

    nn.Linear(in_features, out_features) реализует аффинное преобразование:

    Где:

  • — входной вектор признаков
  • — матрица весов (обучаемый параметр)
  • — вектор смещения (обучаемый параметр)
  • — выход слоя
  • Важно: в реальных моделях чаще подают батч, поэтому вход обычно имеет форму (batch, in_features), а выход — (batch, out_features).

    Пример:

    Пример: свёртка nn.Conv2d

    Изображения обычно имеют форму (batch, channels, height, width). Свёрточный слой nn.Conv2d учится находить локальные паттерны.

    Нелинейности и служебные слои

    Часто используемые модули:

  • nn.ReLU() — нелинейность
  • nn.Flatten() — разворачивает измерения (часто перед MLP)
  • nn.Dropout(p=...) — регуляризация (ведёт себя по-разному в train() и eval())
  • nn.BatchNorm1d/2d/3d — нормализация (тоже зависит от train() и eval())
  • Своя модель: наследование от nn.Module

    Чтобы создать модель, вы обычно:

  • Наследуетесь от nn.Module
  • В __init__ объявляете слои и сохраняете их в атрибутах self...
  • В forward описываете вычисления
  • Минимальный MLP:

    Почему важно объявлять слои в __init__

    PyTorch «видит» параметры и подмодули, только если они:

  • Являются атрибутами объекта nn.Module (то есть лежат в self.xxx)
  • Или находятся внутри специальных контейнеров nn.Module (про них ниже)
  • Если создать слой как локальную переменную внутри forward, его параметры не станут частью модели, не попадут в state_dict() и не будут оптимизироваться.

    Как nn.Module связан с autograd

    Параметры слоёв (например, веса nn.Linear) хранятся как nn.Parameter. По умолчанию у них requires_grad=True, поэтому:

  • Прямой проход logits = model(x) строит граф вычислений
  • Потери loss = ... дают скаляр
  • loss.backward() вычисляет градиенты и записывает их в param.grad
  • Короткий пример (без оптимизатора, только градиенты):

    Контейнеры модулей

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

    nn.Sequential

    nn.Sequential автоматически определяет forward: он просто пропускает вход через слои по цепочке.

    Когда удобно:

  • Простая «цепочка» без ветвлений и без нескольких входов/выходов
  • Когда неудобно:

  • Нужны пропуски (skip connections), ветвления, объединения, несколько тензоров
  • nn.ModuleList

    nn.ModuleList — список модулей, который регистрирует их как подмодули, но не задаёт forward.

    Если использовать обычный Python-список вместо nn.ModuleList, слои могут не попасть в model.parameters().

    nn.ModuleDict

    nn.ModuleDict — словарь модулей, полезен для архитектур, где блоки удобно выбирать по ключу.

    train() и eval(): режимы работы модели

    model.train() включает режим обучения, model.eval() включает режим инференса.

    Это влияет не на всё подряд, а на модули, у которых есть различающееся поведение:

  • nn.Dropout в train() зануляет часть активаций, а в eval() выключается
  • nn.BatchNorm* в train() обновляет внутренние статистики, а в eval() использует накопленные
  • Типичный шаблон:

    Для инференса обычно дополнительно отключают построение графа:

    Параметры, буферы и state_dict

    parameters и named_parameters

  • model.parameters() возвращает итератор по обучаемым параметрам
  • model.named_parameters() возвращает пары (имя, параметр)
  • Имена отражают дерево модулей, например fc1.weight, fc1.bias.

    Буферы (buffers)

    Иногда нужно хранить тензоры внутри модели, но не обучать их градиентным спуском: маски, константы, статистики. Для этого есть буферы.

  • register_buffer(name, tensor) добавляет тензор в модуль
  • Буфер попадает в state_dict() и переносится через to(device)
  • Буфер не является параметром и не возвращается в parameters()
  • Пример (фиксированная маска):

    Сохранение и загрузка через state_dict

    state_dict — это словарь, где ключи являются строковыми именами, а значения — тензоры параметров и буферов.

    Практическая рекомендация: чаще сохраняют именно state_dict, а не целый объект модели.

    torch.nn.functional: функции вместо модулей

    torch.nn.functional (часто импортируют как F) содержит функциональные версии операций.

    Разница между подходами:

  • nn.Module удобен, когда операция имеет состояние или должна участвовать в train()/eval() (Dropout, BatchNorm), или когда вы хотите хранить её как часть модели
  • F.* удобен как «операция в месте» внутри forward
  • Пример: ReLU можно писать и так, и так:

    Важный нюанс: для некоторых функциональных операций нужно явно учитывать режим обучения. Например, F.dropout по умолчанию применяет dropout как в обучении, поэтому в forward обычно пишут training=self.training.

    Перенос модели на устройство

    Как и для тензоров, все параметры модели должны оказаться на правильном device.

    Если вход на GPU, а модель на CPU (или наоборот), вы получите ошибку про разные устройства.

    Типичные ошибки при сборке моделей

  • Забыли super().__init__() в конструкторе
  • Создали слой внутри forward, из-за чего параметры не регистрируются
  • Хранили слои в обычном списке вместо nn.ModuleList или nn.Sequential
  • Ожидали, что model.eval() отключит граф вычислений (он не отключает, для этого нужен torch.no_grad())
  • Использовали функциональный F.dropout без training=self.training, и dropout работал даже в eval()
  • !Дерево модулей показывает, как модель хранит слои и как PyTorch собирает параметры

    Мини-итог

  • nn.Module — базовый контейнер для слоёв, параметров, режимов train/eval, переноса на устройство и сохранения
  • Слои вроде nn.Linear, nn.Conv2d, nn.Dropout уже являются nn.Module
  • Для композиции используйте nn.Sequential, nn.ModuleList, nn.ModuleDict
  • state_dict() — основной способ сохранять и загружать веса
  • Следующий логичный шаг после сборки модели — разобраться с обучающим циклом: функциями потерь, оптимизаторами, обнулением градиентов и обновлением параметров.

    3. Функции потерь, оптимизаторы и цикл обучения

    Функции потерь, оптимизаторы и цикл обучения

    В предыдущих статьях вы разобрали тензоры, устройства и autograd, а затем научились собирать модели через nn.Module. Теперь соберём всё в работающую схему обучения: выберем функцию потерь, подключим оптимизатор и напишем цикл обучения так, как это принято в PyTorch.

    Официальные источники:

  • torch.nn (loss-функции и слои)
  • torch.optim
  • torch.utils.data (Dataset и DataLoader)
  • Saving and loading models
  • Что такое функция потерь

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

    Практически это выглядит так:

  • модель выдаёт предсказание pred
  • у нас есть правильный ответ target
  • loss_fn(pred, target) возвращает скаляр (одно число)
  • loss.backward() считает градиенты по параметрам модели
  • Почему loss обычно должен быть скаляром

    Autograd в PyTorch ожидает, что вызов backward() будет сделан от скалярной величины. Поэтому loss-функции почти всегда возвращают одно число, обычно усреднённое по батчу.

    У большинства loss-функций есть параметр reduction:

  • reduction="mean" усредняет по батчу
  • reduction="sum" суммирует
  • reduction="none" возвращает значение для каждого объекта батча
  • Популярные loss-функции и их ожидания по входам

    Регрессия: nn.MSELoss

    Подходит, когда модель предсказывает вещественные значения.

  • вход: pred формы (batch, d)
  • цель: target той же формы
  • тип: обычно float32
  • Пример:

    Многоклассовая классификация: nn.CrossEntropyLoss

    Это основная loss-функция для классификации на классов. Важный нюанс: она ожидает логиты, а не вероятности.

  • logits: форма (batch, C), тип float
  • target: форма (batch,), тип int64, значения от 0 до C-1
  • внутри она делает log_softmax и nll_loss, поэтому отдельно применять softmax обычно не нужно
  • Пример:

    Типичная ошибка: передать one-hot в CrossEntropyLoss. Для one-hot обычно используют другие варианты, но в базовом пайплайне PyTorch чаще используются именно индексные метки.

    Бинарная классификация: nn.BCEWithLogitsLoss

    Используйте её, если у вас два класса или несколько независимых бинарных меток.

  • вход: логиты (без sigmoid), форма часто (batch, 1) или (batch, K)
  • цель: тензор float с 0/1 (или вероятностями), той же формы
  • функция численно стабильнее, чем комбинация sigmoid + BCELoss
  • Пример:

    Оптимизаторы: как параметры обновляются

    Оптимизатор обновляет параметры модели на основе градиентов, которые посчитал autograd.

    Общая идея градиентного спуска часто записывается так:

    Где:

  • это параметры модели (веса и смещения слоёв)
  • это loss (функция потерь)
  • это градиент loss по параметрам
  • это шаг обучения (lr, learning rate)
  • В PyTorch оптимизатор хранит ссылки на параметры и имеет метод step(), который делает обновление.

    SGD и Adam

    | Оптимизатор | Когда часто используют | Ключевые параметры | |---|---|---| | torch.optim.SGD | классические задачи, сильный контроль динамики обучения | lr, momentum | | torch.optim.Adam | хороший стартовый выбор, обычно сходится быстрее без тонкой настройки | lr, betas, eps |

    Пример подключения Adam:

    Weight decay

    Многие оптимизаторы поддерживают weight_decay. На практике это часто работает как L2-регуляризация: помогает уменьшать переобучение, удерживая веса от чрезмерного роста.

    Группы параметров

    Иногда разным частям модели задают разные lr.

    Цикл обучения: стандартный шаблон

    Ниже минимальный шаблон итерации обучения. Это тот самый мост между nn.Module и реальным обучением.

    !Блок-схема показывает порядок действий внутри одного шага обучения

    Порядок действий внутри шага

  • model.train() включает режим обучения (важно для Dropout и BatchNorm)
  • прямой проход: pred = model(x)
  • считаем loss: loss = loss_fn(pred, y)
  • обнуляем градиенты: optimizer.zero_grad(...)
  • считаем градиенты: loss.backward()
  • обновляем параметры: optimizer.step()
  • Почему нужно обнулять градиенты: градиенты в PyTorch накапливаются в .grad. Если их не сбрасывать, вы будете обновлять параметры суммой градиентов из нескольких итераций.

    Рекомендуемый вариант обнуления:

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

    Полный пример: обучение маленькой модели на синтетических данных

    Ниже пример без DataLoader, чтобы сфокусироваться на механике.

    Ключевые моменты:

  • модель возвращает логиты
  • BCEWithLogitsLoss принимает логиты напрямую
  • zero_grad делаем перед backward
  • Валидация и инференс: model.eval() и torch.no_grad()

    Во время оценки качества обычно нужно:

  • выключить специфичное для обучения поведение слоёв
  • не строить граф autograd, чтобы экономить память и время
  • Для этого используют вместе:

  • model.eval()
  • with torch.no_grad(): ...
  • Пример:

    Важно различать:

  • model.eval() переключает поведение некоторых модулей (Dropout, BatchNorm)
  • torch.no_grad() отключает построение графа и вычисление градиентов
  • Одно не заменяет другое.

    Данные батчами: где появляется DataLoader

    В реальных задачах вы обучаете модель не на всём датасете сразу, а батчами. Обычно это делают через Dataset и DataLoader.

    Мини-шаблон цикла с батчами:

    Самое важное здесь из предыдущих статей:

  • и данные, и модель должны быть на одном device
  • формы и dtype должны совпадать с ожиданиями loss-функции
  • Частые ошибки и быстрые проверки

  • Передали не тот формат меток
  • - CrossEntropyLoss: метки классов должны быть индексами (int64), а не one-hot - BCEWithLogitsLoss: метки обычно float 0/1 и той же формы, что и логиты
  • Применили softmax перед CrossEntropyLoss
  • - обычно не нужно и может ухудшить численную стабильность
  • Забыли optimizer.zero_grad()
  • - градиенты накапливаются, обучение становится некорректным
  • Думаете, что model.eval() отключает градиенты
  • - не отключает, нужен torch.no_grad()
  • Перемешали устройства CPU и GPU
  • - переносите и модель, и батчи на один device

    Мини-итог

  • loss-функция превращает предсказание и цель в скалярный loss
  • loss.backward() считает градиенты параметров
  • оптимизатор (SGD, Adam) обновляет параметры через optimizer.step()
  • корректный цикл обучения почти всегда включает train(), zero_grad(), backward(), step()
  • для валидации используйте eval() вместе с torch.no_grad()
  • Следующий логичный шаг после базового цикла обучения: метрики качества, логирование, сохранение чекпоинтов, планировщики lr и более продвинутые практики обучения.

    4. Данные: Dataset, DataLoader и предобработка

    Данные: Dataset, DataLoader и предобработка

    В прошлой статье вы собрали цикл обучения: прямой проход, loss, backward(), optimizer.step(). Теперь добавим недостающий компонент, без которого почти не бывает реальных проектов: подачу данных батчами с предобработкой.

    В PyTorch за это отвечают два ключевых объекта:

  • torch.utils.data.Dataset описывает, как получить один пример
  • torch.utils.data.DataLoader описывает, как получать батчи, перемешивать, распараллеливать загрузку и собирать батч в тензоры
  • Официальные источники:

  • torch.utils.data
  • torch.utils.data.Dataset
  • torch.utils.data.DataLoader
  • !Общая схема движения данных от Dataset к обучающему шагу

    Зачем нужен Dataset и DataLoader

    Если у вас есть тензоры X и Y, технически можно обучаться на всём датасете целиком, но почти всегда это плохо:

  • не помещается в память
  • хуже по качеству оптимизации, потому что модель видит данные без перемешивания
  • невозможно эффективно загружать данные параллельно
  • Стандартный поток выглядит так:

  • Dataset умеет по индексу вернуть один объект: (x, y).
  • DataLoader берёт много индексов, достаёт много объектов из Dataset и превращает их в батч.
  • В обучающем цикле вы переносите батч на device и делаете шаг обучения.
  • Dataset: контракт и типы датасетов

    Dataset это класс с двумя основными методами:

  • __len__() возвращает размер датасета
  • __getitem__(index) возвращает один пример по индексу
  • Важное правило: __getitem__ должен возвращать данные в том виде, который понимает ваша модель и loss.

    Map-style и Iterable-style Dataset

    В PyTorch есть два семейства датасетов.

  • Map-style датасеты индексируемые: у них есть __len__ и __getitem__.
  • Iterable-style датасеты потоковые: отдают элементы при итерации, часто используются для бесконечных потоков или больших файлов.
  • В базовых задачах начинайте с map-style.

    Готовые варианты: TensorDataset и random_split

    Если данные уже в тензорах, удобен TensorDataset.

    Чтобы разделить датасет на train и val, часто используют random_split.

    Свой Dataset: пример с предобработкой

    Частый сценарий: у вас есть список путей к файлам и отдельный источник меток. Тогда Dataset хранит индексы и метаданные, а сами данные читает и обрабатывает в __getitem__.

    Ниже пример без реальных файлов, но с типичной структурой.

    На что обратить внимание:

  • dtype должен соответствовать задаче
  • форма (shape) должна совпадать с тем, что ждёт модель
  • предобработку часто удобнее делать прямо в Dataset, чтобы она была частью пайплайна
  • DataLoader: батчи, перемешивание, параллелизм

    DataLoader превращает Dataset в источник батчей.

    Основные параметры DataLoader

    | Параметр | Что делает | Типичная рекомендация | |---|---|---| | batch_size | размер батча | начните с 32 или 64, затем подгоняйте под память | | shuffle | перемешивание | True для train, False для val и test | | num_workers | число процессов для загрузки | 0 для простоты, затем увеличивайте для ускорения | | pin_memory | закрепляет память на CPU для ускорения копирования на GPU | True, если обучаетесь на CUDA | | drop_last | выбрасывает последний неполный батч | полезно, если нужна фиксированная форма батча | | persistent_workers | не пересоздаёт воркеры каждый эпох | ускоряет при num_workers > 0 |

    Практическое замечание: слишком большой num_workers может замедлить или привести к нехватке памяти. Подбирают экспериментально.

    Перенос батча на GPU

    Если вы включили pin_memory=True, перенос на GPU часто делают с non_blocking=True.

    Важно: перенос на device обычно делают в обучающем цикле, а не внутри Dataset.

    Как DataLoader собирает батч: collate_fn

    По умолчанию DataLoader делает простую операцию:

  • берёт список [(x1, y1), (x2, y2), ...]
  • склеивает x по новой первой оси, получается тензор формы (batch, ...)
  • Это работает, только если все x в батче одинаковой формы.

    Проблема переменной длины

    Тексты и последовательности часто имеют разную длину. Тогда стандартная склейка не сработает.

    Решение: задать collate_fn, который внутри батча делает padding до максимальной длины.

    Для этого удобно использовать pad_sequence.

  • Документация: torch.nn.utils.rnn.pad_sequence
  • Пример: батч последовательностей разной длины.

    Идея такая:

  • pad_sequence делает все последовательности одинаковой длины внутри батча
  • padding_value выбирается так, чтобы модель могла игнорировать padding (часто это 0)
  • Предобработка: где она живёт и как её организовать

    Под предобработкой обычно понимают преобразования входа до подачи в модель:

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

  • Делать предобработку на лету внутри Dataset.
  • Делать предобработку заранее и хранить готовые тензоры.
  • На лету проще экспериментировать и меньше места на диске, но больше нагрузка на CPU.

    Трансформации как отдельный объект

    Часто предобработку оформляют как callable-объект transform, который передают в Dataset. Это помогает:

  • переиспользовать один и тот же код
  • различать train-трансформации и val-трансформации
  • Для изображений часто используют torchvision.transforms.

  • Документация: torchvision transforms
  • Типичный пример: нормализация.

    Практические подсказки:

  • статистики mean и std обычно считают на train-части
  • предобработку стоит делать детерминированной для val и test
  • Частые ошибки и как их быстро диагностировать

  • Ошибка про разные устройства
  • - проверьте, что и батчи, и модель на одном device
  • Ошибка при склейке батча
  • - вероятно, примеры разной формы, нужен collate_fn
  • Loss ругается на тип меток
  • - для CrossEntropyLoss метки обычно int64 и формы (batch,)
  • Обучение медленное при чтении с диска
  • - попробуйте увеличить num_workers, включить persistent_workers, оптимизировать предобработку

    Мини-итог

  • Dataset отвечает за выдачу одного примера и обычно содержит логику чтения и предобработки
  • DataLoader отвечает за батчи, перемешивание и параллельную загрузку
  • collate_fn нужен, когда стандартная сборка батча не подходит, например для последовательностей разной длины
  • корректные shape, dtype и device связывают тему данных с темами nn.Module, loss и обучающего цикла
  • 5. Отладка, сохранение, инференс и ускорение (GPU, mixed precision)

    Отладка, сохранение, инференс и ускорение (GPU, mixed precision)

    В предыдущих статьях вы научились собирать модели через nn.Module, писать цикл обучения с loss и оптимизатором, а также подавать данные батчами через Dataset и DataLoader. Теперь закроем практические вопросы, без которых сложно делать реальные проекты:

  • Отладка: как быстро находить ошибки форм, типов, устройств и «плохих» градиентов
  • Сохранение и загрузка: как делать чекпоинты, продолжать обучение и переносить модели между устройствами
  • Инференс: как правильно получать предсказания быстро и без лишней памяти
  • Ускорение: базовые приёмы для GPU и mixed precision (AMP)
  • Полезные официальные источники:

  • Saving and Loading Models
  • torch.save
  • torch.load
  • torch.inference_mode
  • Automatic Mixed Precision (AMP)
  • torch.profiler
  • !Блок-схема, связывающая обучение, инференс и сохранение

    Отладка: быстрые проверки до сложных инструментов

    Большинство проблем в PyTorch упираются в четыре вещи:

  • shape не совпадает с ожиданиями слоя или loss
  • dtype не подходит (например, метки не int64 для CrossEntropyLoss)
  • device отличается (часть тензоров на CPU, часть на GPU)
  • градиенты «ломаются» (NaN, взрыв, затухание)
  • Мини-набор проверок в одном батче

    Рекомендуется в начале проекта прогнать один батч и проверить всё руками.

    Типичные ожидания:

  • для классификации с nn.CrossEntropyLoss:
  • - out: (batch, C) и float32 или float16/bfloat16 в AMP - y: (batch,) и int64

    Проверка, что параметры реально обучаются

    Иногда модель «обучается», но параметры не обновляются (например, забыли добавить параметры в оптимизатор или случайно отключили градиенты).

    Если .grad равен None, значит градиент не посчитан или этот параметр не участвует в графе.

    Поиск NaN и проблем в backward

    Если loss стал nan или обучение нестабильно, включайте диагностику.

  • Поиск NaN и бесконечностей в тензорах:
  • Поиск места, где ломается backward:
  • Документация: torch.autograd.set_detect_anomaly

    Воспроизводимость: фиксируем случайность

    Для отладки полезно делать результаты повторяемыми.

    Важно понимать ограничения:

  • полная детерминированность на GPU может снижать скорость и не всегда достижима
  • для строгого режима есть отдельные настройки, но их обычно включают только для экспериментов и отладки
  • Сохранение и загрузка: state_dict, чекпоинты и перенос между устройствами

    В прошлой статье про nn.Module вы уже видели state_dict(). Практическая рекомендация в PyTorch почти всегда такая:

  • сохраняйте веса через model.state_dict()
  • при необходимости сохраняйте ещё и состояние оптимизатора и номер эпохи
  • Сохранение только весов

    Загрузка:

    Зачем map_location:

  • позволяет загрузить файл, сохранённый на GPU, на машине без GPU
  • позволяет явно контролировать устройство при загрузке
  • Чекпоинт для продолжения обучения

    Если вы хотите возобновить обучение с того же места, нужно сохранять не только веса.

    Загрузка:

    Практические детали:

  • сохраняйте лучший чекпоинт отдельно (например, по метрике на валидации)
  • если вы меняли архитектуру, load_state_dict может ругаться на несовпадение ключей
  • - иногда это нормально (например, вы добавили голову классификатора) - тогда используют strict=False, но делайте это осознанно

    Документация: torch.nn.Module.load_state_dict

    Инференс: eval, no_grad и inference_mode

    Инференс отличается от обучения двумя вещами:

  • некоторые слои должны работать иначе (Dropout выключается, BatchNorm использует накопленные статистики)
  • градиенты не нужны, поэтому можно экономить память и ускоряться
  • Базовый шаблон инференса

    Более быстрый режим: torch.inference_mode

    Для чистого инференса часто используют torch.inference_mode(). Он обычно быстрее и экономнее по памяти, чем no_grad, потому что дополнительно отключает ряд механизмов autograd.

    Документация: torch.inference_mode

    Частые ошибки в инференсе

  • забыли model.eval() и получили «плавающие» результаты из-за Dropout
  • сделали softmax там, где не нужно
  • - для выбора класса часто достаточно argmax по логитам
  • не совпадает предобработка между train и inference
  • - нормализация, порядок каналов, масштабы значений должны быть одинаковыми

    Ускорение на GPU: базовые правила производительности

    Перенос на GPU без лишних копирований

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

    Если используете DataLoader(pin_memory=True), перенос часто ускоряется с non_blocking=True.

    Настройки DataLoader, которые реально влияют

    | Цель | Что попробовать | Комментарий | |---|---|---| | уменьшить простой GPU | num_workers > 0 | распараллеливает загрузку | | ускорить CPU→GPU копирование | pin_memory=True | полезно при CUDA | | убрать накладные расходы | persistent_workers=True | работает при num_workers > 0 |

    cudnn benchmark для свёрток

    Если размеры входов фиксированы (например, всегда 64x64), можно ускорить свёрточные сети:

    Если размеры входов часто меняются, это может не помочь или даже навредить.

    Избегайте скрытых синхронизаций CPU и GPU

    Некоторые операции заставляют CPU ждать GPU, что «ломает» параллелизм:

  • частые .item() внутри цикла
  • печать слишком большого количества значений
  • измерение времени без torch.cuda.synchronize() (если вы меряете именно GPU-время)
  • Mixed precision (AMP): быстрее и меньше памяти

    Mixed precision означает, что часть вычислений идёт в float16 или bfloat16, а часть остаётся в float32, чтобы сохранить качество. В PyTorch это обычно делается через AMP.

    Ключевые компоненты:

  • autocast автоматически выбирает подходящую точность для операций
  • GradScaler помогает избежать проблем с слишком маленькими градиентами в float16
  • Документация: Automatic Mixed Precision (AMP)

    AMP в обучении: шаблон

    Что здесь происходит:

  • внутри autocast многие операции выполняются в более низкой точности
  • scale(loss) временно увеличивает loss, чтобы градиенты не «обнулились» из-за ограниченной точности float16
  • scaler.step корректно делает шаг оптимизатора, если градиенты «в порядке»
  • AMP в инференсе

    В инференсе обычно достаточно autocast без GradScaler.

    Когда AMP может не дать выигрыша

  • модель маленькая и упирается не в математику, а в загрузку данных
  • обучение идёт на CPU
  • часть операций не поддерживает низкую точность и выполняется в float32 (это нормально, просто выигрыш меньше)
  • Профилирование: как понять, что именно тормозит

    Если обучение медленное, гадать не нужно: измеряйте.

    torch.profiler: минимальный пример

    Документация: torch.profiler

    Практический смысл профайлера:

  • увидеть, что вы упираетесь в DataLoader (CPU), а не в модель (GPU)
  • найти самые дорогие операции
  • понять, не происходит ли лишних копирований CPU↔GPU
  • Итоговый чеклист

    Перед тем как считать, что «PyTorch медленный» или «модель не обучается», пройдитесь по списку:

  • модель и батчи на одном device
  • формы согласованы со слоями и loss
  • для инференса используете model.eval() и torch.inference_mode()
  • сохраняете state_dict и, если нужно, чекпоинт с оптимизатором
  • DataLoader настроен под вашу задачу (num_workers, pin_memory)
  • при CUDA попробовали AMP, если модель достаточно тяжёлая
  • После этого у вас есть полный базовый набор навыков: от данных и модели до обучения, сохранения, инференса и ускорения.