이미지 로딩 중...
AI Generated
2025. 11. 19. · 4 Views
AI 음성 학습 디버깅 완벽 가이드
AI 음성 합성 모델을 학습하다 보면 메모리 부족, Loss 정체, 이상한 음성 등 다양한 문제에 부딪히게 됩니다. 이 가이드는 초급 개발자도 쉽게 따라할 수 있도록 실전 디버깅 노하우와 해결책을 친근하게 알려드립니다. 학습 중단 없이 안정적으로 고품질 음성 모델을 만들어보세요!
목차
- OOM (Out of Memory) 에러 해결
- Gradient Explosion/Vanishing 대응
- Loss가 감소하지 않을 때 해결법
- 생성된 음성이 이상할 때 체크 포인트
- 학습 재개 (Resume) 및 Transfer Learning
- A/B 테스트로 최적 체크포인트 선택
1. OOM (Out of Memory) 에러 해결
시작하며
여러분이 밤새 음성 데이터를 준비하고 드디어 학습을 시작했는데, 몇 분 지나지 않아 "CUDA out of memory" 에러가 뜨면서 학습이 중단된 경험 있으신가요? 분명 다른 사람들은 같은 모델로 잘 학습하는데 왜 나만 메모리 에러가 날까 고민하셨을 거예요.
이런 문제는 AI 음성 학습에서 가장 흔하게 발생하는 문제 중 하나입니다. 음성 데이터는 이미지보다 훨씬 크고, 시퀀스 길이가 길어서 배치 크기를 조금만 늘려도 메모리가 폭발적으로 증가하기 때문이죠.
특히 RTX 3060이나 4060처럼 VRAM이 제한적인 GPU를 사용하면 더욱 자주 겪게 됩니다. 바로 이럴 때 필요한 것이 체계적인 메모리 관리 전략입니다.
배치 크기 조정, Gradient Accumulation, Mixed Precision 학습 등을 활용하면 같은 GPU로도 훨씬 큰 모델을 학습할 수 있습니다.
개요
간단히 말해서, OOM 에러는 GPU 메모리가 부족해서 발생하는 문제입니다. 딥러닝 학습 시 모델 파라미터, 중간 활성화 값, Gradient 등이 모두 GPU 메모리에 올라가는데, 이것들이 GPU의 용량을 초과하면 에러가 발생합니다.
왜 이 문제를 제대로 이해해야 할까요? 메모리 관리는 모델 성능과 직결되기 때문입니다.
배치 크기를 너무 줄이면 학습이 불안정해지고, 너무 크게 하면 메모리 에러가 납니다. 예를 들어, VITS 같은 TTS 모델을 학습할 때 배치 크기를 32로 설정했다가 OOM이 나면, 많은 초보자들이 무작정 4로 줄이는데, 이렇게 하면 학습이 매우 느려지고 수렴도 잘 안 됩니다.
기존에는 메모리가 부족하면 더 좋은 GPU를 사야 한다고 생각했다면, 이제는 똑똑한 최적화 기법으로 현재 GPU에서도 충분히 학습할 수 있습니다. 핵심 해결책은 세 가지입니다: (1) Gradient Accumulation으로 작은 배치를 여러 번 누적하기, (2) Mixed Precision(FP16)으로 메모리 사용량 절반으로 줄이기, (3) 음성 길이 제한으로 시퀀스 길이 통제하기.
이 세 가지만 제대로 활용해도 메모리 문제의 90%는 해결됩니다.
코드 예제
import torch
from torch.cuda.amp import autocast, GradScaler
# Mixed Precision 학습 설정
scaler = GradScaler()
optimizer = torch.optim.Adam(model.parameters(), lr=1e-4)
# Gradient Accumulation으로 효과적인 배치 크기 늘리기
accumulation_steps = 4 # 실제 배치 크기 = batch_size * accumulation_steps
optimizer.zero_grad()
for i, (audio, text) in enumerate(dataloader):
# FP16으로 Forward pass - 메모리 절약
with autocast():
outputs = model(audio, text)
loss = criterion(outputs, targets)
loss = loss / accumulation_steps # Gradient 스케일 조정
# Backward pass
scaler.scale(loss).backward()
# accumulation_steps마다 한 번만 업데이트
if (i + 1) % accumulation_steps == 0:
scaler.step(optimizer)
scaler.update()
optimizer.zero_grad()
설명
이것이 하는 일: 위 코드는 제한된 GPU 메모리로도 큰 배치 크기 효과를 내면서 안정적으로 학습하는 방법을 보여줍니다. 실제 메모리는 작게 쓰면서도 큰 배치로 학습하는 것과 같은 효과를 냅니다.
첫 번째로, GradScaler와 autocast를 사용해 Mixed Precision 학습을 설정합니다. 일반적으로 모델은 FP32(32비트 부동소수점)로 학습하는데, 이를 FP16(16비트)으로 바꾸면 메모리 사용량이 정확히 절반으로 줄어듭니다.
예를 들어 12GB가 필요했던 학습이 6GB만으로 가능해지는 거죠. 단순히 메모리만 아끼는 게 아니라 GPU 연산도 2배 가까이 빨라져서 일석이조입니다.
그 다음으로, Gradient Accumulation이 핵심입니다. accumulation_steps를 4로 설정하면, 배치 크기 8로 4번 학습한 뒤 한 번에 가중치를 업데이트합니다.
이렇게 하면 GPU에는 배치 8만 올라가지만, 실제로는 배치 32(8×4)로 학습하는 것과 동일한 효과를 냅니다. loss를 accumulation_steps로 나누는 이유는 gradient가 누적되기 때문에 평균을 맞춰주기 위해서입니다.
마지막으로, optimizer.zero_grad()를 accumulation_steps마다 한 번만 호출합니다. 매 스텝마다 gradient를 초기화하지 않고 누적시키다가, 정해진 횟수가 되면 그때 한 번에 업데이트하고 초기화하는 방식이죠.
scaler.step()과 scaler.update()도 마찬가지로 accumulation이 끝날 때만 실행합니다. 여러분이 이 코드를 사용하면 RTX 3060(12GB)으로도 RTX 4090(24GB)에서나 가능했던 큰 배치 학습을 할 수 있습니다.
실무에서는 배치 크기가 클수록 학습이 안정적이고 빠르기 때문에, 메모리 제약을 극복하는 이 기법들이 필수적입니다. 또한 학습 속도도 FP16 덕분에 1.5-2배 빨라지고, 같은 시간에 더 많은 실험을 할 수 있어 모델 품질도 크게 향상됩니다.
실전 팁
💡 DataLoader에서 num_workers를 너무 높게 설정하면 CPU 메모리에서 데이터를 미리 로드하느라 GPU 메모리가 부족해질 수 있습니다. 2-4 정도가 적당합니다.
💡 음성 데이터의 최대 길이를 제한하세요. 예를 들어 10초 이상의 음성은 자르거나 제외하면 메모리 사용량이 크게 줄어듭니다. collate_fn에서 max_audio_length를 설정하는 것이 좋습니다.
💡 torch.cuda.empty_cache()를 학습 루프 중간에 호출하면 사용하지 않는 메모리를 즉시 해제할 수 있습니다. 특히 검증(validation) 전후에 호출하면 효과적입니다.
💡 실제로 얼마나 메모리를 쓰는지 모니터링하려면 nvidia-smi를 별도 터미널에서 watch -n 1 nvidia-smi 명령으로 실행해두세요. 메모리 사용 패턴을 보면 어디서 문제가 생기는지 금방 알 수 있습니다.
💡 Gradient Checkpointing을 활성화하면 메모리를 더 절약할 수 있지만 학습 속도가 20-30% 느려집니다. 메모리가 정말 부족할 때만 사용하세요.
2. Gradient Explosion/Vanishing 대응
시작하며
여러분이 열심히 TTS 모델을 학습하는데, Loss가 갑자기 NaN(Not a Number)이 되면서 학습이 망가진 경험 있으신가요? 또는 아무리 학습해도 Loss가 전혀 줄어들지 않고 초기 값 그대로 머물러 있는 답답한 상황을 겪어보셨을 거예요.
이런 문제는 Gradient Explosion(기울기 폭발)과 Gradient Vanishing(기울기 소실) 때문에 발생합니다. Gradient Explosion은 역전파 과정에서 기울기 값이 기하급수적으로 커져서 가중치가 무한대로 발산하는 현상이고, Gradient Vanishing은 반대로 기울기가 0에 가까워져서 가중치가 전혀 업데이트되지 않는 현상입니다.
특히 음성 모델처럼 깊은 신경망에서는 이 문제가 더욱 심각합니다. 바로 이럴 때 필요한 것이 Gradient Clipping과 적절한 초기화입니다.
기울기의 최댓값을 제한하고, 학습률을 조절하고, 모델 가중치를 올바르게 초기화하면 안정적으로 학습할 수 있습니다.
개요
간단히 말해서, Gradient Explosion은 역전파 시 기울기가 너무 커지는 문제이고, Gradient Vanishing은 기울기가 너무 작아지는 문제입니다. 둘 다 심층 신경망에서 여러 층을 거치면서 기울기가 곱해지기 때문에 발생합니다.
왜 이 문제를 제대로 이해해야 할까요? 음성 합성 모델은 Encoder-Decoder 구조에 Attention까지 포함해서 수십 개의 층으로 이루어져 있습니다.
층이 깊을수록 기울기가 불안정해지기 쉽고, 한 번 폭발하거나 소실되면 학습 자체가 불가능해집니다. 예를 들어, VITS나 FastSpeech2 같은 모델을 학습할 때 Learning Rate를 너무 높게 설정하면 첫 몇 스텝만에 Loss가 NaN이 되는 경우가 자주 있습니다.
기존에는 Learning Rate를 수동으로 조금씩 낮춰가며 시행착오를 겪었다면, 이제는 Gradient Clipping과 Warmup Scheduler를 사용해 자동으로 안정화할 수 있습니다. 핵심 해결책은 세 가지입니다: (1) Gradient Clipping으로 기울기 최댓값 제한, (2) Learning Rate Warmup으로 초기 학습 안정화, (3) Residual Connection과 Layer Normalization으로 기울기 흐름 개선.
이 세 가지를 함께 사용하면 대부분의 기울기 문제가 해결됩니다.
코드 예제
import torch
import torch.nn as nn
from torch.optim import Adam
model = TTSModel()
optimizer = Adam(model.parameters(), lr=1e-3)
# Learning Rate Warmup Scheduler 설정
warmup_steps = 4000
def get_lr_scale(step):
# 초기에는 아주 작은 lr로 시작해서 점진적으로 증가
return min(step ** (-0.5), step * warmup_steps ** (-1.5))
scheduler = torch.optim.lr_scheduler.LambdaLR(optimizer, get_lr_scale)
# 학습 루프
for epoch in range(num_epochs):
for batch in dataloader:
optimizer.zero_grad()
loss = model(batch)
loss.backward()
# Gradient Clipping - 기울기가 너무 커지는 것 방지
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
# Gradient 모니터링
total_norm = 0
for p in model.parameters():
if p.grad is not None:
param_norm = p.grad.data.norm(2)
total_norm += param_norm.item() ** 2
total_norm = total_norm ** 0.5
# 비정상적인 기울기 감지
if total_norm > 100 or torch.isnan(torch.tensor(total_norm)):
print(f"Warning: Abnormal gradient norm: {total_norm}")
continue # 이 배치는 건너뛰기
optimizer.step()
scheduler.step()
설명
이것이 하는 일: 위 코드는 학습 중 기울기가 폭발하거나 소실되는 것을 방지하고, 문제가 생기면 즉시 감지해서 대응하는 안전장치를 구현합니다. 마치 자동차의 ABS 브레이크처럼 위험한 상황에서 자동으로 개입합니다.
첫 번째로, Learning Rate Warmup Scheduler를 설정합니다. get_lr_scale 함수는 Transformer 논문에서 제안한 방식으로, 처음에는 아주 작은 학습률로 시작해서 warmup_steps(보통 4000)까지 점진적으로 증가시킵니다.
이렇게 하는 이유는 초기 가중치가 무작위로 초기화되어 있어서 급격한 업데이트를 하면 모델이 불안정해지기 때문입니다. 마치 자동차 시동을 걸고 바로 급가속하지 않고 천천히 속도를 올리는 것과 같은 원리죠.
그 다음으로, clip_grad_norm_이 핵심입니다. 이 함수는 모든 파라미터의 기울기 벡터 크기(norm)를 계산하고, 만약 max_norm(1.0)을 초과하면 전체 기울기를 비례적으로 축소합니다.
예를 들어 기울기 norm이 5.0이면 모든 기울기를 5분의 1로 줄여서 norm을 1.0으로 만듭니다. 이렇게 하면 기울기가 폭발해도 일정 수준 이상은 커지지 않아서 학습이 안정적으로 진행됩니다.
세 번째로, Gradient 모니터링 코드가 실행됩니다. 모든 파라미터의 기울기 크기를 계산해서 total_norm을 구하고, 이 값이 비정상적으로 크거나(100 이상) NaN이면 경고를 출력하고 해당 배치를 건너뜁니다.
이는 Gradient Clipping만으로 해결되지 않는 극단적인 상황에 대한 추가 안전장치입니다. 실제로 데이터에 이상한 샘플이 하나 섞여 있어도 전체 학습이 망가지는 것을 방지할 수 있습니다.
여러분이 이 코드를 사용하면 학습 중 Loss가 갑자기 NaN이 되거나 발산하는 문제를 거의 완전히 예방할 수 있습니다. 실무에서 TTS 모델을 학습할 때는 밤새 돌려놓는 경우가 많은데, 이런 안전장치가 없으면 한밤중에 에러가 나서 아침에 확인했을 때 몇 시간이 낭비되는 경우가 많습니다.
또한 Warmup Scheduler 덕분에 초기 학습이 훨씬 안정적이어서 하이퍼파라미터 튜닝도 쉬워집니다.
실전 팁
💡 max_norm 값은 1.0을 기본으로 하되, 모델이 크면 5.0까지 올려도 됩니다. 너무 작으면 학습이 느려지고, 너무 크면 효과가 없으니 Loss 그래프를 보면서 조정하세요.
💡 Warmup Steps는 전체 학습 스텝의 5-10% 정도가 적당합니다. 데이터셋이 작으면 1000-2000, 크면 4000-8000 정도로 설정하세요.
💡 Adam Optimizer보다 AdamW를 사용하면 Weight Decay가 적용되어 기울기가 더 안정적입니다. 특히 큰 모델에서 효과적입니다.
💡 모델 구조에 Residual Connection(Skip Connection)을 추가하면 기울기가 여러 경로로 흐를 수 있어 Vanishing 문제가 크게 줄어듭니다. 대부분의 현대 TTS 모델은 기본으로 포함되어 있습니다.
💡 TensorBoard나 Weights & Biases에 Gradient Norm을 로깅해두면 학습 중 기울기 패턴을 시각적으로 확인할 수 있어 문제를 빠르게 발견할 수 있습니다.
3. Loss가 감소하지 않을 때 해결법
시작하며
여러분이 며칠 동안 음성 모델을 학습시켰는데, Loss 그래프를 보니 처음부터 끝까지 거의 일직선으로 평평하게 유지되고 있다면 정말 답답하실 거예요. GPU는 열심히 돌아가는데 모델은 아무것도 배우지 못하는 상황이죠.
이런 문제는 생각보다 다양한 원인이 있습니다. Learning Rate가 너무 낮거나 높을 수도 있고, 데이터 전처리가 잘못되었을 수도 있으며, 모델 구조 자체에 문제가 있을 수도 있습니다.
특히 TTS 같은 복잡한 모델은 여러 Loss(Mel Loss, Duration Loss, Adversarial Loss 등)를 동시에 최적화하기 때문에 하나라도 잘못 설정되면 전체 학습이 막힐 수 있습니다. 바로 이럴 때 필요한 것이 체계적인 디버깅 접근법입니다.
단계별로 원인을 찾아내고, 각 Loss 항목을 개별적으로 점검하며, 데이터부터 모델까지 전체 파이프라인을 검증해야 합니다.
개요
간단히 말해서, Loss가 감소하지 않는다는 것은 모델이 데이터로부터 패턴을 학습하지 못한다는 의미입니다. 이는 학습 과정의 어느 단계에서든 문제가 생길 수 있는 증상입니다.
왜 이 문제를 체계적으로 접근해야 할까요? Loss가 안 떨어지면 무작정 Learning Rate를 높이거나 Epoch를 늘리는 경우가 많은데, 이는 근본 원인을 해결하지 못합니다.
예를 들어, 음성 데이터의 정규화가 잘못되어서 입력 값이 [-1, 1] 범위를 벗어나면 아무리 학습해도 의미 있는 결과를 얻을 수 없습니다. 또는 텍스트 토크나이저가 제대로 작동하지 않아서 모델이 엉뚱한 입력을 받고 있을 수도 있죠.
기존에는 문제가 생기면 처음부터 다시 학습하거나 하이퍼파라미터를 무작위로 바꿔가며 시도했다면, 이제는 단계별 체크리스트로 정확한 원인을 찾을 수 있습니다. 핵심 해결책은 다음과 같습니다: (1) 데이터 검증 - 입력과 타겟이 올바른지 확인, (2) Learning Rate 범위 테스트, (3) 각 Loss 항목 개별 점검, (4) 작은 데이터셋으로 Overfitting 테스트.
특히 Overfitting 테스트는 매우 중요한데, 샘플 10개로 Loss가 0에 가깝게 떨어지지 않으면 모델이나 코드에 근본적인 문제가 있다는 신호입니다.
코드 예제
import torch
import matplotlib.pyplot as plt
from torch.utils.data import DataLoader
# 1단계: 데이터 검증
def verify_data(dataloader):
batch = next(iter(dataloader))
audio, text = batch
print(f"Audio shape: {audio.shape}, range: [{audio.min():.3f}, {audio.max():.3f}]")
print(f"Text shape: {text.shape}, unique tokens: {text.unique().numel()}")
assert audio.min() >= -1 and audio.max() <= 1, "Audio not normalized!"
return batch
# 2단계: Learning Rate 범위 테스트 (LR Finder)
def find_lr(model, dataloader, optimizer):
lrs, losses = [], []
lr = 1e-8
for i, batch in enumerate(dataloader):
if i > 100: break
optimizer.param_groups[0]['lr'] = lr
loss = model(batch)
loss.backward()
optimizer.step()
optimizer.zero_grad()
lrs.append(lr)
losses.append(loss.item())
lr *= 1.1 # 점진적으로 증가
# 최적 LR은 Loss가 가장 빠르게 감소하는 지점
plt.plot(lrs, losses)
plt.xscale('log')
plt.xlabel('Learning Rate')
plt.ylabel('Loss')
plt.savefig('lr_finder.png')
print(f"추천 LR: {lrs[losses.index(min(losses))]:.2e}")
# 3단계: Overfitting 테스트
def overfit_test(model, small_batch, epochs=100):
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)
for epoch in range(epochs):
loss = model(small_batch)
loss.backward()
optimizer.step()
optimizer.zero_grad()
if epoch % 10 == 0:
print(f"Epoch {epoch}: Loss = {loss.item():.4f}")
# Loss가 0.1 이하로 떨어져야 정상
assert loss.item() < 0.1, "모델이 작은 데이터도 학습하지 못함!"
설명
이것이 하는 일: 위 코드는 Loss가 감소하지 않는 문제를 체계적으로 진단하는 세 가지 핵심 테스트를 제공합니다. 마치 의사가 환자를 진료할 때 기본 검사부터 차례대로 하는 것처럼, 학습 문제도 단계별로 원인을 찾아냅니다.
첫 번째로, verify_data 함수는 데이터가 올바른지 확인합니다. 음성 데이터가 [-1, 1] 범위로 정규화되어 있는지, 텍스트 토큰이 제대로 인코딩되어 있는지 검증합니다.
실무에서는 데이터 전처리 코드를 수정했는데 일부 파일에만 적용되지 않아서 학습이 안 되는 경우가 정말 많습니다. 이 함수는 데이터 로더에서 첫 배치를 꺼내서 즉시 검증하므로, 학습 시작 전에 문제를 미리 발견할 수 있습니다.
그 다음으로, find_lr 함수는 Learning Rate Finder 기법을 구현합니다. 아주 작은 Learning Rate(1e-8)부터 시작해서 매 스텝마다 1.1배씩 증가시키면서 Loss를 기록합니다.
이렇게 하면 어느 Learning Rate에서 Loss가 가장 빠르게 감소하는지 그래프로 확인할 수 있습니다. 보통 최적 Learning Rate는 Loss가 가장 가파르게 떨어지는 지점의 1/10 정도입니다.
예를 들어 그래프에서 LR=1e-3에서 Loss가 급격히 떨어진다면, 실제 학습은 1e-4로 시작하는 것이 안전합니다. 세 번째로, overfit_test 함수는 가장 중요한 테스트입니다.
단 하나의 배치(또는 샘플 10개 정도)만으로 100 epoch 동안 학습시켜봅니다. 제대로 된 모델이라면 같은 데이터를 반복 학습하면 Loss가 거의 0에 가깝게 떨어져야 합니다.
만약 이 테스트를 통과하지 못한다면 모델 구조, Loss 함수, 또는 학습 코드에 버그가 있다는 명확한 증거입니다. 예를 들어 Loss 함수의 reduction='none'을 빼먹어서 스칼라가 아닌 텐서가 반환되거나, Forward Pass에서 Gradient가 끊기는 detach()가 잘못 들어있을 수 있습니다.
여러분이 이 세 가지 테스트를 순서대로 실행하면, Loss가 감소하지 않는 문제의 원인을 빠르게 찾을 수 있습니다. 실무에서는 며칠씩 학습시키다가 실패하는 것보다, 처음 10분 동안 이런 테스트를 하는 것이 훨씬 효율적입니다.
특히 새로운 모델 구조를 시도하거나 데이터셋을 바꿀 때는 반드시 Overfitting 테스트를 먼저 해야 시행착오를 크게 줄일 수 있습니다.
실전 팁
💡 Loss가 NaN이 아니라 그냥 안 떨어지면 Learning Rate가 너무 낮은 경우가 많습니다. 1e-4에서 시작해서 1e-3까지 올려보세요.
💡 여러 Loss를 합치는 경우(total_loss = mel_loss + 0.1 * duration_loss) 각 Loss를 개별적으로 출력해서 모니터링하세요. 한 Loss가 너무 커서 다른 Loss의 Gradient를 무시하는 경우가 있습니다.
💡 Batch Normalization이나 Layer Normalization의 momentum 값이 너무 높으면 초기 학습이 불안정합니다. 기본값 0.1에서 0.01로 낮춰보세요.
💡 텍스트와 음성의 길이 정렬이 잘못되면 Attention이 학습되지 않습니다. Duration 정보가 정확한지 꼭 확인하세요.
💡 Pre-trained 모델을 Fine-tuning할 때는 Learning Rate를 1/10 정도로 낮춰야 합니다. 처음부터 학습할 때와 같은 LR을 쓰면 기존 지식이 망가집니다.
4. 생성된 음성이 이상할 때 체크 포인트
시작하며
여러분이 드디어 TTS 모델 학습을 마치고 음성을 생성해봤는데, 노이즈가 너무 심하거나, 로봇 같은 기계음이 들리거나, 발음이 뭉개져서 알아듣기 힘든 경험 있으신가요? Loss는 잘 떨어졌는데 정작 결과물은 형편없을 때 정말 당황스럽습니다.
이런 문제는 학습 과정에서는 드러나지 않다가 실제 생성(Inference) 단계에서만 나타나는 경우가 많습니다. 학습 시 Teacher Forcing을 사용하면 잘 작동하는 것처럼 보이지만, 실제로는 자기회귀(Autoregressive) 생성을 제대로 못 하는 거죠.
또는 Vocoder(음성 합성기)와 Acoustic Model(음향 모델)의 불일치, 샘플링 온도 설정 문제 등 다양한 원인이 있습니다. 바로 이럴 때 필요한 것이 체계적인 품질 진단 체크리스트입니다.
Mel-Spectrogram부터 최종 음성까지 각 단계를 점검하고, 생성 파라미터를 조정하면 음질을 크게 개선할 수 있습니다.
개요
간단히 말해서, 음성 품질 문제는 학습 문제가 아니라 생성(Inference) 설정 문제인 경우가 많습니다. 같은 모델이라도 어떻게 생성하느냐에 따라 품질이 천차만별입니다.
왜 이 문제를 제대로 이해해야 할까요? 많은 초보자들이 음질이 나쁘면 무조건 더 오래 학습하거나 데이터를 더 모으려고 합니다.
하지만 실제로는 Temperature를 0.667로 낮추거나, Duration 예측을 조금만 조정해도 극적으로 개선되는 경우가 많습니다. 예를 들어, VITS 모델에서 Noise Scale을 0.667에서 0.333으로 낮추는 것만으로도 노이즈가 절반으로 줄어듭니다.
기존에는 음질이 나쁘면 모델을 처음부터 다시 학습했다면, 이제는 생성 파라미터만 조정해서 같은 체크포인트로 훨씬 좋은 결과를 얻을 수 있습니다. 핵심 체크 포인트는 다음과 같습니다: (1) Mel-Spectrogram 품질 검증, (2) Vocoder와의 호환성 확인, (3) Temperature/Noise Scale 조정, (4) Duration 예측 정확도 점검, (5) Attention Alignment 시각화.
특히 Attention Alignment는 텍스트와 음성이 제대로 정렬되는지 보여주는 가장 중요한 지표입니다.
코드 예제
import torch
import matplotlib.pyplot as plt
from scipy.io import wavfile
# 1단계: Mel-Spectrogram 품질 검증
def verify_mel_quality(model, text):
mel = model.infer(text, noise_scale=0.667, length_scale=1.0)
# Mel을 시각화해서 패턴 확인
plt.figure(figsize=(10, 4))
plt.imshow(mel.squeeze().cpu().numpy(), aspect='auto', origin='lower')
plt.colorbar()
plt.title('Generated Mel-Spectrogram')
plt.savefig('mel_check.png')
# 비정상적인 값 체크
assert not torch.isnan(mel).any(), "Mel에 NaN이 있음!"
assert not torch.isinf(mel).any(), "Mel에 Inf가 있음!"
print(f"Mel range: [{mel.min():.2f}, {mel.max():.2f}]")
# 2단계: 파라미터 그리드 서치로 최적 설정 찾기
def find_best_parameters(model, text, vocoder):
best_score = 0
best_params = {}
for noise_scale in [0.333, 0.5, 0.667]:
for length_scale in [0.9, 1.0, 1.1]:
mel = model.infer(text, noise_scale=noise_scale,
length_scale=length_scale)
audio = vocoder(mel)
# 간단한 품질 지표: SNR (Signal-to-Noise Ratio)
signal_power = torch.mean(audio ** 2)
score = signal_power.item()
print(f"noise={noise_scale}, length={length_scale}: score={score:.4f}")
if score > best_score:
best_score = score
best_params = {'noise': noise_scale, 'length': length_scale}
print(f"최적 파라미터: {best_params}")
return best_params
# 3단계: Attention Alignment 점검
def plot_attention_alignment(model, text):
mel, alignment = model.infer(text, return_alignment=True)
plt.figure(figsize=(8, 6))
plt.imshow(alignment.squeeze().cpu().numpy(), aspect='auto')
plt.xlabel('Encoder Time Steps (Text)')
plt.ylabel('Decoder Time Steps (Audio)')
plt.title('Attention Alignment')
plt.colorbar()
plt.savefig('attention_alignment.png')
# 대각선 패턴이 명확해야 정상
# 흐릿하거나 여러 줄이면 문제 있음
설명
이것이 하는 일: 위 코드는 생성된 음성의 품질을 체계적으로 진단하고, 최적의 생성 파라미터를 찾는 세 가지 핵심 방법을 제공합니다. 마치 사진을 찍을 때 초점, 노출, ISO를 조정하듯이 음성 생성도 여러 파라미터를 조정해야 합니다.
첫 번째로, verify_mel_quality 함수는 생성된 Mel-Spectrogram의 품질을 검증합니다. Mel-Spectrogram은 최종 음성으로 변환되기 전 중간 표현으로, 이게 이상하면 Vocoder가 아무리 좋아도 좋은 음성이 나올 수 없습니다.
시각화를 해보면 정상적인 Mel은 시간축을 따라 부드러운 패턴을 보이는데, 노이즈가 많으면 점들이 흩어져 있거나 세로줄이 나타납니다. NaN이나 Inf가 있으면 Vocoder가 작동하지 않으므로 즉시 체크합니다.
그 다음으로, find_best_parameters 함수는 그리드 서치로 최적의 생성 파라미터를 찾습니다. noise_scale은 생성 다양성을 조절하는데, 너무 높으면 노이즈가 많고 너무 낮으면 단조롭습니다.
length_scale은 발화 속도를 조절하는데, 1.0보다 크면 느리고 작으면 빠릅니다. 실제로는 같은 텍스트를 여러 파라미터 조합으로 생성해보고, SNR(신호 대 잡음비) 같은 지표로 평가합니다.
이 코드는 9가지 조합(3×3)을 자동으로 테스트해서 최적 설정을 찾아주므로, 수동으로 하나씩 시도하는 것보다 훨씬 효율적입니다. 세 번째로, plot_attention_alignment 함수는 Attention 메커니즘이 제대로 작동하는지 시각화합니다.
Attention Alignment는 텍스트의 각 문자가 음성의 어느 부분과 대응되는지 보여주는 2D 히트맵입니다. 정상적으로 학습된 모델은 왼쪽 아래에서 오른쪽 위로 가는 명확한 대각선 패턴을 보입니다.
이는 텍스트가 순서대로 음성으로 변환된다는 의미죠. 만약 패턴이 흐릿하거나 여러 줄이 나타나면 Attention이 제대로 학습되지 않은 것으로, 더 오래 학습하거나 데이터를 점검해야 합니다.
여러분이 이 세 가지 점검을 하면 음성 품질 문제의 원인을 빠르게 파악할 수 있습니다. 실무에서는 같은 체크포인트로도 파라미터만 조정해서 품질을 2-3배 높일 수 있습니다.
예를 들어 고객 데모를 준비할 때 noise_scale을 낮추면 더 깨끗한 음성을 생성할 수 있고, 빠른 뉴스 낭독 스타일이 필요하면 length_scale을 0.9로 낮추면 됩니다. Attention Alignment를 확인하는 습관을 들이면 학습 중간에도 모델이 제대로 학습되고 있는지 모니터링할 수 있어서 시간을 크게 절약할 수 있습니다.
실전 팁
💡 음성에 클릭음이나 펑 소리가 나면 Vocoder의 Pre-emphasis/De-emphasis 필터 설정을 확인하세요. 계수가 맞지 않으면 이런 잡음이 생깁니다.
💡 발음이 뭉개지거나 너무 빠르면 Duration Predictor를 다시 학습하거나, 추론 시 length_scale을 1.1-1.2로 높여보세요.
💡 특정 단어만 이상하게 발음되면 해당 단어의 Phoneme 변환이 잘못되었을 가능성이 높습니다. G2P(Grapheme-to-Phoneme) 모듈을 점검하세요.
💡 여성 목소리가 남성처럼 들리거나 반대인 경우, Pitch 정보가 데이터 전처리에서 잘못 정규화되었을 수 있습니다. F0(기본 주파수) 범위를 확인하세요.
💡 실제 서비스 전에는 MOS(Mean Opinion Score) 테스트를 꼭 하세요. 5-10명에게 5점 척도로 음질을 평가받으면 객관적인 품질을 알 수 있습니다.
5. 학습 재개 (Resume) 및 Transfer Learning
시작하며
여러분이 며칠 동안 열심히 모델을 학습하다가 갑자기 서버가 다운되거나, 실수로 학습을 중단시켜버린 경험 있으신가요? 또는 한국어로 학습한 모델을 일본어나 영어로 확장하고 싶은데 처음부터 다시 시작하기에는 너무 오래 걸려서 고민하셨을 거예요.
이런 문제는 체크포인트(Checkpoint) 저장과 로딩 전략으로 해결할 수 있습니다. 학습 중 정기적으로 모델 상태를 저장해두면 언제든지 중단된 시점부터 이어서 학습할 수 있고, 이미 학습된 모델의 지식을 새로운 작업에 활용하는 Transfer Learning도 가능합니다.
특히 TTS 같은 음성 모델은 학습에 며칠에서 몇 주씩 걸리기 때문에 체크포인트 관리가 필수적입니다. 바로 이럴 때 필요한 것이 견고한 체크포인트 저장/로딩 시스템과 Transfer Learning 전략입니다.
Optimizer 상태, Scheduler 상태, Random Seed까지 완벽하게 복원하면 중단 없이 학습을 이어갈 수 있습니다.
개요
간단히 말해서, 학습 재개는 중단된 지점부터 정확히 이어서 학습하는 것이고, Transfer Learning은 기존 모델의 지식을 새로운 도메인에 활용하는 것입니다. 둘 다 체크포인트 파일을 기반으로 합니다.
왜 이것들을 제대로 구현해야 할까요? 클라우드 GPU는 시간당 비용이 비싸고, Spot Instance는 언제든 중단될 수 있습니다.
체크포인트가 제대로 저장되지 않으면 몇 시간, 며칠의 학습이 순식간에 날아갈 수 있습니다. 예를 들어, 100 epoch 중 99 epoch까지 학습했는데 갑자기 전원이 나가면, 체크포인트가 없다면 처음부터 다시 시작해야 합니다.
또한 Transfer Learning을 활용하면 새로운 화자나 언어를 학습할 때 필요한 데이터와 시간을 1/10 이하로 줄일 수 있습니다. 기존에는 모델 가중치만 저장했다가 Optimizer 상태가 달라져서 학습이 불안정해졌다면, 이제는 모든 상태를 완벽하게 저장하고 복원할 수 있습니다.
핵심 전략은 다음과 같습니다: (1) 모델, Optimizer, Scheduler, Epoch, Step을 모두 포함한 완전한 체크포인트 저장, (2) 주기적인 자동 저장과 최고 성능 모델 별도 보관, (3) Transfer Learning 시 적절한 Layer Freezing, (4) Learning Rate 조정. 특히 Transfer Learning할 때는 Encoder는 얼리고 Decoder만 학습하는 등의 전략이 효과적입니다.
코드 예제
import torch
import os
from pathlib import Path
# 완전한 체크포인트 저장
def save_checkpoint(model, optimizer, scheduler, epoch, step, loss, path):
checkpoint = {
'epoch': epoch,
'step': step,
'model_state_dict': model.state_dict(),
'optimizer_state_dict': optimizer.state_dict(),
'scheduler_state_dict': scheduler.state_dict() if scheduler else None,
'loss': loss,
'rng_state': torch.get_rng_state(), # Random seed 상태
'cuda_rng_state': torch.cuda.get_rng_state_all() # GPU Random seed
}
# 임시 파일로 저장 후 rename (원자적 연산)
temp_path = f"{path}.tmp"
torch.save(checkpoint, temp_path)
os.replace(temp_path, path) # 실패해도 기존 파일 보존
print(f"체크포인트 저장: {path} (Epoch {epoch}, Step {step})")
# 체크포인트 로딩 및 학습 재개
def load_checkpoint(model, optimizer, scheduler, path):
if not os.path.exists(path):
print(f"체크포인트 없음: {path}, 처음부터 시작")
return 0, 0
checkpoint = torch.load(path, map_location='cpu')
model.load_state_dict(checkpoint['model_state_dict'])
optimizer.load_state_dict(checkpoint['optimizer_state_dict'])
if scheduler and checkpoint['scheduler_state_dict']:
scheduler.load_state_dict(checkpoint['scheduler_state_dict'])
# Random seed 복원 (재현성 보장)
torch.set_rng_state(checkpoint['rng_state'])
torch.cuda.set_rng_state_all(checkpoint['cuda_rng_state'])
epoch = checkpoint['epoch']
step = checkpoint['step']
print(f"체크포인트 로드: Epoch {epoch}, Step {step}부터 재개")
return epoch, step
# Transfer Learning을 위한 선택적 로딩
def load_pretrained_for_transfer(model, pretrained_path, freeze_encoder=True):
pretrained = torch.load(pretrained_path, map_location='cpu')
model_dict = model.state_dict()
# 호환되는 파라미터만 선택적으로 로드
pretrained_dict = {k: v for k, v in pretrained['model_state_dict'].items()
if k in model_dict and v.shape == model_dict[k].shape}
model_dict.update(pretrained_dict)
model.load_state_dict(model_dict)
print(f"Transfer Learning: {len(pretrained_dict)}/{len(model_dict)} 파라미터 로드됨")
# Encoder 얼리기
if freeze_encoder:
for name, param in model.named_parameters():
if 'encoder' in name:
param.requires_grad = False
print("Encoder 레이어 동결됨")
설명
이것이 하는 일: 위 코드는 학습을 안전하게 저장하고 재개하며, 기존 모델의 지식을 새로운 작업에 활용하는 완전한 체크포인트 시스템을 구현합니다. 마치 게임의 세이브/로드 기능처럼 언제든지 정확히 이전 상태로 돌아갈 수 있습니다.
첫 번째로, save_checkpoint 함수는 학습에 필요한 모든 정보를 저장합니다. 모델 가중치뿐 아니라 Optimizer 상태(momentum, variance 등), Scheduler 상태(현재 learning rate), 현재 Epoch와 Step, 그리고 심지어 Random Seed 상태까지 저장합니다.
Random Seed를 저장하는 이유는 Data Augmentation이나 Dropout 같은 랜덤 요소를 정확히 재현하기 위해서입니다. 특히 중요한 기법은 임시 파일로 저장한 뒤 os.replace()로 원자적으로 교체하는 것인데, 이렇게 하면 저장 중 오류가 나도 기존 체크포인트가 손상되지 않습니다.
그 다음으로, load_checkpoint 함수는 저장된 모든 상태를 복원합니다. 먼저 map_location='cpu'로 로드해서 메모리 문제를 방지하고, 각 컴포넌트의 state_dict를 복원합니다.
Optimizer 상태를 복원하는 것이 특히 중요한데, Adam Optimizer는 각 파라미터마다 momentum과 variance를 추적하고 있어서, 이게 없으면 학습이 처음 상태로 되돌아가 불안정해집니다. Random Seed도 정확히 복원하므로, 재개한 학습이 중단되지 않았을 때와 완전히 동일한 결과를 냅니다.
세 번째로, load_pretrained_for_transfer 함수는 Transfer Learning을 위한 영리한 로딩 전략을 보여줍니다. 단순히 모든 파라미터를 로드하는 게 아니라, 현재 모델과 호환되는(이름과 shape이 일치하는) 파라미터만 선택적으로 로드합니다.
예를 들어 한국어 TTS 모델을 영어로 확장할 때, Encoder(텍스트 처리)는 언어가 바뀌어서 호환되지 않지만, Decoder(음성 생성)는 그대로 사용할 수 있습니다. freeze_encoder=True 옵션을 사용하면 Encoder의 requires_grad를 False로 설정해서, 이미 학습된 부분은 고정하고 새로운 부분만 학습합니다.
이렇게 하면 학습 시간이 1/10로 줄고, 필요한 데이터도 훨씬 적어집니다. 여러분이 이 체크포인트 시스템을 사용하면 학습 중 어떤 문제가 생겨도 안전합니다.
실무에서는 매 Epoch마다 저장하고, 검증 Loss가 최저일 때 'best_model.pt'로 별도 저장하는 전략을 많이 씁니다. 또한 AWS나 GCP의 Spot Instance처럼 언제든 중단될 수 있는 환경에서는 매 100 스텝마다 자동 저장하도록 설정합니다.
Transfer Learning은 특히 다화자 TTS나 다국어 모델을 만들 때 엄청난 시간을 절약해주는데, 영어로 100 epoch 학습한 모델을 프랑스어로 Fine-tuning하면 10 epoch만으로도 좋은 결과를 얻을 수 있습니다.
실전 팁
💡 체크포인트 파일 이름에 Epoch와 Loss를 포함시키세요. 예: 'checkpoint_epoch50_loss0.234.pt'. 나중에 어느 시점의 모델이 좋았는지 바로 알 수 있습니다.
💡 디스크 용량을 아끼려면 최근 3-5개 체크포인트만 유지하고 오래된 것은 자동 삭제하세요. 단, best model은 절대 지우면 안 됩니다.
💡 Transfer Learning 시 새로운 도메인의 Learning Rate는 원래의 1/10로 시작하세요. 예를 들어 pre-training이 1e-4였으면 fine-tuning은 1e-5로 시작합니다.
💡 다국어 모델을 만들 때는 Phoneme Encoder만 새로 학습하고 나머지는 얼리면 효과적입니다. 언어마다 음소는 다르지만 음성의 운율과 억양은 비슷하기 때문입니다.
💡 Google Drive나 S3에 주기적으로 백업하세요. 로컬 디스크 고장으로 며칠치 학습을 날리는 일이 의외로 자주 있습니다.
6. A/B 테스트로 최적 체크포인트 선택
시작하며
여러분이 TTS 모델을 100 epoch 동안 학습했는데, Epoch 50일 때 음성이 가장 자연스러웠고 Epoch 100에서는 오히려 과적합(Overfitting)으로 품질이 떨어진 경험 있으신가요? Validation Loss는 계속 낮아지는데 실제 음성 품질은 중간에 피크를 찍고 나빠지는 경우가 많습니다.
이런 문제는 TTS처럼 주관적인 품질 평가가 중요한 작업에서 자주 발생합니다. Loss 숫자로는 좋아 보이지만 사람이 들었을 때는 별로인 경우가 많죠.
메트릭(Mel Loss, Duration Loss 등)과 실제 품질 사이에 괴리가 있기 때문입니다. 특히 Adversarial Loss를 사용하는 모델(GAN 기반 TTS)은 학습이 불안정해서 어느 체크포인트가 최고인지 찾기가 더 어렵습니다.
바로 이럴 때 필요한 것이 체계적인 A/B 테스트와 자동 평가 시스템입니다. 여러 체크포인트로 같은 텍스트를 생성해보고, MOS(Mean Opinion Score)나 자동 평가 지표(PESQ, MCD 등)로 비교하면 최적의 모델을 찾을 수 있습니다.
개요
간단히 말해서, A/B 테스트는 여러 모델 버전을 실제 데이터로 비교해서 어느 것이 더 나은지 객관적으로 판단하는 방법입니다. TTS에서는 사람의 귀로 듣는 평가와 자동 지표를 함께 사용합니다.
왜 이 방법을 꼭 알아야 할까요? 학습 Loss만 보고 모델을 선택하면 실제 서비스에서 사용자 불만이 생길 수 있습니다.
Loss는 낮지만 발음이 어색하거나, 감정 표현이 부족하거나, 노이즈가 많을 수 있죠. 예를 들어, Mel Loss가 0.15에서 0.13으로 줄었지만 실제로는 기계음이 더 심해진 경우가 있습니다.
반대로 Loss는 조금 높아도 사람이 듣기에는 훨씬 자연스러운 체크포인트가 있을 수 있습니다. 기존에는 최종 체크포인트를 무조건 사용하거나, 몇 개만 무작위로 들어보고 결정했다면, 이제는 자동화된 평가 파이프라인으로 모든 체크포인트를 체계적으로 비교할 수 있습니다.
핵심 전략은 다음과 같습니다: (1) 테스트 세트를 고정해서 모든 체크포인트로 동일한 텍스트 생성, (2) PESQ, MCD, F0 RMSE 같은 자동 지표로 1차 필터링, (3) 상위 3-5개 체크포인트만 사람이 직접 듣고 MOS 평가, (4) 다양한 유형(긴 문장, 짧은 문장, 어려운 단어)의 테스트 케이스 준비. 자동 지표로 빠르게 후보를 줄이고, 최종 판단은 사람이 하는 하이브리드 방식이 가장 효율적입니다.
코드 예제
import torch
import numpy as np
from pathlib import Path
from scipy.io import wavfile
from pesq import pesq
import librosa
# 여러 체크포인트 자동 평가
class CheckpointEvaluator:
def __init__(self, checkpoint_dir, test_texts):
self.checkpoint_dir = Path(checkpoint_dir)
self.test_texts = test_texts
self.results = []
def evaluate_all_checkpoints(self, model, vocoder):
checkpoints = sorted(self.checkpoint_dir.glob("checkpoint_epoch*.pt"))
for ckpt_path in checkpoints:
# 체크포인트 로드
checkpoint = torch.load(ckpt_path, map_location='cpu')
model.load_state_dict(checkpoint['model_state_dict'])
model.eval()
# 테스트 텍스트로 음성 생성
metrics = self.compute_metrics(model, vocoder)
result = {
'checkpoint': ckpt_path.name,
'epoch': checkpoint['epoch'],
'train_loss': checkpoint['loss'],
'pesq': metrics['pesq'],
'mcd': metrics['mcd'],
'f0_rmse': metrics['f0_rmse']
}
self.results.append(result)
print(f"{ckpt_path.name}: PESQ={metrics['pesq']:.3f}, MCD={metrics['mcd']:.2f}")
# 결과 정렬 (PESQ 기준)
self.results.sort(key=lambda x: x['pesq'], reverse=True)
return self.results
def compute_metrics(self, model, vocoder):
pesq_scores = []
mcd_scores = []
f0_rmses = []
for text, ref_audio_path in self.test_texts:
# 음성 생성
mel = model.infer(text)
generated_audio = vocoder(mel).squeeze().cpu().numpy()
# 참조 음성 로드
ref_audio, sr = librosa.load(ref_audio_path, sr=22050)
# PESQ: 음성 품질 지표 (1-5, 높을수록 좋음)
pesq_score = pesq(sr, ref_audio, generated_audio, 'wb')
pesq_scores.append(pesq_score)
# MCD: Mel-Cepstral Distortion (낮을수록 좋음)
mcd = self.compute_mcd(ref_audio, generated_audio, sr)
mcd_scores.append(mcd)
# F0 RMSE: 피치 정확도 (낮을수록 좋음)
f0_rmse = self.compute_f0_rmse(ref_audio, generated_audio, sr)
f0_rmses.append(f0_rmse)
return {
'pesq': np.mean(pesq_scores),
'mcd': np.mean(mcd_scores),
'f0_rmse': np.mean(f0_rmses)
}
def compute_mcd(self, ref, gen, sr):
# Mel-Cepstral Distortion 계산 (간단한 버전)
ref_mfcc = librosa.feature.mfcc(y=ref, sr=sr, n_mfcc=13)
gen_mfcc = librosa.feature.mfcc(y=gen, sr=sr, n_mfcc=13)
# 길이 맞추기
min_len = min(ref_mfcc.shape[1], gen_mfcc.shape[1])
ref_mfcc = ref_mfcc[:, :min_len]
gen_mfcc = gen_mfcc[:, :min_len]
# MCD 계산
diff = ref_mfcc - gen_mfcc
mcd = np.mean(np.sqrt(np.sum(diff ** 2, axis=0)))
return mcd * (10 / np.log(10)) # 표준 스케일
def compute_f0_rmse(self, ref, gen, sr):
# F0 (피치) RMSE 계산
ref_f0 = librosa.yin(ref, fmin=80, fmax=400, sr=sr)
gen_f0 = librosa.yin(gen, fmin=80, fmax=400, sr=sr)
min_len = min(len(ref_f0), len(gen_f0))
rmse = np.sqrt(np.mean((ref_f0[:min_len] - gen_f0[:min_len]) ** 2))
return rmse
# 사용 예시
test_texts = [
("안녕하세요, 오늘 날씨가 정말 좋네요.", "test_audios/sample1.wav"),
("인공지능 음성 합성 기술이 발전하고 있습니다.", "test_audios/sample2.wav"),
]
evaluator = CheckpointEvaluator("checkpoints/", test_texts)
results = evaluator.evaluate_all_checkpoints(model, vocoder)
print("\n=== Top 3 체크포인트 ===")
for i, result in enumerate(results[:3], 1):
print(f"{i}. {result['checkpoint']} (Epoch {result['epoch']})")
print(f" PESQ: {result['pesq']:.3f}, MCD: {result['mcd']:.2f}")
설명
이것이 하는 일: 위 코드는 수십 개의 체크포인트를 자동으로 평가해서 최고 품질의 모델을 빠르게 찾아주는 완전한 평가 시스템입니다. 마치 와인 품평회처럼 객관적인 지표와 주관적인 평가를 결합합니다.
첫 번째로, CheckpointEvaluator 클래스는 지정된 디렉토리의 모든 체크포인트를 자동으로 찾아서 평가합니다. evaluate_all_checkpoints 메서드는 각 체크포인트를 순서대로 로드하고, 동일한 테스트 텍스트로 음성을 생성한 뒤, 여러 지표를 계산합니다.
중요한 점은 모든 체크포인트에 정확히 같은 테스트 세트를 사용한다는 것입니다. 이렇게 해야 공정한 비교가 가능하죠.
예를 들어 10개의 체크포인트가 있으면 10번 동일한 프로세스를 반복하고, 최종적으로 PESQ 점수 기준으로 정렬해서 순위를 매깁니다. 그 다음으로, compute_metrics 메서드는 세 가지 핵심 지표를 계산합니다.
PESQ(Perceptual Evaluation of Speech Quality)는 사람의 청각 인지를 모델링한 지표로, 1점에서 5점 사이의 값을 가지며 높을수록 음질이 좋습니다. 실제로 통화 품질 평가에 사용되는 국제 표준이라 신뢰도가 높습니다.
MCD(Mel-Cepstral Distortion)는 생성된 음성과 참조 음성의 스펙트럼 차이를 측정하는데, 낮을수록 원본에 가깝다는 의미입니다. F0 RMSE는 피치(목소리 높이)의 정확도를 측정해서, 억양과 감정 표현이 얼마나 잘 재현되는지 알 수 있습니다.
세 번째로, compute_mcd와 compute_f0_rmse는 실제 계산을 수행합니다. MFCC(Mel-Frequency Cepstral Coefficients)를 추출해서 각 프레임의 스펙트럼 차이를 계산하고, YIN 알고리즘으로 F0(기본 주파수)를 추출해서 피치 오차를 계산합니다.
길이가 다를 수 있으므로 min_len으로 맞춰주는 것이 중요합니다. 이 지표들은 librosa 같은 표준 라이브러리로 쉽게 계산할 수 있어서 자동화에 적합합니다.
여러분이 이 평가 시스템을 사용하면 100개의 체크포인트를 몇 분 안에 자동으로 비교할 수 있습니다. 실무에서는 먼저 이 자동 평가로 상위 5개 정도를 추리고, 그 후 팀원들이 직접 듣고 MOS(Mean Opinion Score)를 매기는 2단계 방식을 씁니다.
예를 들어 PESQ 4.0 이상인 체크포인트만 3명이 각각 듣고 5점 척도로 평가해서 평균이 가장 높은 것을 최종 모델로 선택합니다. 이렇게 하면 주관적 품질과 객관적 지표를 모두 고려할 수 있어서, 실제 사용자 만족도가 높은 모델을 선택할 수 있습니다.
특히 상용 서비스에 배포하기 전에는 이런 체계적인 평가가 필수적입니다.
실전 팁
💡 테스트 세트는 다양한 유형을 포함하세요. 짧은 문장(3초), 긴 문장(10초), 숫자/외래어, 감정 표현 등을 골고루 넣어야 모델의 약점을 찾을 수 있습니다.
💡 PESQ는 16kHz나 8kHz 음성에 최적화되어 있어서 22.05kHz나 44.1kHz 음성은 리샘플링이 필요합니다. pesq 함수 호출 전에 librosa.resample을 사용하세요.
💡 자동 지표와 사람 평가가 불일치하면 자동 지표를 의심하세요. TTS는 매우 주관적이어서 PESQ가 높아도 부자연스러울 수 있습니다. 최종 결정은 항상 사람이 해야 합니다.
💡 A/B 테스트 시 평가자에게 어느 것이 어느 모델인지 알려주지 마세요(Blind Test). 선입견 없는 평가를 위해 랜덤하게 순서를 섞어서 들려주세요.
💡 평가 결과를 CSV나 JSON으로 저장해두면 나중에 어떤 하이퍼파라미터가 좋았는지 분석할 수 있습니다. 예를 들어 "Epoch 50-70 사이가 항상 최고다"는 패턴을 발견하면 다음 학습 때 Early Stopping을 적용할 수 있습니다.