이미지 로딩 중...
AI Generated
2025. 11. 16. · 6 Views
하이퍼파라미터 튜닝 완벽 가이드
머신러닝 모델의 성능을 최대로 끌어올리는 핵심 기법인 하이퍼파라미터 튜닝을 배워봅니다. Learning Rate, Batch Size, Epochs 등 주요 하이퍼파라미터를 조정하는 실전 노하우를 초급자도 쉽게 이해할 수 있도록 설명합니다.
목차
- Learning Rate 기초
- Batch Size 이해하기
- Epochs 설정하기
- Learning Rate Scheduler
- Grid Search
- Random Search
- Early Stopping
- Learning Rate Finder
1. Learning Rate 기초
시작하며
여러분이 처음 딥러닝 모델을 학습시킬 때 이런 상황을 겪어본 적 있나요? 학습을 시작했는데 손실(loss)이 전혀 줄어들지 않거나, 반대로 갑자기 NaN이 되어버리는 상황 말이죠.
이런 문제는 실제 개발 현장에서 가장 자주 발생하는 초급자의 고민입니다. 대부분의 경우 Learning Rate(학습률)이 너무 크거나 작아서 발생합니다.
Learning Rate가 너무 크면 모델이 최적점을 찾지 못하고 계속 튕겨나가고, 너무 작으면 학습이 너무 느려서 며칠을 기다려도 좋은 결과를 얻지 못합니다. 바로 이럴 때 필요한 것이 적절한 Learning Rate 설정입니다.
마치 자전거를 탈 때 속도를 조절하듯이, 모델이 학습하는 속도를 적절히 조절하면 빠르고 안정적으로 좋은 성능을 얻을 수 있습니다.
개요
간단히 말해서, Learning Rate는 모델이 한 번 학습할 때 얼마나 크게 변화할지를 결정하는 숫자입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하자면, 모델은 데이터를 보면서 조금씩 자신의 가중치(weight)를 변경합니다.
이때 한 번에 얼마나 크게 변경할지를 결정하는 것이 Learning Rate입니다. 예를 들어, 이미지 분류 모델을 학습시킬 때 Learning Rate를 0.001로 설정하면 천천히 안정적으로 학습하고, 0.1로 설정하면 빠르지만 불안정하게 학습합니다.
기존에는 0.01 같은 고정된 값을 사용했다면, 이제는 모델과 데이터의 특성에 맞춰 0.001부터 0.1까지 다양하게 실험하여 최적의 값을 찾을 수 있습니다. Learning Rate의 핵심 특징은 세 가지입니다.
첫째, 너무 크면 학습이 발산하고, 둘째, 너무 작으면 학습이 너무 느리며, 셋째, 학습 중간에 동적으로 조절할 수 있습니다. 이러한 특징들이 모델의 최종 성능을 크게 좌우하기 때문에 매우 중요합니다.
코드 예제
import torch
import torch.nn as nn
import torch.optim as optim
# 간단한 신경망 모델 정의
model = nn.Sequential(
nn.Linear(784, 128),
nn.ReLU(),
nn.Linear(128, 10)
)
# Learning Rate를 0.001로 설정한 옵티마이저
# 이 값이 너무 크면 학습이 불안정해지고, 너무 작으면 학습이 느려집니다
optimizer = optim.Adam(model.parameters(), lr=0.001)
# 다양한 Learning Rate로 실험해보기
learning_rates = [0.0001, 0.001, 0.01, 0.1]
for lr in learning_rates:
optimizer = optim.Adam(model.parameters(), lr=lr)
print(f"Learning Rate: {lr}로 학습 시작")
설명
이것이 하는 일: Learning Rate는 경사하강법(Gradient Descent)에서 가중치를 업데이트할 때 사용하는 스텝 크기를 결정합니다. 첫 번째로, 모델은 손실 함수를 계산하고 역전파를 통해 각 가중치가 얼마나 변경되어야 하는지(gradient)를 계산합니다.
이때 gradient는 방향만 알려줄 뿐, 실제로 얼마나 크게 움직일지는 알려주지 않습니다. 그래서 Learning Rate를 곱해서 실제 변경량을 결정하는 것이죠.
그 다음으로, optimizer가 실행되면서 새로운_가중치 = 기존_가중치 - learning_rate × gradient라는 공식으로 가중치를 업데이트합니다. 만약 Learning Rate가 0.01이고 gradient가 2.0이라면, 가중치는 0.02만큼 감소합니다.
Learning Rate가 0.1이었다면 0.2만큼 감소했겠죠. 마지막으로, 이 과정이 반복되면서 모델의 손실이 점점 줄어들고 성능이 향상됩니다.
적절한 Learning Rate를 사용하면 보통 수십 에폭 내에 좋은 성능에 도달할 수 있습니다. 여러분이 이 코드를 사용하면 다양한 Learning Rate를 빠르게 실험해볼 수 있고, 어떤 값이 가장 좋은 결과를 내는지 비교할 수 있습니다.
실무에서는 TensorBoard나 wandb 같은 도구로 각 Learning Rate별 학습 곡선을 시각화하여 최적값을 찾습니다.
실전 팁
💡 처음에는 0.001로 시작해보세요. Adam 옵티마이저에서 가장 안정적인 기본값입니다.
💡 학습 중 손실이 NaN이 되면 Learning Rate를 10분의 1로 줄여보세요. 너무 큰 값이 원인일 가능성이 높습니다.
💡 손실이 전혀 줄어들지 않으면 Learning Rate를 10배 늘려보세요. 너무 작아서 학습이 안 되는 것일 수 있습니다.
💡 SGD 옵티마이저는 Adam보다 10100배 큰 Learning Rate가 필요합니다. 0.010.1 범위를 시도해보세요.
💡 Learning Rate를 바꿀 때마다 모델을 처음부터 다시 학습시켜야 정확한 비교가 가능합니다.
2. Batch Size 이해하기
시작하며
여러분이 GPU 메모리가 부족하다는 에러(CUDA Out of Memory)를 겪어본 적 있나요? 또는 학습이 너무 느려서 하루 종일 기다려도 한 에폭도 끝나지 않는 상황은 어떤가요?
이런 문제는 실제 개발 현장에서 하드웨어 제약과 학습 효율 사이의 균형을 맞추지 못해 발생합니다. Batch Size가 너무 크면 GPU 메모리가 부족해지고, 너무 작으면 GPU를 제대로 활용하지 못해 학습이 느려집니다.
또한 Batch Size는 모델의 최종 성능에도 영향을 미칩니다. 바로 이럴 때 필요한 것이 적절한 Batch Size 설정입니다.
마치 트럭으로 짐을 나를 때 한 번에 얼마나 실을지 결정하는 것처럼, 한 번에 몇 개의 데이터를 처리할지 결정하면 효율성과 성능을 모두 개선할 수 있습니다.
개요
간단히 말해서, Batch Size는 모델이 한 번의 forward-backward pass에서 처리하는 샘플(데이터)의 개수입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하자면, GPU는 병렬 처리에 최적화되어 있어서 여러 개의 데이터를 동시에 처리할 때 효율이 높습니다.
한 번에 한 개씩 처리하면 GPU의 성능을 1%도 활용하지 못하는 것이죠. 예를 들어, 이미지 분류 작업에서 Batch Size 32로 설정하면 32장의 이미지를 동시에 처리하여 학습 속도가 크게 향상됩니다.
기존에는 메모리가 허용하는 한 최대한 큰 Batch Size를 사용했다면, 이제는 학습 안정성과 일반화 성능을 고려하여 16에서 128 사이의 적절한 값을 선택합니다. Batch Size의 핵심 특징은 세 가지입니다.
첫째, 클수록 학습 속도가 빠르지만 메모리를 많이 사용하고, 둘째, 작을수록 노이즈가 많아 일반화에 도움이 되지만 학습이 불안정할 수 있으며, 셋째, Learning Rate와 함께 조정해야 최적의 성능을 얻을 수 있습니다. 이러한 특징들이 학습 효율성과 모델 품질을 동시에 결정하기 때문에 신중하게 선택해야 합니다.
코드 예제
import torch
from torch.utils.data import DataLoader, TensorDataset
# 가상의 데이터셋 생성 (1000개의 샘플)
X = torch.randn(1000, 784) # 입력 데이터
y = torch.randint(0, 10, (1000,)) # 레이블
dataset = TensorDataset(X, y)
# Batch Size 32로 DataLoader 생성
# shuffle=True는 매 에폭마다 데이터 순서를 섞어 과적합 방지
dataloader = DataLoader(dataset, batch_size=32, shuffle=True)
# 다양한 Batch Size 비교
batch_sizes = [8, 16, 32, 64, 128]
for bs in batch_sizes:
loader = DataLoader(dataset, batch_size=bs, shuffle=True)
print(f"Batch Size {bs}: {len(loader)}개의 배치로 나뉨")
# Batch Size 8: 125개 배치, Batch Size 128: 8개 배치
설명
이것이 하는 일: Batch Size는 전체 데이터셋을 여러 개의 작은 묶음(배치)으로 나누어 학습할 수 있게 해줍니다. 첫 번째로, DataLoader는 전체 데이터셋을 Batch Size 크기의 청크로 나눕니다.
예를 들어 1000개의 샘플이 있고 Batch Size가 32라면, 31개의 배치(30개는 32개씩, 마지막은 8개)로 나뉩니다. 이렇게 나누는 이유는 모든 데이터를 한 번에 메모리에 올릴 수 없고, 작은 단위로 나누어 처리해야 하기 때문입니다.
그 다음으로, 각 배치에 대해 forward pass를 수행하여 예측값을 계산하고, 손실을 계산한 후, backward pass로 gradient를 구합니다. 중요한 점은 이 gradient가 배치 내의 모든 샘플에 대한 평균 gradient라는 것입니다.
Batch Size가 32라면 32개 샘플의 gradient를 평균내어 하나의 업데이트를 수행합니다. 마지막으로, optimizer가 이 평균 gradient를 사용하여 가중치를 업데이트합니다.
큰 Batch Size는 더 정확한 gradient 추정을 제공하지만 업데이트 횟수가 줄어들고, 작은 Batch Size는 노이즈가 많지만 더 자주 업데이트하여 다양한 방향을 탐색할 수 있습니다. 여러분이 이 코드를 사용하면 데이터셋 크기와 GPU 메모리에 맞는 최적의 Batch Size를 찾을 수 있습니다.
실무에서는 GPU 메모리가 허용하는 범위 내에서 가능한 한 큰 값을 사용하되, 32나 64처럼 2의 거듭제곱을 사용하면 GPU 연산 효율이 좋습니다.
실전 팁
💡 GPU 메모리 부족 에러가 나면 Batch Size를 절반으로 줄여보세요. 32 → 16 → 8 순서로 시도하면 됩니다.
💡 Batch Size를 2배로 늘릴 때는 Learning Rate도 2배로 늘려보세요. 두 값은 서로 연관되어 있습니다.
💡 작은 데이터셋(1000개 미만)에서는 Batch Size 8~16이 좋습니다. 너무 크면 배치가 몇 개 안 돼서 학습이 불안정합니다.
💡 2의 거듭제곱(8, 16, 32, 64, 128)을 사용하면 GPU가 더 효율적으로 연산합니다.
💡 num_workers 파라미터를 4~8로 설정하면 데이터 로딩 속도가 크게 향상됩니다.
3. Epochs 설정하기
시작하며
여러분이 모델을 학습시킬 때 "몇 번을 반복해야 하지?"라는 고민을 해본 적 있나요? 너무 적게 학습하면 성능이 안 나오고, 너무 많이 학습하면 과적합(overfitting)이 발생하는 딜레마 말이죠.
이런 문제는 실제 개발 현장에서 매우 흔합니다. Epochs를 5로 설정했는데 아직 손실이 계속 줄어들고 있어서 더 학습하면 좋을 것 같거나, 반대로 100 에폭을 돌렸는데 50 에폭부터는 검증 손실이 오히려 증가하는 경우가 있습니다.
이는 시간과 자원의 낭비를 초래합니다. 바로 이럴 때 필요한 것이 적절한 Epochs 설정과 Early Stopping 같은 기법입니다.
마치 요리할 때 불을 얼마나 오래 켤지 결정하는 것처럼, 모델을 얼마나 오래 학습시킬지 정하면 최적의 성능과 효율을 얻을 수 있습니다.
개요
간단히 말해서, Epochs는 전체 훈련 데이터셋을 몇 번 반복해서 학습할지를 나타내는 숫자입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하자면, 모델은 데이터를 한 번 보는 것만으로는 충분히 학습할 수 없습니다.
마치 우리가 책을 한 번 읽는 것보다 여러 번 읽을 때 더 깊이 이해하는 것처럼, 모델도 같은 데이터를 여러 번 보면서 패턴을 더 잘 학습합니다. 예를 들어, 손글씨 숫자 분류 모델을 학습시킬 때 10 에폭 정도면 기본적인 패턴을 학습하고, 50 에폭 정도면 세밀한 특징까지 학습합니다.
기존에는 100이나 200 같은 큰 숫자를 고정으로 사용했다면, 이제는 검증 손실을 모니터링하면서 더 이상 개선되지 않으면 자동으로 멈추는 방식을 사용합니다. Epochs의 핵심 특징은 세 가지입니다.
첫째, 너무 적으면 underfitting(과소적합)이 발생하고, 둘째, 너무 많으면 overfitting(과적합)이 발생하며, 셋째, 데이터셋 크기와 모델 복잡도에 따라 적절한 값이 크게 달라집니다. 이러한 특징들이 모델의 일반화 성능을 결정하기 때문에 신중하게 관찰하면서 조정해야 합니다.
코드 예제
import torch
import torch.nn as nn
model = nn.Sequential(nn.Linear(784, 128), nn.ReLU(), nn.Linear(128, 10))
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
criterion = nn.CrossEntropyLoss()
# 전체 학습 과정
num_epochs = 50 # 50번 반복 학습
best_val_loss = float('inf')
for epoch in range(num_epochs):
# 한 에폭: 전체 데이터셋을 한 번 순회
model.train()
for batch_x, batch_y in train_loader:
optimizer.zero_grad()
outputs = model(batch_x)
loss = criterion(outputs, batch_y)
loss.backward()
optimizer.step()
# 검증 성능 확인
model.eval()
val_loss = evaluate(model, val_loader) # 검증 함수
print(f'Epoch {epoch+1}/{num_epochs}, Val Loss: {val_loss:.4f}')
# 최고 성능 모델 저장
if val_loss < best_val_loss:
best_val_loss = val_loss
torch.save(model.state_dict(), 'best_model.pth')
설명
이것이 하는 일: Epochs는 모델이 전체 훈련 데이터를 처음부터 끝까지 한 번 학습하는 과정을 몇 번 반복할지 정의합니다. 첫 번째로, 한 에폭 동안 모델은 DataLoader를 통해 모든 배치를 순회하며 학습합니다.
예를 들어 훈련 데이터가 1000개이고 Batch Size가 32라면, 한 에폭에 약 31번의 가중치 업데이트가 발생합니다. 이것이 한 사이클입니다.
그 다음으로, 각 에폭이 끝날 때마다 검증 데이터셋으로 모델의 성능을 평가합니다. 훈련 손실은 계속 줄어들 수 있지만, 검증 손실이 증가하기 시작하면 과적합의 신호입니다.
이때가 학습을 멈춰야 할 시점입니다. 마지막으로, 위 코드에서는 가장 낮은 검증 손실을 기록한 모델을 파일로 저장합니다.
이렇게 하면 50 에폭을 모두 돌려도 과적합이 시작되기 전의 최고 성능 모델을 얻을 수 있습니다. 최종적으로 'best_model.pth' 파일에 저장된 모델을 실전에 사용하면 됩니다.
여러분이 이 코드를 사용하면 학습 과정을 완전히 제어할 수 있고, 각 에폭마다 성능 변화를 추적하여 언제 학습을 멈춰야 할지 판단할 수 있습니다. 실무에서는 TensorBoard로 훈련/검증 손실 그래프를 그려서 시각적으로 확인합니다.
실전 팁
💡 처음에는 10 에폭 정도로 빠르게 실험하고, 손실이 계속 줄어들면 점진적으로 늘려보세요.
💡 훈련 손실은 줄어드는데 검증 손실이 증가하면 즉시 학습을 중단하세요. 과적합이 시작된 것입니다.
💡 각 에폭마다 모델을 저장하지 말고, 검증 성능이 개선될 때만 저장하세요. 디스크 공간을 절약할 수 있습니다.
💡 작은 데이터셋(<1000개)에서는 100200 에폭이 필요하지만, 큰 데이터셋(>100,000개)에서는 1030 에폭이면 충분합니다.
💡 학습 중간에 Ctrl+C로 중단해도 best_model.pth는 저장되어 있으니 걱정하지 마세요.
4. Learning Rate Scheduler
시작하며
여러분이 모델을 학습시키다가 어느 순간부터 손실이 전혀 줄어들지 않는 plateau 현상을 겪어본 적 있나요? 처음에는 잘 학습되다가 중간부터 멈춘 것처럼 보이는 상황 말이죠.
이런 문제는 실제 개발 현장에서 고정된 Learning Rate를 사용할 때 자주 발생합니다. 학습 초기에는 큰 Learning Rate로 빠르게 최적점 근처로 가야 하지만, 후반부에는 작은 Learning Rate로 세밀하게 조정해야 합니다.
마치 목적지에 가까워질수록 속도를 줄여야 정확히 도착하는 것과 같습니다. 바로 이럴 때 필요한 것이 Learning Rate Scheduler입니다.
학습 진행에 따라 Learning Rate를 자동으로 조정하면 더 빠르고 안정적으로 최적의 성능에 도달할 수 있습니다.
개요
간단히 말해서, Learning Rate Scheduler는 학습 중에 Learning Rate를 동적으로 변경하는 도구입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하자면, 학습 단계마다 최적의 Learning Rate가 다릅니다.
초기에는 큰 Learning Rate(예: 0.01)로 빠르게 탐색하다가, 중후반부에는 작은 Learning Rate(예: 0.0001)로 미세 조정해야 최고 성능을 얻을 수 있습니다. 예를 들어, ImageNet 같은 대규모 이미지 분류 작업에서는 30 에폭마다 Learning Rate를 10분의 1로 줄이는 것이 일반적입니다.
기존에는 손실이 멈추면 수동으로 Learning Rate를 조정했다면, 이제는 Scheduler가 자동으로 최적의 타이밍에 조정해줍니다. Learning Rate Scheduler의 핵심 특징은 세 가지입니다.
첫째, StepLR처럼 일정 간격으로 줄이는 방식, 둘째, ReduceLROnPlateau처럼 성능이 정체될 때 줄이는 방식, 셋째, CosineAnnealing처럼 코사인 함수로 부드럽게 줄이는 방식이 있습니다. 이러한 다양한 전략들이 모델의 수렴 속도와 최종 성능을 크게 향상시킵니다.
코드 예제
import torch
import torch.optim as optim
from torch.optim.lr_scheduler import StepLR, ReduceLROnPlateau
model = torch.nn.Linear(10, 2)
optimizer = optim.Adam(model.parameters(), lr=0.01)
# 방법 1: 매 10 에폭마다 Learning Rate를 0.1배로 감소
scheduler_step = StepLR(optimizer, step_size=10, gamma=0.1)
# 방법 2: 검증 손실이 3 에폭 동안 개선되지 않으면 0.5배로 감소
scheduler_plateau = ReduceLROnPlateau(optimizer, mode='min',
patience=3, factor=0.5)
for epoch in range(50):
# 학습 과정
train_loss = train_one_epoch(model, optimizer)
val_loss = validate(model)
# Scheduler 업데이트
scheduler_step.step() # StepLR: 매 에폭 호출
# scheduler_plateau.step(val_loss) # ReduceLROnPlateau: 검증 손실 전달
current_lr = optimizer.param_groups[0]['lr']
print(f'Epoch {epoch}, LR: {current_lr:.6f}')
설명
이것이 하는 일: Learning Rate Scheduler는 에폭이나 성능 지표에 따라 optimizer의 Learning Rate를 자동으로 조정합니다. 첫 번째로, Scheduler 객체를 생성할 때 조정 전략을 정의합니다.
StepLR의 경우 step_size=10은 10 에폭마다 조정하고, gamma=0.1은 Learning Rate를 현재 값의 0.1배(즉, 10분의 1)로 줄인다는 뜻입니다. 예를 들어 초기 Learning Rate가 0.01이면, 10 에폭 후 0.001, 20 에폭 후 0.0001이 됩니다.
그 다음으로, 매 에폭마다 scheduler.step()을 호출하여 Learning Rate를 업데이트합니다. StepLR은 단순히 에폭 수만 보지만, ReduceLROnPlateau는 검증 손실 값을 인자로 받아서 성능이 개선되지 않으면 Learning Rate를 줄입니다.
patience=3은 3 에폭 동안 개선이 없으면 조정한다는 의미입니다. 마지막으로, optimizer.param_groups[0]['lr']로 현재 Learning Rate를 확인할 수 있습니다.
이를 로그로 기록하면 학습 과정에서 Learning Rate가 어떻게 변했는지 추적할 수 있고, 성능 변화와 함께 분석할 수 있습니다. 여러분이 이 코드를 사용하면 수동으로 Learning Rate를 조정할 필요 없이 자동으로 최적화할 수 있습니다.
실무에서는 ReduceLROnPlateau가 가장 범용적으로 사용되며, Transformer 모델에서는 WarmupScheduler와 CosineAnnealing을 조합해서 사용합니다.
실전 팁
💡 초보자라면 ReduceLROnPlateau를 사용하세요. 성능 기반으로 자동 조정되어 실패 확률이 낮습니다.
💡 StepLR의 step_size는 전체 에폭의 1/3 정도로 설정하세요. 50 에폭이면 step_size=15 정도가 적당합니다.
💡 scheduler.step()은 optimizer.step() 이후, 한 에폭이 완전히 끝난 후 호출해야 합니다.
💡 CosineAnnealingLR은 Learning Rate가 부드럽게 변해서 안정적이지만, 최소값(eta_min)을 너무 작게 설정하면 학습이 멈춥니다.
💡 여러 Scheduler를 동시에 사용하지 마세요. 충돌이 발생하여 예상치 못한 동작을 합니다.
5. Grid Search
시작하며
여러분이 "Learning Rate는 0.001이 좋을까, 0.01이 좋을까? Batch Size는?" 같은 질문에 일일이 실험하느라 며칠을 보낸 적 있나요?
하나씩 바꿔가며 시도하다 보면 어떤 조합이 최선인지 놓치기 쉽습니다. 이런 문제는 실제 개발 현장에서 하이퍼파라미터 조합이 너무 많아 체계적으로 탐색하지 못할 때 발생합니다.
Learning Rate 3가지, Batch Size 3가지, Epochs 2가지만 해도 18가지 조합이 나오는데, 이를 무작위로 시도하면 최적해를 찾기 어렵습니다. 바로 이럴 때 필요한 것이 Grid Search입니다.
모든 하이퍼파라미터 조합을 체계적으로 시도하여 최고의 조합을 찾을 수 있습니다.
개요
간단히 말해서, Grid Search는 지정한 하이퍼파라미터의 모든 가능한 조합을 시도하여 최적의 조합을 찾는 방법입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하자면, 하이퍼파라미터들은 서로 상호작용합니다.
Learning Rate가 0.01일 때는 Batch Size 32가 좋지만, 0.001일 때는 64가 더 좋을 수 있습니다. 따라서 각각을 따로 최적화하는 것보다 조합으로 탐색하는 것이 정확합니다.
예를 들어, 중요한 프로젝트에서 0.5%의 성능 차이가 중요하다면 Grid Search로 철저히 탐색해야 합니다. 기존에는 경험이나 직관에 의존했다면, 이제는 컴퓨팅 파워를 활용하여 수십, 수백 가지 조합을 자동으로 실험할 수 있습니다.
Grid Search의 핵심 특징은 세 가지입니다. 첫째, 지정한 범위 내에서 빠짐없이 모든 조합을 시도하고, 둘째, 결과를 체계적으로 비교하여 최선의 조합을 찾으며, 셋째, 파라미터 개수와 값이 많을수록 시간이 기하급수적으로 증가합니다.
이러한 특징들이 철저한 탐색과 시간 비용 사이의 균형을 요구합니다.
코드 예제
from sklearn.model_selection import ParameterGrid
import torch
# 탐색할 하이퍼파라미터 정의
param_grid = {
'learning_rate': [0.0001, 0.001, 0.01],
'batch_size': [16, 32, 64],
'num_epochs': [20, 50]
}
# 모든 조합 생성 (3 × 3 × 2 = 18가지)
grid = ParameterGrid(param_grid)
best_accuracy = 0
best_params = None
for params in grid:
print(f"Testing: {params}")
# 각 조합으로 모델 학습
model = create_model()
optimizer = torch.optim.Adam(model.parameters(),
lr=params['learning_rate'])
train_loader = DataLoader(dataset, batch_size=params['batch_size'])
# 학습 및 평가
accuracy = train_and_evaluate(model, train_loader,
num_epochs=params['num_epochs'])
# 최고 성능 기록
if accuracy > best_accuracy:
best_accuracy = accuracy
best_params = params
print(f"Best params: {best_params}, Accuracy: {best_accuracy:.4f}")
설명
이것이 하는 일: Grid Search는 하이퍼파라미터 공간을 격자(grid) 모양으로 나누어 각 점마다 모델을 학습하고 평가합니다. 첫 번째로, ParameterGrid가 모든 조합을 생성합니다.
위 예제에서는 Learning Rate 3개 × Batch Size 3개 × Epochs 2개 = 18가지 조합이 나옵니다. 각 조합은 딕셔너리 형태로 {'learning_rate': 0.001, 'batch_size': 32, 'num_epochs': 20} 같이 표현됩니다.
그 다음으로, for 루프에서 각 조합을 순회하며 모델을 처음부터 학습시킵니다. 중요한 점은 매번 새로운 모델을 만들어야 한다는 것입니다.
이전 조합의 가중치가 남아있으면 공정한 비교가 되지 않습니다. create_model()로 매번 초기화합니다.
마지막으로, 각 조합의 성능(정확도, 손실 등)을 기록하고 최고 성능을 낸 조합을 저장합니다. 18번의 학습이 끝나면 best_params에 최적의 하이퍼파라미터가 저장됩니다.
이를 사용하여 최종 모델을 학습시키면 됩니다. 여러분이 이 코드를 사용하면 추측이나 직관 대신 데이터에 기반한 최적의 하이퍼파라미터를 찾을 수 있습니다.
실무에서는 Ray Tune이나 Optuna 같은 라이브러리로 병렬 처리하여 시간을 크게 단축할 수 있습니다.
실전 팁
💡 처음에는 2-3개 파라미터만, 각각 2-3개 값으로 시작하세요. 조합이 너무 많으면 며칠이 걸립니다.
💡 대략적인 범위를 먼저 찾고(예: 0.0001~0.1), 그 주변을 세밀하게 탐색하는 2단계 전략을 사용하세요.
💡 각 조합의 결과를 CSV 파일로 저장하면 나중에 분석하기 좋습니다. pandas로 정렬해서 상위 5개를 확인하세요.
💡 GPU가 여러 개면 torch.multiprocessing으로 병렬 실행하여 시간을 1/N으로 줄일 수 있습니다.
💡 전체 데이터가 아닌 일부(10~20%)로 빠르게 Grid Search하고, 최종 조합만 전체 데이터로 학습하세요.
6. Random Search
시작하며
여러분이 Grid Search를 하려는데 탐색 공간이 너무 커서 시간이 일주일 이상 걸린다는 계산이 나온 적 있나요? 5개 파라미터에 각각 10개 값이면 100,000가지 조합이라 현실적으로 불가능합니다.
이런 문제는 실제 개발 현장에서 하이퍼파라미터 탐색에 무한정 시간을 쓸 수 없을 때 발생합니다. Grid Search는 완벽하지만 너무 느리고, 실제로 모든 조합을 시도할 필요도 없습니다.
중요한 파라미터는 소수이고, 나머지는 영향이 작기 때문입니다. 바로 이럴 때 필요한 것이 Random Search입니다.
무작위로 조합을 샘플링하여 적은 시도로도 좋은 결과를 얻을 수 있습니다.
개요
간단히 말해서, Random Search는 하이퍼파라미터 공간에서 무작위로 조합을 선택하여 시도하는 방법입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하자면, 많은 연구에서 Random Search가 같은 횟수의 시도에서 Grid Search보다 더 좋은 결과를 낸다는 것이 밝혀졌습니다.
왜냐하면 정말 중요한 파라미터는 1-2개이고, Random Search는 그 파라미터의 다양한 값을 더 많이 시도하기 때문입니다. 예를 들어, Learning Rate가 가장 중요하다면 Random Search가 더 다양한 Learning Rate를 탐색합니다.
기존에는 격자 형태로 균일하게 탐색했다면, 이제는 확률 분포에서 샘플링하여 더 효율적으로 탐색합니다. Random Search의 핵심 특징은 세 가지입니다.
첫째, 같은 시간에 Grid Search보다 더 넓은 영역을 탐색하고, 둘째, 중요한 파라미터를 더 집중적으로 탐색하며, 셋째, 연속적인 값(예: Learning Rate 0.00123...)도 쉽게 탐색할 수 있습니다. 이러한 특징들이 제한된 자원으로 효율적인 탐색을 가능하게 합니다.
코드 예제
import numpy as np
import torch
# 탐색할 범위 정의
param_distributions = {
'learning_rate': (1e-5, 1e-1), # 0.00001 ~ 0.1 사이
'batch_size': [16, 32, 64, 128],
'dropout': (0.0, 0.5) # 0.0 ~ 0.5 사이
}
def random_sample(distributions, n_trials=20):
"""무작위로 n_trials개의 조합 생성"""
for _ in range(n_trials):
params = {}
# Learning Rate: 로그 스케일로 샘플링
lr_min, lr_max = distributions['learning_rate']
params['learning_rate'] = 10 ** np.random.uniform(
np.log10(lr_min), np.log10(lr_max))
# Batch Size: 리스트에서 무작위 선택
params['batch_size'] = np.random.choice(distributions['batch_size'])
# Dropout: 균등 분포에서 샘플링
params['dropout'] = np.random.uniform(*distributions['dropout'])
yield params
# 20번의 무작위 시도
best_accuracy = 0
for params in random_sample(param_distributions, n_trials=20):
print(f"Trying: {params}")
accuracy = train_and_evaluate(params)
best_accuracy = max(best_accuracy, accuracy)
설명
이것이 하는 일: Random Search는 각 하이퍼파라미터를 확률 분포에서 독립적으로 샘플링하여 조합을 만듭니다. 첫 번째로, Learning Rate 같은 연속 변수는 범위를 지정하고 그 사이에서 무작위 값을 뽑습니다.
중요한 점은 로그 스케일로 샘플링한다는 것입니다. 0.00001~0.1 범위에서 균등하게 뽑으면 대부분 0.1 근처만 나오므로, 로그를 취해서 작은 값도 공평하게 뽑히도록 합니다.
그 다음으로, Batch Size 같은 이산 변수는 후보 목록에서 무작위로 선택합니다. np.random.choice()가 [16, 32, 64, 128] 중 하나를 균등 확률로 뽑아줍니다.
Dropout 같은 비율은 0.0~0.5 사이에서 균등 분포로 샘플링합니다. 마지막으로, 지정한 횟수(n_trials)만큼 반복하면서 각 조합으로 모델을 학습하고 성능을 기록합니다.
20번 시도하면 20개 조합이 나오는데, 이는 Grid Search로 5×4=20개를 시도하는 것보다 더 다양한 영역을 탐색합니다. 여러분이 이 코드를 사용하면 제한된 시간 안에 최대한 넓은 범위를 탐색할 수 있습니다.
실무에서는 Optuna나 Ray Tune 라이브러리를 사용하면 베이지안 최적화까지 결합하여 더욱 효율적으로 탐색할 수 있습니다.
실전 팁
💡 처음 탐색할 때는 Random Search로 50~100번 시도하여 대략적인 범위를 찾으세요.
💡 Learning Rate는 반드시 로그 스케일로 샘플링하세요. 선형 스케일은 큰 값에 편향됩니다.
💡 모든 시도 결과를 저장하고, 상위 5개 조합의 평균 범위를 확인하면 최적 영역을 알 수 있습니다.
💡 시간이 충분하면 Random Search 후 상위 조합 주변을 Grid Search로 정밀 탐색하세요.
💡 재현성을 위해 np.random.seed()를 설정하면 같은 조합이 다시 나옵니다.
7. Early Stopping
시작하며
여러분이 100 에폭을 설정해놓고 학습을 시작했는데, 50 에폭 이후부터 검증 손실이 계속 올라가는 것을 보면서도 멈출 수 없어 50 에폭을 낭비한 적 있나요? 이런 문제는 실제 개발 현장에서 고정된 에폭 수로 학습할 때 매우 흔합니다.
과적합이 시작되었는데도 계속 학습하면 검증 성능은 나빠지고, 시간과 GPU 자원만 낭비됩니다. 게다가 언제 멈춰야 할지 사람이 계속 모니터링할 수도 없습니다.
바로 이럴 때 필요한 것이 Early Stopping입니다. 검증 성능이 더 이상 개선되지 않으면 자동으로 학습을 중단하여 시간을 절약하고 최고 성능 모델을 보존할 수 있습니다.
개요
간단히 말해서, Early Stopping은 검증 성능이 일정 기간 개선되지 않으면 학습을 자동으로 멈추는 기법입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하자면, 모델은 학습 데이터에 대해서는 계속 성능이 좋아지지만, 어느 순간부터 검증 데이터에 대한 성능은 나빠지기 시작합니다.
이것이 과적합입니다. Early Stopping은 이 시점을 자동으로 감지하여 멈춰줍니다.
예를 들어, 검증 손실이 5 에폭 동안 개선되지 않으면 학습을 중단하고, 가장 좋았던 시점의 모델을 사용합니다. 기존에는 충분히 큰 에폭(예: 200)을 설정하고 끝까지 학습했다면, 이제는 Early Stopping으로 필요한 만큼만 학습하여 시간을 절약합니다.
Early Stopping의 핵심 특징은 세 가지입니다. 첫째, patience 파라미터로 몇 에폭 동안 개선이 없으면 멈출지 결정하고, 둘째, 최고 성능 시점의 모델을 자동으로 보존하며, 셋째, 과적합을 방지하여 일반화 성능을 향상시킵니다.
이러한 특징들이 효율적이고 안정적인 학습을 가능하게 합니다.
코드 예제
import torch
import numpy as np
class EarlyStopping:
"""Early Stopping 헬퍼 클래스"""
def __init__(self, patience=5, min_delta=0.001):
self.patience = patience # 몇 에폭 기다릴지
self.min_delta = min_delta # 의미있는 개선의 최소 크기
self.counter = 0
self.best_loss = np.inf
self.early_stop = False
self.best_model = None
def __call__(self, val_loss, model):
# 검증 손실이 개선되었는지 확인
if val_loss < self.best_loss - self.min_delta:
# 개선됨: 카운터 리셋, 모델 저장
self.best_loss = val_loss
self.counter = 0
self.best_model = model.state_dict().copy()
print(f"Validation improved to {val_loss:.4f}")
else:
# 개선 안 됨: 카운터 증가
self.counter += 1
print(f"No improvement for {self.counter} epochs")
if self.counter >= self.patience:
self.early_stop = True
print("Early stopping triggered!")
# 사용 예시
early_stopping = EarlyStopping(patience=5)
for epoch in range(100): # 최대 100 에폭
train_loss = train_one_epoch(model, train_loader)
val_loss = validate(model, val_loader)
# Early Stopping 체크
early_stopping(val_loss, model)
if early_stopping.early_stop:
print(f"Stopped at epoch {epoch}")
model.load_state_dict(early_stopping.best_model)
break
설명
이것이 하는 일: Early Stopping은 매 에폭마다 검증 손실을 모니터링하고, patience 에폭 동안 개선이 없으면 학습을 종료합니다. 첫 번째로, 각 에폭이 끝날 때마다 현재 검증 손실과 지금까지의 최고 기록(best_loss)을 비교합니다.
만약 현재 손실이 더 낮으면(개선됨) best_loss를 업데이트하고 카운터를 0으로 리셋합니다. 이때 모델의 가중치도 저장해둡니다.
그 다음으로, 만약 개선되지 않았다면 counter를 1 증가시킵니다. 이 counter가 patience에 도달하면 early_stop 플래그를 True로 설정합니다.
예를 들어 patience=5라면, 5 에폭 연속으로 개선이 없을 때 멈춥니다. 마지막으로, early_stop이 True가 되면 for 루프를 break하여 학습을 종료하고, 저장해둔 최고 성능 모델(best_model)을 불러옵니다.
이렇게 하면 과적합이 시작되기 직전의 모델을 사용하게 됩니다. 여러분이 이 코드를 사용하면 밤새 학습시켜놓고 아침에 와서 확인할 필요 없이, 최적 시점에 자동으로 멈춰서 최고의 모델을 얻을 수 있습니다.
실무에서는 PyTorch Lightning이나 Keras의 내장 EarlyStopping 콜백을 사용하면 더 간편합니다.
실전 팁
💡 patience는 전체 에폭의 1020% 정도로 설정하세요. 50 에폭 계획이면 patience=510이 적당합니다.
💡 min_delta를 설정하면 미세한 개선(예: 0.0001)은 무시하고 의미있는 개선만 인정합니다.
💡 학습 초기에는 손실이 요동칠 수 있으므로, 최소 10~20 에폭은 학습한 후 Early Stopping을 적용하세요.
💡 ReduceLROnPlateau와 함께 사용하면 효과적입니다. Learning Rate를 줄여도 개선이 없으면 멈추는 전략이죠.
💡 검증 데이터가 너무 작으면 노이즈가 많아 Early Stopping이 너무 일찍 작동할 수 있습니다. 전체의 10~20%는 확보하세요.
8. Learning Rate Finder
시작하며
여러분이 새로운 데이터셋이나 모델로 학습을 시작할 때 "Learning Rate를 얼마로 설정해야 하지?"라는 고민에 빠진 적 있나요? 0.001로 시작했는데 너무 느리거나, 0.1로 했는데 발산하는 상황 말이죠.
이런 문제는 실제 개발 현장에서 매번 새로운 작업을 시작할 때마다 발생합니다. 논문에 나온 Learning Rate를 그대로 사용하면 내 데이터나 모델에는 맞지 않을 수 있습니다.
너무 작으면 학습에 며칠이 걸리고, 너무 크면 수렴하지 못합니다. 바로 이럴 때 필요한 것이 Learning Rate Finder입니다.
자동으로 최적의 Learning Rate 범위를 찾아주어 시행착오를 크게 줄일 수 있습니다.
개요
간단히 말해서, Learning Rate Finder는 Learning Rate를 점진적으로 증가시키면서 손실 변화를 관찰하여 최적의 Learning Rate를 찾는 기법입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하자면, 각 모델과 데이터셋마다 적절한 Learning Rate가 다릅니다.
ResNet50과 VGG16은 같은 이미지 분류 작업을 해도 최적 Learning Rate가 다를 수 있습니다. Learning Rate Finder는 실제 데이터로 짧게 학습해보면서 어떤 Learning Rate에서 손실이 가장 빠르게 줄어드는지 자동으로 찾아줍니다.
예를 들어, 0.00001부터 1.0까지 지수적으로 증가시키면서 손실을 기록하면 최적 범위가 그래프로 보입니다. 기존에는 여러 값을 직접 실험해야 했다면, 이제는 한 번의 짧은 실행으로 최적 범위를 알 수 있습니다.
Learning Rate Finder의 핵심 특징은 세 가지입니다. 첫째, 매우 작은 Learning Rate부터 큰 값까지 지수적으로 증가시키며, 둘째, 손실이 가장 빠르게 감소하는 지점을 찾고, 셋째, 몇 백 번의 iteration만으로 빠르게 결과를 얻을 수 있습니다.
이러한 특징들이 새로운 작업을 시작할 때 초기 설정 시간을 크게 단축시킵니다.
코드 예제
import torch
import matplotlib.pyplot as plt
import numpy as np
def find_lr(model, train_loader, optimizer, criterion,
start_lr=1e-7, end_lr=1, num_iter=100):
"""Learning Rate Finder 구현"""
lrs = []
losses = []
# Learning Rate를 지수적으로 증가
lr_mult = (end_lr / start_lr) ** (1 / num_iter)
lr = start_lr
for i, (inputs, targets) in enumerate(train_loader):
if i >= num_iter:
break
# 현재 Learning Rate 설정
for param_group in optimizer.param_groups:
param_group['lr'] = lr
# 한 스텝 학습
optimizer.zero_grad()
outputs = model(inputs)
loss = criterion(outputs, targets)
loss.backward()
optimizer.step()
# 기록
lrs.append(lr)
losses.append(loss.item())
# Learning Rate 증가
lr *= lr_mult
# 그래프 그리기
plt.plot(lrs, losses)
plt.xscale('log')
plt.xlabel('Learning Rate')
plt.ylabel('Loss')
plt.title('Learning Rate Finder')
plt.show()
# 손실이 가장 빠르게 감소하는 지점 찾기
min_grad_idx = np.argmin(np.gradient(losses))
return lrs[min_grad_idx]
# 사용 예시
optimal_lr = find_lr(model, train_loader, optimizer, criterion)
print(f"Optimal Learning Rate: {optimal_lr:.6f}")
설명
이것이 하는 일: Learning Rate Finder는 매우 작은 Learning Rate에서 시작하여 각 iteration마다 조금씩 증가시키면서 손실 변화를 추적합니다. 첫 번째로, 시작 Learning Rate(예: 0.0000001)를 설정하고 100~200번의 iteration 동안 지수적으로 증가시킵니다.
지수적 증가란 매번 일정 비율(예: 1.05배)씩 곱하는 것으로, 1e-7 → 1e-6 → 1e-5 → ... → 1e-1 → 1 처럼 넓은 범위를 빠르게 탐색합니다.
그 다음으로, 각 iteration마다 현재 Learning Rate로 한 번의 가중치 업데이트를 수행하고 손실을 기록합니다. Learning Rate가 너무 작으면 손실이 천천히 줄고, 적절한 범위에서는 빠르게 줄어들며, 너무 크면 발산하여 손실이 급증합니다.
마지막으로, Learning Rate(x축)와 손실(y축)을 로그 스케일 그래프로 그립니다. 손실이 가장 가파르게 감소하는 구간이 최적 범위입니다.
보통 손실 기울기(gradient)가 가장 큰 음수인 지점의 Learning Rate를 선택하거나, 그보다 약간 작은 값(1/10)을 사용합니다. 여러분이 이 코드를 사용하면 새로운 프로젝트를 시작할 때마다 몇 분 안에 최적의 Learning Rate를 찾을 수 있습니다.
실무에서는 fastai 라이브러리의 lr_find() 함수나 PyTorch Lightning의 Tuner를 사용하면 더 편리합니다.
실전 팁
💡 그래프에서 손실이 가장 가파르게 내려가는 지점의 1/10을 사용하세요. 너무 경계값에 가까우면 불안정합니다.
💡 학습 시작 전 한 번만 실행하세요. 매 에폭마다 할 필요는 없습니다.
💡 num_iter는 100~200이면 충분합니다. 너무 많으면 시간만 오래 걸립니다.
💡 손실 그래프가 U자 형태가 아니라 계속 감소하면 end_lr을 더 크게 설정하세요.
💡 Adam 옵티마이저보다 SGD에서 더 명확한 패턴이 나타납니다. 둘 다 시도해보세요.