Optimizer 종류와 특징
딥러닝 모델의 학습에서 Optimizer(최적화 알고리즘)는 손실 함수(Loss Function)를 최소화하기 위해 가중치(Weight)를 업데이트하는 핵심적인 역할을 한다. 아래는 대표적인 옵티마이저의 종류와 각 알고리즘의 특징에 대해 정리한 내용이다.
이번 블로그에서는 GD, SGD, Momentum, Adagrad 부터 살펴보겠다.
Gradient Descent (GD)
Gradient Descent는 최적화 알고리즘의 가장 기본적인 형태로,
전체 데이터셋에 대해 손실 함수의 기울기(Gradient)를 계산한 후, 가중치를 업데이트 한다.
수식은 아래와 같다.
\( w_{t+1} = w_t - \eta \cdot \nabla L(w_t) \)
\(w_t\) : 현재 단계(t)의 가중치 벡터
\(\eta\) : 학습률(Learning Rate)이며, 너무 크면 최적점을 지나칠 수 있고, 너무 작으면 수렴 속도가 느려짐
\(\nabla L(w_t)\) : 손실 함수 \(L(w)\)의 가중치 \(w_t\)에 대한 기울기(∇)
처음에 언급했던 optimizer 의 목표는
손실함수(Loss Funtion)을 최소화하기 위해 가중치(w)를 업데이트하는 역할이다.
수식을 쉽게 얘기해보면,
현재 가중치 \(w_t\) 를 업데이트하는 것이다. 그래서 기울기는 손실 함수의 증가 방향을 가리키므로, 기울기의 반대 방향으로 이동해야 손실이 줄어들기 때문에 '-' 를 해준것이다. 에서 손실 함수의 기울기 \(\nabla L(w_t)\) 방향으로 학습률 \(\eta\) 만큼 이동하여 \(w_t\)
비유를 통한 이해
산에서 내려오는 과정으로 비유
: 현재 위치(산 위의 지점)
: 현재 위치에서 가장 가파르게 올라가는 방향
− \(\nabla L(w_t)\) : 내려가는 방향
\(\eta\) : 한 번에 이동하는 거리(발걸음 크기)
경사하강법은 산 정상에서 시작하여 기울기를 따라 내려가면서(손실 최소화) 산 아래(최적값)에 도달하려고 하는 과정이다.
업데이트 과정
- 현재 위치 \(w_t\) 에서 기울기 방향(손실이 줄어드는 방향)으로 조금씩 이동하여 다음 위치 \(w_{t+1}\) 를 계산한다.
- 이 과정을 반복하면 최적의 가중치에 점점 가까워진다.
- 이것이 가장 기본적 GD 의 개념이며, 어떻게 딥러닝이 손실값을 최적화하는지 이해할 수 있다.
- 하지만 GD 의 단점이 있어서 다른 optimzer 함수(SGD, Momentum, Adam...)가 계속 발전한다.
GD의 단점
1. 지역 최적점(Local Minima) 문제
손실 함수가 비선형인 경우, 전역 최적점(Global Optimum) 대신 지역 최적점(Local Optimum)에 빠질 수 있다.
2. 느린 수렴 속도
학습률을 잘못 설정하면,
너무 작을 경우, 수렴 속도가 느려지고
너무 클 경우, 최적점에 도달하지 못하고 발산할 위험이 있다.
특히, 심한 곡률(Curvature)을 가진 손실 함수에서는 더 느려진다.
3. 스케일링 문제
각 변수(파라미터)의 변화율이 다를 경우, 업데이트가 비효율적으로 진행된다.
예를 들어, 하나의 변수는 작은 변화만 필요하고 다른 변수는 큰 변화가 필요할 때, 동일한 학습률로 조정하기 어렵다.
4. 모든 데이터를 사용하는 고비용 문제
배치 경사하강법 (Batch Gradient Descent)은 매번 전체 데이터를 사용해 기울기를 계산하므로, 데이터가 많을수록 계산 비용이 매우 높다. 이는 대규모 데이터셋에서 비효율적이다.
이를 보완하기 위해 나온 알고리즘이 SGD(Stochastic Gradient Descent) 이다.
SGD (Stochastic Gradient Descent)
SGD는 GD의 단점을 해결하기 위해 전체 데이터셋이 아닌 샘플 데이터의 일부(Mini-batch)로 기울기를 계산하고, 가중치를 업데이트한다.
수식은 아래와 같다.
\( w_{t+1} = w_t - \eta \cdot \nabla L(w_t; x_i, y_i) \)
\(w_t\) : 현재 가중치 벡터
\( x_i, y_i \) : \(x_i\) 입력 데이터의 i번째 샘플( 특징 벡터)이며, \(y_i\) 입력 데이터에 대응하는 실제 정답(레이블)
\(L(w_t; x_i, y_i)\) : \(x_i\)에 대한 손실 함수 값
\( \nabla L(w_t; x_i, y_i) \) : 손실 함수 L(w)의 현재 가중치 wt에 대한 기울기(Gradient)이다.
일반적인 경사하강법(GD)은 전체 데이터셋을 사용해 기울기를 계산하지만, SGD는 무작위로 선택된 하나의 샘플만을 사용한다. 따라서 계산 비용이 훨씬 적다.
또한, 매번 무작위 샘플을 사용하므로, 노이즈가 포함된 업데이트가 이루어지며, 이는 국소 최적점(Local Minima)에서 탈출할 가능성을 높여준다.
SGD의 단점
수렴 속도 문제 (느림)
SGD는 기울기 방향으로만 업데이트하므로 곡률이 급격히 변하는 손실 함수의 경우 느리게 학습된다.
경사가 급한 방향에서는 너무 빠르게, 경사가 완만한 방향에서는 너무 느리게 이동한다.
기울기의 노이즈 문제
SGD는 단일 샘플(또는 소규모 배치)로 기울기를 계산하기 때문에 업데이트가 불안정하다.
학습 과정에서 진동(Oscillation)이 발생하여 최적점 근처에서도 정확히 수렴하지 못할 수 있다.
학습률 조정의 어려움
고정된 학습률은 모든 파라미터에 동일하게 적용되기 때문에,
학습률이 너무 크면 발산할 위험이 있고,
너무 작으면 학습 속도가 지나치게 느려진다.
지역 최적점(Local Minima)과 Saddle Point 문제
SGD는 손실 함수가 복잡한 경우(예: Saddle Point)에서 빠져나오기 어려울 수 있다.
특히 비선형 모델에서 전역 최적점(Global Optimum)으로 도달하지 못할 가능성이 있다.
이를 보완하기 위해 나온 알고리즘이 Momentum과 AdaGrad가 있다.
Momentum
Momentum은 SGD의 진동 문제를 해결하기 위해, 이전의 업데이트 방향(모멘텀)을 고려해 가중치를 조정합니다. 이를 통해 학습 속도를 가속화하고 진동을 줄인다.
수식은 아래와 같다.
\( v_t = \gamma v_{t-1} + \eta \cdot \nabla L(w_t) \)
\( w_{t+1} = w_t - v_t \)
1. 속도( \( v_t \) )를 업데이트 : \( v_t = \gamma v_{t-1} + \eta \cdot \nabla L(w_t) \)
: 현재 단계에서의 "속도" (이전 기울기 정보를 포함).
\( \gamma \) : 모멘텀 계수로, 이전 단계 속도 vt−1v_{t-1}에 얼마나 의존할지를 결정하는 값 (보통 0.9).
\( \nabla L(w_t) \) : 현재 가중치 wtw_t에서의 손실 함수의 기울기.
2. 가중치(파라미터)를 업데이트 : \( w_{t+1} = w_t - v_t \)
: 현재 단계의 가중치(파라미터).
: 다음 단계의 가중치.
\( v_t \) : 속도를 사용해 가중치를 업데이트
Momentum은 물리학의 관성 개념에서 영감을 받았다. 가중치 업데이트를 단순히 기울기 방향만 따라가는 대신, 이전 업데이트의 "속도(velocity)"를 유지하며 앞으로 나아가도록 도와준다.
이를 통해 2가지 주요 문제를 해결한다.
- 진동(Oscillation) 감소
- 경사하강법은 경사가 급격히 변하는 곡선에서 진동하며 학습 속도가 느려질 수 있다.
- Momentum은 이전 방향(속도)을 반영해 진동을 줄이고 부드럽게 이동한다.
- 수렴 속도 향상
- 반복적으로 "속도"를 축적하기 때문에, 곡률이 완만한 방향에서는 더 빠르게 이동할 수 있다.
좀 더 쉽게 비유로 설명하면,
경사하강법(SGD): 산에서 내려올 때, 즉각적인 기울기만 따라가는 등산가
→ 매 순간 주변 지형에 따라 방향을 바꿔 움직이며 진동할 가능성이 높다.
Momentum: 산에서 내려올 때 기울기를 따라 가속도를 붙여 이동하는 등산가
→ 관성을 이용해 더 부드럽고 빠르게 내려올 수 있다.
Momentum의 단점
Momentum은 학습률이 고정되어 있다.
따라서 모든 파라미터에 동일한 학습률이 적용되며, 자주 업데이트되는 파라미터와 거의 업데이트되지 않는 파라미터 간의 학습률 조정이 어렵다. 중요하지 않은 파라미터에도 동일한 크기의 업데이트가 적용될 수 있으며, 이는 파라미터마다 다른 학습률을 요구하는 문제에서는 비효율적이다.
Momentum 의 단점을 개선하기 위해 NAG, RMSProp 등이 등장했다.
Adagrad
Adagrad는 각 파라미터별로 학습률을 조정하며, 자주 업데이트된 파라미터의 학습률을 감소시키고 드물게 업데이트된 파라미터의 학습률을 유지한다. 이는 희소 데이터(Sparse Data)에 적합합니다.
수식은 아래와 같다.
\( g_t = \nabla L(w_t) \)
\( G_t = G_{t-1} + g_t^2 \)
\( w_{t+1} = w_t - \frac{\eta}{\sqrt{G_t + \epsilon}} \cdot g_t \)
- 기울기 계산 : \( g_t = \nabla L(w_t) \)
- \( g_t \) : 현재 가중치 \( w_t \) 에서의 손실 함수의 기울기(Gradient)
- 이는 손실 함수가 가장 가파르게 변화하는 방향을 나타낸다.
- 기울기 히스토리 누적 : \( G_t = G_{t-1} + g_t^2 \)
- \( G_t \) : 각 파라미터에 대해 지금까지의 기울기 제곱값을 누적한 값
- 즉, 현재까지 학습 과정에서 얼마나 많은 변화가 있었는지를 누적 기록한다.
- 가중치 업데이트 : \( w_{t+1} = w_t - \frac{\eta}{\sqrt{G_t + \epsilon}} \cdot g_t \)
- 적응적인 학습률 조정:
- 학습률을 \(\frac{1}{\sqrt{G_t + \epsilon}}\)로 나눠서 자주 업데이트된 파라미터는 학습률을 줄이고, 희소한(드물게 업데이트된) 파라미터는 학습률을 높인다.
- ϵ: 작은 상수(분모가 0이 되는 것을 방지)
- 적응적인 학습률 조정:
수식만 보면 쉽게 이해가 어려우니,
직관적으로 이해해보자.
- 핵심 아이디어
- 손실 함수의 기울기를 따라 이동하되, 각 파라미터의 업데이트 빈도를 반영하여 학습률을 조정
- 자주 변화하는 파라미터는 학습률을 줄이고, 거의 변화하지 않는 파라미터는 학습률을 높여 학습 효율을 높임
- 파라미터별 학습률 조정:
- 자주 변화하는 파라미터:
- 값이 커짐 → 학습률 \( \frac{\eta}{\sqrt{G_t}} \) 이 작아짐.
- 거의 변화하지 않는 파라미터:
- \( G_t \) 값이 작음 → 학습률 \( \frac{\eta}{\sqrt{G_t}} \) 이 커짐.
- 자주 변화하는 파라미터:
- 왜 필요한가?:
- 실제 머신러닝 모델에서는 파라미터마다 중요도와 학습 속도가 다르다.
- 예를 들어, 어떤 파라미터는 큰 변화가 필요하고, 어떤 파라미터는 작은 변화만 필요할 수 있습니다. AdaGrad는 이를 자동으로 조정해 준다.
Adagrad 단점
학습률 감소 문제:
\( G_t \) 가 계속 누적되면서 값이 커지면, 학습률 \( \frac{\eta}{\sqrt{G_t}} \ 이 너무 작아져 학습이 멈출 수 있다.
이 문제를 해결하기 위해 RMSProp이라는 알고리즘이 등장했다.
RMSProp은 \( G_t \) 대신 기울기 제곱의 지수 이동 평균을 사용해 이 문제를 완화합니다.
간단한 예제로 비교하는 코드
ResNet-18 모델을 CIFAR-10 데이터셋에 대해 다양한 Optimizer(GD, SGD, Momentum, AdaGrad)를 사용하여 학습했을 때의 손실 값(Loss) 감소를 보여준다. 각 Optimizer가 손실 감소에 얼마나 효과적인지 비교해볼 수 있다.
import torch
import torch.nn as nn
import torch.optim as optim
import torchvision
import torchvision.transforms as transforms
from torchvision.models import resnet18
import matplotlib.pyplot as plt
# CIFAR-10 데이터 로드
def load_data(batch_size=64):
transform = transforms.Compose([
transforms.Resize((32, 32)),
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])
train_dataset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
return train_loader
# 모델 초기화
def get_model():
model = resnet18(num_classes=10) # CIFAR-10 클래스 수 설정
return model
# 학습 함수
def train_model(optimizer_name, model, train_loader, criterion, device, epochs=10):
# 모델을 GPU로 이동 (옵티마이저 초기화 전에 호출)
model.to(device)
# 옵티마이저 초기화
if optimizer_name == "GD":
optimizer = optim.SGD(model.parameters(), lr=0.01)
elif optimizer_name == "SGD":
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0)
elif optimizer_name == "Momentum":
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9)
elif optimizer_name == "Adagrad":
optimizer = optim.Adagrad(model.parameters(), lr=0.01)
else:
raise ValueError("Unsupported optimizer")
losses = []
for epoch in range(epochs):
model.train()
running_loss = 0.0
for inputs, targets in train_loader:
# 데이터를 GPU로 이동
inputs, targets = inputs.to(device), targets.to(device)
# 옵티마이저 초기화 및 학습 진행
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, targets)
loss.backward()
optimizer.step()
running_loss += loss.item()
avg_loss = running_loss / len(train_loader)
losses.append(avg_loss)
print(f"Epoch {epoch+1}/{epochs}, {optimizer_name} Loss: {avg_loss:.4f}")
return losses
# Main 실행
def main():
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
train_loader = load_data()
criterion = nn.CrossEntropyLoss()
optimizers = ["GD", "SGD", "Momentum", "Adagrad"]
results = {}
for optimizer_name in optimizers:
model = get_model()
print(f"\nTraining with {optimizer_name} optimizer...")
losses = train_model(optimizer_name, model, train_loader, criterion, device)
results[optimizer_name] = losses
# 결과 시각화
plt.figure(figsize=(10, 6))
for optimizer_name, losses in results.items():
plt.plot(range(1, len(losses) + 1), losses, label=optimizer_name)
plt.title("Loss Comparison of Optimizers")
plt.xlabel("Epochs")
plt.ylabel("Loss")
plt.legend()
plt.grid()
plt.show()
if __name__ == "__main__":
main()
결과
- AdaGrad
- 초기 손실 감소 속도가 가장 빠르며, 학습 초반에 좋은 성능을 보인다.
- 하지만 학습이 진행될수록 학습률이 너무 작아져 후반부 학습 속도가 느려지는 단점이 나타날 수 있다.
- Momentum
- 전체적으로 안정적이고 빠른 수렴을 보여준다.
- 특히 SGD보다 진동이 적으며, 학습 후반부까지도 일정한 성능 향상을 유지한다.
- SGD
- GD보다 빠르지만, 초기 학습 단계에서의 진동으로 인해 안정성이 부족할 수 있다.
- 모멘텀을 추가한 Momentum에 비해 학습 속도가 느리다.
- GD
- 안정적이지만, 다른 Optimizer에 비해 학습 속도가 느리고 계산 비용이 높아 실제로는 잘 사용되지 않는다.
이상으로 초기 optimizer 에 대한 내용 마치겠습니다.
다음에는 RMSProp, Adam, AdamW, AdamP 에 대한 차이를 알아보겠습니다.
감사합니다.
끝!
'딥러닝 (Deep Learning) > [04] - 학습 및 최적화' 카테고리의 다른 글
[Optimizer] - 일반적으로 왜 Adam만 사용할까? [2] (1) | 2025.01.01 |
---|---|
[경량화/최적화] - 레이버별 민감도 기반 Pruning [3] (2) | 2024.12.24 |
[경량화/최적화] - Pruning 방법의 분류 [2] (2) | 2024.12.24 |
[경량화/최적화] - Pruning 이란? [1] (1) | 2024.12.23 |
Pseudo 라벨링(Pseudo Labeling)이란 무엇인가? (0) | 2024.11.24 |