본문 바로가기
Artificial Intelligence/60. Python

[PYTHON] 커스텀 옵티마이저 구현 시 Weight Decay와 L2 Regularization 2가지 차이 반영 방법

by Papa Martino V 2026. 4. 15.
728x90

Weight Decay vs L2 Regularization
Weight Decay vs L2 Regularization

딥러닝 모델의 일반화 성능을 높이기 위해 사용하는 Weight DecayL2 Regularization은 실무에서 혼용되곤 하지만, 수식적으로는 엄밀히 다른 개념입니다. 특히 AdamW와 같은 최신 옵티마이저를 직접 구현하거나 커스텀할 때 이 차이를 무시하면 하이퍼파라미터 최적화에 실패할 가능성이 큽니다. 본 가이드에서는 이 두 기법의 수식적 결합 방식의 차이를 분석하고, 파이썬(PyTorch)을 활용해 7가지 실전 옵티마이저 구현 예제를 상세히 다룹니다.


1. Weight Decay vs L2 Regularization: 수식적 차이점 분석

기존 SGD에서는 두 개념이 수학적으로 동일한 업데이트 결과를 낳지만, 모멘텀(Momentum)이나 적응형 학습률(Adaptive Learning Rate)을 사용하는 옵티마이저에서는 그 결과가 완전히 달라집니다.

핵심 메커니즘 비교 요약

항목 L2 Regularization (Decoupled X) Weight Decay (Decoupled O)
적용 시점 손실 함수(Loss) 단계에서 페널티 추가 가중치 업데이트(Update) 직전 직접 감쇄
수식적 위치 Gradient에 포함됨 ($\nabla L + \lambda w$) 가중치 차감 ($w = w - \eta (\dots) - \lambda w$)
Adam과의 호환성 학습률 조정에 의해 효과가 희석됨 효과가 독립적으로 유지됨 (AdamW 방식)
주요 장점 전통적 통계 기반 규제화에 적합 하이퍼파라미터 튜닝이 훨씬 용이함

가장 중요한 차이Decoupling(분리)에 있습니다. L2 Regularization은 Gradient에 $\lambda w$를 더해주기 때문에, Adam처럼 Gradient의 이동 평균을 사용하는 알고리즘에서는 규제 강도가 의도치 않게 변형됩니다. 반면 Weight Decay는 가중치 업데이트 시점에서 직접 수행되므로 규제의 의도가 명확히 반영됩니다.


2. 실무 옵티마이저 구현을 위한 7가지 실전 Example

PyTorch의 torch.optim.Optimizer 클래스를 상속받아 커스텀 옵티마이저를 구현할 때 이 차이를 어떻게 반영하는지 단계별로 살펴봅니다.

Example 1: 순수 L2 Regularization 기반 SGD 구현

import torch
from torch.optim import Optimizer

class SimpleL2SGD(Optimizer):
    def __init__(self, params, lr=1e-3, l2_reg=1e-2):
        defaults = dict(lr=lr, l2_reg=l2_reg)
        super().__init__(params, defaults)

    def step(self, closure=None):
        for group in self.param_groups:
            for p in group['params']:
                if p.grad is None: continue
                # L2 Regularization: 그레이디언트에 페널티를 먼저 더함
                d_p = p.grad.data + group['l2_reg'] * p.data
                p.data.add_(d_p, alpha=-group['lr'])
        

Example 2: Decoupled Weight Decay 기반 SGD 구현

class WeightDecaySGD(Optimizer):
    def __init__(self, params, lr=1e-3, weight_decay=1e-2):
        defaults = dict(lr=lr, weight_decay=weight_decay)
        super().__init__(params, defaults)

    def step(self):
        for group in self.param_groups:
            for p in group['params']:
                if p.grad is None: continue
                # Step 1: 가중치 감쇄를 업데이트와 별개로 먼저 수행
                p.data.mul_(1 - group['lr'] * group['weight_decay'])
                # Step 2: 그레이디언트 적용
                p.data.add_(p.grad.data, alpha=-group['lr'])
        

