앞선 장에서 우리는 SLM(Small Language Model)과 Ollama 환경을 구축하고,
Hugging Face 모델을 불러오는 과정을 모두 끝냈다.
이제는 그 모델을 내 데이터로 파인튜닝(fine-tuning) 하고,
그 결과를 Perplexity 및 생성 예시로 평가한다.
이번 장은 두 파트로 구성된다.
| 구분 | 내용 |
|---|---|
| 4장 | LoRA/QLoRA 기반 파인튜닝 수행 |
| 5장 | 학습 결과 모델의 정량·정성 평가 및 로그 관리 |
pip install -U transformers datasets peft accelerate bitsandbytes
이전 장에서 만든 train.jsonl 파일에는 instruction–response 쌍이 포함되어 있다.
from datasets import load_dataset
dataset = load_dataset("json", data_files="train.jsonl")
print(dataset)
print(dataset["train"][0])
데이터는 다음과 같이 구성되어 있어야 한다.
{"text": "### Instruction:\nExplain regression vs classification.\n### Response:\nRegression predicts continuous values..."}
from transformers import AutoTokenizer, AutoModelForCausalLM
BASE_MODEL = "facebook/opt-1.3b"
tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL, use_fast=True)
# pad_token 지정
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
model = AutoModelForCausalLM.from_pretrained(BASE_MODEL)
model.resize_token_embeddings(len(tokenizer))
from peft import LoraConfig, get_peft_model
lora_cfg = LoraConfig(
r=8, # 랭크 (조정 강도)
lora_alpha=16, # LoRA 스케일링
target_modules=["q_proj", "v_proj"],
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM",
)
model = get_peft_model(model, lora_cfg)
print("LoRA 설정 완료")
언어모델은 labels가 필요하다.
labels = input_ids로 설정하되, padding 부분은 -100으로 마스킹한다.
def tokenize_function(batch):
enc = tokenizer(batch["text"], truncation=True, padding="max_length", max_length=512)
labels = [
[tok if tok != tokenizer.pad_token_id else -100 for tok in ids]
for ids in enc["input_ids"]
]
enc["labels"] = labels
return enc
tokenized = dataset.map(tokenize_function, batched=True, remove_columns=["text"])
from transformers import Trainer, TrainingArguments, DataCollatorForLanguageModeling
import torch
args = TrainingArguments(
output_dir="./lora-out",
num_train_epochs=2,
per_device_train_batch_size=2,
gradient_accumulation_steps=4,
learning_rate=2e-4,
logging_steps=20,
save_strategy="epoch",
report_to="none",
fp16=torch.cuda.is_available(),
)
trainer = Trainer(
model=model,
args=args,
train_dataset=tokenized["train"],
data_collator=DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False),
)
trainer.train()
첫 학습에서는 작은 데이터(100개 이하)로 테스트한 뒤 전체 데이터로 확장하는 것이 좋다.
model.save_pretrained("./lora-out/adapter")
tokenizer.save_pretrained("./lora-out/tokenizer")
print("파인튜닝 모델 저장 완료")
이제 학습이 끝났으니, 다음 단계로 넘어가자.
모델이 얼마나 좋아졌는지를 평가해보는 것이다.
훈련용 데이터에서 5%를 분리하여 평가용으로 사용한다.
from datasets import load_dataset
dataset = load_dataset("json", data_files="train.jsonl")
split = dataset["train"].train_test_split(test_size=0.05, seed=42)
eval_data = split["test"]
print(f"평가 데이터 수: {len(eval_data)}")
from transformers import AutoModelForCausalLM, AutoTokenizer, DataCollatorForLanguageModeling
from peft import PeftModel
import torch
BASE_MODEL = "facebook/opt-1.3b"
ADAPTER_DIR = "./lora-out/adapter"
TOKENIZER_DIR = "./lora-out/tokenizer"
# 토크나이저 로드
tokenizer = AutoTokenizer.from_pretrained(TOKENIZER_DIR, use_fast=True)
tokenizer.pad_token = tokenizer.eos_token
# 기본 모델 로드
base_model = AutoModelForCausalLM.from_pretrained(BASE_MODEL)
# 토크나이저의 어휘 크기에 맞춰 모델의 임베딩 레이어 크기 조정
base_model.resize_token_embeddings(len(tokenizer))
# LoRA 어댑터 로드
model = PeftModel.from_pretrained(base_model, ADAPTER_DIR)
model.eval()
from transformers import Trainer, TrainingArguments
import math
def tokenize_eval(batch):
enc = tokenizer(batch["text"], truncation=True, padding="max_length", max_length=512)
enc["labels"] = [
[tok if tok != tokenizer.pad_token_id else -100 for tok in ids]
for ids in enc["input_ids"]
]
return enc
eval_tokenized = eval_data.map(tokenize_eval, batched=True, remove_columns=["text"])
collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False)
args = TrainingArguments(
output_dir="./eval-out",
per_device_eval_batch_size=2,
dataloader_drop_last=False,
report_to="none",
fp16=torch.cuda.is_available(),
)
trainer = Trainer(
model=model,
args=args,
eval_dataset=eval_tokenized,
data_collator=collator,
)
result = trainer.evaluate()
loss = result["eval_loss"]
ppl = math.exp(loss)
print(f"평가 손실(loss): {loss:.4f}")
print(f"Perplexity(PPL): {ppl:.2f}")
from transformers import pipeline
prompt = """### Instruction:
Explain the difference between regression and classification.
### Response:
"""
gen = pipeline(
"text-generation",
model=model,
tokenizer=tokenizer,
device_map="auto" if torch.cuda.is_available() else None
)
output = gen(prompt, max_new_tokens=120, do_sample=False)[0]["generated_text"]
print("=== 모델 생성 결과 ===\n")
print(output)
이제 파인튜닝된 모델이 instruction-response 구조를 명확히 따르고,
훈련 데이터의 문체를 반영하는지 직접 확인할 수 있다.
import csv, os, time
from pathlib import Path
Path("experiments").mkdir(exist_ok=True)
log_file = "experiments/results.csv"
new_file = not os.path.exists(log_file)
row = [int(time.time()), BASE_MODEL, ADAPTER_DIR, f"{loss:.4f}", f"{ppl:.2f}"]
fields = ["timestamp", "model", "adapter", "eval_loss", "ppl"]
with open(log_file, "a", newline="", encoding="utf-8") as f:
w = csv.writer(f)
if new_file:
w.writerow(fields)
w.writerow(row)
print(f"평가 결과가 '{log_file}'에 저장되었습니다.")
지금까지 우리는
다음 6장에서는 이 모델을 Ollama에서 직접 실행 가능한 형태(GGUF 변환 + Modelfile 생성) 로 내보내는 배포 단계를 다룬다.