기울기 소실과 폭발: 깊은 네트워크의 고질적 문제

깊은 신경망 학습을 방해하는 기울기 소실(Vanishing Gradient)과 기울기 폭발(Exploding Gradient)의 수학적 원인을 이해한다. ReLU, 잔차 연결, 배치 정규화, Gradient Clipping 등 현대적 해결책을 코드와 함께 정리한다.

· 7 min read · PALDYN Team

지난 글에서 드롭아웃이 과적합을 막는 앙상블 정규화임을 배웠다. 이번 글에서는 깊은 신경망 학습의 가장 근본적인 난제인 **기울기 소실(Vanishing Gradient)**과 **기울기 폭발(Exploding Gradient)**을 다룬다. 이 두 문제는 1990년대에 처음 발견되었고, 2012년 딥러닝 붐 이전까지 깊은 네트워크 학습을 사실상 불가능하게 만들었다. 오늘날 ResNet, LSTM, Transformer 등 현대 아키텍처들은 모두 이 문제의 해결책을 내포하고 있다.

기울기 소실의 수학적 원인

역전파는 연쇄 법칙으로 기울기를 계산한다. N층 네트워크에서 첫 번째 층의 기울기는:

$$\frac{\partial L}{\partial W_1} = \frac{\partial L}{\partial a_N} \cdot \prod_{k=2}^{N} \frac{\partial a_k}{\partial a_{k-1}} \cdot \frac{\partial a_1}{\partial W_1}$$

각 층의 로컬 기울기 ∂aₖ/∂aₖ₋₁ = W^(k) · f'(z^(k-1))를 곱한다.

Sigmoid를 사용하는 경우:

  • σ’(z) = σ(z)(1−σ(z)) ≤ 0.25
  • N층이면 최대 기울기 ≤ 0.25ᴺ
  • 10층이면 최대 0.25¹⁰ ≈ 10⁻⁶ (사실상 0)
import torch
import torch.nn as nn
import matplotlib.pyplot as plt

# Sigmoid 기울기 소실 실험
def check_gradient_flow(model, x):
    y = model(x)
    loss = y.sum()
    loss.backward()
    grad_norms = []
    for name, param in model.named_parameters():
        if param.grad is not None and 'weight' in name:
            grad_norms.append(param.grad.norm().item())
    return grad_norms

# Sigmoid 20층 네트워크
sigmoid_model = nn.Sequential(
    *[layer for _ in range(20)
      for layer in [nn.Linear(64, 64), nn.Sigmoid()]]
)

x = torch.randn(32, 64)
grads = check_gradient_flow(sigmoid_model, x)
for i, g in enumerate(grads):
    print(f"층 {i+1:2d}: 기울기 norm = {g:.2e}")
# 층 20: 기울기 norm = 2.15e-01
# 층 15: 기울기 norm = 3.47e-03
# 층 10: 기울기 norm = 1.82e-05
# 층  1: 기울기 norm = 4.23e-09  ← 사실상 0!

기울기 폭발

반대로 가중치 행렬의 최대 특이값(spectral norm)이 1보다 크면 기울기가 지수적으로 증가한다.

# 기울기 폭발 시뮬레이션
import torch

# 큰 가중치를 가진 선형 변환 반복
x = torch.tensor([1.0])
W = torch.tensor([[2.0]])  # |W| = 2 > 1

for i in range(20):
    x = W @ x
    print(f"Step {i+1:2d}: x = {x.item():.2e}")
# Step  1: x = 2.00e+00
# Step  5: x = 3.20e+01
# Step 10: x = 1.02e+03
# Step 20: x = 1.05e+06  → 폭발

# RNN에서 시퀀스 길이만큼 반복되므로 특히 심각

기울기 폭발은 NaN이나 Inf가 파라미터에 나타나며 학습이 완전히 망가진다.

기울기 소실과 폭발 문제

해결책 1: ReLU 활성화

ReLU의 미분은 z > 0이면 1, z ≤ 0이면 0이다. 양수 영역에서 기울기가 그대로 통과되어 소실이 일어나지 않는다.

# ReLU vs Sigmoid 기울기 비교
relu_model = nn.Sequential(
    *[layer for _ in range(20)
      for layer in [nn.Linear(64, 64), nn.ReLU()]]
)
nn.init.kaiming_normal_(relu_model[0].weight)

x = torch.randn(32, 64)
grads_relu = check_gradient_flow(relu_model, x)
# 층 20: 기울기 norm = 2.15e-01
# 층  1: 기울기 norm = 1.87e-01  ← 소실 없음!

# Dying ReLU: 모든 입력이 음수면 기울기 = 0
# → Leaky ReLU, ELU, He 초기화로 완화

해결책 2: 잔차 연결 (Residual Connection)

2015년 He et al.이 제안한 ResNet의 핵심 아이디어. 서브레이어의 출력에 입력을 더한다.

class ResidualBlock(nn.Module):
    def __init__(self, dim, dropout=0.1):
        super().__init__()
        self.layers = nn.Sequential(
            nn.Linear(dim, dim),
            nn.LayerNorm(dim),
            nn.ReLU(),
            nn.Dropout(dropout),
            nn.Linear(dim, dim),
        )
        self.norm = nn.LayerNorm(dim)

    def forward(self, x):
        # 잔차 연결: f(x) + x
        # 기울기: ∂L/∂x = ∂L/∂y · (I + ∂f/∂x)
        # I (항등 행렬)로 인해 항상 최소 기울기 1 보장
        return self.norm(self.layers(x) + x)

# 100층도 안정적으로 학습 가능
deep_resnet = nn.Sequential(
    nn.Linear(784, 256),
    *[ResidualBlock(256) for _ in range(50)],
    nn.Linear(256, 10),
)
print(f"파라미터: {sum(p.numel() for p in deep_resnet.parameters()):,}")

잔차 연결이 있으면 기울기가 f(x) + x → x 경로를 통해 직접 초기 층까지 흐를 수 있다. 이것이 ResNet이 100층, 200층도 학습 가능한 이유다.

해결책 3: Gradient Clipping

기울기 폭발은 주로 RNN/LSTM에서 발생한다. Gradient Clipping은 기울기 벡터의 노름이 임계값을 초과하면 스케일다운한다.

import torch.nn as nn

model = nn.LSTM(input_size=128, hidden_size=256,
                num_layers=4, batch_first=True)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)

for batch in dataloader:
    optimizer.zero_grad()
    out, _ = model(batch)
    loss = criterion(out, targets)
    loss.backward()

    # 기울기 노름 계산 및 클리핑
    total_norm = nn.utils.clip_grad_norm_(
        model.parameters(),
        max_norm=1.0  # 임계값
    )

    if total_norm > 10:  # 폭발 감지
        print(f"Warning: grad norm = {total_norm:.2f}")

    optimizer.step()

기울기 모니터링

def log_gradient_norms(model, step, writer=None):
    total_norm = 0.0
    for name, param in model.named_parameters():
        if param.grad is None:
            continue
        norm = param.grad.data.norm(2).item()
        total_norm += norm ** 2
        if writer:
            writer.add_scalar(f"grad/{name}", norm, step)
    total_norm = total_norm ** 0.5

    if total_norm < 1e-6:
        print(f"Step {step}: 기울기 소실 의심 ({total_norm:.2e})")
    elif total_norm > 100:
        print(f"Step {step}: 기울기 폭발 의심 ({total_norm:.2e})")
    return total_norm

기울기 소실 해결책 코드

현대 아키텍처의 종합적 해결책

문제해결책적용된 아키텍처
기울기 소실ReLU/GELU 활성화모든 현대 네트워크
기울기 소실잔차 연결ResNet, Transformer
기울기 소실배치/레이어 정규화ResNet, Transformer
기울기 소실He/Xavier 초기화모든 현대 네트워크
기울기 폭발Gradient ClippingRNN, Transformer 학습
기울기 소실/폭발LSTM/GRU 게이트순환 신경망

기울기 소실·폭발 문제는 완전히 “해결”된 것이 아니라, 적절한 기법들의 조합으로 관리되는 것이다. 현대 딥러닝의 여러 설계 결정들—ReLU, 잔차 연결, 배치 정규화, 신중한 초기화—이 모두 이 문제를 염두에 두고 개발되었다. 이로써 신경망 기초 시리즈가 마무리된다. 다음 단계는 이미지를 위한 합성곱 신경망(CNN)이다.


지난 글: 드롭아웃: 과적합을 막는 앙상블 정규화

다음 글: 합성곱 연산: CNN의 핵심 원리


읽어주셔서 감사합니다. 😊