확률적으로 출력이 흔들리는 생성 파이프라인을 디버깅하고 계신다면, 실패를 한 번 보고 원인을 지목하는 습관부터 점검해 보시기를 권합니다. 저희는 그렇게 원인을 두 개 찾았고, 발행 직전에 재현해보니 하나는 재현되지 않았습니다.

무슨 일이 있었나

캐릭터 마스코트 한 장을 기준 렌더로 삼고, 그 이미지를 정체성 레퍼런스로 넘겨 네 방향의 뷰를 생성했습니다. 측면과 후면은 원하는 대로 나왔습니다.

후면 180도 180도 후면. 뒤통수와 망토만 남고 얼굴은 사라졌습니다.

문제는 3/4 두 장이었습니다. 45도는 거의 제자리였고, 135도는 카메라가 뒤로 가지 않고 얼굴이 계속 정면을 봤습니다. 원인을 두 가지로 정리했습니다. 하나는 프롬프트 배선 버그였고, 다른 하나는 뷰를 각도로 서술한 문구였습니다.

두 번째가 틀렸습니다.

진짜였던 원인

먼저 맞았던 쪽입니다. 저희 이미지 편집 래퍼는 입력 이미지마다 역할을 붙일 수 있습니다. “이 이미지는 캐릭터 정체성이니 실루엣과 팔레트를 그대로 재현하고, 포즈와 카메라 각도는 무시하라”는 지시가 역할에 담깁니다. 그런데 그 역할 문단을 이미지가 두 장 이상일 때만 프롬프트에 붙이도록 짜여 있었습니다.

턴어라운드는 기준 렌더 한 장만 넘깁니다. 그래서 역할이 조용히 사라졌고, 모델은 카메라를 움직여도 된다는 말을 애초에 듣지 못했습니다.

이건 확인하는 데 이미지 생성이 필요 없었습니다. 최종 프롬프트만 출력하는 옵션을 붙여 호출해 보면 끝입니다.

$ edit_image.py --input base.png --role identity --prompt "3/4 view" --emit-prompt
3/4 view

역할을 분명히 넘겼는데 출력에 역할이 없습니다. 고친 뒤 같은 호출을 하면 역할 문단이 프롬프트 맨 앞에 붙습니다. 실패를 재현할 필요도, 통계도 필요 없습니다. 입력과 출력을 직접 비교할 수 있는 종류의 원인이기 때문입니다.

가짜였던 원인

두 번째는 이렇게 정리했습니다. 성공한 두 뷰는 “full profile side view”, “straight-on back view”처럼 카메라의 이름을 부르고, 실패한 두 뷰는 “body turned 135 degrees from camera”처럼 회전을 각도로 서술한다. 그러니 각도 표현이 모호해서 모델이 안전한 쪽으로 해소한 것이다.

그럴듯했습니다. 표도 네 줄로 깔끔하게 나왔습니다. 뷰 정의를 “무엇이 보이는가”로 바꿔 다시 돌렸더니 두 뷰 다 통과했고, 그걸로 확인됐다고 생각했습니다.

발행용으로 “고치기 전” 이미지를 만들려고 옛 문구를 한 번 더 돌렸습니다. 정상적인 후면 뷰가 나왔습니다.

그래서 같은 기준 이미지에 옛 문구 3회, 새 문구 3회를 돌렸습니다.

옛 문구와 새 문구 각 3회 윗줄이 옛 문구, 아랫줄이 새 문구입니다. 여섯 장 모두 카메라가 뒤로 갔습니다.

조건 시행 후면 성공 기준 대비 변화량 평균
옛 문구 (각도 서술) 3 3 0.109
새 문구 (보이는 것 서술) 3 3 0.113

두 조건이 갈리지 않습니다. 그리고 원래 실패했던 그 뷰의 변화량은 0.073으로, 여섯 장이 만든 0.104에서 0.117 사이 구간 바깥에 혼자 떨어져 있습니다. 문구가 만든 차이가 아니라 드물게 나오는 뽑기였다는 뜻입니다.

정리하면 저는 표를 만들 만큼 확신했는데 각 칸이 한 번의 시행이었습니다. 네 칸짜리 표는 데이터처럼 생겼지만 데이터가 아니었습니다.

두 원인을 가른 기준

두 원인은 그럴듯함에서 차이가 없었습니다. 갈린 지점은 하나입니다. 확인에 재현이 필요한가.

역할 누락은 프롬프트 문자열을 찍어보면 그 자리에서 끝납니다. 모델을 부르지 않고, 확률이 개입하지 않고, 한 번 보면 참거짓이 정해집니다. 각도 문구는 그럴 수 없습니다. 같은 입력에 같은 출력이 나오지 않는 지점을 건드리므로, 주장하려면 시행을 쌓는 것 말고는 방법이 없습니다.

