Chapter 12: 실전사례 — 현장의 목소리, JoyCaption의 DPO 실전기

원문: fpgaminer, “How OpenAI Misled You on RLHF” https://aerial-toothpaste-34a.notion.site/How-OpenAI-Misled-You-on-RLHF-1f83f742d9dd80a68129d06503464aff

이론을 벗어나 잠시 현장으로 가보자. 레딧에서 활동하는 비전 모델 제작자 fpgaminer가 공개한 JoyCaption Beta One의 DPO 훈련 기록은 이 교재의 여러 챕터에서 다룬 개념들이 실전에서 어떻게 부딪히는지를 생생하게 보여준다.

배경

JoyCaption은 오픈소스 이미지 캡셔닝 VLM(Visual Language Model)이다. Llama 3.1 기반으로, 이미지를 입력받아 디퓨전 모델 학습용 캡션을 생성한다. 검열에서 자유로운 이미지 캡셔닝 기능을 오픈소스로 제공하기 위해 만들어졌고 NSFW 이미지를 학습하고자하는 로컬 이미지 생성 모델 제작자들이 많이 사용한다.

SFT만으로 학습된 이전 버전(Alpha Two)은 약 90%의 신뢰도로 작동했지만, 10번 중 1번꼴로 반복 루프에 빠지거나 엉뚱한 출력을 내놓았다. 개발자는 이 "헛발질 확률"을 줄이기 위해 오프라인 DPO를 선택했다.

데이터셋은 어떻게 만들었는가

JoyCaption의 DPO 데이터셋 구축 과정은 Ch2에서 배운 "좋은 쌍의 조건"과 Ch11에서 배운 "오프라인 데이터 파이프라인"을 실전에서 보여준다.

[Step 1] 이미지 수집과 태스크 균형

JoyCaption은 여러 캡셔닝 모드를 지원한다:
  - 서술형 캡션 (formal/informal, 짧은/긴)
  - Stable Diffusion 프롬프트 (태그 + 구문 혼합)
  - VQA (이미지에 대한 질의응답)
  - 태그 증강 (지정된 태그를 캡션에 통합)

데이터셋은 모든 모드에 걸쳐 균형 있게 구성했다.
  - 특정 모드만 잘 되고 나머지가 퇴화하는 것을 방지
[Step 2] 후보 응답 생성

각 (이미지, 프롬프트) 쌍에 대해 현재 모델로 10개 응답을 샘플링. 높은 temperature로 다양성을 확보.
[Step 3] SOTA VLM으로 심사

방대한 작업을 사람이 할 수는 없으니 GPT-4o급 VLM을 평가자로 사용한다. 각 응답에 1–10점을 매기도록 지시:
  - "10점 = 프롬프트를 완벽히 따르고, 사용자에게 유용하고, 100% 정확"\
  → 1라운드: 10k 선호 쌍 구축\
  → 2라운드: 20k 선호 쌍 구축 (모델이 업데이트된 후 새로 생성)

실제 데이터는 어떻게 생겼는가? 이미지 캡셔닝 DPO 쌍의 전형적인 예를 보자:

[예제 1] 서술형 캡션 — 나쁜 쌍 (학습 시그널 약함)

  이미지: 해질녘 바닷가에서 서핑하는 사람
  프롬프트: "Write a long descriptive caption in a formal tone."

  y_w (8점): "A solitary surfer navigates a modest wave against the backdrop
   of a vivid sunset, with warm hues of orange and crimson reflecting off
   the ocean surface. The individual maintains a crouched stance..."

  y_l (7점): "A lone surfer rides a wave during sunset. The sky displays
   warm orange and red tones that reflect on the water. The surfer is in
   a crouching position on their board..."

  마진: 1점. 둘 다 정확하고 형식도 맞다.
  차이: y_w가 약간 더 상세하고 어휘가 풍부할 뿐.
  → judge도 미묘하게 판정 → 모델은 여기서 거의 배우지 못한다.
  → Ch2의 "쌍 B"에 해당하지만, 마진이 너무 좁은 경우.
