限时福利领取


背景与痛点:为什么自己训 ChatTTS 这么难?

做语音合成最怕两件事:数据烂、GPU 烧。
ChatTTS 虽然开源了全套训练代码,但真跑起来才发现:

  • 音频采样率 44.1 k 与 16 k 混用,梅尔频谱对不上,损失曲线直接“跳楼”
  • 中文文本里“¥%@”这类符号没清洗,导致音素序列里突然蹦出 <unk>,注意力对齐全乱
  • 单卡 24 G 显存,batch=4 就 OOM,开混合精度又担心梯度爆炸
  • 想加方言,数据量不到 2 h,过拟合到 5 k step 就开始“电音”

一句话:Demo 五分钟,训练五昼夜。下面把我踩过的坑和加速方案打包奉上,尽量让你“一次跑通”。

技术方案对比:Tacotron2 vs FastSpeech2 vs ChatTTS 原生架构

ChatTTS 默认用的是“非自回归+Transformer+可学习时长预测器”的混合架构,官方叫 NS22。跟经典方案比:

维度 Tacotron2 FastSpeech2 ChatTTS-NS2
对齐方式 单调注意力 时长预测器 可学习对齐+CTC 损失
训练速度 慢(串行) 快(并行) 并行,且支持混合精度
数据饥饿 >10 h 才稳 2 h 可用 30 min 也能“出声”,但质量随缘
硬件要求 最低 8 G 12 G 16 G(开 fp16 可压到 10 G)

结论:

  1. 想快速出 demo,直接用 NS2;
  2. 追求极致音质,可把 NS2 的 Decoder 换成 FastSpeech2 的 Variance Predictor,再蒸馏一次;
  3. 硬件 < 12 G 就别折腾了,先租 A100 再谈理想。

核心实现:从原始录音到可训练样本

1. 数据预处理流程

先给目录树,方便后面脚本直接 copy:

dataset/
├─ raw_audio/          # 原始 wav
├─ transcripts.txt     # 格式:文件名|文本
├─ processed/
│  ├─ wav_16k/         # 重采样后
│  ├─ mels/            # 梅尔频谱 .npy
│  └─ phonemes/        # 音素序列 .txt

步骤:

  1. 统一重采样 16 kHz,mono,16-bit
  2. 用 WeTextProcessing 做中文文本规范化:数字“123”→“一百二十三”,符号“¥”→“元”
  3. 强制对齐获取音素时长(Montreal Forced Aligner 3.0),输出 csv
  4. 根据对齐结果裁剪静音段,保证首尾无静音
  5. 提取 80 维梅尔频谱,帧长 1024,帧移 256,预加重 0.97

关键代码(Python 3.9,Librosa 0.10):

import librosa, numpy as np, soundfile as sf
from tqdm import tqdm

def resample_and_trim(src_path, dst_path, top_db=30):
    y, _ = librosa.load(src_path, sr=16000)
    y, _ = librosa.effects.trim(y, top_db=top_db)
    sf.write(dst_path, y, 16000)

for src in tqdm(Path("raw_audio").rglob("*.wav")):
    resample_and_trim(src, f"processed/wav_16k/{src.name}")

2. 模型配置参数详解

ChatTTS 把模型拆成 4 个 json:

  • model.json 控制网络宽度、头数
  • data.json 指定采样率、梅尔维度
  • train.json 学习率、warmup、梯度裁剪
  • speaker.json 多说话人嵌入维度

经验值(单说话人,中文,NS2):

  • encoder-dim: 512
  • decoder-dim: 512
  • attention-head: 4 (< 4 中文长句对齐崩)
  • dropout: 0.1 (数据 < 5 h 降到 0.05)
  • lr: 0.3e-3,warmup 4 k step,总步数 100 k

3. 完整训练脚本(单卡可跑)

下面给出最小可运行版本,已含混合精度、梯度裁剪、断点续训:

import torch, json, os
from torch.cuda import amp
from chatts.model import NS22
from chatts.data import MelPhonemeDataset, collate_fn
from torch.utils.data import DataLoader
from transformers import get_cosine_schedule_with_warmup

