5장. LoRA 파인튜닝과 DPO 정렬
실습 4 — 어댑터 불러오기와 합치기
실습 3에서 저장한 어댑터를 기본 모델에 다시 끼워 답을 확인하고, 어댑터를 잠시 끄면 원래 모델로 돌아가는지 봅니다. 마지막으로 어댑터를 가중치에 합칩니다. 실습 3을 먼저 실행해 out_lora_shop 이 있어야 합니다. CPU 에서도 됩니다.
1단계. 어댑터 끼우기
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
from peft import PeftModel
name = "Qwen/Qwen2.5-0.5B-Instruct"
tok = AutoTokenizer.from_pretrained(name)
base = AutoModelForCausalLM.from_pretrained(name, dtype=torch.float32) # CPU 에서도 되도록 float32
model = PeftModel.from_pretrained(base, "out_lora_shop").eval() # 기본 모델 + 어댑터
print(type(model).__name__, "/ 어댑터 이름:", list(model.peft_config.keys()))PeftModelForCausalLM / 어댑터 이름: ['default']2단계. 어댑터를 켜고 끄며 견주기
SYSTEM = "당신은 온라인 쇼핑몰 고객센터 상담원입니다."
def ask(m, q):
msgs = [{"role": "system", "content": SYSTEM}, {"role": "user", "content": q}]
enc = tok.apply_chat_template(msgs, add_generation_prompt=True, return_tensors="pt", return_dict=True)
with torch.no_grad():
out = m.generate(**enc, max_new_tokens=60, do_sample=False)
return tok.decode(out[0][enc["input_ids"].shape[1]:], skip_special_tokens=True).strip().replace("\n", " ")
q = "쿠폰 몇 장 쓸 수 있는지 알려 주세요."
print("[어댑터 켬]", ask(model, q))
with model.disable_adapter(): # 이 안에서만 어댑터를 끈다 → 원래 모델
print("[어댑터 끔]", ask(model, q)[:120])[어댑터 켬] 안녕하세요, 고객님. 주문 하나에 쿠폰은 한 장만 쓰실 수 있습니다. 감사합니다.
[어댑터 끔] 죄송합니다, 저는 인공지능으로서 개인적인 사용자 경험을 제공할 수 없습니다. 하지만 일반적으로 쿠폰이 10개 이상을 할당받을 수 있습니다. 이는 쿠폰의 제한된 수량에 따라 �3단계. 합치기
merged = model.merge_and_unload() # B·A 를 가중치에 더하고 LoRA 를 걷어 낸다
print(type(merged).__name__)
print("파라미터 수:", f"{sum(p.numel() for p in merged.parameters()):,}")
print("[합친 모델]", ask(merged, q))Qwen2ForCausalLM
파라미터 수: 494,032,768
[합친 모델] 안녕하세요, 고객님. 주문 하나에 쿠폰은 한 장만 쓰실 수 있습니다. 감사합니다.이 실습이 뜻하는 것
이 실습은 카메라 본체에 저장해 둔 필터 렌즈를 끼웠다 뺐다 하는 것과 같습니다. 본체에 필터 렌즈를 끼우고(1단계), 렌즈를 끼운 채와 뺀 채로 같은 장면을 찍어 견주고(2단계), 마지막으로 렌즈를 본체에 붙박이로 붙입니다(3단계). 여기서 카메라 본체가 기본 모델, 필터 렌즈가 실습 3의 LoRA 어댑터, 붙박이로 붙이는 일이 합치기(merge)입니다. 비유와 다른 점은, 붙박이 렌즈는 무게를 늘리지만 합친 모델은 어댑터의 값이 가중치 안으로 들어가 파라미터 수가 원래와 같다는 것입니다.
| 단계 | 출력 | 뜻 |
|---|---|---|
| 1. 끼우기 | PeftModelForCausalLM, ['default'] | 기본 모델에 어댑터 하나가 "default"라는 이름으로 끼워졌습니다 |
| 2. 어댑터 켬 | "주문 하나에 쿠폰은 한 장만 쓰실 수 있습니다." | 실습 3에서 배운 쇼핑몰 정책대로 답합니다 |
| 2. 어댑터 끔 | "…일반적으로 쿠폰이 10개 이상을…" | 원래 모델로 돌아가 이 쇼핑몰의 정책과 다른 일반론을 말합니다 |
| 3. 합친 클래스 | Qwen2ForCausalLM | 어댑터가 따로 붙지 않은 보통 모델로 돌아왔습니다 |
| 3. 파라미터 수 | 494,032,768 | 실습 1의 전체 수와 같습니다. A·B 의 값이 원래 가중치에 더해져 들어갔기 때문입니다 |
| 3. 합친 모델의 답 | 어댑터를 켰을 때와 같은 답 | 합쳐도 답이 그대로라, 합치기가 바르게 되었다는 확인입니다 |
한 줄로 줄이면, 4.2MB 어댑터 하나로 같은 모델이 "쇼핑몰 상담원"과 "원래 모델"을 오갈 수 있고, 합치면 보통 모델 하나가 된다는 것을 보인 실습입니다. 질문 하나로 견준 것이라, 모든 질문에서 합친 모델의 답이 똑같다는 확인까지는 아닙니다. 합치기는 프로젝트에서 SFT 의 LoRA 위에 DPO(Direct Preference Optimization, 직접 선호 최적화)를 할 때 다시 씁니다.
코드 설명
PeftModel.from_pretrained(기본 모델, 폴더)는 폴더의 어댑터 설정을 읽어 같은 자리에 LoRA 를 붙이고 학습한 값을 채웁니다. 기본 모델은 실습 3과 같은 이름이어야 합니다.- 어댑터를 켜면 "주문 하나에 쿠폰은 한 장만 쓰실 수 있습니다"라고 쇼핑몰의 정책대로 답하고,
disable_adapter()안에서 끄면 "저는 인공지능으로서 … 일반적으로 쿠폰이 10개 이상을"처럼 원래 모델의 답으로 돌아갑니다. 같은 모델 객체 하나로 두 모습을 오갈 수 있습니다. - 합친 뒤에는 클래스가 보통 모델(
Qwen2ForCausalLM)로 돌아오고, 파라미터 수도 원래와 같은 494,032,768개입니다. LoRA 의 값이 가중치 안으로 들어갔기 때문입니다. 답은 어댑터를 켰을 때와 똑같습니다. - 합친 모델은
merged.save_pretrained("폴더")로 보통 모델처럼 저장할 수 있습니다. 1GB 넘게 차지하므로 이 실습에서는 저장하지 않았습니다.