ELU: пол, а не обрыв
Вы узнаете
Как ELU сохраняет мягкую отрицательную сторону с полом на -alpha и почему её наклон никогда не умирает до конца.
ELU сохраняет положительную сторону ReLU и заменяет обрыв на отрицательной стороне кривой alpha (e^x - 1), которая выполаживается к -alpha. При alpha 1 вход x = -3 даёт -0.9502, уже близко к полу -1, а наклон там 0.0498: мал, но не 0, так что нейрон ещё может сдвинуться. Отрицательные выходы не выбрасываются, поэтому среднее выходов слоя ближе к 0. Спека elu_activation.t27 вычисляет кривую, наклон и пол, а также свой пакет при -1, 0 и 2: -0.632, 0 и 2.
Попробовать
Поставьте alpha на 0.5 и прочитайте пол; затем найдите, как далеко влево должен уйти x, чтобы наклон стал меньше 0.01.

Drag along ELU: x above zero, alpha (e^x - 1) below. The negative side never falls under -alpha, so outputs average closer to zero, and its slope never quite dies.
specs/ml/activation/elu_activation.t27
// SPDX-License-Identifier: Apache-2.0
// t27/specs/
// Derivative | φ² + 1/φ² = 3 | TRINITY
// #8980: `math::exp` named a module this spec never imports, forward_batch grew
// an array with `+`, and every test called a two-argument function with one
// argument and asserted `result != undefined`. Now each test checks a value.
module Elu;
use base::types;
use math::constants;
// ═══════════════════════════════════════════════════════════
// 1. Constants
// ═══════════════════════════════════════════════════════════
const DEFAULT_ALPHA : f32 = 1.0;
// ═══════════════════════════════════════════════════════════
// 2. Types
// ═══════════════════════════════════════════════════════════
pub const ELUConfig = struct {
alpha : f32,
};
// ═══════════════════════════════════════════════════════════
// 3. Core Functions
// ═══════════════════════════════════════════════════════════
// forward(x, alpha) = x for x > 0, alpha * (exp(x) - 1) otherwise
fn forward(x: f32, alpha: f32) -> f32 {
if (x > 0.0) {
return x;
}
return alpha * (exp(x) - 1.0);
}
// forward_batch: output[i] = forward(input[i], alpha)
fn forward_batch(input: []const f32, alpha: f32, output: []f32) -> void {
var i : usize = 0;
while (i < input.len) {
output[i] = forward(input[i], alpha);
i = i + 1;
}
}
// derivative(x, alpha) = 1 for x > 0, alpha * exp(x) otherwise
fn derivative(x: f32, alpha: f32) -> f32 {
if (x > 0.0) {
return 1.0;
}
return alpha * exp(x);
}
// The inputs the tests and invariants share: one negative, zero, one positive.
fn negative_input() -> f32 {
return -1.0;
}
fn valid_input() -> f32 {
return 2.0;
}
fn batch_at(index: usize) -> f32 {
const input : [3]f32 = [-1.0, 0.0, 2.0];
var output : [3]f32 = [0.0] ** 3;
forward_batch(&input, DEFAULT_ALPHA, &output);
return output[index];
}
// ═══════════════════════════════════════════════════════════
// TDD: Tests (from .tri behaviors)
// ═══════════════════════════════════════════════════════════
// ELU(-1) = exp(-1) - 1 = -0.6321206; ELU(2) = 2
test "forward_basic_case"
given input = negative_input()
when result = forward(input, DEFAULT_ALPHA)
then @abs(result - (-0.6321206)) < 0.000001 and forward(valid_input(), DEFAULT_ALPHA) == 2.0
// alpha scales only the negative side, and ELU(x) never goes below -alpha (in f32
// ELU(-20) rounds to exactly -alpha)
test "forward_alpha_scales_negative_side"
given input = negative_input()
when result = forward(input, 0.5)
then @abs(result - 0.5 * forward(input, 1.0)) < 0.000001 and forward(-20.0, 0.5) >= -0.5
test "forward_batch_basic_case"
given input = negative_input()
when result = batch_at(0)
then result == forward(input, DEFAULT_ALPHA) and batch_at(1) == 0.0 and batch_at(2) == 2.0
// ELU'(-1) = exp(-1) = 0.3678794; ELU'(2) = 1
test "derivative_basic_case"
given input = negative_input()
when result = derivative(input, DEFAULT_ALPHA)
then @abs(result - 0.3678794) < 0.000001 and derivative(valid_input(), DEFAULT_ALPHA) == 1.0
test "derivative_matches_finite_difference"
given slope = (forward(-1.0 + 0.001, DEFAULT_ALPHA) - forward(-1.0 - 0.001, DEFAULT_ALPHA)) / 0.002
when result = derivative(-1.0, DEFAULT_ALPHA)
then @abs(result - slope) < 0.001
// ═══════════════════════════════════════════════════════════
// TDD: Invariants (from .tri constraints)
// ═══════════════════════════════════════════════════════════
invariant elu_constraint_0
assert DEFAULT_ALPHA > 0.0
// name: elu_formula -- identity on the positive side
invariant elu_constraint_1
assert forward(valid_input(), DEFAULT_ALPHA) == valid_input()
// name: derivative -- 1 on the positive side
invariant elu_constraint_2
assert derivative(valid_input(), DEFAULT_ALPHA) == 1.0
// "Clevert et al. (2016) - Fast and Accurate Deep Network Learning by Exponential Linear Units (ELUs)":
// ELU is continuous at 0
invariant elu_constraint_3
assert forward(0.0, DEFAULT_ALPHA) == 0.0
Все уроки
Модуль 1 · Один нейрон
Нейрон складывает входы и изгибает сумму: ReLU, сигмоида и tanh, и где каждая перестаёт пропускать градиент назад.
Модуль 2 · Новые изгибы
Гладкие кривые нынешних моделей: GELU с провалом, Swish с ручкой и ELU с полом вместо обрыва.
Модуль 3 · Слой
Что окружает нейроны: softmax превращает оценки в выбор, эмбеддинг превращает слово в числа, dropout выключает нейроны.
Модуль 4 · Картинки
Чтение картинки по участкам: окно, усреднённое по участку, max pooling и пакетная нормализация признаков разного масштаба.
Модуль 5 · Ошибка
Насколько неверна догадка, одним числом: среднеквадратичная ошибка, перекрёстная энтропия для вероятностей и функция Хьюбера для выбросов.
Модуль 6 · Под гору
Спуск по функции потерь: градиентный спуск с моментом, RMSProp без оглядки на величину градиента и Adam, который соединяет оба.
Модуль 7 · Обучение
Откуда берутся градиенты и чего они добиваются: обратное распространение через слой, сеть, которая учит XOR, и LAMB для больших пакетов.
Модуль 8 · Память и внимание
Сети, которые читают последовательность: рекуррентная ячейка и её затухающее эхо, вентили LSTM и внимание ко всем позициям сразу.
Модуль 9 · Трансформер и награда
Части трансформера и агента: позиции как часы, один блок энкодера и PPO, который учится на награде без рывков.