cfg = json.load(open('config/train.json'))
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# 1. 数据
ds = MelPhonemeDataset(meta='processed/transcript_clean.json')
dl = DataLoader(ds, batch_size=cfg['batch_size'],
                shuffle=True, collate_fn=collate_fn,
                num_workers=4, pin_memory=True)

# 2. 模型
model = NS:2(cfg['model']).to(device)
optimizer = torch.optim.AdamW(model.parameters(), lr=cfg['lr'])
scheduler = get_cosine_schedule_with_warmup(optimizer, cfg['warmup'], cfg['total_steps'])
scaler = amp.GradScaler()

# 3. 训练循环
step = 0
for epoch in range(1000):
    for batch in dl:
        optimizer.zero_grad(set_to_none=True)
        x, y, length = batch
        x, y = x.to(device), y.to(device)

        withamp.autocast():
            mel_pred, post_pred, dur_pred, loss = model(x, y, length)
        scaler.scale(loss).backward()
        scaler.unscale_(optimizer)
        torch.nn.utils.clip_grad_norm_(model.parameters(), cfg['grad_clip'])
        scaler.step(optimizer)
        scaler.update()
        scheduler.step()

        step += 1
        if step % 100 == 0:
            print(f"step={step}, loss={loss.item():.4f}, lr={scheduler.get_last_lr()[0]:.2e}")
        if step >= cfg['total_steps']:
            torch.save(model.state_dict(), 'ckpt/final.pth')
            exit()

跑起来后显存占用约 10.5 G(fp16)。如果只有 12 G 卡,可把 batch_size 降到 16,再开 torch.backends.cudnn.benchmark=True 提速。

性能优化:让 100 k step 从 3 天变 8 小时

  1. 混合精度:上面脚本已用 amp.GradScaler,loss 缩放因子默认 1024,中文语料未出现梯度下溢
  2. 分布式:两张 A100 就用 torchrun 启动,NS2 的 BatchNorm1d 已改 SyncBN,无需改代码
  3. 数据预取:把梅尔提前存盘,训练时直接 np.load,CPU→GPU 带宽省 30 %
  4. 编译模式:PyTorch 2.1+ 开 torch.compile(model, mode='max-autotune'),step 时间从 0.28 s→0.19 s

避坑指南:失败模式 Top5

症状 根因 解决
第 3 k step 后 loss 突然 NaN 梯度爆炸 把 grad_clip 从 1 改 0.5,或把 lr 减半
合成语音全是“哒哒哒” 音素序列与梅尔帧长对不齐 检查 MFA 对齐 csv,是否出现负时长
音色忽大忽小 未做音量归一化 在 trim 后加 RMS 归一化到 - 27 dB
电音+金属声 梅尔谱高频全 0 把 n_fft 从 1024 提到 2048,或加 spectral_subtraction
多说话人混训后音色串扰 speaker embedding 维度过低 把 speaker_dim 从 128 提到 256,加 AAM 损失

部署考量:从实验室到生产

  1. 量化:NS2 的 Linear 层用 torch.quantization.dynamic_linear 可压 42 %,RTF 从 0.35→0.21,基本听不出差别
  2. 推理优化:把梅尔解码器拆出来单独 torch.jit.trace,再开 tensorrt 插件,首帧延迟从 180 ms→90 ms
  3. 热启动:生产环境用 onnxruntime-gpu 跑,注意把 mel_stats.npy 一起打包,否则响度漂移
  4. 监控:实时流式合成时,记录 RTF、首包延迟、MOS 打分,低于阈值自动回滚上一版本模型

开放性问题

同样的 2 h 方言数据,你把 encoder-dim 降到 384 再叠加 2 层 variance predictor,MOS 会涨还是跌?
如果把 attention-head 改成 6,再开 relative_positional_encoding,合成长句(>80 音素)时对齐会不会更稳?

欢迎换组超参跑一遍,把结果贴在评论区,一起把 ChatTTS 玩成“方言自由”!

限时福利领取


Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