ChatTTS训练自己的语音模型:从数据准备到模型微调实战指南
ChatTTS 默认用的是“非自回归+Transformer+可学习时长预测器”的混合架构,官方叫。一句话:Demo 五分钟,训练五昼夜。下面把我踩过的坑和加速方案打包奉上,尽量让你“一次跑通”。欢迎换组超参跑一遍,把结果贴在评论区,一起把 ChatTTS 玩成“方言自由”!如果只有 12 G 卡,可把。如果把 attention-head 改成 6,再开。,合成长句(>80 音素)时对齐会不会更
背景与痛点:为什么自己训 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) |
结论:
- 想快速出 demo,直接用 NS2;
- 追求极致音质,可把 NS2 的 Decoder 换成 FastSpeech2 的 Variance Predictor,再蒸馏一次;
- 硬件 < 12 G 就别折腾了,先租 A100 再谈理想。
核心实现:从原始录音到可训练样本
1. 数据预处理流程
先给目录树,方便后面脚本直接 copy:
dataset/
├─ raw_audio/ # 原始 wav
├─ transcripts.txt # 格式:文件名|文本
├─ processed/
│ ├─ wav_16k/ # 重采样后
│ ├─ mels/ # 梅尔频谱 .npy
│ └─ phonemes/ # 音素序列 .txt
步骤:
- 统一重采样 16 kHz,mono,16-bit
- 用 WeTextProcessing 做中文文本规范化:数字“123”→“一百二十三”,符号“¥”→“元”
- 强制对齐获取音素时长(Montreal Forced Aligner 3.0),输出 csv
- 根据对齐结果裁剪静音段,保证首尾无静音
- 提取 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 小时
- 混合精度:上面脚本已用
amp.GradScaler,loss 缩放因子默认 1024,中文语料未出现梯度下溢 - 分布式:两张 A100 就用
torchrun启动,NS2 的BatchNorm1d已改SyncBN,无需改代码 - 数据预取:把梅尔提前存盘,训练时直接
np.load,CPU→GPU 带宽省 30 % - 编译模式: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 损失 |
部署考量:从实验室到生产
- 量化:NS2 的 Linear 层用
torch.quantization.dynamic_linear可压 42 %,RTF 从 0.35→0.21,基本听不出差别 - 推理优化:把梅尔解码器拆出来单独
torch.jit.trace,再开tensorrt插件,首帧延迟从 180 ms→90 ms - 热启动:生产环境用
onnxruntime-gpu跑,注意把mel_stats.npy一起打包,否则响度漂移 - 监控:实时流式合成时,记录 RTF、首包延迟、MOS 打分,低于阈值自动回滚上一版本模型
开放性问题
同样的 2 h 方言数据,你把 encoder-dim 降到 384 再叠加 2 层 variance predictor,MOS 会涨还是跌?
如果把 attention-head 改成 6,再开 relative_positional_encoding,合成长句(>80 音素)时对齐会不会更稳?
欢迎换组超参跑一遍,把结果贴在评论区,一起把 ChatTTS 玩成“方言自由”!
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐




所有评论(0)