ReLU: оставить положительную часть
Вы узнаете
Что ReLU делает с положительным и с отрицательным входом и почему наклон 0 останавливает обучение нейрона.
Нейрон складывает взвешенные входы и пропускает сумму через функцию активации. ReLU из них самая простая, max(0, x): положительная сумма проходит без изменений, отрицательная становится 0. Её наклон равен 1 или 0, а наклон 0 означает нейрон, который перестал учиться: градиент через него назад не течёт. Утечка это исправляет: с наклоном 0.1 вход x = -2 даёт -0.2 и пропускает назад градиент 0.1. Спека relu_activation.t27 вычисляет обе кривые, которые рисует виджет.
Попробовать
Поставьте x на -2 и прочитайте наклон с утечкой и без неё; затем найдите наклон при x = 1.5.

Drag along the curve and watch ReLU pass every positive input and zero every negative one; give it a leaky slope and a little of the negative side gets through.
specs/ml/activation/relu_activation.t27
// SPDX-License-Identifier: Apache-2.0
// t27/specs/
// ReLU produces sparse gradients | φ² + 1/φ² = 3 | TRINITY
// #8980: the tests called forward and backward with one or two of their three or
// four arguments, two asserted only `result != undefined`, and the invariants
// asserted `true`. Each test now runs the functions on buffers and checks values.
module Relu;
use base::types;
use math::constants;
// ═══════════════════════════════════════════════════════════
// 1. Constants
// ═══════════════════════════════════════════════════════════
const ZERO : f32 = 0.0;
// ═══════════════════════════════════════════════════════════
// 2. Types
// ═══════════════════════════════════════════════════════════
pub const ReLUConfig = struct {
negative_slope : f32,
inplace : bool,
};
// ═══════════════════════════════════════════════════════════
// 3. Core Functions
// ═══════════════════════════════════════════════════════════
// forward(input: []const f32, output: []f32, config: ReLUConfig) → void
fn forward(input: []const f32, output: []f32, config: ReLUConfig) -> void {
// Values at or below ZERO take the negative branch, values above ZERO pass through
// For negative_slope = 0.0, this gives max(0, x) behavior
var i : usize = 0;
while (i < input.len) : (i += 1) {
if (input[i] > ZERO) {
output[i] = input[i]; // Positive branch: identity
} else {
output[i] = input[i] * config.negative_slope; // Negative branch: scaled
}
}
}
// backward(grad_output: []const f32, input: []const f32, grad_input: []f32, config: ReLUConfig) → void
fn backward(grad_output: []const f32, input: []const f32, grad_input: []f32, config: ReLUConfig) -> void {
// The gradient gate is on the forward input, not grad_output - this makes gradients sparse
var i : usize = 0;
while (i < input.len) : (i += 1) {
if (input[i] > ZERO) {
grad_input[i] = grad_output[i]; // Positive branch: identity gradient
} else {
grad_input[i] = grad_output[i] * config.negative_slope; // Negative branch: scaled gradient
}
}
}
// Note: config.inplace is declared but currently unused by these functions.
// The buffers are separate parameters, so the aliasing case is not handled.
// ═══════════════════════════════════════════════════════════
// TDD: Tests (from .tri behaviors)
// ═══════════════════════════════════════════════════════════
fn cfg(slope: f32) -> ReLUConfig {
return ReLUConfig{ .negative_slope = slope, .inplace = false };
}
// output[index] of forward over (a, b, c)
fn forward_at(a: f32, b: f32, c: f32, slope: f32, index: usize) -> f32 {
const input : [3]f32 = [a, b, c];
var output : [3]f32 = [9.0] ** 3;
forward(&input, &output, cfg(slope));
return output[index];
}
// grad_input[index] of backward over (a, b, c), every grad_output 1
fn backward_at(a: f32, b: f32, c: f32, slope: f32, index: usize) -> f32 {
const grad_output : [3]f32 = [1.0, 1.0, 1.0];
const input : [3]f32 = [a, b, c];
var grad_input : [3]f32 = [9.0] ** 3;
backward(&grad_output, &input, &grad_input, cfg(slope));
return grad_input[index];
}
// The shared fixture: one negative, zero, one positive.
fn valid_input() -> f32 {
return -2.0;
}
// max(0, x) on (-2, 0, 2)
test "forward_basic_case"
given negative = forward_at(-2.0, 0.0, 2.0, 0.0, 0)
when positive = forward_at(-2.0, 0.0, 2.0, 0.0, 2)
then negative == 0.0 and forward_at(-2.0, 0.0, 2.0, 0.0, 1) == 0.0 and positive == 2.0
test "forward_positive_identity"
given slope = 0.0
when first = forward_at(1.0, 2.0, 3.0, slope, 0)
then first == 1.0 and forward_at(1.0, 2.0, 3.0, slope, 1) == 2.0 and forward_at(1.0, 2.0, 3.0, slope, 2) == 3.0
test "forward_negative_scaling"
given slope = 0.5
when first = forward_at(-1.0, -2.0, -3.0, slope, 0)
then first == -0.5 and forward_at(-1.0, -2.0, -3.0, slope, 1) == -1.0 and forward_at(-1.0, -2.0, -3.0, slope, 2) == -1.5
test "forward_zero_negative_slope"
given slope = 0.0
when first = forward_at(-1.0, 0.0, 1.0, slope, 0)
then first == 0.0 and forward_at(-1.0, 0.0, 1.0, slope, 1) == 0.0 and forward_at(-1.0, 0.0, 1.0, slope, 2) == 1.0
// grad_output * negative_slope for input <= ZERO (0 included), grad_output above it
test "backward_input_gate"
given slope = 0.5
when first = backward_at(-1.0, 0.0, 1.0, slope, 0)
then first == 0.5 and backward_at(-1.0, 0.0, 1.0, slope, 1) == 0.5 and backward_at(-1.0, 0.0, 1.0, slope, 2) == 1.0
// Plain ReLU: the gradient is 0 left of zero and passes through right of it
test "backward_basic_case"
given negative = backward_at(-2.0, 0.0, 2.0, 0.0, 0)
when positive = backward_at(-2.0, 0.0, 2.0, 0.0, 2)
then negative == 0.0 and positive == 1.0
// ═══════════════════════════════════════════════════════════
// TDD: Invariants (from .tri constraints)
// ═══════════════════════════════════════════════════════════
// input.len == output.len: forward writes every element of its buffer (none keeps the 9.0 fill)
invariant relu_constraint_0
assert forward_at(valid_input(), 0.0, 1.0, 0.0, 0) != 9.0 and forward_at(valid_input(), 0.0, 1.0, 0.0, 2) != 9.0
// A finite input gives a finite output: ReLU never exceeds its input's magnitude
invariant relu_constraint_1
assert @abs(forward_at(valid_input(), 0.0, 1.0, 0.5, 0)) <= @abs(valid_input())
Все уроки
Модуль 1 · Один нейрон
Нейрон складывает входы и изгибает сумму: ReLU, сигмоида и tanh, и где каждая перестаёт пропускать градиент назад.
Модуль 2 · Новые изгибы
Гладкие кривые нынешних моделей: GELU с провалом, Swish с ручкой и ELU с полом вместо обрыва.
Модуль 3 · Слой
Что окружает нейроны: softmax превращает оценки в выбор, эмбеддинг превращает слово в числа, dropout выключает нейроны.
Модуль 4 · Картинки
Чтение картинки по участкам: окно, усреднённое по участку, max pooling и пакетная нормализация признаков разного масштаба.
Модуль 5 · Ошибка
Насколько неверна догадка, одним числом: среднеквадратичная ошибка, перекрёстная энтропия для вероятностей и функция Хьюбера для выбросов.
Модуль 6 · Под гору
Спуск по функции потерь: градиентный спуск с моментом, RMSProp без оглядки на величину градиента и Adam, который соединяет оба.
Модуль 7 · Обучение
Откуда берутся градиенты и чего они добиваются: обратное распространение через слой, сеть, которая учит XOR, и LAMB для больших пакетов.
Модуль 8 · Память и внимание
Сети, которые читают последовательность: рекуррентная ячейка и её затухающее эхо, вентили LSTM и внимание ко всем позициям сразу.
Модуль 9 · Трансформер и награда
Части трансформера и агента: позиции как часы, один блок энкодера и PPO, который учится на награде без рывков.