[예제 2] 서술형 캡션 — 좋은 쌍 (학습 시그널 강함)

  같은 이미지, 같은 프롬프트.

  y_w (9점): "A solitary surfer navigates a modest wave against the backdrop
   of a vivid sunset, with warm hues of orange and crimson reflecting off
   the ocean surface. The individual maintains a crouched stance on a
   white shortboard, arms extended for balance..."

  y_l (3점): "The image shows a person surfing. The image shows a person
   surfing. The sky is orange. The water is blue. The person is on a
   surfboard. The image shows a person surfing on a wave."

  마진: 6점. y_l은 반복 루프에 빠졌다 ("The image shows a person surfing"이 3번).
  → 이것이 JoyCaption의 핵심 헛발질 출력: 반복 루프.
  → 모델은 여기서 "반복하지 마라"는 강한 시그널을 받는다.
  → Ch2의 "쌍 B"에 해당하면서 마진이 충분한 경우.
[예제 3] SD 프롬프트 모드 — 가장 어려운 경우

  같은 이미지.
  프롬프트: "Write a Stable Diffusion prompt for this image."

  y_w (7점): "1girl, surfer, ocean, sunset, orange sky, waves, dynamic pose,
   crouching, wet hair, white surfboard, golden hour, spray, action shot"

  y_l (2점): "A beautiful sunset scene with a surfer riding waves in the
   golden hour light, the ocean reflecting warm colors as they skillfully
   navigate the surf, creating a picturesque moment of athletic grace
   and natural beauty"

  마진: 5점. y_l은 문장형으로 썼다 — 서술형 캡션으로는 좋지만
  SD 프롬프트 형식(태그 + 짧은 구문)을 따르지 않았다.
  → "형식을 따라야 한다"는 시그널.
  → 그런데 이 모드가 가장 개선이 느렸다:
    judge도 SD 프롬프트의 "좋음"을 판정하기 어려웠고,
    베이스 모델의 서포트도 빈약했다 (Ch10: 스펙트럼 부족).
[예제 4] 삐꾸 vs 정상 — DPO보다 SFT가 적합한 경우

  y_w (9점): "A solitary surfer navigates a modest wave..."

  y_l (0점): "surfing surfing surfing surfing surfing surfing surfing
   surfing surfing surfing [500 토큰 반복]"

  마진: 9점. 최대한 넓다.
  → 하지만 Ch2에서 배웠다: 이것은 "쌍 A"(정답 vs 삐꾸)에 해당.
  → 모델이 이미 이런 극단적 삐꾸에 거의 0 확률을 부여하고 있다면,
    이 쌍에서는 그래디언트가 거의 0이다.
  → SFT로 정상 캡션만 보여줘도 동일한 효과.
  → DPO의 가치는 예제 2와 3처럼 "그럴듯하지만 문제 있는" 쌍에서 나온다.

첫 번째 시도의 실패 — 약한 예제의 함정

10k 쌍의 데이터셋을 만들고 DPO를 돌렸지만, 정확도가 60%에서 정체했다. 원인을 조사한 결과:

문제점: 대부분의 응답 쌍이 비슷비슷했다.
  - 모델이 이미 잘하는 영역 → 좋은 답 두 개 → 판별 어려움
  - 모델이 못하는 영역 → 나쁜 답 두 개 → 역시 판별 어려움
  → 심사 모델(judge)의 일치율이 낮았다
  → "마진이 좁은 쌍"으로는 모델이 학습할 방향을 못 잡는다

이것은 Ch2(마진을 벌려라)의 핵심 통찰에 해당한다. DPO가 학습하려면 선호/비선호 사이의 마진이 충분히 커야 한다.

해결: 마진 기반 필터링

개발자는 심사 방식을 바꿨다. VLM의 프롬프팅을 바꿔서 단순히 "어느 쪽이 나은가"를 묻는 대신, 각 응답에 1–10점을 매기게 하고, 점수 차이가 큰 쌍만 남겼다.

