文本转语音(TTS)实战:以CosyVoice2为例 | 附代码

大模型机器学习数据库
目录
  • 引言
  • 训练准备
  • 训练阶段
  • 训练改进
  • 小结
引言

在之前的调研报告:文本转语音(TTS)调研报告(下篇)中,我们提到了几款适合本地训练&部署的模型,比如Spark-TTS、CosyVoice2、Moss-TTSD。今天这篇文章将从实战角度介绍CosyVoice2模型的训练。

picture.image

官方Github仓库: https://github.com/FunAudioLLM/CosyVoice.git

我们将根据CosyVoice官方仓库的examples/libritts/cosyvoice2/run.sh的流程来拆解训练的步骤。

先来试听一下,以周杰伦声音为参考音频,测试文本为:欢迎关注微信公众号:小窗幽记机器学习的TTS结果:

听完这段模仿周杰伦的音频,小伙伴们感觉如何呢?欢迎评论区留言!

更多AI相关欢迎关注公众号"小窗幽记机器学习":

训练准备

以下是run.sh数据准备阶段的代码参考,以下代码我根据本地数据仓库结构做了部分修改;

  1. 构造训练数据范式

根据官方LibriTTS的音频数据文件来看,其中参考文件夹的目录如下所示:

  
|--XXX.normalized.txt # 语音Transcript文本  
|  
|--XXX.wav # Wav原语音  

其中训练语音数据的ASR文本可以通过各类开源ASR模型转述,如Seaco-Paraformer、SenseVoice、Belle-Whisper-large-V3等语音类模型实现,因此处ASR数据转换的内容并非训练的重点,故我们在此处不再展开。

  1. 设置训练模型路径及训练数据路径。
  
data\_dir=/data1/nfs/audio\_data\_zoo/cosyvoice\_data  
pretrained\_model\_dir=../TTS/pretrained\_models/CosyVoice2-0.5B  

  1. 生成训练数据准备的相关信息,如 wav.scp/text/utt2spk/spk2utt等。
  
if [ ${stage} -le 0 ] && [ ${stop\_stage} -ge 0 ]; then  
  echo "Data preparation, prepare wav.scp/text/utt2spk/spk2utt"  
  for x in train dev test; do  
    mkdir -p exp\_data/$x  
    python local/prepare\_data.py --src\_dir $data\_dir/$x --des\_dir exp\_data/$x  
  done  
fi  

  1. 通过Camplus模型提取带不同说话人语音标记的embedding
  
if [ ${stage} -le 1 ] && [ ${stop\_stage} -ge 1 ]; then  
  echo "Extract campplus speaker embedding, you will get spk2embedding.pt and utt2embedding.pt in exp\_data/$x dir"  
  for x in train dev test; do  
    tools/extract\_embedding.py --dir exp\_data/$x \  
      --onnx\_path $pretrained\_model\_dir/campplus.onnx  
  done  
fi  

  1. 提取各自分离的Speech Token,以pt格式存储于data内。
  
if [ ${stage} -le 2 ] && [ ${stop\_stage} -ge 2 ]; then  
  echo "Extract discrete speech token, you will get utt2speech\_token.pt in exp\_data/$x dir"  
  for x in train dev test; do  
    tools/extract\_speech\_token.py --dir exp\_data/$x \  
      --onnx\_path $pretrained\_model\_dir/speech\_tokenizer\_v2.onnx  
  done  
fi  

  1. 根据前置构造的各类数据,合成训练数据映射Parquet文件。
  
if [ ${stage} -le 3 ] && [ ${stop\_stage} -ge 3 ]; then  
  echo "Prepare required parquet format data, you should have prepared wav.scp/text/utt2spk/spk2utt/utt2embedding.pt/spk2embedding.pt/utt2speech\_token.pt"  
  for x in train dev test; do  
    mkdir -p exp\_data/$x/parquet  
    tools/make\_parquet\_list.py --num\_utts\_per\_parquet 1000 \  
      --num\_processes 10 \  
      --src\_dir exp\_data/$x \  
      --des\_dir exp\_data/$x/parquet  
  done  
fi  

构造训练数据的部分比较基础,根据官方文档步骤一步一步构建即可,其中提取Speech Token阶段耗时较久,以及训练的语音数据需限制为16Khz以上。

