Пакетная нормализация
Вы узнаете
Как пакетная нормализация приводит признаки разного масштаба к одному и чем она пользуется при выводе.
Признаки разного масштаба дёргают сеть: в пакете спеки один признак в десять раз больше другого. Пакетная нормализация вычитает среднее каждого признака по пакету и делит на его разброс, поэтому признаки 1, 2, 3, 4 и 10, 20, 30, 40 оба выходят как -1.34, -0.45, 0.45 и 1.34, а -1, -1, 1, 1 выходит как -1 и 1. Затем выученные gamma и beta масштабируют и сдвигают его обратно, где это полезно. При выводе слой вместо пакета использует скользящие средние: при среднем 0, дисперсии 1, gamma 1 и beta 0.25 входы 1, 2 и 3 дают 1.25, 2.25 и 3.25. Спека batchnorm_layer.t27 вычисляет оба режима.
Попробовать
Сравните два нормализованных столбца; затем поставьте скользящее среднее 2 и дисперсию 4 и прочитайте выходы вывода.

A batch of 4 samples whose 3 features live on scales 1, 10 and 1: batch norm gives each feature mean 0 and variance 1. Then set gamma and the running statistics for inference.
specs/ml/layers/batchnorm_layer.t27
// SPDX-License-Identifier: Apache-2.0
// t27/specs/
// Prevent division by zero in sqrt | φ² + 1/φ² = 3 | TRINITY
// #8980: forward copied one element and backward zeroed two gradients
// (placeholders), and the tests pinned those placeholders: output[0] == input[0],
// grad_gamma[0] == grad_beta[0] == 0. They now compute batch normalisation:
// forward/backward in inference form, normalising by the running statistics, and
// forward_train normalising by the batch's own mean and variance.
module Batchnorm;
use base::types;
use math::constants;
// ═══════════════════════════════════════════════════════════
// 1. Constants
// ═══════════════════════════════════════════════════════════
const DEFAULT_EPSILON : f32 = 1.0e-5;
const DEFAULT_MOMENTUM : f32 = 0.1;
// ═══════════════════════════════════════════════════════════
// 2. Types
// ═══════════════════════════════════════════════════════════
pub const BatchNormConfig = struct {
num_features : u32,
epsilon : f32,
momentum : f32,
affine : bool,
track_running_stats : bool,
};
pub const BatchNormState = struct {
running_mean : []f32,
running_var : []f32,
};
// ═══════════════════════════════════════════════════════════
// 3. Core Functions
// ═══════════════════════════════════════════════════════════
// Inference: output[f] = gamma[f] (input[f] - running_mean[f]) / sqrt(running_var[f] + eps) + beta[f]
fn forward(input: []const f32, gamma: []const f32, beta: []const f32, state: BatchNormState, output: []f32, config: BatchNormConfig) -> void {
var f : usize = 0;
while (f < config.num_features) {
const x_hat = (input[f] - state.running_mean[f]) / sqrt(state.running_var[f] + config.epsilon);
output[f] = gamma[f] * x_hat + beta[f];
f = f + 1;
}
}
// Inference backward (mean and variance are constants here):
// grad_input[f] = grad_output[f] gamma[f] / sqrt(var[f] + eps)
// grad_gamma[f] = grad_output[f] x_hat[f], grad_beta[f] = grad_output[f]
fn backward(grad_output: []const f32, input: []const f32, gamma: []const f32, beta: []const f32, mean: []const f32, var: []const f32, grad_gamma: []f32, grad_beta: []f32, grad_input: []f32, config: BatchNormConfig) -> void {
var f : usize = 0;
while (f < config.num_features) {
const inv_std = 1.0 / sqrt(var[f] + config.epsilon);
grad_gamma[f] = grad_output[f] * (input[f] - mean[f]) * inv_std;
grad_beta[f] = grad_output[f];
grad_input[f] = grad_output[f] * gamma[f] * inv_std;
f = f + 1;
}
}
// Training: input is batch x num_features, row-major. Each feature is normalised by
// its own batch mean and (biased) batch variance, then scaled and shifted.
fn forward_train(input: []const f32, batch: usize, gamma: []const f32, beta: []const f32, output: []f32, config: BatchNormConfig) -> void {
const nf : usize = config.num_features;
var f : usize = 0;
while (f < nf) {
var mean : f32 = 0.0;
var b : usize = 0;
while (b < batch) {
mean = mean + input[b * nf + f];
b = b + 1;
}
mean = mean / @as(f32, @floatFromInt(batch));
var variance : f32 = 0.0;
b = 0;
while (b < batch) {
const d = input[b * nf + f] - mean;
variance = variance + d * d;
b = b + 1;
}
variance = variance / @as(f32, @floatFromInt(batch));
b = 0;
while (b < batch) {
output[b * nf + f] = gamma[f] * (input[b * nf + f] - mean) / sqrt(variance + config.epsilon) + beta[f];
b = b + 1;
}
f = f + 1;
}
}
fn cfg() -> BatchNormConfig {
return BatchNormConfig{ .num_features = 3, .epsilon = DEFAULT_EPSILON, .momentum = DEFAULT_MOMENTUM, .affine = true, .track_running_stats = true };
}
// forward on input (1, 2, 3), gamma g, beta 0.25, running mean 0 / var 1 (or mean 2 / var 4)
fn forward_at(g: f32, shifted: bool, index: usize) -> f32 {
const input : [3]f32 = [1.0, 2.0, 3.0];
const gamma : [3]f32 = [g, g, g];
const beta : [3]f32 = [0.25, 0.25, 0.25];
var mean : [3]f32 = [0.0, 0.0, 0.0];
var variance : [3]f32 = [1.0, 1.0, 1.0];
if (shifted) {
mean = [2.0, 2.0, 2.0];
variance = [4.0, 4.0, 4.0];
}
const state = BatchNormState{ .running_mean = &mean, .running_var = &variance };
var output : [3]f32 = [0.0] ** 3;
forward(&input, &gamma, &beta, state, &output, cfg());
return output[index];
}
// which: 0 = grad_gamma, 1 = grad_beta, 2 = grad_input; grad_output 1, gamma 2, mean 0, var 1
fn backward_at(which: u8, index: usize) -> f32 {
const grad_output : [3]f32 = [1.0, 1.0, 1.0];
const input : [3]f32 = [1.0, 2.0, 3.0];
const gamma : [3]f32 = [2.0, 2.0, 2.0];
const beta : [3]f32 = [0.0, 0.0, 0.0];
const mean : [3]f32 = [0.0, 0.0, 0.0];
const variance : [3]f32 = [1.0, 1.0, 1.0];
var grad_gamma : [3]f32 = [9.0] ** 3;
var grad_beta : [3]f32 = [9.0] ** 3;
var grad_input : [3]f32 = [9.0] ** 3;
backward(&grad_output, &input, &gamma, &beta, &mean, &variance, &grad_gamma, &grad_beta, &grad_input, cfg());
if (which == 0) {
return grad_gamma[index];
}
if (which == 1) {
return grad_beta[index];
}
return grad_input[index];
}
// forward_train on a batch of 4 samples x 3 features; returns output[b][f]
fn train_at(b: usize, f: usize) -> f32 {
const input : [12]f32 = [1.0, 10.0, -1.0, 2.0, 20.0, -1.0, 3.0, 30.0, 1.0, 4.0, 40.0, 1.0];
const gamma : [3]f32 = [1.0, 1.0, 1.0];
const beta : [3]f32 = [0.0, 0.0, 0.0];
var output : [12]f32 = [0.0] ** 12;
forward_train(&input, 4, &gamma, &beta, &output, cfg());
return output[b * 3 + f];
}
fn valid_input() -> u32 {
return cfg().num_features;
}
// ═══════════════════════════════════════════════════════════
// TDD: Tests (from .tri behaviors)
// ═══════════════════════════════════════════════════════════
// With mean 0, var 1, gamma 1, beta 0 the layer is the identity up to eps:
// 1 / sqrt(1 + 1e-5) = 0.999995 (the old `==` held only for the copy placeholder)
test "forward_basic_case"
given out0 = forward_at(1.0, false, 0) - 0.25
when out2 = forward_at(1.0, false, 2) - 0.25
then @abs(out0 - 1.0) < 0.00001 and @abs(out2 - 3.0) < 0.00003
// mean 2, var 4: (x - 2) / 2 * gamma + beta, so x = 3 with gamma 2 gives 1.25
test "forward_uses_running_statistics"
given result = forward_at(2.0, true, 2)
when centre = forward_at(2.0, true, 1)
then @abs(result - 1.25) < 0.00001 and centre == 0.25
// grad_gamma = g x_hat = (1, 2, 3) / sqrt(1 + eps), grad_beta = g = 1,
// grad_input = g gamma / sqrt(1 + eps) = 2 (the old 0, 0 were the placeholder's)
test "backward_basic_case"
given grad_gamma = backward_at(0, 2)
when grad_input = backward_at(2, 0)
then @abs(grad_gamma - 3.0) < 0.0001 and backward_at(1, 0) == 1.0 and grad_input != 0.0 and @abs(grad_input - 2.0) < 0.0001
// Each feature of the batch comes out with mean 0 and variance 1
test "forward_train_normalises_each_feature"
given mean_f1 = train_at(0, 1) + train_at(1, 1) + train_at(2, 1) + train_at(3, 1)
when var_f1 = (train_at(0, 1) * train_at(0, 1) + train_at(1, 1) * train_at(1, 1) + train_at(2, 1) * train_at(2, 1) + train_at(3, 1) * train_at(3, 1)) / 4.0
then @abs(mean_f1) < 0.00001 and @abs(var_f1 - 1.0) < 0.0001 and @abs(train_at(0, 0) - train_at(0, 1)) < 0.00001 and @abs(train_at(0, 2) + 1.0) < 0.0001
// ═══════════════════════════════════════════════════════════
// TDD: Invariants (from .tri constraints)
// ═══════════════════════════════════════════════════════════
// num_features > 0
invariant batchnorm_constraint_0
assert valid_input() > 0
// epsilon > 0
invariant batchnorm_constraint_1
assert DEFAULT_EPSILON > 0.0
// 0 <= momentum < 1
invariant batchnorm_constraint_2
assert DEFAULT_MOMENTUM >= 0.0 and DEFAULT_MOMENTUM < 1.0
// gamma.size() == num_features: the fixtures carry 3 of each
invariant batchnorm_constraint_3
assert valid_input() == 3
// beta.size() == num_features: beta shifts every feature, so x = mean gives exactly beta
invariant batchnorm_constraint_4
assert forward_at(2.0, true, 1) == 0.25
Все уроки
Модуль 1 · Один нейрон
Нейрон складывает входы и изгибает сумму: ReLU, сигмоида и tanh, и где каждая перестаёт пропускать градиент назад.
Модуль 2 · Новые изгибы
Гладкие кривые нынешних моделей: GELU с провалом, Swish с ручкой и ELU с полом вместо обрыва.
Модуль 3 · Слой
Что окружает нейроны: softmax превращает оценки в выбор, эмбеддинг превращает слово в числа, dropout выключает нейроны.
Модуль 4 · Картинки
Чтение картинки по участкам: окно, усреднённое по участку, max pooling и пакетная нормализация признаков разного масштаба.
Модуль 5 · Ошибка
Насколько неверна догадка, одним числом: среднеквадратичная ошибка, перекрёстная энтропия для вероятностей и функция Хьюбера для выбросов.
Модуль 6 · Под гору
Спуск по функции потерь: градиентный спуск с моментом, RMSProp без оглядки на величину градиента и Adam, который соединяет оба.
Модуль 7 · Обучение
Откуда берутся градиенты и чего они добиваются: обратное распространение через слой, сеть, которая учит XOR, и LAMB для больших пакетов.
Модуль 8 · Память и внимание
Сети, которые читают последовательность: рекуррентная ячейка и её затухающее эхо, вентили LSTM и внимание ко всем позициям сразу.
Модуль 9 · Трансформер и награда
Части трансформера и агента: позиции как часы, один блок энкодера и PPO, который учится на награде без рывков.