Example 3: AdamW 스타일의 분리형 Weight Decay 반영법

class CustomAdamW(Optimizer):
    def step(self):
        for group in self.param_groups:
            for p in group['params']:
                if p.grad is None: continue
                # AdamW의 핵심: Update 계산 전 Weight Decay 적용
                p.data.mul_(1 - group['lr'] * group['weight_decay'])
                
                # 이후 Adam의 m, v 연산 진행 (생략)
                # p.data.addcdiv_(exp_avg, denom, value=-step_size)
        

Example 4: 특정 레이어만 Weight Decay 제외하는 필터링 기법

# 실무 해결 방법: BatchNorm이나 Bias는 규제에서 제외하는 것이 일반적
optimizer_grouped_parameters = [
    {'params': [p for n, p in model.named_parameters() if not any(nd in n for nd in ['bias', 'LayerNorm'])], 'weight_decay': 0.01},
    {'params': [p for n, p in model.named_parameters() if any(nd in n for nd in ['bias', 'LayerNorm'])], 'weight_decay': 0.0}
]
optimizer = torch.optim.AdamW(optimizer_grouped_parameters, lr=1e-5)
        

Example 5: 수동으로 L2 Regularization을 Loss에 추가하는 방법

def train_step(model, inputs, targets, optimizer, l2_lambda):
    optimizer.zero_grad()
    outputs = model(inputs)
    loss = criterion(outputs, targets)
    
    # 수동 L2 Regularization 반영
    l2_reg = sum(p.pow(2.0).sum() for p in model.parameters())
    loss = loss + l2_lambda * l2_reg
    
    loss.backward()
    optimizer.step()
        

Example 6: AdaFactor 스타일의 상대적 Weight Decay 구현

# 파라미터의 노름(Norm)에 비례하여 감쇄율을 조정하는 방식
param_norm = p.data.norm(2)
rel_decay = group['weight_decay'] * max(param_norm, 1e-3)
p.data.add_(p.data, alpha=-group['lr'] * rel_decay)
        

Example 7: 정규화 강도를 학습 중에 변경하는 Scheduler 연동

# 학습이 진행됨에 따라 규제를 강화하는 전략
for epoch in range(100):
    current_wd = initial_wd * (epoch / 100)
    for group in optimizer.param_groups:
        group['weight_decay'] = current_wd
    train(model, optimizer)
        

3. 성능 최적화를 위한 실무적 해결 전략

커스텀 옵티마이저를 구현할 때 가장 많이 범하는 실수는 Learning Rate와 Weight Decay의 결합을 간과하는 것입니다. 만약 lr이 스케줄러에 의해 작아진다면, L2 Regularization 기반 옵티마이저는 규제 강도도 함께 약해지는 부작용이 발생합니다. 이를 해결하기 위해 Decoupled Weight Decay를 기본값으로 채택하는 것이 2026년 현재 딥러닝 아키텍처의 트렌드입니다.

또한, 복잡한 수식 연산 시 GPU 메모리 오버헤드를 줄이기 위해 in-place 연산(예: add_, mul_)을 적극적으로 활용하여 성능 병목 현상을 해결해야 합니다.


4. 결론

Weight Decay와 L2 Regularization은 겉으로 비슷해 보이지만, 옵티마이저의 내부 동작 방식에 따라 모델의 최종 수렴 지점을 완전히 바꿀 수 있습니다. 특히 대규모 모델 학습 시에는 AdamW 스타일의 Weight Decay 분리 기법을 적용하여 하이퍼파라미터 간의 상호 의존성을 낮추는 것이 가장 효율적인 방법입니다.


내용 출처 및 참고 문헌:

  • Loshchilov, I., & Hutter, F. (2019). "Decoupled Weight Decay Regularization". ICLR.
  • PyTorch Official Documentation: "torch.optim.Optimizer"
  • Fast.ai Blog: "AdamW and Super-convergence"
  • Kingma, D. P., & Ba, J. (2015). "Adam: A Method for Stochastic Optimization". ICLR.
728x90