Фаза 03 · урок 11
Введение в PyTorch
Цель урока: У вас есть работающий мини-фреймворк. Линейные слои, ReLU, dropout, batch norm, Adam, DataLoader , цикл обучения. Он обучает 4-слойную сеть на задаче классификации кругов на чистом Python.
Текущий релиз AlexBred.com: первые 100 уроков русскоязычной программы.
Содержание урока
- Цели обучения
- Проблема
- Концепция
- Почему победил PyTorch
- Тензоры
- Autograd
- nn.Module
- Функции потерь и оптимизаторы
- Цикл обучения
- Dataset и DataLoader
- Обучение на GPU
- Сравнение: мини-фреймворк, PyTorch и JAX
- Соберите это
- Шаг 1: загрузите MNIST из необработанных файлов
- Шаг 2: определите модель
- Шаг 3: цикл обучения
- Шаг 4: свяжите всё вместе
- Используйте это
- Быстрое сравнение: мини-фреймворк и PyTorch
- Сохранение и загрузка моделей
- Планирование скорости обучения
- Выпустите это
- Упражнения
- Ключевые термины
- Дополнительное чтение
Вы построили двигатель из поршней и коленвалов. Теперь научитесь пользоваться тем, на котором ездит каждый.
Тип: Сборка Языки: Python Предварительные требования: Урок 03.10 (Создайте собственный мини-фреймворк) Время: ~75 минут
Цели обучения
- Создавать и обучать нейронные сети с помощью PyTorch:
nn.Module,nn.Sequentialиautograd - Использовать тензоры PyTorch, ускорение на GPU и стандартный цикл обучения (
zero_grad, forward, loss, backward, step) - Переносить компоненты своего мини-фреймворка, созданного с нуля, на их эквиваленты в PyTorch
- Профилировать и сравнивать скорость обучения вашего чисто Python-фреймворка и PyTorch на одной задаче
Проблема
У вас есть работающий мини-фреймворк. Линейные слои, ReLU, dropout, batch norm, Adam, DataLoader, цикл обучения. Он обучает 4-слойную сеть на задаче классификации кругов на чистом Python.
Он также в 500 раз медленнее PyTorch на той же задаче.
Ваш мини-фреймворк обрабатывает по одному примеру за раз во вложенных циклах Python. PyTorch отправляет те же операции в оптимизированные ядра C++/CUDA, работающие на GPU. На одном NVIDIA A100 PyTorch обучает ResNet-50 (25,6 млн параметров) на ImageNet (1,28 млн изображений) примерно за 6 часов. На эту же задачу вашему фреймворку потребовалось бы примерно 3 000 часов — если бы он прежде не исчерпал память.
Скорость — не единственный разрыв. У вашего фреймворка нет поддержки GPU. Нет автоматического дифференцирования: вы вручную писали backward() для каждого модуля. Нет сериализации. Нет распределённого обучения. Нет смешанной точности. Нет способа отладить поток градиентов без операторов print.
PyTorch закрывает каждый из этих пробелов. И делает это, сохраняя точно такую же ментальную модель, которую вы уже построили: Module, forward(), parameters(), backward(), optimizer.step(). Концепции переносятся один к одному. Синтаксис почти идентичен. Разница в том, что PyTorch скрывает за тем же интерфейсом десятилетие системной инженерии, которую вы спроектировали с нуля.
Концепция
Почему победил PyTorch
В 2015 году TensorFlow требовал определить статический вычислительный граф до выполнения чего-либо. Вы строили граф, компилировали его, затем подавали в него данные. Отладка означала разглядывание визуализаций графа. Изменение архитектуры означало перестроить граф с нуля.
PyTorch появился в 2017 году с другой философией: немедленным выполнением (eager execution). Вы пишете Python. Он выполняется сразу. y = model(x) действительно вычисляет y прямо сейчас, а не «добавляет узел в граф, который когда-нибудь вычислит y». Поэтому работали стандартные инструменты отладки Python. Работал print(). Работал pdb. Работал if/else в вашем прямом проходе.
К 2020 году рынок высказался. Доля PyTorch в исследовательских статьях по ML выросла с 7% (2017) до более 75% (2022). Meta, Google DeepMind, OpenAI, Anthropic и Hugging Face используют PyTorch как основной фреймворк. TensorFlow 2.x в ответ внедрил немедленное выполнение — молчаливое признание правильности дизайна PyTorch.
Урок: опыт разработчика накапливается. Фреймворк, который на 10% медленнее, но на 50% быстрее отлаживается, побеждает каждый раз.
Тензоры
Тензор — это многомерный массив с тремя критически важными свойствами: формой (shape), типом данных (dtype) и устройством (device).
import torch
x = torch.zeros(3, 4) # shape: (3, 4), dtype: float32, device: cpu
x = torch.randn(2, 3, 224, 224) # batch of 2 RGB images, 224x224
x = torch.tensor([1, 2, 3]) # from a Python list
Форма — это размерность. Скаляр имеет форму (), вектор — (n,), матрица — (m, n), пакет изображений — (batch, channels, height, width).
Dtype управляет точностью и памятью.
| dtype | Биты | Диапазон | Вариант использования |
|---|---|---|---|
| float32 | 32 | ~7 десятичных знаков | Обучение по умолчанию |
| float16 | 16 | ~3,3 десятичных знака | Смешанная точность |
| bfloat16 | 16 | Тот же диапазон, что у float32, меньшая точность | Обучение LLM |
| int8 | 8 | от -128 до 127 | Квантованный инференс |
Устройство определяет, где происходит вычисление.
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
x = torch.randn(3, 4, device=device)
x = x.to("cuda")
x = x.cpu()
Каждая операция требует, чтобы все тензоры находились на одном устройстве. Это ошибка PyTorch №1, с которой сталкиваются начинающие: RuntimeError: Expected all tensors to be on the same device. Исправьте её, переместив всё на одно устройство до вычисления.
Изменение формы выполняется за константное время: меняются метаданные, а не данные.
x = torch.randn(2, 3, 4)
x.view(2, 12) # reshape to (2, 12) -- must be contiguous
x.reshape(6, 4) # reshape to (6, 4) -- works always
x.permute(2, 0, 1) # reorder dimensions
x.unsqueeze(0) # add dimension: (1, 2, 3, 4)
x.squeeze() # remove size-1 dimensions
Autograd
Ваш мини-фреймворк требовал реализовать backward() для каждого модуля. PyTorch не требует. Он записывает каждую операцию над тензорами в ориентированный ациклический граф (вычислительный граф), а затем проходит этот граф в обратном порядке, чтобы автоматически вычислить градиенты.
Ключевое отличие от вашего фреймворка: PyTorch использует ленточное автоматическое дифференцирование (tape-based autodiff). Каждая операция добавляется на «ленту» во время прямого прохода. Вызов .backward() воспроизводит ленту в обратном порядке.
x = torch.randn(3, requires_grad=True)
y = x ** 2 + 3 * x
z = y.sum()
z.backward()
print(x.grad) # dz/dx = 2x + 3
Три правила autograd:
- Только листовые тензоры с
requires_grad=Trueнакапливают градиенты. - По умолчанию градиенты накапливаются — вызывайте
optimizer.zero_grad()перед каждым обратным проходом. torch.no_grad()отключает отслеживание градиентов (используйте при оценке).
nn.Module
nn.Module — базовый класс для каждого компонента нейронной сети в PyTorch. Вы уже строили эту абстракцию в уроке 10. Версия PyTorch добавляет автоматическую регистрацию параметров, рекурсивное обнаружение модулей, управление устройствами и сериализацию state_dict.
import torch.nn as nn
class MLP(nn.Module):
def __init__(self, input_dim, hidden_dim, output_dim):
super().__init__()
self.layer1 = nn.Linear(input_dim, hidden_dim)
self.relu = nn.ReLU()
self.layer2 = nn.Linear(hidden_dim, output_dim)
def forward(self, x):
x = self.layer1(x)
x = self.relu(x)
x = self.layer2(x)
return x
Когда вы присваиваете nn.Module или nn.Parameter атрибуту в __init__, PyTorch автоматически регистрирует его. model.parameters() рекурсивно собирает каждый зарегистрированный параметр. Поэтому вам никогда не нужно вручную собирать веса, как в мини-фреймворке.
Ключевые строительные блоки:
| Модуль | Что делает | Параметры |
|---|---|---|
| nn.Linear(in, out) | Wx + b | in*out + out |
| nn.Conv2d(in_ch, out_ch, k) | Двумерная свёртка | in_chout_chk*k + out_ch |
| nn.BatchNorm1d(features) | Нормализует активации | 2 * features |
| nn.Dropout(p) | Случайное зануление | 0 |
| nn.ReLU() | max(0, x) | 0 |
| nn.GELU() | Gaussian error linear | 0 |
| nn.Embedding(vocab, dim) | Таблица поиска | vocab * dim |
| nn.LayerNorm(dim) | Нормализация по примеру | 2 * dim |
Функции потерь и оптимизаторы
PyTorch поставляет готовые к промышленному использованию версии всего, что вы построили.
Функции потерь (из torch.nn):
| Функция потерь | Задача | Вход |
|---|---|---|
| nn.MSELoss() | Регрессия | Любая форма |
| nn.CrossEntropyLoss() | Многоклассовая классификация | Логиты (не softmax) |
| nn.BCEWithLogitsLoss() | Бинарная классификация | Логиты (не sigmoid) |
| nn.L1Loss() | Регрессия (робастная) | Любая форма |
| nn.CTCLoss() | Выравнивание последовательностей | Логарифмы вероятностей |
Примечание: CrossEntropyLoss внутренне объединяет LogSoftmax + NLLLoss. Передавайте исходные логиты, а не выходы softmax. Это распространённая ошибка, которая незаметно производит неправильные градиенты.
Оптимизаторы (из torch.optim):
| Оптимизатор | Когда использовать | Типичная LR |
|---|---|---|
| SGD(params, lr, momentum) | CNN, хорошо настроенные конвейеры | 0.01–0.1 |
| Adam(params, lr) | Стартовая точка по умолчанию | 1e-3 |
| AdamW(params, lr, weight_decay) | Трансформеры, дообучение | 1e-4–1e-3 |
| LBFGS(params) | Малый масштаб, второй порядок | 1.0 |
Цикл обучения
Каждый цикл обучения PyTorch следует одному и тому же пятишаговому шаблону. Вы уже знаете его из урока 10.
Канонический шаблон:
for epoch in range(num_epochs):
model.train()
for inputs, targets in train_loader:
inputs, targets = inputs.to(device), targets.to(device)
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, targets)
loss.backward()
optimizer.step()
Пять строк внутри цикла пакета. Пять строк, которые обучили GPT-4, Stable Diffusion и LLaMA. Меняется архитектура. Меняются данные. Эти пять строк не меняются.
Dataset и DataLoader
Dataset в PyTorch — абстрактный класс с двумя методами: __len__ и __getitem__. DataLoader оборачивает его, добавляя пакетирование, перемешивание и многопроцессную загрузку данных.
from torch.utils.data import Dataset, DataLoader
class MNISTDataset(Dataset):
def __init__(self, images, labels):
self.images = images
self.labels = labels
def __len__(self):
return len(self.labels)
def __getitem__(self, idx):
return self.images[idx], self.labels[idx]
loader = DataLoader(dataset, batch_size=64, shuffle=True, num_workers=4)
num_workers=4 запускает 4 процесса, параллельно загружающих данные, пока GPU обучается на текущем пакете. На нагрузках, ограниченных диском (крупные изображения, аудио), уже одно это может удвоить скорость обучения.
Обучение на GPU
Перемещение модели на GPU:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = model.to(device)
Это рекурсивно переносит каждый параметр и буфер на GPU. Затем перемещайте каждый пакет при обучении:
inputs, targets = inputs.to(device), targets.to(device)
Смешанная точность сокращает потребление памяти вдвое и удваивает пропускную способность на современных GPU (A100, H100, RTX 4090), выполняя прямой/обратный проход в float16 при сохранении основных весов в float32:
from torch.amp import autocast, GradScaler
scaler = GradScaler()
for inputs, targets in loader:
with autocast(device_type="cuda"):
outputs = model(inputs)
loss = criterion(outputs, targets)
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
optimizer.zero_grad()
Сравнение: мини-фреймворк, PyTorch и JAX
| Возможность | Мини-фреймворк (L10) | PyTorch | JAX |
|---|---|---|---|
| Автодифференцирование | Ручной backward() |
Ленточный autograd | Функциональные преобразования |
| Выполнение | Немедленное (циклы Python) | Немедленное (ядра C++) | Трассировка + JIT-компиляция |
| Поддержка GPU | Нет | Да (CUDA, ROCm, MPS) | Да (CUDA, TPU) |
| Скорость (MNIST MLP) | ~300 с/эпоху | ~0,5 с/эпоху | ~0,3 с/эпоху |
| Система модулей | Пользовательский класс Module | nn.Module | Функции без состояния (Flax/Equinox) |
| Отладка | print() | print(), pdb, breakpoint() | Сложнее (JIT-трассировка ломает print) |
| Экосистема | Нет | Hugging Face, Lightning, timm | Flax, Optax, Orbax |
| Кривая обучения | Вы её построили | Умеренная | Крутая (функциональная парадигма) |
| Использование в продакшене | Игрушечные задачи | Meta, OpenAI, Anthropic, HF | Google DeepMind, Midjourney |
dropout-mask
Соберите это
3-слойный MLP, обученный на MNIST только с помощью примитивов PyTorch. Без высокоуровневых обёрток. Без torchvision.datasets. Мы сами загрузим и разберём необработанные данные.
Шаг 1: загрузите MNIST из необработанных файлов
MNIST поставляется в 4 сжатых gzip-файлах: обучающие изображения (60 000 x 28 x 28), обучающие метки, тестовые изображения (10 000 x 28 x 28), тестовые метки. Мы загружаем их и разбираем двоичный формат.
import torch
import torch.nn as nn
import struct
import gzip
import urllib.request
import os
def download_mnist(path="./mnist_data"):
base_url = "https://storage.googleapis.com/cvdf-datasets/mnist/"
files = [
"train-images-idx3-ubyte.gz",
"train-labels-idx1-ubyte.gz",
"t10k-images-idx3-ubyte.gz",
"t10k-labels-idx1-ubyte.gz",
]
os.makedirs(path, exist_ok=True)
for f in files:
filepath = os.path.join(path, f)
if not os.path.exists(filepath):
urllib.request.urlretrieve(base_url + f, filepath)
def load_images(filepath):
with gzip.open(filepath, "rb") as f:
magic, num, rows, cols = struct.unpack(">IIII", f.read(16))
data = f.read()
images = torch.frombuffer(bytearray(data), dtype=torch.uint8)
images = images.reshape(num, rows * cols).float() / 255.0
return images
def load_labels(filepath):
with gzip.open(filepath, "rb") as f:
magic, num = struct.unpack(">II", f.read(8))
data = f.read()
labels = torch.frombuffer(bytearray(data), dtype=torch.uint8).long()
return labels
Шаг 2: определите модель
3-слойный MLP: 784 -> 256 -> 128 -> 10. Активации ReLU. Dropout для регуляризации. Без batch norm, чтобы оставить пример простым.
class MNISTModel(nn.Module):
def __init__(self):
super().__init__()
self.net = nn.Sequential(
nn.Linear(784, 256),
nn.ReLU(),
nn.Dropout(0.2),
nn.Linear(256, 128),
nn.ReLU(),
nn.Dropout(0.2),
nn.Linear(128, 10),
)
def forward(self, x):
return self.net(x)
Выходной слой выдаёт 10 необработанных логитов (по одному на цифру). Без softmax: CrossEntropyLoss обрабатывает его внутри.
Число параметров: 784256 + 256 + 256128 + 128 + 128*10 + 10 = 235 146. По современным меркам крошечная модель. У GPT-2 small 124M. Эта обучается за секунды.
Шаг 3: цикл обучения
Канонический шаблон прямой проход — потеря — обратный проход — шаг.
def train_one_epoch(model, loader, criterion, optimizer, device):
model.train()
total_loss = 0
correct = 0
total = 0
for images, labels in loader:
images, labels = images.to(device), labels.to(device)
optimizer.zero_grad()
outputs = model(images)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
total_loss += loss.item() * images.size(0)
_, predicted = outputs.max(1)
correct += predicted.eq(labels).sum().item()
total += labels.size(0)
return total_loss / total, correct / total
def evaluate(model, loader, criterion, device):
model.eval()
total_loss = 0
correct = 0
total = 0
with torch.no_grad():
for images, labels in loader:
images, labels = images.to(device), labels.to(device)
outputs = model(images)
loss = criterion(outputs, labels)
total_loss += loss.item() * images.size(0)
_, predicted = outputs.max(1)
correct += predicted.eq(labels).sum().item()
total += labels.size(0)
return total_loss / total, correct / total
Обратите внимание на torch.no_grad() при оценке. Он отключает autograd, уменьшая потребление памяти и ускоряя инференс. Без него PyTorch строит вычислительный граф, который вы никогда не используете.
Шаг 4: свяжите всё вместе
def main():
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
download_mnist()
train_images = load_images("./mnist_data/train-images-idx3-ubyte.gz")
train_labels = load_labels("./mnist_data/train-labels-idx1-ubyte.gz")
test_images = load_images("./mnist_data/t10k-images-idx3-ubyte.gz")
test_labels = load_labels("./mnist_data/t10k-labels-idx1-ubyte.gz")
train_dataset = torch.utils.data.TensorDataset(train_images, train_labels)
test_dataset = torch.utils.data.TensorDataset(test_images, test_labels)
train_loader = torch.utils.data.DataLoader(
train_dataset, batch_size=64, shuffle=True
)
test_loader = torch.utils.data.DataLoader(
test_dataset, batch_size=256, shuffle=False
)
model = MNISTModel().to(device)
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
num_params = sum(p.numel() for p in model.parameters())
print(f"Device: {device}")
print(f"Parameters: {num_params:,}")
print(f"Train samples: {len(train_dataset):,}")
print(f"Test samples: {len(test_dataset):,}")
print()
for epoch in range(10):
train_loss, train_acc = train_one_epoch(
model, train_loader, criterion, optimizer, device
)
test_loss, test_acc = evaluate(
model, test_loader, criterion, device
)
print(
f"Epoch {epoch+1:2d} | "
f"Train Loss: {train_loss:.4f} | Train Acc: {train_acc:.4f} | "
f"Test Loss: {test_loss:.4f} | Test Acc: {test_acc:.4f}"
)
torch.save(model.state_dict(), "mnist_mlp.pt")
print(f"\nModel saved to mnist_mlp.pt")
print(f"Final test accuracy: {test_acc:.4f}")
Ожидаемый результат после 10 эпох: ~97,8% точности на тесте. Время обучения на CPU: ~30 секунд. На GPU: ~5 секунд. В вашем мини-фреймворке с той же архитектурой: ~45 минут.
Используйте это
Быстрое сравнение: мини-фреймворк и PyTorch
| Мини-фреймворк (урок 10) | PyTorch |
|---|---|
model = Sequential(Linear(784, 256), ReLU(), ...) |
model = nn.Sequential(nn.Linear(784, 256), nn.ReLU(), ...) |
pred = model.forward(x) |
pred = model(x) |
optimizer.zero_grad() |
optimizer.zero_grad() |
grad = criterion.backward() then model.backward(grad) |
loss.backward() |
optimizer.step() |
optimizer.step() |
| Нет GPU | model.to("cuda") |
| Ручной обратный проход для каждого модуля | Autograd обрабатывает всё |
Интерфейс почти идентичен. Разница — во всём, что находится под капотом.
Сохранение и загрузка моделей
torch.save(model.state_dict(), "model.pt")
model = MNISTModel()
model.load_state_dict(torch.load("model.pt", weights_only=True))
model.eval()
Всегда сохраняйте state_dict() (словарь параметров), а не объект модели. Сохранение объекта модели использует pickle, который ломается при рефакторинге кода. State dict переносимы.
Планирование скорости обучения
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
optimizer, T_max=10
)
for epoch in range(10):
train_one_epoch(model, train_loader, criterion, optimizer, device)
scheduler.step()
PyTorch поставляет более 15 планировщиков: StepLR, ExponentialLR, CosineAnnealingLR, OneCycleLR, ReduceLROnPlateau. Все подключаются к одному интерфейсу оптимизатора.
Выпустите это
Этот урок создаёт два артефакта:
outputs/prompt-pytorch-debugger.md— промпт для диагностики распространённых сбоев обучения PyTorchoutputs/skill-pytorch-patterns.md— справочник-практика по шаблонам обучения PyTorch
Упражнения
-
Добавьте batch normalization. Вставьте
nn.BatchNorm1dпосле каждого линейного слоя (перед активацией). Сравните точность теста и скорость обучения с версией только с dropout. Batch norm должен достичь 98%+ за меньшее число эпох. -
Реализуйте поиск скорости обучения. Обучайте одну эпоху с экспоненциально возрастающей скоростью обучения (от 1e-7 до 1.0). Постройте функцию потерь относительно LR. Оптимальная LR находится непосредственно перед тем, как функция потерь начинает расти. Используйте это для выбора лучшей LR для модели MNIST.
-
Перенесите на GPU со смешанной точностью. Добавьте
torch.amp.autocastиGradScalerв цикл обучения. Измерьте пропускную способность (примеров в секунду) с смешанной точностью и без неё на GPU. На A100 ожидайте ~2x ускорение. -
Создайте пользовательский Dataset. Скачайте Fashion-MNIST (тот же формат, что и MNIST, но с предметами одежды). Реализуйте класс
FashionMNISTDataset(Dataset)с__getitem__и__len__. Обучите тот же MLP и сравните точность. Fashion-MNIST сложнее: ожидайте ~88% против ~98%. -
Замените Adam на SGD + momentum. Обучайте с
SGD(params, lr=0.01, momentum=0.9). Сравните кривые сходимости. Затем добавьте планировщикCosineAnnealingLRи проверьте, догонит ли SGD Adam к 10-й эпохе.
Ключевые термины
| Термин | Как обычно говорят | Что это действительно означает |
|---|---|---|
| Тензор | «Многомерный массив» | Типизированный массив, учитывающий устройство и имеющий поддержку автоматического дифференцирования в каждой операции |
| Autograd | «Автоматическое обратное распространение» | Ленточная система, записывающая операции во время прямого прохода, затем воспроизводящая их в обратном порядке для вычисления точных градиентов |
| nn.Module | «Слой» | Базовый класс для любого блока дифференцируемых вычислений: регистрирует параметры, поддерживает вложение, обрабатывает режимы train/eval |
| state_dict | «Веса модели» | OrderedDict, сопоставляющий имена параметров с тензорами: переносимое сериализуемое представление обученной модели |
| .backward() | «Вычислить градиенты» | Обойти вычислительный граф в обратном порядке, вычисляя и накапливая градиенты для каждого листового тензора с requires_grad=True |
| .to(device) | «Перенести на GPU» | Рекурсивно перенести все параметры и буферы на указанное устройство (CPU, CUDA, MPS) |
| DataLoader | «Конвейер данных» | Итератор, который пакетирует, перемешивает и при необходимости параллелит загрузку данных из Dataset |
| Смешанная точность | «Использовать float16» | Обучать с float16 в прямом/обратном проходе для скорости, сохраняя основные веса float32 для численной стабильности |
| Немедленное выполнение | «Выполнить сейчас» | Операции выполняются сразу при вызове, а не откладываются до последующей компиляции: ключевой выбор дизайна, отличающий PyTorch от TF 1.x |
| zero_grad | «Сбросить градиенты» | Установить все градиенты параметров в ноль перед следующим обратным проходом, поскольку PyTorch по умолчанию накапливает градиенты |
Дополнительное чтение
- Paszke et al., «PyTorch: An Imperative Style, High-Performance Deep Learning Library» (2019) — оригинальная статья, объясняющая компромиссы дизайна PyTorch
- PyTorch Tutorials: «Learning PyTorch with Examples» (https://pytorch.org/tutorials/beginner/pytorch_with_examples.html) — официальный путь от тензоров к
nn.Module - PyTorch Performance Tuning Guide (https://pytorch.org/tutorials/recipes/recipes/tuning_guide.html) — смешанная точность, воркеры DataLoader, закреплённая память и другие оптимизации для продакшена
- Horace He, «Making Deep Learning Go Brrrr» (https://horace.io/brrr_intro.html) — почему обучение на GPU быстрое, с оптимизациями, специфичными для PyTorch
Источник: Introduction to PyTorch 03.10 — Создайте собственный мини-фреймворк · Фаза 3 — Основы глубокого обучения · Полный каталог · 03.12 — Введение в JAX