Dropout: выключенные нейроны
Вы узнаете
Почему выключение нейронов во время обучения помогает и почему выживших нужно масштабировать.
Во время обучения dropout случайно выключает долю p нейронов, чтобы остальные не опирались ни на один из них. Выжившие нейроны тогда нужно масштабировать, иначе следующий слой видел бы сигнал другого размера при обучении, чем при выводе. Обратный dropout умножает их на 1 / (1 - p) при обучении: с p = 0.5 вход 1, 2, 3, 4 становится 2, 0, 6, 0, а вывод это простой проход. Классический dropout оставляет 1, 0, 3, 0 и умножает на 0.5 уже при выводе. Спека dropout_layer.t27 использует фиксированную маску, оставить, выбросить, оставить, выбросить, поэтому числа каждый раз одинаковые.
Попробовать
Сравните выходы четырёх режимов; затем прочитайте градиент выброшенного нейрона при обучении.

Switch between training and inference, inverted and classic dropout: units 1 and 3 are dropped, the survivors are scaled so that the layer's output keeps its expected size.
specs/ml/layers/dropout_layer.t27
// SPDX-License-Identifier: Apache-2.0
// t27/specs/
// Prevent co-adaptation of neurons | φ² + 1/φ² = 3 | TRINITY
// #8980: the tests called `allocf32` and `allocbool`, which no spec declares, and
// asserted `result != undefined`. Inference also scaled by (1 - p) even for
// inverted dropout, which already scaled by 1 / (1 - p) in training, so the
// expected activation shrank by (1 - p) between training and inference. Inference
// is now the identity when scale_during_training is set.
module Dropout;
use base::types;
use math::constants;
// ═══════════════════════════════════════════════════════════
// 1. Constants
// ═══════════════════════════════════════════════════════════
const DEFAULT_DROP_RATE : f32 = 0.5;
// ═══════════════════════════════════════════════════════════
// 2. Types
// ═══════════════════════════════════════════════════════════
pub const DropoutConfig = struct {
p : f32,
inplace : bool,
scale_during_training : bool,
};
// ═══════════════════════════════════════════════════════════
// 3. Core Functions
// ═══════════════════════════════════════════════════════════
// forward(input: []const f32, output: []f32, mask: []bool, training: bool, config: DropoutConfig) → void
fn forward(input: []const f32, output: []f32, mask: []bool, training: bool, config: DropoutConfig) -> void {
const n = input.len;
var i : usize = 0;
while (i < n) {
if (training) {
if (mask[i]) {
// Neuron is kept, scale during training if configured
if (config.scale_during_training) {
output[i] = input[i] * (1.0 / (1.0 - config.p));
} else {
output[i] = input[i];
}
} else {
// Neuron is dropped
output[i] = 0.0;
}
} else {
if (config.scale_during_training) {
// Inverted dropout already scaled in training: inference is the identity
output[i] = input[i];
} else {
// Classic dropout: scale by the keep probability at inference
output[i] = input[i] * (1.0 - config.p);
}
}
i = i + 1;
}
}
// backward(grad_output: []const f32, mask: []const bool, grad_input: []f32, config: DropoutConfig) → void
fn backward(grad_output: []const f32, mask: []const bool, grad_input: []f32, config: DropoutConfig) -> void {
const n = grad_output.len;
var i : usize = 0;
while (i < n) {
if (mask[i]) {
// Gradient flows through kept neurons
if (config.scale_during_training) {
grad_input[i] = grad_output[i] * (1.0 / (1.0 - config.p));
} else {
grad_input[i] = grad_output[i];
}
} else {
// Gradient is zero for dropped neurons
grad_input[i] = 0.0;
}
i = i + 1;
}
}
// ═══════════════════════════════════════════════════════════
// TDD: Tests (from .tri behaviors)
// ═══════════════════════════════════════════════════════════
fn cfg(scaled: bool) -> DropoutConfig {
return DropoutConfig{ .p = DEFAULT_DROP_RATE, .inplace = false, .scale_during_training = scaled };
}
// forward on input (1, 2, 3, 4) with mask (keep, drop, keep, drop)
fn forward_at(training: bool, scaled: bool, index: usize) -> f32 {
const input : [4]f32 = [1.0, 2.0, 3.0, 4.0];
var output : [4]f32 = [9.0] ** 4;
var mask : [4]bool = [true, false, true, false];
forward(&input, &output, &mask, training, cfg(scaled));
return output[index];
}
fn backward_at(scaled: bool, index: usize) -> f32 {
const grad_output : [4]f32 = [1.0, 1.0, 1.0, 1.0];
const mask : [4]bool = [true, false, true, false];
var grad_input : [4]f32 = [9.0] ** 4;
backward(&grad_output, &mask, &grad_input, cfg(scaled));
return grad_input[index];
}
// ═══════════════════════════════════════════════════════════
// TDD: Tests (from .tri behaviors)
// ═══════════════════════════════════════════════════════════
// p = 0.5, inverted: kept units are doubled, dropped ones are 0
test forward_basic_case
given kept = forward_at(true, true, 2)
when dropped = forward_at(true, true, 1)
then kept == 6.0 and dropped == 0.0 and forward_at(true, true, 0) == 2.0
// The expected activation is the same in training and inference
test forward_inference_keeps_the_expectation
given inverted = forward_at(false, true, 2)
when classic = forward_at(false, false, 2)
then inverted == 3.0 and classic == 1.5 and forward_at(true, false, 2) == 3.0
// The gradient flows only through kept units, with the same scale
test backward_basic_case
given kept = backward_at(true, 0)
when dropped = backward_at(true, 3)
then kept == 2.0 and dropped == 0.0 and backward_at(false, 2) == 1.0
invariant dropout_rate_is_a_probability
assert DEFAULT_DROP_RATE >= 0.0 and DEFAULT_DROP_RATE < 1.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, который учится на награде без рывков.