오픈소스 LLM 파인튜닝 완벽 가이드: 실전 예제로 배우기
오픈소스 LLM을 자신의 데이터로 파인튜닝하면 특정 도메인에서 더 나은 성능을 얻을 수 있습니다. 이 글에서는 파인튜닝의 기초부터 실전 구현까지 모든 과정을 상세히 다룹니다.
LLM 파인튜닝이란?
파인튜닝(Fine-tuning)은 사전 학습된 대규모 언어 모델을 특정 작업이나 도메인에 맞게 추가 학습시키는 과정입니다.
파인튜닝이 필요한 경우
- 도메인 특화: 의료, 법률, 금융 등 전문 분야
- 톤앤매너: 브랜드에 맞는 응답 스타일
- 특수 작업: 데이터 추출, 분류, 요약 등
- 언어 지원: 특정 언어나 방언 지원 강화
- 비용 절감: 큰 모델 대신 작은 모델 최적화
파인튜닝 vs 프롬프트 엔지니어링
| 특징 | 프롬프트 엔지니어링 | 파인튜닝 |
|---|---|---|
| 비용 | 낮음 | 중~높음 |
| 시간 | 즉시 | 수시간~수일 |
| 전문성 요구 | 낮음 | 높음 |
| 성능 개선 | 제한적 | 대폭 향상 가능 |
| 데이터 요구량 | 적음 | 많음 (수백~수천 샘플) |
파인튜닝 기법 비교
1. Full Fine-tuning
모델의 모든 파라미터를 업데이트하는 전통적인 방식입니다.
장점:
- 최고의 성능 달성 가능
- 모델 구조를 완전히 활용
단점:
- 막대한 GPU 메모리 필요 (예: LLaMA 7B = ~28GB)
- 긴 학습 시간
- 높은 비용
2. LoRA (Low-Rank Adaptation)
작은 어댑터 레이어만 학습하는 효율적인 방법입니다.
장점:
- GPU 메모리 사용량 대폭 감소 (~3배)
- 빠른 학습 속도
- 여러 LoRA 어댑터를 교체하며 사용 가능
단점:
- Full fine-tuning보다 약간 낮은 성능
- 추가적인 추론 오버헤드 (미미함)
3. QLoRA (Quantized LoRA)
LoRA에 양자화를 결합한 최신 기법입니다.
장점:
- 극도로 적은 메모리 사용 (~10배 감소)
- 소비자용 GPU로도 학습 가능
- LoRA와 유사한 성능
단점:
- 약간 느린 학습 속도
- 구현 복잡도 증가
환경 설정
필요한 하드웨어
| 모델 크기 | Full Fine-tuning | LoRA | QLoRA |
|---|---|---|---|
| 7B | A100 40GB | RTX 3090 24GB | RTX 3060 12GB |
| 13B | A100 80GB | A100 40GB | RTX 3090 24GB |
| 70B | 8xA100 | 2xA100 | A100 40GB |
Python 환경 구성
# 가상환경 생성
conda create -n llm-finetune python=3.10
conda activate llm-finetune
# PyTorch 설치 (CUDA 11.8)
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118
# 필수 라이브러리 설치
pip install transformers==4.36.0
pip install datasets==2.16.0
pip install peft==0.7.0 # LoRA 지원
pip install bitsandbytes==0.41.3 # QLoRA 지원
pip install accelerate==0.25.0
pip install trl==0.7.10 # 강화학습 기반 파인튜닝
# 모니터링 도구
pip install wandb tensorboard
실전 예제 1: LoRA로 감정 분석 모델 만들기
데이터셋 준비
# dataset_prep.py
import json
from datasets import Dataset
# 한국어 감정 분석 데이터 예시
data = [
{
"instruction": "다음 문장의 감정을 분석해주세요.",
"input": "오늘 정말 기분이 좋아요!",
"output": "긍정적 (positive) - 행복하고 즐거운 감정이 표현되어 있습니다."
},
{
"instruction": "다음 문장의 감정을 분석해주세요.",
"input": "이 영화는 정말 실망스러웠어요.",
"output": "부정적 (negative) - 실망과 불만족이 드러납니다."
},
{
"instruction": "다음 문장의 감정을 분석해주세요.",
"input": "날씨가 흐리네요.",
"output": "중립적 (neutral) - 객관적인 사실 진술로 특별한 감정이 없습니다."
},
# 실제로는 최소 500~1000개 이상 필요
]
# Alpaca 형식으로 변환
def format_instruction(sample):
return f"""### Instruction:
{sample['instruction']}
### Input:
{sample['input']}
### Response:
{sample['output']}"""
# 데이터셋 생성
dataset = Dataset.from_list(data)
dataset = dataset.train_test_split(test_size=0.1)
# 저장
dataset.save_to_disk("./sentiment_dataset")
print(f"Training samples: {len(dataset['train'])}")
print(f"Test samples: {len(dataset['test'])}")
LoRA 파인튜닝 코드
# finetune_lora.py
import torch
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
TrainingArguments,
Trainer,
DataCollatorForLanguageModeling
)
from peft import (
LoraConfig,
get_peft_model,
prepare_model_for_kbit_training,
TaskType
)
from datasets import load_from_disk
# 1. 모델과 토크나이저 로드
model_name = "beomi/llama-2-ko-7b" # 한국어 LLaMA 모델
tokenizer = AutoTokenizer.from_pretrained(model_name)
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"
model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype=torch.float16,
device_map="auto",
trust_remote_code=True
)
# 2. LoRA 설정
lora_config = LoraConfig(
r=16, # LoRA rank (낮을수록 메모리 절약, 높을수록 성능 향상)
lora_alpha=32, # LoRA scaling factor
target_modules=[
"q_proj",
"k_proj",
"v_proj",
"o_proj",
"gate_proj",
"up_proj",
"down_proj",
], # LLaMA 모델의 attention과 MLP 레이어
lora_dropout=0.05,
bias="none",
task_type=TaskType.CAUSAL_LM
)
# LoRA 어댑터 적용
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# 출력 예: trainable params: 4,194,304 || all params: 6,742,609,920 || trainable%: 0.06%
# 3. 데이터 준비
dataset = load_from_disk("./sentiment_dataset")
def format_instruction(sample):
return f"""### Instruction:
{sample['instruction']}
### Input:
{sample['input']}
### Response:
{sample['output']}"""
def preprocess_function(examples):
# 텍스트 포맷팅
texts = [format_instruction(ex) for ex in examples]
# 토크나이징
model_inputs = tokenizer(
texts,
max_length=512,
truncation=True,
padding="max_length",
return_tensors="pt"
)
# labels 설정 (causal LM은 input_ids를 그대로 사용)
model_inputs["labels"] = model_inputs["input_ids"].clone()
return model_inputs
# 데이터셋 전처리
tokenized_train = dataset["train"].map(
preprocess_function,
batched=True,
remove_columns=dataset["train"].column_names
)
tokenized_test = dataset["test"].map(
preprocess_function,
batched=True,
remove_columns=dataset["test"].column_names
)
# 4. 학습 설정
training_args = TrainingArguments(
output_dir="./lora-sentiment-output",
num_train_epochs=3,
per_device_train_batch_size=4,
per_device_eval_batch_size=4,
gradient_accumulation_steps=4, # 실질적 배치 크기 = 4 * 4 = 16
learning_rate=2e-4,
fp16=True, # Mixed precision 학습
logging_steps=10,
evaluation_strategy="steps",
eval_steps=50,
save_strategy="steps",
save_steps=100,
save_total_limit=3,
load_best_model_at_end=True,
warmup_steps=100,
lr_scheduler_type="cosine",
optim="paged_adamw_32bit", # 메모리 효율적인 옵티마이저
report_to="tensorboard",
)
# 5. Trainer 설정 및 학습
trainer = Trainer(
model=model,
args=training_args,
train_dataset=tokenized_train,
eval_dataset=tokenized_test,
data_collator=DataCollatorForLanguageModeling(tokenizer, mlm=False),
)
# 학습 시작
print("Starting training...")
trainer.train()
# 6. 모델 저장
model.save_pretrained("./lora-sentiment-final")
tokenizer.save_pretrained("./lora-sentiment-final")
print("Training complete! Model saved to ./lora-sentiment-final")
추론 및 테스트
# inference.py
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel
# 베이스 모델 로드
base_model_name = "beomi/llama-2-ko-7b"
base_model = AutoModelForCausalLM.from_pretrained(
base_model_name,
torch_dtype=torch.float16,
device_map="auto"
)
tokenizer = AutoTokenizer.from_pretrained(base_model_name)
# LoRA 어댑터 로드 및 병합
model = PeftModel.from_pretrained(base_model, "./lora-sentiment-final")
model = model.merge_and_unload() # LoRA를 베이스 모델에 병합
# 추론 함수
def analyze_sentiment(text):
prompt = f"""### Instruction:
다음 문장의 감정을 분석해주세요.
### Input:
{text}
### Response:
"""
inputs = tokenizer(prompt, return_tensors="pt").to("cuda")
with torch.no_grad():
outputs = model.generate(
**inputs,
max_new_tokens=100,
temperature=0.7,
top_p=0.9,
do_sample=True,
pad_token_id=tokenizer.eos_token_id
)
response = tokenizer.decode(outputs[0], skip_special_tokens=True)
# Response 부분만 추출
response = response.split("### Response:")[-1].strip()
return response
# 테스트
test_sentences = [
"이 제품 정말 최고예요! 강추합니다!",
"배송이 너무 늦어서 짜증나네요.",
"보통 수준이에요. 가격대비 괜찮습니다.",
]
for sentence in test_sentences:
print(f"\n입력: {sentence}")
print(f"분석: {analyze_sentiment(sentence)}")
실전 예제 2: QLoRA로 대화형 챗봇 만들기
QLoRA는 4bit 양자화를 사용하여 메모리를 극도로 절약하면서 파인튜닝합니다.
QLoRA 파인튜닝 코드
# finetune_qlora.py
import torch
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
BitsAndBytesConfig,
TrainingArguments,
)
from peft import LoraConfig, prepare_model_for_kbit_training, get_peft_model
from trl import SFTTrainer
from datasets import load_dataset
# 1. 4bit 양자화 설정
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_use_double_quant=True, # 이중 양자화로 추가 메모리 절약
bnb_4bit_quant_type="nf4", # Normal Float 4bit
bnb_4bit_compute_dtype=torch.bfloat16 # 계산은 bfloat16으로
)
# 2. 모델 로드 (4bit 양자화 적용)
model_name = "meta-llama/Llama-2-7b-hf"
model = AutoModelForCausalLM.from_pretrained(
model_name,
quantization_config=bnb_config,
device_map="auto",
trust_remote_code=True,
use_auth_token=True # HuggingFace 토큰 필요
)
tokenizer = AutoTokenizer.from_pretrained(model_name)
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"
# 3. k-bit 학습 준비
model = prepare_model_for_kbit_training(model)
# 4. LoRA 설정 (QLoRA는 LoRA + 양자화)
lora_config = LoraConfig(
r=64, # 더 큰 rank 사용 가능 (메모리 여유가 있으므로)
lora_alpha=16,
target_modules=[
"q_proj",
"k_proj",
"v_proj",
"o_proj",
"gate_proj",
"up_proj",
"down_proj",
],
lora_dropout=0.1,
bias="none",
task_type="CAUSAL_LM"
)
model = get_peft_model(model, lora_config)
# 5. 대화 데이터셋 준비
# 고객 서비스 챗봇 예시
conversations = [
{
"messages": [
{"role": "user", "content": "제품 배송은 얼마나 걸리나요?"},
{"role": "assistant", "content": "일반 배송은 2-3일, 빠른 배송은 1일이 소요됩니다. 주말과 공휴일은 제외됩니다."}
]
},
{
"messages": [
{"role": "user", "content": "환불 정책이 궁금합니다."},
{"role": "assistant", "content": "구매 후 14일 이내 미개봉 제품은 전액 환불 가능합니다. 개봉 제품은 제품 상태에 따라 부분 환불됩니다."}
]
},
# 더 많은 대화 데이터...
]
# ChatML 형식으로 변환
def format_chat(example):
formatted = ""
for msg in example["messages"]:
role = msg["role"]
content = msg["content"]
if role == "user":
formatted += f"<|user|>\n{content}\n"
else:
formatted += f"<|assistant|>\n{content}\n"
formatted += "<|endoftext|>"
return {"text": formatted}
from datasets import Dataset
dataset = Dataset.from_list(conversations)
dataset = dataset.map(format_chat)
# 6. SFTTrainer 설정 (Supervised Fine-Tuning)
training_args = TrainingArguments(
output_dir="./qlora-chatbot-output",
num_train_epochs=3,
per_device_train_batch_size=1, # 4bit이므로 배치 크기 1로도 충분
gradient_accumulation_steps=16,
learning_rate=2e-4,
bf16=True, # bfloat16 사용
logging_steps=5,
save_strategy="epoch",
optim="paged_adamw_32bit",
max_grad_norm=0.3,
warmup_ratio=0.03,
lr_scheduler_type="constant",
)
trainer = SFTTrainer(
model=model,
train_dataset=dataset,
tokenizer=tokenizer,
args=training_args,
peft_config=lora_config,
dataset_text_field="text",
max_seq_length=512,
)
# 7. 학습 시작
trainer.train()
# 8. 저장
model.save_pretrained("./qlora-chatbot-final")
tokenizer.save_pretrained("./qlora-chatbot-final")
QLoRA 모델 추론
# inference_qlora.py
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from peft import PeftModel
# 4bit 양자화 설정
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_use_double_quant=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16
)
# 베이스 모델 로드
base_model_name = "meta-llama/Llama-2-7b-hf"
base_model = AutoModelForCausalLM.from_pretrained(
base_model_name,
quantization_config=bnb_config,
device_map="auto"
)
# LoRA 어댑터 로드
model = PeftModel.from_pretrained(base_model, "./qlora-chatbot-final")
tokenizer = AutoTokenizer.from_pretrained(base_model_name)
# 대화 함수
def chat(user_message, conversation_history=""):
prompt = conversation_history + f"<|user|>\n{user_message}\n<|assistant|>\n"
inputs = tokenizer(prompt, return_tensors="pt").to("cuda")
with torch.no_grad():
outputs = model.generate(
**inputs,
max_new_tokens=150,
temperature=0.7,
top_p=0.9,
do_sample=True,
repetition_penalty=1.1,
pad_token_id=tokenizer.eos_token_id
)
response = tokenizer.decode(outputs[0], skip_special_tokens=True)
assistant_response = response.split("<|assistant|>")[-1].strip()
return assistant_response
# 대화 테스트
print("챗봇: 안녕하세요! 무엇을 도와드릴까요?")
conversation = ""
while True:
user_input = input("사용자: ")
if user_input.lower() in ["quit", "exit", "종료"]:
break
response = chat(user_input, conversation)
print(f"챗봇: {response}")
# 대화 이력 업데이트
conversation += f"<|user|>\n{user_input}\n<|assistant|>\n{response}\n"
실전 예제 3: 도메인 특화 모델 (의료 QA)
의료 분야처럼 전문적인 도메인에서 더 정확한 답변을 위한 파인튜닝 예시입니다.
의료 데이터셋 구성
# medical_dataset.py
import json
from datasets import Dataset
# 의료 QA 데이터 예시
medical_data = [
{
"question": "당뇨병의 주요 증상은 무엇인가요?",
"answer": """당뇨병의 주요 증상은 다음과 같습니다:
1. 다음(多飮) - 과도한 갈증
2. 다뇨(多尿) - 잦은 소변
3. 다식(多食) - 과도한 배고픔
4. 체중 감소
5. 피로감
6. 시력 저하
7. 상처 치유 지연
이러한 증상이 지속된다면 혈당 검사를 받아보시기 바랍니다."""
},
{
"question": "고혈압 관리를 위한 생활습관은?",
"answer": """고혈압 관리를 위한 주요 생활습관:
1. 식이요법
- 나트륨 섭취 제한 (하루 2,000mg 이하)
- DASH 식단 (과일, 채소, 저지방 유제품)
- 포화지방 줄이기
2. 운동
- 주 5일, 30분 이상 유산소 운동
- 걷기, 수영, 자전거 타기 권장
3. 체중 관리
- 정상 BMI(18.5-24.9) 유지
4. 금연 및 절주
5. 스트레스 관리
6. 규칙적인 혈압 측정"""
},
# 실제로는 수천 개의 의료 QA 필요
]
def format_medical_qa(sample):
return {
"text": f"""아래는 의료 관련 질문과 전문적인 답변입니다.
### 질문:
{sample['question']}
### 답변:
{sample['answer']}
"""
}
dataset = Dataset.from_list(medical_data)
dataset = dataset.map(format_medical_qa)
dataset.save_to_disk("./medical_qa_dataset")
의료 모델 파인튜닝 (LoRA + 특수 설정)
# finetune_medical.py
import torch
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
TrainingArguments,
)
from peft import LoraConfig, get_peft_model, TaskType
from trl import SFTTrainer
from datasets import load_from_disk
# 모델 설정
model_name = "beomi/llama-2-ko-7b"
model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype=torch.float16,
device_map="auto"
)
tokenizer = AutoTokenizer.from_pretrained(model_name)
tokenizer.pad_token = tokenizer.eos_token
# LoRA 설정 (의료 도메인에 최적화)
lora_config = LoraConfig(
r=32, # 전문 도메인이므로 높은 rank 사용
lora_alpha=64,
target_modules=[
"q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj",
],
lora_dropout=0.05,
bias="none",
task_type=TaskType.CAUSAL_LM,
)
model = get_peft_model(model, lora_config)
# 데이터셋 로드
dataset = load_from_disk("./medical_qa_dataset")
train_test = dataset.train_test_split(test_size=0.1)
# 학습 설정 (의료 도메인 특화)
training_args = TrainingArguments(
output_dir="./medical-lora-output",
num_train_epochs=5, # 전문 도메인은 더 많은 에폭
per_device_train_batch_size=2,
gradient_accumulation_steps=8,
learning_rate=1e-4, # 낮은 학습률로 안정적 학습
fp16=True,
logging_steps=10,
evaluation_strategy="steps",
eval_steps=50,
save_strategy="steps",
save_steps=100,
save_total_limit=5,
load_best_model_at_end=True,
warmup_ratio=0.1, # 더 긴 워밍업
weight_decay=0.01,
lr_scheduler_type="cosine",
optim="adamw_torch",
report_to="wandb", # Weights & Biases 로깅
)
# SFT Trainer
trainer = SFTTrainer(
model=model,
train_dataset=train_test["train"],
eval_dataset=train_test["test"],
tokenizer=tokenizer,
args=training_args,
peft_config=lora_config,
dataset_text_field="text",
max_seq_length=1024, # 긴 의료 답변을 위해 길이 증가
)
# 학습
trainer.train()
# 저장
model.save_pretrained("./medical-model-final")
tokenizer.save_pretrained("./medical-model-final")
고급 기법
1. 데이터 증강 (Data Augmentation)
# data_augmentation.py
from transformers import pipeline
# Back-translation으로 데이터 증강
translator_ko_en = pipeline("translation", model="Helsinki-NLP/opus-mt-ko-en")
translator_en_ko = pipeline("translation", model="Helsinki-NLP/opus-mt-en-ko")
def augment_with_backtranslation(text):
# 한국어 -> 영어 -> 한국어
en_text = translator_ko_en(text)[0]['translation_text']
augmented = translator_en_ko(en_text)[0]['translation_text']
return augmented
original = "당뇨병의 주요 증상은 무엇인가요?"
augmented = augment_with_backtranslation(original)
print(f"원본: {original}")
print(f"증강: {augmented}")
# Paraphrasing으로 데이터 증강
from transformers import AutoModelForSeq2SeqLM, AutoTokenizer
paraphrase_model = AutoModelForSeq2SeqLM.from_pretrained("humarin/chatgpt_paraphraser_on_T5_base")
paraphrase_tokenizer = AutoTokenizer.from_pretrained("humarin/chatgpt_paraphraser_on_T5_base")
def paraphrase(text, num_return_sequences=3):
inputs = paraphrase_tokenizer(f"paraphrase: {text}", return_tensors="pt")
outputs = paraphrase_model.generate(
**inputs,
max_length=128,
num_return_sequences=num_return_sequences,
num_beams=5
)
return [paraphrase_tokenizer.decode(output, skip_special_tokens=True) for output in outputs]
paraphrases = paraphrase("당뇨병의 주요 증상은 무엇인가요?")
for i, p in enumerate(paraphrases, 1):
print(f"변형 {i}: {p}")
2. 커스텀 Loss 함수
# custom_loss.py
import torch
import torch.nn as nn
from transformers import Trainer
class WeightedLossTrainer(Trainer):
"""중요한 토큰에 더 높은 가중치를 부여하는 커스텀 Trainer"""
def __init__(self, *args, important_token_ids=None, weight_multiplier=2.0, **kwargs):
super().__init__(*args, **kwargs)
self.important_token_ids = important_token_ids or []
self.weight_multiplier = weight_multiplier
def compute_loss(self, model, inputs, return_outputs=False):
labels = inputs.pop("labels")
outputs = model(**inputs)
logits = outputs.logits
# Cross Entropy Loss 계산
loss_fct = nn.CrossEntropyLoss(reduction='none')
shift_logits = logits[..., :-1, :].contiguous()
shift_labels = labels[..., 1:].contiguous()
loss = loss_fct(
shift_logits.view(-1, shift_logits.size(-1)),
shift_labels.view(-1)
)
# 중요한 토큰에 가중치 부여
weights = torch.ones_like(shift_labels, dtype=torch.float)
for token_id in self.important_token_ids:
weights[shift_labels == token_id] *= self.weight_multiplier
weighted_loss = (loss * weights.view(-1)).mean()
return (weighted_loss, outputs) if return_outputs else weighted_loss
# 사용 예시
important_tokens = tokenizer.encode("당뇨병 고혈압 증상 치료", add_special_tokens=False)
trainer = WeightedLossTrainer(
model=model,
args=training_args,
train_dataset=train_dataset,
important_token_ids=important_tokens,
weight_multiplier=2.5
)
3. 멀티태스크 학습
# multitask_learning.py
from datasets import concatenate_datasets, load_from_disk
# 여러 작업의 데이터셋 로드
sentiment_dataset = load_from_disk("./sentiment_dataset")
qa_dataset = load_from_disk("./medical_qa_dataset")
summarization_dataset = load_from_disk("./summarization_dataset")
# 작업 식별자 추가
def add_task_prefix(example, task_name):
example["text"] = f"[TASK: {task_name}]\n{example['text']}"
return example
sentiment_with_task = sentiment_dataset.map(
lambda x: add_task_prefix(x, "SENTIMENT")
)
qa_with_task = qa_dataset.map(
lambda x: add_task_prefix(x, "QA")
)
summarization_with_task = summarization_dataset.map(
lambda x: add_task_prefix(x, "SUMMARIZATION")
)
# 데이터셋 결합
combined_dataset = concatenate_datasets([
sentiment_with_task,
qa_with_task,
summarization_with_task
])
# 셔플
combined_dataset = combined_dataset.shuffle(seed=42)
# 이제 combined_dataset으로 파인튜닝
평가 및 벤치마킹
정량적 평가
# evaluation.py
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel
from datasets import load_from_disk
from tqdm import tqdm
import numpy as np
# 모델 로드
base_model = AutoModelForCausalLM.from_pretrained(
"beomi/llama-2-ko-7b",
torch_dtype=torch.float16,
device_map="auto"
)
model = PeftModel.from_pretrained(base_model, "./medical-model-final")
tokenizer = AutoTokenizer.from_pretrained("beomi/llama-2-ko-7b")
# 테스트 데이터 로드
test_dataset = load_from_disk("./medical_qa_dataset")["test"]
# Perplexity 계산
def calculate_perplexity(model, tokenizer, texts):
model.eval()
total_loss = 0
total_tokens = 0
with torch.no_grad():
for text in tqdm(texts):
inputs = tokenizer(text, return_tensors="pt").to("cuda")
outputs = model(**inputs, labels=inputs["input_ids"])
loss = outputs.loss
num_tokens = inputs["input_ids"].numel()
total_loss += loss.item() * num_tokens
total_tokens += num_tokens
perplexity = torch.exp(torch.tensor(total_loss / total_tokens))
return perplexity.item()
texts = [example["text"] for example in test_dataset]
ppl = calculate_perplexity(model, tokenizer, texts)
print(f"Perplexity: {ppl:.2f}")
# BLEU, ROUGE 점수 계산
from nltk.translate.bleu_score import sentence_bleu
from rouge import Rouge
def evaluate_generation(model, tokenizer, questions, reference_answers):
model.eval()
bleu_scores = []
rouge = Rouge()
rouge_scores = []
for question, reference in tqdm(zip(questions, reference_answers)):
prompt = f"""### 질문:
{question}
### 답변:
"""
inputs = tokenizer(prompt, return_tensors="pt").to("cuda")
with torch.no_grad():
outputs = model.generate(
**inputs,
max_new_tokens=200,
temperature=0.7,
do_sample=True
)
generated = tokenizer.decode(outputs[0], skip_special_tokens=True)
generated = generated.split("### 답변:")[-1].strip()
# BLEU 점수
reference_tokens = reference.split()
generated_tokens = generated.split()
bleu = sentence_bleu([reference_tokens], generated_tokens)
bleu_scores.append(bleu)
# ROUGE 점수
try:
scores = rouge.get_scores(generated, reference)[0]
rouge_scores.append(scores)
except:
continue
avg_bleu = np.mean(bleu_scores)
avg_rouge1 = np.mean([s["rouge-1"]["f"] for s in rouge_scores])
avg_rouge2 = np.mean([s["rouge-2"]["f"] for s in rouge_scores])
avg_rougeL = np.mean([s["rouge-l"]["f"] for s in rouge_scores])
print(f"Average BLEU: {avg_bleu:.4f}")
print(f"Average ROUGE-1: {avg_rouge1:.4f}")
print(f"Average ROUGE-2: {avg_rouge2:.4f}")
print(f"Average ROUGE-L: {avg_rougeL:.4f}")
questions = [ex["question"] for ex in test_dataset]
answers = [ex["answer"] for ex in test_dataset]
evaluate_generation(model, tokenizer, questions, answers)
정성적 평가
# human_evaluation.py
import random
def create_evaluation_set(test_data, num_samples=50):
"""사람이 평가할 샘플 생성"""
samples = random.sample(test_data, num_samples)
results = []
for i, sample in enumerate(samples):
question = sample["question"]
reference = sample["answer"]
generated = generate_answer(model, tokenizer, question)
results.append({
"id": i,
"question": question,
"reference": reference,
"generated": generated,
"rating": None, # 1-5점 척도
"comments": ""
})
return results
# CSV로 저장하여 사람이 평가
import pandas as pd
eval_set = create_evaluation_set(test_dataset, num_samples=50)
df = pd.DataFrame(eval_set)
df.to_csv("human_evaluation.csv", index=False)
print("human_evaluation.csv 파일을 열어서 rating과 comments를 작성해주세요.")
모델 배포
1. HuggingFace Hub에 업로드
# upload_to_hub.py
from huggingface_hub import HfApi, create_repo
# 로그인 (토큰 필요)
from huggingface_hub import login
login(token="your_huggingface_token")
# 저장소 생성
repo_name = "my-finetuned-medical-llm"
create_repo(repo_name, private=True)
# 모델 업로드
model.push_to_hub(repo_name)
tokenizer.push_to_hub(repo_name)
# README 작성
readme = """---
language: ko
license: llama2
tags:
- medical
- healthcare
- korean
---
# Korean Medical LLM
이 모델은 의료 QA 데이터셋으로 파인튜닝된 한국어 LLaMA 모델입니다.
## 사용 예시
```python
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel
model = AutoModelForCausalLM.from_pretrained("beomi/llama-2-ko-7b")
model = PeftModel.from_pretrained(model, "your-username/my-finetuned-medical-llm")
tokenizer = AutoTokenizer.from_pretrained("your-username/my-finetuned-medical-llm")
# 추론 코드...
성능
- Perplexity: 15.2
- BLEU: 0.42
- ROUGE-L: 0.58
주의사항
이 모델은 교육 목적으로만 사용되어야 하며, 실제 의료 진단에 사용해서는 안 됩니다. “””
with open(“README.md”, “w”) as f: f.write(readme)
api = HfApi() api.upload_file( path_or_fileobj=”README.md”, path_in_repo=”README.md”, repo_id=f”your-username/{repo_name}” )
### 2. FastAPI로 서빙
```python
# serve_model.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel
import uvicorn
app = FastAPI(title="Medical LLM API")
# 모델 로드 (시작 시 한 번만)
print("Loading model...")
base_model = AutoModelForCausalLM.from_pretrained(
"beomi/llama-2-ko-7b",
torch_dtype=torch.float16,
device_map="auto"
)
model = PeftModel.from_pretrained(base_model, "./medical-model-final")
tokenizer = AutoTokenizer.from_pretrained("beomi/llama-2-ko-7b")
model.eval()
print("Model loaded!")
class QueryRequest(BaseModel):
question: str
max_tokens: int = 200
temperature: float = 0.7
class QueryResponse(BaseModel):
answer: str
tokens_used: int
@app.post("/generate", response_model=QueryResponse)
async def generate_answer(request: QueryRequest):
try:
prompt = f"""### 질문:
{request.question}
### 답변:
"""
inputs = tokenizer(prompt, return_tensors="pt").to("cuda")
with torch.no_grad():
outputs = model.generate(
**inputs,
max_new_tokens=request.max_tokens,
temperature=request.temperature,
do_sample=True,
pad_token_id=tokenizer.eos_token_id
)
answer = tokenizer.decode(outputs[0], skip_special_tokens=True)
answer = answer.split("### 답변:")[-1].strip()
return QueryResponse(
answer=answer,
tokens_used=len(outputs[0])
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/health")
async def health_check():
return {"status": "healthy"}
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
3. Docker 컨테이너화
# Dockerfile
FROM nvidia/cuda:11.8.0-cudnn8-runtime-ubuntu22.04
# Python 설치
RUN apt-get update && apt-get install -y \
python3.10 \
python3-pip \
&& rm -rf /var/lib/apt/lists/*
# 작업 디렉토리
WORKDIR /app
# 의존성 설치
COPY requirements.txt .
RUN pip3 install --no-cache-dir -r requirements.txt
# 모델과 코드 복사
COPY ./medical-model-final ./medical-model-final
COPY serve_model.py .
# 포트 노출
EXPOSE 8000
# 실행
CMD ["python3", "serve_model.py"]
# Docker 빌드 및 실행
docker build -t medical-llm-api .
docker run --gpus all -p 8000:8000 medical-llm-api
# 테스트
curl -X POST http://localhost:8000/generate \
-H "Content-Type: application/json" \
-d '{"question": "당뇨병의 증상은 무엇인가요?"}'
트러블슈팅
1. CUDA Out of Memory 오류
# 메모리 최적화 팁
# 1) Gradient Checkpointing 활성화
model.gradient_checkpointing_enable()
# 2) 배치 크기 줄이기
training_args = TrainingArguments(
per_device_train_batch_size=1, # 최소값
gradient_accumulation_steps=32, # 실질적 배치 크기 유지
)
# 3) Mixed Precision 사용
training_args = TrainingArguments(
fp16=True, # 또는 bf16=True
)
# 4) Optimizer 상태 오프로딩
training_args = TrainingArguments(
optim="adafactor", # 메모리 효율적인 옵티마이저
)
# 5) 시퀀스 길이 줄이기
training_args = TrainingArguments(
max_seq_length=512, # 1024 대신
)
2. 학습이 불안정할 때
# 안정적인 학습을 위한 설정
training_args = TrainingArguments(
learning_rate=1e-5, # 더 낮은 학습률
warmup_ratio=0.1, # 워밍업 증가
weight_decay=0.01, # 정규화
max_grad_norm=1.0, # Gradient Clipping
lr_scheduler_type="cosine", # 부드러운 학습률 감소
)
# LoRA alpha 조정
lora_config = LoraConfig(
r=16,
lora_alpha=32, # alpha = 2 * r (일반적 규칙)
)
3. 과적합 방지
# 과적합 방지 기법
# 1) 데이터 증강
from nlpaug.augmenter.word import SynonymAug
aug = SynonymAug()
augmented_text = aug.augment(original_text)
# 2) Dropout 증가
lora_config = LoraConfig(
lora_dropout=0.1, # 기본값 0.05 대신
)
# 3) Early Stopping
from transformers import EarlyStoppingCallback
trainer = Trainer(
callbacks=[EarlyStoppingCallback(early_stopping_patience=3)]
)
# 4) 정규화
training_args = TrainingArguments(
weight_decay=0.01,
)
4. 생성 품질이 낮을 때
# 생성 품질 개선
# 1) 다양한 디코딩 전략 시도
outputs = model.generate(
**inputs,
# Beam Search
num_beams=5,
no_repeat_ngram_size=3,
# Top-k + Top-p Sampling
do_sample=True,
top_k=50,
top_p=0.95,
temperature=0.7,
# Repetition Penalty
repetition_penalty=1.2,
# Length Penalty
length_penalty=1.0,
)
# 2) 프롬프트 엔지니어링
prompt = f"""당신은 전문 의료 AI입니다. 정확하고 상세하게 답변해주세요.
### 질문:
{question}
### 답변:
"""
# 3) Few-shot 예시 추가
prompt = f"""아래는 의료 질문에 대한 전문적인 답변 예시입니다.
질문: 고혈압의 정상 수치는?
답변: 정상 혈압은 수축기 120mmHg 미만, 이완기 80mmHg 미만입니다.
질문: {question}
답변:"""
성능 비교 및 선택 가이드
모델 크기별 추천
| 모델 크기 | 추천 용도 | 필요 GPU | 파인튜닝 방법 |
|---|---|---|---|
| 1B-3B | 간단한 분류, 챗봇 | GTX 1080 Ti | Full / LoRA |
| 7B | 일반적인 QA, 생성 | RTX 3090 | LoRA / QLoRA |
| 13B | 전문 도메인, 복잡한 추론 | A100 40GB | QLoRA |
| 30B-70B | 최고 품질 필요 시 | 2x A100 | QLoRA |
데이터셋 크기별 가이드
- 100-500 샘플: Few-shot prompting 고려
- 500-2,000 샘플: LoRA 파인튜닝 시작
- 2,000-10,000 샘플: Full LoRA 효과
- 10,000+ 샘플: Full fine-tuning 고려
실전 체크리스트
파인튜닝 시작 전
- 명확한 목표 설정 (작업, 성능 지표)
- 충분한 데이터 확보 (최소 500개)
- 데이터 품질 검증 (오타, 포맷 일관성)
- 베이스 모델 선택 (언어, 크기 고려)
- GPU 리소스 확인
학습 중
- 학습 곡선 모니터링 (Loss, Perplexity)
- 주기적으로 샘플 생성 테스트
- GPU 메모리 사용량 체크
- 과적합 징후 확인 (Train/Val loss 차이)
학습 후
- 정량적 평가 (BLEU, ROUGE, Perplexity)
- 정성적 평가 (사람의 판단)
- 다양한 엣지 케이스 테스트
- 베이스 모델과 비교
- 배포 전 안전성 검증
추가 리소스
유용한 오픈소스 도구
- Axolotl: 파인튜닝 자동화 프레임워크
- LM Studio: GUI 기반 파인튜닝 툴
- Weights & Biases: 실험 추적 및 시각화
- vLLM: 빠른 추론 서버
추천 학습 자료
- HuggingFace Transformers 공식 문서
- PEFT (Parameter-Efficient Fine-Tuning) 라이브러리
- QLoRA 논문: “QLoRA: Efficient Finetuning of Quantized LLMs”
- LoRA 논문: “LoRA: Low-Rank Adaptation of Large Language Models”
데이터셋 소스
- HuggingFace Datasets: 수천 개의 공개 데이터셋
- AI Hub (한국어): 한국어 NLP 데이터셋
- OpenOrca: 고품질 instruction 데이터
- Alpaca: Instruction following 데이터
마무리
오픈소스 LLM 파인튜닝은 처음에는 복잡해 보이지만, 적절한 도구와 기법을 사용하면 비교적 적은 리소스로도 훌륭한 결과를 얻을 수 있습니다.
핵심 포인트:
- 목적에 맞는 방법 선택: Full fine-tuning vs LoRA vs QLoRA
- 품질 좋은 데이터: 양보다 질이 중요
- 체계적인 평가: 정량적 + 정성적 평가 병행
- 반복적 개선: 실험과 분석을 통한 지속적 개선
이 가이드의 예제 코드를 기반으로 자신만의 특화된 LLM을 만들어보세요. 질문이나 문제가 있다면 HuggingFace 포럼이나 GitHub Issues를 활용하시기 바랍니다.
다음 글에서는 파인튜닝된 모델을 프로덕션 환경에 배포하고 모니터링하는 방법을 다루겠습니다.