데이터 품질이 결정적
"Garbage in, garbage out" — 파인튜닝 결과는 데이터 품질에 달려 있습니다.
flowchart LR
RAW["원시 데이터\n수집"]
CLEAN["정제\n(중복·오류 제거)"]
FORMAT["포맷 변환\n(지시-응답 형식)"]
SPLIT["분할\n(train/val/test)"]
UPLOAD["업로드\n(HuggingFace Hub)"]
RAW --> CLEAN --> FORMAT --> SPLIT --> UPLOAD
데이터 수집 방법
# 1. 기존 데이터에서 추출 (고객 Q&A, 지원 티켓 등)
import pandas as pd
support_tickets = pd.read_csv("support_tickets.csv")
# 해결된 티켓만 사용 (질문-답변 쌍)
resolved = support_tickets[support_tickets["status"] == "resolved"]
qa_pairs = resolved[["question", "answer"]].dropna()
# 2. Hugging Face 공개 데이터셋 활용
from datasets import load_dataset
dataset = load_dataset("databricks/databricks-dolly-15k")
print(dataset["train"][0])
# {'instruction': '...', 'context': '...', 'response': '...', 'category': '...'}
# 3. LLM으로 합성 데이터 생성 (Self-Instruct)
from openai import OpenAI
client = OpenAI()
def generate_qa_pair(topic: str) -> dict:
response = client.chat.completions.create(
model="gpt-4o",
messages=[{
"role": "user",
"content": f"{topic}에 관한 전문적인 Q&A 쌍을 JSON으로 생성해줘."
}],
response_format={"type": "json_object"}
)
return json.loads(response.choices[0].message.content)
데이터 정제
import re
from datasets import Dataset
def clean_text(text: str) -> str:
# 이상한 공백 제거
text = re.sub(r'\s+', ' ', text).strip()
# HTML 태그 제거
text = re.sub(r'<[^>]+>', '', text)
# URL 제거 (선택적)
text = re.sub(r'https?://\S+', '[URL]', text)
return text
def is_quality_sample(sample: dict) -> bool:
instruction = sample.get("instruction", "")
output = sample.get("output", "")
# 최소 길이
if len(instruction) < 10 or len(output) < 20:
return False
# 최대 길이 (토큰 초과 방지)
if len(instruction) > 2000 or len(output) > 4000:
return False
# 유해 콘텐츠 필터 (간단한 예시)
forbidden = ["욕설", "개인정보"]
if any(word in output for word in forbidden):
return False
return True
# 필터 적용
raw_data = [...] # 원시 데이터
cleaned = [
{
"instruction": clean_text(d["instruction"]),
"output": clean_text(d["output"]),
}
for d in raw_data
if is_quality_sample(d)
]
print(f"원본: {len(raw_data)}, 정제 후: {len(cleaned)}")
ChatML 포맷 변환
def to_chat_format(sample: dict) -> dict:
"""지시-응답 데이터를 ChatML 대화 형식으로 변환"""
messages = []
if system_prompt := sample.get("system"):
messages.append({"role": "system", "content": system_prompt})
user_content = sample["instruction"]
if context := sample.get("input"):
user_content = f"컨텍스트:\n{context}\n\n질문: {user_content}"
messages.append({"role": "user", "content": user_content})
messages.append({"role": "assistant", "content": sample["output"]})
return {"messages": messages}
# 전체 데이터셋 변환
chat_data = [to_chat_format(d) for d in cleaned]
# HuggingFace Dataset 객체로 변환
dataset = Dataset.from_list(chat_data)
학습/검증/테스트 분할
from datasets import DatasetDict
# 8:1:1 분할
splits = dataset.train_test_split(test_size=0.2, seed=42)
train_val = splits["train"]
test = splits["test"]
train_val_splits = train_val.train_test_split(test_size=0.1, seed=42)
final_dataset = DatasetDict({
"train": train_val_splits["train"], # 72%
"validation": train_val_splits["test"], # 8%
"test": test, # 20%
})
print(final_dataset)
# DatasetDict({
# train: Dataset({features: [...], num_rows: 7200})
# validation: Dataset({features: [...], num_rows: 800})
# test: Dataset({features: [...], num_rows: 2000})
# })
데이터 통계 확인
import matplotlib.pyplot as plt
# 길이 분포 확인
instruction_lengths = [len(d["messages"][1]["content"]) for d in chat_data]
output_lengths = [len(d["messages"][-1]["content"]) for d in chat_data]
print(f"지시 평균 길이: {sum(instruction_lengths)/len(instruction_lengths):.0f}자")
print(f"응답 평균 길이: {sum(output_lengths)/len(output_lengths):.0f}자")
print(f"지시 최대 길이: {max(instruction_lengths)}자")
print(f"응답 최대 길이: {max(output_lengths)}자")
정리
| 단계 | 핵심 작업 |
|---|---|
| 수집 | 기존 데이터 추출, 공개 데이터셋, LLM 생성 |
| 정제 | 중복 제거, 길이 필터, 품질 기준 적용 |
| 포맷 | 지시-응답 or 대화(ChatML) 형식 |
| 분할 | train/validation/test (8:1:1) |
| 검증 | 길이 분포, 샘플 확인 |
다음 편에서는 OpenAI 파인튜닝 — API를 통해 GPT 모델을 직접 파인튜닝하는 방법을 배웁니다.