7장. SFT 데이터 구성과 Chat Template
좋은 SFT 데이터의 조건
요리 학원에서 레시피 카드로 연습한다고 해 봅시다. 카드에 소금 양이 틀리게 적혀 있으면 틀린 맛을 배웁니다. 카드마다 계량 단위가 제각각이면 무엇을 따라야 할지 헷갈립니다. 김치찌개 카드만 가득하면 다른 요리는 못 합니다. 같은 카드가 열 장씩 겹쳐 있거나 얼룩이 묻어 있으면 이상한 습관이 듭니다. SFT 데이터도 똑같습니다.
그림의 다섯 상자가 강사자료가 꼽은 좋은 데이터의 조건입니다. 왼쪽 둘(정확성, 일관성)은 "답안이 바르고 한결같은가", 가운데(다양성)는 "질문이 고루 퍼졌는가", 오른쪽 둘(적정 길이, 깨끗함)은 "모양이 고른가"를 봅니다.
다섯 조건과 지키지 않을 때
| 조건 | 확인할 점 | 지키지 않으면 |
|---|---|---|
| 정확성 | 답변 내용이 사실과 규정에 맞는가 | 틀린 내용을 그대로 학습 |
| 일관성 | 말투·형식·기준이 예시마다 같은가 | 모델이 무엇을 따라야 할지 혼란 |
| 다양성 | 질문 주제와 표현이 충분히 다양한가 | 학습한 질문에서만 잘 동작 |
| 적정 길이 | 너무 짧거나 너무 긴 예시가 섞여 있지 않은가 | max_length 에서 잘리거나 끝맺음을 못 배움 |
| 깨끗하고 안전한 데이터 | 중복·HTML·복사 오류, 개인정보·유해 표현이 없는가 | 이상한 패턴을 학습하거나 민감정보 노출 |
양보다 질이 중요합니다. LIMA(2023) 연구는 잘 고른 예시 1,000개만으로도 대화 품질을 크게 끌어올릴 수 있음을 보였습니다. 말투나 형식처럼 좁은 목표라면 깨끗한 예시 수백 개로도 효과를 볼 수 있습니다. 이 장의 데이터도 327개입니다.
짧은 예시
import pandas as pd
# 일부러 문제를 섞은 작은 상담 데이터(설명용으로 만든 다섯 행)
raw = pd.DataFrame({
"prompt": ["배송비는 얼마인가요?", "배송비는 얼마인가요?", "환불 되나요?", "비밀번호를 잊었어요", None],
"response": ["3만 원 이상이면 무료입니다.", "3만 원 이상이면 무료입니다.", "네",
"<p>재설정 링크를 보내 드립니다.</p>", "회원 정보에서 바꿀 수 있습니다."],
})
# 조건마다 걸리는 행 수를 센다
issues = {
# isna().any(axis=1): 행 안에 빈칸이 하나라도 있으면 True
"빈값": int(raw.isna().any(axis=1).sum()),
# duplicated(): 앞에 똑같은 행이 있으면 True(첫 행은 남긴다)
"중복 문답": int(raw.duplicated().sum()),
# 5자 미만 답변은 너무 짧아 끝맺음을 배우기 어렵다
"짧은 답": int((raw["response"].str.len() < 5).sum()),
# "<p>" 같은 HTML 태그가 섞인 답변. regex=True 는 정규식으로 찾는다는 뜻
"HTML": int(raw["response"].str.contains("<[a-z/]", regex=True).sum()),
}
print(issues) # 결과: {'빈값': 1, '중복 문답': 1, '짧은 답': 1, 'HTML': 1}
# 문제 행을 걸러 낸다: 빈값 행 빼기 → 중복 빼기 → 짧은 답과 HTML 빼기
clean = raw.dropna().drop_duplicates()
clean = clean[(clean["response"].str.len() >= 5) & ~clean["response"].str.contains("<[a-z/]", regex=True)]
print(len(raw), "→", len(clean)) # 결과: 5 → 1이 코드가 뜻하는 것
레시피 카드 다섯 장 중 얼룩진 카드, 겹친 카드, 반쯤 찢긴 카드, 빈 카드를 골라내는 일입니다. 다섯 장 중 네 장이 하나씩 문제가 있어 깨끗한 카드는 한 장만 남습니다. 실제 데이터에서는 이렇게 지우기 전에 왜 그런 행이 생겼는지 원인을 먼저 봅니다. HTML 태그는 지우지 않고 태그만 벗겨 살릴 수도 있습니다.
| 줄 | 하는 일 | 쉽게 말하면 |
|---|---|---|
raw.isna().any(axis=1) | 빈칸이 있는 행을 찾는다 | 빈 카드 |
raw.duplicated() | 앞과 똑같은 행을 찾는다 | 겹친 카드 |
str.len() < 5 | 너무 짧은 답변을 찾는다 | 반쯤 찢긴 카드 |
str.contains("<[a-z/]", regex=True) | HTML 태그가 섞인 답변을 찾는다 | 얼룩진 카드 |
주의할 점
- 정확성과 일관성은 코드로 다 잡히지 않습니다. 몇십 개를 뽑아 사람이 직접 읽어 보는 일을 건너뛰지 않습니다.
- 개인정보(이름, 전화번호, 주문번호)가 답변에 들어 있으면 모델이 그대로 외워 다른 손님에게 말할 수 있습니다. 학습 전에 지우거나 가립니다.