이미지 로딩 중...
AI Generated
2025. 11. 20. · 2 Views
하이퍼파라미터 설정과 최적화 완벽 가이드
AI 모델 학습의 핵심인 하이퍼파라미터 설정 방법을 초급자도 이해할 수 있도록 쉽게 설명합니다. Learning Rate부터 Mixed Precision Training까지, 실무에서 바로 적용할 수 있는 최적화 전략을 단계별로 배워보세요.
목차
- Learning Rate 설정 (1e-4 ~ 5e-5 범위)
- Batch Size와 Gradient Accumulation
- Warmup Steps와 Scheduler 선택
- LoRA Rank/Alpha 튜닝 가이드
- Max Steps vs Epochs 결정
- Mixed Precision Training (FP16/BF16)
1. Learning Rate 설정 (1e-4 ~ 5e-5 범위)
시작하며
여러분이 AI 모델을 처음 학습시킬 때 이런 상황을 겪어본 적 있나요? 학습을 시작했는데 Loss 값이 전혀 줄어들지 않거나, 반대로 갑자기 튀면서 학습이 망가지는 경우 말이죠.
이런 문제는 실제 개발 현장에서 가장 자주 발생하는 실수 중 하나입니다. Learning Rate(학습률)을 잘못 설정하면 모델이 제대로 학습하지 못하거나, 심지어 며칠간의 학습 시간과 비용을 날릴 수도 있습니다.
바로 이럴 때 필요한 것이 적절한 Learning Rate 설정입니다. 마치 자전거를 탈 때 속도를 적절히 조절하는 것처럼, 모델이 가장 효율적으로 학습할 수 있는 속도를 찾아주는 것이죠.
개요
간단히 말해서, Learning Rate는 모델이 한 번에 얼마나 크게 변화할지를 결정하는 속도 조절기입니다. 왜 이것이 중요할까요?
너무 크면 모델이 최적의 지점을 계속 지나쳐버리고, 너무 작으면 학습이 너무 느리거나 아예 멈춰버립니다. 예를 들어, GPT나 BERT 같은 대형 언어 모델을 파인튜닝할 때 Learning Rate를 잘못 설정하면 기존에 학습된 지식을 모두 잊어버리는 "Catastrophic Forgetting" 현상이 발생할 수 있습니다.
전통적인 딥러닝에서는 1e-3 정도의 Learning Rate를 사용했다면, 최신 Pre-trained 모델들은 1e-4에서 5e-5 사이의 훨씬 작은 값을 사용합니다. 이미 충분히 학습된 모델이기 때문에 아주 조금씩만 조정해야 하기 때문이죠.
대형 언어 모델 파인튜닝의 핵심 특징은 첫째, 작은 Learning Rate 사용(1e-4 ~ 5e-5), 둘째, Warmup 단계 활용, 셋째, 점진적 감소 전략입니다. 이러한 특징들이 모델의 안정적인 학습과 성능 향상을 보장합니다.
코드 예제
from transformers import TrainingArguments
# Learning Rate 설정 예시
training_args = TrainingArguments(
output_dir="./results",
# 추천: 1e-4 (큰 모델) ~ 5e-5 (작은 모델)
learning_rate=5e-5,
# Warmup으로 천천히 시작
warmup_steps=100,
# Cosine Scheduler로 점진적 감소
lr_scheduler_type="cosine",
# Learning Rate 끝값 설정
warmup_ratio=0.1,
)
# 수동으로 Learning Rate 설정하기
optimizer = torch.optim.AdamW(
model.parameters(),
lr=5e-5, # 기본 Learning Rate
betas=(0.9, 0.999), # Adam 파라미터
eps=1e-8, # 안정성을 위한 작은 값
weight_decay=0.01 # 과적합 방지
)
설명
이것이 하는 일: Learning Rate는 모델의 가중치를 업데이트할 때 한 번에 얼마나 큰 폭으로 변경할지를 결정합니다. 마치 산을 내려갈 때 한 걸음의 크기를 정하는 것과 같습니다.
첫 번째로, learning_rate=5e-5 부분은 기본 학습 속도를 설정합니다. 5e-5는 0.00005를 의미하며, 이는 매우 작은 값입니다.
왜 이렇게 작을까요? GPT나 BERT 같은 모델은 이미 방대한 데이터로 학습된 상태이기 때문에, 너무 큰 변화를 주면 기존 지식을 잊어버릴 수 있습니다.
그 다음으로, warmup_steps=100이 실행되면서 처음 100 스텝 동안은 Learning Rate를 0에서 시작해 점진적으로 5e-5까지 증가시킵니다. 내부에서는 매 스텝마다 Learning Rate가 조금씩 커지면서 모델이 급격한 변화에 적응할 시간을 갖게 됩니다.
이는 학습 초기의 불안정성을 크게 줄여줍니다. lr_scheduler_type="cosine"은 Warmup 이후 학습이 진행되면서 Learning Rate를 코사인 함수 곡선처럼 부드럽게 감소시킵니다.
마지막으로, 학습 후반부에는 아주 작은 Learning Rate로 미세 조정을 하여 최종적으로 최적의 성능을 만들어냅니다. 여러분이 이 설정을 사용하면 학습 초기의 불안정성을 방지하고, 중반부에는 효율적으로 학습하며, 후반부에는 정밀한 최적화를 수행할 수 있습니다.
실무에서는 GPU 메모리 절약, 학습 시간 단축, 그리고 더 나은 최종 성능이라는 세 가지 이점을 동시에 얻을 수 있습니다.
실전 팁
💡 큰 모델(7B 이상)은 1e-4, 작은 모델(1B 이하)은 5e-5로 시작하세요. 모델 크기에 따라 Learning Rate를 조절하는 것이 안정적인 학습의 첫걸음입니다.
💡 학습 중 Loss가 갑자기 튀면 Learning Rate가 너무 큰 것입니다. 즉시 1/10로 줄이고 해당 체크포인트부터 다시 시작하세요.
💡 Validation Loss가 개선되지 않으면 Learning Rate를 절반으로 줄여보세요. 많은 경우 이것만으로도 성능이 크게 향상됩니다.
💡 여러 Learning Rate를 동시에 실험하려면 Wandb나 TensorBoard로 Loss 그래프를 비교하세요. 시각적으로 보면 어떤 값이 최적인지 금방 알 수 있습니다.
💡 파인튜닝 시 Pre-training보다 10배 작은 Learning Rate를 사용하는 것이 일반적입니다. 이미 학습된 지식을 보존하면서 새로운 태스크에 적응시킬 수 있습니다.
2. Batch Size와 Gradient Accumulation
시작하며
여러분이 GPU에서 모델을 학습시킬 때 이런 에러를 본 적 있나요? "CUDA out of memory" - GPU 메모리 부족 에러 말이죠.
또는 Batch Size를 줄였더니 학습이 너무 불안정해지는 경험을 하셨을 겁니다. 이런 문제는 GPU 메모리 한계와 최적의 Batch Size 사이의 트레이드오프 때문에 발생합니다.
큰 Batch Size는 학습을 안정적으로 만들지만 메모리를 많이 사용하고, 작은 Batch Size는 메모리는 적게 쓰지만 학습이 불안정해집니다. 바로 이럴 때 필요한 것이 Gradient Accumulation입니다.
작은 Batch로 여러 번 계산한 결과를 모아서 마치 큰 Batch처럼 동작하게 만드는 똑똑한 방법이죠.
개요
간단히 말해서, Batch Size는 한 번에 처리하는 데이터 개수이고, Gradient Accumulation은 여러 번의 작은 배치를 누적해서 큰 배치처럼 만드는 기술입니다. 왜 이것이 필요할까요?
최신 LLM들은 수십 GB의 메모리를 필요로 합니다. 예를 들어, LLaMA 7B 모델을 파인튜닝할 때 Batch Size 8을 사용하면 24GB GPU 메모리가 필요하지만, Batch Size 1에 Gradient Accumulation 8을 사용하면 같은 효과를 내면서도 8GB 메모리로도 충분합니다.
기존에는 큰 GPU를 여러 개 사용해야만 대형 모델을 학습할 수 있었다면, 이제는 Gradient Accumulation을 활용해 일반 GPU 한 개로도 같은 결과를 얻을 수 있습니다. 핵심 특징은 첫째, 효과적인 Batch Size = per_device_batch_size × gradient_accumulation_steps × GPU 개수, 둘째, 메모리 사용량은 실제 배치 크기에만 의존, 셋째, 학습 안정성은 효과적인 배치 크기에 의존합니다.
이러한 특징들이 제한된 하드웨어에서도 대형 모델 학습을 가능하게 만듭니다.
코드 예제
from transformers import TrainingArguments
# Gradient Accumulation 설정
training_args = TrainingArguments(
output_dir="./results",
# GPU당 실제 배치 크기 (메모리에 맞게 설정)
per_device_train_batch_size=1,
# 그래디언트 누적 스텝 수
gradient_accumulation_steps=8,
# 효과적인 배치 크기 = 1 × 8 = 8
# 그래디언트 체크포인팅으로 메모리 추가 절약
gradient_checkpointing=True,
# 최대 그래디언트 노름 (안정성)
max_grad_norm=1.0,
)
# 실제 학습 루프에서의 동작
# for step in range(total_steps):
# for i in range(gradient_accumulation_steps):
# loss = model(batch[i])
# loss.backward() # 그래디언트 누적
# optimizer.step() # 누적된 그래디언트로 업데이트
# optimizer.zero_grad() # 초기화
설명
이것이 하는 일: Batch Size와 Gradient Accumulation은 함께 작동하여 메모리 효율성과 학습 안정성의 균형을 맞춥니다. 작은 배치로 계산하되, 여러 번 누적해서 큰 배치의 효과를 얻는 것이죠.
첫 번째로, per_device_train_batch_size=1은 GPU 메모리에 한 번에 1개의 샘플만 올립니다. 이렇게 하면 메모리 사용량을 최소화할 수 있습니다.
왜 이렇게 작게 설정할까요? 대형 모델은 모델 자체가 메모리를 많이 차지하기 때문에, 배치 크기를 줄여야 메모리 부족 에러를 피할 수 있습니다.
그 다음으로, gradient_accumulation_steps=8이 실행되면서 8번의 forward-backward 계산을 수행하지만, 실제로 가중치를 업데이트하는 것은 8번이 모두 끝난 후 단 한 번만 합니다. 내부에서는 각 스텝의 그래디언트가 더해지면서 마치 배치 크기 8로 학습하는 것과 동일한 효과를 냅니다.
gradient_checkpointing=True는 메모리를 더욱 절약하기 위한 추가 기법입니다. Forward 계산 시 중간 결과를 저장하지 않고 필요할 때 다시 계산하는 방식으로, 속도는 약간 느려지지만 메모리를 30-40% 절약할 수 있습니다.
마지막으로, max_grad_norm=1.0이 그래디언트가 너무 커지는 것을 방지하여 학습의 안정성을 높입니다. 여러분이 이 설정을 사용하면 16GB GPU로도 7B 모델을 학습할 수 있고, 학습 안정성도 큰 배치와 동일하게 유지하며, 메모리 부족 에러 없이 안정적인 학습을 진행할 수 있습니다.
실무에서는 비싼 A100 GPU 대신 저렴한 RTX 3090으로도 충분히 학습할 수 있어 비용을 크게 절감할 수 있습니다.
실전 팁
💡 효과적인 배치 크기는 8-32 사이가 이상적입니다. per_device_batch_size × gradient_accumulation_steps가 이 범위에 들도록 조절하세요.
💡 메모리 에러가 나면 먼저 batch_size를 줄이고, 그만큼 gradient_accumulation_steps를 늘려서 효과적인 배치 크기를 유지하세요.
💡 gradient_checkpointing을 켜면 메모리를 30-40% 절약하지만 학습 속도가 20% 정도 느려집니다. 메모리가 부족할 때만 사용하세요.
💡 다중 GPU 학습 시 효과적인 배치 크기 = batch_size × accumulation_steps × GPU 개수입니다. 4개 GPU라면 accumulation_steps를 1/4로 줄일 수 있습니다.
💡 배치 크기가 너무 작으면 (effective batch size < 4) Batch Normalization 계열 레이어가 불안정해집니다. Layer Normalization을 사용하는 Transformer는 괜찮지만, CNN 계열은 주의하세요.
3. Warmup Steps와 Scheduler 선택
시작하며
여러분이 모델 학습을 시작한 첫 몇 분 동안 Loss가 급격히 튀거나, 학습 초반에는 잘 되다가 중반 이후 개선이 멈춘 경험이 있나요? 이는 Learning Rate를 처음부터 끝까지 고정값으로 사용했기 때문일 가능성이 높습니다.
이런 문제는 학습의 각 단계마다 필요한 Learning Rate가 다르다는 점을 간과했기 때문에 발생합니다. 초반에는 큰 변화가 필요하지만 안정성이 중요하고, 후반에는 미세 조정이 필요합니다.
바로 이럴 때 필요한 것이 Warmup과 Learning Rate Scheduler입니다. 학습의 각 단계에 맞게 Learning Rate를 자동으로 조절해서 최적의 성능을 끌어내는 전략이죠.
개요
간단히 말해서, Warmup은 학습 초반에 Learning Rate를 천천히 증가시키는 기법이고, Scheduler는 학습 진행에 따라 Learning Rate를 자동으로 조절하는 전략입니다. 왜 이것이 필요할까요?
학습 시작 시점에서 모델의 가중치는 무작위이거나 Pre-trained 상태입니다. 이때 갑자기 큰 Learning Rate로 업데이트하면 기존 지식이 깨지거나 학습이 불안정해집니다.
예를 들어, BERT를 파인튜닝할 때 Warmup 없이 시작하면 처음 몇 스텝에서 Pre-training으로 얻은 언어 이해 능력이 손상될 수 있습니다. 전통적인 딥러닝에서는 고정 Learning Rate나 단순 Step Decay를 사용했다면, 최신 Transformer 모델들은 Warmup + Cosine/Linear Decay 조합을 표준으로 사용합니다.
이는 수많은 실험을 통해 검증된 최적의 전략입니다. 핵심 특징은 첫째, Warmup으로 안정적인 학습 시작, 둘째, Cosine/Linear Scheduler로 점진적 감소, 셋째, 전체 학습 스텝의 5-10%를 Warmup에 할당합니다.
이러한 특징들이 모델이 Pre-trained 지식을 유지하면서도 새로운 태스크에 효과적으로 적응하도록 만듭니다.
코드 예제
from transformers import TrainingArguments, get_scheduler
import torch
training_args = TrainingArguments(
output_dir="./results",
# 전체 학습 스텝
max_steps=1000,
# Warmup 스텝: 전체의 10%
warmup_steps=100,
# 또는 비율로 설정
# warmup_ratio=0.1,
# Scheduler 타입 선택
lr_scheduler_type="cosine", # cosine, linear, polynomial 등
learning_rate=5e-5,
)
# 수동으로 Scheduler 설정하기
optimizer = torch.optim.AdamW(model.parameters(), lr=5e-5)
# Warmup + Cosine Decay Scheduler
scheduler = get_scheduler(
name="cosine", # 또는 "linear", "polynomial"
optimizer=optimizer,
num_warmup_steps=100, # Warmup 기간
num_training_steps=1000, # 전체 학습 스텝
)
# 학습 루프에서 사용
# for epoch in range(num_epochs):
# for batch in dataloader:
# loss = model(batch)
# loss.backward()
# optimizer.step()
# scheduler.step() # 매 스텝마다 LR 업데이트
# optimizer.zero_grad()
설명
이것이 하는 일: Warmup과 Scheduler는 학습의 각 단계에서 최적의 Learning Rate를 제공하여 안정성과 성능을 동시에 확보합니다. 마치 자동차가 출발할 때 천천히 가속하고, 목적지에 가까워지면 속도를 줄이는 것과 같습니다.
첫 번째로, warmup_steps=100 부분은 처음 100 스텝 동안 Learning Rate를 0에서 시작해 설정한 최대값(5e-5)까지 선형적으로 증가시킵니다. 왜 이렇게 할까요?
학습 초기에는 그래디언트의 방향이 불안정하기 때문에, 작은 Learning Rate로 시작해서 모델이 안정적인 방향을 찾도록 도와줍니다. 그 다음으로, Warmup이 끝나면 lr_scheduler_type="cosine"이 작동하면서 Learning Rate를 부드러운 코사인 곡선을 따라 감소시킵니다.
내부에서는 매 스텝마다 현재 진행도(current_step / total_steps)를 계산하고, 코사인 함수를 적용해 Learning Rate를 조절합니다. Linear Scheduler는 직선으로 감소하지만, Cosine은 초반에는 천천히, 후반에는 빠르게 감소해 더 나은 성능을 보입니다.
scheduler.step()은 매 학습 스텝마다 호출되어 Learning Rate를 업데이트합니다. 마지막으로, 전체 학습이 끝날 때쯤이면 Learning Rate가 거의 0에 가까워지면서 최종적으로 모델을 미세 조정하여 최적의 성능을 만들어냅니다.
여러분이 이 설정을 사용하면 학습 초기의 불안정성과 발산을 방지하고, 중반에는 효율적으로 빠르게 학습하며, 후반에는 정밀한 최적화로 최고 성능에 도달할 수 있습니다. 실무에서는 Warmup 덕분에 실패 확률이 크게 줄고, Cosine Scheduler 덕분에 Linear보다 평균 2-5% 더 나은 성능을 얻을 수 있습니다.
실전 팁
💡 Warmup은 전체 학습 스텝의 5-10%가 적절합니다. 너무 길면 학습 시간이 낭비되고, 너무 짧으면 효과가 없습니다.
💡 대부분의 경우 Cosine Scheduler가 Linear보다 성능이 좋습니다. 특별한 이유가 없다면 Cosine을 선택하세요.
💡 작은 데이터셋(< 10K 샘플)에서는 Warmup 비율을 20%까지 늘려도 좋습니다. 데이터가 적을수록 초기 안정성이 더 중요합니다.
💡 학습 중 Loss 그래프를 보면서 Scheduler가 제대로 작동하는지 확인하세요. Learning Rate가 Warmup 후 부드럽게 감소해야 합니다.
💡 Polynomial Scheduler(power=2)는 Cosine과 Linear의 중간 특성을 가집니다. 실험적으로 여러 Scheduler를 비교해보는 것도 좋은 방법입니다.
4. LoRA Rank/Alpha 튜닝 가이드
시작하며
여러분이 수십억 파라미터를 가진 LLaMA나 GPT 모델을 파인튜닝하려고 할 때, GPU 메모리 부족과 엄청난 학습 시간 때문에 좌절한 경험이 있으신가요? 전체 모델을 업데이트하려면 수백 GB의 메모리와 며칠의 시간이 필요합니다.
이런 문제는 대형 모델의 모든 파라미터를 업데이트하려는 Full Fine-tuning 방식의 한계 때문에 발생합니다. 비용도 많이 들고, 시간도 오래 걸리며, 실수로 모델을 망가뜨릴 위험도 큽니다.
바로 이럴 때 필요한 것이 LoRA(Low-Rank Adaptation)입니다. 전체 모델의 1% 미만 파라미터만 학습하면서도 Full Fine-tuning과 거의 같은 성능을 내는 혁신적인 기법이죠.
개요
간단히 말해서, LoRA는 원본 모델은 그대로 두고 작은 "어댑터" 행렬만 추가해서 학습하는 효율적인 파인튜닝 기법입니다. 왜 이것이 필요할까요?
7B 모델을 Full Fine-tuning하면 약 28GB의 GPU 메모리가 필요하지만, LoRA를 사용하면 8GB로도 충분합니다. 예를 들어, LLaMA 7B를 특정 도메인에 적응시킬 때 LoRA를 쓰면 학습 시간을 70% 줄이고, 메모리는 1/4만 사용하면서도 성능은 거의 동일하게 유지할 수 있습니다.
전통적인 방법에서는 모든 레이어의 수십억 파라미터를 업데이트했다면, LoRA는 핵심 Attention 레이어에만 작은 저랭크 행렬을 추가해 수백만 개 파라미터만 학습합니다. 핵심 특징은 첫째, Rank(r)는 어댑터의 크기를 결정(일반적으로 4-64), 둘째, Alpha(α)는 LoRA 업데이트의 영향력을 조절(보통 r의 2배), 셋째, 학습 가능한 파라미터 = 2 × d × r × 적용 레이어 수입니다.
이러한 특징들이 효율성과 성능의 완벽한 균형을 만들어냅니다.
코드 예제
from peft import LoraConfig, get_peft_model, TaskType
# LoRA 설정
lora_config = LoraConfig(
r=8, # LoRA Rank (4, 8, 16, 32, 64 중 선택)
lora_alpha=16, # Alpha 값 (보통 r의 2배)
target_modules=["q_proj", "k_proj", "v_proj", "o_proj"], # Attention 레이어
lora_dropout=0.05, # 과적합 방지
bias="none", # Bias 학습 안 함
task_type=TaskType.CAUSAL_LM, # 태스크 타입
)
# 원본 모델에 LoRA 적용
model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-2-7b-hf")
model = get_peft_model(model, lora_config)
# 학습 가능한 파라미터 확인
model.print_trainable_parameters()
# 출력: trainable params: 4.2M || all params: 6.7B || trainable%: 0.06%
# LoRA 어댑터만 저장 (수 MB)
model.save_pretrained("./lora_adapter")
# 나중에 로드
# model = PeftModel.from_pretrained(base_model, "./lora_adapter")
설명
이것이 하는 일: LoRA는 거대한 모델의 가중치 행렬을 두 개의 작은 행렬(A, B)로 분해해서 학습합니다. 원본 가중치 W는 고정하고, W + A×B만 업데이트하는 방식이죠.
마치 거대한 건물은 그대로 두고 작은 확장 공간만 개조하는 것과 같습니다. 첫 번째로, r=8은 LoRA의 랭크를 설정합니다.
이 값이 클수록 표현력이 좋아지지만 메모리와 시간이 더 필요합니다. 왜 8을 선택할까요?
실험 결과 r=8이면 대부분의 태스크에서 충분한 성능을 내면서도 효율적이기 때문입니다. r=4는 너무 작아 성능이 떨어질 수 있고, r=64는 너무 커서 효율성이 낮아집니다.
그 다음으로, lora_alpha=16이 실행되면서 LoRA 업데이트의 스케일을 조절합니다. 내부적으로는 LoRA의 출력에 alpha/r을 곱해서 최종 업데이트량을 결정합니다.
Alpha가 크면 LoRA의 영향력이 커지고, 작으면 원본 모델을 더 많이 보존합니다. 보통 alpha = 2×r로 설정하면 적절한 균형을 얻을 수 있습니다.
target_modules는 어느 레이어에 LoRA를 적용할지 결정합니다. Transformer의 Attention 레이어(q_proj, k_proj, v_proj, o_proj)가 가장 중요하므로 이들에만 적용합니다.
마지막으로, 학습 후에는 수 MB 크기의 LoRA 어댑터만 저장하면 되므로 최종적으로 모델 공유와 배포가 매우 쉬워집니다. 여러분이 이 설정을 사용하면 16GB GPU로도 7B 모델을 학습할 수 있고, 학습 시간을 70% 이상 단축하며, 여러 태스크별 어댑터를 쉽게 전환할 수 있습니다.
실무에서는 동일한 베이스 모델에 여러 LoRA 어댑터를 붙여 다양한 도메인에 대응할 수 있어 매우 유연한 시스템을 구축할 수 있습니다.
실전 팁
💡 일반적인 태스크는 r=8, alpha=16이 최적입니다. 복잡한 태스크(코드 생성, 추론)는 r=16~32로 높이세요.
💡 메모리가 부족하면 r을 줄이기보다는 target_modules를 ["q_proj", "v_proj"]만 선택하세요. 성능 손실이 적습니다.
💡 여러 LoRA를 실험할 때는 r과 alpha의 비율(alpha/r)을 일정하게 유지하세요. 예: r=4,alpha=8 / r=8,alpha=16 / r=16,alpha=32
💡 LoRA + Gradient Checkpointing + 8bit 양자화를 조합하면 12GB GPU로도 13B 모델을 학습할 수 있습니다.
💡 파인튜닝 후 성능이 부족하면 r을 2배로 늘려보세요. r=8→16으로만 바꿔도 복잡한 태스크에서 5-10% 성능 향상을 얻을 수 있습니다.
5. Max Steps vs Epochs 결정
시작하며
여러분이 모델 학습을 시작할 때 이런 고민을 해본 적 있나요? "3 Epoch 돌릴까, 아니면 10,000 Steps를 설정할까?" 또는 학습을 시작했는데 너무 일찍 끝나거나, 반대로 과적합되어 성능이 떨어지는 경험을 하셨을 겁니다.
이런 문제는 데이터셋 크기와 학습 목표에 맞지 않는 학습 길이 설정 때문에 발생합니다. Epoch는 전체 데이터를 몇 번 반복할지를 의미하고, Steps는 업데이트 횟수를 직접 지정합니다.
각각 장단점이 있죠. 바로 이럴 때 필요한 것이 데이터셋 특성에 맞는 학습 길이 결정 전략입니다.
작은 데이터셋과 큰 데이터셋, Pre-training과 Fine-tuning에 따라 최적의 방법이 다르기 때문입니다.
개요
간단히 말해서, Epochs는 전체 데이터를 반복하는 횟수이고, Max Steps는 총 업데이트 횟수를 직접 제한하는 방식입니다. 왜 이 선택이 중요할까요?
작은 데이터셋(1K 샘플)에서는 Epoch 기반이 적합하지만, 대형 데이터셋(100M 샘플)에서는 Steps 기반이 훨씬 효율적입니다. 예를 들어, GPT-3 Pre-training처럼 수백억 개의 토큰을 사용할 때는 1 Epoch도 완료하기 전에 충분한 학습이 끝나므로 Steps로 제한하는 것이 합리적입니다.
전통적인 머신러닝에서는 고정된 Epoch 수(보통 10-100)를 사용했다면, 최신 LLM 학습에서는 Max Steps를 설정하고 조기 종료(Early Stopping)를 조합하는 방식이 표준입니다. 핵심 특징은 첫째, 작은 데이터셋(< 10K)은 Epochs(3-10), 큰 데이터셋은 Steps(1K-100K), 둘째, Total Steps = (데이터 크기 / Batch Size) × Epochs, 셋째, Evaluation을 주기적으로 수행해 조기 종료 판단입니다.
이러한 특징들이 과적합을 방지하면서도 충분한 학습을 보장합니다.
코드 예제
from transformers import TrainingArguments
# 방법 1: Epochs 기반 (작은 데이터셋)
training_args_epochs = TrainingArguments(
output_dir="./results",
num_train_epochs=3, # 전체 데이터 3번 반복
# max_steps가 없으면 자동으로 계산됨
# total_steps = (dataset_size / batch_size) * epochs
# 조기 종료 설정
evaluation_strategy="epoch",
save_strategy="epoch",
load_best_model_at_end=True, # 최고 성능 모델 저장
metric_for_best_model="eval_loss",
)
# 방법 2: Steps 기반 (큰 데이터셋)
training_args_steps = TrainingArguments(
output_dir="./results",
max_steps=10000, # 정확히 10,000번 업데이트
# num_train_epochs는 무시됨
# 주기적 평가
evaluation_strategy="steps",
eval_steps=500, # 500 스텝마다 평가
save_steps=500,
save_total_limit=3, # 최근 3개 체크포인트만 유지
)
# 실제 계산 예시
# 데이터셋: 10,000 샘플
# Batch size: 8, Gradient accumulation: 4
# Effective batch size = 8 * 4 = 32
# Steps per epoch = 10,000 / 32 = 312.5 ≈ 313
# 3 epochs = 313 * 3 = 939 steps
설명
이것이 하는 일: Epochs와 Steps는 모델이 얼마나 오래 학습할지를 결정하는 두 가지 방법입니다. Epochs는 데이터 중심적이고, Steps는 업데이트 횟수 중심적입니다.
마치 여행을 계획할 때 "3일 동안"이라고 할지, "10개 도시 방문"이라고 할지 선택하는 것과 같습니다. 첫 번째로, num_train_epochs=3 설정은 전체 데이터셋을 정확히 3번 반복합니다.
왜 이것이 작은 데이터셋에 적합할까요? 데이터가 적으면 한 번만 보고는 충분히 학습하기 어렵기 때문에 여러 번 반복해야 합니다.
10,000개 샘플이라면 3-5 Epochs가 적절하고, 1,000개라면 10-20 Epochs도 필요할 수 있습니다. 그 다음으로, max_steps=10000이 실행되면서 정확히 10,000번의 가중치 업데이트 후 학습을 종료합니다.
내부에서는 현재 스텝 카운터가 10,000에 도달하면 즉시 학습을 멈춥니다. 이는 대형 데이터셋에서 유용한데, 예를 들어 100M 샘플을 1 Epoch 돌리면 너무 오래 걸리므로 충분한 Steps만큼만 학습하고 끝내는 것이 효율적입니다.
evaluation_strategy="steps"와 eval_steps=500은 500 스텝마다 검증 세트로 성능을 평가합니다. 마지막으로, load_best_model_at_end=True가 학습 과정에서 가장 좋았던 모델을 최종적으로 선택하여 과적합된 마지막 모델 대신 최적의 성능을 보장합니다.
여러분이 이 설정을 사용하면 데이터셋 크기에 관계없이 최적의 학습 길이를 설정할 수 있고, 과적합을 조기에 감지하여 방지하며, 체크포인트 관리로 디스크 공간도 절약할 수 있습니다. 실무에서는 Steps 기반 + 조기 종료 조합이 가장 안전하고 효율적인 방법으로 널리 사용됩니다.
실전 팁
💡 작은 데이터셋(< 10K): 3-10 Epochs, 중간 데이터셋(10K-100K): 1-3 Epochs, 대형 데이터셋(> 100K): Max Steps 10K-100K 사용하세요.
💡 실험 초기에는 max_steps를 작게(500-1000) 설정해서 빠르게 프로토타입을 만들고, 나중에 본격적으로 늘리세요.
💡 Validation Loss가 3회 연속 개선되지 않으면 학습을 멈추는 Early Stopping 콜백을 추가하세요. 시간과 비용을 크게 절약합니다.
💡 save_total_limit=3으로 설정하면 최근 3개 체크포인트만 유지해 디스크 공간을 절약하면서도 롤백 옵션을 확보할 수 있습니다.
💡 Pre-training은 Steps 기반(수만~수십만), Fine-tuning은 Epochs 기반(3-5)이 일반적입니다. 태스크 특성에 맞게 선택하세요.
6. Mixed Precision Training (FP16/BF16)
시작하며
여러분이 대형 모델을 학습시킬 때 GPU 메모리가 부족하거나, 학습 속도가 너무 느려서 답답한 경험이 있으신가요? 특히 몇 시간씩 걸리는 학습을 기다리다가 지쳐본 적이 있을 겁니다.
이런 문제는 기본적으로 32비트 부동소수점(FP32)을 사용하기 때문에 발생합니다. 높은 정밀도는 좋지만, 메모리를 2배로 사용하고 계산 속도도 느립니다.
최신 GPU는 16비트 연산에 특화되어 있는데 이를 활용하지 못하는 것이죠. 바로 이럴 때 필요한 것이 Mixed Precision Training입니다.
16비트(FP16 또는 BF16)로 학습하면서도 정확도는 유지하고, 속도는 2-3배 빠르게, 메모리는 절반으로 줄이는 혁신적인 기법입니다.
개요
간단히 말해서, Mixed Precision은 학습 중에 32비트와 16비트 연산을 혼합해서 사용하여 속도와 메모리 효율을 높이는 기술입니다. 왜 이것이 필요할까요?
최신 GPU(V100, A100, RTX 30/40 시리즈)는 16비트 연산을 위한 전용 하드웨어(Tensor Core)를 가지고 있어 FP16/BF16 연산이 FP32보다 2-3배 빠릅니다. 예를 들어, A100 GPU에서 BF16을 사용하면 같은 모델을 절반의 메모리로 2배 빠르게 학습할 수 있습니다.
전통적인 방법에서는 모든 계산을 FP32로 수행했다면, 현대적인 방법은 Forward/Backward는 FP16/BF16으로, 가중치 업데이트는 FP32로 수행합니다. 이것이 "Mixed"의 의미입니다.
핵심 특징은 첫째, FP16은 범위가 좁지만 대부분 GPU 지원, BF16은 범위가 넓고 최신 GPU에 최적, 둘째, 메모리 사용량 50% 감소, 학습 속도 2-3배 향상, 셋째, 손실 스케일링(Loss Scaling)으로 언더플로우 방지입니다. 이러한 특징들이 제한된 하드웨어로도 대형 모델을 효율적으로 학습할 수 있게 만듭니다.
코드 예제
from transformers import TrainingArguments
import torch
# 방법 1: FP16 (대부분의 GPU 지원)
training_args_fp16 = TrainingArguments(
output_dir="./results",
fp16=True, # FP16 활성화
fp16_opt_level="O1", # Apex 최적화 레벨 (O0, O1, O2, O3)
# O1: 안전한 Mixed Precision (권장)
# O2: 거의 모든 연산을 FP16으로
)
# 방법 2: BF16 (A100, RTX 30/40 시리즈)
training_args_bf16 = TrainingArguments(
output_dir="./results",
bf16=True, # BF16 활성화
# BF16은 loss scaling 불필요 (범위가 FP32와 동일)
)
# PyTorch 네이티브 Mixed Precision
from torch.cuda.amp import autocast, GradScaler
model = MyModel()
optimizer = torch.optim.AdamW(model.parameters(), lr=5e-5)
scaler = GradScaler() # Loss Scaling
for batch in dataloader:
optimizer.zero_grad()
# FP16으로 Forward/Backward
with autocast():
loss = model(batch)
# Gradient Scaling (언더플로우 방지)
scaler.scale(loss).backward()
scaler.step(optimizer)
scaler.update()
설명
이것이 하는 일: Mixed Precision Training은 계산 집약적인 부분은 16비트로, 정밀도가 중요한 부분은 32비트로 수행하여 속도와 정확도를 모두 잡습니다. 마치 고속도로에서는 빠르게 달리고, 복잡한 도심에서는 천천히 주행하는 것과 같습니다.
첫 번째로, fp16=True 또는 bf16=True 설정은 학습 중 대부분의 연산을 16비트로 수행하도록 만듭니다. 왜 이것이 빠를까요?
16비트 숫자는 32비트의 절반 크기이므로 메모리 대역폭을 절반만 사용하고, GPU의 Tensor Core가 활성화되어 하드웨어 가속을 받기 때문입니다. 그 다음으로, autocast() 컨텍스트 내에서 실행되는 연산들은 자동으로 FP16으로 변환됩니다.
내부에서는 행렬 곱셈, Convolution 같은 연산은 FP16으로 수행하고, Softmax나 Layer Normalization 같이 정밀도가 중요한 연산은 FP32를 유지합니다. 이것이 "Mixed"의 핵심입니다.
GradScaler는 FP16의 작은 수 표현 범위 때문에 발생할 수 있는 언더플로우(너무 작은 값이 0이 되는 현상)를 방지합니다. Loss 값을 크게 스케일링해서 그래디언트를 계산하고, 마지막 업데이트 시에만 다시 원래 크기로 되돌립니다.
마지막으로, 가중치 자체는 항상 FP32로 유지되어 최종적으로 정밀한 학습 결과를 보장합니다. 여러분이 이 설정을 사용하면 동일한 GPU에서 2배 큰 배치 크기를 사용할 수 있고, 학습 시간을 절반 이하로 단축하며, 성능 손실은 거의 없이(< 0.1%) 모든 이점을 누릴 수 있습니다.
실무에서는 Mixed Precision 없이 대형 모델을 학습하는 것은 거의 불가능할 정도로 필수적인 기술이 되었습니다.
실전 팁
💡 A100, H100, RTX 30/40 시리즈는 무조건 BF16을 사용하세요. FP16보다 안정적이고 빠릅니다.
💡 구형 GPU(V100, P100, RTX 20 시리즈)는 BF16을 지원하지 않으므로 FP16을 사용하되, fp16_opt_level="O1"로 설정하세요.
💡 Loss가 NaN이 되면 FP16의 언더플로우 문제입니다. BF16으로 전환하거나, loss scaling factor를 늘리세요.
💡 Mixed Precision + Gradient Accumulation + Gradient Checkpointing을 조합하면 16GB GPU로도 13B 모델을 학습할 수 있습니다.
💡 학습 시작 전 torch.cuda.is_bf16_supported()로 GPU가 BF16을 지원하는지 확인하세요. 지원하지 않는 GPU에서 bf16=True를 설정하면 에러가 발생합니다.