Zero to Hero, лекция 1: нейросети и backprop с нуля — пишем micrograd
Подробный конспект первой лекции Карпатого: что измеряет производная, как класс Value записывает граф вычислений, как обратный проход раз за разом применяет цепное правило — и как на автограде в полторы сотни строк обучить настоящую нейросеть.
Коротко
Заголовок раздела «Коротко»- Карпатый с пустого Jupyter-ноутбука пишет micrograd — крошечный автоградавтоматическое дифференцирование (автоград)EN automatic differentiation (autograd)Программа записывает операции в граф и сама считает производные по цепному правилу. Для loss → параметры используют обратный режим (reverse mode) — это и есть backprop.: движок,
который сам считает производные. В оригинальном репозитории ядро (
engine.py) занимает меньше 100 строк, библиотека нейросетей поверх него (nn.py) — около 60. - Главная мысль лекции: нейросеть — это просто большое математическое выражение. Входы — данные и веса, выход — предсказание или loss. Обратное распространение (backpropобратное распространение ошибкиEN backpropagation) вообще не знает про нейросети: оно умеет дифференцировать любое выражение, а обучение сетей — лишь одно из применений.
- Backprop — это цепное правилоцепное правилоEN chain ruleПроизводная композиции — произведение производных звеньев — (f(g(x)))′ = f′(g(x))·g′(x). Математическая основа backprop., применённое рекурсивно от выхода к входам. Каждому узлу графа достаточно знать свою локальную производную: сложение «раздаёт» градиент без изменений, умножение домножает его на значение соседа, tanh — на .
- micrograd работает с отдельными числами (скалярами). Настоящие библиотеки упаковывают числа в тензоры, но, по словам Карпатого, математика от этого не меняется — меняется только скорость.
- Финал: свой многослойный перцептрон (MLP) на 41 параметр учится на четырёх примерах. По дороге Карпатый в кадре ловит два бага, которые делает почти каждый: градиент перезаписывается вместо накопления и градиенты не обнуляются между шагами.
Как смотреть вместе с курсом
Заголовок раздела «Как смотреть вместе с курсом»Лекция покрывает сразу два блока курса. Математика — производная, цепное правило, градиент — разобрана в уроках 1.2.1, 1.2.2 и 1.2.3. Сам micrograd курс пишет вместе с вами в модуле 3.2: урок 3.2.1 «Класс Value и вычислительный граф», 3.2.2 «Автоматическое дифференцирование» и 3.2.3 «Обучаем MLP на своём автограде».
Удобнее всего так: пройти урок 1.2.2, посмотреть первую половину лекции (до 1:22), затем уроки 3.2.1–3.2.2 и вторую половину. Если математика уже знакома, можно смотреть лекцию целиком перед модулем 3.2 — курс повторит то же самое в своём темпе и с домашним заданием. Новичку не стоит пропускать ни одной главы: лекция идёт медленно и нарочно показывает ошибки.
Код лекции — два ноутбука в папке lectures/micrograd
репозитория nn-zero-to-hero (MIT). Готовая библиотека — репозиторий karpathy/micrograd
(MIT). Для отрисовки графов нужен пакет graphviz, остальной код работает на чистом Python. Пояснения на китайском
есть в учебной версии курса на китайском (репозиторий karpathy-zero-to-hero-zh-transcripts):
ноутбук 001_micrograd.
Карта лекции
Заголовок раздела «Карта лекции»Таймкоды — из глав в описании видео.
| Время | Глава в оригинале | О чём |
|---|---|---|
| 0:00 | intro | что будет: от пустого ноутбука до обученной сети |
| 0:29 | micrograd overview | что такое автоград, пример из README |
| 8:09 | derivative of a simple function with one input | производная как наклон, численная оценка |
| 14:15 | derivative of a function with multiple inputs | частные производные на примере a*b + c |
| 19:11 | starting the core Value object of micrograd and its visualization | класс Value, граф, рисование через graphviz |
| 32:11 | manual backpropagation example #1: simple expression | обратный проход руками, цепное правило |
| 51:10 | preview of a single optimization step | шаг по градиенту увеличивает выход |
| 52:52 | manual backpropagation example #2: a neuron | нейрон с tanh, ещё раз руками |
| 1:09:05 | implementing the backward function for each operation | замыкания _backward |
| 1:17:36 | implementing the backward function for a whole expression graph | топологическая сортировка, backward() |
| 1:22:32 | fixing a backprop bug when one node is used multiple times | = против += |
| 1:27:05 | breaking up a tanh, exercising with more operations | exp, степень, деление, вычитание |
| 1:39:33 | doing the same thing but in PyTorch: comparison | тот же нейрон в PyTorch |
| 1:43:55 | building out a neural net library (multi-layer perceptron) in micrograd | Neuron, Layer, MLP |
| 1:51:08 | creating a tiny dataset, writing the loss function | четыре примера и квадратичный loss |
| 1:58:01 | collecting all of the parameters of the neural net | parameters() |
| 2:01:18 | doing gradient descent optimization manually, training the network | цикл обучения и баг с zero_grad |
| 2:14:03 | summary of what we learned, how to go towards modern neural nets | итоги, путь к GPT |
| 2:16:50 | walkthrough of the full code of micrograd on github | обзор репозитория |
| 2:21:13 | real stuff: diving into PyTorch, finding their backward pass for tanh | где в PyTorch живёт backward для tanh |
| 2:24:42 | conclusion | заключение |
1. Что такое micrograd
Заголовок раздела «1. Что такое micrograd»Автоград (automatic gradient) — программа, которая по записанному выражению сама находит производные выхода по всем
входам. В README micrograd есть нарочно запутанный пример: из двух чисел и через сложения, умножения,
степени и ReLU получается . Прямой проход даёт . После вызова g.backward() в a.grad лежит
, в b.grad — . Читать эти числа нужно так: если чуть увеличить , то вырастет примерно
в 138 раз сильнее этой прибавки.
Карпатый подчёркивает три вещи.
- Нейросеть — частный случай выражения. Входы — данные и веса, выход — предсказания или loss. Backprop работает с любым выражением, нейросети ему не нужны.
- Скаляры — ради понятности. micrograd дробит сеть до отдельных сложений и умножений. В реальной работе так никто не делает: числа складывают в тензоры, чтобы считать параллельно. Но математика та же.
- Всё остальное — эффективность. Его формулировка (около 6:50): «micrograd — это всё, что нужно для обучения нейросетей, а всё остальное — лишь эффективность». Эффективность, конечно, отдельная огромная тема, но принцип обучения умещается в полторы сотни строк.
2. Производная на пальцах
Заголовок раздела «2. Производная на пальцах»Прежде чем писать автоград, Карпатый добивается, чтобы было ясно, что измеряет производная. Он берёт параболу и не выводит формулу производной, а оценивает её численно — по определению:
Словами: сдвинь вход на крошечное , посмотри, на сколько изменился выход, и раздели на («подъём на пробег»). В точке получается почти ровно 14 (и правда: , ). В точке наклон отрицательный, : при движении вправо функция идёт вниз. В точке наклон нулевой — это вершина параболы.
Почему символьно никто не дифференцирует: выражение настоящей нейросети содержит десятки тысяч слагаемых. А численная оценка ломается, если слишком мало: числа с плавающей точкой конечны. Проверим:
def f(x): return 3*x**2 - 4*x + 5
for h in [1e-3, 1e-6, 1e-10, 1e-14, 1e-16]: print(h, (f(3 + h) - f(3)) / h)# 0.001 14.003 — грубо, но близко# 1e-06 14.000003 — хорошо# 1e-10 14.0000012# 1e-14 14.21 — уже ошибки округления# 1e-16 0.0 — 3 + 1e-16 в float64 равно 3, разность исчезлаДальше — функция трёх входов в точке , , . Сдвигаем по очереди каждый вход и видим: по наклон (это значение ), по наклон (значение ), по наклон . Это и есть частные производные — урок 1.2.3 называет их так же.
3. Класс Value: число, которое помнит своё происхождение
Заголовок раздела «3. Класс Value: число, которое помнит своё происхождение»Нейросети — огромные выражения, и нужна структура данных, которая их хранит. Карпатый заводит класс Value:
data— само число;_prev— из каких узлов получено (детей хранит множество, а в конструктор их передают кортежем_children);_op— какой операцией:'+','*','tanh';label— подпись для картинки;grad— производная итогового выхода по этому узлу. Сначала 0: считаем, что узел на выход не влияет.
Сложение и умножение работают через магические методымагический метод (dunder-метод)EN special method, magic methodМетоды вида __add__, __len__, __call__: Python вызывает их сам, когда вы пишете a + b, len(a), a(x). Python: выражение a + b
Python превращает в вызов a.__add__(b). Метод возвращает новый Value и запоминает, кто его родил. Так при
обычном вычислении L = (a*b + c) * f в памяти сам собой строится вычислительный графвычислительный графEN computational graphСхема вычисления — узлы-операции и рёбра-значения. По нему идёт прямой проход (значения) и обратный (градиенты)..
Функция draw_dot (около 40 строк поверх graphviz) рисует его: прямоугольники — значения, кружки — операции.
flowchart LR
a["a = 2"] --> m(("*"))
b["b = -3"] --> m
m --> e["e = -6"]
e --> p(("+"))
c["c = 10"] --> p
p --> d["d = 4"]
d --> m2(("*"))
f["f = -2"] --> m2
m2 --> L["L = -8"]4. Обратный проход руками
Заголовок раздела «4. Обратный проход руками»Теперь нужно заполнить grad у каждого узла: . Карпатый делает это вручную,
от конца к началу, и каждое значение проверяет численно. Для этого он пишет функцию-«песочницу» lol(): она дважды
строит граф, во второй раз добавив к одному из узлов, и печатает . Карпатый называет это
встроенной проверкой градиента (gradient checkпроверка градиентаEN gradient checkСравнение аналитического градиента с численным (центральные разности) по относительной ошибке. Главный способ найти баг в backward.).
| Узел | Как получен | Градиент | Почему |
|---|---|---|---|
| выход | изменение на меняет на | ||
| производная произведения по — второй множитель | |||
| то же, симметрично | |||
| , | сложение передаёт градиент без изменений | ||
| цепное правило: градиент × локальная производная | |||
| то же |
Самое важное место лекции — градиент . Узел «плюс» ничего не знает о том, что происходит дальше в графе. Он знает только локальную производную: . А нам нужна . Связывает их цепное правило:
Карпатый поясняет его примером из Википедии: машина едет вдвое быстрее велосипеда, велосипед вчетверо быстрее пешехода — значит, машина в раз быстрее пешехода. Скорости изменения перемножаются.
Отсюда два правила, которые дальше повторяются всю серию:
- сложение — раздатчик: градиент выхода просто копируется во все входы;
- умножение — обменник: градиент входа равен градиенту выхода, умноженному на значение другого входа.
Backprop целиком, по словам Карпатого, — это рекурсивное применение цепного правила назад по графу. Чтобы показать, зачем всё это, он делает один шаг «оптимизации»: сдвигает листья на , то есть по направлению градиента. растёт с до , как и ожидалось.
5. Нейрон с tanh
Заголовок раздела «5. Нейрон с tanh»Второй пример — модель нейрона: входы , веса , смещение , сумма и функция активации tanhгиперболический тангенсEN hyperbolic tangent (tanh)tanh x = (eˣ − e⁻ˣ)/(eˣ + e⁻ˣ) — «сигмоида» со значениями от −1 до 1. Производная 1 − tanh² x., которая сжимает любое число в интервал от до . Число Карпатый подбирает нарочно, чтобы числа вышли круглыми: при , , , получается и , то есть .
tanh реализован одной операцией, а не через экспоненты. Его локальная производная берётся из таблицы: . Подставляем и получаем ровно . Дальше работают два правила из прошлого раздела:
| Узел | Градиент | Комментарий |
|---|---|---|
| , , , | через tanh, потом плюсы раздают | |
| вход нулевой — вес ни на что не влияет |
Ноль у Карпатый объясняет интуитивно: пока , шевели этот вес как угодно — выход нейрона не изменится, значит, и производная нулевая.
6. Автоматизируем: _backward и топологическая сортировка
Заголовок раздела «6. Автоматизируем: _backward и топологическая сортировка»Хватит считать руками. Каждая операция при создании результата кладёт в него маленькую функцию _backward —
замыканиезамыканиеEN closureВнутренняя функция, которая запоминает переменные внешней функции и после её завершения. Так устроены планировщики learning rate., которое помнит входы и знает, как передать им градиент. Для листьев это пустая
функция. В кадре случается смешная ошибка: Карпатый пишет out._backward = _backward() со скобками, сохраняет None
вместо функции и получает «‘NoneType’ object is not callable». Он оставляет её в видео.
Вызывать _backward нужно в правильном порядке: узел можно обработать только тогда, когда градиент от всех
его потребителей уже пришёл. Такой порядок даёт топологическая сортировкатопологическая сортировкаEN topological sortПорядок узлов ориентированного графа без циклов, в котором каждый узел идёт после всех, от кого зависит. backward() обходит его в обратную сторону.:
узел добавляется в список только после всех своих детей, а обратный проход идёт по списку задом наперёд.
Код из ноутбука лекции (MIT), комментарии наши:
import math
class Value: def __init__(self, data, _children=(), _op=''): self.data = data self.grad = 0.0 # dL/d(этот узел); 0 — «пока не влияет» self._backward = lambda: None # у листьев передавать градиент некому self._prev = set(_children) # из каких узлов получен self._op = _op # какой операцией
def __add__(self, other): other = other if isinstance(other, Value) else Value(other) # 2 → Value(2) out = Value(self.data + other.data, (self, other), '+') def _backward(): # сложение раздаёт градиент обоим входам self.grad += 1.0 * out.grad other.grad += 1.0 * out.grad out._backward = _backward # сохраняем функцию, а не результат вызова! return out
def __mul__(self, other): other = other if isinstance(other, Value) else Value(other) out = Value(self.data * other.data, (self, other), '*') def _backward(): # умножение: градиент × значение соседа self.grad += other.data * out.grad other.grad += self.data * out.grad out._backward = _backward return out
def tanh(self): t = math.tanh(self.data) out = Value(t, (self,), 'tanh') def _backward(): # tanh'(x) = 1 - tanh(x)^2 self.grad += (1 - t**2) * out.grad out._backward = _backward return out
def backward(self): topo, visited = [], set() def build_topo(v): # узел попадает в список после всех детей if v not in visited: visited.add(v) for child in v._prev: build_topo(child) topo.append(v) build_topo(self) self.grad = 1.0 # базовый случай: dL/dL = 1 for node in reversed(topo): # от выхода к листьям node._backward()
x1, x2 = Value(2.0), Value(0.0)w1, w2 = Value(-3.0), Value(1.0)b = Value(6.8813735870195432)o = (x1*w1 + x2*w2 + b).tanh()o.backward()print(round(o.data, 4), x1.grad, w1.grad, x2.grad, w2.grad)# 0.7071 -1.5 1.0 0.5 0.0 (с точностью до округления)7. Баг: узел используется дважды
Заголовок раздела «7. Баг: узел используется дважды»«Не стоит слишком радоваться», — говорит Карпатый и показывает баг. Возьмём b = a + a при . Производная
по равна 2, а первая версия кода выдаёт 1. Причина: в __add__ и self, и other — один и тот же объект,
и вторая запись grad = ... затирает первую. Тот же баг ломает любое выражение, где переменная встречается больше
одного раза, например .
Исправление — многомерное цепное правило: если узел влияет на выход по нескольким путям, вклады путей
складываются. Поэтому в _backward везде стоит +=, а grad стартует с нуля. Курс разбирает это в разделе
«Накопление градиентов: почему +=» урока 3.2.2 и раньше — в разделе
«Ветвление: градиенты складываются» урока 1.2.2.
8. Больше операций — и свобода в выборе «атомов»
Заголовок раздела «8. Больше операций — и свобода в выборе «атомов»»Чтобы потренироваться, Карпатый раскладывает tanh на более простые операции: . Для этого нужны:
__radd__и__rmul__, чтобы работало2 * a: Python сначала пробуетint.__mul__, не справляется и вызывает запасной метод у правого операнда (перегрузка операторовперегрузка операторовEN operator overloading в уроке 0.2.3);exp— его локальная производная равна самому , которое уже лежит вout.data;- степень с постоянным показателем,
x**k, с производной ; деление выражается какa * b**-1; - вычитание как сложение с отрицанием, а отрицание — умножение на .
Граф становится длиннее, но прямой проход и градиенты листьев совпадают с версией, где tanh — одна операция.
Вывод, который Карпатый повторит в следующих лекциях: на каком уровне делать операции, решать вам. Можно
дифференцировать отдельный плюс, а можно целый tanh. Важно лишь, чтобы операция умела прямой проход и знала свою
локальную производную. (В ноутбуке оставлено примечание: в видео в exp по ошибке стояло = вместо +=, в коде
это исправлено.)
9. То же самое в PyTorch
Заголовок раздела «9. То же самое в PyTorch»micrograd намеренно повторяет API PyTorch. Тот же нейрон на PyTorch выглядит так:
import torch
# тензоры из одного числа; .double() — float64, как обычные числа Pythonx1 = torch.Tensor([2.0]).double(); x1.requires_grad = Truex2 = torch.Tensor([0.0]).double(); x2.requires_grad = Truew1 = torch.Tensor([-3.0]).double(); w1.requires_grad = Truew2 = torch.Tensor([1.0]).double(); w2.requires_grad = Trueb = torch.Tensor([6.8813735870195432]).double(); b.requires_grad = True
o = torch.tanh(x1*w1 + x2*w2 + b)o.backward()print(o.item()) # 0.7071...print(x2.grad.item(), w2.grad.item(), x1.grad.item(), w1.grad.item())# ≈ 0.5 0.0 -1.5 1.0 — те же числа, что у microgradОтличия Карпатый перечисляет сам. В PyTorch всё — тензоры, то есть массивы чисел. По умолчанию они хранятся
в float32, отсюда приведение к double. У листьев requires_grad по умолчанию выключен ради экономии: градиенты
по входным данным обычно не нужны. .item() достаёт число из тензора с одним элементом. В остальном micrograd — это
частный случай PyTorch, когда все тензоры состоят из одного числа. Подробно это сравнение проводит урок
3.3.1.
10. Мини-библиотека нейросетей
Заголовок раздела «10. Мини-библиотека нейросетей»Поверх Value строится то, что в micrograd лежит в nn.py:
import random
class Neuron: def __init__(self, nin): self.w = [Value(random.uniform(-1, 1)) for _ in range(nin)] # по весу на вход self.b = Value(random.uniform(-1, 1)) # смещение def __call__(self, x): act = sum((wi * xi for wi, xi in zip(self.w, x)), self.b) # w·x + b return act.tanh() def parameters(self): return self.w + [self.b]
class Layer: # слой — независимые нейроны на общем входе def __init__(self, nin, nout): self.neurons = [Neuron(nin) for _ in range(nout)] def __call__(self, x): outs = [n(x) for n in self.neurons] return outs[0] if len(outs) == 1 else outs def parameters(self): return [p for n in self.neurons for p in n.parameters()]
class MLP: # перцептрон — слои друг за другом def __init__(self, nin, nouts): sz = [nin] + nouts self.layers = [Layer(sz[i], sz[i + 1]) for i in range(len(nouts))] def __call__(self, x): for layer in self.layers: x = layer(x) return x def parameters(self): return [p for layer in self.layers for p in layer.parameters()]Метод __call__ позволяет вызывать объект как функцию: n(x). Хитрость с sum(..., self.b): у sum есть второй
аргумент — начальное значение, и сумма начинается сразу со смещения. Сеть MLP(3, [4, 4, 1]) — три входа, два
скрытых слоя по четыре нейрона и один выход. Параметров , и ,
всего 41.
Данные — четыре примера по три числа и желаемые ответы . Это крошечная бинарная классификация. Качество измеряет одно число — функция потерьфункция потерьEN loss function: сумма квадратов разностей между предсказаниями и ответами (MSEсреднеквадратичная ошибка (MSE)EN mean squared error (MSE)Корень из MSE — RMSE (root mean squared error), измеряется в тех же единицах, что и целевая переменная.). Loss равен нулю только при идеальных предсказаниях и тем больше, чем сильнее промахи. Карпатый рисует граф этого loss и ужасается: в нём четыре полных прямых прохода сети и квадратичная потеря поверх. Градиенты получают все листья, в том числе входные данные, но данные менять нельзя. Нужны только градиенты по весам и смещениям.
11. Градиентный спуск и баг с обнулением
Заголовок раздела «11. Градиентный спуск и баг с обнулением»Градиент указывает направление, в котором loss растёт. Поэтому шаг делается в обратную сторону:
p.data += -0.01 * p.grad. Карпатый сначала ошибается со знаком и разбирает на конкретном весе, почему нужен минус.
Цикл «прямой проход → обратный проход → шаг» повторяется. Loss падает с 4.84 до 4.36, потом до 3.9 и дальше.
С шагом Карпатый экспериментирует. При слишком большом learning rate сеть «перепрыгивает»: градиент описывает loss только локально, и далёкий шаг уводит туда, где картина совсем другая. Подбор шага он называет «тонким искусством» (subtle art). Подробно про learning rateскорость обученияEN learning rate — в уроке 1.4.1.
Затем оформляется настоящий цикл обучения:
xs = [[2.0, 3.0, -1.0], [3.0, -1.0, 0.5], [0.5, 1.0, 1.0], [1.0, 1.0, -1.0]]ys = [1.0, -1.0, -1.0, 1.0] # желаемые ответыn = MLP(3, [4, 4, 1])
for k in range(20): ypred = [n(x) for x in xs] # прямой проход loss = sum((yout - ygt)**2 for ygt, yout in zip(ys, ypred)) # сумма квадратов ошибок for p in n.parameters(): p.grad = 0.0 # обнулить! иначе += копит градиенты прошлых шагов loss.backward() # обратный проход for p in n.parameters(): p.data += -0.1 * p.grad # шаг против градиента print(k, loss.data)И тут Карпатый признаётся, что в первой версии этого цикла не было обнуления градиентов. Это одна из самых частых
ошибок, и он делал её, по его словам, «в двадцатый раз в жизни». Он вспоминает свой старый твит со списком
типичных ошибок в нейросетях — эта там под номером три. Раз _backward прибавляет к grad, градиенты всех
прошлых шагов складывались. Сеть всё равно обучилась, причём быстрее: задача слишком простая, а накопленный
градиент работал как огромный шаг. На задаче посложнее такой баг сорвал бы обучение. Урок Карпатого — нейросеть
может «работать» и с багом, поэтому одной сходимости loss мало. После исправления спуск идёт медленнее, но честно.
12. Итоги: от 41 параметра до GPT
Заголовок раздела «12. Итоги: от 41 параметра до GPT»Карпатый подытоживает. Нейросеть — математическое выражение. Входы — данные и параметры. Поверх стоит loss, который мал, когда сеть делает то, что нужно. Backprop даёт градиент. Градиентный спуск много раз сдвигает параметры против градиента. GPT с сотнями миллиардов параметров обучается на тех же принципах. Отличий немного: вместо квадратичной потери там кросс-энтропияперекрёстная энтропияEN cross-entropy для предсказания следующего токена, а вместо простого шага — более сложное правило обновления. Сеть, обученная предсказывать следующее слово на текстах из интернета, проявляет удивительные свойства, но механика обучения та же.
Обзор репозитория micrograd показывает, чем он отличается от кода лекции:
- в
engine.pyвместо tanh нелинейность ReLU (на момент записи tanh в библиотеке не было). Разница, по словам Карпатого, несущественна, а tanh в лекции выбран потому, что его производная сложнее и лучше тренирует; - в
nn.pyесть базовый классModuleс методомzero_grad()— какnn.Moduleв PyTorch; - тесты в
test/сравнивают прямой и обратный проход micrograd с PyTorch на нескольких выражениях; demo.ipynb— задача посложнее: точки двух классов на плоскости («луны»). Там сеть побольше, мини-батчи, другая потеря (max-margin, она же hinge), L2-регуляризация и убывающий learning rate. Курс решает ту же задачу в уроке 3.2.3, только с логистической потерей.
13. «Настоящий» PyTorch: где живёт backward для tanh
Заголовок раздела «13. «Настоящий» PyTorch: где живёт backward для tanh»В конце Карпатый ищет в исходниках PyTorch обратный проход tanh — аналог нашей строки (1 - t**2) * out.grad. Минут
пятнадцать поиска ничего не дают: по запросу «tanh» находятся тысячи упоминаний в сотнях файлов. В итоге код
нашёлся через чужое обсуждение: есть отдельное ядро для CPU и отдельное для GPU. CPU-версия длинная, потому что
обрабатывает комплексные числа и тип bfloat16, а в глубине стоит та же формула «градиент × (1 − b²)». Его вывод:
большие библиотеки сделаны, чтобы ими пользоваться, а не чтобы их читать. micrograd простой, а найти нужное место
в «настоящем» коде трудно.
Последнее, что он показывает, — как добавить в PyTorch собственную операцию. Нужно унаследоваться от
torch.autograd.Function и описать два метода: forward (как считать) и backward (локальный градиент). В
документации пример — многочлен Лежандра третьей степени. Это ровно то, что мы делали с Value: если операция
умеет прямой проход и знает свою локальную производную, она становится ещё одним «кубиком лего» в большом графе.
Где курс объясняет то же самое и что есть сверх курса
Заголовок раздела «Где курс объясняет то же самое и что есть сверх курса»| Тема лекции | Где в курсе | Что в лекции сверх курса |
|---|---|---|
| Производная как наклон, численная оценка | 1.2.1 | эксперимент с размером и ошибками округления — в видео вживую |
| Цепное правило, граф, сумма по ветвям | 1.2.2, 1.2.3 | ручной backprop с численной проверкой каждого узла |
| Нейрон, слой, MLP; прямой и обратный проход | 3.1.1, 3.1.2 | — |
Класс Value, перегрузка операторов, рисование графа | 3.2.1 | draw_dot через graphviz, разбор tanh на exp |
_backward, топологическая сортировка, += | 3.2.2 | баги в кадре: None вместо функции, перезапись градиента |
Neuron / Layer / MLP, цикл обучения, zero_grad | 3.2.3 | как баг с обнулением маскируется на простой задаче |
| Сравнение с PyTorch | 3.3.1 | поиск backward tanh в исходниках PyTorch, autograd.Function |
| Градиентный спуск, learning rate | 1.4.1 | — |
Курс в модуле 3.2 идёт дальше лекции в двух местах. Сеть учится на «лунах», а не на четырёх точках. И есть
домашнее задание s3-hw01-micrograd с автотестами, которое сверяет ваш движок с эталоном.
Что сделать руками
Заголовок раздела «Что сделать руками»- Повторите лекцию с пустого ноутбука, не копируя код. Карпатый советует именно так: смотреть, ставить на паузу, набирать самому.
- Для выражения посчитайте все градиенты на бумаге, затем сверьте с
L.backward()и с численной проверкой. - Упражнения из описания видео (Colab-ноутбук; его копия с пояснениями на китайском лежит в репозитории
karpathy-zero-to-hero-zh-transcripts, файл
001_micrograd/micrograd_exercises.ipynb):- для выведите аналитический градиент в точке , затем оцените его численно;
- сравните обычную разность с симметричной : какая точнее при том же ?
- допишите в
Valueоперацииexp,log, деление и другие, соберите softmax и отрицательное лог-правдоподобие, сравните градиенты с PyTorch.
- Уберите из
_backwardвсе+=(замените на=) и найдите самое короткое выражение, на котором градиент становится неверным. - Уберите обнуление градиентов из цикла обучения и сравните кривые loss при learning rate 0.01 и 0.1.
- Добавьте в
Valueметодrelu()и обучитеMLPс ReLU вместо tanh. Что меняется в скорости обучения?
Словарик
Заголовок раздела «Словарик»| EN | RU |
|---|---|
| autograd engine | автоград, движок автоматического дифференцирования |
| backpropagation | обратное распространение ошибки, backprop |
| derivative / slope | производная / наклон |
| chain rule | цепное правило |
| local derivative | локальная производная |
| computational graph, expression graph | вычислительный граф |
| leaf node | лист графа |
| topological sort | топологическая сортировка |
gradient accumulation (+=) | накопление градиента |
| gradient check | проверка градиента |
| forward pass / backward pass | прямой проход / обратный проход |
| neuron, weights, bias | нейрон, веса, смещение |
| activation function, squashing function | функция активации, «сжимающая» функция |
| multi-layer perceptron (MLP) | многослойный перцептрон |
| loss, mean squared error | функция потерь, среднеквадратичная ошибка |
| gradient descent, step size, learning rate | градиентный спуск, размер шага, learning rate |
| zero grad | обнуление градиентов |
| learning rate decay | затухание learning rate |