t27.aiEnglish

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 использует фиксированную маску, оставить, выбросить, оставить, выбросить, поэтому числа каждый раз одинаковые.

Попробовать

Сравните выходы четырёх режимов; затем прочитайте градиент выброшенного нейрона при обучении.

Открыть интерактивный урок →

Dropout: training a network with neurons switched off
Dropout: training a network with neurons switched off ↗

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

Открыть спеку урока в плеере ↗

Все уроки