Перейти к содержимому
Ступень 33.4 Как обучать глубокие сети

CNN и RNN — краткая история архитектур

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

УРОКstage-3/3-4-deep-training/03-cnn-rnn.mdx1 ч 30 минУрок 3 из 3
После урока вы сможете
  • Посчитать свёртку руками, размер выхода и число параметров свёрточного слоя и объяснить, почему CNN экономнее MLP на картинках
  • Записать уравнения RNN и LSTM и объяснить, почему градиент в RNN затухает во времени, а ячейка LSTM его сохраняет
  • Сравнить MLP, CNN, RNN и self-attention по параллельности, длине пути между позициями и индуктивному смещению
  • Объяснить, какие идеи CNN и RNN живут в современных LLM (короткие свёртки, линейное внимание, гибридные модели)

До 2017 года глубокое обучение держалось на двух архитектурах. Свёрточные сети (свёрточная нейросетьсвёрточная нейросетьEN convolutional neural network (CNN)) научили компьютер видеть: от чтения рукописных цифр до распознавания тысяч классов объектов. Рекуррентные сети (рекуррентная нейросетьрекуррентная нейросетьEN recurrent neural network (RNN)) читали текст, речь и временные ряды по одному элементу за раз. Остаточные связи и BatchNorm из прошлых уроков появились именно в CNN.

Трансформер вытеснил обе архитектуры из языковых моделей, но не отменил их. ViT (трансформер для картинок) режет картинку на патчи — квадратные кусочки 16×16 пикселей, — и в коде это делает свёртка с ядром и шагом 16 (ступень 9). В слоях линейного внимания Qwen3-Next и Kimi Linear стоят короткие свёртки, а сами эти слои — это RNN с матричным состоянием. Чтобы понять, почему трансформер победил и почему RNN возвращаются, нужно знать, как они устроены и где ломались. В подборке статей Ли Му трансформер так и назван: «четвёртый большой класс архитектур после MLP, CNN и RNN».

Цветная картинка 224×224 — это 224⋅224⋅3=150 528224 \cdot 224 \cdot 3 = 150\,528 чисел. Полносвязный слой на 1000 нейронов потребует 150 миллионов весов, и это только первый слой. Хуже того, MLP не знает, что пиксели соседние: если кошку сдвинуть на 10 пикселей, для него это совсем другой вход, и всё придётся учить заново для каждой позиции.

У картинок есть две подсказки, которые хочется встроить в архитектуру:

  1. Локальность. Смысл складывается из соседних пикселей: край, угол, текстура видны в маленьком окне.
  2. Сдвиг не меняет смысла. Кошка в левом углу и в правом — всё ещё кошка. Детектор «уха» должен работать в любом месте.

свёрткасвёрткаEN convolutionМаленькое окно весов скользит по входу; в каждой позиции считается скалярное произведение окна и фрагмента входа. В нейросетях это формально взаимная корреляция (cross-correlation). воплощает обе идеи. Маленькое окно весов — ядроядро свёртки (фильтр)EN convolution kernel (filter)Обучаемая матрица весов свёртки, например 3×3; одна и та же во всех позициях входа., например 3×3, — скользит по картинке. В каждой позиции считается сумма поэлементных произведений окна и фрагмента картинки:

yij=∑u=0k−1∑v=0k−1Kuv xi+u, j+v+b.y_{ij} = \sum_{u=0}^{k-1}\sum_{v=0}^{k-1} K_{uv}\, x_{i+u,\, j+v} + b.

Здесь написано: выход в точке (i,j)(i, j) — скалярное произведение ядра KK и квадрата картинки размера k×kk \times k, который начинается в (i,j)(i, j), плюс смещение bb. Одно и то же ядро используется во всех позициях — это разделение весовразделение весовEN weight sharingОдни и те же параметры применяются в разных местах — ядро свёртки во всех позициях картинки, матрицы RNN на всех шагах времени.. Поэтому свёртка обладает свойством эквивариантность к сдвигуэквивариантность к сдвигуEN translation equivarianceСдвинули вход — выход свёртки сдвинулся так же. Это индуктивное смещение CNN; d2l-zh говорит о близком свойстве — инвариантности к сдвигу.: сдвинули картинку — карта откликов сдвинулась так же.

