t27.aiРусский

ReLU: keep the positive part

You will learn

What ReLU does to a positive and to a negative input, and why a slope of 0 stops a unit from learning.

A neuron adds up its weighted inputs and passes the sum through an activation. ReLU is the plainest one, max(0, x): a positive sum goes through unchanged and a negative one becomes 0. Its slope is 1 or 0, and a slope of 0 is a unit that has stopped learning, because no gradient flows back through it. A leaky slope fixes that: with a slope of 0.1, x = -2 gives -0.2 and passes back a gradient of 0.1. The spec relu_activation.t27 computes both curves the widget draws.

Try it

Put x at -2 and read the slope with and without the leak; then find the slope at x = 1.5.

Open the interactive lesson →

ReLU: what a neuron does with a negative number
ReLU: what a neuron does with a negative number ↗

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())

Open the lesson's spec in the player ↗

All lessons