트랜스포머·PEFT 2일 과정8장 · PEFT와 LoRA 학습8 / 18← 이전목차다음 →MetaContents

8장. PEFT와 LoRA 학습

학습 흐름과 SFTConfig

요리 대회에 나간다고 해 봅시다. 재료(데이터)를 준비하고, 조리 도구(기반 모델)를 꺼내고, 어디에 양념할지(LoraConfig)를 정하고, 불 세기와 조리 시간(SFTConfig)을 정한 뒤, 요리사(SFTTrainer)에게 맡깁니다. 요리사는 재료 손질(템플릿 적용, 토큰화, labels 마스킹, 패딩)부터 조리(학습 반복)까지 한 번에 합니다. 7장에서 손으로 해 본 일들을 이제 SFTTrainer 가 대신합니다.

데이터, 모델, LoraConfig, SFTConfig, 학습의 다섯 걸음을 이은 흐름도

그림의 첫 상자는 7장의 persona_ds(학습 263개)입니다. 둘째 상자가 Qwen2.5-0.5B-Instruct, 셋째가 실습 1에서 만든 lora_config 입니다. 넷째 상자의 SFTConfig 가 이 절의 주제이고, 다섯째 상자의 trainer.train() 이 99번 가중치를 고칩니다. 이 학습 데이터는 "냉소적인 절대 군주" 말투로 답합니다. 말투 차이를 눈으로 보기 쉽게 고른 학습용 데이터입니다.

SFTConfig 주요 설정

SFTConfig 는 Hugging Face 의 TrainingArguments(학습 설정표)에 SFT 전용 설정을 더한 클래스입니다. VRAM 8GB 에서도 돌도록 작은 배치에 그래디언트 누적을 씁니다.

설정값뜻요리로 치면
per_device_train_batch_size1한 번에 학습하는 예시 수한 번에 굽는 전 한 장
per_device_eval_batch_size1한 번에 검증하는 예시 수맛보기 한 장씩
gradient_accumulation_steps88번의 그래디언트를 모아 한 번에 고친다8장 맛보고 양념 한 번 조정
learning_rate2e-4LoRA 학습률양념을 한 번에 얼마나 바꿀지
lr_scheduler_typecosine학습률을 코사인 모양으로 줄인다끝으로 갈수록 살살
warmup_ratio0.05처음 5% 스텝은 학습률을 0에서 서서히 올린다처음엔 약불로 예열
max_length256템플릿을 입힌 뒤 최대 토큰 수냄비 크기
completion_only_lossTrue답변 구간만 손실을 잰다손님 주문서는 채점하지 않음
eval_strategyepochepoch 마다 검증한다한 바퀴마다 시식
save_strategyno중간 저장을 하지 않는다중간 사진은 안 찍음
bf16 / fp16GPU 에 맞춰숫자 정밀도계량컵 눈금

completion_only_loss=True 는 7장의 labels 에서 질문 부분을 -100 으로 두던 일과 같습니다. 채점에서 빼는 문제(질문)는 빼고 답만 채점합니다. num_train_epochs 는 적지 않았으므로 기본값 3 을 씁니다.

실질 배치와 스텝 수 손계산

  • 실질 배치 = 배치 1 × 누적 8 = 8. 예시 8개를 보고 가중치를 한 번 고칩니다.
  • epoch 하나의 스텝 수 = 263 ÷ 8 = 32.875 → 올림해서 33 번. 마지막 한 번은 예시 7개로 고칩니다.
  • 전체 스텝 수 = 33 × 3 epoch = 99. 실습 3의 출력 [99/99 03:11, Epoch 3/3] 과 같습니다.
  • warmup 스텝 = 99 × 0.05 = 4.95 → 올림해서 5.

짧은 예시

Python
import math

train_n, batch, accum, epochs = 263, 1, 8, 3   # persona_ds["train"] 263개와 SFTConfig 의 값
effective = batch * accum                      # 실질 배치
steps_per_epoch = math.ceil(train_n / effective)   # ceil = 올림
total = steps_per_epoch * epochs
warmup = math.ceil(total * 0.05)
print(effective, steps_per_epoch, total, warmup)   # 결과: 8 33 99 5

def lr_at(step, peak=2e-4):
    # warmup 동안은 0 에서 peak 까지 곧게 올린다
    if step < warmup:
        return peak * step / warmup
    # 그다음은 코사인 곡선으로 peak 에서 0 까지 내린다
    progress = (step - warmup) / (total - warmup)
    return peak * 0.5 * (1 + math.cos(math.pi * progress))

for s in [0, 1, 5, 33, 52, 66, 99]:
    print(s, f"{lr_at(s):.6f}")
# 결과: 0 0.000000 / 1 0.000040 / 5 0.000200 / 33 0.000159 / 52 0.000100 / 66 0.000055 / 99 0.000000

이 코드가 뜻하는 것

전 263장을 한 장씩 굽되 8장마다 양념을 고치면 몇 번 고치게 되는지, 그리고 불 세기를 어떻게 바꾸는지 미리 계산한 코드입니다. 여기서 전 장수가 train_n, 양념을 고치는 횟수가 total, 불 세기가 lr_at(step) 입니다.

줄하는 일쉽게 말하면
batch * accum1 × 8 = 88장 맛보고 한 번 고치기
math.ceil(train_n / effective)263 ÷ 8 을 올림 = 33한 바퀴에 33번 고침
peak * step / warmup처음 5스텝은 곧게 올림약불 예열
0.5 * (1 + math.cos(...))1 에서 0 으로 부드럽게끝으로 갈수록 살살

52번째 스텝(warmup 뒤 구간의 한가운데)에서 학습률이 0.0001, 꼭 절반이 됩니다. 이 계산은 Hugging Face 의 cosine 스케줄러 식을 따라 쓴 것입니다.

주의할 점

  • 배치 8을 한 번에 넣으면 GPU 메모리가 모자랄 수 있습니다. 배치 1 × 누적 8 은 메모리는 배치 1만큼 쓰면서 고치는 효과는 배치 8과 비슷하게 냅니다. 대신 시간이 더 걸립니다.
  • max_length 보다 긴 예시는 뒤가 잘립니다. 답의 끝(종료 토큰 <|im_end|>)이 잘리면 모델이 답을 끝내는 법을 못 배웁니다. 이 데이터는 대부분 100토큰 안팎이라 256 이면 넉넉합니다.
  • 학습 시간과 손실 값은 GPU 마다, 실행할 때마다 조금씩 달라집니다. 이 장의 숫자는 RTX 3080 에서 한 번 돌린 값입니다.
← 이전 절8장 · Qwen 에 LoRA 붙이기와 하이퍼파라미터다음 절 →8장 · 손실 곡선 읽기