파인튜닝 성패를 좌우하는 학습 데이터의 설계 원칙, 수집 전략, 데이터 형식을 실전 관점에서 체계적으로 안내합니다.
파인튜닝에서 데이터의 중요성은 아무리 강조해도 지나치지 않습니다. 최첨단 학습 기법을 사용하더라도 데이터가 부실하면 결과가 좋을 수 없습니다. 반대로, 잘 설계된 수천 개의 고품질 데이터는 수만 개의 저품질 데이터보다 훨씬 나은 결과를 만들어 냅니다.
LIMA 논문(2023)은 이 사실을 명확하게 보여줍니다. Meta 연구진은 단 1,000개의 신중하게 큐레이션된 예제만으로 GPT-4에 근접하는 성능의 모델을 학습시켰습니다. 이는 "Less Is More for Alignment"라는 부제가 시사하듯, 데이터의 양보다 질이 압도적으로 중요하다는 것을 실증적으로 증명한 사례입니다.
실무에서 필요한 데이터 규모는 작업의 복잡도와 목표 성능에 따라 달라집니다.
작업 유형별 권장 데이터 규모:
단순 분류/추출: 500 ~ 2,000개
지시 따르기 (일반): 1,000 ~ 5,000개
도메인 특화 대화: 3,000 ~ 10,000개
복잡한 추론 작업: 5,000 ~ 20,000개
다국어/다중 작업: 10,000 ~ 50,000개데이터를 무작정 늘리는 것은 오히려 해로울 수 있습니다. 저품질 데이터가 섞이면 모델이 잘못된 패턴을 학습하게 되며, 이를 바로잡기 위해 더 많은 고품질 데이터가 필요해지는 악순환에 빠질 수 있습니다.
파인튜닝의 유형에 따라 데이터 형식이 달라집니다. 가장 널리 사용되는 세 가지 형식을 살펴봅니다.
SFT에서 가장 기본적인 형식으로, 지시문(instruction), 입력(input), 출력(output)의 세 필드로 구성됩니다. Alpaca 형식이라고도 불립니다.
{
"instruction": "주어진 SQL 쿼리를 최적화해 주세요.",
"input": "SELECT * FROM users WHERE created_at > '2025-01-01' AND status = 'active' ORDER BY created_at DESC",
"output": "최적화된 쿼리:\n\nSELECT id, name, email, created_at\nFROM users\nWHERE status = 'active'\n AND created_at > '2025-01-01'\nORDER BY created_at DESC\n\n변경 사항:\n1. SELECT *를 필요한 컬럼만 명시하도록 변경했습니다.\n2. WHERE 조건에서 선택도가 높은 status 조건을 먼저 배치했습니다.\n3. (status, created_at) 복합 인덱스 생성을 권장합니다."
}대화형 모델의 학습에 사용하는 형식입니다. 시스템 메시지, 사용자 메시지, 어시스턴트 메시지의 연속으로 구성됩니다. 멀티턴 대화를 자연스럽게 학습시킬 수 있습니다.
{
"messages": [
{
"role": "system",
"content": "당신은 시니어 백엔드 엔지니어입니다. 코드 리뷰를 수행할 때 보안, 성능, 가독성 관점에서 구체적인 개선 사항을 제시합니다."
},
{
"role": "user",
"content": "이 API 엔드포인트를 리뷰해 주세요.\n\ndef get_user(user_id):\n query = \"SELECT * FROM users WHERE id = \" + str(user_id)\n return db.execute(query)"
},
{
"role": "assistant",
"content": "이 코드에는 심각한 보안 취약점과 몇 가지 개선 사항이 있습니다.\n\n1. SQL 인젝션 취약점: 문자열 연결로 쿼리를 구성하고 있어 SQL 인젝션 공격에 노출됩니다. 파라미터 바인딩을 사용해야 합니다.\n\n2. SELECT * 사용: 필요한 컬럼만 명시적으로 지정하는 것이 성능과 보안 측면에서 바람직합니다.\n\n3. 에러 처리 부재: 사용자가 존재하지 않는 경우의 처리가 없습니다.\n\n개선된 코드를 제안합니다."
}
]
}DPO, RLHF 등 선호도 기반 학습에 사용하는 형식입니다. 동일한 프롬프트에 대해 선호(chosen)와 비선호(rejected) 응답 쌍을 제공합니다.
{
"prompt": "Python 딕셔너리에서 특정 키가 존재하는지 확인하는 방법은?",
"chosen": "Python에서 딕셔너리의 키 존재 여부를 확인하는 가장 권장되는 방법은 in 연산자를 사용하는 것입니다.\n\nif 'key' in my_dict:\n value = my_dict['key']\n\n또는 .get() 메서드를 사용하면 키가 없을 때 기본값을 반환받을 수 있습니다.\n\nvalue = my_dict.get('key', default_value)\n\n.get()은 KeyError를 방지하면서 코드를 간결하게 작성할 수 있어 실무에서 자주 사용됩니다.",
"rejected": "has_key 메서드를 사용하면 됩니다. my_dict.has_key('key')로 확인할 수 있습니다."
}rejected 응답에서 has_key()는 Python 3에서 제거된 메서드입니다. 이처럼 rejected 응답은 명확하게 잘못되었거나 품질이 낮아야 합니다. chosen과 rejected의 차이가 모호하면 모델이 의미 있는 선호도를 학습하기 어렵습니다.
고품질 학습 데이터를 확보하는 방법은 크게 네 가지로 나뉩니다.
조직 내에 이미 축적된 데이터를 활용하는 것이 가장 효율적입니다. 고객 상담 로그, 기술 문서, 코드 리뷰 기록, FAQ 등이 좋은 소스입니다.
# 기존 고객 상담 로그를 학습 데이터로 변환하는 예시
import json
def convert_support_logs(logs):
training_data = []
for log in logs:
if log["resolution_rating"] >= 4: # 높은 평가를 받은 상담만
entry = {
"messages": [
{
"role": "system",
"content": "고객 지원 담당자로서 정확하고 "
"친절한 응답을 제공합니다."
},
{
"role": "user",
"content": log["customer_query"]
},
{
"role": "assistant",
"content": log["agent_response"]
}
]
}
training_data.append(entry)
return training_data가장 높은 품질을 보장하지만 비용이 많이 드는 방법입니다. 도메인 전문가가 직접 입력-출력 쌍을 작성하거나, 기존 데이터를 검수합니다.
효과적인 어노테이션을 위한 핵심 원칙은 다음과 같습니다.
강력한 LLM을 사용하여 학습 데이터를 생성하는 방법입니다. 비용 효율이 높고 대량 생산이 가능하지만, 품질 검증이 반드시 필요합니다.
# Claude API를 사용한 합성 데이터 생성 예시
import anthropic
client = anthropic.Anthropic()
def generate_synthetic_data(topic, num_examples):
prompt = (
"다음 주제에 대한 고품질 SFT 학습 데이터를 "
+ str(num_examples) + "개 생성해 주세요.\n\n"
"주제: " + topic + "\n\n"
"각 예제는 다음 JSON 형식을 따라야 합니다:\n"
'{"instruction": "...", "input": "...", "output": "..."}\n\n'
"요구사항:\n"
"1. instruction은 구체적이고 다양해야 합니다.\n"
"2. output은 정확하고 상세해야 합니다.\n"
"3. 난이도를 초급/중급/고급으로 골고루 분포시켜 주세요.\n"
"4. 각 예제는 JSON 형식으로, 하나씩 줄바꿈으로 구분해 주세요."
)
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=4096,
messages=[{"role": "user", "content": prompt}]
)
return response.content[0].text합성 데이터에는 모델 붕괴(Model Collapse)의 위험이 있습니다. LLM이 생성한 데이터로 다른 LLM을 학습시키면, 세대를 거듭할수록 다양성이 줄어들고 특정 패턴에 수렴하는 현상이 발생합니다. 합성 데이터는 전체 데이터의 50% 이하로 유지하고, 반드시 인간 검수를 거치는 것이 좋습니다.
기존 소수의 시드(Seed) 예제를 사용하여 LLM이 새로운 예제를 자동 생성하는 방법입니다. 2023년 Stanford의 Alpaca 프로젝트에서 널리 알려졌습니다.
Self-Instruct 파이프라인:
시드 예제 (수십 개)
|
LLM으로 새로운 instruction 생성
|
중복/저품질 필터링
|
LLM으로 각 instruction에 대한 output 생성
|
품질 검증 및 필터링
|
최종 학습 데이터셋학습 데이터의 다양성은 모델의 일반화 능력에 직접적인 영향을 미칩니다. 특정 패턴에 편중된 데이터로 학습하면 모델은 해당 패턴만 잘 처리하고 나머지에서는 성능이 떨어집니다.
다양성은 여러 차원에서 확보해야 합니다.
다양성 차원:
입력 다양성:
- 질문/지시의 형태 (명령형, 의문형, 조건부 등)
- 주제 범위 (도메인 내 다양한 하위 주제)
- 난이도 분포 (초급 ~ 고급)
- 입력 길이 분포 (짧은 질문 ~ 긴 문맥)
출력 다양성:
- 응답 길이 분포 (한 줄 ~ 여러 단락)
- 응답 형식 (설명, 코드, 목록, 비교표 등)
- 어조와 격식 수준from collections import Counter
import numpy as np
def measure_diversity(dataset):
"""학습 데이터의 다양성을 측정하는 간단한 방법"""
# 1. 입력 길이 분포
input_lengths = [len(d["instruction"].split()) for d in dataset]
print("입력 길이 - 평균: " + str(np.mean(input_lengths))
+ ", 표준편차: " + str(np.std(input_lengths)))
# 2. 출력 길이 분포
output_lengths = [len(d["output"].split()) for d in dataset]
print("출력 길이 - 평균: " + str(np.mean(output_lengths))
+ ", 표준편차: " + str(np.std(output_lengths)))
# 3. 시작 단어 다양성 (응답 패턴 편중 감지)
first_words = [d["output"].split()[0] for d in dataset if d["output"]]
word_counts = Counter(first_words)
top_5 = word_counts.most_common(5)
print("가장 빈번한 시작 단어: " + str(top_5))
# 4. 고유 n-gram 비율
all_words = " ".join([d["output"] for d in dataset]).split()
bigrams = list(zip(all_words[:-1], all_words[1:]))
unique_ratio = len(set(bigrams)) / len(bigrams) if bigrams else 0
print("고유 바이그램 비율: " + str(round(unique_ratio, 4)))학습 데이터를 모델에 입력하기 전에 프롬프트 템플릿을 적용하여 일관된 형식으로 변환합니다. 템플릿은 모델이 입력과 출력의 경계를 명확하게 인식하도록 돕습니다.
현재 가장 널리 사용되는 템플릿 형식입니다. OpenAI에서 제안한 ChatML을 기반으로 하며, 대부분의 최신 모델이 이 형식을 지원합니다.
<|im_start|>system
당신은 숙련된 Python 개발자입니다.<|im_end|>
<|im_start|>user
리스트 컴프리헨션의 사용법을 알려주세요.<|im_end|>
<|im_start|>assistant
리스트 컴프리헨션은 기존 리스트에서 새로운 리스트를 생성하는 간결한 방법입니다...<|im_end|>Meta의 Llama 모델 시리즈에서 사용하는 템플릿입니다.
<s>[INST] <<SYS>>
당신은 숙련된 Python 개발자입니다.
<</SYS>>
리스트 컴프리헨션의 사용법을 알려주세요. [/INST]
리스트 컴프리헨션은 기존 리스트에서 새로운 리스트를 생성하는 간결한 방법입니다... </s>사용하는 베이스 모델의 토크나이저에 내장된 chat_template을 확인하고, 해당 형식에 맞춰 데이터를 구성하는 것이 가장 안전합니다. transformers 라이브러리의 tokenizer.apply_chat_template() 메서드를 사용하면 자동으로 올바른 형식이 적용됩니다.
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-3.1-8B-Instruct")
messages = [
{"role": "system", "content": "당신은 숙련된 Python 개발자입니다."},
{"role": "user", "content": "리스트 컴프리헨션의 사용법을 알려주세요."},
]
# 토크나이저의 내장 템플릿 적용
formatted = tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True
)
print(formatted)학습 데이터를 train/validation/test 세트로 분할하는 것은 과적합을 방지하고 모델 성능을 정확하게 평가하기 위해 필수적입니다.
from sklearn.model_selection import train_test_split
def split_dataset(data, test_size=0.1, val_size=0.1, seed=42):
"""학습 데이터를 train/val/test로 분할"""
# test 세트 분리
train_val, test = train_test_split(
data, test_size=test_size, random_state=seed
)
# validation 세트 분리
adjusted_val_size = val_size / (1 - test_size)
train, val = train_test_split(
train_val, test_size=adjusted_val_size, random_state=seed
)
print("Train: " + str(len(train))
+ ", Val: " + str(len(val))
+ ", Test: " + str(len(test)))
return train, val, test분할 시 주의해야 할 점은 데이터 누출(Data Leakage)입니다. 동일하거나 유사한 예제가 train과 test 세트에 동시에 포함되면 평가 결과가 부풀려집니다. 특히 합성 데이터를 사용할 때 동일한 시드 예제에서 파생된 데이터가 양쪽에 나뉠 수 있으므로 주의가 필요합니다.
이번 장에서는 파인튜닝 학습 데이터의 설계와 구축 방법을 다루었습니다.
다음 장에서는 수집된 원시 데이터를 학습에 적합한 형태로 가공하는 품질 관리와 전처리 파이프라인을 구축합니다. 데이터 정제, 중복 제거, 토큰화, 패딩 전략 등 실전에서 반드시 필요한 기법을 다룹니다.
이 글이 도움이 되셨나요?
관련 주제 더 보기
파인튜닝 학습 데이터의 정제, 중복 제거, 토큰화, 패딩 전략 등 실전 전처리 파이프라인을 구축하는 방법을 다룹니다.
LLM 파인튜닝이 무엇인지, 사전 학습 모델과 어떤 관계가 있는지, 언제 파인튜닝이 필요한지를 체계적으로 정리합니다.
LoRA(Low-Rank Adaptation)의 수학적 원리를 이해하고, 타겟 레이어 선택부터 하이퍼파라미터 튜닝까지 실전 적용법을 다룹니다.