中文语音识别系统实战:基于PyTorch的ASR源码全流程解析

📅 发布时间:2026/9/23 1:24:20
中文语音识别系统实战:基于PyTorch的ASR源码全流程解析
简介这是一套基于深度学习的中文语音识别系统完整源码面向具备Python编程与神经网络基础的研究者、开发者和语音识别初学者。系统由声学模型与语言模型两大部分组成均基于神经网络实现。声学模型部分涵盖GRU-CTC、CNN-CTC、DFCNN等多种结构其中CNN模型参考科大讯飞DFCNN并做了调整还新增基于CBHG结构的语言模型覆盖从语音特征提取、声学建模到文本输出的完整流程便于算法对比与二次开发。资源压缩包共87个文件约32.12MB以30个py源码、29个txt说明文档、22个lst数据列表为主另有Markdown说明、ipynb实战教程和模型文件等。作者建议优先训练cnn_with_full.py并配套CNNCTC实战教程可帮助快速理解CTC损失、声学特征处理和语言模型融合等核心环节。目前已有535人学习下载是一份结构清晰、实操性很强的中文语音识别学习资料。1. 中文语音识别最难的地方不是模型而是音频进来之后那一堆“脏活”做过ASR项目的朋友应该都有体会模型结构再漂亮如果训练音频里夹杂着口音、环境噪声、静音段和前后端混响最终识别率也会被拉回同一起跑线。这套基于Python深度学习的中文语音识别系统源码核心价值不在于某个特别“新”的网络——它其实是把数据清洗、特征工程、CTC/Attention混合解码、语言模型融合这一整条链路完整地串了一遍尤其针对中文的拼音、声调和数字/人名等集外词做了处理。对刚接触语音识别的人来说你能看出一个工业级任务从音频文件到文字输出需要经历哪些阶段对做过英文端到端识别的人来说这套代码里中文特有的tone嵌入、全角半角统一和词典约束部分仍然值得翻一翻。本文我就按做这个项目最常见的落地路径把“理论—数据—训练—解码”这四层拆开最后放到几个特别容易翻车的细节上。2. 从声学特征到文字输出深度学习耳鼻喉的结构拆解2.1 先搞清中文语音识别跟英文差在哪英文ASR可以直接用char或者subword做输出单位比如“hello”拆成h/e/l/l/o模型比较容易学。中文不能直接拿汉字做细粒度预测因为常用汉字三千多生僻字更多输出空间巨大且样本不平衡。常见的做法是两级建模第一级用声学模型预测拼音带声调拼音库只有400多个音节不带调约410个带调约1300个第二级用语言模型或字典把拼音序列转成汉字序列。这套源码里采用的是“端到端外部语言模型”折衷方案声学模型输出拼音或者带空隙的字符序列然后用一个基于统计的二元语言模型做解码重打分。这样既避免了纯端到端在长文本上的漂移又比传统GMM-HMM少了不少工程复杂度。2.2 特征侧为什么用Fbank而不是MFCC很多入门教程都会拿MFCC说话但现在的深度学习语音识别几乎统一用FbankFilter Bank特征。原因是MFCC在做DCT变换时丢掉了部分高维非线性相关性而神经网络恰好能自己学习特征之间的关系。所以源码里默认提取80维Fbank特征帧长25ms帧移10ms用librosa落盘。一个关键的中文处理点是Pitch特征。中文是声调语言相同声母和韵母不同声调代表完全不同的字而Fbank不包含基频信息。所以源码在Fbank后面直接拼接了3维pitch特征及相关统计量用于辅助声调分类。这一步在英文任务里可以不做中文任务里必须做。2.3 解码单元中文的建模单元怎么选源码提供了三种可配置的建模单元char直接用汉字最小粒度但需要大量数据否则生僻字永远学不好pinyin输出带调拼音后续靠语言模型转字稳定但需要两阶段subword把高频字和低频字拆成Byte Pair Encoding片段适合平衡词表大小。我一般推荐中小数据集上选pinyin因为声学模型更容易收敛后续转字环节用词典LM控制效果远好于直接预测字。源码里的config.yaml中有一行unit: pinyin切换单元后只需要重新生成dict文件训练和解码代码不需要改动。3. 从音频到训练样本Python里的数据管道怎么搭3.1 最容易被忽略的音频预处理步骤源码先把每个wav文件做了以下预处理librosa.load统一采样到16kHz单声道使用webrtcvad截掉首尾静音段并把中间的过长停顿切成逻辑子句对能量过低的音频做增益归一化目标是RMS值到-26dBFS。这些工作在语音识别里属于“脏活”中的“脏活”但对最终模型的效果影响是决定性的。尤其是中文口语数据里大量存在 “嗯…啊…那个” 这类填充词源码里专门建了一个filler_words.txt用来在标注时把它们替换成[UNK]或者直接删除避免模型学到噪音模式。预处理完代码会将每条数据写入一个manifest.json序列{ audio_path: data/thchs30/wav/train/A2_0.wav, duration: 6.72, text: 今天天气不错适合出去玩, pinyin: jin1 tian1 tian1 qi4 bu4 cuo4 shi4 he2 chu1 qu1 wan2 }这个manifest就是后续Dataset的输入源。好处是特征提取和训练解耦你可以先全量提取特征存成数组也可以边训练边提取。3.2 用PyTorch Dataset写一个语音批处理加载器源码的dataset.py继承torch.utils.data.Dataset在__getitem__中做动态特征提取和序列paddingdef __getitem__(self, idx): item self.manifest[idx] sig, sr torchaudio.load(item[audio_path]) feat self.compute_fbank(sig) # (T, feat_dim) feat torch.cat([feat, self.compute_pitch(sig)], dim-1) # 时间维降采样以匹配CTC下采样倍数 feat feat[::2, :] # CNN/RNN stride 共2倍 pinyin_ids self.encode_pinyin(item[pinyin]) return feat, pinyin_ids采集多个样本后用collate_fn做padding把不同长度的特征统一到相同时间步并返回合法的target_lengths供CTC计算损失。需要注意的一点是源码里把特征在时间维做了2倍下采样对应的标签序列也需要同步缩放否则CTC关于路径的长度就对应不上了。3.3 模型结构参考从CNN到双向LSTM再到CNN的经典组合这里给出源码所采用的主体模型结构在model.py中定义class ConvLSTMASR(nn.Module): def __init__(self, input_dim83, hidden_size256, output_dim1000): super().__init__() self.cnn nn.Sequential( nn.Conv2d(1, 32, kernel_size(3, 3), stride1, padding1), nn.BatchNorm2d(32), nn.ReLU(), nn.Conv2d(32, 64, kernel_size(3, 3), stride2, padding1), nn.BatchNorm2d(64), nn.ReLU(), ) self.lstm nn.LSTM( input_size64 * (input_dim // 2), hidden_sizehidden_size, num_layers4, bidirectionalTrue, batch_firstTrue ) self.ctc nn.Linear(hidden_size * 2, output_dim)这个结构不算新但非常稳。前两层的CNN把输入维从input_dim降到一半时间维也减半双向LSTM负责建模长程上下文最后接线性层输出每个时间步在拼音/汉字上的后验分布。损失函数用torch.nn.CTCLoss训练时只算CTC解码时才引入语言模型。参数解读input_dim8380维Fbank 3维pitchhidden_size256单层128维×双向4层足够适应中文音节的上下文跨度output_dim1000取决于建模单元数量带调拼音大约1300如果去掉声调就只有410。4. 训练与调参CER不是唯一指标但却是最该看的4.1 数据增强中文ASR尤其要用速度扰动源码里train.py训练循环中默认开启了SpecAugment和随机速度扰动。速度扰动通过torchaudio.transforms.SpeedPerturbation(orig_freq16000, speeds[0.9, 1.0, 1.1])实现作用不只是数据扩增更重要的是让模型对语速不敏感——中文语速变化比英文更明显口语尤其如此。SpecAugment参数建议from spec_augment import SpecAugment spec_aug SpecAugment( freq_mask_max13, time_mask_max25, freq_masks2, time_masks2, p1.0 )在__getitem__中作用在特征矩阵上注意不能在时间维遮掉太多否则长句子会断裂。经验值是时间掩码最大长度不超过25帧约250ms频率掩码不超过13维。4.2 学习率调度和梯度裁剪比网络结构更值得花时间这套源码的optimizer用的是AdamW初始lr5e-4配合warmup 指数衰减scheduler torch.optim.lr_scheduler.LambdaLR( optimizer, lr_lambdalambda epoch: 0.5 ** (epoch // 5) if epoch 3 else float(epoch 1) / 4 )前3个epoch线性warmup到基学习率之后每5个epoch减半。同时设置clip_grad_norm_5.0否则双向LSTM在长序列上很容易梯度爆炸。训练日志会打印每个epoch的train loss、valid loss和验证集上的CER字符错误率。CER的计算代码通常是from jiwer import cer def compute_cer(pred_texts, ref_texts): # 去掉加空格、忽略标点后 return cer(ref_texts, pred_texts)注意jiwer.cer默认对中文也会按“字符”算所以预处理要把全角括号、标点去掉否则标点错误也会计入CER导致分数虚低。我一般会让评估函数自动忽略《》()[]。等符号。4.3 遇到过拟合时先查这两处中文ASR小数据集几十小时很容易过拟合表现为train loss持续下降但valid CER不降反升。源码里预先留了三个开关依次检查dropout是否设置LSTM层间dropout建议设为0.3到0.5weight_decay是否太小AdamW下建议weight_decay1e-5是否做了文本归一化——这个最容易忽略。比如中文数字“一二三”和阿拉伯数字“123”在文本里如果混用模型会无所适从。源码的utils/text_norm.py里强制把所有数字转成中文大写时间格式统一成“晚上八点”而不是“20:00”。最后一点特别重要很多人训练出来的模型把“2023年”读成“二零二三年”但测试集里写的是“2023年”于是CER直接偏高。这种规范不统一的问题是中文ASR中最隐蔽的干扰因素。5. 解码阶段与部署验证集束搜索、语言模型重打分和热词加固5.1 使用pyctcdecode做带语言模型的集束搜索源码里没有走纯贪心解码而是在decode.py里引入了pyctcdecode这个纯Python解码库。它支持把外部语言模型KenLM生成的arpa格式直接融合到CTC概率上。调用方式如下from pyctcdecode import build_ctcdecoder decoder build_ctcdecoder( labelsvocab_list, # [pad, jin1, tian1, ...] kenlm_model_pathlm/zh_2gram.arpa, alpha0.6, beta1.2 ) def decode_batch(feat_batch): logits model(feat_batch) # (B, T, V) probs torch.softmax(logits, dim-1).cpu().numpy() return [decoder.decode(p.replace(pad, ), beam_width16) for p in probs]参数alpha和beta分别控制语言模型得分和单词插入惩罚。中文里建议alpha在0.5~0.8之间beta取1.0~1.5因为中文的拼音长度比英文词短如果没有插入惩罚解码器会倾向于输出很短的拼音串。5.2 热词纠错专有名词和人名怎么强制走对纯靠统计LM很难覆盖“颟顸”“綦江”这类低频词。源码在解码后用一个正则替换表做强制纠偏。这部分不是模型学出来的而是规则表格形如HOT_WORD_MAP { ai1 si1 tang2: 艾丝堂, diao4 yu2 dao3: 钓鱼岛, xia4 ji1: 夏季, }实现时遍历每个候选词如果拼音序列完全匹配就直接替换为指定的汉字组合。这种做法的好处是可控、可解释坏处是覆盖率有限。进阶做法是把这个表送进解码器作为偏置牺牲一点解码速度换取更自然的融合。5.3 最后检查验证CER前先把标点和数字规则跑一遍部署到业务前我通常建议写一个集成脚本evaluate.py统一流程为对测试集音频进行和训练完全相同的预处理包括VAD和速度归一化模型输出拼音序列用词典LM转成汉字做标点恢复、数字转换。标点恢复可以简单用一个基于规则的模块遇到语气词“啊”“吗”“呢”结尾补问号遇到时间词后补逗号。不需要上Bert业务场景里规则恢复已经够用。如果你手头正好有这套源码解压出来优先把config.yaml里的manifests_path改成自己的数据路径然后执行python prepare_data.py --data_dir data/ --unit pinyin python train.py --config config.yaml --epochs 30 python decode.py --model_dir checkpoint/ --lm lm/zh_2gram.arpa至于模型到底能不能到95%以上准确率跟数据量、口音覆盖度直接相关。如果只有5小时数据别期待“上市级”效果但把这套管道跑通后至少你对每个环节需要做什么、指标波动可能来自哪一层会有非常清晰的感觉。最后再提一个容易踩但极少被文档提及的问题训练和推理时的特征提取状态必须完全一致特别是normalize均值方差那一项推荐在compute_fbank里用全局统计量而不是在训练集上在线算——否则部署时的CER会比验证时高2到3个百分点。本文还有配套的精品资源点击获取