Сверточный слой в компьютерном зрении: обучаемые 3D-фильтры, отступы, шаг и разделение весов
Сверточный слой в компьютерном зрении: обучаемые 3D-фильтры, отступы, шаг и разделение весов
Представьте скромное изображение размером пикселей с тремя цветовыми каналами RGB. Если подать его в полносвязный слой всего с 512 скрытыми нейронами, матрица весов потребует миллионов параметров. Для одной только первой связи потребуется около 400 мегабайт оперативной памяти, а модель мгновенно переобучится, запоминая шум. Но главная проблема даже не в памяти: выпрямляя матрицу в одномерный вектор через Flatten, мы полностью разрушаем пространственную структуру сцены — пиксель перестает быть соседом пикселя и оказывается оторван от своего локального контекста.
Как обрабатывать изображения высокого разрешения, не раздувая число весов до сотен миллионов и сохраняя пространственные связи между соседними пикселями? Ответом стали сверточные нейронные сети (Convolutional Neural Networks, CNN), в основе которых лежит операция локальной многоканальной свертки.
От фиксированных ядер к обучаемым 3D-фильтрам
В классической обработке изображений вы уже применяли операцию дискретной свертки: ядра Собеля выделяли градиенты яркости, а фильтр Гаусса сглаживал высокочастотный шум. Однако в OpenCV ядра были жестко зафиксированы разработчиком. Если освещение менялось или дефект поворачивался под нестандартным углом, эвристические матрицы переставали работать.
Сверточный слой нейросети использует ту же математическую операцию скользящего окна, но с принципиальным отличием: значения внутри ядра свертки — это обучаемые параметры , которые оптимизируются алгоритмом градиентного спуска через обратное распространение ошибки.
Кроме того, реальные тензоры изображений имеют три измерения: высоту , ширину и число каналов (например, 3 для RGB или десятки/сотни на глубоких слоях сети). Поэтому фильтр сверточного слоя — это не плоская матрица, а трехмерный параллелепипед весов формы , где — пространственный размер ядра (обычно или ).
Ключевой принцип глубины фильтра: Пространственная глубина сверточного фильтра всегда строго равна количеству входных каналов обрабатываемого тензора.
Математика многоканальной свертки
Когда 3D-фильтр прикладывается к локальному участку входного тензора , происходит следующее:
- Для каждого входного канала вычисляется поэлементное произведение между срезом входного тензора и соответствующим двумерным ядром фильтра.
- Результаты произведений суммируются по всей площади окна и по всем входным каналам.
- К полученной скалярной сумме прибавляется общее для данного фильтра скалярное смещение (bias).
Математически значение выходного элемента на позиции для -го фильтра выражается формулой:
Разберем эту формулу на практике:
- — результирующее числовое значение в выходной карте признаков на строке и столбце , сгенерированное -м фильтром;
- — обучаемое числовое смещение (bias), добавляемое ко всему результату одного -го фильтра;
- — количество каналов на входе (например, 3 для RGB-кадра);
- — размер стороны квадратного ядра (например, );
- — значение пикселя входного тензора в канале со смещением относительно начала текущего окна;
- — конкретный обучаемый вес -го фильтра для канала в позиции ядра .
Один трехмерный фильтр, проходя по всему входному тензору , формирует одну двумерную матрицу — карту признаков (feature map) размером . Если мы хотим извлечь на данном слое различных признаков (например, вертикальные линии, текстуры и цветовые переходы), мы берем ансамбль из независимых 3D-фильтров. В результате на выходе получается 3D-тензор формы .
Разделение весов и трансляционная эквивариантность
Почему свертка требует на порядки меньше параметров, чем полносвязный слой? Причина кроется в концепции разделения весов (weight sharing).
В полносвязном слое каждый выходной нейрон имеет свой уникальный набор весов для каждого входного пикселя. В сверточном слое один и тот же 3D-фильтр скользит по всему изображению. Веса ядра остаются неизменными независимо от того, обрабатывает ли фильтр левый верхний угол, центр или правый нижний край кадра.
Из разделения весов вытекают два важнейших свойства сверточных сетей:
- Резкое сокращение числа параметров: для фильтра на RGB-входе требуется всего весов (плюс 1 смещение), и это число вообще не зависит от разрешения входного изображения ( или ).
- Трансляционная эквивариантность (translation equivariance): если объект на входном изображении сместится на 10 пикселей вправо, активация на карте признаков сместится ровно на те же 10 позиций, не изменив своей формы. Сети не нужно заново учиться распознавать трещину или контур детали в каждом отдельном углу кадра.
Геометрия свертки: отступы (Padding) и шаг (Stride)
Размер выходной карты признаков определяется не только размером ядра , но и двумя ключевыми гиперпараметрами слоя: отступами (padding) и шагом (stride).
Отступы (Padding)
Когда ядро размера () скользит по изображению без выхода за границы, крайние пиксели участвуют в вычислениях гораздо реже, чем центральные. Кроме того, пространственный размер матрицы с каждым слоем уменьшается: для ядра мы теряем по 1 пикселю с каждой стороны ().
Чтобы предотвратить неконтролируемое сжатие карт признаков и сохранить информацию на границах, по периметру входного тензора добавляют фиктивные рамки из нулей (zero padding):
- Valid Padding (): дополнение отсутствует. Ядро скользит только внутри исходного тензора, размер выхода уменьшается.
- Same Padding ( при нечетном и шаге ): вход дополняется нулями так, чтобы пространственный размер выхода в точности совпадал с входом (, ).
Шаг свертки (Stride)
Шаг свертки определяет величину дискретного смещения скользящего окна на каждом шаге.
- При фильтр сдвигается на 1 пиксель, сканируя вход непрерывно.
- При фильтр перескакивает через один пиксель как по вертикали, так и по горизонтали. Это приводит к уменьшению пространственного разрешения карты признаков приблизительно в 2 раза, заменяя операцию пулинга.
Поясним компоненты формулы:
- — исходная высота входного тензора в пикселях;
- — размер отступа (padding) с каждой из сторон (если padding=1, к высоте добавляется пикселя);
- — пространственный размер ядра (kernel size);
- — величина шага смещения фильтра (stride);
- — операция округления вниз (целочисленное деление), гарантирующая, что неполный шаг ядра за пределами границы отбрасывается.
Разберем практический пример: пусть входной тензор имеет высоту , размер ядра , отступ и шаг . Подставляем значения в формулу:
Выходное пространственное разрешение уменьшилось ровно в 2 раза — с до .
Сверточный слой против полносвязного: анатомия параметров
Сравним архитектурные затраты полносвязного и сверточного слоев.
| Критерий | Полносвязный слой (nn.Linear) |
Сверточный слой (nn.Conv2d) |
|---|---|---|
| Входной формат | 1D-вектор признаков | 4D-тензор |
| Связи нейронов | Каждый выход соединен со всеми входами | Локальные связи в пределах окна |
| Разделение весов | Отсутствует (у каждой связи свой вес) | Есть (один 3D-фильтр на всю карту) |
| Формула параметров | ||
| Зависимость весов от | Напрямую зависит от разрешения кадра | Не зависит от пространственного размера |
| Пространственная топология | Разрушается операцией Flatten |
Сохраняется в виде карты признаков |
Формула расчета обучаемых параметров сверточного слоя наглядно показывает его компактность:
Здесь:
- — количество выходных фильтров (каждый формирует свой канал признаков);
- — количество весовых коэффициентов в теле одного 3D-фильтра;
- — одно обучаемое смещение (bias) на каждый фильтр (если параметр
bias=True).
Пример: слой nn.Conv2d(in_channels=64, out_channels=128, kernel_size=3, bias=True) содержит:
Этот слой может обрабатывать изображение как , так и пикселей — число весов останется строго 73 856.
Реализация сверточного слоя в PyTorch
В PyTorch сверточный слой для двумерных изображений реализован в классе torch.nn.Conv2d. На вход он ожидает 4D-тензор стандартного формата (Batch_size, Channels, Height, Width) — NCHW.
import torch
import torch.nn as nn
# Создаем синтетический батч: 4 RGB-изображения размером 128x128
input_tensor = torch.randn(4, 3, 128, 128)
# Сверточный слой: 3 входных канала -> 16 выходных карт признаков
# Ядро 3x3, шаг 1, отступ 1 (Same padding)
conv_layer = nn.Conv2d(
in_channels=3,
out_channels=16,
kernel_size=3,
stride=1,
padding=1,
bias=True
)
# Прямой проход
output_tensor = conv_layer(input_tensor)
print(f"Форма входа: {input_tensor.shape}") # torch.Size([4, 3, 128, 128])
print(f"Форма выхода: {output_tensor.shape}") # torch.Size([4, 16, 128, 128])
# Проверяем количество параметров: 16 * (3 * 3 * 3 + 1) = 16 * 28 = 448
total_params = sum(p.numel() for p in conv_layer.parameters() if p.requires_grad)
print(f"Обучаемых параметров: {total_params}") # 448
Тензор весов conv_layer.weight имеет форму (out_channels, in_channels, kernel_size, kernel_size), то есть (16, 3, 3, 3). Смещение conv_layer.bias имеет форму (16,) — ровно по одному числу на каждую выходную карту признаков.
Сверточный слой преобразует локальные пиксельные паттерны в абстрактные карты признаков. Однако при последовательном наложении нескольких слоев возникает вопрос: какую область исходного кадра охватывает отдельный нейрон на глубоких уровнях сети и как сжать карты признаков перед финальной классификацией? Эту задачу решают операции пулинга и концепция рецептивного поля, которые мы разберем в следующей главе.