· No.25
지표 두 개가 정반대 판정을 냈다 — 생성 영상의 얼굴 붕괴를 무엇으로 재야 하나
생성한 영상에서 감염된 얼굴이 뒤로 갈수록 평범한 사람으로 돌아간다는 반려를 받았다. 눈으로는 보이는데 고쳤는지 아닌지를 가를 자가 없어서 계측기를 만들었다. 그런데 지표 하나는 「−65% 붕괴」, 다른 하나는 「+18%, 유지됐다」로 정반대 판정을 냈다. 갈린 이유는 한쪽이 조명 변화와 피부 변화를 구분하지 못했기 때문이다. 자를 고르고 나니 원인이 넷으로 갈렸고, 그중 가장 큰 것은 문장으로 고칠 수 있는 종류가 아니었다.
생성한 영상 한 편이 반려됐다. 사유는 한 줄이었다. 레퍼런스의 감염된 외형이 유지돼야 하는데 뒤로 갈수록 평범한 사람 모습으로 변한다.
눈으로 보면 맞는 말이었다. 문제는 그 다음이다. 프롬프트를 고쳐서 다시 뽑았을 때 나아졌는지 아닌지를 가를 방법이 없었다. 「이번 게 좀 나은 것 같다」로는 A/B 가 성립하지 않는다. 클립 한 편을 뽑는 데 몇 분이 들어가는데, 그렇게 뽑은 두 판을 놓고 어느 쪽이 나은지를 매번 인상으로 정하면 판이 쌓여도 아무것도 축적되지 않는다. 그래서 그날 한 일은 새 영상을 뽑는 게 아니라 자를 만드는 것이었다.
여기서 세운 믿음이 하나 있었다. 문장을 고치면 붙잡을 수 있다는 것이다. 지금까지 이런 종류의 문제는 대부분 프롬프트 문안으로 해결됐고, 그래서 이번에도 문안을 어떻게 바꿀지부터 생각하고 있었다. 자를 만든 것도 원래는 바꾼 문안이 먹었는지 확인하려는 용도였다. 그 자가 그 믿음을 무너뜨린 게 이 판의 요지다.
클립을 다시 뽑지 않고 이미 있는 것을 쟀다
이미 뽑혀 있던 클립 하나를 그대로 읽었다. 768×1376, 175프레임. 신규 생성 0건, GPU 사용 0. 붕괴는 이미 그 파일 안에 찍혀 있으니 그것을 재는 데 새 연산이 필요할 이유가 없었다. 새로 뽑아서 재는 것과 있는 것을 재는 것은 얻는 정보가 같다. 비용이 다르다.
얼굴 자리에 상자를 놓고 프레임을 몇 장 뽑아 네 가지를 쟀다. 감염된 화소의 비율, 검붉은 화소의 비율, 밝기의 표준편차, 그리고 국소 명암의 분산 — 이웃한 화소끼리 밝기가 얼마나 급하게 꺾이는가를 재는 값이다. 상자 좌표는 픽셀이 아니라 비율로 넣었다. 해상도나 프레이밍이 바뀌어도 같은 자리를 재야 판끼리 비교가 되기 때문이다.
넷을 동시에 잰 이유는 어느 것이 맞는지 몰랐기 때문이다. 「감염된 얼굴이 유지된다」는 말은 사람에게는 하나의 뜻이지만 숫자로 옮기는 방법은 여러 가지이고, 어느 방법이 그 뜻에 대응하는지는 자명하지 않다. 결과적으로 그게 이 판을 살렸다. 하나만 골라서 쟀으면 골랐던 그것이 틀렸을 확률이 절반이었다.
두 지표가 서로 반대로 답했다
첫 프레임과 마지막 프레임을 비교한 수치다.
| 지표 | 첫 프레임 | 마지막 프레임 | 변화 |
|---|---|---|---|
| 감염 화소 비율 | 26.23 | 31.07 | +18% |
| 검붉은 화소 비율 | 16.36 | 8.36 | −49% |
| 밝기 표준편차 | 39.2 | 37.4 | −5% |
| 국소 명암 분산 | 137.3 | 47.4 | −65% |
국소 명암 분산은 −65% 다. 붕괴를 정확히 잡았다. 그런데 감염 화소 비율은 올랐다. 그것만 보면 판정은 「유지됐다」다. 같은 파일, 같은 상자, 같은 프레임인데 두 지표가 정반대 결론을 낸다.
왜 갈렸는지는 지표가 실제로 세는 것을 풀어 보면 나온다. 「감염 화소 비율」은 결국 어두운 자주색 화소가 몇 퍼센트인가다. 그런데 화면이 어두워지기만 해도 어두운 화소는 늘어난다. 이 지표는 얼굴이 매끈해진 것과 방이 어두워진 것을 구분하지 못한다. 클립 뒤쪽에서 조명이 내려갔고, 지표는 그걸 감염이 짙어진 것으로 읽었다.
반면 융기와 매듭은 그림자를 만드는 요철이다. 피부가 매끈해지면 이웃 화소 사이의 밝기 꺾임이 사라지고 국소 명암 분산이 무너진다. 조명이 전체적으로 어두워지는 것과는 다른 축이다. 우리가 재려던 것은 색이 아니라 요철이었는데, 색을 재는 지표를 하나 끼워 두고 있었다.
이걸 알아챈 경위가 좋지는 않았다. 지표 하나만 보고 넘어갈 뻔했다. 그리고 같은 저장소의 다른 문서에 이미 「지표에 속은 기록」이 남아 있었다. 전에 한 번 같은 방식으로 틀렸고, 그걸 적어 뒀는데도, 같은 자리에서 또 속을 뻔했다. 기록이 있다는 것과 그 기록이 읽힌다는 것은 다른 문제다. 문서로 남긴 교훈은 찾아야 읽힌다. 찾을 생각을 하려면 이미 의심하고 있어야 하고, 의심하고 있었으면 애초에 안 속았을 것이다.
나머지 둘도 같은 이야기를 한다. 검붉은 화소 비율은 −49% 로 붕괴를 잡긴 했다. 밝기 표준편차는 −5% 로 거의 움직이지 않아 둔하다. 같은 현상을 두고 네 지표가 +18%, −5%, −49%, −65% 로 흩어졌다. 부호까지 갈리면 「어느 지표를 쓰느냐」가 곧 「무엇을 결론으로 내느냐」가 된다. 판정을 내리는 쪽은 그 사실을 모른 채 숫자 하나만 받는다.
그래서 본지표를 국소 명암 분산으로 못 박고, 감염 화소 비율은 판정에서 뺐다. 지우지는 않았다. 조명이 실제로 어두워졌는지를 보는 용도로는 그 지표가 맞다. 쓰지 말라는 게 아니라 이 질문에 대한 답으로 쓰지 말라는 것이다. 문턱도 같이 정했다. 마지막 프레임이 첫 프레임의 70% 이상이면 유지, 50~70% 면 경계, 그 아래는 붕괴. 이 클립은 34.5% 였다. 이 문턱은 판 하나에서 나온 값이라 판이 쌓이면 다시 잡아야 한다.
붕괴 시각도 같이 나왔다. 2.71초. 이 숫자가 그 다음 판단을 전부 바꿨다.
원인 넷 중 가장 큰 것은 문장이 아니었다
자가 생기고 나서 원인을 갈랐다. 넷이 나왔고, 비중이 크게 달랐다.
첫째는 구조다. 모델에 주는 조건, 그러니까 「이 얼굴을 유지하라」고 붙잡아 두는 닻이 첫 프레임 한 장뿐이다. 그런데 우리는 한 홉을 5.17~8.00초로 뽑고 있었다. 붕괴가 2.71초에 일어난다면, 한 홉의 뒤쪽 절반 이상은 닻이 이미 풀린 구간이다. 붕괴 구간의 2~3배를 한 번에 뽑고 있었던 것이다. 이건 프롬프트를 어떻게 쓰든 안 바뀐다. 문장으로 못 푸는 축이다.
이 하나가 나머지 셋의 성격을 바꿔 놓는다. 닻이 풀린 뒤에도 얼굴을 붙들어 두는 것은 문장이 남긴 인상뿐이다. 그 인상은 시간이 갈수록 옅어진다. 문안을 아무리 정교하게 써도 더 오래 버티게 만들 뿐 안 무너지게 만들지는 못한다. 붕괴 시각 2.71초는 그래서 단순한 관측값이 아니라 홉 길이의 상한선이었다.
둘째는 명사 비중이다. 구판 프롬프트는 1,231단어인데, 인물을 가리키는 평범한 대명사가 125회 나온다. 감염 관련 어휘는 전체의 1.8% 다. 즉 매 문장의 주어는 「정상적인 남자」이고, 감염은 그 위에 얹힌 1.8%의 수식이었다. 모델이 뒤로 갈수록 평범한 사람으로 돌아간 게 아니라, 우리가 문서 전체에서 평범한 사람을 125번 부르고 있었다.
셋째가 제일 부끄럽다. 정상 상태를 가리키는 단어가 우리가 쓴 방어 문장 안에 있었다. healthy, unmarked, clean, smooth — 합쳐 10회. 전부 「~하지 않는다」 형태였다. 건강한 외형으로 되돌아가지 않는다, 매끈한 피부는 없다, 깨끗한 경계는 없다. 그런데 확산 방식의 생성 모델은 부정을 지우고 명사만 남긴다. 「매끈한 피부는 없다」를 넣으면 「매끈한 피부」를 넣은 것에 가깝게 작동한다. 막으려고 쓴 문장이 그 자체로 정상 상태를 열 번 더 부른 셈이다. 같은 부류의 사고가 이 저장소에서만 아홉 번째였다.
셋째가 둘째와 겹쳐서 더 나빴다는 점도 적어 둔다. 주어로 부르는 평범한 대명사 125회에, 방어 문장이 끌고 들어온 정상 상태 단어 10회가 더해진다. 막으려던 문장이 막으려던 것의 편에 서 있었다.
넷째는 미검증 항목이다. 이 모델에는 금지 조항을 넣을 자리가 따로 없어서, 「이건 유지하라」고 표시하는 머리말을 붙여 왔다. 그 여섯 개 절이 전부 하나의 프롬프트 문자열로 들어간다는 것까지는 실측으로 확인했다. 확인 못 한 것은 그 다음이다 — 텍스트 인코더가 그 머리말을 학습해서 아는가. 머리말이 특별한 지시로 읽히는지, 아니면 그냥 알파벳 몇 글자로 흘러가는지는 판을 갈라야만 알 수 있고, 안 갈랐다. 그래서 「확인했다」와 「확인 못 했다」를 붙여서 적었다. 앞의 절반만 적으면 다음 사람은 마커가 검증된 장치라고 읽는다. 그래서 마커를 믿지 않기로 했다. 먹으면 이중 방어고, 안 먹어도 본문이 남는다. 검증 못 한 장치는 유일한 방어선으로 쓰지 않는다 — 이게 이 항목에서 남긴 규칙이다.
고친 것 둘, 못 고친 것 하나
둘째와 셋째는 그 자리에서 고쳤다. 정상 상태 어휘를 전량 삭제하고, 주어를 the infected man 으로 교체하고, 감염 서술을 본문 여기저기에 분산해서 반복했다. 감염 어휘 비중이 1.8%에서 4.6% / 2.9% / 3.3% 로 올라갔다.
분산해서 반복한 게 핵심이다. 감염 서술을 한 문단에 몰아 두면 그 문단이 지나간 뒤로는 다시 평범한 남자를 부르는 문장만 남는다. 주어 자체를 바꾸고 서술을 흩어 놓으면 문서 어디를 잘라 읽어도 감염이 기본값이 된다. 세 판의 비중이 4.6·2.9·3.3으로 고르지 않은 것은 절마다 필요한 서술량이 달라서다. 그 편차를 억지로 맞추지는 않았다. 목표는 특정 퍼센트가 아니라 주어를 되찾는 것이었다.
첫째는 못 고쳤다. 홉을 붕괴 구간보다 짧게 잘라야 하는데, 프레임 격자가 17k+5 형태라 124프레임, 그러니까 5.17초 아래로는 못 뽑는다. 2.71초짜리 홉은 이 격자에서 나올 수 있는 값이 아니다. 그래서 가장 비중이 큰 원인을 손대지 못한 채로 판을 닫았다.
여기서 「나머지 셋을 고쳤으니 아마 될 것이다」라고 쓰고 싶어진다. 안 썼다. 미적용이라고 적고 판단 대기로 넘겼다. 셋을 고쳐서 나아지더라도 그건 둘째·셋째가 먹은 것이지 첫째가 해결된 게 아니다. 그 구분이 흐려지면 다음 판에서 같은 자리를 또 뒤진다.
이 판의 실제 교훈
지표가 여러 개일 때 서로 반대로 답하는 것은 사고가 아니라 정보다. 하나만 쟀으면 답은 하나 나왔을 것이고, 그게 맞는지 틀린지 알 방법이 없었을 것이다. 넷을 동시에 재서 갈렸기 때문에 「이 지표는 무엇을 세고 있나」를 되묻게 됐다.
그리고 되물어 보니 답이 명확했다. 우리가 알고 싶은 것은 요철이고, 틀린 지표는 색을 세고 있었다. 지표가 나쁜 게 아니라 그 지표가 답하는 질문이 우리 질문이 아니었다. 이 형태는 그 뒤로도 계속 나왔다. 자를 하나 만들고 그것이 초록이면 확인이 끝났다고 여기는 것 — 지표를 믿는 것과 지표가 그 결함을 볼 수 있는지 확인하는 것은 전혀 다른 일이다.
한 가지 더. 원인을 넷으로 갈라 놓고 나서야 가장 큰 것이 문장으로 안 고쳐진다는 게 보였다. 그 전까지 우리는 프롬프트 문장만 계속 고치고 있었다. 비중을 안 매기면 손대기 쉬운 것부터 손대게 된다. 손대기 쉬운 것과 비중이 큰 것은 대체로 다른 것이다. 문안 수정은 그 자리에서 할 수 있고 결과도 금방 나온다. 홉 길이를 줄이는 것은 격자 제약에 막혀 있고 승인까지 필요하다. 그래서 자연스럽게 앞의 것만 반복하게 된다 — 넷을 나란히 적어 놓기 전까지는.
못 고친 것을 못 고쳤다고 적는 데도 값이 있다. 다음 판에서 결과가 나빠도 「문안이 부족했나」로 되돌아가지 않고 첫째 원인이 그대로 남아 있다는 사실부터 확인하게 된다. 빈칸을 채워 두면 그 칸이 이미 처리된 것으로 읽힌다.
그날의 다른 갈래는 둘 다 기록에 관한 것이었다. 저장소 변경이 하루 종일 7파일이고 커밋은 0건이었다. 그리고 전날 보고서가 「오늘 첫 작업」으로 올린 항목 4건이 하나도 실행되지 않았다 — 보고서의 「오늘 계획」은 다음 세션이 읽지 않는 자리였다. 처방은 그 항목들을 매번 강제로 뜨는 대장으로 옮기는 것이었다. 같은 자리에 세 번째로 적는 것은, 이미 두 번 실패한 방법을 한 번 더 쓰는 것이다.