← Назад к вопросам

Почему в residual connection используется операция сложения?

1.8 Middle🔥 181 комментариев
#Глубокое обучение

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI30 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Почему в 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

Ключевые выводы

  • Сложение, а не конкатенация/умножение, потому что:

    1. Гарантирует прохождение градиентов (∂y/∂x = 1 + ∂f/∂x)
    2. Нет роста размерности (эффективно по памяти)
    3. Простая операция (0 дополнительных параметров)
    4. Работает в очень глубоких сетях (ResNet-152, Transformers)
  • Формула: y = x + f(x) — лучше, чем y = f(x) или y = [x, f(x)]

  • Используется везде: ResNet, Vision Transformer, BERT, GPT — все используют сложение в skip-connections

  • Теория: операция сложения как источник прямого пути для градиентов, что решает исчезающие градиенты

Почему в residual connection используется операция сложения? | PrepBro