지난 글까지 세 편이 모두 같은 전제 위에 있었다. 학습에 넣을 데이터가 이미 있다는 전제다. 실제로는 그 데이터를 만드는 일이 전체 작업의 대부분이고, 사람이 쓰면 예시 하나에 수천 원과 며칠이 든다. 그래서 지금은 상당 부분을 모델이 만든다.
「모델이 만든 데이터로 모델을 가르치면 새로 배우는 것이 없지 않나」가 자연스러운 의문이다. 답은 생성과 검증의 난이도가 다르다는 데 있다. 모델이 수학 문제를 한 번에 맞힐 확률은 낮아도, 열 번 풀어 그중 맞은 것을 골라내는 일은 검증기가 한다. 골라낸 것만 모으면 원래 모델보다 정확한 데이터가 된다. 스무 번 중 한 번 나오는 좋은 답을 항상 나오게 만드는 것 — 합성 데이터가 하는 일은 대체로 이것이다.
만드는 방법들
무엇이 부족한지에 따라 쓰는 방법이 다르다. 프롬프트가 부족한 경우와 좋은 답이 부족한 경우는 다른 문제다.
| 방법 | 무엇을 늘리나 | 어떻게 |
|---|---|---|
| 씨앗 확장 | 프롬프트 | 사람이 쓴 예시 몇 개를 보여 주고 비슷한 것을 계속 만들게 한다 |
| 난이도 진화 | 프롬프트 | 기존 프롬프트에 조건을 더하거나 단계를 늘려 어렵게 만든다 |
| 페르소나 조합 | 프롬프트 | 「간호사가」「법무팀이」처럼 화자를 바꿔 같은 주제를 다시 낸다 |
| 거부 샘플링 | 답 | 한 프롬프트에 답을 여러 개 만들고 검증을 통과한 것만 남긴다 |
| 증류 | 답 | 더 강한 모델에게 답을 받는다 |
| 역생성 | 프롬프트 | 가진 문서에서 「이 문서가 답이 되는 질문」을 거꾸로 만든다 |
거부 샘플링이 실무에서 가장 자주 쓰인다. 앞 글의 검증기를 그대로 재활용할 수 있고, 우리 모델이 낸 답이라 문체와 형식이 이미 우리 것이기 때문이다. 코드는 짧다.
def build_dataset(prompts, k=8):
rows = []
for p in prompts:
cands = model.generate(p, n=k, temperature=1.0)
ok = [c for c in cands if verify(p, c)]
if not ok:
continue # 하나도 못 맞힌 문제는 버린다
if len(ok) == k:
continue # 다 맞힌 문제는 배울 것이 없다
rows.append({"prompt": p, "answer": shortest(ok)})
return rows
두 개의 continue 가 이 함수의 전부다. 하나도 못 맞힌 문제는 답이 없어서 버리고, 전부 맞힌 문제는 이미 아는 것이라 버린다. 남는 것은 「가끔 맞히는」 구간이고, 학습으로 바뀔 여지가 있는 곳이 정확히 거기다. 앞 글에서 무리의 통과율이 0이나 1로 굳은 프롬프트를 뺀 것과 같은 판단이다.
shortest도 사소하지 않다. 맞은 답 중 무엇을 남길지가 학습되는 답의 성격을 정한다. 가장 짧은 것을 고르면 군더더기 없는 풀이가 학습되고, 아무거나 고르면 길이가 들쭉날쭉해진다. 정답률이 같다면 짧은 쪽이 대개 낫다.
거르는 단계가 진짜 작업이다
생성은 API 호출이고 거르기는 설계다. 순서대로 통과시킨다.
- 검증 통과. 채점할 수 있는 과제라면 여기서 대부분이 걸러진다
- 형식. 요구한 구조를 지켰는지 본다. 파싱이 안 되는 것은 조용히 버린다
- 중복 제거. 글자가 겹치는 것은 n-gram으로, 뜻이 겹치는 것은 임베딩으로 거른다. 둘 다 필요하다 — 표현만 바꾼 같은 문제는 글자 비교로 안 잡힌다
- 난이도. 통과율이 0이나 1인 프롬프트를 뺀다
- 오염. 평가에 쓸 벤치마크 문제가 섞여 들어왔는지 검사한다
마지막 줄을 빠뜨리면 모든 숫자가 거짓이 된다. 씨앗을 인터넷에서 모으거나 강한 모델에게 「수학 문제를 만들어 달라」고 하면, 그 모델이 학습 때 본 벤치마크 문제를 그대로 뱉는 일이 실제로 있다. 그러면 평가 점수는 오르는데 실제 능력은 그대로다. 검사는 단순하다 — 평가셋의 각 문항과 학습 데이터를 n-gram으로 대조해 겹치는 것을 뺀다.
다양성이 조용히 무너진다
합성 데이터의 가장 흔한 실패는 품질이 아니라 다양성 쪽에서 온다. 씨앗 20개를 보여 주고 2만 개를 만들면, 2만 개가 나오기는 하는데 서로 비슷하다.
이유는 단순하다. 모델은 자기가 잘 아는 것을 낸다. 같은 조건에서 계속 생성하면 확률이 높은 영역으로 답이 몰리고, 그 영역 밖은 아무리 많이 만들어도 채워지지 않는다. 그렇게 만든 데이터로 학습하면 모델은 그 좁은 영역을 더 잘하게 되고 나머지를 더 못하게 된다. 이 순환이 여러 세대 반복되며 분포가 좁아지는 현상을 모델 붕괴라 부른다.
막는 방법은 생성 단계와 거르는 단계에 나뉘어 있다.
- 씨앗을 늘린다. 20개와 200개의 차이가 온도나 프롬프트 기법의 차이보다 훨씬 크다
- 조건을 바꿔 가며 생성한다. 화자·분야·난이도·길이 같은 축을 프롬프트에 심고 조합을 돌린다. 무작위성에 맡기는 것보다 축을 정해 훑는 편이 넓게 퍼진다
- 비슷한 것을 버린다. 임베딩으로 유사도를 재서 기준을 넘으면 뺀다. 여기서 절반 이상이 사라지는 것은 정상이다
- 분포를 눈으로 본다. 임베딩을 2차원으로 줄여 그려 보면 위 그림 중 어느 쪽인지 한 번에 보인다
그리고 사람이 쓴 데이터를 완전히 대체하지 않는다. 씨앗과 검증 기준은 사람 쪽에 남겨 두고, 부피만 모델로 채우는 구성이 안전하다. 세대를 거듭할수록 합성 비율이 올라가는 파이프라인은 위 순환에 그대로 들어간다.
강한 모델로 증류할 때
가장 손쉬운 방법이지만 여기에는 기술과 무관한 제약이 붙는다.
- 이용약관. 상용 API의 출력으로 경쟁 모델을 학습하는 것을 금지하는 조항이 흔하다. 쓰기 전에 확인할 일이고, 나중에 학습 데이터의 출처를 되짚을 수 없으면 대응할 방법도 없다
- 출처 기록. 행마다 어느 모델의 어느 버전이 언제 만들었는지 남긴다. 데이터가 섞이고 나면 복원할 수 없다
- 상한. 증류로 얻는 성능은 원본 모델을 넘지 못한다. 넘어서려면 검증이 필요하고, 그것이 앞의 거부 샘플링이다
세 번째가 방법 선택의 기준이 된다. 이미 강한 모델이 잘하는 것을 우리 모델에 옮기려면 증류, 아무도 안 하는 것을 만들려면 거부 샘플링이다.
정리
합성 데이터는 「모델에게 데이터를 만들게 한다」가 아니라 「많이 만들고 대부분 버린다」로 이해하는 편이 정확하다. 그래서 이 작업의 품질은 생성 프롬프트가 아니라 필터에 달려 있다.
- 씨앗은 사람이 쓰고, 수는 모델이 채우고, 판정은 규칙이 한다
- 하나도 못 맞힌 것과 전부 맞힌 것을 버린다. 남는 구간이 학습이 일어나는 곳이다
- 중복은 글자와 뜻 양쪽으로 거른다
- 평가셋 오염을 반드시 검사한다. 이것을 빠뜨리면 이후의 모든 숫자가 무의미하다
- 임베딩 분포를 그려 다양성이 무너지지 않았는지 눈으로 확인한다
여기까지가 학습에 넣을 것을 준비하는 이야기였다. 다음 글에서는 학습을 마친 뒤 — 여러 개로 나뉜 어댑터를 어떻게 하나로 합칠 것인가를 다룬다.
읽어주셔서 감사합니다. 😊

