Gradient Descent·SGD·Adam: 학습 조건에 맞는 최적화 알고리즘 선택

Gradient Descent, SGD, Adam의 갱신 방식과 학습률·배치·안정화 설정을 비교하고 분산 학습 환경에서의 선택 기준을 정리한다.

2026-08-14 · 최초 발행 2024-04-29

손실 곡면을 어떻게 내려갈지 결정하는 문제

모델 학습에서 최적화 알고리즘은 파라미터를 갱신하는 방식 자체를 정한다. 같은 손실 함수라도 전체 데이터를 한 번에 보는지, 미니배치로 자주 움직이는지, 각 파라미터에 적응형 스텝을 적용하는지에 따라 학습 비용과 수렴 양상이 달라진다.

대규모 데이터, 비정상적인 손실 곡면, 분산 학습 환경에서는 단순히 옵티마이저 이름만 고르기보다 학습률·배치 전략·안정화 설정까지 함께 설계해야 한다.

전체 평균 기울기를 사용하는 Gradient Descent

Gradient Descent는 데이터셋 전체의 평균 그라디언트로 파라미터를 갱신하는 배치 최적화 방식이다.

w ← w − η · ∇L(w)

여기서 η는 학습률이고, ∇L(w)는 전체 배치 손실의 기울기다. 각 스텝의 손실 변화는 비교적 매끄러운 경향이 있지만, 한 번 갱신할 때 전체 데이터를 처리해야 하므로 스텝 비용과 메모리·IO 부담이 커진다.

미니배치로 자주 갱신하는 SGD

SGD(Stochastic Gradient Descent)는 1개 또는 소수의 샘플, 즉 미니배치에서 구한 근사 그라디언트로 파라미터를 업데이트한다.

w ← w − η · ∇L_batch(w)

근사값을 사용하므로 노이즈가 생기지만, 단위 시간당 업데이트 횟수를 늘릴 수 있다. 큰 데이터셋과 잘 맞고 일반화 성능에 유리한 경향이 있다.

모멘트 추정을 이용하는 Adam

Adam은 그라디언트의 1차 모멘트(평균 m)와 2차 모멘트(분산 v)를 추정해 적응형 학습률을 적용한다.

  • m_t ← β1 m_{t−1} + (1−β1) g_t, v_t ← β2 v_{t−1} + (1−β2) g_t^2
  • m̂_t = m_t/(1−β1^t), v̂_t = v_t/(1−β2^t)
  • w ← w − η · m̂_t/(sqrt(v̂_t)+ε)

스케일 불변성과 초기 수렴 속도 측면에서 강점이 있으며, 희소 그라디언트와 불안정한 손실 곡면에도 강하다. 대신 파라미터마다 m, v를 저장하므로 메모리 오버헤드가 늘어난다.

학습률과 배치 설정이 만드는 차이

학습률은 고정, 감소, 코사인, 지수, 스텝 스케줄로 운영할 수 있다. 워밍업은 310% 에폭을 권장한다. 원본 기준 권장값은 GD가 1e−11e−2(정규화 전제), SGD가 1e−2~1e−1, Adam이 1e−3이며 β1=0.9, β2=0.999, ε=1e−8이다.

미니배치 크기는 메모리와 통신 오버헤드를 함께 고려해 32~512 범위에서 잡는다. 대배치 학습에서는 LARS, LAMB, 스케일링 법칙을 적용할 수 있다. 데이터 병렬 방식은 동기식 AllReduce가 일관성 측면에서 유리하고, 비동기 방식은 처리량을 높이는 대신 수렴 일관성이 낮아질 수 있다.

SGD에 모멘텀을 부여하면 진동을 줄이고 수렴을 가속할 수 있으며, nesterov 옵션은 선견 보정에 사용된다. Adam은 좌표별 스텝 크기를 조정하므로 조건수가 큰 문제에 유리하다. 과적합이 발생하면 weight decay를 함께 적용한다.

Weight Decay(L2), Dropout, Label Smoothing도 함께 고려한다. Adam에서는 디커플드 decay를 적용하는 AdamW를 권장한다. 그래디언트 클리핑(norm/value)과 혼합정밀도 환경의 loss scaling은 NaN 예방에 사용한다.

수렴 여부는 Early Stopping, 검증 손실 플래토, 그라디언트 노름으로 관찰한다. 재현성이 필요한 경우 시드, Deterministic 연산 옵션, 데이터 셔플을 고정해야 한다.

배치 생성부터 체크포인트까지의 루프

미수렴수렴입력: 데이터셋, 모델,손실함수, 하이퍼파라미터배치 생성/셔플Forward: 예측 계산Loss 계산: L(y, ŷ)Backward: ∇L 계산옵티마 선택GD: 전체 평균 ∇L로 업데이트SGD: 미니배치 ∇L로 업데이트Adam: m,v 추정 바이어스보정안정화: 클리핑, NaN/Inf 체크,AMP loss scaling스케줄러/워밍업 적용수렴 검사출력: 체크포인트, 메트릭,아티팩트

