AI 출력을 '왠지 좋다'는 느낌으로 판단하지 않기 위해 기준(자)을 먼저 만들었는데, 그 기준 자체에서 2가지 버그가 발견되었다
요약
본 글은 교육용 앱의 음성 클로닝 기능을 개발하며 발생하는 주관적 평가 문제를 해결하는 방법을 제시합니다. 단순히 '비슷한가'라는 유사도 점수만으로는 부족하기 때문에, 음색과 발음을 분리하여 측정하고 객관적인 기준(상한선/하한선)을 설정하는 것이 중요하다고 강조합니다.
핵심 포인트
- 음성 클로닝 평가는 '비슷함'이 아닌 상반된 두 축으로 접근해야 한다.
- 억양 억제와 음색 보존은 트레이드오프 관계이며, 이 점을 간과해서는 안 된다.
- 객관적 측정을 위해 화자 일치 모델(Speaker Verification)과 기존 발음 채점 API를 활용한다.
- 단일 점수보다 '자신의 녹음끼리'의 유사도를 상한선으로 설정하는 것이 핵심이다.
내 목소리로 영어 발화를 시키는 기능을 개발하려고 한다. TOEIC 학습 앱의
자신의 목소리 그대로, 정확한 발음의 모범 사례를 듣는다라는 용도다.
방법은 두 가지가 있다.
| 음성 양 | GPU | 3개월 비용 (AWS 도쿄/실제 가격 기준 계산) |
|---|---|---|
| Zero-shot (추가 학습 없음) | 30초 | 수 초 |
| Fine-tuning | 10분~1시간 | 수 시간 × 시도 횟수 |
Zero-shot으로 충분하다면 1/3 비용으로 끝낼 수 있다. 그렇다면 이것이 충분한지 여부를 어떻게 판정할까.
보통은 이렇게 된다.
모델 A로 만들었다 → '오, 비슷한 것 같은데?'
설정을 바꿨더니 → '아까보다 나은가?'
참조 음성을 길게 했다 → '음, 글쎄…'
모두 '왠지 같다'는 느낌으로 끝난다. 일주일이 지나면 어떤 설정이 좋았는지도 기억나지 않는다.
그리고 그 위에 1만 엔의 판단이 올라간다.
그래서 모델을 만지기 전에, 기준(자)을 먼저 만들었다. 그 이야기와,
만드는 도중에 테스트가 포착한 두 가지 버그에 대한 이야기.
한 축으로는 속는다
목소리 클로닝의 평가는 '비슷한가'라고 생각했다. 이것이 틀렸다.
하고 싶은 일을 분해하면, 상반되는 두 가지를 동시에 요구하고 있다.
| 원하는 것 | 어디에서 가져올지 |
|---|---|
| 음색 (목소리 자체) | 참조 음성으로부터 가져온다 |
| 발음・운율 (네이티브 영어) | 참조 음성으로부터 가져와서는 안 된다 |
참조 음성이 '일본식 억양의 영어'라면, 억양이 그대로 복사된다.
이는 accent leakage라는 이름이 붙은 알려진 문제이며, 연구에서도
'억양 억제와 음색 보존은 트레이드오프 관계'라고 쓰여 있다.
학습 용도에서는, 억양이 실리는 순간 목적 자체가 사라진다.
자신의 억양을 모범 사례로 삼아도 의미가 없다.
여기서 무서운 점은, '비슷한가'만 보고 있으면 여기에 눈치채지 못한다는 것이다.
자신의 목소리와 똑같이 + 자신의 억양이 똑같이
→ 유사도 숫자는 좋다
→ 하지만 모범 사례로서는 완전히 망가져 있다
숫자는 좋아 보이지만, 목적이 파괴되어 있다. 한 축만으로는 간파할 수 없다.
두 축으로 만든다. 둘 다 추가할 것이 없었다
| 측정하는 것 | 무엇을 사용할지 |
|---|---|
| 발음의 정확성 | 이미 앱이 가지고 있는 발음 채점 API에, 녹음을 대신하여 클로닝 음성을 투입한다 |
| 목소리의 유사도 | 화자 일치(Speaker Verification) (목소리를 192차원 벡터로 만드는 모델)의 cosine 유사도. CPU에서 구동 가능하다 |
발음 채점 쪽은, 자신의 녹음을 채점하는 기존 경로를 그대로 사용할 수 있다.
추가할 코드는 제로였다.
점수만으로는 의미가 없다
0.72
이라고 나왔다고 해도, 좋은지 나쁜지는 그것만으로는 결정되지 않는다.
녹음 환경・마이크・화자 일치 모델을 바꿨을 뿐인데 의미가 달라진다.
그래서 먼저 상한선과 하한선을 측정한다.
| 측정하는 것 | 하한 (이 이하이면 실패) | 상한 (이것이 천장) |
|---|---|
| 목소리의 유사도 | 타인 간의 유사도 | 자신의 다른 녹음끼리의 유사도 |
| 발음의 정확성 | 자신이 영어를 읽은 녹음 점수 | 모범 사례 TTS 그대로의 점수 |
'자신의 다른 녹음끼리'가 상한선이 된다는 것이 핵심이다.
같은 사람이라도 녹음할 때마다 조금씩 흔들리기 때문에, 그 흔들림이 천장이 된다.
1.0은 될 수 없다. 1.0과 비교하면, 어떤 클로닝도 '비슷하지 않다'고 보여버린다.
상한선이 0.78이라면 0.72는 상당히 좋다. 상한선이 0.95라면 0.72는 전혀 안 된다.
기준 네 가지를 측정해야 비로소 점수에 의미가 생긴다.
그 위에 4가지 경우로 판정한다.
| 목소리의 유사도 | 발음 | 무슨 일이 일어나고 있는가 | 다음에 할 일 |
|---|---|---|---|
| 상한선에 가깝다 | 상한선에 가깝다 | 성공적이다 | 다음 단계로 진행한다 |
| 낮다 | 상한선에 가깝다 | 목소리가 비슷하지 않다 | 참조 음성을 길고 깨끗하게 / Fine-tuning을 한다 |
| 상한선에 가깝다 | 낮다 | 억양이 실리고 있다 | 참조 음성을 일본어로 바꾼다 |
| 낮다 | 낮다 | 모델이나 절차가 잘못되었다 | 처음부터 다시 시작한다 |
'다음으로 할 일'이 바뀌기 때문에, 두 축을 만드는 것이 가치가 있다.
하나의 종합 점수에 섞어버리면, 이 부분이 사라진다.
본론: 기준(자)를 쓰니, 기준 자체에 버그가 2개 있었다
통신도 모델도 건드리지 않는 부분 (유사도 계산・정규화・판단・CSV)을
의존 제로 모듈로 분리하여, 손으로 만든 정답으로 테스트를 작성했다.
GPU도 통신도 필요하지 않아, 이 시점에서는 1엔도 들지 않았다.
그리고 테스트를 두 가지 발견했다.
Number(null)
은 0
이 된다
버그 1: 정규화는 이렇게 작성되어 있었다.
function num(v) { var n = Number(v); return isFinite(n) ? n : null; }
function norm(value, lo, hi) {
var v = num(value), a = num(lo), b = num(hi);
...
언뜻 보기에 측정할 수 없는 값은 null이 되어 걸러지는 것처럼 보인다. 그렇지 않다.
Number(null) // 0
Number('') // 0
Number([]) // 0
...
모두 0이 되어 isFinite를 통과한다.
무슨 일이 일어나는가. 측정할 수 없었던 항목이 '하한치와 정확히 같다' = 가장 나쁜 값으로 판정된다.
- 임베딩을 가져올 수 없었다 → 「목소리가 전혀 비슷하지 않다」고 표시됨
- 채점 API가 다운되었다 → 「발음이 최악이다」라고 표시됨
둘 다 '측정할 수 없었던 것'이지 '나빴던 것'은 아니다.
그런데 표에는 당당하게 숫자가 나열되고, 모델 탓이라고 생각하며 다음 조치를 취하게 된다.
수정한 형태는 다음과 같다.
function num(v) {
if (typeof v === 'number') return isFinite(v) ? v : null;
if (typeof v === 'string' && v.trim() !== '') {
...
실은 이것은 같은 프로젝트에서 이전에 밟았던 것이다.
발음 채점 화면에서, 스코어가 null일 때 빈 문자열을 반환하는 함수가 있었는데,
채점 데이터가 하나도 없는데 「✅ 눈에 띄는 문제는 없었습니다」라고 표시된 적이 있었다.
null을 '좋음'으로 해석해서는 안 된다.
같은 형태의 버그를, 다른 장소에서, 반년 후에 다시 작성했다.
차이점은, 이번에는 테스트가 먼저 있었다는 것뿐이다.
버그 2: 경계선과 정확히 일치하는 값은 부동소수점에서는 결정되지 않는다
임계값(threshold)은 '상한 대비 비율'로 가지고 있다 (절대값으로 가지면 환경에 따라 의미가 달라지기 때문에).
기본값은 7할. 정확히 7할인 항목을 통과시킬 생각으로 >=를 사용했다.
var v = s >= simOk ? ... : ...;
테스트에서, 정확히 7할이 되는 값을 넣었다.
하한 0.20 / 상한 0.80 / 값 0.62
→ (0.62 - 0.20) / (0.80 - 0.20)
손 계산으로는 0.42 / 0.60 = 0.7이다. 실제로는:
(0.62 - 0.20) / (0.80 - 0.20) // 0.6999999999999998
>= 0.7이 false가 된다. 정확히 7할인 항목이 떨어진다.
여기서 떨어뜨려도 얻는 것은 아무것도 없기에, 통과시키는 쪽으로 바꿨다.
var EPS = 1e-9;
var v = s >= simOk - EPS ? ... : ...;
이런 종류의 오차는 움직여 봐야 알아차리지 못하는 버그이다.
'임계값 경계라서 떨어진 거겠지'라고 납득해버리는 종류의 버그다.
먼저 만들면, 순서가 역전된다
이 두 가지 버그가 치명적인 이유는, 자(物差し)를 완성한 후에 모델을 만졌을 때이다.
역순으로 진행했다면, 이렇게 보였을 것이다.
모델을 구동한다 → 표에 이상한 숫자가 나열된다 → 「이 모델은 안 되네」
모델 탓이라고 생각한다. 자가 고장 난 것까지는 생각하지 않는다.
그리고 다음 모델을 시도하기 위해, 또 GPU 비용을 지불한다.
자를 먼저 만들면, 자 자체의 버그가 자의 버그로 나온다.
게다가 합성된 입력(손으로 나열한 정답)으로 나오기 때문에, 0원으로, 몇 분 만에 나온다.
이것은 새로운 생각이 아니다. 신호 처리 부분은 처음부터
**합성 신호(정답을 아는 소리)**로 검증해왔다.
한 것은, 그 방식을 AI 출력의 평가에도 적용했을 뿐이다.
요약
AI의 출력을 '왠지 좋다'로 판단하면, 돈에 대한 판단이 그 위에 덧씌워진다-
1차원적인 관점으로는 속는다. 「비슷하다」만 보면, 어색한 부분이 감지되지 않는다 -
점수 자체보다는 「상한과 하한의 어느 곳인가」를 본다. 상한은 완벽한 값이 아니라,
자신이 녹음한 버전(別録音)처럼 현실적인 천장을 측정하다 -
측정할 수 없었던 것을 '가장 나쁜 것'으로 간주하여 통과시키지 않는 Number(null)은 0이다. 경계 지점은 부동 소수점으로 결정되지 않는다.
잣대(자)를 먼저 만들면, 그 잣대의 버그가 먼저 나온다. 이것이 가장 큰 수확이었다.
아직 모델은 한 번도 작동시키지 않았다. 작동시키기 전에, 19개의 테스트가 통과하고 있다.
논의 (Discussion)

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn ML의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기