트랜스포머·PEFT 2일 과정9장 · 모델 평가와 활용, QLoRA17 / 20← 이전목차다음 →MetaContents

9장. 모델 평가와 활용, QLoRA

실습 5 — QLoRA 로 같은 학습 해 보기

4비트로 불러온 기반 모델에 같은 데이터, 같은 LoRA 설정으로 학습하고 LoRA 결과와 견줍니다. 6~8차시의 준비 코드를 먼저 실행해 둡니다(8장의 sft_config, lora_config, eval_log 와 실습 4의 bnb_config 를 씁니다). 학습에 이 PC 에서 약 5분이 걸립니다.

코드를 단계별로 따라가기 전에 실행 흐름을 한 장으로 봅니다. 8장 실습 3의 흐름과 거의 같고, 2단계에서 4비트 모델을 준비하는 두 줄이 다릅니다.

실습 5 코드의 실행 흐름도. 1단계에서 sft_config 를 깊은 복사해 저장 폴더만 바꾸고, 2단계에서 4비트 기반 모델을 불러와 prepare_model_for_kbit_training 으로 준비한 뒤 SFTTrainer 를 만든다. 3단계에서 q_trainer.train 으로 3 epoch 99 step 을 04:46 에 마치고, 학습 기록 중 eval_loss 가 있는 것만 골라 LoRA 3.209 와 QLoRA 3.298 을 견준 뒤 답을 하나 받아 보고, 4단계에서 GPU 를 비운다
단계하는 일반복과 조건핵심 한 줄
1LoRA 학습 설정을 복사하고 저장 폴더만 바꾸기없음q_sft_config = copy.deepcopy(sft_config)
24비트 모델을 학습용으로 준비하고 Trainer 만들기없음q_model = prepare_model_for_kbit_training(q_model)
3학습하고 LoRA 와 견주기q_trainer.train() 안의 3 epoch, 99 step 은 Trainer 가 안에서 도는 반복. 코드의 반복은 log_history 를 훑는 리스트 내포와 if "eval_loss" in historyq_trainer.train()
4Trainer 와 모델을 지워 GPU 비우기없음del q_trainer, q_model
  • 흐름도 읽는 법: 실습 1 과 같습니다. q_trainer.train() 은 99 step 의 반복을 품고 있지만 코드에 보이지 않으므로 네모 하나로 그렸습니다.
  • 고리와 마름모: 3단계의 고리는 학습 기록을 하나씩 보며 "검증 손실이 있나?"를 묻고, "예"일 때만 qlora_eval_losses 에 담습니다. 담긴 값은 3.241, 3.217, 3.298 세 개이고 마지막 값을 표에 씁니다.
  • 8차시 LoRA 학습과 같은 조건: 1단계의 깊은 복사 덕분에 배치, 학습률, epoch 이 LoRA 와 같습니다. 다른 것은 기반 모델이 4비트라는 것뿐이라, 3.209 대 3.298 의 차이를 4비트 탓으로 읽을 수 있습니다.

1단계. LoRA 학습 설정 복사하기

지난번 연수 계획표를 복사해 장소(저장 폴더)만 바꿉니다. 조건이 같아야 결과를 공정하게 견줄 수 있습니다.

Python
# LoRA 학습 조건을 복사하고 QLoRA 결과의 저장 경로만 바꿉니다.
# copy = 객체를 복사하는 표준 도구
import copy  # 학습 설정을 안전하게 복사합니다.

# deepcopy = 안쪽까지 통째로 복사. 그냥 대입하면 같은 설정표를 함께 써서 원본(sft_config)까지 바뀐다
q_sft_config = copy.deepcopy(sft_config)  # LoRA와 같은 학습 조건입니다.
# 배치 1, 그래디언트 누적 8, 학습률 2e-4, 3 epoch 등은 그대로 두고 저장 폴더만 바꾼다
q_sft_config.output_dir = "outputs/persona-qlora"  # QLoRA 결과는 다른 폴더에 저장합니다.

2단계. 4비트 모델에 LoRA 를 붙일 Trainer 만들기

압축해 둔 교과서(4비트 기반 모델)에 메모 필름(LoRA)을 붙일 준비를 합니다.