그래서 규칙은 짧습니다. 원인 후보를 세웠으면 확인 비용부터 따집니다. 결정론적으로 확인되는 후보는 먼저 확인하고 끝냅니다. 확률이 끼는 후보는 시행 수를 정하고 그만큼 돌리기 전까지 결론 칸에 적지 않습니다. 저는 두 번째를 건너뛰고 표에 먼저 적었습니다.

게이트는 이 실패를 잡지 못합니다

여기서 더 곤란한 사실이 나옵니다. 저 실패들을 저희 검증 게이트가 전혀 걸러내지 못했습니다.

파이프라인에는 정체성 게이트가 있습니다. 팔레트와 실루엣을 비교해 “생성된 프레임이 여전히 같은 캐릭터인가”를 점수로 냅니다. 네 뷰를 넣었더니 전부 99.8점에서 100점, 실루엣 일치도 1.0, 지적된 프레임 0건이었습니다. 정면과 프로필의 실루엣이 같을 리 없죠. 지표가 포화됐다는 뜻이고, 통과 여부로 쓸 정보가 남아 있지 않습니다.

포화보다 방향이 문제입니다. 이 게이트는 유사도가 높을수록 좋은 점수를 줍니다. 그런데 턴어라운드의 실패 모드가 바로 유사도입니다. 카메라가 안 움직인 뷰는 원본과 거의 같고, 따라서 만점을 받습니다. 게이트가 잡아야 할 실패가 게이트의 보상 함수와 같은 방향입니다. 역할이 사라져 정면 그대로가 나온 그 뷰도 이 게이트라면 통과했습니다.

그래서 축을 하나 더 놓았습니다. 다운스케일한 흑백 프레임을 기준 렌더와 비교해 평균 절대차를 냅니다. 위 표의 변화량이 그 값입니다. 회전량과 대체로 같이 움직이니 이걸로 각도를 판정하고 싶어지는데, 그러면 안 됩니다. 실패했던 뷰도 0.073이었지 0이 아니었습니다. 이 지표가 답하는 질문은 “아예 안 움직였는가” 하나뿐입니다. 상관관계를 게이트로 승격하는 순간, 그 게이트를 믿는 다음 사람이 잘못된 초록불을 봅니다.

프롬프트가 말하지 않은 것들

같은 파일럿에서 두 가지가 더 나왔는데, 둘 다 결정론적인 쪽이었습니다. 정보가 프롬프트에 아예 없었으니까요.

기준 렌더에서 팔레트가 통째로 뒤집혀 나온 적이 있습니다. 캐릭터 스펙에는 주색과 보조색과 강조색이 색상 코드로 적혀 있었는데, 세 개를 나열하는 것으로는 어느 색이 면적을 지배하는지가 전달되지 않습니다. 모델은 보조색으로 몸을 칠하고 주색을 테두리에 썼는데, 스펙만 놓고 보면 어긋난 데가 없죠. “주색이 지배색이고 몸의 가장 넓은 면적을 차지한다, 이 순서를 뒤집지 마라”를 문장으로 넣자 정정됐습니다.

같은 렌더에서 지휘봉이 반대 손에 가 있기도 했습니다. 스펙의 “오른손에 든다”는 캐릭터 기준일 수도 보는 사람 기준일 수도 있습니다. 정면 뷰에서 캐릭터의 오른손은 보는 사람의 왼쪽에 온다는 문장을 앞에 두자 해결됐습니다.

두 결함 모두 캐릭터 한 종에서 나왔지만 열두 종 전부에 걸려 있었습니다. 시트 백여 장을 뽑은 뒤에 알았다면 그 백여 장이 전부 재작업이었을 겁니다.

턴어라운드 다섯 장 전부 고친 뒤의 결과입니다. 왼쪽부터 정면, 3/4 정면, 측면, 3/4 후면, 후면.

남는 이야기

파이프라인을 열두 종으로 늘리기 전에 한 종으로 먼저 돌려본 이유가 이겁니다. 뷰당 소요는 113초에서 124초, 네 뷰 턴어라운드가 약 8분, 열두 종이면 약 100분입니다.

그리고 잘못 지목한 원인 하나를 발행 전에 잡은 것이 이 파일럿에서 가장 값싼 수확이었습니다. 그대로 나갔으면 저희는 문구 규칙을 하나 만들어 열두 캐릭터에 적용했을 것이고, 그 규칙이 아무것도 하지 않는다는 사실은 아무도 모른 채 남았을 겁니다. 확률적 출력 앞에서 한 번의 관측은 가설의 시작이지 결론이 아닙니다.

이 글의 수치와 이미지는 실제로 돌린 결과이며, 등장하는 캐릭터는 저희가 권리를 가진 오리지널 디자인입니다.

태그: character-pipeline, debugging, evaluation-gates, 이미지 생성, 프롬프트 엔지니어링

카테고리:

업데이트: