Gradient clipping: 왜 손실(loss)이 NaN으로 변하는지, 그리고 스칼라 곱 하나가 어떻게 학습을 망치지 않고 구원하는가
요약
심층 신경망 학습 중 발생하는 NaN 손실 문제의 원인인 그래디언트 폭주(exploding gradient) 현상을 분석합니다. 그래디언트 클리핑의 두 가지 방식인 값 기반 클리핑과 노름 기반 클리핑의 차이점과 작동 원리를 설명합니다.
핵심 포인트
- 그래디언트 폭주는 연쇄 법칙에 의한 반복적인 곱셈으로 인해 발생함
- 폭주한 그래디언트는 가중치를 무작위 지점으로 튕겨나가게 하여 NaN을 유발함
- Clip by value는 성분별로 제한하여 업데이트 방향을 왜곡할 수 있음
- Clip by norm은 벡터의 방향을 유지하며 크기만 조절하여 최급강하를 보존함
심층 신경망(deep net)이나 순환 신경망(recurrent net)을 학습시키고 있는데, 모든 것이 정상적으로 보이는 듯하다가 갑자기 손실(loss) 값이 nan으로 출력되며 회복되지 않는 상황을 겪고 계신가요? 학습률(learning rate)을 낮추면 이를 지연시킬 수는 있지만 근본적인 해결책은 되지 못합니다. 이는 거의 항상 폭주하는 그래디언트(exploding gradient) 문제이며, 단 한 줄의 코드로 해결할 수 있습니다. 저는 의도적으로 폭주하도록 설정된 환경에서 실제 4개 유닛의 선형 RNN(linear RNN)이 진정한 시간 기반 역전파(back-prop-through-time, BPTT)를 수행하는 데모를 제작했습니다. 그래디언트 노름(gradient norm) 클리핑(clipping)을 껐을 때는 손실이 몇 단계 만에 NaN으로 치솟았지만, 노름 기준 클리핑(clip-by-norm)을 켰을 때는 동일한 네트워크가 유한한 값을 유지하며 수렴했습니다. 모든 노름(norm), 클리핑(clip), 그리고 방향 각도(direction angle)는 실시간으로 계산됩니다. 지금부터 어떤 일이 일어나고 있는지 살펴보겠습니다.
그래디언트가 폭주하는 이유: 반복적인 곱셈
역전파(Back-prop)는 연쇄 법칙(chain rule)을 통해 그래디언트를 계산하는데, 이는 모든 레이어의 국소 야코비안(local Jacobian) — 또는 RNN의 경우 모든 타임스텝(time-step) — 을 곱합니다. 많은 숫자를 곱하는 것은 통제 불능의 과정입니다. 만약 인자(factors)들의 평균이 1보다 크다면, 그 곱은 기하급수적으로 성장합니다. T 스텝 동안 펼쳐진(unrolled) 네트워크의 경우, 그래디언트는 Wᵀ와 같은 인자를 포함하게 됩니다. 만약 W의 스펙트럼 반경(spectral radius) ρ(가장 큰 고윳값)가 1을 초과하면, 해당 인자는 ρᵀ와 같이 성장합니다. ρ=1.3이고 T=15라면 이미 약 50배에 달합니다.
기하학적으로 이는 손실 표면(loss surface)에서 가파른 **절벽(cliff)**과 같습니다. 벽 앞의 평탄한 고원(plateau)에서는 그래디언트가 완만하지만, 벽에 도달하면 엄청나게 커집니다. 경사 하강법(gradient descent)은 이 거대한 그래디언트에 학습률(learning rate)을 곱하여 비례적으로 거대한 스텝(step)을 밟습니다. 따라서 벽을 따라 조심스럽게 내려가는 대신, 가중치(weights)가 무작위의 끔찍한 지점으로 튕겨 나가게 됩니다. 그러면 다음 순전파(forward pass)에서 inf로 오버플로(overflow)가 발생하고, inf − inf = NaN이 되어 모든 파라미터(parameter)로 퍼져나가게 됩니다.
해결책: 크기는 제한하고, 방향은 유지하라
클리핑(Clipping)은 그래디언트의 *방향(direction)*은 여전히 유용하지만, *크기(magnitude)*가 병적인 상태라는 점을 인정합니다. 이는 backward()가 그래디언트를 채운 후와 opt.step()이 이를 사용하기 전에 실행됩니다. 두 가지 방식이 있습니다.
**값에 의한 클리핑 (Clip by value)**은 모든 요소를 [−c, c] 범위로 제한합니다:
torch.nn.utils.clip_grad_value_(params, clip_value=1.0)
# g_i <- max(-c, min(c, g_i)) 모든 요소에 대하여
결코 NaN이 발생하지 않지만, 각 성분(component)을 서로 다른 양만큼 클램핑(clamping)하면 그 비율이 변하게 되어, 업데이트가 더 이상 진정한 최급강하(steepest descent) 방향을 가리키지 않게 됩니다. 투박하지만 효과적인 안전벨트와 같습니다.
**노름에 의한 클리핑 (Clip by norm)**은 벡터 전체의 길이를 고려합니다. 만약 ‖g‖가 임계값 $\tau$보다 작으면 그대로 두고, 만약 $\tau$를 초과하면 벡터 _전체_를 $\tau/‖g‖$로 재조정(rescale)합니다:
torch.nn.utils.clip_grad_norm_(params, max_norm=1.0)
# if ‖g‖ > τ: g <- g * (τ / ‖g‖) # 방향은 동일, 길이 = τ
# else: g unchanged
노름 클리핑이 보폭을 비틀지 않는 이유
이것이 핵심입니다. 벡터에 양의 스칼라(scalar) $\alpha = \tau/‖g‖$를 곱하는 것은 벡터가 가리키는 방향을 바꿀 수 없으며, 오직 길이만 바꿉니다. 클리핑된 그래디언트(gradient)와 원래 그래디언트 사이의 각도는 정확히 **0°**이므로, 노름 클리핑은 보폭(stride)만 제한될 뿐 여전히 진정한 최급강하 방향으로 이동합니다. 반면, 각 성분을 독립적으로 클램핑하는 값에 의한 클리핑(Clip-by-value)은 일반적으로 업데이트 방향을 0이 아닌 실제 각도만큼 회전시킵니다. 데모에서 이를 직접 확인할 수 있습니다. 방향 변화(direction-change) 판독값을 보면 노름은 0.00°를 나타내는 반면, 값에 의한 클리핑은 수십 도를 나타냅니다. 이것이 노름 클리핑이 RNN과 Transformer에서 기본값으로 사용되는 이유입니다.
라이브러리가 처리하는 또 다른 미묘한 점은 다음과 같습니다: clip_grad_norm_은 각 텐서(tensor)를 개별적으로 클리핑하지 않습니다. 대신 모든 파라미터의 그래디언트를 하나의 거대한 벡터로 연결(concatenate)한 뒤, 그 단일한 전역(global) 노름을 계산하여 하나의 공유된 계수로 모든 것을 재조정함으로써 전체 업데이트의 방향을 보존합니다. 편리하게도 클리핑 전의 전체 노름을 _반환(return)_하므로, 로그(log)를 남기기에 매우 적합합니다.
$\tau$ 선택하기 — 추측하지 말고 측정하라
$\tau$는 실제 하이퍼파라미터(hyperparameter)입니다. 안정적인 실행 상태에서 수백 스텝 동안 전체 그래디언트 노름(grad-norm)을 기록하고, 분포를 살펴본 뒤, 드물게 발생하는 스파이크(spike)에만 반응하도록 일반적인 값보다 약간 높게 $\tau$를 설정하세요:
tau = np.quantile(norms, 0.90) # 상위 약 10%를 클리핑
너무 낮게 설정하면 모든 정직한 업데이트를 억제하게 되어 (느리고 편향된 학습), 너무 높게 설정하면 절벽(cliffs)을 그대로 통과해 버립니다. 권장되는 기본값은 다음과 같습니다: Transformer/LLM의 경우 ~1.0, RNN/LSTM의 경우 5–10입니다. (혼합 정밀도 (mixed precision)를 사용할 때는 클리핑하기 전에 반드시 unscale_을 먼저 수행해야 합니다. 그렇지 않으면 잘못된 크기(magnitude)를 클리핑하게 됩니다.)
그리고 관점을 유지하세요: 클리핑은 _증상_을 다루는 것입니다. 기울기 폭주 (exploding gradients)에 대한 진짜 치료법은 좋은 초기화 (initialisation), 정규화 (normalisation) (Batch/Layer/RMSNorm), 잔차 연결 (residual connections), 그리고 게이트 유닛 (gated units) (LSTM/GRU)입니다. 이러한 방법들을 사용하여 스파이크 (spikes)를 줄이되, 클리핑은 여전히 발생하는 예외 상황들을 위한 저렴하고 항상 작동하는 안전벨트로 유지하세요.
실제 RNN을 학습시키고, $\tau$를 조절하며, NaN이 발생했다가 다시 회복되는 과정을 확인해 보세요:
https://dev48v.infy.uk/dl/day44-gradient-clipping.html
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기