CNN и RNN — краткая история архитектур
Как свёрточные сети научили компьютер видеть, как рекуррентные сети читали текст по одному слову, почему RNN упёрлись в последовательность и затухание градиентов и почему их идеи возвращаются в китайских LLM 2026 года.
Зачем это нужно
Заголовок раздела «Зачем это нужно»До 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».
CNN: как сеть научилась видеть
Заголовок раздела «CNN: как сеть научилась видеть»Почему MLP плох для картинок
Заголовок раздела «Почему MLP плох для картинок»Цветная картинка 224×224 — это чисел. Полносвязный слой на 1000 нейронов потребует 150 миллионов весов, и это только первый слой. Хуже того, MLP не знает, что пиксели соседние: если кошку сдвинуть на 10 пикселей, для него это совсем другой вход, и всё придётся учить заново для каждой позиции.
У картинок есть две подсказки, которые хочется встроить в архитектуру:
- Локальность. Смысл складывается из соседних пикселей: край, угол, текстура видны в маленьком окне.
- Сдвиг не меняет смысла. Кошка в левом углу и в правом — всё ещё кошка. Детектор «уха» должен работать в любом месте.
Свёртка
Заголовок раздела «Свёртка»свёрткасвёрткаEN convolutionМаленькое окно весов скользит по входу; в каждой позиции считается скалярное произведение окна и фрагмента входа. В нейросетях это формально взаимная корреляция (cross-correlation). воплощает обе идеи. Маленькое окно весов — ядроядро свёртки (фильтр)EN convolution kernel (filter)Обучаемая матрица весов свёртки, например 3×3; одна и та же во всех позициях входа., например 3×3, — скользит по картинке. В каждой позиции считается сумма поэлементных произведений окна и фрагмента картинки:
Здесь написано: выход в точке — скалярное произведение ядра и квадрата картинки размера , который начинается в , плюс смещение . Одно и то же ядро используется во всех позициях — это разделение весовразделение весовEN weight sharingОдни и те же параметры применяются в разных местах — ядро свёртки во всех позициях картинки, матрицы RNN на всех шагах времени.. Поэтому свёртка обладает свойством эквивариантность к сдвигуэквивариантность к сдвигуEN translation equivarianceСдвинули вход — выход свёртки сдвинулся так же. Это индуктивное смещение CNN; d2l-zh говорит о близком свойстве — инвариантности к сдвигу.: сдвинули картинку — карта откликов сдвинулась так же.
Пример. Картинка 5×5: два левых столбца тёмные (0), три правых светлые (1). Ядро ищет переход «тёмное → светлое»:
Окно в первой позиции захватывает столбцы : в каждой строке , итого 3. Во второй позиции столбцы дают те же 3. В третьей позиции окно целиком на светлом: . Ядро «загорается» на границе и молчит на однородном фоне. Выход — карта признаков (feature map) 3×3, по-китайски буквально «изображение признаков».
Размер выхода. Ядро на входе даёт позиций по каждой оси. Чтобы размер не уменьшался, вокруг картинки добавляют рамку из нулей — дополнениедополнение (padding)EN paddingРамка из нулей вокруг входа, чтобы свёртка не уменьшала размер: для ядра 3×3 и шага 1 нужна рамка шириной 1. . Чтобы выход стал меньше, окно двигают с шагомшаг свёртки (stride)EN strideНа сколько позиций сдвигается окно свёртки; при шаге 2 выход вдвое меньше по каждой оси. . Общая формула:
Скобки означают округление вниз до целого: (окно, которое не помещается целиком, не считается). Проверим формулу на примере выше: , , , даёт — та самая карта 3×3. Для , , , размер сохраняется: . Первый слой ResNet (, , ) превращает 224 в .
Каналы. На входе у цветной картинки 3 канала (R, G, B), поэтому ядро на самом деле трёхмерное: .
Слой учит сразу много ядер, например 64, и каждое даёт свою карту признаков. Параметров у Conv2d(3, 64, kernel_size=3)
всего — против 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 классов"]
Краткая история
Заголовок раздела «Краткая история»| Год | Сеть | Что нового |
|---|---|---|
| 1998 | LeNet-5 (Лекун и соавт.) | свёртки + пулинг, распознавание рукописных цифр |
| 2012 | AlexNet (Крижевский, Суцкевер, Хинтон) | ImageNet: ошибка top-5 15.3% против 26.2% у второго места; ReLU, dropout, обучение на двух GPU |
| 2014 | VGG | только свёртки 3×3, 16–19 слоёв |
| 2015 | ResNet (MSRA) | остаточные связи, 152 слоя, ошибка 3.57% |
| 2020 | ViT | картинка как последовательность патчей 16×16 — трансформер вместо свёрток |
Победа AlexNet на ImageNet в 2012 году считается стартом эпохи глубокого обучения. А на ViT видно главное различие архитектур. Свёртка встраивает в модель сильное индуктивное смещениеиндуктивное смещение (априорные предпочтения модели)EN inductive biasКакие решения модель «предпочитает» при прочих равных. В «арбузной книге» Чжоу Чжихуа — «индуктивное предпочтение». — заранее заложенное «предположение о мире», которое модели не надо выучивать из данных. У свёртки это локальность и разделение весов: «важны соседние пиксели» и «один и тот же узор может быть где угодно». Трансформер такой подсказки не имеет: ему нужно больше данных, но с большими данными он выигрывает. Подробнее — в уроке 9.1.
RNN: память в одном векторе
Заголовок раздела «RNN: память в одном векторе»Текст, речь, курс акций — это последовательности разной длины. Рекуррентная сеть читает их по одному элементу и хранит всё прочитанное в скрытом состояниискрытое состояниеEN hidden stateВектор h_t, в котором RNN хранит «память» о прочитанном: h_t = f(h_{t−1}, x_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 обычным backprop по развёрнутому графу — это обратное распространение во времениобратное распространение во времени (BPTT)EN backpropagation through time (BPTT)Backprop по развёрнутой во времени RNN. Градиент к ранним шагам проходит через одну и ту же матрицу много раз, отсюда затухание и взрыв.. Граф для текста из 1000 токенов — это 1000 копий одной ячейки, соединённых цепочкой.
Беда первая: всё по очереди
Заголовок раздела «Беда первая: всё по очереди»нельзя посчитать, пока не готово . Текст из 1000 токенов — это 1000 последовательных шагов, каждый из которых — маленькое матричное умножение. Видеокарта с тысячами ядер в основном простаивает. Трансформер считает все позиции одновременно одним большим умножением матриц — тем, что GPU умеют лучше всего.
Беда вторая: затухание во времени
Заголовок раздела «Беда вторая: затухание во времени»Градиент от последнего шага к шагу проходит через произведение якобианов (матриц производных из урока 1.2.3):
Словами: чтобы дойти от шага до шага , градиент раз проходит через одну и ту же ячейку и каждый раз умножается на две вещи — на производную в этой точке ( — то, что стояло под на шаге ; — матрица с этими производными на диагонали) и на матрицу . Это цепное правило для длинной цепочки из урока 1.2.2.
Это та же история, что в уроке об инициализации, но хуже: здесь одна и та же матрица умножается сама на себя раз. Если её характерный множитель , то через 100 шагов останется ; если — получится . Плюс каждый раз ещё уменьшает градиент. На практике простая RNN удерживает контекст в лучшем случае на десяток-другой шагов (в практике ниже вы это измерите).
Эту проблему описали Зепп Хохрайтер (1991) и Бенджио с соавторами (1994). Взрыв градиентов лечат
обрезкой градиентаобрезка градиента (gradient clipping)EN gradient clippingЕсли норма градиента больше порога, градиент масштабируют до порога: clip_grad_norm_(params, 1.0). Стандарт в обучении LLM. (Паскану и соавт., 2013) — тот же приём до сих пор стоит в
каждом цикле обучения LLM. А против затухания придумали LSTM.
LSTM: остаточная связь во времени
Заголовок раздела «LSTM: остаточная связь во времени»LSTMLSTM (долгая краткосрочная память)EN long short-term memory (LSTM)RNN с ячейкой памяти c_t и тремя гейтами (Hochreiter, Schmidhuber, 1997). Ячейка обновляется сложением c_t = f·c_{t−1} + i·g — «остаточная связь во времени». (Хохрайтер и Шмидхубер, 1997; привычный нам вид она приняла к 2000 году) добавляет к состоянию отдельную ячейку памяти и три гейтагейт (вентиль)EN gate, gatingВектор из чисел от 0 до 1 (через сигмоиду), который поэлементно решает, какую долю сигнала пропустить. Из LSTM гейты перешли в SwiGLU, gated attention и Gated DeltaNet. — векторы со значениями от 0 до 1 (через сигмоиду), которые поэлементно решают, что пропустить:
Не пугайтесь шести строк: первые четыре устроены одинаково — «линейный слой от , потом сигмоида или ». Запись означает склейку двух векторов в один: . Гейт со значением 0 «закрывает» (обнуляет), со значением 1 — «пропускает» как есть: например, сохранит первое число старой памяти и сотрёт второе. Главная строка — пятая. Ячейка обновляется сложением, и производная по прямому пути через ячейку — без матриц и без (гейты тоже зависят от прошлого через , но градиенту достаточно прямого пути по ячейке). Если гейт забывания , то : старая память плюс поправка. Это ровно остаточная связь из прошлого урока, только во времени, а не в глубину. Градиент идёт по ячейке, как по «трассе». Гейт забывания добавили Герс, Шмидхубер и Камминс в 2000 году; в исходной LSTM 1997 года его не было.
Связь не случайна. Highway Networks (2015) перенесли гейты LSTM из времени в глубину, а ResNet упростил их до «всегда открытого» обхода .
За память приходится платить: у 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: — «растёт пропорционально », — «не зависит от », — «растёт как квадрат » (вдвое длиннее текст — вчетверо больше работы). Здесь — длина последовательности, — ширина векторов, — размер ядра свёртки. Последний столбец добавили мы, в статье его нет:
| Слой | Вычисления на слой | Последовательных шагов | Длина пути между позициями | Индуктивное смещение |
|---|---|---|---|---|
| Self-attention | почти нет | |||
| RNN | порядок, «свежее важнее» | |||
| Свёртка | локальность |
- Последовательных шагов — все позиции считаются параллельно. RNN требует шагов подряд.
- Длина пути — любой токен смотрит на любой другой напрямую. В RNN информации от первого токена к тысячному нужно пройти 999 преобразований.
- Цена — по длине: каждый токен сравнивается с каждым. Об этом — в уроке 4.4, а о борьбе с квадратичной стоимостью — на ступени 5.
Практика
Заголовок раздела «Практика»Код для локального запуска: свёртка руками, RNN руками и эксперимент с градиентом во времени.
Свёртка руками и через PyTorch
Заголовок раздела «Свёртка руками и через PyTorch»import torchimport torch.nn as nnimport 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×28mlp = 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×28tensor([[3., 3., 0.], [3., 3., 0.], [3., 3., 0.]])tensor([[3., 3., 0.], [3., 3., 0.], [3., 3., 0.]])78500 160torch.Size([1, 16, 28, 28])Свёртка руками совпала с F.conv2d. Строго говоря, в нейросетях «свёртка» не переворачивает ядро, то есть
математически это взаимная корреляция. Для обучаемых ядер разницы нет. Шестнадцать фильтров стоят 160 параметров,
полносвязный слой на 100 нейронов — 78 500. При этом свёртка выдаёт 16 карт по 28×28, то есть 12 544 числа, а MLP —
всего 100: параметров в 490 раз меньше, а выходов в 125 раз больше.
RNN руками
Заголовок раздела «RNN руками»import torchimport torch.nn as nn
torch.manual_seed(0)d_in, d_h, T = 8, 16, 5rnn = 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.RNNprint(sum(p.numel() for p in rnn.parameters())) # параметры не зависят от длины Tprint(sum(p.numel() for p in nn.LSTM(d_in, d_h).parameters())) # у LSTM вчетверо больше: 4 «слоя» гейтовTrue4161664Цикл for t in range(T) — и есть главная беда RNN: его нельзя распараллелить по . У PyTorch два вектора смещений
(bias_ih и bias_hh), поэтому параметров .
Градиент во времени: RNN против LSTM
Заголовок раздела «Градиент во времени: RNN против LSTM»Подадим последовательность из 100 шагов и посмотрим, насколько выход последнего шага чувствителен к входу на шаге . Третья модель — LSTM, у которой гейт забывания принудительно почти открыт (). Так выглядит LSTM, которая в ходе обучения «решила помнить»:
import torchimport torch.nn as nn
torch.manual_seed(0)T, d_in, d_h = 100, 8, 64x = 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+00LSTM шаг 99: 3.9e-01 шаг 90: 4.3e-03 шаг 50: 4.4e-11 шаг 0: 3.2e-21LSTM, f≈1 шаг 99: 3.4e-01 шаг 90: 4.6e-02 шаг 50: 1.7e-01 шаг 0: 7.9e-01Как читать:
- RNN забывает за десяток шагов: уже на 9 шагов назад градиент упал в 250 раз, а до шага 0 не дошло ничего (в float32 получился ровный ноль).
- LSTM со случайными весами тоже забывает: при инициализации , и — ничто. Сама по себе LSTM не волшебная.
- LSTM с открытым гейтом проносит градиент через все 100 шагов. Ячейка с — это «трасса» во времени. Обучение может само открыть гейт там, где нужно долго помнить. Поэтому сдвиг гейта забывания часто инициализируют единицей.
Даже с LSTM остаются две беды: последовательный цикл и фиксированный размер памяти. Всё прочитанное надо уместить в из нескольких сотен чисел. Внимание решает обе: каждый токен напрямую смотрит на все остальные, без сжатия в один вектор.
Проверь себя
Заголовок раздела «Проверь себя»- CNN: маленькое ядро скользит по картинке; локальность и разделение весов дают в тысячи раз меньше параметров, чем у MLP, и эквивариантность к сдвигу. Размер выхода — .
- RNN хранит историю в векторе и применяет одни и те же веса на каждом шаге. Две беды: шаги строго по очереди и затухание градиента во времени.
- LSTM обновляет ячейку сложением: при открытом гейте забывания это остаточная связь во времени. Но последовательность и фиксированный размер памяти остаются.
- Трансформер победил за счёт параллельности и пути длины ценой . В 2025–2026 годах идеи RNN и CNN вернулись в гибридных китайских моделях: линейное внимание с матричным состоянием и короткие свёртки.
Как об этом спрашивают на собеседовании — вопросы по CNN, RNN и всей теории ступеней 1–3 с разбором ответов — в уроке 11.3.2.
Дальше: модуль 3.5 «Компьютерное зрение и свёрточные сети» — «Свёрточный слой изнутри»: свёртка с каналами, padding, stride и dilation, реализация через im2col и обратный проход. После него — обучение CNN на CIFAR-10 и перенос обучения, а затем ступень 4 — языковые модели.