Перейти к содержимому
ПереводыАндрей Карпатый

Zero to Hero, лекция 6: строим WaveNet (makemore, часть 5)

Подробный конспект лекции Карпатого: MLP-модель имён превращается в иерархическую сеть в духе WaveNet, а по дороге появляются свой мини-torch.nn, коварный баг в BatchNorm и связь со свёртками.

  • Карпатый берёт MLP-модель имён из третьей лекции (три символа контекста, один скрытый слой) и делает её глубже. Символы теперь сливаются не все разом, а попарно: пары, потом пары пар и так далее. Получается дерево, как в WaveNet — модели DeepMind 2016 года для генерации звука.
  • По дороге код «пайторчизируется»: слои Embedding, FlattenConsecutive и контейнер Sequential пишутся вручную и повторяют интерфейс torch.nn. После этой лекции Карпатый считает torch.nn «открытым» и дальше берёт готовые слои.
  • Главный технический приём: оператор @ в PyTorch умножает только по последней оси, а все оси левее считает батчем. Поэтому группы символов можно обрабатывать параллельно, а view склеивает соседей без копирования памяти.
  • Ловится тонкий баг: самописный BatchNorm на трёхмерном входе считает статистики не по тем осям — и при этом не падает.
  • Итог по валидации: 2.105 → 1.993 при 76 тыс. параметров. Сам Карпатый предупреждает: без экспериментальной обвязки это «угадывание», а не исследование.
  • Свёртки в WaveNet — не другая модель, а быстрый способ применить то же дерево сразу ко всем позициям текста.

Смотрите после урока 4.1.4 «Нейросетевая языковая модель: MLP по Бенджио». Урок курса повторяет вторую лекцию makemore на русских именах, а эта лекция — её прямое продолжение (четвёртую лекцию, про ручной backprop, Карпатый сам называет отступлением). Пригодятся ещё два урока: 3.3.2 про nn.Module и 3.4.1 про инициализацию и BatchNorm. Свёртки — причинные и расширенные — курс разбирает в уроке 3.5.1, там уже есть отсылка к этой лекции.

Новичку можно бегло пролистать середину главы про баг BatchNorm. Главное в лекции — следить за формами тензоров и понять, что делает FlattenConsecutive.

Код лекции — ноутбук makemore_part5_cnn1.ipynb (лицензия MIT). Файл с именами names.txt лежит в репозитории karpathy/makemore. Если читаете по-китайски, есть учебная версия курса на китайском (репозиторий karpathy-zero-to-hero-zh-transcripts): ноутбук 006_makemore_WaveNet с переведёнными пояснениями и комментариями.

Таймкоды — из глав в описании видео.

ВремяГлава в оригиналеО чём
0:00introкуда движемся: от MLP к WaveNet
1:40starter code walkthroughкод из третьей лекции: данные, слои, обучение
6:56let’s fix the learning rate plotсглаживаем график loss через view
9:16pytorchifying our codeсвои Embedding, Flatten, Sequential; баг с режимом BatchNorm
17:11overview: WaveNetидея постепенного слияния контекста
19:33dataset bump the context size to 8контекст 3 → 8
19:55re-running baseline code on block_size 8плоская сеть на 8 символах: 2.027
21:36implementing WaveNetумножение по батч-осям, FlattenConsecutive
37:41training the WaveNet: first passиерархия при том же числе параметров: 2.029
38:50fixing batchnorm1d bugстатистики по осям (0, 1)
45:21re-training WaveNet with bug fix2.022
46:07scaling up our WaveNet76 тыс. параметров: 1.993
46:58experimental harnessпочему без обвязки мы «в темноте»
47:44WaveNet but with “dilated causal convolutions”свёртка как цикл по позициям
51:34torch.nnмы переписали torch.nn — дальше пользуемся им
52:28the development process of building deep neural netsдокументация, формы, Jupyter → репозиторий
54:17going forwardчто будет в следующих лекциях
55:26improve on my loss!задание: побить 1.993

Данные те же, что во всей серии makemore: 32 033 английских имени, самое длинное — 15 букв. Словарь — 26 букв и точка ., которая отмечает начало и конец имени, всего 27 символов. Имена перемешиваются (random.seed(42)) и делятся на обучение, валидацию и тест в пропорции 80/10/10. Каждое имя режется на примеры «контекст → следующий символ»; при контексте в три символа получается около 182 тыс. обучающих примеров.

