서론: 왜 학습 데이터가 모델 아키텍처보다 중요한가
2024년, Microsoft Research의 Phi-3 논문은 업계에 충격을 줬습니다. 3.8B 파라미터 모델이 7B~13B 모델을 능가하는 성능을 보였고, 그 비결은 **철저하게 큐레이션된 고품질 학습 데이터**였습니다. Meta의 LIMA 논문("Less Is More for Alignment")은 단 1,000개의 고품질 샘플로도 GPT-4에 근접한 성능을 달성할 수 있음을 보여줬습니다.
"Data is the new oil"이라는 말은 이제 진부하지만, LLM 시대에는 그 어느 때보다 정확합니다. 모델 아키텍처의 혁신(Transformer, MoE, State Space Models)도 중요하지만, **같은 아키텍처라도 학습 데이터의 품질과 다양성에 따라 성능이 천지 차이**입니다.
한국어 LLM 생태계도 빠르게 성장하고 있습니다:
| 모델 | 개발사 | 파라미터 | 특징 |
|------|--------|----------|------|
| SOLAR | Upstage | 10.7B | Depth Up-Scaling, 한국어 특화 |
| EXAONE | LG AI Research | 7.8B | 기업용 한국어 LLM |
| HyperCLOVA X | NAVER | 비공개 | 한국어 최대 규모 |
| Qwen-KO | 커뮤니티 | 다양 | Qwen 기반 한국어 파인튜닝 |
| KULLM | 고려대 | 13B | 한국어 오픈소스 LLM |
| Polyglot-Ko | EleutherAI | 12.8B | 한국어 사전학습 모델 |
이 모든 모델의 성능을 좌우하는 것은 결국 **학습 데이터**입니다. 이 가이드에서는 Hugging Face 데이터셋 활용법부터 한국어 데이터 수집, 전처리, Instruction Tuning 포맷, RLHF/DPO 데이터셋 구축까지 전 과정을 다룹니다.
1. Hugging Face Datasets 딥다이브
1.1 플랫폼 개요
Hugging Face는 2023년 기준 15만 개 이상의 데이터셋을 호스팅하는 ML 커뮤니티 플랫폼입니다. 데이터셋 뷰어, 다운로드 통계, 자동 문서화 등의 기능을 제공합니다.
**핵심 기능:**
- **Dataset Viewer**: 브라우저에서 바로 데이터 미리보기
- **Download Stats**: 월간 다운로드 수 확인
- **Dataset Card**: 데이터셋 메타데이터, 라이선스, 사용법 문서
- **Streaming**: 전체 다운로드 없이 스트리밍 로드
- **Git LFS**: 대용량 파일 버전 관리
1.2 데이터셋 유형별 분류
사전학습(Pre-training) 데이터
대규모 텍스트 코퍼스로, 모델의 기본 언어 이해력을 형성합니다.
| 데이터셋 | 크기 | 언어 | 설명 |
|----------|------|------|------|
| CC-100 | 2.5TB | 100+언어 | Common Crawl 기반 정제 코퍼스 |
| mC4 | 27TB | 101언어 | Google의 다국어 C4 |
| Korean Wikipedia | ~1GB | 한국어 | 위키피디아 한국어판 전문 |
| Namuwiki | ~5GB | 한국어 | 나무위키 덤프 (비상업적 용도) |
| KCC (Korean Crawl Corpus) | ~30GB | 한국어 | 한국어 웹 크롤 데이터 |
| OSCAR | 다양 | 다국어 | Common Crawl 기반 분류된 코퍼스 |
SFT/Instruction Tuning 데이터
LLM이 지시를 따르도록 학습하는 핵심 데이터입니다.
| 데이터셋 | 크기 | 포맷 | 설명 |
|----------|------|------|------|
| Alpaca (Stanford) | 52K | instruction/input/output | Self-Instruct로 생성 |
| ShareGPT | 90K+ | conversations | 실제 ChatGPT 대화 수집 |
| LIMA | 1K | instruction/output | 수작업 큐레이션 고품질 |
| OpenOrca | 4M | instruction/output | GPT-4 응답 포함 |
| Dolly 2.0 | 15K | instruction/output | 수작업, 상업적 사용 가능 |
| FLAN Collection | 1836 tasks | 다양 | Google의 대규모 Instruction 모음 |
RLHF/DPO 데이터
인간 선호도를 반영하는 정렬(Alignment) 데이터입니다.
| 데이터셋 | 크기 | 구조 | 설명 |
|----------|------|------|------|
| HH-RLHF (Anthropic) | 170K | chosen/rejected | 도움됨 + 무해함 선호도 |
| UltraFeedback | 64K | 4점 척도 | GPT-4 기반 자동 평가 |
| Nectar | 183K | ranked list | 7개 모델 응답 순위 |
| Chatbot Arena | 지속 갱신 | ELO 점수 | 인간 블라인드 비교 |
평가(Evaluation) 벤치마크
| 벤치마크 | 영역 | 한국어 지원 |
|----------|------|------------|
| MMLU | 57개 학문 분야 | 번역 버전 존재 |
| ARC | 과학 추론 | 번역 버전 |
| HellaSwag | 상식 추론 | 번역 버전 |
| KoBBQ | 편향 평가 | 한국어 네이티브 |
| KLUE | 한국어 NLU | 한국어 네이티브 |
| KorNAT | 한국어 상식 | 한국어 네이티브 |
1.3 한국어 특화 데이터셋
한국어 LLM 데이터셋 생태계
├── 사전학습
│ ├── Korean Wikipedia (~600K articles)
│ ├── Namuwiki Dump (~5GB)
│ ├── AI Hub 말뭉치 (국립국어원)
│ └── mC4-ko (Korean subset)
├── Instruction Tuning
│ ├── KoAlpaca (beomi) - 52K
│ ├── KoVicuna (melodysdreamj) - 40K+
│ ├── KOpen-platypus - 25K
│ ├── ko_wikidata_QA - 위키 기반 QA
│ └── kullm-v2 (고려대) - 152K
├── 선호도/정렬
│ ├── ko-rlhf (커뮤니티)
│ └── KoreanFeedback (자체 구축)
└── 평가
├── KLUE (8 tasks)
├── KoBBQ (편향)
└── KorNAT (상식)
1.4 datasets 라이브러리 실전 활용
기본 로딩 및 탐색
from datasets import load_dataset, Dataset, DatasetDict
기본 로딩
ds = load_dataset("beomi/KoAlpaca-v1.1a")
print(ds)
DatasetDict({
train: Dataset({
features: ['instruction', 'output'],
num_rows: 21155
})
})
특정 split 로딩
train_ds = load_dataset("beomi/KoAlpaca-v1.1a", split="train")
처음 5개 확인
for example in train_ds.select(range(5)):
print(f"Instruction: {example['instruction'][:50]}...")
print(f"Output: {example['output'][:50]}...")
print("---")
필터링 및 변환
길이 기반 필터링
filtered_ds = ds["train"].filter(
lambda x: len(x["instruction"]) > 10 and len(x["output"]) > 20
)
print(f"필터링 후: {len(filtered_ds)} / {len(ds['train'])}")
Alpaca 포맷으로 변환
def format_alpaca(example):
text = f"""### Instruction:
{example['instruction']}
Response:
{example['output']}"""
return {"text": text}
formatted_ds = filtered_ds.map(format_alpaca)
토크나이저 적용
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("beomi/llama-2-ko-7b")
def tokenize_function(examples):
return tokenizer(
examples["text"],
truncation=True,
max_length=2048,
padding="max_length",
)
tokenized_ds = formatted_ds.map(
tokenize_function,
batched=True,
remove_columns=formatted_ds.column_names,
)
스트리밍 모드 (대용량 데이터)
스트리밍으로 대용량 데이터 처리 (메모리 효율적)
streaming_ds = load_dataset(
"allenai/c4",
"ko",
split="train",
streaming=True,
)
처음 100개만 순회
for i, example in enumerate(streaming_ds):
if i >= 100:
break
process(example["text"])
스트리밍 + 필터링 + 배치 처리
filtered_stream = streaming_ds.filter(
lambda x: len(x["text"]) > 100
).take(10000)
배치 단위로 처리
batch = []
for example in filtered_stream:
batch.append(example)
if len(batch) == 32:
process_batch(batch)
batch = []
Hugging Face Hub에 업로드
from datasets import Dataset
데이터프레임에서 데이터셋 생성
df = pd.DataFrame({
"instruction": ["한국의 수도는?", "파이썬이란?"],
"output": ["한국의 수도는 서울입니다.", "파이썬은 프로그래밍 언어입니다."],
})
my_dataset = Dataset.from_pandas(df)
Hub에 업로드
my_dataset.push_to_hub(
"my-org/my-korean-dataset",
private=True, # 비공개 설정
token="hf_xxxxx",
)
Dataset Card 자동 생성
from huggingface_hub import DatasetCard
card = DatasetCard.load("my-org/my-korean-dataset")
card.text = """
My Korean Dataset
한국어 Instruction Tuning 데이터셋입니다.
데이터 구조
- instruction: 질문/지시문
- output: 응답
라이선스
CC-BY-4.0
"""
card.push_to_hub("my-org/my-korean-dataset")
2. 한국어 데이터 수집 방법
2.1 웹 크롤링
newspaper3k를 이용한 뉴스 크롤링
from newspaper import Article
def crawl_article(url):
"""뉴스 기사 크롤링 (robots.txt 준수 필수!)"""
article = Article(url, language="ko")
article.download()
article.parse()
return {
"title": article.title,
"text": article.text,
"publish_date": str(article.publish_date),
"source_url": url,
}
Scrapy를 이용한 대규모 크롤링
scrapy_spider.py
"""
class KoreanTextSpider(scrapy.Spider):
name = 'korean_text'
custom_settings = {
'ROBOTSTXT_OBEY': True, # robots.txt 준수 필수
'DOWNLOAD_DELAY': 2, # 2초 간격
'CONCURRENT_REQUESTS': 4, # 동시 요청 제한
}
def parse(self, response):
text = response.css('article::text').getall()
yield {
'url': response.url,
'text': ' '.join(text),
}
"""
**크롤링 시 주의사항:**
- `robots.txt` 반드시 준수
- 요청 간격 최소 1~2초
- 저작권/라이선스 확인
- 개인정보 필터링 필수
2.2 공공 데이터 소스
| 소스 | URL | 데이터 유형 | 라이선스 |
|------|-----|------------|---------|
| AI Hub | aihub.or.kr | 다양한 한국어 말뭉치 | 공공 |
| 모두의말뭉치 | corpus.korean.go.kr | 문어/구어 코퍼스 | CC BY |
| NIKL (국립국어원) | korean.go.kr | 표준 말뭉치 | 학술용 |
| 공공데이터포털 | data.go.kr | 정부 공공데이터 | 공공 |
AI Hub 데이터 로딩 예시
def load_aihub_data(data_dir):
"""AI Hub JSON 포맷 데이터 로딩"""
all_data = []
for filepath in glob.glob(f"{data_dir}/**/*.json", recursive=True):
with open(filepath, "r", encoding="utf-8") as f:
data = json.load(f)
AI Hub 형식에 따라 파싱
if "document" in data:
for doc in data["document"]:
for sent in doc.get("sentence", []):
all_data.append({
"text": sent.get("form", ""),
"source": "aihub",
})
return all_data
2.3 번역 기반 데이터 생성
NLLB (No Language Left Behind)를 이용한 번역
from transformers import AutoModelForSeq2SeqLM, AutoTokenizer
model_name = "facebook/nllb-200-distilled-600M"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForSeq2SeqLM.from_pretrained(model_name)
def translate_en_to_ko(text):
"""영어 -> 한국어 번역"""
tokenizer.src_lang = "eng_Latn"
inputs = tokenizer(text, return_tensors="pt", max_length=512, truncation=True)
translated = model.generate(
**inputs,
forced_bos_token_id=tokenizer.convert_tokens_to_ids("kor_Hang"),
max_length=512,
)
return tokenizer.decode(translated[0], skip_special_tokens=True)
번역 품질 검증
def validate_translation(original, translated):
"""번역 품질 자동 검증"""
checks = {
"not_empty": len(translated.strip()) > 0,
"not_too_short": len(translated) > len(original) * 0.3,
"not_too_long": len(translated) < len(original) * 3,
"no_english_majority": sum(1 for c in translated if c.isascii()) / max(len(translated), 1) < 0.5,
}
return all(checks.values()), checks
2.4 합성 데이터 생성
Self-Instruct 방식
Self-Instruct: 시드 데이터에서 새로운 지시문 생성
SEED_INSTRUCTIONS = [
"한국의 4계절에 대해 설명해주세요.",
"파이썬에서 리스트 컴프리헨션의 장점은?",
"이메일 작성 시 주의사항을 알려주세요.",
]
def generate_new_instructions(seed_instructions, num_generate=10):
"""GPT-4로 새로운 instruction 생성"""
prompt = f"""다음은 한국어 지시문 예시입니다:
{chr(10).join(f'{i+1}. {inst}' for i, inst in enumerate(seed_instructions))}
위 예시와 비슷한 스타일이지만 완전히 새로운 한국어 지시문을 {num_generate}개 생성하세요.
다양한 주제(과학, 역사, 기술, 일상생활 등)를 포함하세요.
각 지시문은 번호와 함께 한 줄로 작성하세요."""
client = openai.OpenAI()
response = client.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": prompt}],
temperature=0.8,
)
return parse_instructions(response.choices[0].message.content)
def generate_response(instruction):
"""지시문에 대한 응답 생성"""
client = openai.OpenAI()
response = client.chat.completions.create(
model="gpt-4",
messages=[
{"role": "system", "content": "당신은 도움이 되는 한국어 AI 어시스턴트입니다."},
{"role": "user", "content": instruction},
],
temperature=0.7,
)
return response.choices[0].message.content
Evol-Instruct 방식
def evolve_instruction(instruction, evolution_type="deepen"):
"""WizardLM의 Evol-Instruct: 지시문을 점진적으로 복잡하게 만들기"""
evolution_prompts = {
"deepen": f"""다음 지시문을 더 깊이 있고 구체적으로 만들어주세요.
원본: {instruction}
진화된 버전:""",
"broaden": f"""다음 지시문의 범위를 넓혀서 더 포괄적으로 만들어주세요.
원본: {instruction}
진화된 버전:""",
"concretize": f"""다음 지시문에 구체적인 조건이나 제약을 추가해주세요.
원본: {instruction}
진화된 버전:""",
"reasoning": f"""다음 지시문을 단계적 추론이 필요한 형태로 변환해주세요.
원본: {instruction}
진화된 버전:""",
}
client = openai.OpenAI()
response = client.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": evolution_prompts[evolution_type]}],
temperature=0.7,
)
return response.choices[0].message.content
2.5 커뮤니티 데이터 소스
- **나무위키**: 풍부한 한국어 콘텐츠 (비상업적 CC-BY-NC-SA)
- **한국어 Reddit**: r/korea, r/hanguk 등
- **Stack Overflow 한국어**: 기술 Q&A
- **네이버 지식iN**: 크롤링 주의 (이용약관 확인)
- **한국어 위키백과**: CC-BY-SA 라이선스
3. 데이터 전처리 파이프라인
3.1 전체 파이프라인 아키텍처
원본 데이터
│
▼
┌─────────────────┐
│ 1. 텍스트 정제 │ HTML 태그 제거, 인코딩 정리
└────────┬────────┘
▼
┌─────────────────┐
│ 2. 언어 감지 │ 한국어 텍스트만 필터링
└────────┬────────┘
▼
┌─────────────────┐
│ 3. 중복 제거 │ MinHash LSH, Exact Match
└────────┬────────┘
▼
┌─────────────────┐
│ 4. 품질 필터링 │ Perplexity, 길이, 독성
└────────┬────────┘
▼
┌─────────────────┐
│ 5. PII 제거 │ 개인정보 마스킹
└────────┬────────┘
▼
┌─────────────────┐
│ 6. 토크나이징 │ SentencePiece / BPE
└────────┬────────┘
▼
정제된 데이터
3.2 텍스트 정제
def clean_text(text):
"""한국어 텍스트 기본 정제"""
HTML 엔티티 디코딩
text = html.unescape(text)
HTML 태그 제거
text = re.sub(r'<[^>]+>', '', text)
URL 제거
text = re.sub(r'https?://\S+|www\.\S+', '', text)
이메일 제거
text = re.sub(r'\S+@\S+\.\S+', '[EMAIL]', text)
전화번호 마스킹
text = re.sub(r'\d{2,3}-\d{3,4}-\d{4}', '[PHONE]', text)
연속 공백 정리
text = re.sub(r'\s+', ' ', text)
Unicode 정규화 (NFC)
text = unicodedata.normalize('NFC', text)
한국어에 불필요한 특수문자 제거 (기본 문장부호 유지)
text = re.sub(r'[^\w\s가-힣ㄱ-ㅎㅏ-ㅣa-zA-Z0-9.,!?;:\'\"()\-]', '', text)
return text.strip()
def clean_korean_specific(text):
"""한국어 특화 정제"""
자음/모음만 있는 경우 제거 (ㅋㅋㅋ, ㅎㅎㅎ 등은 상황에 따라)
광고성 텍스트 패턴 제거
ad_patterns = [
r'지금\s*바로\s*클릭',
r'무료\s*상담',
r'카카오톡?\s*문의',
r'전화\s*주세요',
]
for pattern in ad_patterns:
if re.search(pattern, text):
return None # 광고성으로 판단되면 제거
return text
배치 처리
def clean_batch(texts):
"""배치 단위 정제"""
cleaned = []
for text in texts:
result = clean_text(text)
result = clean_korean_specific(result)
if result and len(result) > 20:
cleaned.append(result)
return cleaned
3.3 중복 제거 (Deduplication)
from datasketch import MinHash, MinHashLSH
class TextDeduplicator:
"""MinHash LSH 기반 근사 중복 제거"""
def __init__(self, threshold=0.8, num_perm=128):
self.threshold = threshold
self.num_perm = num_perm
self.lsh = MinHashLSH(threshold=threshold, num_perm=num_perm)
self.seen_exact = set()
def get_minhash(self, text):
"""텍스트의 MinHash 생성"""
m = MinHash(num_perm=self.num_perm)
3-gram 단위로 분할
for i in range(len(text) - 2):
m.update(text[i:i+3].encode('utf-8'))
return m
def is_duplicate(self, text, doc_id):
"""중복 여부 확인"""
1. 정확 매칭 (해시 기반)
text_hash = hashlib.md5(text.encode('utf-8')).hexdigest()
if text_hash in self.seen_exact:
return True
self.seen_exact.add(text_hash)
2. 근사 매칭 (MinHash LSH)
minhash = self.get_minhash(text)
result = self.lsh.query(minhash)
if result:
return True
self.lsh.insert(doc_id, minhash)
return False
def deduplicate(self, documents):
"""문서 리스트 중복 제거"""
unique_docs = []
for i, doc in enumerate(documents):
if not self.is_duplicate(doc["text"], f"doc_{i}"):
unique_docs.append(doc)
print(f"중복 제거: {len(documents)} -> {len(unique_docs)} "
f"({len(documents) - len(unique_docs)}개 제거)")
return unique_docs
3.4 언어 감지 필터링
fasttext 언어 감지 모델 로드
model_path = "lid.176.bin" # 사전 다운로드 필요
lang_model = fasttext.load_model(model_path)
def detect_language(text):
"""텍스트 언어 감지"""
줄바꿈 제거 (fasttext는 한 줄 입력)
text_clean = text.replace('\n', ' ')[:200]
predictions = lang_model.predict(text_clean)
lang = predictions[0][0].replace('__label__', '')
confidence = predictions[1][0]
return lang, confidence
def filter_korean(documents, min_confidence=0.7):
"""한국어 텍스트만 필터링"""
korean_docs = []
for doc in documents:
lang, conf = detect_language(doc["text"])
if lang == "ko" and conf >= min_confidence:
korean_docs.append(doc)
return korean_docs
3.5 품질 필터링
from transformers import AutoModelForCausalLM, AutoTokenizer
class QualityFilter:
"""텍스트 품질 필터링"""
def __init__(self):
self.criteria = {
"min_length": 50,
"max_length": 10000,
"min_words": 10,
"max_repetition_ratio": 0.3,
"max_special_char_ratio": 0.1,
}
def check_length(self, text):
"""길이 기반 필터"""
return self.criteria["min_length"] <= len(text) <= self.criteria["max_length"]
def check_repetition(self, text):
"""반복 텍스트 감지"""
words = text.split()
if len(words) == 0:
return False
unique_ratio = len(set(words)) / len(words)
return unique_ratio >= (1 - self.criteria["max_repetition_ratio"])
def check_special_chars(self, text):
"""특수 문자 비율 확인"""
special = sum(1 for c in text if not c.isalnum() and not c.isspace()
and c not in '.,!?;:')
return special / max(len(text), 1) < self.criteria["max_special_char_ratio"]
def compute_perplexity(self, text, model, tokenizer, device="cuda"):
"""Perplexity 기반 품질 평가 (낮을수록 자연스러운 텍스트)"""
inputs = tokenizer(text, return_tensors="pt", truncation=True,
max_length=512).to(device)
with torch.no_grad():
outputs = model(**inputs, labels=inputs["input_ids"])
return torch.exp(outputs.loss).item()
def filter(self, text):
"""종합 품질 필터링"""
return (
self.check_length(text)
and self.check_repetition(text)
and self.check_special_chars(text)
)
3.6 PII (개인정보) 제거
from typing import Dict, List
class PIIRemover:
"""개인식별정보(PII) 제거"""
PATTERNS = {
"주민등록번호": r'\d{6}[-]?\d{7}',
"전화번호": r'0\d{1,2}[-.]?\d{3,4}[-.]?\d{4}',
"이메일": r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}',
"카드번호": r'\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}',
"계좌번호": r'\d{3,6}[-]?\d{2,6}[-]?\d{2,6}[-]?\d{0,3}',
"IP주소": r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}',
}
def remove_pii(self, text: str) -> str:
"""PII를 마스킹 토큰으로 대체"""
for pii_type, pattern in self.PATTERNS.items():
mask_token = f"[{pii_type}]"
text = re.sub(pattern, mask_token, text)
return text
def detect_pii(self, text: str) -> Dict[str, List[str]]:
"""PII 감지 (제거 전 확인용)"""
found = {}
for pii_type, pattern in self.PATTERNS.items():
matches = re.findall(pattern, text)
if matches:
found[pii_type] = matches
return found
3.7 토크나이징 고려사항
from tokenizers import Tokenizer, models, trainers, pre_tokenizers
SentencePiece 한국어 토크나이저 학습
def train_korean_tokenizer(text_files, vocab_size=32000):
"""한국어 특화 BPE 토크나이저 학습"""
tokenizer = Tokenizer(models.BPE(unk_token="[UNK]"))
한국어는 pre-tokenization이 중요
tokenizer.pre_tokenizer = pre_tokenizers.Sequence([
pre_tokenizers.ByteLevel(add_prefix_space=False),
])
trainer = trainers.BpeTrainer(
vocab_size=vocab_size,
special_tokens=["[UNK]", "[CLS]", "[SEP]", "[PAD]", "[MASK]"],
min_frequency=2,
)
tokenizer.train(text_files, trainer)
return tokenizer
토크나이저 효율성 비교
def compare_tokenizer_efficiency(text, tokenizers_dict):
"""여러 토크나이저의 한국어 토큰 효율성 비교"""
print(f"원문 ({len(text)}자): {text[:50]}...")
print("-" * 60)
for name, tok in tokenizers_dict.items():
tokens = tok.encode(text)
token_strs = tok.convert_ids_to_tokens(tokens)
fertility = len(tokens) / len(text.split())
print(f"{name}: {len(tokens)} tokens, fertility={fertility:.2f}")
print(f" 처음 10토큰: {token_strs[:10]}")
4. Instruction Tuning 데이터 포맷
4.1 Alpaca 포맷
가장 기본적이고 널리 사용되는 포맷입니다.
{
"instruction": "다음 텍스트를 요약해주세요.",
"input": "인공지능(AI)은 인간의 학습능력, 추론능력, 지각능력을 인공적으로 구현한 컴퓨터 과학의 세부분야입니다...",
"output": "인공지능은 인간의 지능을 컴퓨터로 구현하는 기술로, 머신러닝과 딥러닝의 발전으로 다양한 분야에서 활용되고 있습니다."
}
def format_alpaca(instruction, input_text="", output=""):
"""Alpaca 포맷 생성"""
if input_text:
return {
"instruction": instruction,
"input": input_text,
"output": output,
}
return {
"instruction": instruction,
"input": "",
"output": output,
}
Alpaca 프롬프트 템플릿
ALPACA_TEMPLATE = """Below is an instruction that describes a task. Write a response that appropriately completes the request.
Instruction:
{instruction}
Input:
{input}
Response:
{output}"""
ALPACA_TEMPLATE_NO_INPUT = """Below is an instruction that describes a task. Write a response that appropriately completes the request.
Instruction:
{instruction}
Response:
{output}"""
4.2 ShareGPT 포맷
다중 턴 대화를 표현하는 포맷입니다.
{
"conversations": [
{
"from": "human",
"value": "파이썬에서 리스트와 튜플의 차이점이 뭐야?"
},
{
"from": "gpt",
"value": "파이썬에서 리스트와 튜플의 주요 차이점은...\n\n1. **가변성**: 리스트는 변경 가능(mutable), 튜플은 변경 불가(immutable)\n2. **성능**: 튜플이 메모리 효율적\n3. **문법**: 리스트는 [], 튜플은 ()"
},
{
"from": "human",
"value": "그러면 언제 튜플을 쓰는 게 좋아?"
},
{
"from": "gpt",
"value": "튜플은 다음과 같은 경우에 사용하면 좋습니다:\n\n1. 데이터가 변경되면 안 되는 경우 (좌표, RGB 값)\n2. 딕셔너리의 키로 사용할 때\n3. 함수에서 여러 값을 반환할 때"
}
]
}
4.3 OpenAI Messages 포맷
OpenAI API와 호환되는 표준 포맷입니다.
{
"messages": [
{
"role": "system",
"content": "당신은 한국어에 능통한 AI 어시스턴트입니다. 정확하고 도움이 되는 답변을 제공하세요."
},
{
"role": "user",
"content": "머신러닝과 딥러닝의 차이를 설명해줘."
},
{
"role": "assistant",
"content": "머신러닝과 딥러닝의 핵심 차이를 설명하겠습니다..."
}
]
}
4.4 Chat Template (모델별 차이)
Llama 3 Chat Template
LLAMA3_TEMPLATE = """<|begin_of_text|><|start_header_id|>system<|end_header_id|>
{system_message}<|eot_id|><|start_header_id|>user<|end_header_id|>
{user_message}<|eot_id|><|start_header_id|>assistant<|end_header_id|>
{assistant_message}<|eot_id|>"""
Mistral Chat Template
MISTRAL_TEMPLATE = """<s>[INST] {system_message}
{user_message} [/INST]{assistant_message}</s>"""
Qwen 2 Chat Template
QWEN2_TEMPLATE = """<|im_start|>system
{system_message}<|im_end|>
<|im_start|>user
{user_message}<|im_end|>
<|im_start|>assistant
{assistant_message}<|im_end|>"""
4.5 포맷 간 변환
def sharegpt_to_openai(sharegpt_data):
"""ShareGPT -> OpenAI Messages 변환"""
messages = []
role_map = {"human": "user", "gpt": "assistant", "system": "system"}
for conv in sharegpt_data["conversations"]:
messages.append({
"role": role_map.get(conv["from"], conv["from"]),
"content": conv["value"],
})
return {"messages": messages}
def alpaca_to_openai(alpaca_data, system_prompt=""):
"""Alpaca -> OpenAI Messages 변환"""
messages = []
if system_prompt:
messages.append({"role": "system", "content": system_prompt})
user_content = alpaca_data["instruction"]
if alpaca_data.get("input"):
user_content += f"\n\n{alpaca_data['input']}"
messages.append({"role": "user", "content": user_content})
messages.append({"role": "assistant", "content": alpaca_data["output"]})
return {"messages": messages}
def openai_to_sharegpt(openai_data):
"""OpenAI Messages -> ShareGPT 변환"""
role_map = {"user": "human", "assistant": "gpt", "system": "system"}
conversations = []
for msg in openai_data["messages"]:
conversations.append({
"from": role_map.get(msg["role"], msg["role"]),
"value": msg["content"],
})
return {"conversations": conversations}
대량 변환
def batch_convert(dataset, source_format, target_format):
"""데이터셋 전체 포맷 변환"""
converters = {
("sharegpt", "openai"): sharegpt_to_openai,
("alpaca", "openai"): alpaca_to_openai,
("openai", "sharegpt"): openai_to_sharegpt,
}
converter = converters.get((source_format, target_format))
if not converter:
raise ValueError(f"지원하지 않는 변환: {source_format} -> {target_format}")
return [converter(item) for item in dataset]
5. RLHF/DPO 데이터셋 구축
5.1 선호도 데이터 구조
DPO (Direct Preference Optimization) 데이터 구조
dpo_example = {
"prompt": "한국의 전통 음식 중 건강에 좋은 것을 추천해주세요.",
"chosen": "한국 전통 음식 중 건강에 좋은 대표적인 음식들을 소개하겠습니다.\n\n1. **김치**: 유산균이 풍부하고 비타민C, 식이섬유가 많습니다.\n2. **된장찌개**: 발효 식품으로 항암 효과가 있으며 단백질이 풍부합니다.\n3. **잡곡밥**: 다양한 영양소를 고르게 섭취할 수 있습니다.\n4. **나물**: 시금치, 콩나물 등 다양한 비타민과 미네랄을 제공합니다.",
"rejected": "음... 비빔밥이요. 맛있으니까요. 불고기도요.",
}
5.2 인간 평가 가이드라인
ANNOTATION_GUIDELINES = """
선호도 평가 가이드라인
평가 기준 (1-5점)
1. **도움됨 (Helpfulness)**: 질문에 얼마나 잘 답했는가
2. **정확성 (Accuracy)**: 사실적으로 맞는 정보인가
3. **안전성 (Safety)**: 유해하거나 편향된 내용은 없는가
4. **유창성 (Fluency)**: 한국어가 자연스러운가
비교 평가 시 주의사항
- 두 응답 모두 읽은 후 비교할 것
- 길이가 아닌 품질 기준으로 판단
- 확신이 없으면 'tie'로 표시
- 개인 의견이 아닌 객관적 품질 기준으로 판단
"""
평가 데이터 수집 도구
class PreferenceCollector:
def __init__(self):
self.annotations = []
def add_comparison(self, prompt, response_a, response_b, preference, annotator_id):
"""선호도 비교 결과 저장"""
self.annotations.append({
"prompt": prompt,
"response_a": response_a,
"response_b": response_b,
"preference": preference, # "a", "b", "tie"
"annotator_id": annotator_id,
"timestamp": datetime.now().isoformat(),
})
def compute_agreement(self):
"""평가자 간 일치도 계산"""
from collections import Counter
같은 prompt에 대한 평가 비교
prompt_votes = {}
for ann in self.annotations:
key = (ann["prompt"], ann["response_a"][:50])
if key not in prompt_votes:
prompt_votes[key] = []
prompt_votes[key].append(ann["preference"])
agreements = []
for key, votes in prompt_votes.items():
if len(votes) >= 2:
most_common = Counter(votes).most_common(1)[0][1]
agreements.append(most_common / len(votes))
return np.mean(agreements) if agreements else 0
5.3 AI 기반 자동 순위 매기기
def ai_rank_responses(prompt, responses, model="gpt-4"):
"""Constitutional AI 방식의 자동 순위 매기기"""
ranking_prompt = f"""다음 질문에 대한 여러 응답을 평가해주세요.
질문: {prompt}
"""
for i, resp in enumerate(responses):
ranking_prompt += f"응답 {i+1}: {resp}\n\n"
ranking_prompt += """각 응답을 다음 기준으로 1-5점 평가하고, 최종 순위를 매겨주세요:
1. 도움됨 (Helpfulness)
2. 정확성 (Accuracy)
3. 안전성 (Safety)
4. 유창성 (Fluency)
JSON 형식으로 결과를 반환하세요."""
client = openai.OpenAI()
response = client.chat.completions.create(
model=model,
messages=[{"role": "user", "content": ranking_prompt}],
response_format={"type": "json_object"},
)
return json.loads(response.choices[0].message.content)
5.4 UltraFeedback 방법론
def create_ultrafeedback_data(prompts, models_to_evaluate):
"""UltraFeedback 스타일의 다중 모델 응답 수집 및 평가"""
dataset = []
for prompt in prompts:
responses = {}
여러 모델에서 응답 수집
for model_name in models_to_evaluate:
responses[model_name] = generate_response(prompt, model_name)
GPT-4로 각 응답 평가 (1-10점)
evaluations = {}
for model_name, response in responses.items():
score = evaluate_single_response(prompt, response)
evaluations[model_name] = score
최고/최저 점수 응답 선택 (DPO용)
best_model = max(evaluations, key=evaluations.get)
worst_model = min(evaluations, key=evaluations.get)
dataset.append({
"prompt": prompt,
"chosen": responses[best_model],
"rejected": responses[worst_model],
"chosen_model": best_model,
"rejected_model": worst_model,
"scores": evaluations,
})
return dataset
6. 데이터 품질 메트릭
6.1 다양성 측정
from collections import Counter
def vocabulary_diversity(texts):
"""어휘 다양성 측정 (Type-Token Ratio)"""
all_tokens = []
for text in texts:
all_tokens.extend(text.split())
types = len(set(all_tokens))
tokens = len(all_tokens)
ttr = types / tokens if tokens > 0 else 0
return {"type_token_ratio": ttr, "unique_words": types, "total_words": tokens}
def topic_diversity(texts, n_topics=10):
"""토픽 다양성 (LDA 기반)"""
from sklearn.decomposition import LatentDirichletAllocation
from sklearn.feature_extraction.text import CountVectorizer
vectorizer = CountVectorizer(max_features=5000)
dtm = vectorizer.fit_transform(texts)
lda = LatentDirichletAllocation(n_components=n_topics, random_state=42)
topic_dist = lda.fit_transform(dtm)
토픽 엔트로피 (높을수록 균등 분포)
avg_dist = topic_dist.mean(axis=0)
entropy = -np.sum(avg_dist * np.log(avg_dist + 1e-10))
return {"topic_entropy": entropy, "max_entropy": np.log(n_topics)}
def instruction_diversity(instructions):
"""지시문 시작 동사 다양성"""
first_words = [inst.split()[0] if inst.split() else "" for inst in instructions]
counter = Counter(first_words)
return {
"unique_starters": len(counter),
"top_10": counter.most_common(10),
"starter_entropy": -sum(
(c/len(first_words)) * np.log(c/len(first_words))
for c in counter.values()
),
}
6.2 길이 분포 분석
def analyze_length_distribution(dataset, text_field="text"):
"""데이터셋 길이 분포 분석"""
lengths = [len(item[text_field]) for item in dataset]
stats = {
"count": len(lengths),
"mean": np.mean(lengths),
"median": np.median(lengths),
"std": np.std(lengths),
"min": np.min(lengths),
"max": np.max(lengths),
"p25": np.percentile(lengths, 25),
"p75": np.percentile(lengths, 75),
"p95": np.percentile(lengths, 95),
}
print("=== 길이 분포 통계 ===")
for k, v in stats.items():
print(f" {k}: {v:.1f}")
return stats
6.3 벤치마크 오염 검사
def check_contamination(train_data, benchmark_data, n_gram=13):
"""학습 데이터와 벤치마크 간 오염(contamination) 검사"""
벤치마크 n-gram 집합 생성
benchmark_ngrams = set()
for item in benchmark_data:
text = item["question"] if "question" in item else item["text"]
words = text.split()
for i in range(len(words) - n_gram + 1):
ngram = " ".join(words[i:i+n_gram])
benchmark_ngrams.add(ngram)
학습 데이터에서 겹치는 n-gram 검색
contaminated = []
for i, item in enumerate(train_data):
text = item.get("instruction", "") + " " + item.get("output", "")
words = text.split()
for j in range(len(words) - n_gram + 1):
ngram = " ".join(words[j:j+n_gram])
if ngram in benchmark_ngrams:
contaminated.append({
"train_idx": i,
"matched_ngram": ngram,
})
break
contamination_rate = len(contaminated) / max(len(train_data), 1)
print(f"오염률: {contamination_rate:.4%} ({len(contaminated)}/{len(train_data)})")
return contaminated
7. 실전 예제: 한국어 SFT 데이터셋 처음부터 구축하기
7.1 전체 파이프라인
"""
한국어 SFT 데이터셋 구축 전체 파이프라인
수집 -> 정제 -> 포맷 변환 -> 검증 -> 업로드
"""
from datasets import Dataset
from tqdm import tqdm
===== Step 1: 데이터 수집 =====
def collect_data():
"""다양한 소스에서 데이터 수집"""
all_data = []
1-1. 기존 데이터셋 로드
from datasets import load_dataset
koalpaca = load_dataset("beomi/KoAlpaca-v1.1a", split="train")
for item in koalpaca:
all_data.append({
"instruction": item["instruction"],
"input": "",
"output": item["output"],
"source": "koalpaca",
})
1-2. 합성 데이터 추가
synthetic = generate_synthetic_data(num_samples=1000)
all_data.extend(synthetic)
print(f"총 수집: {len(all_data)}개")
return all_data
===== Step 2: 데이터 정제 =====
def clean_data(raw_data):
"""데이터 정제 파이프라인"""
cleaned = []
pii_remover = PIIRemover()
quality_filter = QualityFilter()
for item in tqdm(raw_data, desc="정제 중"):
텍스트 정제
instruction = clean_text(item["instruction"])
output = clean_text(item["output"])
PII 제거
instruction = pii_remover.remove_pii(instruction)
output = pii_remover.remove_pii(output)
품질 필터링
if not quality_filter.filter(instruction) or not quality_filter.filter(output):
continue
cleaned.append({
"instruction": instruction,
"input": item.get("input", ""),
"output": output,
"source": item["source"],
})
print(f"정제 후: {len(cleaned)}/{len(raw_data)}")
return cleaned
===== Step 3: 중복 제거 =====
def remove_duplicates(data):
"""중복 제거"""
dedup = TextDeduplicator(threshold=0.85)
docs = [{"text": item["instruction"] + " " + item["output"], **item} for item in data]
unique = dedup.deduplicate(docs)
return [{"instruction": d["instruction"], "input": d.get("input", ""),
"output": d["output"], "source": d["source"]} for d in unique]
===== Step 4: 포맷 변환 =====
def format_data(data, target_format="openai"):
"""목표 포맷으로 변환"""
formatted = []
system_prompt = "당신은 도움이 되는 한국어 AI 어시스턴트입니다."
for item in data:
if target_format == "openai":
formatted.append(alpaca_to_openai(item, system_prompt))
elif target_format == "sharegpt":
formatted.append({
"conversations": [
{"from": "human", "value": item["instruction"]},
{"from": "gpt", "value": item["output"]},
],
})
return formatted
===== Step 5: 검증 =====
def validate_data(data, format_type="openai"):
"""데이터 품질 검증"""
errors = []
for i, item in enumerate(data):
if format_type == "openai":
if "messages" not in item:
errors.append(f"[{i}] messages 필드 없음")
elif len(item["messages"]) < 2:
errors.append(f"[{i}] 메시지 수 부족")
for msg in item.get("messages", []):
if msg["role"] not in ("system", "user", "assistant"):
errors.append(f"[{i}] 잘못된 role: {msg['role']}")
if not msg["content"].strip():
errors.append(f"[{i}] 빈 content")
if errors:
print(f"검증 오류 {len(errors)}개:")
for e in errors[:10]:
print(f" {e}")
else:
print("검증 통과!")
return len(errors) == 0
===== Step 6: 업로드 =====
def upload_to_hub(data, repo_name):
"""Hugging Face Hub에 업로드"""
ds = Dataset.from_list(data)
Train/Validation 분할
split_ds = ds.train_test_split(test_size=0.05, seed=42)
split_ds.push_to_hub(
repo_name,
private=True,
)
print(f"업로드 완료: {repo_name}")
print(f" Train: {len(split_ds['train'])}, Validation: {len(split_ds['test'])}")
===== 실행 =====
if __name__ == "__main__":
1. 수집
raw_data = collect_data()
2. 정제
cleaned_data = clean_data(raw_data)
3. 중복 제거
unique_data = remove_duplicates(cleaned_data)
4. 포맷 변환
formatted_data = format_data(unique_data, "openai")
5. 검증
is_valid = validate_data(formatted_data, "openai")
6. 업로드
if is_valid:
upload_to_hub(formatted_data, "my-org/korean-sft-v1")
7.2 품질 대시보드
def generate_quality_report(dataset):
"""데이터셋 품질 보고서 생성"""
report = {
"total_samples": len(dataset),
"length_stats": analyze_length_distribution(dataset, "instruction"),
"diversity": vocabulary_diversity([d["instruction"] for d in dataset]),
"source_distribution": Counter(d["source"] for d in dataset),
}
print("=" * 60)
print("데이터셋 품질 보고서")
print("=" * 60)
print(f"총 샘플 수: {report['total_samples']}")
print(f"\n길이 통계:")
for k, v in report['length_stats'].items():
print(f" {k}: {v:.1f}")
print(f"\n어휘 다양성: TTR = {report['diversity']['type_token_ratio']:.4f}")
print(f"\n소스 분포:")
for source, count in report['source_distribution'].most_common():
print(f" {source}: {count} ({count/len(dataset)*100:.1f}%)")
print("=" * 60)
return report
8. 퀴즈
**정답: 철저하게 큐레이션된 고품질 학습 데이터**
Phi-3는 3.8B 파라미터로 7B~13B 모델을 능가했는데, 이는 모델 크기가 아닌 학습 데이터의 품질이 핵심이었습니다. 교과서 수준의 고품질 합성 데이터를 사용하여 학습했습니다.
**정답: 근사 중복 제거 (Approximate Deduplication)**
MinHash LSH는 대규모 데이터에서 유사한 문서를 효율적으로 찾아 중복을 제거하는 알고리즘입니다. 정확한 매칭이 아닌 유사도 기반 근사 매칭으로, O(n) 수준의 시간 복잡도로 작동합니다.
**정답:**
- **Alpaca**: instruction/input/output 단일 턴 구조
- **ShareGPT**: conversations 배열로 다중 턴 대화 (human/gpt 역할)
- **OpenAI Messages**: messages 배열로 system/user/assistant 역할 구분
ShareGPT와 OpenAI 포맷은 모두 다중 턴을 지원하지만, 역할 이름과 구조가 다릅니다.
**정답:**
- **chosen**: 인간이 선호하는 (더 나은) 응답
- **rejected**: 인간이 비선호하는 (덜 나은) 응답
DPO(Direct Preference Optimization)는 이 쌍 데이터를 이용해 모델이 chosen과 유사한 응답을 생성하고 rejected와 다른 응답을 생성하도록 학습합니다. RLHF와 달리 별도의 보상 모델 없이 직접 최적화합니다.
**정답:**
벤치마크 오염은 학습 데이터에 평가 데이터가 포함되어 모델 성능이 과대평가되는 문제입니다.
**위험성:**
- 모델이 실제로는 해당 문제를 "풀지" 못하고 "암기"한 것
- 공정한 모델 비교 불가능
- 실제 배포 시 기대 이하의 성능
**검사 방법:**
- n-gram 겹침 검사 (보통 13-gram 사용)
- 벤치마크 문장의 해시 비교
- GPT-4의 GPT-4 벤치마크 오염 보고서 참고
9. 참고 자료
1. **LIMA: Less Is More for Alignment** - Zhou et al., 2023
2. **Phi-3 Technical Report** - Microsoft Research, 2024
3. **Self-Instruct: Aligning LLMs with Self-Generated Instructions** - Wang et al., 2023
4. **WizardLM: Empowering Large Language Models to Follow Complex Instructions** - Xu et al., 2023
5. **Hugging Face Datasets Documentation** - huggingface.co/docs/datasets
6. **KoAlpaca: Korean Alpaca Model** - beomi, GitHub
7. **UltraFeedback: Boosting Language Models with High-quality Feedback** - Cui et al., 2023
8. **Training Language Models to Follow Instructions with Human Feedback** - Ouyang et al., 2022
9. **Direct Preference Optimization** - Rafailov et al., 2023
10. **Deduplicating Training Data Makes Language Models Better** - Lee et al., 2022
11. **KLUE: Korean Language Understanding Evaluation** - Park et al., 2021
12. **Textbooks Are All You Need** - Gunasekar et al., 2023
13. **Constitutional AI: Harmlessness from AI Feedback** - Bai et al., 2022
14. **The RefinedWeb Dataset for Falcon LLM** - Penedo et al., 2023
현재 단락 (1/916)
2024년, Microsoft Research의 Phi-3 논문은 업계에 충격을 줬습니다. 3.8B 파라미터 모델이 7B~13B 모델을 능가하는 성능을 보였고, 그 비결은 **철저...