完整数据构造Shell脚本如下所示:

  
stage=-1  
stop\_stage=3 # 设置停顿步骤step。  
  
data\_dir=/data1/nfs/audio\_data\_zoo/cosyvoice\_data  
pretrained\_model\_dir=../TTS/pretrained\_models/CosyVoice2-0.5B  
  
if [ ${stage} -le 0 ] && [ ${stop\_stage} -ge 0 ]; then  
  echo "Data preparation, prepare wav.scp/text/utt2spk/spk2utt"  
  for x in train dev test; do  
    mkdir -p exp\_data/$x  
    python local/prepare\_data.py --src\_dir $data\_dir/$x --des\_dir exp\_data/$x  
  done  
fi  
  
if [ ${stage} -le 1 ] && [ ${stop\_stage} -ge 1 ]; then  
  echo "Extract campplus speaker embedding, you will get spk2embedding.pt and utt2embedding.pt in exp\_data/$x dir"  
  for x in train dev test; do  
    tools/extract\_embedding.py --dir exp\_data/$x \  
      --onnx\_path $pretrained\_model\_dir/campplus.onnx  
  done  
fi  
  
if [ ${stage} -le 2 ] && [ ${stop\_stage} -ge 2 ]; then  
  echo "Extract discrete speech token, you will get utt2speech\_token.pt in exp\_data/$x dir"  
  for x in train dev test; do  
    tools/extract\_speech\_token.py --dir exp\_data/$x \  
      --onnx\_path $pretrained\_model\_dir/speech\_tokenizer\_v2.onnx  
  done  
fi  
  
if [ ${stage} -le 3 ] && [ ${stop\_stage} -ge 3 ]; then  
  echo "Prepare required parquet format data, you should have prepared wav.scp/text/utt2spk/spk2utt/utt2embedding.pt/spk2embedding.pt/utt2speech\_token.pt"  
  for x in train dev test; do  
    mkdir -p exp\_data/$x/parquet  
    tools/make\_parquet\_list.py --num\_utts\_per\_parquet 1000 \  
      --num\_processes 10 \  
      --src\_dir exp\_data/$x \  
      --des\_dir exp\_data/$x/parquet  
  done  
fi  

训练阶段
  1. 训练LLM与Flow模型

来到训练阶段,如果按照官方训练脚本,小编在本地实践操作发现HiFigan模块无法训练——当设置model为“HIFT”或“HIFIGAN”的时候遇见不可导的问题使得无法计算CV阶段的loss。并且截止至小编开始记录笔记的今日,在GitHub上仍未开源HiFiGAN部分的训练代码。

  
# train llm  
export CUDA\_VISIBLE\_DEVICES="0,1,2,3"  
num\_gpus=$(echo $CUDA\_VISIBLE\_DEVICES | awk -F "," '{print NF}')  
job\_id=1986  
dist\_backend="nccl"  
num\_workers=2  
prefetch=100  
train\_engine=torch\_ddp  
if [ ${stage} -le 5 ] && [ ${stop\_stage} -ge 5 ]; then  
  echo "Run train. We only support llm traning for now"  
  if [ $train\_engine == 'deepspeed' ]; then  
    echo "Notice deepspeed has its own optimizer config. Modify conf/ds\_stage2.json if necessary"  
  fi  
  cat exp\_data/train/parquet/data.list > exp\_data/train.data.list   
  cat exp\_data/dev/parquet/data.list > exp\_data/dev.data.list  
  # NOTE will update llm/hift training later  
  echo "$pretrained\_model\_dir"  
  # for model in llm flow hifigan; do  
  for model in llm flow; do # 此处该为只训练LLM 与 Flow模型  
    torchrun --nnodes=1 --nproc\_per\_node=$num\_gpus \  
        --rdzv\_id=$job\_id --rdzv\_backend="c10d" --rdzv\_endpoint="localhost:1234" \  
      cosyvoice/bin/train.py \  
      --train\_engine $train\_engine \  
      --config conf/cosyvoice2.yaml \  
      --train\_data exp\_data/train.data.list \  
      --cv\_data exp\_data/dev.data.list \  
      --qwen\_pretrain\_path $pretrained\_model\_dir/CosyVoice-BlankEN \  
      --model $model \  
      --checkpoint $pretrained\_model\_dir/$model.pt \  
      --model\_dir /data1/nfs/audio\_model\_zoo/Cosyvoice-finetune/exp/cosyvoice2/$model/$train\_engine \  
      --tensorboard\_dir /data1/nfs/audio\_model\_zoo/Cosyvoice-finetune/exp/tensorboard/cosyvoice2/$model/$train\_engine \  
      --ddp.dist\_backend $dist\_backend \  
      --num\_workers ${num\_workers} \  
      --prefetch ${prefetch} \  
      --pin\_memory \  
      --use\_amp \  
      --deepspeed\_config ./conf/ds\_stage2.json \  
      --deepspeed.save\_states model+optimizer  
  done  
