Нейрон, слой, MLP
Из чего собрана нейросеть — нейрон, линейный слой, нелинейность и многослойный перцептрон; какие активации стоят в LLM и почему MLP хранит большую часть параметров любой языковой модели.
Зачем это нужно
Заголовок раздела «Зачем это нужно»В уроке 2.2 вы собрали softmax-регрессию — один линейный слой, за которым стоит softmax. Это уже нейросетьнейронная сетьEN neural network, и именно таким слоем заканчивается любая LLM. Но у неё есть потолок: граница решения всегда прямая, и даже XOR ей не по силам.
Чтобы преодолеть этот потолок, между входом и выходом вставляют скрытые слои с нелинейностью. Получается многослойный перцептрон (MLP)многослойный перцептрон (MLP)EN multilayer perceptron (MLP)Чередование линейных слоёв и нелинейных активаций. FFN в блоке трансформера — это MLP с одним скрытым слоем. — главный строительный блок глубокого обучения. В каждом блоке трансформера, кроме внимания, стоит MLP, его там называют FFNполносвязный блок (FFN)EN feed-forward network (FFN) (feed-forward network). И на него приходится большая часть параметров:
- в GPT-2 — две трети весов каждого блока;
- в Qwen3-0.6B — около 60% весов блока;
- в DeepSeek-V3 — по нашей прикидке около 98% всех 671 млрд параметров (это MLP-«эксперты», подробнее в китайском углу ниже).
Кроме того, интерпретационные исследования связывают с MLP-слоями хранение фактических «знаний» модели вроде «столица Китая — Пекин» (подробнее — в китайском углу). Так что, разобравшись с MLP, вы поймёте, где живут миллиарды параметров LLM.
Искусственный нейрон
Заголовок раздела «Искусственный нейрон»Нейроннейрон (искусственный нейрон)EN neuron (artificial neuron, unit)Взвешенная сумма входов плюс сдвиг, пропущенная через нелинейность: a = f(w·x + b). — это крошечная функция. Она берёт чисел , умножает каждое на свой вес, складывает, прибавляет сдвиг (bias) и пропускает результат через нелинейную функцию :
Словами: — «насколько вход похож на то, что ищет нейрон», а решает, что с этой похожестью делать. Веса
и сдвиг — параметрыпараметрEN parameter, их подбирает обучение. Функция называется
функцией активациифункция активацииEN activation function. Запись («w транспонированное на x») — просто короткое имя
для суммы , в NumPy это w @ x (урок 1.0.3).
Пример. Вход , веса , сдвиг :
С активацией ReLU выход равен . Сдвиньте до : тогда , и ReLU выдаст 0 — нейрон «молчит». Сдвиг работает как порог срабатывания.
То же самое в NumPy — три строки:
import numpy as np
x = np.array([2.0, -1.0, 0.5]) # вход: 3 числаw = np.array([0.6, 0.3, -1.0]) # веса нейрона: по одному на каждый входb = 0.4 # сдвиг
z = w @ x + b # взвешенная сумма: 1.2 − 0.3 − 0.5 + 0.4a = max(0.0, z) # ReLU: отрицательное → 0, положительное — как естьprint(round(z, 4), round(a, 4))0.8 0.8Вы уже встречали два нейрона. ПерцептронперцептронEN perceptronМодель Фрэнка Розенблатта (1958) — линейный классификатор с пороговой активацией, предок нейросетей. Розенблатта (1958) — нейрон со ступенькой вместо . Логистическая регрессия из урока 2.2 — нейрон с сигмоидой. Геометрически нейрон делит пространство входов границей и реагирует на то, по какую сторону и как далеко лежит точка. При двух входах эта граница — прямая на плоскости (как в логистической регрессии), при трёх — плоскость, а при большем числе входов её называют гиперплоскостью: это то же «ровное разделение пространства пополам», только нарисовать его уже нельзя.
Слой: много нейронов — одно умножение матриц
Заголовок раздела «Слой: много нейронов — одно умножение матриц»Линейный (полносвязный) слойлинейный (полносвязный) слойEN linear layer (fully connected layer)y = xWᵀ + b. В PyTorch nn.Linear(in, out) хранит weight формы (out, in) — частый источник путаницы. — это нейронов, которые смотрят на один и тот же вход. У каждого свой столбец весов. Сложим столбцы в матрицу размера , сдвиги — в вектор длины , и все нейронов посчитаются одной операцией — матричным умножением из урока 1.1.2:
Читается так: вход — строка из чисел, — таблица , на выходе строка из чисел — по одному на нейрон. Пример: два входа, три нейрона.
Первый нейрон — первый столбец : . Второй: . Третий:
. Итого — три нейрона посчитаны одним умножением x @ W + b.
На практике обрабатывают сразу батч из примеров (пачку, которую сеть обрабатывает за один раз). Строки матрицы — примеры:
Сдвиг прибавляется к каждой строке через broadcasting (урок 0.3.2). Пример: батч из 32 картинок MNIST по пикселя проходит через слой 784 → 128. Получаем формы , формы и формы . Параметров .
import numpy as np
x = np.array([[1.0, 2.0]]) # один пример: форма (1, 2)W = np.array([[1.0, 0.0, -1.0], [0.5, 1.0, 2.0]]) # 2 входа → 3 нейрона: форма (2, 3)b = np.array([0.0, -1.0, 0.5]) # по сдвигу на нейрон: форма (3,)print(x @ W + b) # пример из текста выше
X = np.random.default_rng(0).normal(size=(32, 784)) # «батч» из 32 картинокW1, b1 = np.zeros((784, 128)), np.zeros(128) # слой 784 → 128 (числа сейчас неважны)print((X @ W1 + b1).shape, W1.size + b1.size) # форма выхода и число параметров[[2. 1. 3.5]](32, 128) 100480Зачем нужна нелинейность
Заголовок раздела «Зачем нужна нелинейность»Поставим два линейных слоя подряд без активации:
Получился снова один линейный слой. Хоть сто таких слоёв ставьте — они схлопнутся в один, ведь произведение матриц —
это тоже матрица. Глубина без нелинейности ничего не даёт. На одном числе это видно сразу: «умножить на 2, потом на 3»
— то же самое, что «умножить на 6». Вы встречали этот факт в уроках 1.0.1 и 1.1.2, теперь проверим его в NumPy
(np.allclose(a, b) отвечает, совпадают ли два массива с точностью до ошибок округления):
import numpy as np
rng = np.random.default_rng(0)X = rng.normal(size=(5, 3)) # 5 примеров по 3 признакаW1, b1 = rng.normal(size=(3, 4)), rng.normal(size=4)W2, b2 = rng.normal(size=(4, 2)), rng.normal(size=2)
two_layers = (X @ W1 + b1) @ W2 + b2 # два слоя БЕЗ активацииW, b = W1 @ W2, b1 @ W2 + b2 # один эквивалентный слойprint(np.allclose(two_layers, X @ W + b)) # True: «глубина» пропала
with_relu = np.maximum(X @ W1 + b1, 0) @ W2 + b2 # с ReLU между слоямиprint(np.allclose(with_relu, X @ W + b)) # False: это уже другая функцияTrueFalseXOR: задача, которую не решит ни один линейный слой
Заголовок раздела «XOR: задача, которую не решит ни один линейный слой»XOR — «ровно один из двух входов равен 1». Четыре точки: , , , . Единицы стоят на одной диагонали квадрата, нули — на другой. Никакая прямая их не разделит, поэтому логистическая регрессия бессильна (урок 2.2).
Добавим скрытый слой из двух нейронов с ReLU и подберём веса руками:
| 0, 0 | 0 | 0 | 0 |
| 0, 1 | 1 | 0 | 1 |
| 1, 0 | 1 | 0 | 1 |
| 1, 1 | 2 | 1 | 0 |
flowchart LR
X1(("x₁")) -->|"1"| H1(("h₁ = ReLU(·)<br/>b = 0"))
X2(("x₂")) -->|"1"| H1
X1 -->|"1"| H2(("h₂ = ReLU(·)<br/>b = −1"))
X2 -->|"1"| H2
H1 -->|"1"| Y(("y"))
H2 -->|"−2"| YЧто произошло? Скрытый слой перенёс точки в новое пространство . Там точки и слиплись в одну точку , а уехала в . В новых координатах классы разделяет обычная прямая . Это главная идея глубокого обучения: скрытые слои учатся строить такие признаки, в которых задача становится простой. В LLM то же самое: трансформер превращает текст в векторы, где следующий токен угадывается линейным слоем.
MLP: стопка слоёв
Заголовок раздела «MLP: стопка слоёв»MLP чередует линейные слои и активации:
flowchart LR X["Вход X<br/>(n × 784)"] --> L1["Linear 784→128"] --> A1["ReLU"] --> L2["Linear 128→64"] --> A2["ReLU"] --> L3["Linear 64→10"] --> Z["Логиты<br/>(n × 10)"] Z --> S["softmax + CE<br/>(урок 1.3)"]
Термины:
- входной слой — сами данные (весов у него нет);
- скрытые слоискрытый слойEN hidden layerСлой между входом и выходом. Его значения никто не размечал — сеть сама решает, какие признаки в нём строить. — всё, что между входом и выходом; их выходы никто не размечал, сеть сама решает, что в них положить;
- выходной слой — последний линейный слой. Активации на нём нет: он выдаёт логиты, а softmax и cross-entropy добавляет функция потерь (уроки 1.3 и 2.2);
- глубина — число слоёв с весами, ширина — число нейронов в скрытом слое.
Посчитаем параметры MLP 784 → 128 → 64 → 10:
| Слой | Веса | Сдвиги | Всего |
|---|---|---|---|
| 784 → 128 | 100 352 | 128 | 100 480 |
| 128 → 64 | 8 192 | 64 | 8 256 |
| 64 → 10 | 640 | 10 | 650 |
| Итого | 109 386 |
Почти все параметры сидят в первом слое — там, где вход широкий. В LLM картина похожая: больше всего весов в самых широких матрицах. Это матрицы словаря, которые переводят токены в векторы и векторы обратно в токены (эмбеддинги и LM head, ступень 4), и расширяющие матрицы FFN.
Функции активации
Заголовок раздела «Функции активации»Активация должна быть нелинейной, дешёвой и иметь удобную производную: по производной сеть будет учиться (следующий урок). Вот основные кандидаты:
| 0 | 1 | 2 | |||
|---|---|---|---|---|---|
| сигмоида | 0.119 | 0.269 | 0.5 | 0.731 | 0.881 |
| 0 | 0.762 | 0.964 | |||
| ReLU | 0 | 0 | 0 | 1 | 2 |
| GELU | 0 | 0.841 | 1.954 | ||
| SiLU | 0 | 0.731 | 1.762 |
Сигмоида и tanh: насыщение
Заголовок раздела «Сигмоида и tanh: насыщение»Исторически первыми были сигмоидасигмоида (логистическая функция)EN sigmoid (logistic function)σ(z) = 1 / (1 + e^(−z)) — превращает любое число в вероятность от 0 до 1. и . Обе насыщаются: при кривая почти плоская, и производная почти нулевая. У сигмоиды производная не больше 0.25, у при она равна 0.01 (эти производные вы считали в уроке 1.2.1). Градиент проходит через каждый слой и каждый раз умножается на эту производную. В глубокой сети он затухает экспоненциально, и первые слои перестают учиться. Подробно эту проблему разберём в уроке 3.4. Сигмоида при этом никуда не делась: она работает как «гейт» со значениями от 0 до 1 — в LSTM, в роутерах MoE, в SiLU.
ReLU: просто и быстро
Заголовок раздела «ReLU: просто и быстро»ReLUReLU (выпрямитель)EN rectified linear unit (ReLU)ReLU(z) = max(0, z). Дешёвая, не насыщается при z > 0; минус — «мёртвые» нейроны, у которых z < 0 на всех примерах. стоит почти ничего: одно сравнение. При производная равна ровно 1, и градиент проходит без затухания. В 2012 году AlexNet (сеть для распознавания картинок, о ней — в уроке 3.4.3) показала, что сеть с ReLU учится в разы быстрее, чем с . С тех пор ReLU — активация по умолчанию. Оригинальный трансформер 2017 года тоже использовал ReLU в FFN.
Минус ReLU — «мёртвые» нейроны. Если сдвиг ушёл в большой минус, то на всех примерах. Выход и градиент нейрона тогда всегда нулевые, и он больше не оживёт. Вы увидите такой нейрон в задании следующего урока.
GELU и SiLU: гладкие активации современных LLM
Заголовок раздела «GELU и SiLU: гладкие активации современных LLM»GELUGELUEN Gaussian error linear unit (GELU)GELU(z) = z·Φ(z), где Φ — функция распределения N(0, 1). Гладкая активация BERT и GPT-2 (в GPT-2 — tanh-приближение). (Gaussian Error Linear Unit) умножает вход на вероятность , что стандартная нормальная величина окажется меньше :
Здесь — функция распределенияфункция распределения (CDF)EN cumulative distribution function (CDF)F(x) = P(X ≤ x); для дискретной величины — накопленные суммы вероятностей. Через неё сэмплируют методом обратной функции. стандартного нормального «колокола» из урока 0.3.3: доля площади под колоколом левее точки . Например, (ровно половина колокола левее нуля), , . Отсюда значения из таблицы: , . Длинная формула справа — приближение через : его считают, когда нужно быстро, а вычислять его руками не придётся.
Интуиция: GELU — это «мягкий ReLU». Большие положительные проходят почти целиком (), большие отрицательные гасятся (). Около нуля переход плавный, без излома. GELU стоит в BERT и GPT-2, причём GPT-2 использует tanh-приближение.
SiLUSiLU (Swish)EN sigmoid linear unit (SiLU), SwishSiLU(z) = z·σ(z). Внутри SwiGLU — в FFN Llama, Qwen, DeepSeek. Минимум ≈ −0.278 при z ≈ −1.28. (она же Swish) устроена так же, только вместо — сигмоида:
Пример: , поэтому , а (сигмоиду вы считали в уроке 1.0.1). У SiLU неглубокий «провал» ниже нуля: минимум при . Для отрицательных градиент не нулевой, поэтому нейроны не умирают. Почему гладкие активации работают лучше ReLU на трансформерах, до конца не объяснено: их выбрали эмпирически, по метрикам. Ноам Шазир в статье о SwiGLU прямо написал, что не берётся объяснить успех этих вариантов, и в шутку приписал его «божественной благосклонности».
В Llama, Qwen и DeepSeek SiLU стоит внутри SwiGLUSwiGLUEN SwiGLU (Swish-gated linear unit)FFN современных LLM: down(SiLU(gate(x)) · up(x)), три матрицы без смещений; скрытый размер обычно ≈ 8d/3 и больше (Shazeer, 2020). — FFN с тремя матрицами вместо двух:
Здесь — поэлементное умножение: одна ветка через SiLU решает, какую часть второй ветки пропустить. Словами: вход умножается на две разные матрицы, («ворота») и («расширение»); первый результат пропускается через SiLU и поэлементно умножается на второй, а сжимает итог обратно до исходной ширины. Сейчас достаточно запомнить, что внутри стоит SiLU. Подробно SwiGLU разберём в уроке 4.5.3, когда будем собирать современный блок LLM.
Теорема об универсальной аппроксимации
Заголовок раздела «Теорема об универсальной аппроксимации»Теорема об универсальной аппроксимациитеорема об универсальной аппроксимацииEN universal approximation theoremСеть с одним скрытым слоем и неполиномиальной активацией приближает любую непрерывную функцию на ограниченной области. Ничего не говорит о числе нейронов, обучении и обобщении. (Cybenko, 1989 — для сигмоиды; Hornik, 1991; для любой неполиномиальной активации — Leshno и соавторы, 1993) утверждает:
MLP с одним скрытым слоем и нелинейной (не полиномиальной) активацией может приблизить любую непрерывную функцию на ограниченной области с любой точностью — если нейронов достаточно много.
Звучит как магия, но с ReLU это видно буквально — вы уже собирали кривые из изломов в уроке 1.0.1. Каждый нейрон — это «излом» в точке : левее он молчит, правее растёт с наклоном 1. Сумма изломов с разными весами даёт ломаную, а ломаной можно сколь угодно точно обвести любую гладкую кривую.
Как подобрать веса? Отметим на графике точку и соединим их отрезками. В первой точке ставим нейрон с наклоном первого отрезка. В каждой следующей точке наклон ломаной меняется — ставим там ещё один нейрон, вес которого равен изменению наклона. Пример: если первый отрезок идёт вверх с наклоном 0.9, а второй — с наклоном 0.3, то второму нейрону нужен вес : он «гнёт» ломаную вниз. Построим такую сеть 1 → → 1 для на отрезке . Код короткий, но плотный — читайте комментарии построчно:
import numpy as np
def relu(z): return np.maximum(z, 0)
def relu_net_interpolation(f, a, b, k): """Сеть 1 → k → 1 с ReLU, которая проводит ломаную через k + 1 точек графика f.""" t = np.linspace(a, b, k + 1) # k + 1 равноотстоящих узлов ломаной на [a, b] slopes = np.diff(f(t)) / np.diff(t) # наклон каждого отрезка: (разность высот) / (разность x) # np.diff([a, b, c]) = [b − a, c − b] w2 = np.diff(slopes, prepend=0.0) # вес нейрона i = на сколько меняется наклон в узле t_i def net(x): # x[:, None] — столбец (N, 1), t[None, :-1] — строка (1, k); broadcasting даёт таблицу (N, k): # в клетке (j, i) стоит x_j − t_i, то есть вход нейрона i для точки x_j h = relu(x[:, None] - t[None, :-1]) # скрытый слой: k нейронов ReLU(x − t_i) return f(t[0]) + h @ w2 # выходной слой: старт в f(a) плюс взвешенная сумма изломов return net
x = np.linspace(0, np.pi, 10_001)for k in [2, 4, 8, 16, 32]: net = relu_net_interpolation(np.sin, 0, np.pi, k) err = np.abs(net(x) - np.sin(x)).max() print(f"нейронов: {k:2d} макс. ошибка: {err:.4f}")нейронов: 2 макс. ошибка: 0.2105нейронов: 4 макс. ошибка: 0.0704нейронов: 8 макс. ошибка: 0.0188нейронов: 16 макс. ошибка: 0.0048нейронов: 32 макс. ошибка: 0.0012Удваиваем нейроны — ошибка падает почти в 4 раза. Это и есть теорема в действии.
Чего теорема НЕ говорит — и это важнее самой теоремы:
- Сколько нужно нейронов. Для сложных функций многих переменных — астрономически много.
- Как найти веса. Мы построили их вручную, зная . В реальности неизвестна: есть только примеры, а веса ищет градиентный спуск (следующий урок).
- Будет ли сеть обобщать. Можно идеально запомнить обучающие точки и ошибаться на новых (урок 2.1.2).
Почему тогда сети глубокие
Заголовок раздела «Почему тогда сети глубокие»Глубина даёт композицию: каждый слой работает с признаками, которые построил предыдущий. Простой пример — «палатка» из двух нейронов ReLU, . Посчитаем несколько точек: , , , , . График поднимается до вершины и спускается обратно — отсюда название. Палатка переводит в и как бы складывает отрезок пополам. Применим её несколько раз подряд — по слою из 2 нейронов за раз — и посчитаем, сколько прямых кусков у получившегося графика:
import numpy as np
def tent(x): # «палатка» из двух нейронов ReLU return 2 * np.maximum(x, 0) - 4 * np.maximum(x - 0.5, 0)
x = np.linspace(0, 1, 2**14 + 1) # густая сетка точек на [0, 1]y = xfor depth in range(1, 6): y = tent(y) # ещё один слой из 2 нейронов slopes = np.round(np.diff(y) / np.diff(x), 6) # наклон графика между соседними точками pieces = 1 + np.count_nonzero(np.diff(slopes)) # каждое изменение наклона — новый линейный кусок print(f"слоёв: {depth}, нейронов: {2 * depth:2d}, линейных кусков: {pieces}")слоёв: 1, нейронов: 2, линейных кусков: 2слоёв: 2, нейронов: 4, линейных кусков: 4слоёв: 3, нейронов: 6, линейных кусков: 8слоёв: 4, нейронов: 8, линейных кусков: 16слоёв: 5, нейронов: 10, линейных кусков: 32Глубокая сеть из нейронов даёт линейных кусков. Одному широкому слою для той же «пилы» нужно не меньше нейронов: каждый ReLU-нейрон добавляет не больше одного излома. При это 20 нейронов против 1023. Поэтому на практике сети делают и широкими, и глубокими: у GPT-2 small 12 блоков, у DeepSeek-V3 — 61.
Практика
Заголовок раздела «Практика»Реализуйте прямой проход MLP на NumPy: три активации, линейный слой, сеть произвольной глубины и подсчёт параметров. Тесты проверят значения активаций, формы, XOR с весами из урока и «схлопывание» сети без нелинейности.
Порядок работы: сначала три активации (каждая — одна строка), потом linear, потом mlp_forward, в конце
count_params. После каждой функции нажимайте «Запустить» и смотрите, что печатается. Если застряли — открывайте
подсказки по одной: первые наводят на мысль, последняя почти готовый ответ.
import numpy as np
def relu(z):
"""ReLU: max(0, z) поэлементно. Подсказка: np.maximum."""
# TODO: ваш код
...
def gelu(z):
"""GELU в tanh-приближении (как в GPT-2):
0.5 * z * (1 + tanh( sqrt(2/π) * (z + 0.044715 * z**3) ))
"""
# TODO: ваш код
...
def silu(z):
"""SiLU (она же Swish): z * sigmoid(z), где sigmoid(z) = 1 / (1 + e^(-z))."""
# TODO: ваш код
...
def linear(X, W, b):
"""Линейный слой.
X: (n, d_in) — n примеров по d_in признаков
W: (d_in, d_out), b: (d_out,)
Возвращает массив формы (n, d_out).
"""
# TODO: ваш код
...
def mlp_forward(X, params, act=relu):
"""Прямой проход MLP.
params — список пар (W, b), по одной на слой.
После каждого слоя, КРОМЕ последнего, применяется активация act.
Возвращает выход последнего слоя (логиты), форма (n, d_out).
"""
# TODO: ваш код
...
def count_params(sizes):
"""Число параметров (весов и сдвигов) MLP с размерами слоёв sizes = [d_in, h1, ..., d_out].
Пример: [2, 3, 1] → (2*3 + 3) + (3*1 + 1) = 13.
"""
# TODO: ваш код
...
# Проверьте себя кнопкой «Запустить»: XOR двухслойной сетью с весами «вручную»
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]], dtype=float)
W1 = np.array([[1.0, 1.0], [1.0, 1.0]])
b1 = np.array([0.0, -1.0])
W2 = np.array([[1.0], [-2.0]])
b2 = np.array([0.0])
print(mlp_forward(X, [(W1, b1), (W2, b2)])) # ожидается столбец 0, 1, 1, 0
print(count_params([784, 128, 10])) # ожидается 101770
То же на PyTorch (локально)
Заголовок раздела «То же на PyTorch (локально)»PyTorch — библиотека, на которой пишут почти все нейросети; подробно её разберём в модуле 3.3. Сейчас просто сравните: всё, что вы написали руками, там уже есть готовым. Если PyTorch ещё не установлен (урок 0.1.2), блок можно пропустить и вернуться к нему после модуля 3.3.
import torchimport torch.nn as nnimport torch.nn.functional as F
# MLP для MNIST: 784 → 128 → 64 → 10mlp = nn.Sequential( nn.Linear(784, 128), nn.ReLU(), nn.Linear(128, 64), nn.ReLU(), nn.Linear(64, 10), # на выходе — логиты, без активации)print(sum(p.numel() for p in mlp.parameters())) # 109386print(mlp[0].weight.shape) # PyTorch хранит W как (out, in)
x = torch.randn(32, 784) # батч из 32 «картинок»print(mlp(x).shape)
# XOR с весами из урока. В PyTorch y = x @ W.T + b, поэтому кладём транспонированные матрицыxor = nn.Sequential(nn.Linear(2, 2), nn.ReLU(), nn.Linear(2, 1))with torch.no_grad(): xor[0].weight.copy_(torch.tensor([[1.0, 1.0], [1.0, 1.0]]).T) xor[0].bias.copy_(torch.tensor([0.0, -1.0])) xor[2].weight.copy_(torch.tensor([[1.0], [-2.0]]).T) xor[2].bias.zero_()X = torch.tensor([[0.0, 0.0], [0.0, 1.0], [1.0, 0.0], [1.0, 1.0]])print(xor(X).squeeze().tolist())
z = torch.tensor([-2.0, -1.0, 0.0, 1.0, 2.0])print(F.relu(z).tolist())print([round(v, 4) for v in F.gelu(z).tolist()]) # точная GELU (через erf)print([round(v, 4) for v in F.gelu(z, approximate="tanh").tolist()]) # tanh-приближение, как в GPT-2print([round(v, 4) for v in F.silu(z).tolist()])Ожидаемый вывод:
109386torch.Size([128, 784])torch.Size([32, 10])[0.0, 1.0, 1.0, 0.0][0.0, 0.0, 0.0, 1.0, 2.0][-0.0455, -0.1587, 0.0, 0.8413, 1.9545][-0.0454, -0.1588, 0.0, 0.8412, 1.9546][-0.2384, -0.2689, 0.0, 0.7311, 1.7616]Точная GELU и её tanh-приближение расходятся лишь в четвёртом знаке. Но если загружаете чужие веса, берите тот
вариант, с которым модель обучалась. В конфигах Hugging Face это поле activation_function у GPT-2 (gelu_new) и
hidden_act у Qwen (silu).
Проверь себя
Заголовок раздела «Проверь себя»- Нейрон: . Слой из нейронов — одно умножение , параметров .
- Без нелинейности слои схлопываются в один. Скрытый слой с ReLU решает XOR: он перестраивает пространство так, что классы делятся прямой.
- ReLU — простая активация по умолчанию. GELU (GPT-2, BERT) и SiLU (внутри SwiGLU у Llama, Qwen, DeepSeek) — гладкие активации современных трансформеров.
- Одного широкого скрытого слоя теоретически хватает для любой функции. Но глубина экономит нейроны за счёт композиции, а теорема ничего не говорит об обучении.
- MLP хранит большую часть параметров LLM: 2/3 блока GPT-2 и около 98% DeepSeek-V3.
Дальше: как такую сеть обучать. Выведем обратное распространение ошибки для Linear + ReLU + softmax-CE в матричной форме и научимся проверять градиенты численно.