7장. SFT 데이터 구성과 Chat Template
데이터 불러오기와 점검
장을 봐 온 재료로 요리하기 전에 상한 것은 없는지, 같은 것을 두 번 사 오지 않았는지, 양은 어느 정도인지 먼저 봅니다. 학습 데이터도 학습 전에 빈값, 중복, 길이를 봅니다. 이 일은 데이터 분석 수업에서 하던 탐색(EDA)과 똑같습니다.
큰 그림의 둘째 칸이 이 절입니다. 원본 노트북은 답변의 글자 수로 길이를 대략 가늠합니다. 모델이 실제로 세는 단위는 토큰이므로, 아래 예시에서는 Qwen 토크나이저로 토큰 수도 함께 재어 봅니다.
점검 항목과 pandas 도구
| 점검 | pandas 도구 | 실습 1 의 결과 |
|---|---|---|
| 행 수 | len(df) | 327 |
| 빈값 | df[cols].isna().sum() | prompt 0, response 0 |
| 중복 질문 | df["prompt"].duplicated().sum() | 0 |
| 중복 문답 | df.duplicated(subset=[...]).sum() | 0 |
| 답변 길이 | df["response"].str.len().agg(["min", "median", "max"]) | 18, 30, 56 자 |
짧은 예시
import pandas as pd
# AutoTokenizer = 모델 이름만 주면 그 모델에 맞는 토크나이저를 골라 주는 도구
from transformers import AutoTokenizer
persona = pd.read_csv("data/persona_sft_raw.csv")
# from_pretrained = 이미 만들어 둔(pretrained) 설정을 내려받아 불러오기. 모델 가중치는 불러오지 않는다
tok = AutoTokenizer.from_pretrained("Qwen/Qwen2.5-0.5B-Instruct")
# 답변마다 글자 수와 토큰 수를 잰다. tok(문장)["input_ids"] 가 토큰 ID 목록이다
chars = persona["response"].str.len()
tokens = persona["response"].map(lambda s: len(tok(s)["input_ids"]))
print("글자 수:", chars.agg(["min", "median", "max"]).tolist()) # 결과: [18.0, 30.0, 56.0]
print("토큰 수:", tokens.agg(["min", "median", "max"]).tolist()) # 결과: [13.0, 24.0, 47.0]
# 글자 하나당 토큰이 몇 개인지 평균을 낸다
print("글자당 토큰:", round((tokens / chars).mean(), 2)) # 결과: 0.8이 코드가 뜻하는 것
재료의 무게를 눈대중(글자 수)과 저울(토큰 수)로 함께 재 본 코드입니다. 답변 하나는 13~47 토큰, 가운데 값이 24 토큰입니다. 첫 답변 "떨고 있느냐. ... 귀 기울이지 않아."는 52자, 34토큰입니다. 손으로 보면 34 ÷ 52 ≈ 0.65 이고, 327개를 평균 내면 글자당 0.8 토큰쯤입니다. 질문과 템플릿 머리표를 더해도 예시 하나가 100 토큰 안팎이라, 8장 학습의 max_length 에서 잘릴 걱정이 없습니다.
| 줄 | 하는 일 | 쉽게 말하면 |
|---|---|---|
AutoTokenizer.from_pretrained(...) | Qwen 의 토크나이저를 불러온다 | 모델이 쓰는 자를 빌려 오기 |
persona["response"].str.len() | 글자 수를 잰다 | 눈대중 |
len(tok(s)["input_ids"]) | 토큰 수를 잰다 | 저울 |
(tokens / chars).mean() | 글자당 토큰 수 평균 | 눈대중과 저울의 비율 |
주의할 점
- 글자 수와 토큰 수의 비율은 언어와 토크나이저마다 다릅니다. 영어, 숫자, 이모지는 비율이 크게 달라질 수 있으므로 최종 길이는 꼭 토큰으로 잽니다.
- 중복은 "질문만 같은 것"과 "질문과 답이 모두 같은 것"을 나눠 봅니다. 질문이 같고 답이 다르면 일관성 문제일 수 있습니다.