Пролог: Парадокс глубины
Представьте, что вы строите небоскрёб. Каждый новый этаж — это слой нейросети. Но после 20 этажей здание вдруг начинает... рушиться. Так было в компьютерном зрении до 2015 года: чем глубже сеть, тем хуже она работала.
ResNet решил это гениально просто: добавил "лифты" между этажами — остаточные связи (skip-connections). Теперь, если новый слой бесполезен, сеть просто "пропускает" его через эти лифты.
Разберём на простом примере
Как ResNet из картинки делает предсказание?
Допустим у нас есть задача предсказать, что в данном изображении будет "человек" класс (0) или "машина" класс (1).
1. Входные данные: RGB-изображение
Ваша фотография — это 3 матрицы чисел (Red, Green, Blue). Например, крошечное изображение 4×4:
Канал R: Канал G: Канал B:
[[2, 4, 1, 3] [[2, 4, 1, 3] [[2, 4, 1, 3]
[0, 5, 8, 2] [0, 5, 8, 2] [0, 5, 8, 2]
[4, 2, 7, 1] [4, 2, 7, 1] [4, 2, 7, 1]
[3, 6, 0, 4]] [3, 6, 0, 4]] [3, 6, 0, 4]]
2. Свёртка: "Фильтр-детектор"
Сеть применяет к изображению фильтры (например, 3×3). Вот как это работает:

Фильтр "скользит" по картинке, умножая и суммируя числа.
Пример рассчета
==================================================
Рассчитаем выход для позиции (0, 0):
Канал 0:
(2*1) + (4*-1) + (1*1) + (0*0) + (5*1) + (8*-1) + (4*-1) + (2*0) + (7*1) = 2 + -4 + 1 + 0 + 5 + -8 + -4 + 0 + 7
= -1
Канал 0 Сумма: -1
Канал 1: Такой же как и на предыдущем шаге.
Канал 2:Такой же как и на предыдущем шаге.
Результат для позиции (0, 0) (Сумма по всем каналам): -1 - 1 -1 = -3
==================================================
Рассчитаем выход для позиции (0, 1):
Канал 0:
(4*1) + (1*-1) + (3*1) + (5*0) + (8*1) + (2*-1) + (2*-1) + (7*0) + (1*1) = 4 + -1 + 3 + 0 + 8 + -2 + -2 + 0 + 1
= 11
Канал 0 Сумма: 11
Канал 1: Такой же как и на предыдущем шаге.
Канал 2: Такой же как и на предыдущем шаге.
Результат для позиции (0, 1) (Сумма по всем каналам): 11 + 11 + 11 = 33
==================================================
Рассчитаем выход для позиции (1, 0):
Канал 0:
(0*1) + (5*-1) + (8*1) + (4*0) + (2*1) + (7*-1) + (3*-1) + (6*0) + (0*1) = 0 + -5 + 8 + 0 + 2 + -7 + -3 + 0 + 0
= -5
Канал 0 Сумма: -5
Канал 1: Такой же как и на предыдущем шаге.
Канал 2: Такой же как и на предыдущем шаге.
Результат для позиции (1, 0) (Сумма по всем каналам): -5 -5 -5 = -15
==================================================
Рассчитаем выход для позиции (1, 1):
Канал 0:
(5*1) + (8*-1) + (2*1) + (2*0) + (7*1) + (1*-1) + (6*-1) + (0*0) + (4*1) = 5 + -8 + 2 + 0 + 7 + -1 + -6 + 0 + 4
= 3
Канал 0 Сумма: 3
Канал 1: Такой же как и на предыдущем шаге.
Канал 2: Такой же как и на предыдущем шаге.
Результат для позиции (1, 1) (Сумма по всем каналам): 3 + 3 + 3 = 9
==================================================
Результат — новая матрица признаков:
[[ -3, 33] [-15, 9]]
🔍 Фишка ResNet: Если фильтр "испортил" картинку, оригинал можно вернуть через skip-connection!
3. Pooling: "Сжатие без потерь"
MaxPooling оставляет только самое яркое значение в области 2×2:
Исходная матрица: После MaxPooling:
[[ -3, 33] [[33]]
[-15, 9]]
(Как если бы вы сжали фото, оставив только ключевые детали)

4. Линейный слой: "Финальный вердикт"
Признаки преобразуются в вектор (например,
[0.6, -1.2, 3.8]
).Умножается на веса классификатора:
# Для классов "человек" (0) и "машина" (1): Класс 0: 0.6*0.7 + (-1.2)*0.5 + 3.8*1.0 + 0.1 = 3.7 Класс 1: 0.6*(-1.2) + (-1.2)*0.3 + 3.8*(-0.4) - 0.2 = -2.8
Итог: Softmax(3.8, -1.59) → 99% вероятность "человек".
Ключевые компоненты ResNet в коде
1. Residual Block — "Лифт для градиентов"
class ResBlock(nn.Module):
def forward(self, x):
skip = x # Сохраняем оригинальный вход!
h = self.conv1(x) # Первая свёртка
h = self.bn1(h) # Нормализация
h = self.relu(h) # Активация
h = self.conv2(h) # Вторая свёртка
h = self.bn2(h) # Нормализация
return self.relu(skip + h) # Складываем с исходным значением
Почему работает: Сеть учит не "абсолютное преобразование", а разницу от исходного x
.
2. BatchNorm — "Стабилизатор обучения"
# Нормализует данные: (x - mean)/stddev
x = (x - torch.mean(x)) / torch.sqrt(torch.var(x) + 1e-5)
x = gamma * x + beta # gamma/beta — обучаемые параметры
Эффект: Градиенты не "взрываются" и не "затухают" даже в 100+ слоях.
Как сеть учится? Обучаемые параметры
Что настраивается:
Ядра свёрток: Числовые значения в фильтрах (например, 3×3 матрицы)
Параметры BatchNorm: γ (масштаб) и β (сдвиг) для каждого канала
Веса линейных слоёв: Матрицы в финальных слоях
Смещения (bias): Добавочные константы в слоях
Процесс обучения:
Прямой проход: Изображение преобразуется в предсказание (как в примере выше).
Расчёт ошибки: Сравнение предсказания с истинной меткой через функцию потерь (например, кросс-энтропия).
Обратное распространение:
Вычисляются градиенты (производные ошибки по каждому параметру)
Градиенты "протекают" обратно по сети через остаточные связи
Оптимизация: Параметры корректируются в сторону, уменьшающую ошибку:
параметр = параметр - learning_rate * градиент
Почему ResNet всё ещё актуален?
Скорость: Работает в несколько раз быстрее ViT на небольших данных.
Количество данных для обучения: Нужно в разы меньше данных, чем для обучения трансформеров.
Масштабируемость: От 18 до 152 слоёв без коллапса.
Философский итог: Простая идея (остаточные связи) оказалась мощнее сложной математики. Иногда гениальное решение лежит на поверхности!
В следующей части: Как ViT (Vision Transformer) научился "понимать" изображения без свёрток — и почему это не всегда лучше ResNet.
P.S. Если хотите глубже разобрать BatchNorm или увидеть обучение ResNet с нуля — пишите в комментариях!