t27.aiРусский

Dropout

You will learn

Why switching units off while training helps, and why the survivors must be scaled.

While training, dropout switches off a share p of the units at random, so the rest cannot lean on any one of them. The units that survive must then be scaled, or the next layer would see the signal at another size in training than at inference. Inverted dropout scales them by 1 / (1 - p) while training: with p = 0.5, input 1, 2, 3, 4 becomes 2, 0, 6, 0, and inference is a plain pass. Classic dropout keeps 1, 0, 3, 0 and scales by 0.5 at inference instead. The spec dropout_layer.t27 uses a fixed mask, keep, drop, keep, drop, so the numbers are the same every time.

Try it

Compare the outputs of the four modes; then read the gradient of a dropped unit in training.

Open the interactive lesson →

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

Open the lesson's spec in the player ↗

All lessons