t27.aiEnglish

Max pooling: максимум окна

Вы узнаете

Что оставляет max pooling, откуда, как он помнит, это взято и куда идёт градиент.

Max pooling оставляет наибольшее значение каждого окна и отбрасывает остальные, поэтому сети становится менее важно, где именно лежит признак. На сетке спеки 4 x 4 окна 2 x 2 с шагом 2 оставляют 4, 8, 9 и -2, взятые из клеток 5, 6, 13 и 11. Перекрывающиеся окна 3 x 3 с шагом 1 оставляют 8, 8, 9 и 9. При обратном проходе весь градиент окна идёт в его максимум: градиенты 1, 2, 3 и 4 попадают в клетки 5, 6, 13 и 11, а все остальные клетки получают 0. Спека maxpool2d_layer.t27 вычисляет выходы, их позиции и градиент.

Попробовать

Переключитесь на окна 3 x 3 и найдите клетку, общую для двух выходов; затем включите градиент для окон 2 x 2.

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

Max pooling: what a network keeps when it shrinks a picture
Max pooling: what a network keeps when it shrinks a picture ↗

Pick a window size and stride and tap an output: max pooling keeps the largest value of its window and remembers where it was, and the gradient later goes to that one cell.

specs/ml/layers/maxpool2d_layer.t27

// SPDX-License-Identifier: Apache-2.0
// t27/specs/
// Reduce spatial dimensions | φ² + 1/φ² = 3 | TRINITY