fi  

  1. 观察训练曲线(TensorBoard)

CosyVoice2的官方训练代码提供了tensorboard的路径选项,这便于我们观察训练的loss曲线变化进而调整我们的训练数据、超参等基本参数。

以下小编将分享自己根据公司内部语音数据测试的训练实验记录,并截取其中LLM部分的Loss图供于参考。(注:现阶段为实验阶段,故采用的内部数据整体质量不高,音频采样率为8Khz,语音平均长度范围为3-5s内,音频时长总计1796小时)

以下两图分别是Train及Dev部分的loss、acc训练曲线图。

picture.image

picture.image

我将结合训练曲线的走向做个直观描述小结:

  1. 现阶段训练的数据质量不干净(ASR质量不佳&音质差)导致训练总体曲线持续波动。
  2. 在第8轮epoch,step40k阶段,我们发现CV的Acc开始下降、loss上升。然而Train Loss仍然持续下降、且Acc不断上升,出现“过拟合”现象。(注:“过拟合“现象在单一业务场景不一定是不好的表现,当我们聚焦某一具体业务场景时不需要模型泛化的能力。)

因此,我们也从上述训练的loss曲线中得到两个实验心得反馈,其中最主要的问题便是数据质量

  1. 清洗优化数据质量;增强ASR转述质量;减少音频杂音干扰。
  2. 根据具体业务场景,调整训练Epoch数,辨别是否需要模型出现“过拟合”。
训练改进

根据之前我们训练实验出现的数据问题,我们现在业务场景的现在具有以下限制:1. 数据音频质量差 2.数据缺乏人工标注 3. 音频长度不统一。为了解决上述问题,小编思考设计了一个数据精筛的Pipeline链路,

我们将数据精筛的流程分为以下步骤:

  1. 设计多ASR融合链路,融合多个ASR模型的结果(如Paraformer、Whisper、SenseVoice等)
  2. 标准化所有ASR文本结果,去除标点符号、语音标记等标签。
  3. 通过额外引入的LLM来计算ASR文本之间的语义相似度,若相似度高于0.9则计算文本困惑度,选择语义更加通顺的选项;若相似度低于0.9说明ASR文本差异较大,此时通过代理置信度筛选最佳文本。代理置信度的计算公式为 Score = 文本流畅性 *(1-ratio)+文本健康度 * ratio ,通过文本的流畅性及文本健康度来判断。

根据上述思路完整链路流程图如下所示:

picture.image

以下小编将提供完整复筛选处理流程的代码,其中LM、LLM模型可以自主替换:

  
model\_name = "/data1/nfs/llm/Qwen2.5"  
qwen\_tokenizer = AutoTokenizer.from\_pretrained(model\_name, trust\_remote\_code=True)  
qwen\_model = AutoModelForCausalLM.from\_pretrained(model\_name, trust\_remote\_code=True).eval()  
sbert\_path = "/data1/nfs/llm/sbert"  
sbert\_model = SentenceTransformer(sbert\_path)  
  
def normalize\_text(text):  
    """文本标准化"""  
    text = text.lower().strip()  
    text = re.sub(r'[^\w\s]', '', text)  # 去标点  
    text = re.sub(r'\s+', ' ', text)     # 多空格合并  
    return text  
  
def text\_health\_score(text):  
    """文本健康度打分 [0,1]"""  
    words = text.split()  
    length = len(words)  
      
    # 长度合理性 (5~50词)  
    length\_score = 1.0if5 <= length <= 500else0.5  
      
    # 重复词检测(连续3次相同)  
    repeat\_score = 1.0  
    for i in range(len(words) - 2):  
        if words[i] == words[i+1] == words[i+2]:  
            repeat\_score = 0.6  
            break  
      
    # 字符合理性(避免乱码)  
    alpha\_ratio = len(re.findall(r'[a-zA-Z]', text)) / (len(text) + 1)  
    char\_score = 0.8if0.3 < alpha\_ratio < 0.9else0.6  
      
    return length\_score * repeat\_score * char\_score  
  