NaN 또는 Inf가 발생하면 학습률을 낮추고, Adam의 ε를 늘리거나 클리핑을 강화한 뒤 배치를 재시도한다. 손실이 급격히 증가해 발산으로 판단되면 롤백하고 학습률을 0.1배로 낮춘 뒤 스케줄을 강제로 리셋한다.

옵티마이저별 운영 특성

항목 Gradient Descent SGD Adam
성능(초기 수렴 속도) 낮음, 스텝당 비용 큼 중간, 업데이트 빈도 높음 높음, 적응형 스텝
확장성(대규모 데이터) 낮음, 전체 배치 필요 높음, 미니배치·분산 용이 높음, 분산 가능(통신 오버헤드 주의)
일관성(재현성/분산 편차) 높음 중간, 배치/순서 민감 중간, 시드·동기화 필요
안정성(발산/진동) 높음, 매끄러운 경향 중간, 노이즈로 진동 높음, 스케일 불변·희소 강건
운영 편의(튜닝 난이도) 중간, η 민감 중간, 모멘텀 필요 높음, 기본 설정 강건(메모리↑)

GD는 안정성과 처리량을 맞바꾸고, SGD는 일반화·확장성과 변동성을 함께 가져간다. Adam은 수렴 속도와 안정성에서 장점이 있지만 메모리 비용과 일반화에 관한 논쟁을 고려해야 한다.

데이터와 학습 방식에 따른 선택

비전·NLP 사전학습처럼 대규모 비정형 데이터를 다룰 때는 Adam 또는 AdamW를 기본 선택으로 두고, 대배치·워밍업·코사인 스케줄을 조합할 수 있다. 분산 학습에서는 동기식 AllReduce와 grad clipping 1.0, EMA(지수이동평균) 체크포인트를 병행한다.

온라인·스트리밍 환경이나 비정상 데이터에서는 SGD 또는 Adam을 사용한다. Adam의 β2를 낮추는 방식(예: 0.98)으로 변화 민감도를 확보하고, 주기적 리셋이나 적응형 스케줄러로 개념 표류에 대응한다.

특성 스케일링이 전제된 탭울러·전통 ML·경량 모델은 GD 또는 SGD와 모멘텀만으로도 충분할 수 있다. 메모리가 제한된 환경에서는 SGD를 권장하며, 실무 배치 크기는 64~256이 적정하다.

희소 피처가 많은 추천·랭킹 문제에서는 좌표별 스텝 조정이 가능한 Adam 또는 Adagrad가 유리하다.

수렴 속도와 안정화에서 기대할 수 있는 변화

Adam은 동일 에폭 대비 초기 손실 감소율이 13배 빠를 수 있다. SGD에 미니배치를 도입하면 데이터·IO 병목 완화가 전제될 때 스텝당 벽시간을 3070% 단축할 수 있다. 그래디언트 클리핑과 AMP를 함께 적용하면 NaN·발산 이벤트가 50% 이상 감소한 사례도 있다.

정량 지표 외에도 튜닝 안정화, 운영 재현성 향상, 분산 확장의 용이성, 노이즈 환경과 비정상 손실에서의 강건 학습을 기대할 수 있다.

장기 학습에서의 설정과 트레이드오프

초기 설정으로는 Adam(lr=1e−3, β1=0.9, β2=0.999, ε=1e−8)에 코사인 스케줄과 5% 워밍업을 조합한다. 장기 학습이나 일반화가 중요한 경우 Adam으로 예열한 뒤 모멘텀 0.9의 SGD로 전환하고, 최종 에폭에서는 fine-tune을 수행할 수 있다.

정규화는 AdamW(weight_decay=1e−4)를 사용하고 배치 정규화와의 충돌을 최소화하는 방향으로 잡는다. grad clipping(norm=1.0), AMP와 loss scaling, anomaly detection도 안정화 수단이다. 시드 고정, deterministic 연산, 데이터 셔플과 배치 생성기 시드 동기화는 재현성 확보에 필요하다.

Adam의 파라미터당 m, v 저장은 메모리 오버헤드와 수렴 안정성의 교환 관계다. 대배치 처리량 향상은 일반화 성능 저하 가능성과 맞물리며 LARS, LAMB, Regularization이 필요할 수 있다. 학습률을 급격히 낮추는 강한 스케줄링은 지역 최적점에서 벗어나는 능력을 떨어뜨릴 수 있다.

PyTorch 2.x 예제

전제조건은 Python 3.10+, PyTorch 2.1+, CUDA 선택적이며, 재현성을 위해 시드 고정과 deterministic 옵션을 선택한다.

# pip install torch torchvision
import os, random, math
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import DataLoader, TensorDataset

