t27.aiEnglish

Вентили LSTM

Вы узнаете

Как вентили забывания, входа и выхода LSTM решают, что ячейка хранит, записывает и показывает.

Ячейка LSTM хранит отдельную память c и три вентиля, каждый это сигмоида между 0 и 1: забывание масштабирует старую память, вход впускает новое содержимое g, а выход решает, сколько памяти показать. c' = f c + i g и h' = o tanh(c'). В фикстуре спеки каждый вес равен 0, поэтому каждый вентиль это сигмоида своего смещения: при вентиле забывания 0.9975 и вентиле входа 0.0025 память 1 выходит как 0.9975, а выход равен 0.7587. Так как c проходит лишь с умножением на f, её градиент тоже переживает много шагов. Спека lstm_single.t27 вычисляет ячейку.

Попробовать

Загрузите Remember, а затем Forget и сравните новую память; затем запишите с открытым вентилем входа.

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

LSTM gates: how a cell decides what to keep
LSTM gates: how a cell decides what to keep ↗

Set the biases of the forget, input and output gates and the old memory: the new memory is forget x old + input x candidate, and the output shows tanh of it through the output gate.

specs/ml/recurrent/lstm_single.t27

// SPDX-License-Identifier: Apache-2.0
// t27/specs/
// LSTM solves vanishing gradient problem | φ² + 1/φ² = 3 | TRINITY

// #8980: called `@sigmoid` and `@tanh`, which Zig does not have; each gate weight
// was one number per INPUT, indexed `i % hidden_size`, so it was not an LSTM; and
// every test asserted `true`. Now each gate has its own matrices, W_x
// (hidden x input) and W_h (hidden x hidden), and a bias:
//   i = s(z_i), f = s(z_f), g = tanh(z_g), o = s(z_o),  z = W_x x + W_h h + b
//   c' = f c + i g,  h' = o tanh(c')
// backward gives every gradient of one step, and the tests check it against finite
// differences.

module LstmCell;
    use base::types;
    use math::constants;

    // ═══════════════════════════════════════════════════════════
    // 1. Constants
    // ═══════════════════════════════════════════════════════════

    const FORGET_BIAS_INIT : f32 = 1.0;

    // ═══════════════════════════════════════════════════════════
    // 2. Types
    // ═══════════════════════════════════════════════════════════

    pub const LSTMConfig = struct {
        input_size : u32,
        hidden_size : u32,
    };

    pub const LSTMState = struct {
        h : []f32,
        c : []f32,
    };

    pub const LSTMWeights = struct {
        W_ii : []f32,
        W_hi : []f32,
        b_i : []f32,
        W_if : []f32,
        W_hf : []f32,
        b_f : []f32,
        W_ig : []f32,
        W_hg : []f32,
        b_g : []f32,
        W_io : []f32,
        W_ho : []f32,
        b_o : []f32,
    };

    // ═══════════════════════════════════════════════════════════
    // 3. Core Functions
    // ═══════════════════════════════════════════════════════════

    fn sigmoid(z: f32) -> f32 {
        return 1.0 / (1.0 + exp(-z));
    }

    // Pre-activation of one gate for hidden unit u: W_x[u] . x + W_h[u] . h + b[u]
    fn pre(w_x: []const f32, w_h: []const f32, b: []const f32, x: []const f32, h: []const f32, u: usize, config: LSTMConfig) -> f32 {
        var z : f32 = b[u];
        var j : usize = 0;
        while (j < config.input_size) {
            z = z + w_x[u * config.input_size + j] * x[j];
            j = j + 1;
        }
        var k : usize = 0;
        while (k < config.hidden_size) {
            z = z + w_h[u * config.hidden_size + k] * h[k];
            k = k + 1;
        }
        return z;
    }

    fn forward(input: []const f32, state: LSTMState, weights: LSTMWeights, output_state: LSTMState, config: LSTMConfig) -> void {
        var u : usize = 0;
        while (u < config.hidden_size) {
            const ig = sigmoid(pre(weights.W_ii, weights.W_hi, weights.b_i, input, state.h, u, config));
            const fg = sigmoid(pre(weights.W_if, weights.W_hf, weights.b_f, input, state.h, u, config));
            const gg = tanh(pre(weights.W_ig, weights.W_hg, weights.b_g, input, state.h, u, config));
            const og = sigmoid(pre(weights.W_io, weights.W_ho, weights.b_o, input, state.h, u, config));
            output_state.c[u] = fg * state.c[u] + ig * gg;
            output_state.h[u] = og * tanh(output_state.c[u]);
            u = u + 1;
        }
    }

    // Adds dz * (x, h, 1) into one gate's weight gradients, and dz * W into grad_input / grad_state.h
    fn spread(dz: f32, w_x: []const f32, w_h: []const f32, gw_x: []f32, gw_h: []f32, gb: []f32, x: []const f32, h: []const f32, u: usize, config: LSTMConfig, grad_input: []f32, grad_h: []f32) -> void {
        var j : usize = 0;
        while (j < config.input_size) {
            gw_x[u * config.input_size + j] = gw_x[u * config.input_size + j] + dz * x[j];
            grad_input[j] = grad_input[j] + dz * w_x[u * config.input_size + j];
            j = j + 1;
        }
        var k : usize = 0;
        while (k < config.hidden_size) {
            gw_h[u * config.hidden_size + k] = gw_h[u * config.hidden_size + k] + dz * h[k];
            grad_h[k] = grad_h[k] + dz * w_h[u * config.hidden_size + k];
            k = k + 1;
        }
        gb[u] = gb[u] + dz;
    }

    // One step back. grad_output = dL/dh', grad_cell = dL/dc' from later steps. Writes
    // dL/dx, dL/dh, dL/dc and ADDS the weight gradients into grad_weights.
    //   do = dh' tanh(c');  dc = dh' o (1 - tanh^2 c') + grad_cell
    //   dz_o = do o (1 - o), dz_f = dc c f (1 - f), dz_i = dc g i (1 - i), dz_g = dc i (1 - g^2)
    //   dL/dc = dc f
    fn backward(grad_output: []const f32, grad_cell: []const f32, input: []const f32, state: LSTMState, weights: LSTMWeights, config: LSTMConfig, grad_input: []f32, grad_state: LSTMState, grad_weights: LSTMWeights) -> void {
        var j : usize = 0;
        while (j < config.input_size) {
            grad_input[j] = 0.0;
            j = j + 1;
        }
        var u : usize = 0;
        while (u < config.hidden_size) {
            grad_state.h[u] = 0.0;
            u = u + 1;
        }
        u = 0;
        while (u < config.hidden_size) {
            const ig = sigmoid(pre(weights.W_ii, weights.W_hi, weights.b_i, input, state.h, u, config));
            const fg = sigmoid(pre(weights.W_if, weights.W_hf, weights.b_f, input, state.h, u, config));
            const gg = tanh(pre(weights.W_ig, weights.W_hg, weights.b_g, input, state.h, u, config));
            const og = sigmoid(pre(weights.W_io, weights.W_ho, weights.b_o, input, state.h, u, config));
            const c_new = fg * state.c[u] + ig * gg;
            const tc = tanh(c_new);
            const dc = grad_output[u] * og * (1.0 - tc * tc) + grad_cell[u];
            const dz_o = grad_output[u] * tc * og * (1.0 - og);
            const dz_f = dc * state.c[u] * fg * (1.0 - fg);
            const dz_i = dc * gg * ig * (1.0 - ig);
            const dz_g = dc * ig * (1.0 - gg * gg);
            grad_state.c[u] = dc * fg;
            spread(dz_i, weights.W_ii, weights.W_hi, grad_weights.W_ii, grad_weights.W_hi, grad_weights.b_i, input, state.h, u, config, grad_input, grad_state.h);
            spread(dz_f, weights.W_if, weights.W_hf, grad_weights.W_if, grad_weights.W_hf, grad_weights.b_f, input, state.h, u, config, grad_input, grad_state.h);
            spread(dz_g, weights.W_ig, weights.W_hg, grad_weights.W_ig, grad_weights.W_hg, grad_weights.b_g, input, state.h, u, config, grad_input, grad_state.h);
            spread(dz_o, weights.W_io, weights.W_ho, grad_weights.W_io, grad_weights.W_ho, grad_weights.b_o, input, state.h, u, config, grad_input, grad_state.h);
            u = u + 1;
        }
    }

    // The fixture: input 3, hidden 2. All 48 weights live in one buffer, per gate
    // W_x (6), W_h (4), b (2), in the gate order i, f, g, o.
    fn cfg() -> LSTMConfig {
        return LSTMConfig{ .input_size = 3, .hidden_size = 2 };
    }

    fn weights_of(w: []f32) -> LSTMWeights {
        return LSTMWeights{
            .W_ii = w[0..6], .W_hi = w[6..10], .b_i = w[10..12],
            .W_if = w[12..18], .W_hf = w[18..22], .b_f = w[22..24],
            .W_ig = w[24..30], .W_hg = w[30..34], .b_g = w[34..36],
            .W_io = w[36..42], .W_ho = w[42..46], .b_o = w[46..48],
        };
    }

    // Deterministic weights in [-0.5, 0.5]; the forget biases start at FORGET_BIAS_INIT
    fn fill(w: []f32) -> void {
        var k : usize = 0;
        while (k < 48) {
            w[k] = @as(f32, @floatFromInt((k * 7 + 3) % 11)) / 10.0 - 0.5;
            k = k + 1;
        }
        w[22] = FORGET_BIAS_INIT;
        w[23] = FORGET_BIAS_INIT;
    }

    // L = 0.5 h'_0 - 0.3 h'_1 + 0.2 c'_0 + 0.1 c'_1 after one step, with one input, one h or
    // one c entry moved by `delta` (which: 0 = none, 1 + j = x_j, 4 + k = h_k, 6 + k = c_k,
    // 8 = b_f[1])
    fn loss(which: usize, delta: f32) -> f32 {
        var w : [48]f32 = undefined;
        fill(&w);
        var x : [3]f32 = [1.0, 2.0, 3.0];
        var h : [2]f32 = [0.1, 0.2];
        var c : [2]f32 = [0.3, 0.4];
        if (which >= 1 and which <= 3) {
            x[which - 1] = x[which - 1] + delta;
        }
        if (which >= 4 and which <= 5) {
            h[which - 4] = h[which - 4] + delta;
        }
        if (which >= 6 and which <= 7) {
            c[which - 6] = c[which - 6] + delta;
        }
        if (which == 8) {
            w[23] = w[23] + delta;
        }
        var h_out : [2]f32 = [0.0] ** 2;
        var c_out : [2]f32 = [0.0] ** 2;
        forward(&x, LSTMState{ .h = &h, .c = &c }, weights_of(&w), LSTMState{ .h = &h_out, .c = &c_out }, cfg());
        return 0.5 * h_out[0] - 0.3 * h_out[1] + 0.2 * c_out[0] + 0.1 * c_out[1];
    }

    // The analytic gradient of that loss, same `which` numbering
    fn grad(which: usize) -> f32 {
        var w : [48]f32 = undefined;
        fill(&w);
        const x : [3]f32 = [1.0, 2.0, 3.0];
        var h : [2]f32 = [0.1, 0.2];
        var c : [2]f32 = [0.3, 0.4];
        const grad_output : [2]f32 = [0.5, -0.3];
        const grad_cell : [2]f32 = [0.2, 0.1];
        var gx : [3]f32 = [0.0] ** 3;
        var gh : [2]f32 = [0.0] ** 2;
        var gc : [2]f32 = [0.0] ** 2;
        var gw : [48]f32 = [0.0] ** 48;
        backward(&grad_output, &grad_cell, &x, LSTMState{ .h = &h, .c = &c }, weights_of(&w), cfg(), &gx, LSTMState{ .h = &gh, .c = &gc }, weights_of(&gw));
        if (which >= 1 and which <= 3) {
            return gx[which - 1];
        }
        if (which >= 4 and which <= 5) {
            return gh[which - 4];
        }
        if (which >= 6 and which <= 7) {
            return gc[which - 6];
        }
        return gw[23];
    }

    // Sum of |every gradient| when grad_output and grad_cell are both 0
    fn zero_upstream_total() -> f32 {
        var w : [48]f32 = undefined;
        fill(&w);
        const x : [3]f32 = [1.0, 2.0, 3.0];
        var h : [2]f32 = [0.1, 0.2];
        var c : [2]f32 = [0.3, 0.4];
        const zeros : [2]f32 = [0.0, 0.0];
        var gx : [3]f32 = [0.0] ** 3;
        var gh : [2]f32 = [0.0] ** 2;
        var gc : [2]f32 = [0.0] ** 2;
        var gw : [48]f32 = [0.0] ** 48;
        backward(&zeros, &zeros, &x, LSTMState{ .h = &h, .c = &c }, weights_of(&w), cfg(), &gx, LSTMState{ .h = &gh, .c = &gc }, weights_of(&gw));
        var total : f32 = @abs(gx[0]) + @abs(gx[1]) + @abs(gx[2]) + @abs(gh[0]) + @abs(gh[1]) + @abs(gc[0]) + @abs(gc[1]);
        var k : usize = 0;
        while (k < 48) {
            total = total + @abs(gw[k]);
            k = k + 1;
        }
        return total;
    }

    fn slope(which: usize) -> f32 {
        return (loss(which, 0.001) - loss(which, -0.001)) / 0.002;
    }

    // which: 0 = h'_0, 1 = c'_0 for a 1-unit cell with every weight 0 and the given biases
    fn bias_only(b_i: f32, b_f: f32, b_g: f32, b_o: f32, c_prev: f32, which: u8) -> f32 {
        var w : [48]f32 = [0.0] ** 48;
        w[10] = b_i;
        w[22] = b_f;
        w[34] = b_g;
        w[46] = b_o;
        const x : [3]f32 = [1.0, 2.0, 3.0];
        var h : [2]f32 = [0.0, 0.0];
        var c : [2]f32 = [c_prev, 0.0];
        var h_out : [2]f32 = [9.0] ** 2;
        var c_out : [2]f32 = [9.0] ** 2;
        forward(&x, LSTMState{ .h = &h, .c = &c }, weights_of(&w), LSTMState{ .h = &h_out, .c = &c_out }, cfg());
        if (which == 0) {
            return h_out[0];
        }
        return c_out[0];
    }

    fn valid_input() -> usize {
        return cfg().input_size;
    }

    // ═══════════════════════════════════════════════════════════
    // TDD: Tests (from .tri behaviors)
    // ═══════════════════════════════════════════════════════════

    // All weights 0: every gate is s(0) = 0.5 or tanh(0) = 0, so c' = 0.5 c and h' = 0.5 tanh(c')
    test forward_basic_case
        given c_new = bias_only(0.0, 0.0, 0.0, 0.0, 0.8, 1)
        when h_new = bias_only(0.0, 0.0, 0.0, 0.0, 0.8, 0)
        then @abs(c_new - 0.4) < 0.000001 and @abs(h_new - 0.5 * tanh(c_new)) < 0.000001 and @abs(h_new - 0.1899745) < 0.000001

    // A closed forget gate (b_f = -30) erases the old cell; an open one (b_f = 30) keeps it
    test forward_basic_lstm
        given erased = bias_only(0.0, -30.0, 0.0, 0.0, 0.8, 1)
        when kept = bias_only(-30.0, 30.0, 0.0, 0.0, 0.8, 1)
        then @abs(erased) < 0.000001 and @abs(kept - 0.8) < 0.000001

    // A closed input gate and forget gate leave c' = 0, so h' = 0 whatever the output gate does
    test forward_with_empty_input
        given h_new = bias_only(-30.0, -30.0, 1.0, 30.0, 0.8, 0)
        then @abs(h_new) < 0.000001

    // dL/dx_j against central differences
    test backward_basic_case
        given g0 = grad(1)
        when g2 = grad(3)
        then @abs(g0 - slope(1)) < 0.0005 and @abs(grad(2) - slope(2)) < 0.0005 and @abs(g2 - slope(3)) < 0.0005

    // dL/dh_k, dL/dc_k and dL/db_f[1] against central differences
    test backward_basic_lstm
        given gh0 = grad(4)
        when gc1 = grad(7)
        then @abs(gh0 - slope(4)) < 0.0005 and @abs(grad(5) - slope(5)) < 0.0005 and @abs(grad(6) - slope(6)) < 0.0005 and @abs(gc1 - slope(7)) < 0.0005 and @abs(grad(8) - slope(8)) < 0.0005

    // With no upstream gradient nothing flows back
    test backward_with_empty_grad
        given total = zero_upstream_total()
        then total == 0.0 and @abs(grad(1)) > 0.0

    // ═══════════════════════════════════════════════════════════
    // TDD: Invariants (from .tri constraints)
    // ═══════════════════════════════════════════════════════════

    // input.size() == config.input_size
    invariant lstm_cell_constraint_0
        assert valid_input() == 3

    // state.h.size() == config.hidden_size
    invariant lstm_cell_constraint_1
        assert cfg().hidden_size == 2

    // state.c.size() == config.hidden_size; the forget bias starts open
    invariant lstm_cell_constraint_2
        assert FORGET_BIAS_INIT > 0.0

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

Все уроки