- 引言
- 训练准备
- 训练阶段
- 训练改进
- 小结
在之前的调研报告:文本转语音(TTS)调研报告(下篇)中,我们提到了几款适合本地训练&部署的模型,比如Spark-TTS、CosyVoice2、Moss-TTSD。今天这篇文章将从实战角度介绍CosyVoice2模型的训练。
官方Github仓库: https://github.com/FunAudioLLM/CosyVoice.git
我们将根据CosyVoice官方仓库的examples/libritts/cosyvoice2/run.sh
的流程来拆解训练的步骤。
先来试听一下,以周杰伦声音为参考音频,测试文本为:欢迎关注微信公众号:小窗幽记机器学习
的TTS结果:
听完这段模仿周杰伦的音频,小伙伴们感觉如何呢?欢迎评论区留言!
更多AI相关欢迎关注公众号"小窗幽记机器学习":
以下是run.sh
数据准备阶段的代码参考,以下代码我根据本地数据仓库结构做了部分修改;
- 构造训练数据范式
根据官方LibriTTS的音频数据文件来看,其中参考文件夹的目录如下所示:
|--XXX.normalized.txt # 语音Transcript文本
|
|--XXX.wav # Wav原语音
其中训练语音数据的ASR文本可以通过各类开源ASR模型转述,如Seaco-Paraformer、SenseVoice、Belle-Whisper-large-V3等语音类模型实现,因此处ASR数据转换的内容并非训练的重点,故我们在此处不再展开。
- 设置训练模型路径及训练数据路径。
data\_dir=/data1/nfs/audio\_data\_zoo/cosyvoice\_data
pretrained\_model\_dir=../TTS/pretrained\_models/CosyVoice2-0.5B
- 生成训练数据准备的相关信息,如 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
- 通过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
- 提取各自分离的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
- 根据前置构造的各类数据,合成训练数据映射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
- 训练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
- 观察训练曲线(TensorBoard)
CosyVoice2的官方训练代码提供了tensorboard的路径选项,这便于我们观察训练的loss曲线变化进而调整我们的训练数据、超参等基本参数。
以下小编将分享自己根据公司内部语音数据测试的训练实验记录,并截取其中LLM部分的Loss图供于参考。(注:现阶段为实验阶段,故采用的内部数据整体质量不高,音频采样率为8Khz,语音平均长度范围为3-5s内,音频时长总计1796小时)
以下两图分别是Train及Dev部分的loss、acc训练曲线图。
我将结合训练曲线的走向做个直观描述小结:
- 现阶段训练的数据质量不干净(ASR质量不佳&音质差)导致训练总体曲线持续波动。
- 在第8轮epoch,step40k阶段,我们发现CV的Acc开始下降、loss上升。然而Train Loss仍然持续下降、且Acc不断上升,出现“过拟合”现象。(注:“过拟合“现象在单一业务场景不一定是不好的表现,当我们聚焦某一具体业务场景时不需要模型泛化的能力。)
因此,我们也从上述训练的loss曲线中得到两个实验心得反馈,其中最主要的问题便是数据质量 :
- 清洗优化数据质量;增强ASR转述质量;减少音频杂音干扰。
- 根据具体业务场景,调整训练Epoch数,辨别是否需要模型出现“过拟合”。
根据之前我们训练实验出现的数据问题,我们现在业务场景的现在具有以下限制:1. 数据音频质量差 2.数据缺乏人工标注 3. 音频长度不统一。为了解决上述问题,小编思考设计了一个数据精筛的Pipeline链路,
我们将数据精筛的流程分为以下步骤:
- 设计多ASR融合链路,融合多个ASR模型的结果(如Paraformer、Whisper、SenseVoice等)
- 标准化所有ASR文本结果,去除标点符号、语音标记等标签。
- 通过额外引入的LLM来计算ASR文本之间的语义相似度,若相似度高于0.9则计算文本困惑度,选择语义更加通顺的选项;若相似度低于0.9说明ASR文本差异较大,此时通过代理置信度筛选最佳文本。代理置信度的计算公式为
Score = 文本流畅性 *(1-ratio)+文本健康度 * ratio
,通过文本的流畅性及文本健康度来判断。
根据上述思路完整链路流程图如下所示:
以下小编将提供完整复筛选处理流程的代码,其中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 本地训练的流程与经验。主要心得如下:
- 数据质量至关重要
- ASR 转写质量与音频干净程度直接影响 loss 曲线稳定性。
- 训练前的数据清洗与多模型融合能显著改善效果。
- 当前训练局限
- HiFiGAN 模块尚不支持本地训练。
- LLM 与 Flow 模块可正常运行,但在劣质数据下易过拟合。
- 优化方向
- 构建自动化的数据精筛链路,提升语料可用性。
- 根据业务需求判断是否接受过拟合,或在超参上做进一步调优。
未来,小编计划继续探索:
- 在更高质量数据上的完整训练流程;
- 针对小场景任务的轻量化推理优化。
最后,欢迎大家在评论区留言交流,如有不当之处,敬请指正。语音大模型这块领域小编也在不断探索深耕学习,也期待小伙伴们提出更多宝贵的建议,共同进步。