def set_seed(seed=42, deterministic=True):
    random.seed(seed)
    torch.manual_seed(seed)
    torch.cuda.manual_seed_all(seed)
    if deterministic:
        torch.backends.cudnn.deterministic = True
        torch.backends.cudnn.benchmark = False

class MLP(nn.Module):
    def __init__(self, in_dim=20, hidden=64, out_dim=1):
        super().__init__()
        self.net = nn.Sequential(
            nn.Linear(in_dim, hidden),
            nn.ReLU(),
            nn.Linear(hidden, out_dim)
        )
    def forward(self, x): return self.net(x)

def make_synthetic(n=10000, d=20, noise=0.1):
    X = torch.randn(n, d)
    w_true = torch.randn(d, 1)
    y = X @ w_true + noise * torch.randn(n, 1)
    return X, y

def get_optimizer(name, params, lr, weight_decay=0.0):
    name = name.lower()
    if name == "gd":
        # GD를 미니배치=전체로 시뮬레이션: DataLoader에서 batch_size=len(dataset) 설정
        return torch.optim.SGD(params, lr=lr, momentum=0.0, weight_decay=weight_decay)
    if name == "sgd":
        return torch.optim.SGD(params, lr=lr, momentum=0.9, weight_decay=weight_decay, nesterov=True)
    if name == "adam":
        return torch.optim.Adam(params, lr=lr, betas=(0.9, 0.999), eps=1e-8, weight_decay=weight_decay)
    raise ValueError("Unsupported optimizer")

def train(model, loader, opt, epochs=5, scheduler=None, device="cpu",
          clip_norm=1.0, amp=False):
    scaler = torch.cuda.amp.GradScaler(enabled=amp)
    model.to(device)
    for ep in range(1, epochs+1):
        model.train()
        total_loss = 0.0
        for xb, yb in loader:
            xb, yb = xb.to(device), yb.to(device)
            opt.zero_grad(set_to_none=True)
            with torch.cuda.amp.autocast(enabled=amp):
                pred = model(xb)
                loss = F.mse_loss(pred, yb)
            if torch.isnan(loss) or torch.isinf(loss):
                # NaN/Inf 방어: lr 감소 및 스킵
                for g in opt.param_groups:
                    g['lr'] *= 0.5
                continue
            scaler.scale(loss).backward()
            # 그래디언트 클리핑
            scaler.unscale_(opt)
            if clip_norm is not None:
                torch.nn.utils.clip_grad_norm_(model.parameters(), clip_norm)
            scaler.step(opt)
            scaler.update()
            total_loss += loss.item() * xb.size(0)
        if scheduler is not None:
            scheduler.step()
        avg = total_loss / len(loader.dataset)
        print(f"epoch={ep} loss={avg:.6f} lr={opt.param_groups[0]['lr']:.5g}")
    return model

if __name__ == "__main__":
    set_seed(42)
    X, y = make_synthetic(n=20000, d=20, noise=0.2)
    # 하단에서 GD 시뮬레이션을 위해 batch_size를 len(dataset)로 설정 가능
    ds = TensorDataset(X, y)
    batch_size = 256
    loader = DataLoader(ds, batch_size=batch_size, shuffle=True, drop_last=False, num_workers=0)

    model = MLP(in_dim=20, hidden=64, out_dim=1)
    optimizer_name = os.getenv("OPT", "adam")  # "gd" | "sgd" | "adam"
    lr = {"gd": 0.05, "sgd": 0.05, "adam": 1e-3}[optimizer_name.lower()]
    wd = {"gd": 1e-4, "sgd": 1e-4, "adam": 1e-4}[optimizer_name.lower()]

    opt = get_optimizer(optimizer_name, model.parameters(), lr=lr, weight_decay=wd)
    # 코사인 스케줄러 + 워밍업(간단 구현)
    T = 20
    scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(opt, T_max=T)
    device = "cuda" if torch.cuda.is_available() else "cpu"

    train(model, loader, opt, epochs=T, scheduler=scheduler, device=device, clip_norm=1.0, amp=(device=="cuda"))

GD는 batch_size=len(dataset)OPT=gd 환경변수로 시험한다. SGD는 momentum=0.9, nesterov=True를 기본으로 두고 lr 0.01~0.1 범위를 탐색한다. Adam은 lr=1e−3에서 시작해 학습이 정체되면 1/3로 낮추고 AdamW 전환을 검토한다.

선택 전 확인할 운영 조건

데이터와 모델의 조건수, 스파시티, 가능한 배치 크기를 먼저 진단한다. 초기 수렴 속도와 최종 일반화 중 무엇을 우선할지 정하고, 단일 GPU·멀티노드 여부, 재현성 요구 수준, 메모리 한계를 함께 확인한다.

초기 선택은 AdamW와 코사인·워밍업 조합으로 시작한 뒤, 안정적으로 수렴하면 SGD와 모멘텀으로 전환하는 방식을 검토할 수 있다.

최적화 알고리즘Gradient DescentSGDAdam딥러닝