Слои скопированы из третьей лекции: Linear (веса делятся на fan_in\sqrt{\text{fan\_in}}, это инициализация в духе Kaiming), BatchNorm1d и Tanh. Карпатый ещё раз перечисляет, чем неудобен BatchNormпакетная нормализация (BatchNorm)EN batch normalizationНормирует каждый признак по примерам батча, хранит скользящие статистики для инференса (Ioffe, Szegedy, 2015). Стандарт CNN; в LLM не используется.:

  • у слоя есть состояние — скользящие среднее и дисперсия. Они обучаются не градиентом, а экспоненциальным усреднением прямо в прямом проходе;
  • поведение на обучении и на инференсе разное, и легко забыть переключить режим;
  • слой связывает между собой примеры в батче, хотя обычно батч — просто способ считать быстрее.

Вывод Карпатого — «состояние обычно вредно». Чуть позже в лекции это подтверждается на практике.

Стартовая сеть: эмбеддинги размером 10, скрытый слой из 200 нейронов, около 12 тыс. параметров. Веса последнего слоя умножены на 0.1, чтобы на старте модель не была «уверенно неправа». Обучение — 200 тыс. шагов SGD с батчем 32; learning rate 0.1, после 150 тыс. шагов — 0.01. Результат: train 2.058, val 2.105. Сгенерированные имена уже похожи на настоящие, но запас для улучшения есть.

График loss по шагам выглядит как «ёж»: батч из 32 примеров слишком мал, и loss сильно скачет от батча к батчу. Карпатый сглаживает его одной строкой. Список из 200 тыс. значений превращается в тензор, тензор переразбивается на 200 строк по 1000 соседних значений, и по каждой строке берётся среднее.

import torch
lossi = torch.rand(200_000) # заглушка: 200 тыс. значений loss по шагам
smooth = lossi.view(-1, 1000).mean(dim=1) # (200, 1000) -> среднее по строке
print(smooth.shape) # torch.Size([200])

view не копирует данные, а лишь иначе на них смотрит: тензор из 10 чисел можно увидеть как 2×5 или 5×2, а -1 просит PyTorch вычислить этот размер самому. На сглаженном графике хорошо видна ступенька на 150-тысячном шаге: снижение learning rate «забирает энергию» у оптимизации, и модель оседает в минимуме.

Прямой проход в стартовом коде неряшлив: часть слоёв лежит в списке layers, а таблица эмбеддингов и склейка векторов окна записаны отдельно. Карпатый заворачивает их в слои — и называет так же, как в PyTorch:

  • Embedding хранит матрицу weight и в прямом проходе просто индексирует её: self.weight[IX];
  • Flatten вытягивает векторы символов окна в одну строку через view. Это та же склейка (конкатенация), только бесплатная: память не копируется;
  • Sequential — контейнер. Он хранит список слоёв, по очереди прогоняет через них вход и собирает параметры всех вложенных слоёв.

После этого весь прямой проход — одна строка logits = model(Xb), а сэмплирование и оценка loss упрощаются так же. Курс показывает настоящие nn.Module и nn.Sequential в уроке 3.3.2; здесь полезно увидеть, что внутри нет никакой магии.

После переделки Карпатый запускает генерацию и получает мусор. Причина: ячейку, которая переводит слои в режим оценки (layer.training = False), не перезапустили. BatchNorm остался в режиме обучения и при генерации получил «батч» из одного примера. Дисперсия одного числа не определена:

torch.tensor([5.0]).var() # tensor(nan) и предупреждение: разброс одного числа не определён

nan расползся по всей сети. Хуже то, что при оценке loss на большом батче ошибки не было бы вовсе: код вернул бы число, просто неправильное — посчитанное по статистикам батча вместо накопленных. Это и есть цена «состояния» в слое.

Чтобы улучшить модель, хочется дать ей больше контекста и сделать её глубже. Но в нашей сети все символы сразу сплющиваются в один скрытый слой. Даже если сделать этот слой шире, сжимать столько информации за один шаг неразумно.

В WaveNet сделано иначе: контекст сливается постепенно. Два соседних символа объединяются в представление «биграммы», две биграммы — в представление четырёх символов, и так далее, пока не останется один вектор. На каждом уровне сливаются только два соседних элемента.