Python · GPU 필요 (출력은 RTX 3080 에서 실행한 결과)
# 4비트로 압축한 기반 모델에 LoRA를 붙여 학습합니다.
# prepare_model_for_kbit_training = k비트(4·8비트) 모델을 학습할 수 있게 준비하는 함수
from peft import prepare_model_for_kbit_training  # 양자화 모델을 학습용으로 준비합니다.

# 실습 4와 같은 방법으로 4비트 기반 모델을 GPU 0번에 올린다
q_model = load_base_model(dtype=MODEL_DTYPE, quantization_config=bnb_config, device_map={"": 0})  # 4비트 기반 모델을 불러옵니다.
# 정규화층을 FP32 로 두고, 입력 임베딩에서 그래디언트가 흐르게 설정한다
q_model = prepare_model_for_kbit_training(q_model)  # 양자화 모델을 학습 가능하게 준비합니다.

# SFTTrainer = supervised fine-tuning trainer(모범 답안 학습 담당). 8차시 셀과 인자가 같고 model 과 args 만 다르다
q_trainer = SFTTrainer(  # 4비트 기반 모델용 Trainer를 만듭니다.
    model=q_model,  # 양자화한 기반 모델입니다.
    args=q_sft_config,  # 바로 위 셀의 QLoRA 설정입니다.
    # train 263개로 학습, valid 32개로 epoch 마다 검증
    train_dataset=persona_ds["train"],  # 학습 데이터입니다.
    eval_dataset=persona_ds["valid"],  # 검증 데이터입니다.
    processing_class=tokenizer,  # 템플릿 적용과 토큰화를 맡습니다.
    # peft_config=lora_config: r=16, lora_alpha=32, 7개 선형층. SFTTrainer 가 LoRA 를 붙여 준다
    peft_config=lora_config,  # 같은 LoRA 설정을 붙입니다.
)

3단계. QLoRA 학습하고 LoRA 와 견주기

압축 교과서로 같은 연수를 받게 하고, 연수 성적(마지막 검증 손실)을 지난번 성적과 나란히 놓습니다.

Python · GPU 필요 (출력은 RTX 3080 에서 실행한 결과)
# train() = 학습 시작. 99단계(263개 ÷ 8 ≈ 33단계 × 3 epoch). 결과: 표에 epoch 별 손실이 찍힌다
q_trainer.train()  # 4비트 기반 모델에 LoRA를 학습합니다.

# log_history: 학습 중 기록된 사전들의 리스트. eval_loss 가 있는 기록만 골라 검증 손실을 꺼낸다
qlora_eval_losses = [history["eval_loss"] for history in q_trainer.state.log_history if "eval_loss" in history]  # QLoRA 검증 손실 목록입니다.
summary = pd.DataFrame({  # 두 방식의 마지막 검증 손실을 비교합니다.
    # eval_log[-1][1]: 8차시 eval_log 의 마지막 (epoch, 손실) 중 손실
    "LoRA": [round(eval_log[-1][1], 3)],  # 5.5절에서 기록한 LoRA의 마지막 검증 손실입니다.
    "QLoRA": [round(qlora_eval_losses[-1], 3)],  # 4비트 QLoRA 결과입니다.
}, index=["마지막 검증 손실"])
# display = 노트북 전용 함수(표를 화면에 보여 준다). 결과: LoRA 3.209, QLoRA 3.298
display(summary)
# q_trainer.model: 학습이 끝난 4비트 기반 + LoRA 모델
# 결과: QLoRA 학습 후: 체크리스트에 휘둘리는 거다. 네 속은 무의미하다.
print("QLoRA 학습 후:", chat(q_trainer.model, "업무 실수를 줄이려면 체크리스트를 쓰는 게 좋을까?"))
출력
The tokenizer has new PAD/BOS/EOS tokens that differ from the model config and generation config. The model config and generation config were aligned accordingly, being updated with the tokenizer's values. Updated tokens: {'bos_token_id': None, 'pad_token_id': 151643}.
[99/99 04:46, Epoch 3/3]
Epoch  Training Loss  Validation Loss   Entropy    Num Tokens  Mean Token Accuracy
    1       3.185200         3.241152  3.479087  18743.000000             0.386543
    2       2.478100         3.217460  3.075874  37486.000000             0.399090
    3       1.897700         3.297643  2.930709  56229.000000             0.406859
            LoRA  QLoRA
