파인튜닝 모델 평가부터 배포까지 — 실전 완결편

파인튜닝 모델 평가부터 배포까지 — 실전 완결편
Part 1에서 LoRA의 원리와 첫 파인튜닝을, Part 2에서 QLoRA와 한국어 데이터셋 구축을 다뤘습니다. 학습은 끝났습니다. 이제 남은 질문은 두 가지입니다:
시리즈: Part 1: LoRA 이론 | Part 2: QLoRA + 한국어 | Part 3 (이 글)
- 이 모델, 진짜 좋아진 건가? (평가)
- 어떻게 사용자에게 제공하지? (배포)
Part 3에서는 평가 방법론부터 배포 옵션, 그리고 시리즈 전체를 관통하는 실전 팁까지 마무리합니다.
1. 평가 방법론
파인튜닝 모델의 평가는 크게 네 가지 축으로 나뉩니다.
Perplexity 측정
Perplexity(PPL)는 언어 모델의 가장 기본적인 지표입니다. "모델이 다음 토큰을 얼마나 잘 예측하는가"를 측정합니다. 낮을수록 좋습니다.
import torch
from torch.nn import CrossEntropyLoss
from transformers import AutoModelForCausalLM, AutoTokenizer
from datasets import load_dataset
def calculate_perplexity(model, tokenizer, dataset, max_length=1024):
model.eval()
total_loss = 0
total_tokens = 0
with torch.no_grad():
for example in dataset:
inputs = tokenizer(
example["text"],
return_tensors="pt",
truncation=True,
max_length=max_length,
).to(model.device)
outputs = model(**inputs, labels=inputs["input_ids"])
total_loss += outputs.loss.item() * inputs["input_ids"].size(1)
total_tokens += inputs["input_ids"].size(1)
avg_loss = total_loss / total_tokens
perplexity = torch.exp(torch.tensor(avg_loss)).item()
return perplexity
# 사용 예
eval_dataset = load_dataset("json", data_files="eval_data.jsonl", split="train")
ppl = calculate_perplexity(model, tokenizer, eval_dataset)
print(f"Perplexity: {ppl:.2f}")주의할 점: PPL은 동일한 평가 데이터로 비교해야 의미가 있습니다. 학습 데이터로 측정하면 과적합된 모델이 더 좋은 수치를 받습니다.
KoBEST 벤치마크 (한국어 이해력 평가)
한국어 모델을 평가할 때 KoBEST는 사실상 표준입니다. 5개 태스크로 구성됩니다.
from lm_eval import simple_evaluate
from lm_eval.models.huggingface import HFLM
lm = HFLM(pretrained=model, tokenizer=tokenizer, batch_size=8)
results = simple_evaluate(
model=lm,
tasks=["kobest_boolq", "kobest_copa", "kobest_wic",
"kobest_hellaswag", "kobest_sentineg"],
num_fewshot=5,
)
for task, metrics in results["results"].items():
print(f"{task}: {metrics['acc,none']:.4f}")Task-specific 평가
도메인 파인튜닝 모델은 범용 벤치마크보다 태스크별 지표가 더 중요합니다.
from rouge_score import rouge_scorer
scorer = rouge_scorer.RougeScorer(["rouge1", "rouge2", "rougeL"], use_stemmer=False)
def evaluate_summarization(model, tokenizer, eval_pairs):
"""eval_pairs: list of (input_text, reference_summary)"""
scores = {"rouge1": [], "rouge2": [], "rougeL": []}
for input_text, reference in eval_pairs:
messages = [{"role": "user", "content": f"다음을 요약하세요:\n{input_text}"}]
inputs = tokenizer.apply_chat_template(messages, return_tensors="pt").to("cuda")
outputs = model.generate(inputs, max_new_tokens=256)
prediction = tokenizer.decode(outputs[0], skip_special_tokens=True)
result = scorer.score(reference, prediction)
for key in scores:
scores[key].append(result[key].fmeasure)
return {k: sum(v) / len(v) for k, v in scores.items()}Human Evaluation 가이드라인
자동 지표만으로는 한계가 있습니다. 특히 한국어 자연스러움, 존댓말 일관성, 사실 정확성은 사람이 평가해야 합니다. 평가 설계 시 권장 사항:
- 평가자 수: 최소 3명 (일치도 확인용)
- 평가 척도: 1-5 Likert scale, 항목별 분리 (유창성 / 정확성 / 관련성)
- 블라인드 비교: Base vs Fine-tuned 결과를 섞어서 제시
- 샘플 수: 최소 50개 (통계적 유의성 확보)
Before / After 비교
Qwen 2.5 7B에 한국어 고객 응대 데이터 3,000건으로 QLoRA 학습한 결과입니다.
예시 1: 환불 요청
예시 2: 기술 지원
예시 3: 상품 문의
벤치마크 수치 비교:
KoBEST(범용 한국어 이해)는 크게 변하지 않지만, 도메인 태스크에서는 극적인 개선을 보입니다. 이것이 파인튜닝의 핵심입니다.
2. LoRA 가중치 머지
학습이 끝나면 LoRA 어댑터를 원본 모델에 머지(병합)할 수 있습니다.
merge_and_unload()
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer
# 1. Base 모델 로드
base_model = AutoModelForCausalLM.from_pretrained(
"Qwen/Qwen2.5-7B-Instruct",
torch_dtype=torch.bfloat16,
device_map="cpu", # 머지는 CPU에서 해도 됩니다
)
# 2. LoRA 어댑터 로드
model = PeftModel.from_pretrained(base_model, "./qwen25-qlora-ko-adapter")
# 3. 머지
model = model.merge_and_unload()
# 4. 저장
model.save_pretrained("./qwen25-ko-merged")
tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-7B-Instruct")
tokenizer.save_pretrained("./qwen25-ko-merged")merge_and_unload()는 $W' = W_0 + BA$를 실제로 계산해서 하나의 가중치로 합칩니다. 결과물은 일반 HuggingFace 모델과 동일한 구조입니다.
머지 vs 어댑터 분리
권장: 최종 배포용이면 머지, 실험/A-B 테스트 중이면 어댑터 분리를 유지하세요.
머지 후 GGUF 변환
llama.cpp 호환 포맷인 GGUF로 변환하면 Ollama, LM Studio 등에서 바로 사용할 수 있습니다.
# llama.cpp 클론
git clone https://github.com/ggerganov/llama.cpp
cd llama.cpp
pip install -r requirements.txt
# HuggingFace → GGUF 변환 (Q4_K_M 양자화)
python convert_hf_to_gguf.py ../qwen25-ko-merged \
--outtype q4_k_m \
--outfile qwen25-ko-Q4_K_M.gguf양자화 옵션별 크기/품질 비교:
대부분의 경우 Q4_K_M이 크기와 품질의 최적 균형입니다.
3. 배포 옵션
모델이 준비됐으니 서빙할 차례입니다. 상황별로 네 가지 방법을 비교합니다.
vLLM으로 서빙
프로덕션 환경에서 가장 많이 쓰이는 방법입니다. LoRA 어댑터를 머지 없이 직접 로드할 수 있습니다.
from vllm import LLM, SamplingParams
from vllm.lora.request import LoRARequest
# LoRA 어댑터를 직접 로드
llm = LLM(
model="Qwen/Qwen2.5-7B-Instruct",
enable_lora=True,
max_lora_rank=64,
)
sampling_params = SamplingParams(temperature=0.7, max_tokens=512)
# 어댑터 지정해서 추론
lora_request = LoRARequest("ko-cs", 1, "./qwen25-qlora-ko-adapter")
outputs = llm.generate(
["고객님, 환불 요청에 대해 안내드립니다."],
sampling_params,
lora_request=lora_request,
)
print(outputs[0].outputs[0].text)vLLM의 장점은 여러 LoRA 어댑터를 동시에 서빙할 수 있다는 것입니다. 고객 응대용, 기술 문서용, 마케팅용 어댑터를 하나의 서버에서 스위칭할 수 있습니다.
# OpenAI 호환 API 서버로 실행
vllm serve Qwen/Qwen2.5-7B-Instruct \
--enable-lora \
--lora-modules ko-cs=./qwen25-qlora-ko-adapter \
--port 8000Ollama로 로컬 배포
개인 사용이나 팀 내부 배포에 가장 간편합니다. GGUF 파일이 필요합니다.
# Modelfile
FROM ./qwen25-ko-Q4_K_M.gguf
TEMPLATE """{{ if .System }}<|im_start|>system
{{ .System }}<|im_end|>
{{ end }}<|im_start|>user
{{ .Prompt }}<|im_end|>
<|im_start|>assistant
"""
PARAMETER temperature 0.7
PARAMETER top_p 0.9
PARAMETER stop "<|im_end|>"
SYSTEM "당신은 한국어 고객 응대 전문 AI 어시스턴트입니다. 항상 정중하고 구체적으로 답변합니다."# 모델 생성 및 실행
ollama create qwen25-ko -f Modelfile
ollama run qwen25-ko "배송 상태를 확인하고 싶습니다"HuggingFace Spaces (Gradio 데모)
프로토타입 공유나 비기술팀 데모에 적합합니다.
import gradio as gr
from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline
model = AutoModelForCausalLM.from_pretrained(
"./qwen25-ko-merged", torch_dtype="auto", device_map="auto"
)
tokenizer = AutoTokenizer.from_pretrained("./qwen25-ko-merged")
pipe = pipeline("text-generation", model=model, tokenizer=tokenizer)
def chat(message, history):
messages = [{"role": "user", "content": message}]
for user_msg, bot_msg in history:
messages.insert(-1, {"role": "user", "content": user_msg})
messages.insert(-1, {"role": "assistant", "content": bot_msg})
prompt = tokenizer.apply_chat_template(messages, tokenize=False)
output = pipe(prompt, max_new_tokens=512, temperature=0.7)
return output[0]["generated_text"].split("<|im_start|>assistant\n")[-1]
demo = gr.ChatInterface(chat, title="한국어 고객 응대 AI")
demo.launch()배포 옵션 비교표
4. 실전 팁 모음
시리즈를 마무리하며, 실무에서 반복적으로 마주치는 문제들과 해결법을 정리합니다.
과적합 징후와 해결법
과적합은 LoRA에서도 발생합니다. 특히 데이터가 적을 때 (1,000건 미만) 주의하세요.
징후:
- Train loss는 계속 떨어지는데 eval loss가 올라감
- 학습 데이터와 비슷한 입력에는 완벽하지만, 조금만 달라져도 엉뚱한 답변
- 모델이 학습 데이터의 문장을 그대로 외워서 출력
해결법:
lora_dropout을 0.1-0.15로 높이기r(rank)을 줄이기 (64 → 16)- 학습 에폭 줄이기 (3 → 1)
- 데이터 다양성 늘리기 (가장 효과적)
- Early stopping 적용
from transformers import EarlyStoppingCallback
trainer = SFTTrainer(
model=model,
args=training_args,
train_dataset=train_dataset,
eval_dataset=eval_dataset,
callbacks=[EarlyStoppingCallback(early_stopping_patience=3)],
)데이터 품질 > 데이터 양
1,000건의 고품질 데이터가 10,000건의 노이즈 데이터보다 낫습니다. 실제 실험 결과:
데이터 품질 체크리스트:
- 문법 오류가 없는가?
- 지시(instruction)와 응답(response)이 정확히 대응하는가?
- 중복 데이터가 없는가? (5% 이상 중복이면 성능 저하)
- 답변 길이가 일관적인가? (너무 짧은 답변과 너무 긴 답변이 섞이면 불안정)
Hyperparameter 가이드
시작점 권장: lr=2e-4, epochs=1, r=16, alpha=32, batch=16, warmup=0.1에서 시작한 뒤, eval loss를 보면서 조정하세요.
멀티태스크 LoRA: 어댑터 스위칭
하나의 베이스 모델에 여러 LoRA 어댑터를 만들어 태스크별로 전환할 수 있습니다.
from peft import PeftModel
base_model = AutoModelForCausalLM.from_pretrained(
"Qwen/Qwen2.5-7B-Instruct", torch_dtype=torch.bfloat16, device_map="auto"
)
# 어댑터 1: 고객 응대
model = PeftModel.from_pretrained(base_model, "./adapter-customer-service")
# 고객 응대 추론 ...
# 어댑터 2: 기술 문서로 전환
model.load_adapter("./adapter-tech-docs", adapter_name="tech")
model.set_adapter("tech")
# 기술 문서 추론 ...
# 어댑터 3: 마케팅 카피로 전환
model.load_adapter("./adapter-marketing", adapter_name="marketing")
model.set_adapter("marketing")
# 마케팅 카피 추론 ...베이스 모델 14GB + 어댑터 52MB × 3 = 총 14.15GB. 세 개의 특화 모델을 하나의 GPU에서 운영할 수 있습니다.
LoRA를 쓰면 안 되는 경우
LoRA가 만능은 아닙니다. 다음 상황에서는 다른 접근을 권장합니다.
5. 시리즈 전체 요약
세 편을 관통하는 핵심 메시지: LoRA는 "가성비"의 기술입니다. 풀 파인튜닝 대비 99%의 비용을 절감하면서 90-98%의 성능을 유지합니다. 무료 Colab GPU에서도 7B 모델을 파인튜닝할 수 있고, 52MB짜리 어댑터 하나로 범용 모델을 도메인 전문가로 만들 수 있습니다.
시작하는 분들께 권장하는 순서:
- 먼저 프롬프트 엔지니어링으로 가능한지 확인
- 안 되면 고품질 데이터 1,000건 구축
- QLoRA로 파인튜닝 (Part 2 참고)
- 평가 → 데이터 보강 → 재학습 반복
- 만족스러우면 머지 후 배포