Пример. Картинка 5×5: два левых столбца тёмные (0), три правых светлые (1). Ядро ищет переход «тёмное → светлое»:

K=(−101−101−101).K = \begin{pmatrix} -1 & 0 & 1 \\ -1 & 0 & 1 \\ -1 & 0 & 1 \end{pmatrix}.

Окно в первой позиции захватывает столбцы (0,0,1)(0, 0, 1): в каждой строке −1⋅0+0⋅0+1⋅1=1-1 \cdot 0 + 0 \cdot 0 + 1 \cdot 1 = 1, итого 3. Во второй позиции столбцы (0,1,1)(0, 1, 1) дают те же 3. В третьей позиции окно целиком на светлом: −1+0+1=0-1 + 0 + 1 = 0. Ядро «загорается» на границе и молчит на однородном фоне. Выход — карта признаков (feature map) 3×3, по-китайски буквально «изображение признаков».

Размер выхода. Ядро k×kk \times k на входе n×nn \times n даёт n−k+1n - k + 1 позиций по каждой оси. Чтобы размер не уменьшался, вокруг картинки добавляют рамку из нулей — дополнениедополнение (padding)EN paddingРамка из нулей вокруг входа, чтобы свёртка не уменьшала размер: для ядра 3×3 и шага 1 нужна рамка шириной 1. pp. Чтобы выход стал меньше, окно двигают с шагомшаг свёртки (stride)EN strideНа сколько позиций сдвигается окно свёртки; при шаге 2 выход вдвое меньше по каждой оси. ss. Общая формула:

nout=⌊n+2p−ks⌋+1.n_{out} = \left\lfloor \frac{n + 2p - k}{s} \right\rfloor + 1.

Скобки ⌊⋅⌋\lfloor \cdot \rfloor означают округление вниз до целого: ⌊3.7⌋=3\lfloor 3.7 \rfloor = 3 (окно, которое не помещается целиком, не считается). Проверим формулу на примере выше: n=5n = 5, k=3k = 3, p=0p = 0, s=1s = 1 даёт (5+0−3)/1+1=3(5 + 0 - 3)/1 + 1 = 3 — та самая карта 3×3. Для n=28n = 28, k=3k = 3, p=1p = 1, s=1s = 1 размер сохраняется: (28+2−3)/1+1=28(28 + 2 - 3)/1 + 1 = 28. Первый слой ResNet (k=7k = 7, p=3p = 3, s=2s = 2) превращает 224 в ⌊(224+6−7)/2⌋+1=⌊111.5⌋+1=112\lfloor (224 + 6 - 7)/2 \rfloor + 1 = \lfloor 111.5 \rfloor + 1 = 112.

Каналы. На входе у цветной картинки 3 канала (R, G, B), поэтому ядро на самом деле трёхмерное: 3×3×33 \times 3 \times 3. Слой учит сразу много ядер, например 64, и каждое даёт свою карту признаков. Параметров у Conv2d(3, 64, kernel_size=3) всего 3⋅3⋅3⋅64+64=17923 \cdot 3 \cdot 3 \cdot 64 + 64 = 1792 — против 150 миллионов у полносвязного слоя.

пулингпулинг (субдискретизация)EN poolingСжатие карты признаков — максимум (max pooling) или среднее по окну, например 2×2. Обучаемых параметров нет. сжимает карту признаков: например, max pooling 2×2 оставляет максимум из каждого квадрата 2×2, и картинка становится вдвое меньше по каждой оси. Обучаемых параметров у пулинга нет.

Чем глубже слой, тем больше область исходной картинки, от которой он зависит, — рецептивное полерецептивное полеEN receptive fieldОбласть входа, от которой зависит один нейрон. Две свёртки 3×3 подряд видят 5×5, три — 7×7: глубина расширяет «взгляд».. Две свёртки 3×3 подряд «видят» 5×5, три — 7×7, а пулинг и шаг расширяют поле ещё быстрее. Так возникает иерархия: первые слои находят края, средние — текстуры и части объектов, последние — объекты целиком. Здесь свёртки описаны кратко; как устроен настоящий свёрточный слой с каналами, как его считают на GPU и как через него идёт градиент, разберём в модуле 3.5.