마지막 검증 손실  3.209  3.298
QLoRA 학습 후: 체크리스트에 휘둘리는 거다. 네 속은 무의미하다.

첫 줄의 안내는 토크나이저의 패딩 토큰 번호(151643)를 모델 설정에 맞췄다는 알림이고, 8장 학습 때도 똑같이 나왔습니다. 학습은 99단계에 4분 46초가 걸렸습니다. LoRA 의 3분 11초보다 약 1.5배 깁니다. 검증 손실은 3.241 → 3.217 → 3.298 로 2 epoch 에서 가장 낮고, 학습 손실은 1.90 까지 계속 내려갑니다. LoRA 때와 같은 과적합 신호입니다. 마지막 검증 손실은 QLoRA 3.298, LoRA 3.209 로 QLoRA 가 조금 높지만, 답은 같은 페르소나 말투("네 속은 무의미하다.")입니다.

4단계. QLoRA 모델 정리하기

연수가 끝난 교실을 정리합니다. 다음 작업이 GPU 메모리를 쓸 수 있게 비웁니다.

Python · GPU 필요 (출력은 RTX 3080 에서 실행한 결과)
# 이름을 지운다. 4비트 모델과 Trainer(옵티마이저 상태 포함)가 GPU 메모리를 붙잡고 있다
del q_trainer, q_model  # 양자화 모델과 Trainer 참조를 지웁니다.
gc.collect()  # 파이썬 객체를 정리합니다.
torch.cuda.empty_cache()  # GPU 캐시를 비웁니다.

이 실습이 뜻하는 것

압축 교과서(4비트)로도 같은 연수를 마칠 수 있다는 것을 확인했습니다. 성적은 조금 낮고 시간은 조금 더 들었지만, 말투는 똑같이 바뀌었습니다. 0.5B 모델은 원래 작아서 압축 이득이 크지 않습니다. 7B, 13B, 70B 처럼 16비트로는 GPU 에 올리기 어려운 모델에서 QLoRA 가 진가를 냅니다.

단계출력뜻
1단계없음같은 학습 조건을 복사하고 저장 폴더만 바꿨다
2단계없음4비트 기반 모델에 LoRA 를 붙일 Trainer 를 만들었다
3단계99/99, 04:46, 검증 손실 3.241 → 3.217 → 3.298학습은 되었고 2 epoch 이 가장 좋았다
3단계LoRA 3.209 대 QLoRA 3.2984비트라 조금 손해, 말투는 같다
4단계없음GPU 메모리를 비웠다

한 줄 요약: QLoRA 는 메모리를 아끼는 대신 속도와 정확도를 조금 내줍니다. 큰 모델일수록 남는 장사입니다.

코드 설명

  • copy.deepcopy(sft_config) 로 설정을 통째로 복사했습니다. 그냥 q_sft_config = sft_config 로 쓰면 output_dir 을 바꿀 때 LoRA 설정도 함께 바뀝니다.
  • prepare_model_for_kbit_training 은 정규화층을 FP32 로 두고 입력 임베딩에서 그래디언트가 흐르게 해, 4비트 모델에서도 학습이 안정되게 합니다.
  • SFTTrainer 의 인자는 8장 셀과 같습니다. peft_config=lora_config 를 넘기면 Trainer 가 4비트 모델 위에 LoRA 층을 붙입니다. 학습되는 것은 BF16/FP32 의 LoRA 행렬뿐입니다.
  • q_trainer.state.log_history 에서 eval_loss 가 있는 기록만 골라 마지막 값을 썼습니다. 8장의 eval_log 도 같은 방법으로 만든 리스트입니다.
  • 학습이 끝나면 del 로 Trainer 와 모델을 함께 지워야 합니다. Trainer 안에 모델과 옵티마이저 상태가 남아 있기 때문입니다.
← 이전 절9장 · 실습 4 — 정밀도별 메모리 비교하기다음 절 →9장 · 프로젝트 1 — 어댑터 점검표 만들기