🔬 Synthetic Data Fine-Tuning 완벽 가이드
📑 목차
1. Synthetic Data란?
핵심 정의
Synthetic Data (합성 데이터): AI 모델이 생성한 인공 데이터로, 실제 사람이 만든 데이터를 대체하거나 보완하는 데이터
💡 전통적 방식 vs Synthetic Data
📋 비교표
| 항목 | 전통적 방식 | Synthetic Data |
|---|---|---|
| 데이터 수집 | 사람이 직접 수집/라벨링 | AI가 자동 생성 |
| 비용 | 매우 높음 (시간당 $15-50) | 거의 무료 (API 비용만) |
| 시간 | 수주 ~ 수개월 | 수시간 ~ 수일 |
| 규모 | 제한적 (1k-10k) | 무제한 (100k-1M+) |
| 품질 | 높음 (실제 데이터) | 중간 (검증 필요) |
| 프라이버시 | 위험 (개인정보) | 안전 (합성) |
| 다양성 | 제한적 | 높음 (편향 제거 가능) |
💡 실제 사례로 이해하기
📋 시나리오: 한국어 감정 분석 모델 만들기
전통적 방식:
과정:
1. 크라우드 소싱 플랫폼 계약
2. 작업자 모집 (100명)
3. 가이드라인 작성 및 교육
4. 데이터 수집 (10,000개 문장)
5. 품질 검수
비용: 1,000만원
시간: 2개월
데이터: 10,000개Synthetic Data 방식:
과정:
1. GPT-4로 프롬프트 작성
2. 다양한 감정 표현 생성 요청
3. 자동 생성 (100,000개)
4. 필터링 및 검증
비용: 30만원 (API 비용)
시간: 2일
데이터: 100,000개결과: 비용 97% 절감, 시간 97% 단축, 데이터 10배 증가
2. 왜 Synthetic Data가 필요한가?
3가지 핵심 이유
- 데이터 부족 문제 해결
- 프라이버시 보호
- 편향 제거 및 다양성 확보
💡 이유 1: 데이터 부족 (Data Scarcity)
📋 문제 상황
실제 데이터 구하기 어려운 경우:
희귀_언어:
- 한국어 의료 문서 (병원 프라이버시)
- 법률 문서 (기밀)
- 특정 도메인 전문 용어
새로운_태스크:
- 신규 서비스 (데이터 없음)
- 미래 시나리오 예측
- Edge case 처리
고비용_데이터:
- 전문가 라벨링 필요 (의사, 변호사)
- 대규모 데이터 필요 (100만개+)해결책: Synthetic Data로 무한 생성
💡 이유 2: 프라이버시 보호
📋 개인정보 보호법 준수
문제:
# ❌ 실제 고객 데이터 사용 시 위험
real_data = [
{"text": "홍길동(주민번호: 123456-1234567)님의 진료 기록...", "label": "의료"},
{"text": "김철수 고객님 계좌번호 110-123-456789...", "label": "금융"},
]
# 문제점:
# - GDPR, 개인정보보호법 위반
# - 데이터 유출 위험
# - 법적 책임해결책:
# ✅ Synthetic Data 사용
synthetic_data = [
{"text": "환자 A의 고혈압 진단 기록...", "label": "의료"},
{"text": "고객 B의 대출 신청 내역...", "label": "금융"},
]
# 장점:
# - 실제 개인정보 없음
# - 법적 안전
# - 자유로운 공유 가능💡 이유 3: 편향 제거 및 다양성 확보
📋 데이터 불균형 해결
문제:
# 실제 데이터의 편향
감정_분석_데이터:
긍정: 7,000개 # 70%
부정: 2,000개 # 20%
중립: 1,000개 # 10%
문제점:
- 모델이 긍정에 편향됨
- 부정/중립 정확도 낮음해결책:
# Synthetic Data로 균형 맞추기
합성_데이터_생성:
긍정: 3,000개 (기존 + 합성)
부정: 8,000개 (합성으로 보충) ✅
중립: 9,000개 (합성으로 보충) ✅
결과:
- 균형잡힌 데이터셋
- 모든 클래스 정확도 향상3. Synthetic Data 생성 방법
3단계 파이프라인
- Seed Data 준비 (소량의 실제 데이터 또는 템플릿)
- LLM으로 확장 (GPT-4, Claude 등으로 대량 생성)
- 필터링 및 검증 (품질 낮은 데이터 제거)
💡 방법 1: Self-Instruct (가장 유명)
📋 개념
Self-Instruct란?
LLM에게 “스스로 학습 데이터를 만들어”라고 지시하는 방법
프로세스:
- Seed 작업 예시 제공 (20-30개)
- LLM이 비슷한 작업 생성
- LLM이 답변 생성
- 품질 필터링
- 반복
논문: Self-Instruct (Wang et al., 2023)
💻 실전 예제: Self-Instruct 구현
import anthropic
import json
client = anthropic.Anthropic(api_key="your-api-key")
# 1단계: Seed 데이터 (예시 작업)
seed_tasks = [
{
"instruction": "주어진 단어의 동의어를 5개 나열하세요.",
"input": "행복",
"output": "기쁨, 즐거움, 만족, 환희, 흐뭇함"
},
{
"instruction": "문장을 존댓말로 바꾸세요.",
"input": "오늘 날씨 좋다",
"output": "오늘 날씨가 좋습니다"
},
{
"instruction": "제품 리뷰의 감정을 분석하세요.",
"input": "배송이 빠르고 품질도 좋아요!",
"output": "긍정"
}
]
# 2단계: 새로운 작업 생성
def generate_new_tasks(seed_tasks, num_new_tasks=10):
"""Self-Instruct로 새로운 작업 생성"""
# Seed 예시를 프롬프트로 만들기
examples_text = "\n\n".join([
f"예시 {i+1}:\n"
f"지시사항: {task['instruction']}\n"
f"입력: {task['input']}\n"
f"출력: {task['output']}"
for i, task in enumerate(seed_tasks[:3])
])
prompt = f"""다음은 AI 학습을 위한 작업 예시들입니다:
{examples_text}
위 예시들과 비슷하지만 다양한 새로운 작업을 {num_new_tasks}개 생성해주세요.
각 작업은 다음 형식을 따라야 합니다:
지시사항: [사용자에게 무엇을 요청하는지]
입력: [구체적인 입력 예시]
출력: [기대되는 출력 예시]
다양한 주제와 난이도를 포함해주세요.
JSON 형식으로 반환:
[
{{
"instruction": "...",
"input": "...",
"output": "..."
}},
...
]
"""
message = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=4096,
messages=[{"role": "user", "content": prompt}]
)
# JSON 파싱
response_text = message.content[0].text
# JSON 추출 (```json ... ``` 제거)
import re
json_match = re.search(r'```json\s*(.*?)\s*```', response_text, re.DOTALL)
if json_match:
json_text = json_match.group(1)
else:
json_text = response_text
new_tasks = json.loads(json_text)
return new_tasks
# 실행
new_tasks = generate_new_tasks(seed_tasks, num_new_tasks=10)
print(f"✅ {len(new_tasks)}개 새로운 작업 생성 완료!\n")
for i, task in enumerate(new_tasks[:3], 1):
print(f"작업 {i}:")
print(f" 지시: {task['instruction']}")
print(f" 입력: {task['input']}")
print(f" 출력: {task['output']}\n")출력 예시:
✅ 10개 새로운 작업 생성 완료!
작업 1:
지시: 주어진 숫자를 한글로 변환하세요.
입력: 12345
출력: 만 이천삼백사십오
작업 2:
지시: 영어 문장을 한국어로 번역하세요.
입력: The weather is beautiful today.
출력: 오늘 날씨가 아름답습니다.
작업 3:
지시: 주어진 텍스트의 주제를 분류하세요.
입력: 삼성전자가 새로운 스마트폰을 출시했다.
출력: 기술/IT
💡 방법 2: Alpaca 스타일 (Stanford)
📋 특징
Alpaca 방식:
- GPT-3.5/4를 “Teacher” 모델로 사용
- 다양한 instruction 생성
- 작은 모델(Llama 7B)을 Student로 학습
논문: Alpaca (Taori et al., 2023)
💻 Alpaca 스타일 데이터 생성
import anthropic
import json
from typing import List, Dict
client = anthropic.Anthropic(api_key="your-api-key")
def generate_alpaca_data(num_samples: int = 100) -> List[Dict]:
"""Alpaca 스타일 instruction 데이터 생성"""
# 카테고리별 프롬프트
categories = [
"일반 지식 질문",
"창의적 글쓰기",
"요약",
"번역",
"코드 생성",
"수학 문제",
"감정 분석",
"분류 작업"
]
all_data = []
for category in categories:
print(f"📝 {category} 데이터 생성 중...")
prompt = f"""당신은 AI 학습 데이터를 생성하는 전문가입니다.
"{category}" 주제로 instruction-following 데이터를 생성해주세요.
다음 형식으로 {num_samples // len(categories)}개를 생성:
1. instruction: 사용자가 AI에게 요청하는 명령
2. input: 명령에 필요한 입력 (필요 없으면 비워두기)
3. output: 모범 답변
JSON 배열로 반환:
[
{{
"instruction": "...",
"input": "...",
"output": "..."
}},
...
]
다양하고 현실적인 예시를 만들어주세요.
"""
message = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=4096,
messages=[{"role": "user", "content": prompt}]
)
# JSON 파싱
response_text = message.content[0].text
import re
json_match = re.search(r'```json\s*(.*?)\s*```', response_text, re.DOTALL)
if json_match:
json_text = json_match.group(1)
else:
json_text = response_text
try:
category_data = json.loads(json_text)
all_data.extend(category_data)
except json.JSONDecodeError as e:
print(f"⚠️ JSON 파싱 오류: {e}")
continue
return all_data
# 실행
alpaca_dataset = generate_alpaca_data(num_samples=80)
print(f"\n✅ 총 {len(alpaca_dataset)}개 데이터 생성 완료!")
# 파일 저장
with open("alpaca_synthetic_data.json", "w", encoding="utf-8") as f:
json.dump(alpaca_dataset, f, ensure_ascii=False, indent=2)
# 샘플 출력
print("\n📊 샘플 데이터:")
for i, item in enumerate(alpaca_dataset[:3], 1):
print(f"\n{i}. {item['instruction']}")
if item.get('input'):
print(f" 입력: {item['input']}")
print(f" 출력: {item['output'][:100]}...")💡 방법 3: Orca 스타일 (Microsoft)
📋 특징
Orca 방식:
- GPT-4에게 “설명하면서 답해줘” 요청
- 단순 답변이 아닌 reasoning 과정 포함
- 작은 모델도 복잡한 추론 가능
논문: Orca (Mukherjee et al., 2023)
💻 Orca 스타일 데이터 생성
import anthropic
client = anthropic.Anthropic(api_key="your-api-key")
def generate_orca_style(question: str) -> Dict:
"""Orca 스타일: 추론 과정 포함 데이터 생성"""
prompt = f"""다음 질문에 답하되, 단계별 추론 과정을 자세히 설명해주세요.
질문: {question}
답변 형식:
1. 문제 이해: [질문을 어떻게 이해했는지]
2. 접근 방법: [어떤 방법으로 풀 것인지]
3. 단계별 풀이: [구체적인 과정]
4. 최종 답변: [간단명료한 답]
마치 학생에게 가르치듯이 상세히 설명해주세요.
"""
message = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=2048,
messages=[{"role": "user", "content": prompt}]
)
explanation = message.content[0].text
return {
"question": question,
"explanation": explanation,
"style": "orca_reasoning"
}
# 예시 질문들
questions = [
"파이썬에서 리스트와 튜플의 차이는 무엇인가요?",
"블록체인 기술의 핵심 원리를 설명해주세요.",
"기후 변화가 농업에 미치는 영향은?",
"머신러닝에서 과적합을 방지하는 방법은?"
]
orca_dataset = []
for question in questions:
print(f"🧠 '{question[:30]}...' 처리 중...")
data = generate_orca_style(question)
orca_dataset.append(data)
print(f"\n✅ {len(orca_dataset)}개 Orca 스타일 데이터 생성!")
# 샘플 출력
print("\n📖 샘플 (추론 과정 포함):")
print(orca_dataset[0]['explanation'][:500])출력 예시:
🧠 '파이썬에서 리스트와 튜플의 차이는 무엇인가요?...' 처리 중...
✅ 4개 Orca 스타일 데이터 생성!
📖 샘플 (추론 과정 포함):
1. 문제 이해:
학습자가 파이썬의 두 가지 기본 자료구조인 리스트와 튜플의 차이점을 이해하고자 합니다.
2. 접근 방법:
두 자료구조의 정의, 특성, 사용 사례를 비교하여 설명하겠습니다.
3. 단계별 풀이:
a) 가변성(Mutability):
- 리스트: 가변(mutable) - 생성 후 요소 추가/삭제/수정 가능
- 튜플: 불변(immutable) - 생성 후 변경 불가
예시:
```python
my_list = [1, 2, 3]
my_list[0] = 10 # ✅ 가능
my_tuple = (1, 2, 3)
my_tuple[0] = 10 # ❌ 오류 발생
…
---
## 4. 파인튜닝 프로세스
> [!tip] 전체 파이프라인
>
> 1. Synthetic Data 생성 (위에서 배운 방법)
> 2. 데이터 품질 필터링
> 3. MLX로 파인튜닝 (Apple Silicon 최적화)
> 4. 평가 및 개선
### 💡 4-1. 데이터 품질 필터링
#### 📋 자동 필터링 기법
```python
import json
from typing import List, Dict
import re
def filter_synthetic_data(data: List[Dict]) -> List[Dict]:
"""합성 데이터 품질 필터링"""
filtered = []
for item in data:
instruction = item.get('instruction', '')
output = item.get('output', '')
# 필터 1: 너무 짧은 데이터 제거
if len(instruction) < 10 or len(output) < 10:
continue
# 필터 2: 중복 제거 (유사도 기반)
is_duplicate = False
for existing in filtered:
if similar(instruction, existing['instruction']) > 0.9:
is_duplicate = True
break
if is_duplicate:
continue
# 필터 3: 부적절한 내용 필터링
banned_words = ['욕설', '비속어', '혐오'] # 실제로는 더 많은 단어
if any(word in instruction + output for word in banned_words):
continue
# 필터 4: 언어 일관성 (한국어 데이터셋이라면)
if not is_korean_dominant(instruction + output):
continue
filtered.append(item)
return filtered
def similar(text1: str, text2: str) -> float:
"""간단한 유사도 계산 (실제로는 더 정교한 방법 사용)"""
from difflib import SequenceMatcher
return SequenceMatcher(None, text1, text2).ratio()
def is_korean_dominant(text: str) -> bool:
"""한글이 주된 언어인지 확인"""
korean_chars = len(re.findall(r'[가-힣]', text))
total_chars = len(re.sub(r'\s', '', text))
if total_chars == 0:
return False
return (korean_chars / total_chars) > 0.5
# 사용
raw_data = alpaca_dataset # 위에서 생성한 데이터
filtered_data = filter_synthetic_data(raw_data)
print(f"원본: {len(raw_data)}개")
print(f"필터링 후: {len(filtered_data)}개")
print(f"제거됨: {len(raw_data) - len(filtered_data)}개 ({(1 - len(filtered_data)/len(raw_data)) * 100:.1f}%)")
💡 4-2. MLX로 파인튜닝
📋 데이터 포맷 변환
import json
def convert_to_mlx_format(data: List[Dict], output_file: str):
"""Alpaca 형식 → MLX-LM 형식 변환"""
mlx_data = []
for item in data:
instruction = item['instruction']
input_text = item.get('input', '')
output = item['output']
# MLX-LM 형식: {"text": "전체 대화"}
if input_text:
text = f"""Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.
### Instruction:
{instruction}
### Input:
{input_text}
### Response:
{output}"""
else:
text = f"""Below is an instruction that describes a task. Write a response that appropriately completes the request.
### Instruction:
{instruction}
### Response:
{output}"""
mlx_data.append({"text": text})
# JSONL 형식으로 저장 (한 줄에 하나씩)
with open(output_file, 'w', encoding='utf-8') as f:
for item in mlx_data:
f.write(json.dumps(item, ensure_ascii=False) + '\n')
print(f"✅ {len(mlx_data)}개 데이터를 {output_file}에 저장")
# 사용
convert_to_mlx_format(
filtered_data,
"train.jsonl"
)
# Train/Validation 분리
train_size = int(len(filtered_data) * 0.9)
train_data = filtered_data[:train_size]
valid_data = filtered_data[train_size:]
convert_to_mlx_format(train_data, "train.jsonl")
convert_to_mlx_format(valid_data, "valid.jsonl")
print(f"📊 Train: {len(train_data)}, Valid: {len(valid_data)}")💻 MLX LoRA 파인튜닝 실행
# 디렉토리 구조
mkdir -p my_synthetic_model/data
mv train.jsonl my_synthetic_model/data/
mv valid.jsonl my_synthetic_model/data/
# LoRA 파인튜닝
python -m mlx_lm.lora \
--model meta-llama/Llama-3-8B-Instruct \
--train \
--data ./my_synthetic_model/data \
--iters 1000 \
--batch-size 4 \
--learning-rate 1e-5 \
--lora-layers 16 \
--adapter-file ./my_synthetic_model/adapters/synthetic-lora
# 파라미터 설명:
# --iters: 학습 반복 횟수 (데이터 크기에 따라 조절)
# --batch-size: 배치 크기 (메모리에 따라 조절)
# --learning-rate: 학습률 (너무 높으면 불안정)
# --lora-layers: LoRA 적용 레이어 수학습 중 출력:
Iteration 1: Train loss 2.456, Val loss 2.589, It/sec 10.2
Iteration 100: Train loss 1.234, Val loss 1.456, It/sec 11.5
Iteration 200: Train loss 0.987, Val loss 1.123, It/sec 11.8
...
Iteration 1000: Train loss 0.456, Val loss 0.678, It/sec 12.1
Saved adapter to ./my_synthetic_model/adapters/synthetic-lora.npz
💡 4-3. 모델 평가
📋 정성 평가 (Human Eval)
from mlx_lm import load, generate
# 파인튜닝된 모델 로드
model, tokenizer = load(
"meta-llama/Llama-3-8B-Instruct",
adapter_file="./my_synthetic_model/adapters/synthetic-lora.npz"
)
def test_model(instruction: str, input_text: str = ""):
"""모델 테스트"""
if input_text:
prompt = f"""### Instruction:
{instruction}
### Input:
{input_text}
### Response:
"""
else:
prompt = f"""### Instruction:
{instruction}
### Response:
"""
response = generate(
model,
tokenizer,
prompt=prompt,
max_tokens=500,
temp=0.7
)
return response
# 테스트 케이스
test_cases = [
{
"instruction": "주어진 문장을 존댓말로 바꾸세요.",
"input": "오늘 날씨 좋네"
},
{
"instruction": "파이썬으로 리스트를 정렬하는 방법을 설명하세요.",
"input": ""
},
{
"instruction": "다음 리뷰의 감정을 분석하세요.",
"input": "배송이 너무 느려요. 실망입니다."
}
]
print("🧪 파인튜닝 모델 평가\n")
for i, test in enumerate(test_cases, 1):
print(f"테스트 {i}:")
print(f" 지시: {test['instruction']}")
if test['input']:
print(f" 입력: {test['input']}")
response = test_model(test['instruction'], test['input'])
print(f" 응답: {response}\n")
print("-" * 80 + "\n")📊 정량 평가 (벤치마크)
import json
from typing import List, Dict
def evaluate_accuracy(model, tokenizer, test_data: List[Dict]) -> float:
"""정확도 평가"""
correct = 0
total = len(test_data)
for item in test_data:
instruction = item['instruction']
expected_output = item['output']
# 모델 예측
predicted = test_model(instruction, item.get('input', ''))
# 간단한 매칭 (실제로는 더 정교한 평가 필요)
if expected_output.lower() in predicted.lower():
correct += 1
accuracy = (correct / total) * 100
return accuracy
# 테스트 데이터로 평가
test_accuracy = evaluate_accuracy(model, tokenizer, valid_data[:100])
print(f"📊 테스트 정확도: {test_accuracy:.2f}%")5. 품질 검증 및 개선
합성 데이터의 함정
- Model Collapse: AI가 AI 데이터로 학습 → 품질 저하
- Hallucination 증폭: 거짓 정보 학습
- 편향 강화: Teacher 모델의 편향 상속
💡 품질 검증 체크리스트
📋 1. 다양성 검사
from collections import Counter
import re
def check_diversity(data: List[Dict]):
"""데이터 다양성 검사"""
# 1. 고유 instruction 비율
instructions = [item['instruction'] for item in data]
unique_instructions = set(instructions)
diversity_ratio = len(unique_instructions) / len(instructions)
print(f"📊 다양성 분석:")
print(f" 총 데이터: {len(instructions)}개")
print(f" 고유 instruction: {len(unique_instructions)}개")
print(f" 다양성 비율: {diversity_ratio * 100:.1f}%")
if diversity_ratio < 0.7:
print(" ⚠️ 경고: 다양성이 낮습니다. 중복이 많습니다.")
else:
print(" ✅ 다양성 양호")
# 2. 길이 분포
lengths = [len(item['output']) for item in data]
avg_length = sum(lengths) / len(lengths)
print(f"\n📏 길이 분석:")
print(f" 평균: {avg_length:.0f}자")
print(f" 최소: {min(lengths)}자")
print(f" 최대: {max(lengths)}자")
# 3. 주요 키워드 분포
all_text = ' '.join([item['instruction'] + ' ' + item['output'] for item in data])
words = re.findall(r'\w+', all_text.lower())
word_freq = Counter(words).most_common(20)
print(f"\n🔤 상위 키워드:")
for word, count in word_freq[:10]:
print(f" {word}: {count}회")
# 사용
check_diversity(filtered_data)📋 2. 사실성 검증 (Fact-Checking)
import anthropic
client = anthropic.Anthropic(api_key="your-api-key")
def fact_check(statement: str) -> Dict:
"""합성 데이터의 사실 검증"""
prompt = f"""다음 진술의 사실 여부를 검증해주세요:
"{statement}"
다음 형식으로 답변:
1. 사실 여부: [사실/거짓/불명확]
2. 근거: [왜 그렇게 판단했는지]
3. 수정 제안: [거짓이면 올바른 정보 제공]
"""
message = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
messages=[{"role": "user", "content": prompt}]
)
return message.content[0].text
# 의심스러운 데이터 검증
suspicious_items = [
item for item in filtered_data
if '사실' in item['instruction'] or '역사' in item['instruction']
]
print(f"🔍 {len(suspicious_items)}개 항목 검증 중...\n")
for item in suspicious_items[:3]: # 샘플 3개만
print(f"검증 대상: {item['output'][:100]}...")
result = fact_check(item['output'])
print(f"결과:\n{result}\n")
print("-" * 80 + "\n")📋 3. 편향 감지
def detect_bias(data: List[Dict]) -> Dict:
"""편향 감지"""
bias_keywords = {
'성별': ['남자', '여자', '남성', '여성'],
'나이': ['청년', '노인', '젊은이', '어른'],
'직업': ['의사', '간호사', '엔지니어', '비서'],
}
bias_stats = {category: Counter() for category in bias_keywords}
for item in data:
text = item['instruction'] + ' ' + item['output']
for category, keywords in bias_keywords.items():
for keyword in keywords:
if keyword in text:
bias_stats[category][keyword] += 1
print("⚖️ 편향 분석:")
for category, stats in bias_stats.items():
print(f"\n{category}:")
total = sum(stats.values())
if total > 0:
for keyword, count in stats.most_common():
percentage = (count / total) * 100
print(f" {keyword}: {count}회 ({percentage:.1f}%)")
else:
print(" 언급 없음")
# 사용
detect_bias(filtered_data)6. 고급 기법
💡 기법 1: Iterative Refinement (반복 개선)
📋 개념
반복적으로 품질 향상
- 합성 데이터 생성
- 파인튜닝
- 모델로 새 데이터 생성
- 품질 필터링
- 반복
def iterative_refinement(seed_data: List[Dict], iterations: int = 3):
"""반복적 개선"""
current_data = seed_data
for i in range(iterations):
print(f"\n🔄 반복 {i+1}/{iterations}")
# 1. 현재 데이터로 파인튜닝
print(" 1 파인튜닝 중...")
# (실제 파인튜닝 코드)
# 2. 파인튜닝된 모델로 새 데이터 생성
print(" 2 새 데이터 생성 중...")
new_data = generate_new_tasks(current_data, num_new_tasks=100)
# 3. 품질 필터링
print(" 3 품질 필터링 중...")
filtered = filter_synthetic_data(new_data)
# 4. 기존 데이터와 병합
current_data.extend(filtered)
print(f" ✅ 총 데이터: {len(current_data)}개")
return current_data
# 사용
refined_data = iterative_refinement(seed_tasks, iterations=3)💡 기법 2: Multi-Agent Debate
📋 개념
여러 AI가 토론하며 답변 개선
- Agent A가 답변 생성
- Agent B가 검토 및 반박
- Agent A가 수정
- 최종 답변 선택
import anthropic
client = anthropic.Anthropic(api_key="your-api-key")
def multi_agent_debate(question: str, rounds: int = 2) -> str:
"""Multi-Agent Debate로 고품질 답변 생성"""
# Agent A: 첫 답변
prompt_a = f"질문: {question}\n\n답변을 작성해주세요."
response_a = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
messages=[{"role": "user", "content": prompt_a}]
)
answer_a = response_a.content[0].text
# Agent B: 비판 및 개선안
prompt_b = f"""다음은 어떤 질문에 대한 답변입니다:
질문: {question}
답변: {answer_a}
이 답변을 비판적으로 검토하고, 개선점을 제시해주세요.
더 나은 답변을 작성해주세요.
"""
response_b = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
messages=[{"role": "user", "content": prompt_b}]
)
answer_b = response_b.content[0].text
# 최종 합의
prompt_final = f"""두 AI가 다음 질문에 답변했습니다:
질문: {question}
답변 1: {answer_a}
답변 2 (개선): {answer_b}
두 답변을 종합하여 최고의 답변을 작성해주세요.
"""
response_final = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=1024,
messages=[{"role": "user", "content": prompt_final}]
)
final_answer = response_final.content[0].text
return final_answer
# 사용
question = "양자 컴퓨터가 암호화를 위협하는 이유는?"
best_answer = multi_agent_debate(question, rounds=2)
print(f"질문: {question}\n")
print(f"최종 답변:\n{best_answer}")💡 기법 3: Knowledge Distillation (지식 증류)
📋 개념
Teacher → Student 지식 전달
Knowledge Distillation: 큰 모델(Teacher)의 지식을 작은 모델(Student)에게 전달하여, 작은 모델도 큰 모델처럼 똑똑하게 만드는 기법
핵심 아이디어:
전통적_학습:
Student Model → Hard Label (정답: 고양이)
문제: 0 또는 1만 배움 (고양이 100%, 나머지 0%)
Knowledge_Distillation:
Student Model → Soft Label (Teacher의 확률 분포)
장점: 미묘한 지식까지 배움 (고양이 80%, 개 15%, 호랑이 5%)📋 로짓(Logits)이란?
Softmax 이전의 원시 점수
로짓(Logits): 신경망의 마지막 레이어에서 나오는 소프트맥스 이전의 값
시각화:
신경망 계산 흐름:
입력 → 레이어들 → 마지막 레이어 출력 (로짓)
↓
[2.5, 1.2, -0.8, 4.1, 0.3] ← 로짓 (Logits)
↓
Softmax 함수 적용
↓
[0.21, 0.06, 0.01, 0.71, 0.02] ← 확률 분포
↓
가장 높은 것 선택: 4번째 (71%)
왜 로짓이 중요한가?
# ❌ 확률만 보면 정보 손실
probabilities = [0.71, 0.21, 0.06, 0.02, 0.01]
# "4번이 답이다"만 알 수 있음
# ✅ 로짓을 보면 더 많은 정보
logits = [4.1, 2.5, 1.2, 0.3, -0.8]
# "4번이 답인데, 2번도 꽤 그럴듯하다"를 알 수 있음
# "5번은 확실히 아니다"도 알 수 있음💻 Knowledge Distillation 실습
1. Teacher 모델의 로짓 추출
import torch
import torch.nn.functional as F
from transformers import AutoModelForCausalLM, AutoTokenizer
# Teacher 모델 (큰 모델)
teacher_model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-3-70B")
teacher_tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-3-70B")
# Student 모델 (작은 모델)
student_model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-3-8B")
student_tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-3-8B")
def get_teacher_logits(text: str, temperature: float = 1.0):
"""Teacher 모델의 소프트 로짓 추출"""
# 입력 토큰화
inputs = teacher_tokenizer(text, return_tensors="pt")
# Teacher 모델 실행 (그래디언트 계산 안함)
with torch.no_grad():
outputs = teacher_model(**inputs)
logits = outputs.logits # 로짓 추출
# Temperature scaling으로 소프트하게
soft_logits = logits / temperature
return soft_logits
# 예시
text = "The capital of France is"
teacher_soft_logits = get_teacher_logits(text, temperature=2.0)
print(f"Teacher 로짓 shape: {teacher_soft_logits.shape}")
# torch.Size([1, 7, 128256]) - [배치, 시퀀스, 어휘 크기]2. Temperature Scaling
Temperature란?
로짓을 나누는 값으로, 확률 분포를 부드럽게(soft) 만듦
import torch
import torch.nn.functional as F
# 예시 로짓
logits = torch.tensor([2.0, 1.0, 0.5])
# Temperature = 1 (원본)
temp_1 = F.softmax(logits / 1.0, dim=-1)
print(f"T=1: {temp_1}")
# [0.659, 0.242, 0.099] - 첫 번째가 압도적
# Temperature = 2 (부드럽게)
temp_2 = F.softmax(logits / 2.0, dim=-1)
print(f"T=2: {temp_2}")
# [0.506, 0.307, 0.186] - 분포가 고르게
# Temperature = 5 (매우 부드럽게)
temp_5 = F.softmax(logits / 5.0, dim=-1)
print(f"T=5: {temp_5}")
# [0.391, 0.329, 0.280] - 거의 균등
# Temperature = 0.5 (날카롭게)
temp_05 = F.softmax(logits / 0.5, dim=-1)
print(f"T=0.5: {temp_05}")
# [0.843, 0.114, 0.043] - 첫 번째 극대화Temperature 효과:
T > 1: 소프트 (부드러운 확률 분포)
- Knowledge Distillation에 유리
- Student가 미묘한 차이 학습
T = 1: 원본 (기본값)
- 일반적인 추론
T < 1: 하드 (날카로운 확률 분포)
- 더 확실한 예측
- 창의성 감소3. Distillation Loss 계산
import torch
import torch.nn as nn
import torch.nn.functional as F
def distillation_loss(
student_logits: torch.Tensor,
teacher_logits: torch.Tensor,
labels: torch.Tensor,
temperature: float = 2.0,
alpha: float = 0.5
):
"""
Knowledge Distillation Loss
Args:
student_logits: Student 모델의 로짓
teacher_logits: Teacher 모델의 로짓
labels: 실제 정답 (hard label)
temperature: Temperature scaling 값
alpha: 두 손실 간 균형 (0~1)
Returns:
총 손실
"""
# 1. Soft Loss (Teacher의 지식)
soft_student = F.log_softmax(student_logits / temperature, dim=-1)
soft_teacher = F.softmax(teacher_logits / temperature, dim=-1)
# KL Divergence (두 분포의 차이)
soft_loss = F.kl_div(
soft_student,
soft_teacher,
reduction='batchmean'
) * (temperature ** 2) # Temperature 보정
# 2. Hard Loss (실제 정답)
hard_loss = F.cross_entropy(student_logits, labels)
# 3. 두 손실 결합
total_loss = alpha * soft_loss + (1 - alpha) * hard_loss
return total_loss
# 사용 예시
student_logits = torch.randn(32, 10) # 배치 32, 클래스 10
teacher_logits = torch.randn(32, 10)
labels = torch.randint(0, 10, (32,))
loss = distillation_loss(
student_logits,
teacher_logits,
labels,
temperature=2.0,
alpha=0.7 # Soft loss에 70% 가중치
)
print(f"Total Loss: {loss.item():.4f}")4. MLX에서 Knowledge Distillation
import mlx.core as mx
import mlx.nn as nn
from mlx_lm import load, generate
from typing import List, Dict
class DistillationTrainer:
"""MLX 기반 Knowledge Distillation"""
def __init__(
self,
teacher_model_path: str,
student_model_path: str,
temperature: float = 2.0,
alpha: float = 0.7
):
# Teacher 모델 로드 (큰 모델)
self.teacher_model, self.teacher_tokenizer = load(teacher_model_path)
# Student 모델 로드 (작은 모델)
self.student_model, self.student_tokenizer = load(student_model_path)
self.temperature = temperature
self.alpha = alpha
def get_soft_labels(self, texts: List[str]) -> mx.array:
"""Teacher 모델로부터 소프트 레이블 생성"""
soft_labels = []
for text in texts:
# Teacher 예측
inputs = self.teacher_tokenizer.encode(text)
# Forward pass (그래디언트 계산 없이)
teacher_logits = self.teacher_model(mx.array(inputs))
# Temperature scaling
soft = mx.softmax(teacher_logits / self.temperature, axis=-1)
soft_labels.append(soft)
return mx.stack(soft_labels)
def train_step(self, batch: Dict):
"""단일 학습 스텝"""
texts = batch['texts']
labels = batch['labels']
# Teacher의 소프트 레이블 생성
teacher_soft = self.get_soft_labels(texts)
# Student 예측
student_logits = self.student_model(batch['input_ids'])
# Soft loss (KL divergence)
student_soft = mx.softmax(student_logits / self.temperature, axis=-1)
soft_loss = mx.sum(
teacher_soft * (mx.log(teacher_soft) - mx.log(student_soft))
) * (self.temperature ** 2)
# Hard loss (실제 정답)
hard_loss = nn.losses.cross_entropy(student_logits, labels)
# 결합
total_loss = self.alpha * soft_loss + (1 - self.alpha) * hard_loss
return total_loss
# 사용
trainer = DistillationTrainer(
teacher_model_path="meta-llama/Llama-3-70B-Instruct",
student_model_path="meta-llama/Llama-3-8B-Instruct",
temperature=2.0,
alpha=0.7
)
# 학습 데이터
batch = {
'texts': ["The capital of France is", "Python is a"],
'labels': mx.array([...]), # 정답 토큰 ID
'input_ids': mx.array([...])
}
loss = trainer.train_step(batch)
print(f"Loss: {loss}")📊 최신 기법: FusionRoute (2025)
토큰 레벨 모델 협업
논문: Token-Level LLM Collaboration via FusionRoute (Jan 2025) 핵심: 여러 전문 모델의 로짓을 동적으로 결합
기존 방식 vs FusionRoute:
# 기존 Knowledge Distillation
Teacher (70B) → (학습 시) → Student (8B)
장점: Student 단독 사용 가능
단점: Teacher의 모든 지식 압축 어려움
# FusionRoute (추론 시 협업)
Router → Expert 1 (수학 7B)
→ Expert 2 (코딩 7B)
→ Expert 3 (일반 7B)
→ 로짓 결합 → 최종 출력
장점:
- 각 전문 분야에서 최고 성능
- 토큰마다 최적 모델 선택
- 경량 라우터만 추가 학습
단점:
- 여러 모델 메모리 필요
- 추론 속도 약간 느림FusionRoute 개념 구현:
import mlx.core as mx
from typing import List
class FusionRouter:
"""토큰 레벨 모델 협업"""
def __init__(self, expert_models: List):
self.experts = expert_models # 전문가 모델들
self.router = self._build_router() # 경량 라우터
def _build_router(self):
"""경량 라우터 네트워크"""
# 간단한 2-layer MLP
return nn.Sequential(
nn.Linear(512, 128),
nn.ReLU(),
nn.Linear(128, len(self.experts))
)
def forward(self, token_embedding: mx.array, context: mx.array):
"""토큰별 최적 모델 선택 및 로짓 결합"""
# 1. 라우터가 각 전문가의 가중치 계산
router_input = mx.concatenate([token_embedding, context])
expert_weights = mx.softmax(self.router(router_input), axis=-1)
# 2. 각 전문가의 로짓 생성
expert_logits = []
for expert in self.experts:
logits = expert(token_embedding)
expert_logits.append(logits)
expert_logits = mx.stack(expert_logits) # [num_experts, vocab_size]
# 3. 가중 평균으로 로짓 결합
# weights: [num_experts, 1]
# logits: [num_experts, vocab_size]
combined_logits = mx.sum(
expert_weights[:, None] * expert_logits,
axis=0
)
return combined_logits
# 사용 예시
router = FusionRouter(expert_models=[
math_model, # 수학 전문
code_model, # 코딩 전문
chat_model # 일반 대화 전문
])
# 토큰별 추론
for token_emb, context in input_stream:
logits = router.forward(token_emb, context)
next_token = mx.argmax(logits)📋 Knowledge Distillation vs Synthetic Data
비교표:
| 측면 | Knowledge Distillation | Synthetic Data |
|---|---|---|
| 목적 | 모델 압축 (큰→작은) | 데이터 확장 |
| 입력 | Teacher 모델 | Seed 데이터 |
| 출력 | 압축된 Student 모델 | 대량의 학습 데이터 |
| 사용 시점 | 학습 시 | 데이터 생성 시 |
| 메모리 | 학습 시 Teacher+Student | 추론 시 모델 1개만 |
| 속도 | 배포 후 빠름 (작은 모델) | 생성 시간 필요 |
| 품질 | Teacher 성능에 의존 | Teacher 성능에 의존 |
결합 사용:
# 1단계: Synthetic Data로 학습 데이터 생성
synthetic_dataset = generate_alpaca_data(num=10000)
# 2단계: Teacher 모델 파인튜닝
teacher = finetune(large_model, synthetic_dataset)
# 3단계: Knowledge Distillation으로 압축
student = distill(teacher, small_model)
# 결과: 작고 빠르면서도 고성능인 모델 ✅🎯 실전 활용 시나리오
시나리오 1: 엣지 디바이스 배포
문제:
- 스마트폰에서 70B 모델 실행 불가
- 8B 모델은 성능 부족
해결:
1. Synthetic Data로 100k 데이터 생성
2. 70B 모델 파인튜닝 (서버에서)
3. Knowledge Distillation으로 8B 압축
4. 스마트폰에 배포 ✅
결과:
- 8B 크기 (2GB)
- 70B 수준 성능 (90% 유지)
- 스마트폰에서 실시간 실행시나리오 2: API 비용 절감
문제:
- GPT-4 API 비용 ($0.03/1k tokens)
- 월 1000만 토큰 = $300
해결:
1. GPT-4로 10k Synthetic Data 생성 ($30)
2. Llama-70B 파인튜닝 (무료, 로컬)
3. Distillation → Llama-8B (무료, 로컬)
4. 자체 서버에서 운영
결과:
- 초기 비용: $30
- 운영 비용: $0 (자체 GPU)
- ROI: 1개월 만에 회수7. 실전 프로젝트: 한국어 챗봇 만들기
💡 전체 파이프라인
import anthropic
import json
from typing import List, Dict
client = anthropic.Anthropic(api_key="your-api-key")
# 1단계: 다양한 시나리오 데이터 생성
def create_korean_chatbot_dataset(num_samples: int = 1000) -> List[Dict]:
"""한국어 챗봇 학습 데이터 생성"""
scenarios = [
"일상 대화",
"고객 서비스",
"기술 지원",
"교육 및 학습",
"감정 지원",
"정보 검색",
"일정 관리",
"추천 시스템"
]
all_data = []
for scenario in scenarios:
print(f"📝 {scenario} 시나리오 생성 중...")
prompt = f"""한국어 챗봇 학습을 위한 대화 데이터를 생성해주세요.
시나리오: {scenario}
다음 형식으로 {num_samples // len(scenarios)}개 생성:
{{
"user": "사용자 메시지",
"assistant": "챗봇 응답",
"context": "대화 맥락 (선택)"
}}
자연스럽고 다양한 한국어 대화를 만들어주세요.
JSON 배열로 반환.
"""
message = client.messages.create(
model="claude-3-5-sonnet-20241022",
max_tokens=4096,
messages=[{"role": "user", "content": prompt}]
)
response_text = message.content[0].text
# JSON 추출
import re
json_match = re.search(r'```json\s*(.*?)\s*```', response_text, re.DOTALL)
if json_match:
json_text = json_match.group(1)
else:
json_text = response_text
try:
scenario_data = json.loads(json_text)
all_data.extend(scenario_data)
except:
continue
return all_data
# 2단계: 데이터 생성
print("🤖 한국어 챗봇 데이터 생성 시작...\n")
chatbot_data = create_korean_chatbot_dataset(num_samples=800)
print(f"\n✅ {len(chatbot_data)}개 대화 데이터 생성 완료!")
# 3단계: MLX 형식 변환
def convert_chatbot_to_mlx(data: List[Dict], output_file: str):
"""챗봇 데이터 → MLX 형식"""
mlx_data = []
for item in data:
user_msg = item['user']
assistant_msg = item['assistant']
text = f"""<|user|>
{user_msg}
<|assistant|>
{assistant_msg}"""
mlx_data.append({"text": text})
with open(output_file, 'w', encoding='utf-8') as f:
for item in mlx_data:
f.write(json.dumps(item, ensure_ascii=False) + '\n')
convert_chatbot_to_mlx(chatbot_data, "chatbot_train.jsonl")
# 4단계: 파인튜닝 (Bash에서 실행)
print("\n📚 다음 명령어로 파인튜닝을 시작하세요:")
print("""
python -m mlx_lm.lora \\
--model meta-llama/Llama-3-8B-Instruct \\
--train \\
--data ./chatbot_data \\
--iters 2000 \\
--batch-size 4 \\
--learning-rate 1e-5 \\
--adapter-file ./korean_chatbot_adapter
""")8. 참고 자료
📚 핵심 논문
Synthetic Data 생성:
- Self-Instruct (2023) - Stanford
- Alpaca (2023) - Stanford
- Orca (2023) - Microsoft
- Synthetic Data Survey (2024)
Knowledge Distillation & 모델 협업:
- Distilling the Knowledge in a Neural Network (2015) - Hinton et al.
- FusionRoute (2025) - Token-Level LLM Collaboration
📚 도구 및 라이브러리
9. 마무리
💡 핵심 요약
Synthetic Data Fine-Tuning 체크리스트
✅ 장점:
- 비용 97% 절감
- 시간 97% 단축
- 무제한 확장 가능
- 프라이버시 안전
⚠️ 주의사항:
- 품질 검증 필수
- Teacher 모델 의존성
- Model Collapse 위험
- 사실성 확인 필요
🎯 Best Practices
Synthetic Data 생성:
- 소량의 고품질 Seed 데이터부터 시작
- 다양성 확보를 위한 프롬프트 엔지니어링
- 반복적 필터링으로 품질 관리
- 실제 데이터와 혼합 사용 (80% 합성 + 20% 실제)
- 지속적 평가 및 개선
Knowledge Distillation 활용: 6. Temperature = 2-4 권장 (소프트 레이블 생성) 7. Alpha = 0.7 권장 (Soft loss 70%, Hard loss 30%) 8. Synthetic Data + KD 결합으로 최적 효율 9. FusionRoute 방식 고려 (여러 전문 모델 협업) 10. 모델 크기 10배 압축 가능 (70B → 7B)
문서 작성일: 2026-01-11 최종 업데이트: 2026-01-11