flowchart BT
c1["c1"] --> p1["пара 1–2"]
c2["c2"] --> p1
c3["c3"] --> p2["пара 3–4"]
c4["c4"] --> p2
c5["c5"] --> p3["пара 5–6"]
c6["c6"] --> p3
c7["c7"] --> p4["пара 7–8"]
c8["c8"] --> p4
p1 --> q1["символы 1–4"]
p2 --> q1
p3 --> q2["символы 5–8"]
p4 --> q2
q1 --> r["вектор контекста"]
q2 --> r
r --> out["логиты следующего символа"]
Иерархическое слияние восьми символов контекста: три уровня, на каждом склеиваются два соседа.

В статье WaveNet эта схема нарисована как стопка «расширенных причинных свёрток» (dilated causal convolutions). Звучит страшно, но Карпатый подчёркивает: свёртки здесь — деталь реализации ради скорости. К ним он вернётся в конце лекции, а пока важна сама идея постепенного слияния. Сама WaveNet предсказывает следующий отсчёт звука, а не символ, но постановка та же: авторегрессионная модель угадывает следующий элемент последовательности.

Прежде чем строить дерево, Карпатый проверяет «ленивую» базовую линию: тот же плоский MLP, но с контекстом из восьми символов. Первый линейный слой теперь получает 8⋅10=808 \cdot 10 = 80 чисел вместо 30, поэтому параметров становится около 22 тыс. Уже это даёт заметный выигрыш: val 2.105 → 2.027, и имена на глаз становятся лучше. Этот результат — точка сравнения для иерархической сети.

6. Реализация: умножение по батч-осям и FlattenConsecutive

Заголовок раздела «6. Реализация: умножение по батч-осям и FlattenConsecutive»

Карпатый заводит «черновик»: батч из четырёх примеров и печать формы после каждого слоя. Сейчас картина такая:

СлойФорма выходаЧто произошло
вход Xb4×84 примера по 8 номеров символов
Embedding4×8×10у каждого символа вектор из 10 чисел
Flatten4×808 векторов склеены в строку
Linear4×20080→20080 \to 200 матричным умножением

Матричное умножение в PyTorch не требует двумерного входа. Умножение идёт только по последней оси, а все оси левее считаются батчем и остаются как есть:

x = torch.randn(4, 5, 80)
W = torch.randn(80, 200)
print((x @ W).shape) # torch.Size([4, 5, 200]) — слева можно добавить сколько угодно осей

Значит, на первом уровне дерева в Linear нужно подавать не 80 чисел, а 20 — векторы двух соседних символов. Восемь символов дают четыре такие пары, и их можно обработать параллельно, как вторую ось батча: нужен вход 4×4×20 вместо 4×80.

Пары можно собрать явно: взять символы на чётных позициях, символы на нечётных и склеить по последней оси. Но view даёт ровно тот же результат — соседние векторы в памяти и так лежат подряд:

e = torch.randn(4, 8, 10) # выход Embedding
explicit = torch.cat([e[:, ::2, :], e[:, 1::2, :]], dim=2) # чётные + нечётные позиции
print(explicit.shape) # torch.Size([4, 4, 20])
print((e.view(4, 4, 20) == explicit).all()) # tensor(True)

Отсюда новый слой. Он склеивает n соседних векторов и кладёт их в последнюю ось. Карпатый называет его FlattenConsecutive, а не Flatten, потому что поведение уже отличается от torch.flatten. Код из ноутбука лекции (MIT), комментарии переведены:

