Тензоры и autograd в PyTorch
Переходим от micrograd к PyTorch: тензоры, dtype и device, автоматические градиенты через requires_grad и backward(), и когда GPU действительно быстрее CPU.
Зачем это нужно
Заголовок раздела «Зачем это нужно»В модуле 3.2 вы написали свой автоградавтоматическое дифференцирование (автоград)EN automatic differentiation (autograd)Программа записывает операции в граф и сама считает производные по цепному правилу. Для loss → параметры используют обратный режим (reverse mode) — это и есть backprop.: класс Value, который помнит, из чего он получен, и умеет
делать backward(). Это и есть ядро любого фреймворка глубокого обученияфреймворк глубокого обученияEN deep learning frameworkБиблиотека, которая даёт тензоры, автоматическое дифференцирование, слои и оптимизаторы и умеет считать на GPU: PyTorch, JAX, TensorFlow; в Китае — PaddlePaddle, MindSpore, Jittor..
Но у micrograd три ограничения. Он работает с отдельными числами, а не с матрицами. Он медленный: каждая
операция — это Python-объект. И он умеет считать только на процессоре.
PyTorch снимает все три. Идея та же — вычислительный графвычислительный графEN computational graphСхема вычисления — узлы-операции и рёбра-значения. По нему идёт прямой проход (значения) и обратный (градиенты). и обратный проход, — но единица вычислений теперь
тензортензорEN tensorВ ML — просто многомерный массив чисел. torch.Tensor устроен как np.ndarray, но умеет работать на GPU и считать градиенты.: таблица из тысяч или миллиардов чисел. Операции написаны на C++ и CUDA, а одна строка
x.to("cuda") переносит вычисления на видеокарту. На PyTorch написаны MiniMind, nanoGPT, открытый код
инференса DeepSeek-V3 и обучающие скрипты большинства открытых LLM. Всё, что вы сделаете дальше в курсе, будет
на нём.
Это принцип «сначала руками, потом фреймворк» в действии. Вы уже знаете, что происходит внутри
loss.backward(). Осталось выучить, как это пишется в PyTorch.
Тензор — это ndarray с тремя суперсилами
Заголовок раздела «Тензор — это ndarray с тремя суперсилами»Тензор в PyTorch почти неотличим от массива NumPy из урока 0.3.1: та же индексация, срезы, броадкастинг и векторизованные операции. Слово «тензор» пугает зря: это просто таблица чисел любой размерности — число, вектор, матрица или «стопка матриц». Отличий от NumPy три:
- Device — тензор может жить на видеокарте.
- Autograd — тензор может запоминать операции, чтобы потом посчитать градиент.
- dtype под GPU — по умолчанию float32 (у NumPy float64), есть bfloat16 и float8.
Создание
Заголовок раздела «Создание»import torch
a = torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]) # из вложенного списка Pythonprint(a)print(a.shape, a.dtype, a.device) # форма, тип чисел, устройство
z = torch.zeros(2, 3) # нули формы (2, 3)o = torch.ones(2, 3, dtype=torch.int64) # единицы целого типаr = torch.randn(2, 3) # случайные числа из N(0, 1) — «колокол» из урока 0.3.3i = torch.arange(5) # 0, 1, 2, 3, 4print(i, i.dtype) # целые → int64print(torch.tensor([1, 2, 3]) / 2) # деление целых даёт floattensor([[1., 2., 3.], [4., 5., 6.]])torch.Size([2, 3]) torch.float32 cputensor([0, 1, 2, 3, 4]) torch.int64tensor([0.5000, 1.0000, 1.5000])У каждого тензора три главных атрибута: форма массиваформа массиваEN shapeКортеж размеров по осям, например (batch, seq_len, d_model). (форма), тип элементов массиватип элементов массива (dtype)EN data type (dtype)float32, float16, int64, bool… Определяет точность и сколько байт занимает каждое число. (тип чисел) и
устройствоустройство (device)EN deviceГде хранится тензор и где выполняются операции с ним: "cpu", "cuda", "mps" (Apple), "npu" (Huawei Ascend через плагин torch_npu). Операнды одной операции должны быть на одном устройстве. (где он хранится). Когда что-то не работает, первым делом печатайте все три.
Мост с NumPy: общая память
Заголовок раздела «Мост с NumPy: общая память»import numpy as npimport torch
x_np = np.array([1.0, 2.0, 3.0])shared = torch.from_numpy(x_np) # ОБЩАЯ память с массивом NumPycopied = torch.tensor(x_np) # независимая копияx_np[0] = 100.0 # меняем исходный массив...print(shared) # ...и тензор изменился вместе с нимprint(copied) # а копия — нетtensor([100., 2., 3.], dtype=torch.float64)tensor([1., 2., 3.], dtype=torch.float64)Заметьте dtype=torch.float64: NumPy по умолчанию работает в float64, и тензор унаследовал этот тип. Слои PyTorch
хранят веса во float32. Подайте им такой тензор — получите одну из самых частых ошибок новичка:
RuntimeError: mat1 and mat2 must have the same dtype, but got Double and FloatDouble — это float64, Float — float32. Лечится явным приведением: x.float(). Обратно в NumPy — t.numpy()
(только для тензоров на CPU и без графа).
Операции и оси
Заголовок раздела «Операции и оси»Почти всё, что вы делали в NumPy, работает так же. Главное отличие в названии: вместо axis пишут dim.
x = torch.tensor([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]) # форма (2, 3)
print(x.sum()) # всё: 21print(x.sum(dim=0)) # сворачиваем ось 0 (строки): (3,)print(x.sum(dim=1, keepdim=True)) # сворачиваем ось 1, форма (2, 1)print(x.argmax(dim=1)) # индекс максимума в каждой строкеprint(x @ x.T) # матричное умножение (2,3)@(3,2) → (2,2)print(x.unsqueeze(0).shape) # добавить ось: (1, 2, 3)print(x[1, 2].item()) # одно число → обычный float Pythontensor(21.)tensor([5., 7., 9.])tensor([[ 6.], [15.]])tensor([2, 2])tensor([[14., 32.], [32., 77.]])torch.Size([1, 2, 3])6.0| NumPy | PyTorch | Комментарий |
|---|---|---|
np.array(lst) | torch.tensor(lst) | копирует данные |
a.sum(axis=0) | t.sum(dim=0) | keepdim вместо keepdims |
a.astype(np.float32) | t.float() или t.to(torch.float32) | |
np.concatenate | torch.cat | torch.stack — с новой осью |
a.T, a.transpose(...) | t.T, t.transpose(0, 1), t.permute(...) | permute — любая перестановка осей |
a.reshape(...) | t.reshape(...) или t.view(...) | view иногда падает с ошибкой там, где reshape работает; новичку проще всегда писать reshape |
float(a[0]) | t[0].item() | достать число из тензора |
dtype: сколько бит на одно число
Заголовок раздела «dtype: сколько бит на одно число»Для LLM тип чисел — вопрос не стиля, а денег: он определяет, сколько видеопамяти займут веса и насколько быстро идут вычисления. Сравним три главных типа:
for dt in [torch.float32, torch.float16, torch.bfloat16]: f = torch.finfo(dt) # параметры формата size = torch.tensor([], dtype=dt).element_size() # байт на число print(f"{str(dt):15} байт={size} max={f.max:.3g} eps={f.eps:.3g}")torch.float32 байт=4 max=3.4e+38 eps=1.19e-07torch.float16 байт=2 max=6.55e+04 eps=0.000977torch.bfloat16 байт=2 max=3.39e+38 eps=0.00781max — самое большое представимое число, eps — расстояние от 1 до следующего числа, то есть относительная
точность. Видно, чем различаются два 16-битных формата:
- float16 точнее (
eps≈ 0.001), но тесный: всё больше 65 504 превращается в бесконечность.torch.tensor(70000.0, dtype=torch.float16)даётinf. - bfloat16 держит тот же диапазон, что float32 (до ), зато точность грубая: примерно 2–3 значащие десятичные цифры. Поэтому LLM обучают именно в нём: переполнения почти не бывает.
Грубая точность bfloat16 коварна при накоплении. Сложим 0.01 тысячу раз:
s16 = torch.zeros((), dtype=torch.bfloat16) # форма () — тензор из одного числа (скаляр)s32 = torch.zeros(()) # то же во float32for _ in range(1000): s16 = s16 + 0.01 s32 = s32 + 0.01print(s16.item(), s32.item())4.0 10.000133514404297Вместо 10 получилось 4! Почему: компьютер хранит не любые дробные числа, а только числа с «сетки», и у bfloat16 эта сетка редкая. На значащие цифры (мантиссу, урок 0.0.2) у него отведено всего 7 бит, поэтому между 4 и 8 помещается лишь равноотстоящих значений с шагом . Когда сумма доросла до 4, ближайшие соседи — 4 и 4.03125. Прибавка 0.01 меньше половины этого шага, и округляется обратно к 4 — сумма застревает. Поэтому суммы, нормализации и softmax в LLM считают в float32, даже если веса хранятся в bf16. Подробно о форматах — в уроке «Смешанная точность».
Память под веса — простая арифметика: число параметров × байт на число. Модель на 7 млрд параметров:
| dtype | Байт на параметр | Веса 7B |
|---|---|---|
| float32 | 4 | 28 ГБ |
| bfloat16 | 2 | 14 ГБ |
| int8 / float8 | 1 | 7 ГБ |
| 4 бита | 0.5 | 3.5 ГБ |
Это только веса. При обучении добавятся градиенты, состояние оптимизатора и активации — об этом в следующем уроке.
Device: где живёт тензор
Заголовок раздела «Device: где живёт тензор»центральный процессорцентральный процессор (CPU)EN central processing unit (CPU)Универсальный «мозг» компьютера — от нескольких до десятков мощных ядер; каждое быстро выполняет любые инструкции одну за другой. — универсальный процессор с десятками ядер. графический процессорграфический процессор (GPU, видеокарта)EN graphics processing unit (GPU)Тысячи простых вычислительных ядер. Идеально подходит для матричных умножений, поэтому на GPU обучают нейросети. — тысячи простых ядер, которые
одновременно делают одно и то же над разными числами. Умножение матриц идеально ложится на GPU. Видеокарты NVIDIA
в PyTorch называются устройством "cuda" — по имени платформы CUDACUDAEN CUDA (Compute Unified Device Architecture)Платформа NVIDIA для вычислений на видеокартах. В PyTorch видеокарта NVIDIA — это устройство "cuda" ("cuda:0", "cuda:1"…)..
device = "cuda" if torch.cuda.is_available() else "cpu" # стандартная строка в начале скрипта
x = torch.randn(1000, 1000, device=device) # создать сразу на нужном устройствеy = torch.randn(1000, 1000).to(device) # или перенести существующий тензорz = x @ y # считается там, где лежат операндыprint(z.device)result = z.cpu() # вернуть на CPU, например для NumPyПравило одно: все операнды одной операции — на одном устройстве. Иначе получите ошибку, которую рано или поздно видит каждый (точная формулировка зависит от операции):
RuntimeError: Expected all tensors to be on the same device, but found at least two devices, cuda:0 and cpu!.to(device) для тензора возвращает новый тензор: x.to("cuda") без присваивания ничего не меняет. У модели
(nn.Module) .to() работает на месте — это тема следующего урока.
GPU против CPU: когда быстрее
Заголовок раздела «GPU против CPU: когда быстрее»GPU выигрывает, когда работы много и она однородна. Умножение двух матриц — это операций. Для — 137 миллиардов. Замерим:
import timeimport torch
def bench(device, n=4096, repeats=5): a = torch.randn(n, n, device=device) b = torch.randn(n, n, device=device) a @ b # прогрев: первый запуск всегда медленнее if device == "cuda": torch.cuda.synchronize() # ждём, пока GPU реально досчитает t0 = time.perf_counter() for _ in range(repeats): c = a @ b if device == "cuda": torch.cuda.synchronize() dt = (time.perf_counter() - t0) / repeats tflops = 2 * n**3 / dt / 1e12 # триллионов операций в секунду print(f"{device:4}: {dt * 1000:7.1f} мс на умножение, {tflops:.2f} TFLOPS")
bench("cpu")if torch.cuda.is_available(): bench("cuda")Числа сильно зависят от железа, поэтому запустите сами и запишите результат. Ориентиры такие. Процессор выдаёт от десятков гигафлопс на ноутбуке до единиц терафлопс на многоядерном сервере, и одно такое умножение занимает от десятков миллисекунд до секунд. Видеокарта уровня RTX 4080 по паспорту даёт около 49 TFLOPS во float32 (реальное умножение матриц достигает лишь части пика), то есть единицы миллисекунд на умножение. В bfloat16 включаются тензорные ядра — специальные блоки GPU для умножения матриц, и это ещё быстрее. Во сколько раз, проверьте сами. Как устроены тензорные ядра и память видеокарты — в уроке 5.7.1.
Три ловушки замеров и выбора устройства:
- Асинхронность. Python только ставит операцию на GPU в очередь и сразу идёт дальше. Без
torch.cuda.synchronize()вы измерите постановку в очередь — получится фантастически быстро и неправда. - Прогрев. Первая операция на GPU инициализирует CUDA и подбирает алгоритмы. Её не замеряют.
- Мелкие задачи. Запуск каждой операции на GPU стоит единицы–десятки микросекунд, а копирование CPU → GPU идёт по шине PCIe (канал связи между процессором и видеокартой), а это небыстро. Сложить два вектора по 10 чисел быстрее на CPU. Правило: GPU окупается на больших матрицах и батчах, а данные стоит переносить на видеокарту один раз и надолго.
Autograd: micrograd, только для тензоров
Заголовок раздела «Autograd: micrograd, только для тензоров»Теперь главное. Возьмём крошечную модель — один нейрон без активации — и квадратичную ошибку:
При , , , получаем и . Градиенты посчитаем руками по цепному правилу:
То есть, если чуть увеличить , loss вырастет примерно в 8 раз быстрее, а если — в 4 раза. Теперь то же самое на micrograd и на PyTorch:
from engine import Value # ваш engine.py из ДЗ s3-hw01-micrograd (или micrograd.engine)
x = Value(2.0)w = Value(3.0)b = Value(1.0)y = Value(5.0)
pred = w * x + bloss = (pred - y) ** 2loss.backward()print(w.grad, b.grad) # 8.0 4.0import torch
x = torch.tensor(2.0) # вход: градиент по нему не нуженw = torch.tensor(3.0, requires_grad=True) # вес: «следи за мной»b = torch.tensor(1.0, requires_grad=True) # сдвигy = torch.tensor(5.0) # правильный ответ
pred = w * x + b # прямой проход строит графloss = (pred - y) ** 2print(pred, loss)loss.backward() # обратный проход по графуprint(w.grad, b.grad) # dL/dw и dL/dbprint(x.grad) # None: requires_grad не просилиtensor(7., grad_fn=<AddBackward0>) tensor(4., grad_fn=<PowBackward0>)tensor(8.) tensor(4.)NoneКод почти построчно совпадает. Разница в одном: micrograd следит за каждым Value, а PyTorch — только за
тензорами, где вы явно попросили requires_grad=True, и за всем, что из них вычислено. Это экономия: градиент по
входным данным (картинкам, токенам) обычно не нужен, и PyTorch не тратит на него ни память, ни время.
Что происходит внутри
Заголовок раздела «Что происходит внутри»Во время прямого прохода PyTorch записывает граф. У каждого результата операции есть атрибут grad_fn —
«как меня получили и как через меня пропустить градиент назад». Это ровно ваши _prev и _backward из micrograd.
flowchart LR W["w = 3<br/>requires_grad"] --> M["MulBackward0<br/>w·x = 6"] X["x = 2"] --> M M --> A["AddBackward0<br/>pred = 7"] B["b = 1<br/>requires_grad"] --> A A --> S["SubBackward0<br/>pred − y = 2"] Y["y = 5"] --> S S --> P["PowBackward0<br/>loss = 4"]
# продолжаем пример вышеprint(w.is_leaf, pred.is_leaf) # True Falseprint(loss.grad_fn) # <PowBackward0 ...>print(loss.grad_fn.next_functions) # ((<SubBackward0 ...>, 0),)Тензоры, которые вы создали сами, — листьялистовой тензорEN leaf tensorТензор, созданный пользователем, а не операцией (is_leaf == True). Обычно это параметры модели и входы. Только у листьев с requires_grad после backward() заполняется .grad. графа. Только у листьев с
requires_grad=True после backward() заполняется .grad. У промежуточных (pred) градиент считается, но не
сохраняется: так экономится память. Если он всё-таки нужен для отладки, перед backward() вызовите pred.retain_grad().
Граф строится заново на каждом прямом проходе, прямо во время выполнения Python-кода. Это
динамический графдинамический граф (define-by-run)EN dynamic computational graph (define-by-run)Граф строится заново при каждом выполнении кода, поэтому работают обычные if и for. Так устроены micrograd и PyTorch; противоположность — статический граф (static graph), который сначала описывают, потом запускают. (define-by-run): внутри forward можно писать обычные if и
циклы. После backward() граф удаляется, чтобы освободить память. Повторный backward() по тому же графу даст
RuntimeError: Trying to backward through the graph a second time. В обычном обучении это и не нужно: на каждом
шаге граф строится заново.
Градиенты накапливаются
Заголовок раздела «Градиенты накапливаются»Как и в micrograd, backward() не записывает градиент, а прибавляет его к .grad. В модуле 3.2 вы видели
почему: если переменная используется в графе дважды, вклады обоих путей нужно сложить. Обратная сторона — градиенты
с прошлого шага никуда не исчезают:
x, y = torch.tensor(2.0), torch.tensor(5.0)w = torch.tensor(3.0, requires_grad=True) # свежие параметры: w.grad пока Noneb = torch.tensor(1.0, requires_grad=True)
for step in range(3): loss = (w * x + b - y) ** 2 # каждый раз строим граф заново loss.backward() print(f"шаг {step}: w.grad = {w.grad.item()}")
w.grad = None # обнуляем — так делает optimizer.zero_grad()loss = (w * x + b - y) ** 2loss.backward()print("после обнуления:", w.grad.item())шаг 0: w.grad = 8.0шаг 1: w.grad = 16.0шаг 2: w.grad = 24.0после обнуления: 8.0Забытое обнуление — баг номер один в обучении. Модель при этом не падает, просто учится всё хуже. В уроке «Как отлаживать обучение» вы увидите, как это выглядит на MNIST.
backward() — только от скаляра
Заголовок раздела «backward() — только от скаляра»v = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)q = v ** 2 # вектор, а не числоq.backward() # RuntimeError: grad can be implicitly created only for scalar outputsГрадиент определён для одного числа — функции потерь. Если выход — вектор, сначала сверните его: q.sum().backward()
даст v.grad = tensor([2., 4., 6.]), то есть . В обучении это происходит
естественно: loss всегда усредняют по батчу до одного числа.
Обновление весов: torch.no_grad()
Заголовок раздела «Обновление весов: torch.no_grad()»Соберём всё вместе и обучим линейную регрессию градиентным спуском. Как в уроке 2.2, только производные теперь считает autograd:
import torch
torch.manual_seed(0)X = torch.linspace(-1, 1, 50) # 50 точек на отрезке [-1, 1]Y = 3 * X + 1 + 0.1 * torch.randn(50) # «правда»: y = 3x + 1 плюс шум
w = torch.zeros((), requires_grad=True) # параметры-скаляры, начинаем с нуляb = torch.zeros((), requires_grad=True)lr = 0.1
for step in range(101): loss = ((w * X + b - Y) ** 2).mean() # MSE loss.backward() # заполняет w.grad и b.grad with torch.no_grad(): # обновление — не часть графа w -= lr * w.grad b -= lr * b.grad w.grad = None # обнуляем накопленные градиенты b.grad = None if step % 20 == 0: print(f"шаг {step:3d}: loss = {loss.item():.4f}, w = {w.item():.3f}, b = {b.item():.3f}")шаг 0: loss = 4.2035, w = 0.210, b = 0.201шаг 20: loss = 0.1907, w = 2.360, b = 0.996шаг 40: loss = 0.0214, w = 2.870, b = 1.005шаг 60: loss = 0.0119, w = 2.991, b = 1.005шаг 80: loss = 0.0113, w = 3.020, b = 1.005шаг 100: loss = 0.0113, w = 3.027, b = 1.005Модель нашла , ; отличие от 3 и 1 — из-за шума в данных. Loss остановился около 0.011: это примерно дисперсия шума (дисперсия — средний квадрат отклонения, урок 1.3.1). Шум случаен, и никакая прямая его не предскажет, поэтому лучше на этих данных не сделать.
Конструкция with ...: вам знакома по with open(...) из урока 0.2.4:
она включает особый режим на время выполнения вложенного блока и выключает его после.
Зачем torch.no_grad()? Строка w -= lr * w.grad — тоже операция над тензором с requires_grad. Без
no_grad PyTorch попытался бы записать её в граф, а изменять лист графа на месте (это
операция «на месте»операция «на месте» (in-place)EN in-place operationМеняет тензор, а не создаёт новый: x += 1, x.zero_(), x.add_(y). В PyTorch такие методы оканчиваются на _.) запрещено:
RuntimeError: a leaf Variable that requires grad is being used in an in-place operation.torch.no_grad() говорит: «сейчас граф не нужен». Его же используют при оценке модели и генерации текста: граф не
строится, и памяти тратится заметно меньше. Близкий родственник — t.detach(): тот же тензор, но отрезанный от графа.
Проверка: autograd против конечных разностей
Заголовок раздела «Проверка: autograd против конечных разностей»В уроке 3.1.2 вы проверяли backprop численно: . В PyTorch
такая проверка встроена (x.sin() — синус каждого элемента, .sum() сворачивает всё в одно число):
def f(x): return (x.sin() * x ** 2).sum()
x = torch.randn(5, dtype=torch.float64, requires_grad=True)print(torch.autograd.gradcheck(f, (x,))) # сравнит autograd с конечными разностями → Truegradcheck требует float64. В float32 ошибки округления в огромны: на этом
же примере численная производная расходится с точной на десятки процентов, и проверка не проходит. Пригодится,
когда будете писать собственные операции.
micrograd и PyTorch: что изменилось
Заголовок раздела «micrograd и PyTorch: что изменилось»Тесты micrograd сравнивают его градиенты с PyTorch. Сделаем то же — пример из README micrograd на PyTorch:
import torch
a = torch.tensor(-4.0, dtype=torch.float64, requires_grad=True)b = torch.tensor(2.0, dtype=torch.float64, requires_grad=True)c = a + bd = a * b + b**3c = c + c + 1c = c + 1 + c + (-a)d = d + d * 2 + (b + a).relu()d = d + 3 * d + (b - a).relu()e = c - df = e**2g = f / 2.0g = g + 10.0 / fg.backward()print(f"{g.item():.4f} {a.grad.item():.4f} {b.grad.item():.4f}")24.7041 138.8338 645.5773Те же числа, что в README micrograd. Алгоритм один и тот же, отличается масштаб:
| micrograd | PyTorch | |
|---|---|---|
| Единица вычислений | одно число Value | тензор любой формы |
| Операций | около десятка (+, *, **, tanh, exp, relu) | тысячи, включая свёртки и attention |
| Граф | Python-объекты, замыкания _backward | движок на C++, узлы grad_fn |
| Что отслеживается | каждое значение | только тензоры с requires_grad=True |
| Устройства | только CPU | CPU, GPU NVIDIA и AMD, Apple, NPU |
| Скорость | единицы микросекунд на одну скалярную операцию | одна операция над миллионами чисел за доли миллисекунды |
Последняя строка решает всё. Один слой 784 → 128 на батче из 64 картинок — это млн
умножений и столько же сложений. В micrograd это почти 13 миллионов объектов Value и десятки секунд на один прямой
проход. PyTorch делает ту же работу одним вызовом матричного умножения за доли миллисекунды — в десятки тысяч раз
быстрее, а на GPU ещё быстрее.
Практика
Заголовок раздела «Практика»Код урока запускается локально в окружении из урока 0.1 (pip install torch). Выполните три упражнения:
- Сверка с micrograd. В ДЗ s3-hw01-micrograd такую сверку за вас делали готовые тесты. Теперь напишите её сами:
возьмите свой
engine.pyи создайте рядомtest_vs_torch.pyпо образцуtest/test_engine.pyиз micrograd. Три выражения (сtanh,expи делением) считаются в обоих движках, затемassert abs(v.grad - t.grad.item()) < 1e-6. Тензоры PyTorch создавайте в float64, иначе погрешность float32 не уложится в допуск. Запустите черезpytest. Если тест упал, ищите ошибку в_backwardсоответствующей операции. - Регрессия на PyTorch. Измените пример с
y = 3x + 1так, чтобы модель училась на с тремя параметрами . Подберитеlrи число шагов, чтобы найти , , . Затем уберите строкиw.grad = Noneи опишите, что произошло. - Точность. Повторите опыт «1000 × 0.01» для float16 (должно выйти 9.953). Объясните через
epsи шаг сетки чисел около текущей суммы, почему ответ не ровно 10, но и не 4. При какой сумме застрял бы float16, если прибавлять 0.01 дальше? Проверьте расчёт циклом.
Проверь себя
Заголовок раздела «Проверь себя»- Тензор = массив NumPy +
device+ autograd. Первым делом при любой ошибке печатайтеshape,dtype,device. - dtype решает, сколько памяти займут веса: 4 байта (fp32), 2 (bf16), 1 (fp8). bfloat16 не переполняется, но точность у него грубая, поэтому суммы и нормализации считают в fp32.
- Autograd — тот же micrograd, только для тензоров и на C++:
requires_grad=True→ прямой проход строит граф →backward()складывает градиенты в.gradлистьев. - Градиенты накапливаются, обновление весов делают в
torch.no_grad(),backward()вызывают от скаляра. - GPU выигрывает на больших однородных вычислениях. Замеряйте с прогревом и
torch.cuda.synchronize().
Дальше: nn.Module, оптимизаторы, DataLoader и стандартный цикл обучения. Обучим классификатор рукописных
цифр MNIST на CPU и сохраним чекпоинт.