🔬 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. 데이터 부족 문제 해결
  2. 프라이버시 보호
  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단계 파이프라인

  1. Seed Data 준비 (소량의 실제 데이터 또는 템플릿)
  2. LLM으로 확장 (GPT-4, Claude 등으로 대량 생성)
  3. 필터링 및 검증 (품질 낮은 데이터 제거)

💡 방법 1: Self-Instruct (가장 유명)

📋 개념

Self-Instruct란?

LLM에게 “스스로 학습 데이터를 만들어”라고 지시하는 방법

프로세스:

  1. Seed 작업 예시 제공 (20-30개)
  2. LLM이 비슷한 작업 생성
  3. LLM이 답변 생성
  4. 품질 필터링
  5. 반복

논문: 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. 품질 검증 및 개선

합성 데이터의 함정

  1. Model Collapse: AI가 AI 데이터로 학습 → 품질 저하
  2. Hallucination 증폭: 거짓 정보 학습
  3. 편향 강화: 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 (반복 개선)

📋 개념

반복적으로 품질 향상

  1. 합성 데이터 생성
  2. 파인튜닝
  3. 모델로 새 데이터 생성
  4. 품질 필터링
  5. 반복
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가 토론하며 답변 개선

  1. Agent A가 답변 생성
  2. Agent B가 검토 및 반박
  3. Agent A가 수정
  4. 최종 답변 선택
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 DistillationSynthetic 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 생성:

Knowledge Distillation & 모델 협업:

📚 도구 및 라이브러리


9. 마무리

💡 핵심 요약

Synthetic Data Fine-Tuning 체크리스트

장점:

  • 비용 97% 절감
  • 시간 97% 단축
  • 무제한 확장 가능
  • 프라이버시 안전

⚠️ 주의사항:

  • 품질 검증 필수
  • Teacher 모델 의존성
  • Model Collapse 위험
  • 사실성 확인 필요

🎯 Best Practices

Synthetic Data 생성:

  1. 소량의 고품질 Seed 데이터부터 시작
  2. 다양성 확보를 위한 프롬프트 엔지니어링
  3. 반복적 필터링으로 품질 관리
  4. 실제 데이터와 혼합 사용 (80% 합성 + 20% 실제)
  5. 지속적 평가 및 개선

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