class FlattenConsecutive:
def __init__(self, n):
self.n = n # сколько соседних векторов склеивать
def __call__(self, x):
B, T, C = x.shape
x = x.view(B, T // self.n, C * self.n) # T/n групп, в каждой n векторов подряд
if x.shape[1] == 1: # если осталась одна группа,
x = x.squeeze(1) # убираем лишнюю ось: (B, 1, C) -> (B, C)
self.out = x
return self.out
def parameters(self):
return [] # параметров у слоя нет

Карпатый сознательно не пишет -1 в view: при явных размерах ошибка в формах сразу даст исключение, а не тихо пройдёт дальше. С FlattenConsecutive(8) получается старое поведение, с FlattenConsecutive(2) — дерево:

model = Sequential([
Embedding(vocab_size, n_embd),
FlattenConsecutive(2), Linear(n_embd * 2, n_hidden, bias=False), BatchNorm1d(n_hidden), Tanh(),
FlattenConsecutive(2), Linear(n_hidden * 2, n_hidden, bias=False), BatchNorm1d(n_hidden), Tanh(),
FlattenConsecutive(2), Linear(n_hidden * 2, n_hidden, bias=False), BatchNorm1d(n_hidden), Tanh(),
Linear(n_hidden, vocab_size),
])

Формы по слоям (батч 4, эмбеддинг 10, скрытый размер 200): 4×8×10 → 4×4×20 → 4×4×200 → 4×2×400 → 4×2×200 → 4×400 → 4×200 → 4×27. Последний FlattenConsecutive убирает ось размера 1, и логиты получаются той же формы, что и раньше. У нашей сети три уровня, то есть рецептивное полерецептивное полеEN receptive fieldОбласть входа, от которой зависит один нейрон. Две свёртки 3×3 подряд видят 5×5, три — 7×7: глубина расширяет «взгляд». — 8 символов; на картинке из статьи WaveNet уровней четыре и поле — 16.

Чтобы сравнение было честным, Карпатый уменьшает скрытый слой до 68 нейронов: тогда параметров снова около 22 тыс. Результат — val 2.029 против 2.027 у плоской сети. При равном числе параметров иерархия пока ничего не дала. Карпатый объясняет это двумя причинами: архитектуру никто не подбирал, и, возможно, внутри ещё сидит баг.

Баг действительно есть. На вход первого BatchNorm теперь приходит тензор 32×4×68 (батч × группы × каналы), а слой писался для двумерного входа и считает среднее только по нулевой оси:

x = torch.randn(32, 4, 68)
print(x.mean(0, keepdim=True).shape) # torch.Size([1, 4, 68]) — 4×68 отдельных средних
print(x.mean((0, 1), keepdim=True).shape) # torch.Size([1, 1, 68]) — по одному на канал

Ошибки не возникает: broadcastingbroadcasting (растягивание форм)EN broadcastingПравило, по которому NumPy и PyTorch выполняют операции над массивами разных форм без копирования данных. молча согласует формы. Но слой ведёт себя не так, как задумано. Он хранит отдельные статистики для каждой из четырёх групп, и каждая оценена всего по 32 числам. Буфер running_mean получается формы 1×4×68 вместо 68 чисел. Нужно, чтобы ось групп считалась батчем, как нулевая. Функции mean и var принимают кортеж осей, поэтому исправление короткое:

if x.ndim == 2:
dim = 0 # (N, C): статистики по батчу
elif x.ndim == 3:
dim = (0, 1) # (N, L, C): батч и группы — обе оси «батчевые»
xmean = x.mean(dim, keepdim=True)
xvar = x.var(dim, keepdim=True)

Теперь каждое среднее оценивается по 32⋅4=12832 \cdot 4 = 128 числам, статистики стабильнее, и val улучшается: 2.029 → 2.022. Карпатый честно оговаривается: разница маленькая, и неясно, значима ли она статистически.

С исправленной архитектурой сеть можно растить. Эмбеддинг — 24, скрытый слой — 128, всего 76 579 параметров. Обучение идёт заметно дольше, зато val впервые опускается ниже 2.0. Журнал из ноутбука лекции:

ШагПараметровtrainval
исходная MLP: контекст 3, 200 нейронов12 тыс.2.0582.105
контекст 3 → 822 тыс.1.9182.027
плоская сеть → иерархическая22 тыс.1.9412.029
исправлен BatchNorm22 тыс.1.9122.022
эмбеддинг 24, скрытый слой 12876 тыс.1.7691.993

Примеры имён в конце ноутбука: hendrix, jamylie, marianah, astavia, jaylene, aubreana.

В главе про экспериментальную обвязку Карпатый сбивает пафос. Улучшение с 2.1 до 1.99 — не главное. Эксперименты стали долгими, гиперпараметры подобраны наугад, мы смотрели в основном на кривую обучения — «мы в темноте». В настоящей работе нужна обвязка: скрипт с аргументами, много параллельных запусков, графики train и val loss рядом, поиск гиперпараметров. Кроме того, из статьи WaveNet реализована только структура: в оригинале есть ещё гейтированные активации, а также остаточные и skip-связи.

Возьмём из обучающей выборки одно имя — в ноутбуке это «diondre». В нём семь букв, значит, восемь независимых примеров: от «… → d» до «.diondre → .». Можно восемь раз вызвать модель на каждой строке. Свёртка делает то же самое, но лучше по двум причинам:

  1. Цикл уходит внутрь ядра CUDA. Вместо восьми вызовов из Python одно и то же «дерево» скользит по последовательности, как фильтр, а цикл выполняется на видеокарте.
  2. Промежуточные значения переиспользуются. Узел дерева для одной позиции одновременно служит левым потомком соседнего дерева. При восьми отдельных вызовах он пересчитывается заново, а в свёртке — один раз.

Линейные слои дерева становятся фильтрами свёртки шириной 2. На первом уровне фильтр смотрит на соседние позиции, на втором — на позиции через одну (расширение 2), на третьем — через три (расширение 4). Отсюда и название: расширеннаярасширенная («дырявая») свёрткаEN dilated (atrous) convolutionЯдро с промежутками: при dilation = d между соседними элементами ядра d − 1 пропущенных пикселей. Ядро 3×3 с d = 2 охватывает окно 5×5 без новых параметров. причинная свёрткапричинная свёрткаEN causal convolutionОдномерная свёртка по времени, которая видит только текущий и прошлые шаги: рамку из нулей добавляют только слева (k − 1 нулей). Так устроены WaveNet и короткие свёртки в слоях линейного внимания.. Причинная — потому что смотрит только назад, в прошлое.

Карпатый показывает эту связь на словах и обещает свёрточную версию в будущих лекциях. Её легко проверить кодом. Ниже мы считаем одни и те же три слоя (без BatchNorm, для простоты) двумя способами: восемь вызовов «дерева» на скользящем окне и три причинные свёртки с расширениями 1, 2, 4. Ответы совпадают.

import torch
import torch.nn.functional as F
torch.manual_seed(0)
T, C, H = 8, 10, 16 # длина контекста, размер эмбеддинга, скрытый размер
x = torch.randn(T, C) # эмбеддинги 8 символов одного примера
# три «линейных слоя дерева»: каждый склеивает пару соседей и проецирует
W1, b1 = torch.randn(2 * C, H) * 0.3, torch.randn(H) * 0.1
W2, b2 = torch.randn(2 * H, H) * 0.3, torch.randn(H) * 0.1
W3, b3 = torch.randn(2 * H, H) * 0.3, torch.randn(H) * 0.1
def tree(window): # window: (8, C) -> вектор (H,) — одно «дерево», как в лекции
h = window.view(4, 2 * C) @ W1 + b1 # 4 пары символов
h = torch.tanh(h).view(2, 2 * H) @ W2 + b2 # 2 пары «биграмм»
h = torch.tanh(h).view(1, 2 * H) @ W3 + b3 # 1 пара «четырёхграмм»
return torch.tanh(h)[0]
def as_conv(W, Cin): # тот же линейный слой как веса свёртки: (out, in, ширина 2)
return torch.stack([W[:Cin].T, W[Cin:].T], dim=2)
def causal_conv(seq, W, b, Cin, dilation): # seq: (Cin, L)
seq = F.pad(seq, (dilation, 0)) # причинность: дополняем только слева
return torch.tanh(F.conv1d(seq[None], as_conv(W, Cin), b, dilation=dilation)[0])
# способ 1: восемь отдельных вызовов дерева на скользящем окне
xp = torch.cat([torch.zeros(7, C), x]) # 7 «пустых» позиций слева, как точки в makemore
naive = torch.stack([tree(xp[t:t + 8]) for t in range(T)]) # (8, H)
# способ 2: те же веса как свёртки — все позиции за один проход
h = xp.T # (C, 15)
h = causal_conv(h, W1, b1, C, 1)
h = causal_conv(h, W2, b2, H, 2)
h = causal_conv(h, W3, b3, H, 4)
conv = h.T[-T:] # последние 8 позиций — те же окна
print(torch.allclose(naive, conv, atol=1e-6)) # True

Модель одна и та же, меняется только способ вычисления. Подробно о свёртках, расширении и рецептивном поле — в уроке 3.5.1.

10. torch.nn и как на самом деле выглядит работа

Заголовок раздела «10. torch.nn и как на самом деле выглядит работа»

Подводя итоги, Карпатый делает несколько замечаний.

Мы переписали torch.nn. Всё, что строилось в серии, — это библиотека слоёв поверх torch.Tensor: модули, вложенные друг в друга, контейнеры, параметры и буферы. Дальше Карпатый пользуется настоящим torch.nn.

Процесс разработки он описывает так:

  • много времени уходит на документацию PyTorch: какие формы принимает слой, что именно он делает. Документацию Карпатый ругает: по его словам, она бывает неполной, неясной и даже неверной, так что приходится перепроверять;
  • много «гимнастики» с формами: двумерный тензор или трёхмерный, NCL или NLC, где нужен permute, где view;
  • слой сначала прототипируется в Jupyter, пока формы не сойдутся. Потом код переносится в репозиторий (у Карпатого рядом открыт VS Code), и эксперименты запускаются уже оттуда.

Что дальше в серии: настоящие свёрточные слои вместо дерева; остаточные и skip-связи и зачем они нужны; экспериментальная обвязка; рекуррентные сети (RNN, LSTM, GRU) и трансформеры. Следующая лекция — сразу GPT.

В самом конце Карпатый предлагает соревнование: он почти не экспериментировал, и запас ещё есть. Идеи из видео:

  • распределить каналы между слоями иначе;
  • поменять размер эмбеддинга — возможно, он подобран неудачно;
  • раздуть исходную сеть с одним скрытым слоем и проверить, не обгонит ли она «модную» иерархическую (Карпатый признаёт, что это было бы неловко, но исключать нельзя);
  • прочитать статью WaveNet и реализовать её слои: гейтированную активацию, остаточные и skip-связи;
  • подкрутить инициализацию и оптимизацию.

Где курс объясняет то же самое и что есть сверх курса

Заголовок раздела «Где курс объясняет то же самое и что есть сверх курса»
Тема лекцииГде в курсеЧто в лекции сверх курса
MLP-модель символов, train/val/test, генерация4.1.4 (на русских именах)контекст 8 и иерархическое слияние
nn.Module, nn.Sequential, параметры и буферы3.3.2сборка Embedding, FlattenConsecutive, Sequential с нуля
BatchNorm, режимы train/eval3.4.1баг осей на трёхмерном входе, соглашения NCL и NLC
Причинные и расширенные свёртки, рецептивное поле3.5.1свёртка, выведенная из MLP как «цикл по позициям»
Отладка: формы, журнал запусков3.3.3рабочий процесс Карпатого: Jupyter → репозиторий
Слабость фиксированного окна4.1.4, 4.1.5—

Чего в лекции нет совсем: остаточных связей, Adam и нормального подбора гиперпараметров — всё это появится в следующих лекциях и в курсе (модуль 3.4, урок 4.6.3).

  1. Скачайте ноутбук лекции и names.txt, запустите все ячейки. Модель маленькая, CPU хватит. Можно взять русские имена из урока 4.1.2 — меняется только файл с данными.
  2. После одного прямого прохода напечатайте формы всех слоёв: for layer in model.layers: print(layer.__class__.__name__, tuple(layer.out.shape)).
  3. Откатите исправление BatchNorm и посмотрите на model.layers[3].running_mean.shape. Объясните, откуда берётся 1×4×68.
  4. Замените свои слои на nn.Embedding, nn.Linear, nn.BatchNorm1d. Где понадобится permute и почему?
  5. Сравните плоскую и иерархическую сети при равном числе параметров на нескольких seed и ведите журнал, как у Карпатого. Значима ли разница 2.029 против 2.027?
  6. Реализуйте свёрточную версию через nn.Conv1d(..., dilation=...) и считайте loss сразу на всех позициях имени, а не на одном окне. Сколько это экономит прямых проходов на эпоху?
  7. Попробуйте побить 1.993 на валидации — и только в самом конце один раз проверьте результат на тесте.
ENRU
hierarchical modelиерархическая модель
receptive fieldрецептивное поле
dilated causal convolutionрасширенная причинная свёртка
container (Sequential)контейнер
buffer (running_mean, running_var)буфер (скользящее среднее и дисперсия)
train / eval modeрежим обучения / режим оценки
broadcastingbroadcasting (растягивание форм)
batch dimensionось батча, батчевая ось
CUDA kernelядро CUDA
gated activationгейтированная активация
skip connectionskip-связь
experimental harnessэкспериментальная обвязка