def lm\_score(text):  
    inputs = qwen\_tokenizer(text, return\_tensors="pt")  
    with torch.no\_grad():  
        outputs = qwen\_model(**inputs, labels=inputs["input\_ids"])  
        loss = outputs.loss  
        ppl = math.exp(loss.item())  
        lm\_score = -np.log(ppl + 1e-6)  
    return lm\_score  
  
def build\_proxy\_confidence(text):  
    """构建代理置信度 [0,1]"""  
    norm\_text = normalize\_text(text)  
  
    # 1. 语言流畅性  
    lm = lm\_score(norm\_text)  
    lm\_norm = (lm + 10) / 10# 归一化(假设 -10 ~ 0)  
    lm\_norm = np.clip(lm\_norm, 0, 1)  
  
    # 2. 文本健康度  
    health = text\_health\_score(text)  
  
    # 加权融合  
    score = 0.6 * lm\_norm + 0.4 * health  
    return score  
  
def sentence\_similarity(text\_a, text\_b):  
    """计算语义相似度 [0,1]"""  
    emb\_a = sbert\_model.encode(text\_a)  
    emb\_b = sbert\_model.encode(text\_b)  
    sim = np.dot(emb\_a, emb\_b) / (np.linalg.norm(emb\_a) * np.linalg.norm(emb\_b))  
    return (sim + 1) / 2# 映射到 [0,1]  
  
  
def fuse\_two\_asr\_results(text\_a, text\_b):  
    """  
    融合两个 ASR 模型输出  
    :param text\_a: Model A 输出  
    :param text\_b: Model B 输出  
    :return: 融合后最优文本  
    """  
    ifnot text\_a.strip():  
        return text\_b  
    ifnot text\_b.strip():  
        return text\_a  
  
    norm\_a = normalize\_text(text\_a)  
    norm\_b = normalize\_text(text\_b)  
  
    # Step 1: 完全一致 → 直接返回  
    if norm\_a == norm\_b:  
        return text\_a  
      
    # Step 2: 语义相似度  
      
    sim = sentence\_similarity(text\_a, text\_b)  
      
    if sim > 0.9:  
        # 高相似:选语言更通顺的  
        score\_a = lm\_score(text\_a)  
        score\_b = lm\_score(text\_b)  
        return text\_a if score\_a > score\_b else text\_b  
    else:  
        # 差异大:使用代理置信度  
        conf\_a = build\_proxy\_confidence(text\_a)  
        conf\_b = build\_proxy\_confidence(text\_b)  
        return text\_a if conf\_a > conf\_b else text\_b  

小结

本文从 数据准备 → 模型训练 → 实验观察 → 改进方案 ,完整记录了 CosyVoice2 本地训练的流程与经验。主要心得如下:

  1. 数据质量至关重要
  • ASR 转写质量与音频干净程度直接影响 loss 曲线稳定性。
  • 训练前的数据清洗与多模型融合能显著改善效果。
  • 当前训练局限
  • HiFiGAN 模块尚不支持本地训练。
  • LLM 与 Flow 模块可正常运行,但在劣质数据下易过拟合。
  • 优化方向
  • 构建自动化的数据精筛链路,提升语料可用性。
  • 根据业务需求判断是否接受过拟合,或在超参上做进一步调优。

未来,小编计划继续探索:

  • 在更高质量数据上的完整训练流程;
  • 针对小场景任务的轻量化推理优化。

最后,欢迎大家在评论区留言交流,如有不当之处,敬请指正。语音大模型这块领域小编也在不断探索深耕学习,也期待小伙伴们提出更多宝贵的建议,共同进步。

0
0
0
0
关于作者

文章

0

获赞

0

收藏

0

相关资源
CV 技术在视频创作中的应用
本次演讲将介绍在拍摄、编辑等场景,我们如何利用 AI 技术赋能创作者;以及基于这些场景,字节跳动积累的领先技术能力。
相关产品
评论
未登录
看完啦,登录分享一下感受吧~
暂无评论