LLM 엔지니어링5장 · LoRA 파인튜닝과 DPO 정렬7 / 23← 이전목차다음 →MetaContents

5장. LoRA 파인튜닝과 DPO 정렬

PEFT 로 LoRA 붙이기

4장의 Trainer 를 세탁기의 "표준 코스"에 견줬습니다. LoRA 학습은 같은 세탁기에 "섬세 코스" 단추 하나를 더 누르는 일입니다. 모델을 불러온 뒤 LoRA 를 붙이는 두 줄만 더하면, 나머지는 4장과 같습니다.

기본 모델 불러오기, LoraConfig, get_peft_model, Trainer 로 학습, 어댑터 저장의 다섯 단계를 이은 흐름도

1번에서 기본 모델을 fp16 으로 불러오고, 2번 LoraConfig 로 설정을 정한 뒤, 3번 get_peft_model 로 붙이면 원래 가중치는 저절로 얼려집니다. 4번은 4장의 Trainer 그대로이고, 5번에서 저장하면 어댑터만 몇 MB 로 저장됩니다. 2번과 3번이 4장에 더해진 두 줄입니다.

단계마다 쓰는 것

단계코드4장과 다른 점
불러오기AutoModelForCausalLM.from_pretrained(이름, dtype=torch.float16)fp16 으로 불러 메모리를 절반으로
설정LoraConfig(r=8, …)새로 더한 줄
붙이기model = get_peft_model(model, config)새로 더한 줄
확인model.print_trainable_parameters()학습하는 비율을 찍어 본다
학습Trainer(model=model, …).train()같다
저장model.save_pretrained("폴더")어댑터만 저장된다

이 모델에 맞는 학습 데이터

3장, 4장의 Qwen 지시 모델은 채팅 템플릿으로 학습되었습니다. 그래서 SFT 데이터도 4장의 ### 질문: 템플릿이 아니라 이 모델의 채팅 템플릿으로 만들어야 합니다.

Python
messages = [
    {"role": "system", "content": "당신은 온라인 쇼핑몰 고객센터 상담원입니다."},
    {"role": "user", "content": "배송비는 얼마인가요?"},
    {"role": "assistant", "content": "3만원 이상 주문하시면 무료입니다."},
]
# 학습용 한 줄: system, user, assistant 셋 모두. 물어볼 때는 assistant 를 빼고 생성 표시를 붙인다
for m in messages:
    print(f"{m['role']:>9}: {m['content']}")
print("질문 부분 =", [m["role"] for m in messages[:2]], "/ 답 부분 =", messages[2]["role"])

메시지 세 개 가운데 system 과 user 가 질문 부분, assistant 가 답 부분입니다. 실습 2에서 이 메시지를 채팅 템플릿으로 바꾸고, 4장처럼 질문 부분의 정답 자리를 -100 으로 채웁니다.

코드에 나오는 함수

코드쉬운 뜻
{"role": ..., "content": ...}대화 한 마디다. role(역할)은 누가 말했는지, content(내용)는 말한 글이다
"system"모델의 역할과 규칙을 정하는 말이다. 사용자에게는 보이지 않는 안내문이다
"user", "assistant"사용자의 질문과 모델(assistant, 도우미)의 답이다
f"{m['role']:>9}"역할 이름을 9칸 오른쪽 정렬로 찍는다. 콜론 위치가 맞아 보기 좋다
messages[:2], messages[2]앞의 두 마디(질문 부분)와 셋째 마디(답 부분)를 꺼낸다

이 코드가 뜻하는 것

연극 대본을 떠올리면 쉽습니다. 대본에는 "무대 설명", "손님 대사", "상담원 대사"가 차례로 적힙니다. 모델은 이 대본의 꼴로 학습되었으므로, 새로 가르칠 때도 같은 꼴의 대본을 줘야 알아듣습니다. 여기서 무대 설명이 system, 손님 대사가 user, 상담원 대사가 assistant 입니다.