flowchart LR
I["Картинка<br/>224×224×3"] --> C1["Свёртка + ReLU<br/>края"] --> P1["Пулинг"]
P1 --> C2["Свёртка + ReLU<br/>текстуры, части"] --> P2["Пулинг"]
P2 --> C3["Свёртка + ReLU<br/>объекты"] --> L["Linear<br/>1000 классов"]
Типичная CNN: чередование свёрток и пулинга, в конце — классификатор
ГодСетьЧто нового
1998LeNet-5 (Лекун и соавт.)свёртки + пулинг, распознавание рукописных цифр
2012AlexNet (Крижевский, Суцкевер, Хинтон)ImageNet: ошибка top-5 15.3% против 26.2% у второго места; ReLU, dropout, обучение на двух GPU
2014VGGтолько свёртки 3×3, 16–19 слоёв
2015ResNet (MSRA)остаточные связи, 152 слоя, ошибка 3.57%
2020ViTкартинка как последовательность патчей 16×16 — трансформер вместо свёрток

Победа AlexNet на ImageNet в 2012 году считается стартом эпохи глубокого обучения. А на ViT видно главное различие архитектур. Свёртка встраивает в модель сильное индуктивное смещениеиндуктивное смещение (априорные предпочтения модели)EN inductive biasКакие решения модель «предпочитает» при прочих равных. В «арбузной книге» Чжоу Чжихуа — «индуктивное предпочтение». — заранее заложенное «предположение о мире», которое модели не надо выучивать из данных. У свёртки это локальность и разделение весов: «важны соседние пиксели» и «один и тот же узор может быть где угодно». Трансформер такой подсказки не имеет: ему нужно больше данных, но с большими данными он выигрывает. Подробнее — в уроке 9.1.

Текст, речь, курс акций — это последовательности разной длины. Рекуррентная сеть читает их по одному элементу и хранит всё прочитанное в скрытом состояниискрытое состояниеEN hidden stateВектор h_t, в котором RNN хранит «память» о прочитанном: h_t = f(h_{t−1}, x_t). Его размер фиксирован, сколько бы токенов ни прошло. hth_t — векторе фиксированного размера:

ht=tanh⁡(Whh ht−1+Wxh xt+b).h_t = \tanh\left(W_{hh}\, h_{t-1} + W_{xh}\, x_t + b\right).

Здесь написано: новое состояние — нелинейная смесь старого состояния и нового входа. Матрицы WhhW_{hh} и WxhW_{xh} одни и те же на всех шагах — разделение весов, только во времени. Поэтому число параметров не зависит от длины текста, а сеть может читать последовательности любой длины. Для языковой модели из hth_t линейным слоем получают логиты следующего токена — как в уроке 1.3.

flowchart LR
x1["x1"] --> h1["h1"]
x2["x2"] --> h2["h2"]
x3["x3"] --> h3["h3"]
x4["x4"] --> h4["h4"]
h0["h0 = 0"] --> h1 -->|"W_hh"| h2 -->|"W_hh"| h3 -->|"W_hh"| h4
h4 --> y["прогноз"]
RNN, развёрнутая во времени: одна и та же ячейка применяется на каждом шаге

Обучают RNN обычным backprop по развёрнутому графу — это обратное распространение во времениобратное распространение во времени (BPTT)EN backpropagation through time (BPTT)Backprop по развёрнутой во времени RNN. Градиент к ранним шагам проходит через одну и ту же матрицу много раз, отсюда затухание и взрыв.. Граф для текста из 1000 токенов — это 1000 копий одной ячейки, соединённых цепочкой.

hth_t нельзя посчитать, пока не готово ht−1h_{t-1}. Текст из 1000 токенов — это 1000 последовательных шагов, каждый из которых — маленькое матричное умножение. Видеокарта с тысячами ядер в основном простаивает. Трансформер считает все позиции одновременно одним большим умножением матриц — тем, что GPU умеют лучше всего.

Градиент от последнего шага к шагу tt проходит через произведение якобианов (матриц производных из урока 1.2.3):

