Padding и stride в свёрточных сетях: как считать размер выходного тензора

Ошибка размеров в свёрточной сети часто обнаруживается далеко от причины: Linear ожидает одно количество признаков, а получает другое. Исправлять число наугад неудобно. Надёжнее записать форму входа, параметры каждого слоя и посчитать пространственные оси отдельно. Это позволяет заметить потерянный пиксель до запуска обучения.
Ниже используется соглашение PyTorch для батча изображений: N × C × H × W. Здесь N — количество примеров, C — каналы, H — высота, W — ширина. Разбор относится к обычной Conv2d и MaxPool2d с оговорёнными параметрами; транспонированная свёртка требует другой формулы.
Что именно меняют kernel, padding и stride
Представьте окно, которое последовательно прикладывается к изображению. kernel_size определяет его размер. stride задаёт шаг между соседними положениями окна. padding добавляет границу вокруг исходной области. При стандартном padding_mode у Conv2d это нули; другие поддерживаемые режимы меняют значения на границе, но не превращают каналы в пространственные координаты.
dilation задаёт расстояние между точками ядра. При dilation=1 ядро 3 × 3 занимает область 3 × 3; при dilation=2 те же три позиции вдоль оси охватывают пять входных позиций. Размер весов при этом не становится 5 × 5: увеличивается эффективный охват, а не число точек ядра.
Для одной оси удобно сначала вычислить эффективный размер окна: K_eff = D × (K − 1) + 1. Здесь K — kernel_size, D — dilation. После этого формула выглядит короче:
L_out = floor((L_in + 2 × P − K_eff) / S) + 1
P — симметричный padding с каждой стороны оси, S — stride. floor означает округление вниз. Формула не требует, чтобы деление было целым: неполный следующий шаг просто не создаёт ещё одну позицию обычной свёртки. Если окно вообще не помещается в дополненный вход, корректного положительного выхода для такого слоя нет.
Первый расчёт по клеткам
Возьмём одну ось длиной 7, ядро 3, шаг 2, без дополнения и dilation=1. Окно начинается в позициях 0, 2 и 4 и покрывает соответственно 0–2, 2–4 и 4–6. Получаются три значения. Подстановка даёт floor((7 − 3)/2) + 1 = 3.
Для длины 8 при тех же параметрах результат также равен 3: последнее допустимое начало остаётся 4. Начало 6 потребовало бы позиции 8 за пределами входа. Именно поэтому уменьшение через stride не всегда равно привычному делению размера пополам с округлением, выбранным на глаз.
Теперь добавим padding=1 к длине 7. Дополненная ось имеет длину 9; результат floor((7 + 2 − 3)/2) + 1 = 4. Это не обещание, что сеть станет лучше: дополнение меняет геометрию и участие границы в вычислениях, а качество модели проверяется отдельно.
| L_in | K / S / P / D | K_eff | L_out |
|---|---|---|---|
| 32 | 3 / 1 / 0 / 1 | 3 | 30 |
| 32 | 3 / 1 / 1 / 1 | 3 | 32 |
| 32 | 3 / 2 / 1 / 1 | 3 | 16 |
| 31 | 3 / 2 / 1 / 1 | 3 | 16 |
| 32 | 3 / 1 / 2 / 2 | 5 | 32 |
Высота, ширина и число каналов — три разных вопроса
У Conv2d параметры можно задавать одним числом либо парой. Одно число применяется к обеим пространственным осям; в паре первое значение относится к высоте, второе — к ширине. Поэтому квадратное ядро не делает прямоугольное изображение квадратным.
Авторский пример: вход 4 × 3 × 31 × 47 и слой Conv2d(3, 16, kernel_size=3, stride=2, padding=1). Для высоты получаем floor((31 + 2 − 3)/2) + 1 = 16. Для ширины — floor((47 + 2 − 3)/2) + 1 = 24. Итоговая форма: 4 × 16 × 16 × 24.
Четвёрка не изменилась, поскольку это размер батча. Тройка входных каналов должна соответствовать in_channels слоя, а шестнадцать выходных каналов задаются out_channels. В стандартной свёртке с groups=1 фильтр использует все входные каналы; не нужно отдельно умножать пространственный размер выхода на три.
Для неквадратного случая kernel_size=(3, 5), stride=(2, 3), padding=(1, 2), dilation=1 на входе 31 × 47 высота будет 16, а ширина floor((47 + 4 − 5)/3) + 1 = 16. Совпадение размеров здесь получилось из выбранных параметров, а не из обязательного свойства Conv2d.
Почему same не стоит переносить между библиотеками по памяти
В документации PyTorch padding='valid' соответствует отсутствию дополнения, а padding='same' сохраняет пространственную форму входа. Для Conv2d этот режим поддерживает только stride=1. Поэтому сочетание same и шага 2 нельзя считать универсальным сокращением для произвольного уменьшения изображения.
Для нечётного эффективного ядра и шага 1 симметричное дополнение P=(K_eff−1)/2 сохраняет размер. Например, K=3, D=2 даёт K_eff=5 и P=2. Если эффективное ядро чётное, одинакового целого дополнения с обеих сторон может быть недостаточно для сохранения размера; не подменяйте режим same произвольным округлением этого выражения.
Когда переносите архитектуру, фиксируйте конкретный API и его правила. Даже одинаковые названия аргументов не освобождают от проверки порядка осей, способа дополнения и допустимого шага. Особенно легко ошибиться, если исходный пример использовал другой порядок каналов.
Pooling: похожая арифметика, другие значения и параметры
У MaxPool2d пространственная формула при ceil_mode=False также использует floor. Но если stride не задан, он по умолчанию равен kernel_size. У Conv2d значение stride по умолчанию равно 1. Эта небольшая разница способна незаметно изменить всю цепочку размеров.
Дополнение MaxPool2d в документации описано как отрицательная бесконечность: это существенно для максимума среди отрицательных значений. Не объясняйте pooling как свёртку с обучаемыми весами или как обязательное добавление нулей. Число каналов обычный пространственный max pooling сохраняет.
При ceil_mode=True действуют отдельные правила допустимых окон: допускается выход за границу для окон, начинающихся во входе или левом дополнении, а начинающиеся в правом дополнении игнорируются. Для базового расчёта ниже этот режим выключен. Механически заменить floor на ceil без проверки граничных условий — ненадёжный способ.
Считаем цепочку до Linear
| Этап | Параметры | Форма |
|---|---|---|
| Вход | Четыре RGB-изображения | 4 × 3 × 31 × 47 |
| Conv2d | 3 → 16, K=3, S=2, P=1 | 4 × 16 × 16 × 24 |
| MaxPool2d | K=2, S=2, P=0, ceil_mode=False | 4 × 16 × 8 × 12 |
| Conv2d | 16 → 32, K=3, S=1, P=0 | 4 × 32 × 6 × 10 |
| Flatten с оси 1 | 32 × 6 × 10 | 4 × 1920 |
Если следующий слой Linear получает все эти признаки, его in_features для данного размера входа равен 1920. Батч в произведение не входит: каждому изображению соответствует собственный вектор. Изменение входного разрешения может изменить длину вектора, поэтому число 1920 не является универсальной настройкой сети.
Небольшой калькулятор для проверки записей
def out_size(length, kernel, stride=1, padding=0, dilation=1):
if min(length, kernel, stride, dilation) <= 0 or padding < 0:
raise ValueError("Invalid geometry")
effective = dilation * (kernel - 1) + 1
if length + 2 * padding < effective:
raise ValueError("Kernel does not fit")
return (length + 2 * padding - effective) // stride + 1
assert out_size(31, 3, 2, 1) == 16
assert out_size(47, 3, 2, 1) == 24
assert out_size(32, 3, 1, 2, 2) == 32
assert 32 * out_size(8, 3) * out_size(12, 3) == 1920
Калькулятор использует целочисленные размеры, симметричное дополнение и обычное округление вниз. Он проверяет арифметику примера, но не заменяет ограничения конкретного слоя и не реализует ceil_mode. Для реальной модели дополнительно пропустите тестовый тензор через выбранные слои и сравните shape после каждого этапа с таблицей.
Если формы разошлись, ищите первый неверный переход: порядок H/W, забытый stride у pooling, dilation или число добавленных пикселей. Исправление последнего Linear без выяснения причины скрывает ошибку архитектуры, а последовательная запись размеров показывает, где именно она появилась.



