들어가며
역전파로 각 가중치의 기울기를 구했다. 이제 그 기울기를 이용해서 실제로 가중치를 어떻게 업데이트할지가 문제다. 단순히 기울기 반대 방향으로 이동하는 기본 경사하강법부터, 실무에서 주로 쓰이는 Adam까지 각 방법의 아이디어와 차이점을 정리해둔다.
경사하강법 (Gradient Descent)
손실 함수의 최솟값을 향해 기울기 반대 방향으로 조금씩 이동하는 최적화 방법이다.
w = w - lr * ∂L/∂w
w : 가중치
lr : 학습률 (learning rate)
∂L/∂w : 손실에 대한 가중치의 기울기
import numpy as np
# 간단한 예시: f(x) = x²의 최솟값 찾기
def f(x): return x ** 2
def df(x): return 2 * x
x = 10.0
lr = 0.1
print(f"초기값: x={x:.4f}, f(x)={f(x):.4f}")
for step in range(30):
grad = df(x)
x = x - lr * grad
if (step + 1) % 5 == 0:
print(f"step {step+1:2d}: x={x:.6f}, f(x)={f(x):.8f}")
# step 5: f(x)=10.73...
# step 10: f(x)=1.152...
# step 20: f(x)=0.013...
# step 30: f(x)=0.000... → 0에 수렴
학습률의 중요성
# 학습률이 너무 크면 발산
x = 10.0
lr_too_big = 1.1
for step in range(5):
x = x - lr_too_big * df(x)
print(f"step {step+1}: x={x:.4f}") # 점점 커지며 발산
# 학습률이 너무 작으면 수렴이 느림
x = 10.0
lr_too_small = 0.001
for step in range(30):
x = x - lr_too_small * df(x)
print(f"30step 후: x={x:.4f}") # 아직 최솟값에 멀리 있음
학습률 설정 기준:
너무 크면 : 손실이 들쭉날쭉하거나 발산 (NaN)
너무 작으면: 학습이 너무 느림
적당하면 : 안정적으로 손실이 줄어듦
일반적인 시작값: 0.001 ~ 0.01
Adam 사용 시 : 0.001이 좋은 시작점
배치 경사하강법 종류
데이터를 얼마나 사용해서 기울기를 계산하느냐에 따라 세 가지로 나뉜다.
1. Batch Gradient Descent (BGD)
전체 데이터로 기울기를 계산한다.
def batch_gd(X, y, W, b, lr, epochs):
n = len(X)
for epoch in range(epochs):
y_pred = X @ W + b
loss = np.mean((y_pred - y) ** 2)
# 전체 데이터로 기울기 계산
dW = (2/n) * X.T @ (y_pred - y)
db = (2/n) * np.sum(y_pred - y)
W -= lr * dW
b -= lr * db
# 장점: 안정적인 수렴, 정확한 기울기
# 단점: 데이터가 크면 너무 느림, 메모리 부족
2. Stochastic Gradient Descent (SGD)
데이터 1개씩 기울기를 계산한다.
def sgd(X, y, W, b, lr, epochs):
n = len(X)
for epoch in range(epochs):
indices = np.random.permutation(n)
for i in indices:
xi = X[i:i+1]
yi = y[i:i+1]
y_pred = xi @ W + b
# 1개 샘플로 기울기 계산
dW = 2 * xi.T @ (y_pred - yi)
db = 2 * np.sum(y_pred - yi)
W -= lr * dW
b -= lr * db
# 장점: 빠른 업데이트, 지역 최솟값 탈출 가능
# 단점: 불안정한 수렴, 노이즈가 많음
3. Mini-Batch Gradient Descent
실무에서 거의 항상 쓰이는 방식이다. 전체 데이터를 작은 배치로 나눠서 계산한다.
def mini_batch_gd(X, y, W, b, lr, epochs, batch_size=32):
n = len(X)
for epoch in range(epochs):
indices = np.random.permutation(n)
X_shuffled = X[indices]
y_shuffled = y[indices]
for start in range(0, n, batch_size):
X_batch = X_shuffled[start:start+batch_size]
y_batch = y_shuffled[start:start+batch_size]
batch_n = len(X_batch)
y_pred = X_batch @ W + b
dW = (2/batch_n) * X_batch.T @ (y_pred - y_batch)
db = (2/batch_n) * np.sum(y_pred - y_batch)
W -= lr * dW
b -= lr * db
# 장점: BGD와 SGD의 절충안, GPU 병렬 연산 효율적 활용
# 배치 크기: 32, 64, 128, 256이 일반적
# 클수록 안정적, 작을수록 빠르고 일반화 잘 됨
고급 최적화 알고리즘
Momentum
언덕을 굴러 내려오는 공처럼 이전 방향의 관성을 유지한다.
class SGDMomentum:
def __init__(self, lr=0.01, momentum=0.9):
self.lr = lr
self.momentum = momentum
self.velocity = {}
def update(self, params, grads):
for key in params:
if key not in self.velocity:
self.velocity[key] = np.zeros_like(params[key])
# 이전 속도를 유지하면서 새 기울기 추가
self.velocity[key] = (self.momentum * self.velocity[key]
- self.lr * grads[key])
params[key] += self.velocity[key]
return params
# momentum = 0.9가 일반적인 값
# 기울기가 일정 방향이면 속도가 점점 빨라짐
AdaGrad
각 파라미터마다 학습률을 다르게 조정한다. 많이 업데이트된 파라미터는 학습률을 줄인다.
class AdaGrad:
def __init__(self, lr=0.01, epsilon=1e-8):
self.lr = lr
self.epsilon = epsilon
self.h = {}
def update(self, params, grads):
for key in params:
if key not in self.h:
self.h[key] = np.zeros_like(params[key])
self.h[key] += grads[key] ** 2
params[key] -= self.lr * grads[key] / (np.sqrt(self.h[key]) + self.epsilon)
return params
# 자주 등장하는 특성 → 학습률 감소
# 드물게 등장하는 특성 → 학습률 유지
# 단점: 학습이 진행될수록 학습률이 너무 작아져 학습 중단
RMSProp
AdaGrad의 학습률이 0으로 수렴하는 문제를 해결한다.
class RMSProp:
def __init__(self, lr=0.001, rho=0.9, epsilon=1e-8):
self.lr = lr
self.rho = rho
self.epsilon = epsilon
self.h = {}
def update(self, params, grads):
for key in params:
if key not in self.h:
self.h[key] = np.zeros_like(params[key])
# 지수 이동 평균으로 기울기 제곱 추적
self.h[key] = self.rho * self.h[key] + (1 - self.rho) * grads[key] ** 2
params[key] -= self.lr * grads[key] / (np.sqrt(self.h[key]) + self.epsilon)
return params
Adam (Adaptive Moment Estimation)
현재 가장 널리 사용되는 최적화 알고리즘이다. Momentum + RMSProp의 장점을 결합했다.
class Adam:
def __init__(self, lr=0.001, beta1=0.9, beta2=0.999, epsilon=1e-8):
self.lr = lr
self.beta1 = beta1 # 1차 모멘트 감쇠율
self.beta2 = beta2 # 2차 모멘트 감쇠율
self.epsilon = epsilon
self.m = {} # 1차 모멘트 (평균)
self.v = {} # 2차 모멘트 (분산)
self.t = 0
def update(self, params, grads):
self.t += 1
for key in params:
if key not in self.m:
self.m[key] = np.zeros_like(params[key])
self.v[key] = np.zeros_like(params[key])
# 1차 모멘트: 기울기의 지수 이동 평균
self.m[key] = self.beta1 * self.m[key] + (1 - self.beta1) * grads[key]
# 2차 모멘트: 기울기 제곱의 지수 이동 평균
self.v[key] = self.beta2 * self.v[key] + (1 - self.beta2) * grads[key] ** 2
# 편향 보정 (초반 스텝에서 0에 편향되는 문제 수정)
m_hat = self.m[key] / (1 - self.beta1 ** self.t)
v_hat = self.v[key] / (1 - self.beta2 ** self.t)
params[key] -= self.lr * m_hat / (np.sqrt(v_hat) + self.epsilon)
return params
# 기본값: lr=0.001, beta1=0.9, beta2=0.999
# 대부분의 경우 좋은 시작점
PyTorch에서 옵티마이저 사용
import torch
import torch.nn as nn
model = nn.Sequential(
nn.Linear(10, 64),
nn.ReLU(),
nn.Linear(64, 32),
nn.ReLU(),
nn.Linear(32, 1)
)
# 다양한 옵티마이저
optimizer_sgd = torch.optim.SGD(model.parameters(), lr=0.01)
optimizer_sgd_m = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
optimizer_adam = torch.optim.Adam(model.parameters(), lr=0.001)
optimizer_adamw = torch.optim.AdamW(model.parameters(), lr=0.001, weight_decay=0.01)
# AdamW: Adam + L2 정규화 → Transformer 계열에서 주로 사용
학습률 스케줄러
학습이 진행됨에 따라 학습률을 조정한다.
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
# 1. StepLR: 일정 에폭마다 학습률을 gamma배 감소
scheduler = torch.optim.lr_scheduler.StepLR(
optimizer, step_size=30, gamma=0.1
) # 30 에폭마다 lr = lr * 0.1
# 2. CosineAnnealingLR: 코사인 함수로 학습률 감소
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(
optimizer, T_max=100
)
# 3. ReduceLROnPlateau: 성능 개선 없으면 학습률 감소
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
optimizer, mode='min', factor=0.1, patience=10
)
# 학습 루프에서 사용
for epoch in range(100):
train_loss = train_one_epoch(model, optimizer)
val_loss = validate(model)
scheduler.step(val_loss)
current_lr = optimizer.param_groups[0]['lr']
print(f"Epoch {epoch}: lr={current_lr:.6f}, loss={train_loss:.4f}")
실무에서 옵티마이저 선택
기본 선택:
일반적인 경우 → Adam (lr=0.001)
Transformer 계열 → AdamW (lr=1e-4 ~ 5e-4, weight_decay=0.01)
컴퓨터 비전 → SGD + Momentum (lr=0.01, momentum=0.9)
Fine-tuning → Adam 또는 AdamW (lr: 1e-5 ~ 1e-4)
학습률 선택 팁:
- Adam: 0.001 (거의 항상 좋은 시작점)
- SGD: 0.01 ~ 0.1
- 너무 크면 loss가 NaN → lr을 10배 줄임
- 너무 작으면 loss가 안 줄어듦 → lr을 10배 키움
정리
| 옵티마이저 | 특징 | 사용처 |
|---|---|---|
| SGD | 단순, 느림 | 기본 학습 이해 |
| SGD + Momentum | 관성으로 빠른 수렴 | 컴퓨터 비전 |
| AdaGrad | 적응적 학습률 | 희소 데이터, NLP |
| RMSProp | AdaGrad 개선 | RNN |
| Adam | Momentum + RMSProp | 가장 범용적 |
| AdamW | Adam + L2 정규화 | Transformer, BERT, GPT |
'딥러닝 > 딥러닝 기초 개념' 카테고리의 다른 글
| 과적합과 정규화 (Overfitting, Dropout, BatchNorm) (0) | 2026.05.15 |
|---|---|
| 손실 함수 (Loss Function) (0) | 2026.05.15 |
| 순전파와 역전파 (Feedforward & Backpropagation) (0) | 2026.05.15 |
| 신경망이란? (퍼셉트론, 활성화 함수) (0) | 2026.05.13 |