Урок 35. Знакомство с PyTorch / TensorFlow
Зачем тебе этот урок
В Уроке 34 ты руками написал Linear, ReLU, Sigmoid, MSE и backward к каждому слою. Теперь ты знаешь, что внутри фреймворка: PyTorch для тебя не магия, а автоматизация того, что ты уже делал вручную.
Тот же XOR и та же архитектура — но на инструменте, на котором пишут настоящие модели.
1. Что framework даёт поверх твоего NumPy-кода
| Ты писал сам в Уроке 34 | Что даёт PyTorch |
|---|---|
| backward() к каждому слою | Autograd — считает градиенты сам |
| NumPy, только CPU | Тот же код на видеокарте (CUDA) или на чипе Apple (MPS) |
| Linear, ReLU, Sigmoid | Сотни слоёв: Conv2d, LSTM, Attention, Dropout, BatchNorm |
w -= lr * grad | Оптимизаторы SGD, Adam, AdamW и планировщики learning rate |
| Датасет одним куском в память | Dataset и DataLoader: батчи, перемешивание, параллельная подгрузка |
| Ничего | Экосистема: torchvision, Hugging Face, предобученные модели |
Последняя строка весит больше остальных: в Уроке 38 ты дообучишь готовую сеть одной строкой именно потому, что веса лежат в общем формате.
2. Tensor — главный объект
Tensor — многомерный массив чисел: скаляр, вектор, матрица, картинка RGB. Внешне почти np.ndarray, API нарочно сделан похожим.
a = torch.tensor([[1., 2.], [3., 4.]])
print(a.shape) # torch.Size([2, 2]) — форма
print(a.dtype) # torch.float32 — тип чисел
print(a.device) # cpu — где лежат данные
b = torch.from_numpy(arr) # из NumPy, память общая; a.numpy() — обратно
Три отличия от NumPy:
- device. Знает, в какой памяти лежит:
cpu,cuda(NVIDIA),mps(Apple Silicon). - Градиенты. Помнит, как получился, и умеет по этой истории считать производные.
- dtype float32 по умолчанию, а не float64: видеокарты считают его быстрее, точности хватает.
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
x = torch.randn(1000, 1000).to(device) # перенесли на устройство
y = (x @ x).to("cpu") # посчитали там, вернули обратно
Главное правило: модель и данные лежат на одном устройстве, иначе PyTorch скажет «expected all tensors to be on the same device».
3. Autograd — почему больше не надо писать backward
В Уроке 34 ты для каждого слоя выводил производную и писал backward. Autograd делает это сам: с флагом requires_grad=True tensor на лету пишет граф вычислений — какие операции над ним делали. Потом PyTorch идёт по нему назад и применяет chain rule.
x = torch.tensor(3.0, requires_grad=True)
y = x ** 2 + 5 * x # PyTorch запомнил все операции
y.backward() # проход назад по графу
print(x.grad) # tensor(11.) — dy/dx = 2x + 5 = 11 при x = 3
У параметров модели флаг стоит автоматически: вызвал loss.backward() — во всех весах появилось .grad.
Почему градиенты обнуляют. PyTorch не заменяет .grad, а прибавляет к нему — так можно накопить градиент по нескольким мелким батчам и шагнуть один раз. Забудешь zero_grad() — сложатся все прошлые итерации, шаг выйдет огромным, обучение развалится.
Ещё режим torch.no_grad(): граф не строится и память не тратится — оборачивай в него инференс и валидацию.
4. Та же сеть, но через nn.Module
В Уроке 34 ты писал классы со своими forward и backward и собирал их в Network — ~80 строк. Стало одной строкой: nn.Sequential(nn.Linear(2, 4), nn.ReLU(), nn.Linear(4, 1)).
nn.Sequential годится, когда слои идут строго друг за другом. Появились ветвления — пишут свой класс, наследник nn.Module. Так выглядит почти весь код, который ты будешь читать:
class XORNet(nn.Module):
def __init__(self):
super().__init__() # обязательно, иначе слои не зарегистрируются
self.fc1 = nn.Linear(2, 8)
self.fc2 = nn.Linear(8, 1)
self.relu = nn.ReLU()
def forward(self, x): # только прямой проход
return self.fc2(self.relu(self.fc1(x)))
Ключевое отличие: метода backward здесь нет. Ты описываешь прямой проход, обратный autograd соберёт сам. Веса nn.Linear инициализируются разумно из коробки — про He и Xavier думать не нужно.
5. Цикл обучения построчно
Тот самый цикл из Урока 34. Выучи наизусть: он один и для картинок, и для текста, и для языковых моделей.
pred = model(xb) # 1. forward — прогон вперёд
loss = loss_fn(pred, yb) # 2. loss — насколько ошиблись
optimizer.zero_grad() # 3. стереть градиенты прошлой итерации
loss.backward() # 4. backward — посчитать новые
optimizer.step() # 5. update — сдвинуть веса
model(xb)запускаетforward; писатьmodel.forward(xb)напрямую не принято — не сработают внутренние хуки.optimizer.step()читает.gradи меняет веса — знает какие, потому что получилmodel.parameters().
6. Оптимизаторы: SGD и Adam
SGD — то, что ты писал сам: w ← w − lr · grad, один размер шага для всех параметров. Momentum добавляет разгон туда, куда градиент указывает стабильно: шарик катится по склону, а не пересчитывает маршрут каждый сантиметр.
Adam хранит для каждого параметра две скользящие средние — градиента и его квадрата — и по ним подбирает индивидуальный размер шага. Платишь памятью: две лишние величины на вес.
opt = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
opt = torch.optim.Adam(model.parameters(), lr=1e-3)
opt = torch.optim.AdamW(model.parameters(), lr=1e-3, weight_decay=0.01)
Что брать. По умолчанию Adam или AdamW с lr=1e-3: обучается почти всегда и без настройки. SGD с momentum — когда выжимают последние доли качества. AdamW — стандарт трансформеров.
7. Данные: Dataset, DataLoader, батчи
Dataset отвечает на два вопроса: сколько элементов и как достать i-й. DataLoader оборачивает его и выдаёт батчи.
from torch.utils.data import Dataset, DataLoader, TensorDataset
class MyDataset(Dataset):
def __len__(self): return len(self.X)
def __getitem__(self, i): return self.X[i], self.y[i]
loader = DataLoader(TensorDataset(X, y), batch_size=32, shuffle=True)
Зачем батчи. Память: датасет целиком в видеопамять не влезет, батч из 32 картинок влезет. Скорость: шаг раз в 32 примера, а не раз в 50000. Качество: шумный градиент по батчу помогает выбираться из плохих локальных минимумов — размер батча это гиперпараметр, а не только память.
Зачем shuffle. Если данные лежат по порядку (сначала нули, потом единицы), батчи выйдут однородными и модель будет мотать. Ставят на train, на валидации не нужен.
8. Режимы train() и eval()
Часть слоёв ведёт себя по-разному в обучении и в инференсе:
- Dropout в обучении случайно отключает часть нейронов, чтобы сеть не полагалась на отдельные признаки. В инференсе выключен: иначе один вход даст разные ответы.
- BatchNorm в обучении нормирует по статистике текущего батча, в инференсе берёт накопленное среднее — иначе ответ зависит от соседей по батчу.
model.train() # dropout активен, batchnorm обновляет статистику
model.eval() # dropout выключен, batchnorm использует накопленное
with torch.no_grad():
preds = model(X_test)
Это разные вещи: eval() переключает поведение слоёв, no_grad() отключает граф. На валидации нужны обе, и обе легко забыть — ничего не падает, метрики просто врут.
9. Сохранение и загрузка модели
state_dict — словарь «имя параметра → tensor с весами». Именно его пишут на диск.
torch.save(model.state_dict(), "model.pt")
model = XORNet() # сначала та же архитектура
model.load_state_dict(torch.load("model.pt")) # потом залить веса
model.eval()
Чтобы продолжить обучение с того же места, в файл кладут и optimizer.state_dict(). Модель целиком не сохраняют: такой файл привязан к структуре проекта, переименуешь класс — загрузка сломается. С чужими весами осторожно: формат допускает выполнение кода. В свежих версиях weights_only=True стоит по умолчанию, в старых — указывай явно.
10. Полный пример: XOR на PyTorch
Тот же XOR, но с шумом и в 400 точках, чтобы батчи имели смысл.
import torch
import torch.nn as nn
from torch.utils.data import TensorDataset, DataLoader
# 1. Данные: четыре угла XOR с шумом
corners = torch.tensor([[0., 0.], [0., 1.], [1., 0.], [1., 1.]])
X = corners.repeat(100, 1) + torch.randn(400, 2) * 0.1
y = torch.tensor([[0.], [1.], [1.], [0.]]).repeat(100, 1)
loader = DataLoader(TensorDataset(X, y), batch_size=32, shuffle=True)
# 2. Устройство: NVIDIA, чип Apple или CPU
device = torch.device("cuda" if torch.cuda.is_available()
else "mps" if torch.backends.mps.is_available() else "cpu")
# 3. Модель — архитектура из Урока 34
model = nn.Sequential(
nn.Linear(2, 8),
nn.ReLU(),
nn.Linear(8, 1), # логиты, sigmoid сидит внутри loss
).to(device)
loss_fn = nn.BCEWithLogitsLoss() # sigmoid + cross-entropy, так устойчивее
optimizer = torch.optim.Adam(model.parameters(), lr=0.01)
# 4. Цикл обучения
model.train()
for epoch in range(50):
for xb, yb in loader:
xb, yb = xb.to(device), yb.to(device)
loss = loss_fn(model(xb), yb)
optimizer.zero_grad()
loss.backward()
optimizer.step()
if epoch % 10 == 0:
print(f"epoch {epoch:3d} loss {loss.item():.4f}")
# 5. Проверка и сохранение
model.eval()
with torch.no_grad():
probs = torch.sigmoid(model(corners.to(device)))
print(probs.round().flatten().tolist()) # ждём [0.0, 1.0, 1.0, 0.0]
torch.save(model.state_dict(), "xor.pt")
Собственно обучение — пять строк внутри вложенного цикла, остальное данные, устройство и печать. У большой модели разрастутся данные и архитектура, а пять строк те же.
11. PyTorch vs TensorFlow / Keras
| PyTorch | TensorFlow / Keras | |
|---|---|---|
| Стиль | Обычный код Python, граф строится по ходу | Раньше граф объявляли заранее, теперь тоже по ходу |
| Уровень | Цикл обучения пишешь сам | Keras: model.fit() в одну строку |
| Где силён | Исследования, Hugging Face | Продакшн: сервинг, мобильные, браузер |
Почему в курсе PyTorch. Подавляющее большинство свежих статей, репозиториев и предобученных моделей выходит с кодом на PyTorch, а чужой код читаешь чаще, чем пишешь свой.
Где встретишь TensorFlow. В компаниях со стеком, собранным несколько лет назад; в мобильном и браузерном инференсе; вокруг инфраструктуры Google. Keras теперь работает поверх нескольких движков, так что «Keras» больше не равно «TensorFlow». Религиозной войны нет: понятия те же, выучив один — второй читаешь за вечер.
12. Где это запускать
- CPU. Для Уроков 33–35 хватает: пример обучается за секунды. Ставится через
pip install torch. - Apple Silicon. Backend MPS,
device = "mps", заметно быстрее CPU. Редкие операции бывают не реализованы — тогда помогает переменнаяPYTORCH_ENABLE_MPS_FALLBACK=1: она шлёт их на CPU. - NVIDIA. CUDA,
device = "cuda". Нужна сборка под твой драйвер — на сайте PyTorch есть селектор с командой. - Google Colab. Бесплатный GPU в браузере: Runtime → Change runtime type → GPU. Сессия умирает от простоя — сохраняй веса.
Если GPU нет — не блокер: Урок 36 про компьютерное зрение пойдёт и на CPU, а с Урока 37 работают три приёма — уменьшить датасет, уменьшить модель, дообучить последний слой предобученной сети (Урок 38). Третий превращает сутки на видеокарте в пять минут на ноутбуке.
13. Тонкости и подводные камни
- Забыл zero_grad. Градиенты накапливаются, шаг растёт, loss уходит в разнос или в NaN. Частая ошибка.
- Данные и модель на разных устройствах. «Expected all tensors to be on the same device» — забыл
.to(device), проверяй батчи внутри цикла. - Копишь loss как tensor.
total += lossтащит весь граф и ест память — толькоloss.item(). - Формы не совпали, но broadcasting сработал. Предсказание (N, 1) и ответы (N,) молча дадут матрицу N на N и неверный loss. Проверяй
.shape.
14. Глоссарий
Tensor
Многомерный массив в PyTorch: знает форму, тип чисел и устройство.
Autograd
Автоматическое дифференцирование: пишет граф операций и считает по нему градиенты при .backward().
nn.Module
Базовый класс модели и слоя. Пишешь forward, обратный проход собирается сам.
Optimizer
Объект, обновляющий веса по градиентам. SGD, Adam, AdamW.
DataLoader
Обёртка над Dataset: режет данные на батчи, перемешивает, грузит параллельно.
state_dict
Словарь «имя параметра → веса». Его пишут на диск и заливают обратно в модель.
15. Практика (60 минут)
- Поставь PyTorch, проверь
torch.cuda.is_available()иtorch.backends.mps.is_available(). - Повтори autograd:
x = torch.tensor(2.0, requires_grad=True),y = x ** 3,y.backward()—x.gradдаст 12. - Запусти пример из секции 10 — предсказания должны сойтись к [0, 1, 1, 0].
- Сломай его: закомментируй
optimizer.zero_grad(), посмотри, что станет с loss, верни обратно. - Замени Adam на
SGD(lr=0.01), потом наSGD(lr=0.01, momentum=0.9), потом поиграй сbatch_size(4, 32, 400). Что меняется в скорости и гладкости падения loss? - Открой код из Урока 34 рядом: найди, где твой
net.forward,net.backward,net.step. - Запиши в
progress.md: какое устройство доступно, что было безzero_grad, какая пара «оптимизатор + batch_size» сошлась быстрее.
16. Проверь себя
1. Чем tensor отличается от массива NumPy?
Знает своё устройство (cpu, cuda, mps), копит граф вычислений для градиентов, по умолчанию float32.
2. Что делает loss.backward()?
Идёт назад по графу и заполняет .grad у параметров. Веса не меняются — их меняет optimizer.step().
3. Зачем нужен zero_grad()?
PyTorch прибавляет новые градиенты к старым. Без обнуления складываются все прошлые итерации и обучение разваливается.
4. Чем model.eval() отличается от torch.no_grad()?eval() переключает dropout и batchnorm в режим инференса, no_grad() отключает граф. На валидации нужны обе.
5. Что брать по умолчанию, SGD или Adam?
Adam или AdamW с lr=1e-3: подбирает шаг для каждого параметра сам и обучается без настройки.
17. Что должно остаться в голове
- Фреймворк не делает ничего нового относительно Урока 34: автоматизирует backward, даёт GPU, слои и экосистему.
- Tensor = массив + устройство + история вычислений; модель и данные — на одном устройстве.
- Autograd строит граф на лету,
.backward()идёт назад и заполняет.grad. - Цикл обучения один: forward → loss → zero_grad → backward → step.
- Наследуешь
nn.Moduleи пишешь толькоforward: backward не нужен. - Adam с
lr=1e-3— разумный старт, SGD с momentum — для доводки. - Батчи — ради памяти, скорости и шума в градиенте;
shuffle=Trueтолько на train. train()иeval()переключают dropout и batchnorm,no_grad()— про память.- Сохраняй
state_dict, а не объект модели.
loss.backward(). Поэтому, когда сеть в Уроке 37 откажется учиться, ты будешь понимать, где искать.