Почему в residual connection используется операция сложения?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Почему в residual connections используется сложение?
Residual connection (остаточная связь) — это одна из самых важных инноваций в глубоких нейронных сетях (ResNet, Transformers, Vision Transformers). Главный компонент — операция сложения, а не конкатенации или других операций. Вот почему.
1. Решение проблемы исчезающего градиента (Vanishing Gradient)
В глубоких сетях без residual connections происходит:
Loss → Gradient → Backprop → ... → Gradient × 0.1 × 0.1 × 0.1 × ...
↓
Gradient → 0 (исчезает)
С residual connections:
Gradient идёт двумя путями:
1) y = f(x) → градиент может исчезнуть
2) y = x + f(x) → градиент = 1 + градиент(f)
↓
Градиент(x) НЕ = 0!
Математически:
Без skip-connection: ∂L/∂x = ∂L/∂y × ∂y/∂x = ∂L/∂y × ∂f/∂x
C skip-connection: ∂L/∂x = ∂L/∂y × (1 + ∂f/∂x) ← добавляется 1!
↑
Даже если ∂f/∂x → 0, градиент ≠ 0
Операция сложения критична! Если бы использовали конкатенацию или умножение, градиент всё равно мог бы исчезнуть.
2. Архитектура ResNet: формула
y = x + F(x)
Где:
- x = вход (identity)
- F(x) = основная функция (conv layers)
- + = **сложение**, а не другая операция
Почему именно сложение?
import torch
import torch.nn as nn
class ResidualBlock(nn.Module):
def __init__(self, channels):
super().__init__()
self.conv1 = nn.Conv2d(channels, channels, 3, padding=1)
self.bn1 = nn.BatchNorm2d(channels)
self.relu = nn.ReLU()
self.conv2 = nn.Conv2d(channels, channels, 3, padding=1)
self.bn2 = nn.BatchNorm2d(channels)
def forward(self, x):
identity = x # Сохраняем вход
out = self.conv1(x)
out = self.bn1(out)
out = self.relu(out)
out = self.conv2(out)
out = self.bn2(out)
out = out + identity # ← СЛОЖЕНИЕ!
out = self.relu(out)
return out
3. Сравнение с альтернативными операциями
Вариант 1: Конкатенация (❌ Плохо)
out = torch.cat([out, identity], dim=1) # Конкатенация
Минусы:
- Размерность растёт: (C, H, W) → (2C, H, W) → (3C, H, W) → ...
- Требует extra convolution для вернуться к исходной размерности
- Много дополнительных параметров
- DenseNet использует этот подход → тяжелее в памяти
Вариант 2: Умножение (❌ Плохо)
out = out * identity # Элементное умножение
Минусы:
- Если f(x) → 0, то out → 0 (потеря исходной информации)
- Gradients: ∂L/∂x = ∂L/∂y × (∂f/∂x × identity + f × 0) = ...
- Может привести к обнулению градиентов
- Нестабильно при инициализации (если identity близко к 1)
Вариант 3: Средневзвешенное (❌ Слабее)
out = 0.5 * out + 0.5 * identity
Минусы:
- Работает хуже, чем сложение
- Требует ручной балансировки весов
- Без теоретического обоснования
Вариант 4: Сложение (✅ Идеально)
out = out + identity
Преимущества:
- Нет роста размерности
- Простая операция (одно сложение)
- Теоретическое обоснование через градиенты
- Работает в очень глубоких сетях (ResNet-152)
4. Теория градиентов: почему сложение?
Ключевая идея: градиент становится суммой двух путей
y = x + f(x)
∂y/∂x = ∂(x + f(x))/∂x = 1 + ∂f/∂x
Даже если |∂f/∂x| < 1 (исчезающий градиент),
мы гарантировано получим |∂y/∂x| ≥ 1 - |∂f/∂x| > 0
→ Градиент ВСЕГДА передаётся обратно через identity!
Численный пример:
import numpy as np
# Допустим, градиент в f очень мал
dLdf = 0.001 # ∂L/∂f
df_dx = 0.01 # ∂f/∂x (маленький)
# БЕЗ skip-connection
dL_dx_no_skip = dLdf * df_dx
print(f"Без skip: ∂L/∂x = {dL_dx_no_skip:.6f}") # 0.00001 (почти 0)
# С skip-connection
dL_dx_with_skip = dLdf * (1 + df_dx)
print(f"С skip: ∂L/∂x = {dL_dx_with_skip:.6f}") # 0.001 (остался!)
print(f"\nУлучшение в {dL_dx_with_skip / dL_dx_no_skip:.0f}x раз")
5. Практическое применение
ResNet (сложение)
class ResNet(nn.Module):
def __init__(self):
super().__init__()
self.layer1 = ResidualBlock(64)
self.layer2 = ResidualBlock(64)
self.layer3 = ResidualBlock(64) # Может быть ОЧЕНЬ глубокая сеть
# Благодаря сложению, ResNet-152 обучается стабильно
Vision Transformer (сложение + LayerNorm)
class TransformerBlock(nn.Module):
def forward(self, x):
# Multi-Head Attention + skip-connection
attn_out = self.mha(self.ln1(x))
x = x + attn_out # ← Сложение!
# FFN + skip-connection
ffn_out = self.ffn(self.ln2(x))
x = x + ffn_out # ← Сложение!
return x
Transformers в NLP (Attention + skip)
# В BERT, GPT-2, GPT-3 используется сложение:
y = x + MultiHeadAttention(x)
z = y + FeedForward(y)
6. Почему НЕ конкатенация (как в DenseNet)?
DenseNet использует конкатенацию:
# DenseNet: конкатенирует ВСЕ предыдущие слои
x_0 = x
x_1 = f_1(x_0)
x_2 = f_2([x_0, x_1]) # Конкатенация!
x_3 = f_3([x_0, x_1, x_2])
Почему DenseNet выбрал конкатенацию?
- Усиливает перепады признаков (feature variance)
- Работает хорошо на меньших датасетах (ImageNet)
- Но требует больше памяти (в 2x раза)
ResNet выбрал сложение потому что:
- Меньше памяти (только x и f(x))
- Проще реализовать
- Работает на любой глубине
- Теоретически надёжнее (градиенты)
7. Обобщение: условие идентичности
Чтобы skip-connection работал, НУЖНО условие идентичности:
x и f(x) должны быть одной размерности!
# ✅ Правильно: одинаковые размеры
out = Conv(x, 64) + x # (64, 32, 32) + (64, 32, 32)
# ❌ Неправильно: разные размеры
out = Conv(x, 128) + x # (128, 16, 16) + (64, 32, 32) → ошибка!
# ✅ Исправляем через projection:
identity = Conv1x1(x, 128) # Проекция к нужной размерности
out = Conv(x, 128) + identity
Ключевые выводы
-
Сложение, а не конкатенация/умножение, потому что:
- Гарантирует прохождение градиентов (∂y/∂x = 1 + ∂f/∂x)
- Нет роста размерности (эффективно по памяти)
- Простая операция (0 дополнительных параметров)
- Работает в очень глубоких сетях (ResNet-152, Transformers)
-
Формула: y = x + f(x) — лучше, чем y = f(x) или y = [x, f(x)]
-
Используется везде: ResNet, Vision Transformer, BERT, GPT — все используют сложение в skip-connections
-
Теория: операция сложения как источник прямого пути для градиентов, что решает исчезающие градиенты