// #8980: forward and backward compared two values and threw them away
// (placeholders), and every test asserted `result != undefined`. Max pooling now
// runs over one H x W channel (row-major) with a k x k window and a stride. It
// records each window's argmax, and backward sends each gradient back to that
// element only. Padding and dilation are carried in the config but not read: the
// layer assumes padding 0 and dilation 1.

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

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

    pub const MaxPool2DConfig = struct {
        kernel_size : u32,
        stride : u32,
        padding : u32,
        dilation : u32,
        return_indices : bool,
    };

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

    // Output side for an input side n: (n - k) / stride + 1
    fn out_size(n: u32, config: MaxPool2DConfig) -> u32 {
        return (n - config.kernel_size) / config.stride + 1;
    }

    // output[oy][ox] = max of the k x k window at (oy * stride, ox * stride);
    // indices[oy][ox] = the flat input index that held it
    fn forward(input: []const f32, height: u32, width: u32, config: MaxPool2DConfig, output: []f32, indices: []u32) -> void {
        const oh = out_size(height, config);
        const ow = out_size(width, config);
        var oy : u32 = 0;
        while (oy < oh) {
            var ox : u32 = 0;
            while (ox < ow) {
                var best_index : u32 = (oy * config.stride) * width + ox * config.stride;
                var ky : u32 = 0;
                while (ky < config.kernel_size) {
                    var kx : u32 = 0;
                    while (kx < config.kernel_size) {
                        const idx : u32 = (oy * config.stride + ky) * width + ox * config.stride + kx;
                        if (input[idx] > input[best_index]) {
                            best_index = idx;
                        }
                        kx = kx + 1;
                    }
                    ky = ky + 1;
                }
                output[oy * ow + ox] = input[best_index];
                indices[oy * ow + ox] = best_index;
                ox = ox + 1;
            }
            oy = oy + 1;
        }
    }

    // grad_input[indices[o]] += grad_output[o]; every other input gets 0
    fn backward(grad_output: []const f32, indices: []const u32, grad_input: []f32) -> void {
        var i : usize = 0;
        while (i < grad_input.len) {
            grad_input[i] = 0.0;
            i = i + 1;
        }
        var o : usize = 0;
        while (o < grad_output.len) {
            grad_input[indices[o]] = grad_input[indices[o]] + grad_output[o];
            o = o + 1;
        }
    }

    fn cfg(kernel: u32, stride: u32) -> MaxPool2DConfig {
        return MaxPool2DConfig{ .kernel_size = kernel, .stride = stride, .padding = 0, .dilation = 1, .return_indices = true };
    }

    // The output side on the 4 x 4 fixture for a k x k window and a stride.
    // out_size takes a MaxPool2DConfig struct and does not lower to WASM; this
    // takes two integers, so a host reads the side from the compiled spec.
    fn out_side(kernel: u32, stride: u32) -> u32 {
        return out_size(4, cfg(kernel, stride));
    }

    // The 4 x 4 fixture
    //    1  2  5  6
    //    3  4  8  7
    //    0 -1 -3 -2
    //   -4  9 -6 -5
    fn pool_at(kernel: u32, stride: u32, which: u8, index: usize) -> f32 {
        const input : [16]f32 = [1.0, 2.0, 5.0, 6.0, 3.0, 4.0, 8.0, 7.0, 0.0, -1.0, -3.0, -2.0, -4.0, 9.0, -6.0, -5.0];
        var output : [9]f32 = [0.0] ** 9;
        var indices : [9]u32 = [0] ** 9;
        forward(&input, 4, 4, cfg(kernel, stride), &output, &indices);
        if (which == 0) {
            return output[index];
        }
        return @as(f32, @floatFromInt(indices[index]));
    }

    // 2 x 2, stride 2, gradients (1, 2, 3, 4) for the four windows; grad_input[index]
    fn backward_at(index: usize) -> f32 {
        const input : [16]f32 = [1.0, 2.0, 5.0, 6.0, 3.0, 4.0, 8.0, 7.0, 0.0, -1.0, -3.0, -2.0, -4.0, 9.0, -6.0, -5.0];
        var output : [4]f32 = [0.0] ** 4;
        var indices : [4]u32 = [0] ** 4;
        forward(&input, 4, 4, cfg(2, 2), &output, &indices);
        const grad_output : [4]f32 = [1.0, 2.0, 3.0, 4.0];
        var grad_input : [16]f32 = [9.0] ** 16;
        backward(&grad_output, &indices, &grad_input);
        return grad_input[index];
    }

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

    // 2 x 2, stride 2: windows' maxima are 4, 8, 9, -2
    test forward_basic_case
        given first = pool_at(2, 2, 0, 0)
        when last = pool_at(2, 2, 0, 3)
        then first == 4.0 and pool_at(2, 2, 0, 1) == 8.0 and pool_at(2, 2, 0, 2) == 9.0 and last == -2.0

    // The argmax indices: 4 at 5, 8 at 6, 9 at 13, -2 at 11
    test forward_max_finding
        given first = pool_at(2, 2, 1, 0)
        when third = pool_at(2, 2, 1, 2)
        then first == 5.0 and pool_at(2, 2, 1, 1) == 6.0 and third == 13.0 and pool_at(2, 2, 1, 3) == 11.0

    // 3 x 3, stride 1 gives a 2 x 2 output whose windows overlap
    test forward_overlapping_windows
        given first = pool_at(3, 1, 0, 0)
        when last = pool_at(3, 1, 0, 3)
        then first == 8.0 and pool_at(3, 1, 0, 1) == 8.0 and pool_at(3, 1, 0, 2) == 9.0 and last == 9.0

    test backward_basic_case
        given at_max = backward_at(5)
        when elsewhere = backward_at(0)
        then at_max == 1.0 and elsewhere == 0.0

    // Each window's gradient lands on its argmax only: the sum is conserved
    test backward_gradient_propagation
        given total = backward_at(5) + backward_at(6) + backward_at(13) + backward_at(11)
        when others = backward_at(0) + backward_at(1) + backward_at(4) + backward_at(15)
        then total == 10.0 and others == 0.0 and backward_at(13) == 3.0

    // The four window/stride pairs the nn-maxpool widget offers, on the 4 x 4 fixture:
    // 2 x 2 stride 2 -> 2, 3 x 3 stride 1 -> 2, 2 x 2 stride 1 -> 3, 4 x 4 stride 1 -> 1
    test out_side_of_each_widget_config
        given halved = out_side(2, 2)
        when whole = out_side(4, 1)
        then halved == 2 and out_side(3, 1) == 2 and out_side(2, 1) == 3 and whole == 1

    // Edges: a 1 x 1 window at stride 1 keeps the side; a stride that does not
    // divide (4 - k) rounds down (3 x 3 at stride 2 fits one window, 1 x 1 at
    // stride 3 fits two)
    test out_side_identity_and_floor
        given same = out_side(1, 1)
        when floored = out_side(3, 2)
        then same == 4 and floored == 1 and out_side(1, 3) == 2

    invariant maxpool_output_size_formula
        assert out_size(4, cfg(2, 2)) == 2 and out_size(4, cfg(3, 1)) == 2 and out_size(5, cfg(2, 2)) == 2

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

Все уроки