∂hT∂ht=∏k=t+1Tdiag(tanh⁡′(ak)) Whh.\frac{\partial h_T}{\partial h_t} = \prod_{k=t+1}^{T} \mathrm{diag}\big(\tanh'(a_k)\big)\, W_{hh}.

Словами: чтобы дойти от шага TT до шага tt, градиент T−tT - t раз проходит через одну и ту же ячейку и каждый раз умножается на две вещи — на производную tanh⁡\tanh в этой точке (aka_k — то, что стояло под tanh⁡\tanh на шаге kk; diag\mathrm{diag} — матрица с этими производными на диагонали) и на матрицу WhhW_{hh}. Это цепное правило для длинной цепочки из урока 1.2.2.

Это та же история, что в уроке об инициализации, но хуже: здесь одна и та же матрица WhhW_{hh} умножается сама на себя T−tT - t раз. Если её характерный множитель λ=0.9\lambda = 0.9, то через 100 шагов останется 0.9100≈3⋅10−50.9^{100} \approx 3 \cdot 10^{-5}; если λ=1.1\lambda = 1.1 — получится 1.1100≈14 0001.1^{100} \approx 14\,000. Плюс каждый раз tanh⁡′≤1\tanh' \le 1 ещё уменьшает градиент. На практике простая RNN удерживает контекст в лучшем случае на десяток-другой шагов (в практике ниже вы это измерите).

Эту проблему описали Зепп Хохрайтер (1991) и Бенджио с соавторами (1994). Взрыв градиентов лечат обрезкой градиентаобрезка градиента (gradient clipping)EN gradient clippingЕсли норма градиента больше порога, градиент масштабируют до порога: clip_grad_norm_(params, 1.0). Стандарт в обучении LLM. (Паскану и соавт., 2013) — тот же приём до сих пор стоит в каждом цикле обучения LLM. А против затухания придумали LSTM.

LSTMLSTM (долгая краткосрочная память)EN long short-term memory (LSTM)RNN с ячейкой памяти c_t и тремя гейтами (Hochreiter, Schmidhuber, 1997). Ячейка обновляется сложением c_t = f·c_{t−1} + i·g — «остаточная связь во времени». (Хохрайтер и Шмидхубер, 1997; привычный нам вид она приняла к 2000 году) добавляет к состоянию отдельную ячейку памяти ctc_t и три гейтагейт (вентиль)EN gate, gatingВектор из чисел от 0 до 1 (через сигмоиду), который поэлементно решает, какую долю сигнала пропустить. Из LSTM гейты перешли в SwiGLU, gated attention и Gated DeltaNet. — векторы со значениями от 0 до 1 (через сигмоиду), которые поэлементно решают, что пропустить:

ft=σ(Wf[ht−1,xt]+bf)гейт забывания: что оставить из старой памятиit=σ(Wi[ht−1,xt]+bi)входной гейт: что записать новогоgt=tanh⁡(Wg[ht−1,xt]+bg)кандидат на записьot=σ(Wo[ht−1,xt]+bo)выходной гейт: что показать наружуct=ft⊙ct−1+it⊙gtht=ot⊙tanh⁡(ct)\begin{aligned} f_t &= \sigma(W_f [h_{t-1}, x_t] + b_f) && \text{гейт забывания: что оставить из старой памяти} \\ i_t &= \sigma(W_i [h_{t-1}, x_t] + b_i) && \text{входной гейт: что записать нового} \\ g_t &= \tanh(W_g [h_{t-1}, x_t] + b_g) && \text{кандидат на запись} \\ o_t &= \sigma(W_o [h_{t-1}, x_t] + b_o) && \text{выходной гейт: что показать наружу} \\ c_t &= f_t \odot c_{t-1} + i_t \odot g_t \\ h_t &= o_t \odot \tanh(c_t) \end{aligned}

Не пугайтесь шести строк: первые четыре устроены одинаково — «линейный слой от [ht−1,xt][h_{t-1}, x_t], потом сигмоида или tanh⁡\tanh». Запись [ht−1,xt][h_{t-1}, x_t] означает склейку двух векторов в один: [(1,2),(3)]=(1,2,3)[(1, 2), (3)] = (1, 2, 3). Гейт со значением 0 «закрывает» (обнуляет), со значением 1 — «пропускает» как есть: например, ft=(1,0)f_t = (1, 0) сохранит первое число старой памяти и сотрёт второе. Главная строка — пятая. Ячейка обновляется сложением, и производная по прямому пути через ячейку ∂ct/∂ct−1=ft\partial c_t / \partial c_{t-1} = f_t — без матриц и без tanh⁡′\tanh' (гейты тоже зависят от прошлого через ht−1h_{t-1}, но градиенту достаточно прямого пути по ячейке). Если гейт забывания ft≈1f_t \approx 1, то ct≈ct−1+it⊙gtc_t \approx c_{t-1} + i_t \odot g_t: старая память плюс поправка. Это ровно остаточная связь из прошлого урока, только во времени, а не в глубину. Градиент идёт по ячейке, как по «трассе». Гейт забывания добавили Герс, Шмидхубер и Камминс в 2000 году; в исходной LSTM 1997 года его не было.

Связь не случайна. Highway Networks (2015) перенесли гейты LSTM из времени в глубину, а ResNet упростил их до «всегда открытого» обхода x+F(x)x + F(x).

За память приходится платить: у LSTM параметров вчетверо больше, чем у простой RNN того же размера, — четыре набора матриц (три гейта и кандидат) вместо одного. GRU (GRUGRU (управляемый рекуррентный блок)EN gated recurrent unit (GRU)Упрощённая LSTM с двумя гейтами и без отдельной ячейки памяти (Cho и соавт., 2014)., Чо и соавт., 2014) — упрощённая LSTM: два гейта вместо трёх и нет отдельной ячейки, поэтому наборов три. Работает сопоставимо.

Сравним архитектуры по таблице 1 из «Attention Is All You Need». Self-attention (самовниманиесамовниманиеEN self-attention) — главный слой трансформера: каждый токен напрямую «смотрит» на все остальные токены и берёт из них нужное; подробно — на ступени 4. В таблице используется O-нотация из урока 0.2.1: O(n)O(n) — «растёт пропорционально nn», O(1)O(1) — «не зависит от nn», O(n2)O(n^2) — «растёт как квадрат nn» (вдвое длиннее текст — вчетверо больше работы). Здесь nn — длина последовательности, dd — ширина векторов, kk — размер ядра свёртки. Последний столбец добавили мы, в статье его нет:

СлойВычисления на слойПоследовательных шаговДлина пути между позициямиИндуктивное смещение
Self-attentionO(n2d)O(n^2 d)O(1)O(1)O(1)O(1)почти нет
RNNO(nd2)O(n d^2)O(n)O(n)O(n)O(n)порядок, «свежее важнее»
СвёрткаO(knd2)O(k n d^2)O(1)O(1)O(log⁡kn)O(\log_k n)локальность
  • Последовательных шагов O(1)O(1) — все позиции считаются параллельно. RNN требует nn шагов подряд.
  • Длина пути O(1)O(1) — любой токен смотрит на любой другой напрямую. В RNN информации от первого токена к тысячному нужно пройти 999 преобразований.
  • Цена — O(n2)O(n^2) по длине: каждый токен сравнивается с каждым. Об этом — в уроке 4.4, а о борьбе с квадратичной стоимостью — на ступени 5.

Код для локального запуска: свёртка руками, RNN руками и эксперимент с градиентом во времени.

import torch
import torch.nn as nn
import torch.nn.functional as F
# Картинка 5×5: слева тёмное (0), справа светлое (1) — вертикальная граница
img = torch.tensor([[0, 0, 1, 1, 1]] * 5, dtype=torch.float32)
# Ядро 3×3, которое «ищет» переход от тёмного к светлому слева направо
k = torch.tensor([[-1, 0, 1],
[-1, 0, 1],
[-1, 0, 1]], dtype=torch.float32)
def conv2d(x, k):
"""Свёртка руками: окно k скользит по x, в каждой позиции — сумма поэлементных произведений."""
H, W = x.shape
kh, kw = k.shape
out = torch.zeros(H - kh + 1, W - kw + 1)
for i in range(out.shape[0]):
for j in range(out.shape[1]):
out[i, j] = (x[i:i + kh, j:j + kw] * k).sum()
return out
print(conv2d(img, k))
# То же через PyTorch. F.conv2d ждёт форму (батч, каналы, высота, ширина), поэтому
# img[None, None] добавляет две оси длины 1: (5, 5) → (1, 1, 5, 5); [0, 0] в конце их убирает
print(F.conv2d(img[None, None], k[None, None])[0, 0])
# Сколько параметров: MLP против свёртки на картинке 28×28
mlp = nn.Linear(28 * 28, 100) # 100 нейронов, каждый видит все пиксели
conv = nn.Conv2d(1, 16, kernel_size=3, padding=1) # 16 фильтров 3×3, одни и те же во всех позициях
print(sum(p.numel() for p in mlp.parameters()), sum(p.numel() for p in conv.parameters()))
print(conv(torch.randn(1, 1, 28, 28)).shape) # padding=1 сохраняет размер 28×28
tensor([[3., 3., 0.],
[3., 3., 0.],
[3., 3., 0.]])
tensor([[3., 3., 0.],
[3., 3., 0.],
[3., 3., 0.]])
78500 160
torch.Size([1, 16, 28, 28])

Свёртка руками совпала с F.conv2d. Строго говоря, в нейросетях «свёртка» не переворачивает ядро, то есть математически это взаимная корреляция. Для обучаемых ядер разницы нет. Шестнадцать фильтров стоят 160 параметров, полносвязный слой на 100 нейронов — 78 500. При этом свёртка выдаёт 16 карт по 28×28, то есть 12 544 числа, а MLP — всего 100: параметров в 490 раз меньше, а выходов в 125 раз больше.

import torch
import torch.nn as nn
torch.manual_seed(0)
d_in, d_h, T = 8, 16, 5
rnn = nn.RNN(d_in, d_h, batch_first=True)
x = torch.randn(1, T, d_in) # одна последовательность из 5 «токенов»
# RNN руками: один и тот же набор весов на каждом шаге, шаги строго по очереди.
# Веса nn.RNN: weight_ih_l0 — это W_xh (вход → состояние), weight_hh_l0 — W_hh (состояние → состояние)
h = torch.zeros(d_h)
for t in range(T):
h = torch.tanh(rnn.weight_ih_l0 @ x[0, t] + rnn.bias_ih_l0 + rnn.weight_hh_l0 @ h + rnn.bias_hh_l0)
out, h_last = rnn(x)
print(torch.allclose(h, h_last[0, 0], atol=1e-6)) # True: совпало с nn.RNN
print(sum(p.numel() for p in rnn.parameters())) # параметры не зависят от длины T
print(sum(p.numel() for p in nn.LSTM(d_in, d_h).parameters())) # у LSTM вчетверо больше: 4 «слоя» гейтов
True
416
1664

Цикл for t in range(T) — и есть главная беда RNN: его нельзя распараллелить по tt. У PyTorch два вектора смещений (bias_ih и bias_hh), поэтому параметров 16⋅8+16⋅16+16+16=41616 \cdot 8 + 16 \cdot 16 + 16 + 16 = 416.

Подадим последовательность из 100 шагов и посмотрим, насколько выход последнего шага чувствителен к входу на шаге tt. Третья модель — LSTM, у которой гейт забывания принудительно почти открыт (σ(5)≈0.993\sigma(5) \approx 0.993). Так выглядит LSTM, которая в ходе обучения «решила помнить»:

import torch
import torch.nn as nn
torch.manual_seed(0)
T, d_in, d_h = 100, 8, 64
x = torch.randn(1, T, d_in, requires_grad=True)
rnn = nn.RNN(d_in, d_h, batch_first=True) # h_t = tanh(W_ih x_t + W_hh h_{t-1} + b)
lstm = nn.LSTM(d_in, d_h, batch_first=True)
lstm_f = nn.LSTM(d_in, d_h, batch_first=True)
with torch.no_grad(): # гейты в PyTorch идут в порядке: input, forget, cell, output
lstm_f.bias_ih_l0[d_h:2 * d_h] = 5.0 # σ(5) ≈ 0.993: гейт забывания почти открыт —
lstm_f.bias_hh_l0[d_h:2 * d_h] = 0.0 # так выглядит LSTM, которая «решила помнить»
for name, net in [("RNN", rnn), ("LSTM", lstm), ("LSTM, f≈1", lstm_f)]:
x.grad = None
out, _ = net(x)
out[0, -1].sum().backward() # «loss» зависит только от последнего скрытого состояния
g = x.grad[0].norm(dim=-1) # ‖∂loss/∂x_t‖ для каждого шага t
print(f"{name:10s} шаг 99: {g[99]:.1e} шаг 90: {g[90]:.1e} шаг 50: {g[50]:.1e} шаг 0: {g[0]:.1e}")
RNN шаг 99: 1.7e+00 шаг 90: 6.9e-03 шаг 50: 4.0e-13 шаг 0: 0.0e+00
LSTM шаг 99: 3.9e-01 шаг 90: 4.3e-03 шаг 50: 4.4e-11 шаг 0: 3.2e-21
LSTM, f≈1 шаг 99: 3.4e-01 шаг 90: 4.6e-02 шаг 50: 1.7e-01 шаг 0: 7.9e-01

Как читать:

  • RNN забывает за десяток шагов: уже на 9 шагов назад градиент упал в 250 раз, а до шага 0 не дошло ничего (в float32 получился ровный ноль).
  • LSTM со случайными весами тоже забывает: при инициализации ft≈σ(0)=0.5f_t \approx \sigma(0) = 0.5, и 0.51000.5^{100} — ничто. Сама по себе LSTM не волшебная.
  • LSTM с открытым гейтом проносит градиент через все 100 шагов. Ячейка с f≈1f \approx 1 — это «трасса» во времени. Обучение может само открыть гейт там, где нужно долго помнить. Поэтому сдвиг гейта забывания часто инициализируют единицей.

Даже с LSTM остаются две беды: последовательный цикл и фиксированный размер памяти. Всё прочитанное надо уместить в ctc_t из нескольких сотен чисел. Внимание решает обе: каждый токен напрямую смотрит на все остальные, без сжатия в один вектор.

Проверь себя6 вопросов
1 Вход 32×32, ядро 5×5, padding 0, шаг 1. Какой размер выхода по одной оси?
2 Сколько параметров (с bias) у nn.Conv2d(3, 64, kernel_size=3)?
3 Какие проблемы свойственны простой RNN?Можно выбрать несколько ответов
4 Почему ячейка LSTM помогает градиенту дожить до ранних шагов?
5 Какая длина пути между первым и последним токеном последовательности длины nn в RNN и в self-attention?
6 Две свёртки 3×3 подряд «видят» окно 5×5 исходной картинки, три — 7×7. Какое окно (сторона квадрата) видят пять свёрток 3×3 подряд без пулинга и с шагом 1?
  • CNN: маленькое ядро скользит по картинке; локальность и разделение весов дают в тысячи раз меньше параметров, чем у MLP, и эквивариантность к сдвигу. Размер выхода — ⌊(n+2p−k)/s⌋+1\lfloor (n + 2p - k)/s \rfloor + 1.
  • RNN хранит историю в векторе hth_t и применяет одни и те же веса на каждом шаге. Две беды: шаги строго по очереди и затухание градиента во времени.
  • LSTM обновляет ячейку сложением: при открытом гейте забывания это остаточная связь во времени. Но последовательность и фиксированный размер памяти остаются.
  • Трансформер победил за счёт параллельности и пути длины O(1)O(1) ценой O(n2)O(n^2). В 2025–2026 годах идеи RNN и CNN вернулись в гибридных китайских моделях: линейное внимание с матричным состоянием и короткие свёртки.

Как об этом спрашивают на собеседовании — вопросы по CNN, RNN и всей теории ступеней 1–3 с разбором ответов — в уроке 11.3.2.

Дальше: модуль 3.5 «Компьютерное зрение и свёрточные сети» — «Свёрточный слой изнутри»: свёртка с каналами, padding, stride и dilation, реализация через im2col и обратный проход. После него — обучение CNN на CIFAR-10 и перенос обучения, а затем ступень 4 — языковые модели.