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.

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
Все уроки
Модуль 1 · Один нейрон
Нейрон складывает входы и изгибает сумму: ReLU, сигмоида и tanh, и где каждая перестаёт пропускать градиент назад.
Модуль 2 · Новые изгибы
Гладкие кривые нынешних моделей: GELU с провалом, Swish с ручкой и ELU с полом вместо обрыва.
Модуль 3 · Слой
Что окружает нейроны: softmax превращает оценки в выбор, эмбеддинг превращает слово в числа, dropout выключает нейроны.
Модуль 4 · Картинки
Чтение картинки по участкам: окно, усреднённое по участку, max pooling и пакетная нормализация признаков разного масштаба.
Модуль 5 · Ошибка
Насколько неверна догадка, одним числом: среднеквадратичная ошибка, перекрёстная энтропия для вероятностей и функция Хьюбера для выбросов.
Модуль 6 · Под гору
Спуск по функции потерь: градиентный спуск с моментом, RMSProp без оглядки на величину градиента и Adam, который соединяет оба.
Модуль 7 · Обучение
Откуда берутся градиенты и чего они добиваются: обратное распространение через слой, сеть, которая учит XOR, и LAMB для больших пакетов.
Модуль 8 · Память и внимание
Сети, которые читают последовательность: рекуррентная ячейка и её затухающее эхо, вентили LSTM и внимание ко всем позициям сразу.
Модуль 9 · Трансформер и награда
Части трансформера и агента: позиции как часы, один блок энкодера и PPO, который учится на награде без рывков.