Что делает функция активации в искусственном нейроне
Искусственный нейрон складывает входы с весами, прибавляет смещение (bias) и пропускает результат через функцию активации. Первые два шага дают линейную комбинацию, третий превращает её в выход. Вся арифметика укладывается в две строки:
Y = w1*x1 + w2*x2 + ... + wn*xn + b
A = f(Y)
Работа f в том, чтобы решить, считать ли нейрон активированным и какое значение он передаёт дальше. Разбор функций активации описывает это как основную операцию нейрона: посчитать взвешенную сумму на входах, добавить смещение и решить, использовать значение дальше или отбросить его.
Без активационной функции Y остаётся любым числом от минус бесконечности до плюс бесконечности. У нейрона нет границы, после которой он срабатывает, потому что границы не существует. Активация задаёт эту границу, диапазон выхода и форму производной, которая пойдёт назад при обучении. Отсюда и разница в сходимости: две сети одинаковой архитектуры с разными активациями учатся с разной скоростью.
Зачем нужна нелинейность: что ломается без функции активации
Возьмите два линейных слоя подряд: h = W₁x + b₁ и y = W₂h + b₂. Подстановка даёт y = (W₂W₁)x + (W₂b₁ + b₂). Ровно то же самое посчитает один слой с матрицей W₂W₁ и смещением W₂b₁ + b₂. Поставьте сто слоёв и получите одно линейное преобразование, только матрицу считать дольше.
Линейная активация A = c·Y даёт постоянный градиент c: ошибка не гаснет и не взрывается, обучение идёт предсказуемо. Плата за стабильность - сеть не может описать нелинейные зависимости. Складывать и умножать можно сколько угодно, изгиба в решении не появится.
Классическая иллюстрация - XOR: одной прямой классы не разделить, а сети с нелинейной активацией хватает одного скрытого слоя. Поэтому в скрытых слоях нужна функция с нелинейным профилем, а конкретный выбор сводится к компромиссу между формой градиента и стоимостью вычислений.
Ступенчатая функция активации: бинарный выход 0/1 и его пределы
Самая старая и простая: A = 1, если Y больше границы, иначе A = 0. При границе 0 нейрон выдаёт 1 при положительном Y и 0 в остальных случаях. В первоисточнике она определена именно так: A = 1 при Y > граница, иначе A = 0.
def step(z, threshold=0.0):
return 1.0 if z > threshold else 0.0
print([step(z) for z in (-2.0, -0.01, 0.0, 0.5, 3.0)])
# [0.0, 0.0, 0.0, 1.0, 1.0]
Для бинарного классификатора, который отвечает «да» или «нет» на вопрос об активации, ступенька подходит идеально: выход ровно 1 или ровно 0, никаких полутонов.
На этом достоинства заканчиваются. Производная равна нулю везде, кроме точки излома, где она не определена. Градиентный спуск по такому сигналу не работает: веса нечем обновлять. В PyTorch отдельного слоя nn.Step нет, при необходимости порог собирают вручную: (x > 0).float() или torch.heaviside(x, torch.zeros_like(x)).
Почему при нескольких классах ступенчатая функция ломается
Классов три: класс1, класс2, класс3. У каждого свой выход со ступенькой. Объект похож на два класса сразу, оба нейрона выдают 1. Каким должен быть ответ сети? Однозначного ответа нет, и автор разбора формулирует ту же проблему: при нескольких активированных нейронах возникает вопрос, какой класс в итоге должен получиться.
Чтобы класс определялся однозначно, активироваться должен ровно один нейрон, остальные обязаны выдать 0. Такой режим обучения даётся тяжелее, сходимость хуже.
Промежуточные значения снимают проблему: «активирован на 50%», «активирован на 20%». Когда активны несколько нейронов, выбирают наибольшее значение. Выбирать лучше через softmax, а не max: softmax нормирует выходы всех нейронов так, что сумма равна 1, и каждое значение читается как вероятность класса.
Линейная функция активации: постоянный градиент и потеря смысла многослойности
Формула A = c·Y, чаще всего просто A = Y. Производная постоянна и равна c, поэтому градиент не зависит от входа и не насыщается. Обучение стабильное, но каждый слой остаётся линейным преобразованием.
Композиция линейных слоёв сводится к одному: глубина не даёт новых возможностей, только добавляет параметры и время на вычисления. Двадцать слоёв с линейной активацией эквивалентны одному, и никакая инициализация это не исправит.
Место линейной активации - выходной слой регрессии. Прогноз цены, температуры или спроса не ограничен диапазоном, и сигмоида с tanh здесь только испортят результат. В PyTorch отсутствие активации записывают явно:
import torch.nn as nn
regressor = nn.Sequential(
nn.Linear(32, 64),
nn.ReLU(),
nn.Linear(64, 1),
nn.Identity(), # линейный выход для регрессии
)
Разница между осознанным Identity на выходе и забытой активацией в скрытом слое принципиальна: во втором случае сеть теряет нелинейность целиком.
Сигмоида: нелинейность, диапазон [0,1] и затухающий градиент
σ(z) = 1 / (1 + e^(−z)). Гладкая, монотонная, дифференцируемая, сжимает любое число в интервал (0, 1). Отсюда её главное применение: выход бинарного классификатора, где значение читается как вероятность класса.
import math
import torch
import torch.nn as nn
def sigmoid(z):
return 1.0 / (1.0 + math.exp(-z))
x = torch.tensor([-4.0, -1.0, 0.0, 1.0, 4.0])
print(nn.Sigmoid()(x))
# tensor([0.0180, 0.2689, 0.5000, 0.7311, 0.9820])
Производная: σ'(z) = σ(z)·(1 − σ(z)). Максимум достигается при z = 0 и равен 0,25, а при насыщении, когда σ(z) близка к 0 или 1, производная стремится к нулю.
Почему производная сигмоиды не больше 0,25 и что это значит на практике
При обратном распространении градиент проходит через каждый слой и умножается на локальную производную. Если на каждом шаге множитель не больше 0,25, то через десять слоёв останется порядка 0,25¹⁰ ≈ 9,5·10⁻⁷ от исходного сигнала. Градиент практически исчезает, и ранние слои почти не обновляются. Механика этого умножения подробно разобрана в статье про обратное распространение ошибки.
Симптомы на практике: loss падает медленно, веса первых слоёв меняются в разы слабее, чем последних, а увеличение числа слоёв не улучшает, а ухудшает результат.
Выход сигмоиды всегда положителен и не центрирован вокруг нуля, что добавляет зигзагов при оптимизации. Поэтому в скрытых слоях глубоких сетей её сегодня берут редко, а на выходе бинарного классификатора она остаётся стандартом.
Гиперболический тангенс (tanh): диапазон (−1,1) и более крутой градиент
tanh(z) = (e^z − e^(−z)) / (e^z + e^(−z)). Диапазон (−1, 1), выход центрирован вокруг нуля: отрицательные входы дают отрицательные значения. Это заметно упрощает оптимизацию по сравнению с сигмоидой, у которой все выходы положительные.
Производная: tanh'(z) = 1 − tanh²(z). Максимум равен 1 при z = 0, то есть градиент в четыре раза круче, чем у сигмоиды в её лучшей точке.
import torch
import torch.nn as nn
x = torch.tensor([-2.0, -0.5, 0.0, 0.5, 2.0])
print(nn.Tanh()(x))
# tensor([-0.9640, -0.4621, 0.0000, 0.4621, 0.9640])
Проблема насыщения никуда не делась: при больших |z| производная стремится к нулю, и затухающий градиент возвращается. На десяти слоях tanh держится лучше сигмоиды, на сотне - уже нет.
Сильные стороны tanh раскрываются в рекуррентных сетях, где значения нужно хранить в диапазоне около нуля: LSTM и GRU построены на связке tanh и сигмоиды. Эти активации пробуют и заменять: в разборе собственной gated RNN вместо LSTM и GRU автор заменил tanh и сигмоиду на softsign, и результат оказался чувствительным к seed и потребовал правок формул.
ReLU: разреженная активация, простота и риск «умирающих» нейронов
ReLU(z) = max(0, z). Всё, что ниже нуля, обнуляется, всё положительное проходит без изменений. Производная равна 1 при z > 0 и 0 при z < 0: в положительной области насыщения нет, градиент не гаснет даже на глубоких сетях.
import torch
import torch.nn as nn
def relu(z):
return max(0.0, z)
x = torch.tensor([-3.0, -0.5, 0.0, 0.5, 3.0])
print(nn.ReLU()(x))
# tensor([0., 0., 0., 0.5000, 3.0000])
Вычислять ReLU дешевле, чем сигмоиду или tanh: это сравнение с нулём, без экспонент. Отрицательные входы дают ноль, поэтому активации получаются разреженными: часть нейронов в конкретном проходе не участвует, и это экономит вычисления.
Обратная сторона - умирающие нейроны. Если нейрон ушёл в область z < 0 и производная там нулевая, градиент через него не течёт, веса не обновляются, и нейрон выпадает из обучения навсегда. Типичные причины: слишком большой learning rate, из-за которого веса улетают в отрицательную зону, и неудачная инициализация. Диагностируется просто: доля нулевых активаций по слою растёт от эпохи к эпохе.
При неизвестной природе функции ReLU - рабочая стартовая точка для скрытых слоёв: дешёвая, без насыщения в плюсе и хорошо обучается на практике.
Вариации ReLU для борьбы с нулевым градиентом
Leaky ReLU оставляет маленький наклон для отрицательных входов, обычно 0,01, поэтому градиент там не обнуляется. ELU сглаживает переход у нуля и даёт отрицательные значения на отрицательных входах, приближаясь к −1 асимптотически. GELU заменяет жёсткий излом гладкой кривой и часто встречается в трансформерных архитектурах.
import torch.nn as nn
leaky = nn.LeakyReLU(negative_slope=0.01)
elu = nn.ELU(alpha=1.0)
gelu = nn.GELU()
Порядок действий простой: обучили сеть на ReLU, увидели, что доля мёртвых нейронов растёт или loss встал, попробовали Leaky ReLU. Если задача связана с трансформерами, разумно сразу смотреть в сторону GELU.
Сравнение функций активации: таблица и ключевые различия
| Функция | Формула | Диапазон выхода | Максимум производной | Главная проблема | Где применять |
|---|---|---|---|---|---|
| Ступенчатая | 1 при Y > граница, иначе 0 | {0, 1} | 0 (в точке излома не определена) | Нулевой градиент, неоднозначность при нескольких классах | Бинарный порог, учебные примеры |
| Линейная | c·Y | (−∞, +∞) | c (постоянна) | Сеть вырождается в одно линейное преобразование | Выход регрессии |
| Сигмоида | 1 / (1 + e^(−z)) | (0, 1) | 0,25 | Затухающий градиент, выход не центрирован | Выход бинарной классификации |
| tanh | (e^z − e^(−z)) / (e^z + e^(−z)) | (−1, 1) | 1 | Насыщение при больших |z| | RNN, LSTM, небольшие сети |
| ReLU | max(0, z) | [0, +∞) | 1 | Умирающие нейроны при z < 0 | Скрытые слои глубоких сетей |
Таблица сводит выбор к трём параметрам: насколько быстро гаснет градиент, какие значения выходят из нейрона и сколько стоит вычисление. Сигмоида и tanh проигрывают по первому пункту, ступенька по всем трём, ReLU платит за скорость риском мёртвых нейронов.
Как выбрать функцию активации: практические ориентиры
Скрытые слои глубоких сетей прямого распространения: ReLU как первый кандидат, Leaky ReLU или GELU при проблемах с нулевым градиентом. Рекуррентные архитектуры: tanh внутри ячейки, сигмоида на гейтах. Небольшие сети и задачи, где важна гладкость выхода: tanh.
Активация выходного слоя определяется типом задачи, а не модой. Ошибка здесь ломает и интерпретацию, и функцию потерь: ReLU на выходе классификатора выдаст нули вместо вероятностей, а сигмоида в регрессии обрежет прогноз диапазоном (0, 1).
Активация выходного слоя: как тип задачи определяет выбор
- Бинарная классификация: сигмоида, один выход, вероятность от 0 до 1.
- Многоклассовая классификация с взаимоисключающими классами: softmax, сумма выходов равна 1.
- Несколько независимых меток: сигмоида на каждом выходе отдельно.
- Регрессия: линейный выход или nn.Identity().
import torch.nn as nn
model = nn.Sequential(
nn.Linear(64, 128),
nn.ReLU(),
nn.Linear(128, 32),
nn.ReLU(),
nn.Linear(32, 1),
nn.Sigmoid(), # вероятность для бинарной классификации
)
# для многоклассовой задачи последний блок выглядит иначе:
classifier = nn.Sequential(
nn.Linear(64, 128),
nn.ReLU(),
nn.Linear(128, 10),
# softmax применяют через nn.CrossEntropyLoss, который ждёт логиты
)
С nn.CrossEntropyLoss softmax отдельным слоем не ставят: функция потерь уже включает нормировку и ожидает сырые логиты. Лишний softmax перед ней только ухудшит численную устойчивость.
Как активации влияют на обучение и сходимость: что проверять в своей сети
Обучение стоит, loss почти не меняется: первым делом проверяют насыщение сигмоиды или tanh и мёртвые нейроны ReLU. Быстрый тест - посмотреть на распределение активаций по слоям после нескольких эпох. Если у сигмоиды значения прижаты к 0 или 1, а у tanh к ±1, градиент там близок к нулю.
Доля нулевых активаций после ReLU растёт от слоя к слою и приближается к единице: нейроны умирают, стоит снизить learning rate, проверить инициализацию или перейти на Leaky ReLU. Рост нормы градиента, наоборот, указывает на слишком большой шаг оптимизации.
Полезно логировать по каждому слою три числа: среднее активаций, их стандартное отклонение и долю нулей. Резкий сдвиг любого из них между эпохами сигнализирует о проблеме раньше, чем итоговая метрика. Механику вычисления самих градиентов и то, как они распространяются по слоям, разбирает материал про цепное правило и повторное использование вычислений.
Практический вывод короткий: ReLU в скрытых слоях как точка отсчёта, сигмоида или softmax на выходе под тип задачи, tanh в рекуррентных блоках. Дальше смотрят на кривые обучения и меняют функцию только под конкретный симптом, а не ради новизны.