줄하는 일쉽게 말하면
messages = [...]역할과 내용이 짝지어진 대화 셋을 만든다대본 한 장
for m in messages: print(...)한 마디씩 역할과 내용을 찍는다대본을 소리 내어 읽는다
마지막 print질문 부분과 답 부분을 나눠 찍는다모델이 외울 대사(답)와 그냥 읽을 대사(질문)를 표시한다

결과에서 질문 부분은 ['system', 'user'], 답 부분은 assistant 입니다. 학습할 때 손실은 답 부분에서만 셉니다. 질문은 이미 주어지는 것이라 모델이 맞힐 필요가 없기 때문입니다.

작게 줄여서 눈으로 보기

get_peft_model 이 실제로 무엇을 붙이는지, 층 이름만 실제 모델처럼 q_proj, v_proj 로 지은 작은 모델로 봅니다. 모델을 내려받지 않아도 됩니다.

Python
import torch
from torch import nn
from peft import LoraConfig, get_peft_model

class Tiny(nn.Module):
    """층 이름만 실제 모델처럼 q_proj, v_proj 로 지은 작은 모델."""
    def __init__(self):
        super().__init__()
        self.q_proj = nn.Linear(16, 16)
        self.v_proj = nn.Linear(16, 16)
        self.head = nn.Linear(16, 2)

    def forward(self, x):
        return self.head(self.q_proj(x) + self.v_proj(x))

torch.manual_seed(0)
config = LoraConfig(r=2, lora_alpha=4, target_modules=["q_proj", "v_proj"])
model = get_peft_model(Tiny(), config)            # LoRA 를 붙이고 나머지는 얼린다

model.print_trainable_parameters()
for name, p in model.named_parameters():
    if p.requires_grad:                           # 학습하는 것만 찍는다
        print(name, tuple(p.shape))
출력
trainable params: 128 || all params: 706 || trainable%: 18.1303
base_model.model.q_proj.lora_A.default.weight (2, 16)
base_model.model.q_proj.lora_B.default.weight (16, 2)
base_model.model.v_proj.lora_A.default.weight (2, 16)
base_model.model.v_proj.lora_B.default.weight (16, 2)
결과읽는 법
trainable params: 128 || all params: 706학습하는 수(trainable params)가 128개, 전체(all params)가 706개다. 원래 모델 578개에 LoRA 128개가 더해졌다
trainable%: 18.1303학습하는 몫이 약 18% 다. 모델이 아주 작아서 비율이 크게 나왔고, 실제 0.5B 모델에서는 0.22% 다
q_proj.lora_A …(2, 16), lora_B …(16, 2)이름이 맞은 q_proj 와 v_proj 에만 A(2 × 16)와 B(16 × 2)가 붙었다. 층마다 32 + 32 = 64개, 두 층이라 128개다
찍히지 않은 것head 와 원래의 q_proj, v_proj 가중치는 얼어 있어 목록에 없다

print_trainable_parameters() 는 학습하는 수, 전체 수, 그 비율을 한 줄로 찍는 PEFT 의 함수입니다. named_parameters() 는 파라미터를 이름과 함께 돌려줍니다. 이름 가운데 default 는 어댑터에 이름을 따로 주지 않았을 때 붙는 기본 이름입니다.

주의할 점

  • get_peft_model 을 부르면 원래 모델 객체도 함께 바뀝니다. LoRA 없는 모델과 견주려면 새로 불러오거나, 실습 4의 disable_adapter() 를 씁니다.
  • fp16 으로 불러온 모델을 그대로 전체 학습하면 숫자가 넘쳐 손실이 nan 이 되기 쉽습니다. LoRA 는 학습하는 A, B 만 float32 로 두므로 안전합니다(peft 가 알아서 바꿔 준다).
← 이전 절5장 · LoRA 의 설정다음 절 →5장 · 어댑터 저장과 합치기