기존: 2개 응답 → "어느 쪽?" → 대부분 미묘한 차이 (예제 1처럼)
개선: 10개 응답 → 순위 매기기 → 최상위 vs 최하위 쌍 추출 → 마진 검증

  → 10개 중 최상위(8–9점)와 최하위(2–4점) 사이의 마진이
    자연스럽게 5점 이상이 된다 (예제 2, 3처럼).
  → 예제 1 같은 (8점 vs 7점) 쌍은 자동으로 걸러진다.

결과:
  - DPO 전 모델: SOTA VLM judge 기준 평균 5.14점
  - Round 1 DPO (10k 쌍): 삐꾸율 9.6% → 3.0%, 선호율 2:1 우세
  - Round 2 DPO (20k 쌍, 갱신된 모델로 새로 생성):
    삐꾸율 3.0% → 1.5%, 선호율 다시 2:1 우세
  - DPO 후 모델: 평균 7.03점 (5.14 → 7.03, +1.89점)
  - SD 프롬프트 모드: 삐꾸율 37% → 5%

  Round 2가 Round 1만큼 효과적이었던 이유:
    Round 1 이후 모델이 달라졌으므로, Round 1의 데이터는 더 이상 유효하지 않다.
    → 갱신된 모델로 새 데이터를 생성해야 했다 (Ch11의 Iterative DPO).
    → "RL 데이터셋은 일회용이다"라는 교훈이 여기서 나온다.

교재 개념들이 현장에서 부딪히는 지점

Ch4 (길이 편향):
  캡션 태스크에서도 긴 응답이 짧은 응답보다 로그확률이 낮게 나왔다.
  → 심사 모델이 길이에 편향되지 않도록 별도 지시가 필요했다.

Ch5 (레퍼런스 모델):
  LoRA를 쓰면 레퍼런스 모델이 공짜다 — 어댑터를 끄면 원본이 되니까.
  → 실전에서 LoRA + DPO 조합의 메모리 효율이 결정적이었다.

Ch9 (스펙트럼에서 시그널로):
  "모델이 정말 못하는 태스크는 RL로도 크게 개선되지 않았다."
  → 스펙트럼에 없는 주파수는 RL이 새로 만들 수 없다는 이론과 일치.
  → SD 프롬프트 모드의 개선이 가장 느렸던 이유: 심사 모델도 못 쓰고,
    베이스 모델의 서포트도 빈약했다.

Ch10 (RL의 저랭크 업데이트):
  LoRA rank 128로 DPO를 돌려 full fine-tuning에 준하는 효과를 얻었다.
  → "RLHF로 인한 엄청난 변화가 극히 적은 FLOP으로 발생한다는 것이
    뭔가 큰 의미가 있을 것이다" — 개발자 본인의 관찰.

제작자가 실제 사례에서 얻은 교훈

“RL 데이터셋은 일회용이다. SFT 데이터셋은 재활용할 수 있지만, DPO 데이터셋은 그 특정 모델에만 유효하다.
실수해서 다시 만들어야 하면 비용이 고스란히 반복된다.
머신러닝에 해자(moat)가 있다면, 그것은 RL이다.”

이 관찰은 Ch5(레퍼런스 모델)의 역할과 연결된다. DPO 데이터가 특정 πref\pi_\text{ref}에 종속되는 구조적 이유가 바로 수식에 πref\pi_\text{ref}가 들어가 있기 때문이다. 레퍼런스가 바뀌면 마진의 의미가 달라지고, 따라서 데이터 전체가 무효화된다.

JoyCaption이 겪은 고생을 요약하면:

  1. 데이터셋을 공들여 만들었지만 약한 예제뿐 → Ch11의 [3]
  2. 마진 필터링으로 강한 예제를 골랐더니 겨우 작동 → Ch2의 마진
  3. 다음 라운드를 돌리려면 데이터를 처음부터 다시 만들어야 함 → Ch11의 반복 비용
  4. "정답"이 있었다면? 검증기가 자동으로 평가하고, 매 배치마다 새 데이터를 생성하고, 마진 필터링도 자동으로 할 수 있었을 텐데…

→ Ch11 끝에서 던진 의문을 다시 생각해보자:
“정답이 있는 도메인이라면, 이 모든 고통이 사라지지 않나?”\

다음 장이 그 답이다.