简介:面向语音识别与深度学习开发者,这份源码包完整实现了基于神经网络的中文语音识别系统,涵盖声学模型与语言模型两大核心模块。声学模型部分提供GRU-CTC、CNN-CTC,以及基于DFCNN并融合inception的fbank输入模型,还针对pluse数据集优化了可直接训练的CNN模型;语言模型部分引入CBHG结构,为识别结果提供神经网络级别的序列建模。资源共87个文件,以30个Python脚本为主,辅以22个lst数据列表、29个txt说明文件、2个md说明文档及ipynb演示笔记,压缩包约32.12MB,脚本、列表、文档一应俱全,便于整体查阅和本地调试。目前已有535人学习下载,适合希望从代码层面理解中文语音识别训练流程的高年级学生、算法工程师及竞赛团队。通过源码中的模型对比与教程notebook,可快速掌握时频图特征、CTC损失、自适应网络结构等关键设计,并在此基础上修改数据路径与超参数,搭建自己的识别实验。
1. 中文语音识别的源码包:先分清它属于哪一类深度学习系统
打开压缩包之前,先想清楚一个问题:这个标题里的“源码”是模型定义脚本,还是完整可训练可部署的工程?同样是中文语音识别,有的包只给了一个 PyTorch 模型文件,输入梅尔谱图直接输出汉字概率;有的包把特征提取、训练循环、解码器写成了三层结构;还有的包源码并不完整,需要在运行时从外部加载预训练权重和词典。三者环境配置差异很大,别拿到 zip 就去找 Python 安装教程,先把目录结构翻一遍。以 Python 为入口的深度学习中文语音识别系统,当前主流做法都围绕三个模块:音频特征提取、声学模型、解码器。中文场景还要额外处理文本正则、标点、拼音到汉字的映射。我下面顺着“解压到能跑通”这条线,讲清楚每一步需要改哪些参数,以及做训练和推理时最常踩的坑。
2. Python 与深度学习环境预备:跑通源码包的最低要求
2.1 先按模型代码反推 Python 版本
不要一拿到压缩包就运行pip install -r requirements.txt。先把model.py、train.py或infer.py的头部读一遍,看 import 了哪些深度学习库。常见三种情况:import torch代表 PyTorch 体系,大概率要配合torchaudio;from funasr import AutoModel说明它依赖 FunASR;如果出现import onnxruntime,那你拿到的其实是推理部署包,训练脚本可能根本不在里面。
这三种情况对 Python 版本要求不同。PyTorch 2.x 在 Python 3.9 到 3.11 下最稳,老代码如果用了torch.nn.functional.ctc_loss且训练逻辑里还有 torch 1.8 时代的 API,直接锁 Python 3.9 + torch 1.13 的组合更省事。不要因为想装最新 torch 就去升级 Python,很多函数签名在新版本里已经替换,报错信息会让你误判成依赖缺失。
我一般会建一个独立 conda 环境,避免把系统 Python 或者 Anaconda base 环境搞乱。文件名带“源码+说明文档”的包,通常会在 README 里写 Python 版本,没有的话就按模型代码里 torch 的用法判断。
conda create -n ctc_asr python=3.9 -y conda activate ctc_asr conda install pytorch==2.1.2 torchaudio==2.1.2 cudatoolkit=11.8 -c pytorch pip install -r requirements.txt这段命令先是锁定 Python 3.9,再安装 PyTorch 2.1.2 和 torchaudio 2.1.2。-c pytorch表示从 PyTorch 官方 channel 下载。GPU 环境里cudatoolkit=11.8要跟本机 NVIDIA 驱动匹配;torch.__version__里显示+cu118就代表 CUDA 绑定成功。如果没有 N 卡,把cudatoolkit去掉,CPU 版本同样能跑推理,只是训练慢一个数量级。若创建环境失败,先不要锁版本,只保留python=3.9重试。
对于没有 requirements.txt 的源码包,先手动补齐核心依赖:numpy、soundfile、librosa、tqdm、pypinyin、jieba。pypinyin不是必须的,但中文语音识别常用拼音侧信息,比如把歌词转成拼音后做热词增强,这个依赖就会出现。补齐后用下面的命令做最基础的 import 检查:
python -c "import torch, torchaudio, soundfile, librosa; print('deps ok')"这个命令能通过,说明深度学习环境配置里的绝大部分库已经就位,再往下才轮到训练或推理脚本自带的错误。
2.2 用一条 10 秒音频做冒烟测试
环境配完后不要直接训练,也不要直接跑完整推理。找一个 16kHz 单声道 wav 文件,长度控制在 10 秒以内,先让模型在 CPU 上完成一次前向计算。这一步能筛掉八成左右由 API 调用错误引发的问题。
import torch import torchaudio waveform, sr = torchaudio.load("sample.wav") if sr != 16000: waveform = torchaudio.functional.resample(waveform, sr, 16000) feat = torchaudio.compliance.kaldi.fbank( waveform, num_mel_bins=80, frame_length=25, frame_shift=10 ) feat = feat.unsqueeze(0) model = torch.jit.load("model.zip/model.pt") # 按实际路径改 with torch.no_grad(): out, _ = model(feat, torch.tensor([feat.size(1)])) print(out.shape)这段代码把音频重采样到 16k,然后提取 80 维 fbank 特征,再送入模型。frame_length=25表示 25ms 一帧,frame_shift=10表示 10ms 移动一次,这两个参数要和训练配置一致;不一致时输出的时间帧数会变化,CTC 对齐会整体偏移。torch.jit.load只适用于 PyTorch 打包后的模型,如果是源码包中的.pt文件,可能要改成torch.load。冒烟测试的目的不是识别出文字,而是确认输入输出 tensor 形状正确。
输出形状通常是(batch, time, vocab_size)或者(time, batch, vocab_size)。后者在计算 CTC loss 前不需要转置,前者需要permute(1, 0, 2)后再传给ctc_loss。源码说明文档里如果没写输出维度,就用这个冒烟测试自己确认。
2.3 依赖版本对照表
下面这张表是我维护中文语音识别环境时常用的基线版本。注意这些不是最新版,但相互之间验证过,能避免torch与torchaudio版本不匹配导致的libsox崩溃问题。
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| Python | 3.9 或 3.10 | 过老项目选 3.9 |
| PyTorch | 2.1.2 | 支持 Conformer/Transformer 足够 |
| torchaudio | 2.1.2 | 必须与 torch 同主版本 |
| librosa | 0.10.1 | 加载 wav/mp3 用 |
| pyctcdecode | 0.5.0 | 解码用,可选但推荐 |
| jieba | 0.42.1 | 文本分词和热词切割 |
版本冲突时最典型的报错是torchaudioimport 时出现No module named 'torchaudio._extension'。这不是代码问题,而是两个包版本对不上。处理方式是把它们一起降级或升级,保证主版本号一致,不要单独换torchaudio。
3. 中文语音识别模型架构选型与训练参数设置
3.1 为什么中文场景首选 CTC 作为基础解码
中文语音识别和英文最大的差异在输出单元。中文常用汉字就有三千到五千个,加上标点和生僻字,词表要做到 6000 左右。英文常用词表做到 500 就能覆盖大部分场景,但中文如果直接以词为建模单元,需要先用分词器处理文本,分词误差会一路传导到识别结果。所以现在源码里见到的大多数中文语音识别系统,建模单元都选“字”而不是“词”,并配合 Connectionist Temporal Classification 做序列对齐。
CTC 的核心思想是把文本和音频之间的逐帧对齐交给网络自动学习,训练时不需要人工切分音素边界。对中文来说,这意味着可以直接用汉字序列训练,不需要先把句子转成拼音。相比纯注意力机制,CTC 在长句上不容易出现重复和漏字;缺点是静音帧和连续相同字会被合并,推理时要处理 collapse 规则。常见做法是:先训练一个 Conformer 编码器加 CTC 头,得到稳定的声学模型;如果对识别质量要求更高,再在解码阶段融合 n-gram 语言模型。
3.2 用 Conformer 搭一个中文字符级声学模型
下面这段代码是一个最小可用的中文语音识别模型定义,适合理解源码包中model.py的结构。重点不是它有多强,而是参数怎么对应中文语音识别任务。
import torch import torch.nn as nn from torchaudio.models import Conformer class CharCtcModel(nn.Module): def __init__(self, vocab_size, feat_dim=80): super().__init__() self.encoder = Conformer( input_dim=feat_dim, num_heads=4, ffn_dim=256, num_layers=8, depthwise_conv_kernel_size=31, dropout=0.15, ) self.ctc_head = nn.Linear(256, vocab_size) self.log_softmax = nn.LogSoftmax(dim=-1) def forward(self, features, lengths): out, out_lengths = self.encoder(features, lengths) logits = self.ctc_head(out) return self.log_softmax(logits), out_lengthsConformer来自torchaudio.models,输入是 80 维 fbank,输出保持时间维不变,特征维变为 256。ctc_head把 256 维映射到vocab_size,然后用LogSoftmax输出对数概率。depthwise_conv_kernel_size=31是卷积核大小,中文语音识别里常用 31 或 33,太小会丢失相邻帧信息,太大会让模型只顾短时能量变化。
实际源码包里通常会在模型前面再接一个卷积子采样层,把 80 维特征从 10ms 一帧变成 40ms 一帧再进 Conformer。原因是原始 10ms 帧率对注意力机制来说太密,1 分钟音频有 6000 帧,内存和训练时间都顶不住。如果你看到的源码是Conv2d(... stride=(2, 2))这类结构,那就是在做时间维抽样。训练时要注意同步更新lengths,推理时用同一套feat_lengths / 2的方式计算新序列长度。
3.3 训练参数和字符表生成
中文语音识别训练时最容易被忽略的是字符表生成。有人直接把训练集标注里的所有字收集成一个列表,却忘了统一标点和数字。中文数字“一、二、三”和阿拉伯数字“1、2、3”,如果不当作两个符号,模型预测概率会被分散,最终 CER 虚高。
下面这段代码从标注目录生成词表,并把标点保留在词表里面。要不要把标点纳入词表,取决于应用场景;字幕识别可以保留标点,关键词检索最好在训练前把标点全部删掉。
from pathlib import Path def generate_vocab(trans_dir, save_path="vocab.txt"): chars = set() for txt in Path(trans_dir).glob("*.txt"): text = txt.read_text(encoding="utf-8") for line in text.strip().splitlines(): line = line.replace(" ", "") chars.update(line) chars = sorted(chars) with open(save_path, "w", encoding="utf-8") as f: f.write("<blank>\n") for c in chars: f.write(c + "\n") f.write("<unk>\n")<blank>必须排在词表第一位,因为nn.CTCLoss默认blank=0。<unk>表示未登录字,放在最后。标注中的空格被移除,因为空格在 CTC 里也是一个可输出符号;如果训练文本和测试文本的空格分布不一致,会出现大量删除错误。
训练过程中最核心的 6 个超参数可以参考这个表:
| 参数 | 推荐值 | 影响 |
|---|---|---|
| 采样率 | 16000 Hz | 低于 16k 会丢失部分声母信息 |
| 特征维度 | 80 维 fbank | 太大训练慢,太小识别率低 |
| 帧长/帧移 | 25ms / 10ms | 必须与预训练模型一致 |
| batch size | 32 | 按 GPU 显存调整 |
| 学习率峰值 | 1e-3 | 配合 warmup 使用 |
| 梯度裁剪 | 5.0 | 防 NaN 和训练发散 |
batch size 在 16 到 64 之间浮动。语音特征变长,一个 batch 里音频时长差异过大会导致计算浪费,常见做法是按时长排序后分桶。源码里如果只有DataLoader而没有bucket_sampler,建议自行加上,最多能省三成训练时间。
3.4 CTC loss 写进训练循环时的注意事项
PyTorch 的nn.CTCLoss输入参数顺序容易记反,尤其是log_probs的维度。正确写法是:
loss_fn = nn.CTCLoss(blank=0, zero_infinity=True) loss = loss_fn( log_probs, # (T, N, C) targets, # (N, S) 或 (sum(S),) input_lengths, # (N,) target_lengths, # (N,) )log_probs必须是对数概率,并排成(T, N, C)。如果模型输出是(batch, time, vocab),需要先log_probs.permute(1, 0, 2)转置。zero_infinity=True会把 loss 中的无穷值置零,避免因为一个空标签导致整批梯度变 NaN。源码里如果看到F.ctc_loss,用法一致,但要注意输入同样要转置。
4. 中文语音识别推理脚本与结果验证
4.1 从 wav 到 fbank 的预处理代码
推理阶段不能只做重采样,还要在特征层面做均值归一化。源码包里的推理脚本如果训练时做了全局 CMVN,通常会把统计量写成一个.npy或.ark文件。自己从头写推理时,最简单的是求整句特征的均值方差做归一化。短音频这样做问题不大,长音频遇到前后音量差异大时,会引入全局偏移。
import torch import torchaudio num_mels = 80 def extract_feature(wav_path): waveform, sr = torchaudio.load(wav_path) if sr != 16000: waveform = torchaudio.functional.resample(waveform, sr, 16000) feat = torchaudio.compliance.kaldi.fbank( waveform, num_mel_bins=num_mels, frame_length=25, frame_shift=10, dither=1.0, energy_floor=0.0, ) feat = (feat - feat.mean(dim=0)) / torch.sqrt(feat.var(dim=0) + 1e-9) return feat.unsqueeze(0)参数说明:dither=1.0是加一点随机噪声,模拟训练时的输入扰动,实测能改善低音量音频识别;energy_floor=0.0表示允许静音帧能量为 0,不做下限截断。如果发现多数识别错误集中在句首或句尾,重点检查这段归一化代码和训练时是否一致。训练时如果用的是整个 batch 的统计量,推理时必须改成每句话独立归一化,否则就是处理方式不一致,而不是模型问题。
4.2 贪心解码、束搜索和语言模型融合
模型输出的是每一帧的字符概率,需要转成实际中文。最简单的是贪心解码:每帧取概率最大的字符,再把相邻重复字符合并,最后去掉<blank>。
blank_id = 0 # 假设 batch=1,模型输出形状是 (1, T, C) pred = log_probs.argmax(dim=-1).squeeze(0) # (T,) hyp = [] prev = blank_id for idx in pred.tolist(): if idx == blank_id: prev = blank_id continue if idx != prev: hyp.append(vocab[idx]) prev = idx print("识别结果:", "".join(hyp))注意prev的更新规则。CTC 允许同一字符连续出现多次时编码成重复字符,但t t h h i i会解码成t h i,所以必须合并连续相同字。逻辑里用prev记录上一帧索引,只有当前帧和上一帧不同才写入结果。这个代码有个隐藏坑:如果同一个字符中间夹了一个<blank>,比如t <blank> t,按 CTC 规则是两个独立的t,hyp里会保留两个t;这符合标准解码规则,如果你觉得结果多了重复字,不要在这里改,而应检查模型的 blank 预测分布。
追求更高质量时,使用pyctcdecode把 n-gram 语言模型融合进去。安装命令是pip install pyctcdecode,代码很短:
from pyctcdecode import build_ctcdecoder decoder = build_ctcdecoder( labels=vocab, alpha=0.8, beta=1.5, lm_path="zh_ngram.arpa", ) text = decoder.decode(log_probs.numpy(), beam_width=100)alpha是语言模型权重,控制在 0.3~1.5 之间,太小声学模型占主导,太大会覆盖声学模型判断;beta是单词插入奖励,中文场景通常设 1.0 以上,否则结果会倾向短句,句子中间的“的、了、吗”容易被吞掉。beam_width设为 100 已经足够,超过 256 后 CER 提升很有限,耗时却指数上升。
4.3 用 CER 而不是 WER 衡量中文语音识别
中文词边界需要分词器,分词错误会污染错误率,所以业界常用字符错误率 CER 而不是词错误率 WER。源码包里的说明文档如果只给 WER,别急着当标准,自己算一下 CER 更稳。计算时不能把空格和标点计入分母,标点错一个算一个错,空字符串要单独保护。
import Levenshtein def char_error_rate(ref, hyp): ref = list(ref.replace(" ", "").replace("\n", "")) hyp = list(hyp.replace(" ", "").replace("\n", "")) if len(ref) == 0: return 1.0 if len(hyp) else 0.0 return Levenshtein.distance(ref, hyp) / len(ref)这个函数把删除、插入、替换统一折算成编辑距离。中文语音识别里插入错误影响最大,多出来的字会直接破坏句子语义。调整解码参数时,先看插入错误数量,不要只盯 CER 数值。结果多字多,就把beta调低;少字多,就把beta调高。
验证集要分成“室内安静”和“带噪场景”两部分。同一个模型在安静环境中 CER 3% 不代表真实场景可用,带噪环境 CER 往往翻倍。源码包里如果只给一个 test wav,说明文档价值要大打折扣,至少准备三种环境音频:普通话室内、轻度噪声、电话录音。
5. 让源码包真正可用于生产:噪声、口音、长音频的 3 个修正技巧
最后一个环节,我会把源码用到实际项目时必做的三件事列出来。这三点不复杂,但能解释为什么同一个模型在别人机器上效果好,到你的环境里效果差。
第一,训练时加 SpecAugment 而不是只在推理端滤波。SpecAugment 参数不要直接抄英文语音识别默认值,中文场景建议time_mask_num=2、time_mask_width=20、freq_mask_num=2、freq_mask_width=2;英文任务里常见的time_mask_width=0.1 * T在中文长句上会把声母位置遮没,导致拼音缺失。推理时如果噪声在高频段,可以在特征提取后把 80 维 fbank 的低频段前几维做衰减,更稳妥的做法是对原始 wav 做 200Hz 高通滤波。
第二,口音问题靠“少量数据微调 + 热词偏置”两步解决。不要指望在完整模型上继续训练就能抹平口音,常见做法是冻结 Conformer 前 4 层,只训练靠近分类头的最后几层,用 10 小时带口音数据微调 3 个 epoch。然后在解码阶段把对应的人名、地名加入pyctcdecode的hotwords参数,实测能把关键词召回率提高 5 到 10 个点,而 CER 几乎不变。
第三,长音频切分是隐藏的性能瓶颈。直接把 40 分钟会议录音送进模型,Transformer 的时间复杂度会拖垮整台机器。我会先用静音检测把音频切成 20 到 50 秒一段,每段前后保留 0.3 秒交叠,再逐段解码并拼接。切分点不能落在波形振幅最大处,否则会听到“咔哒”声,但解码本身不受影响。这里有个实用的验证方法:切分后的整句 CER 不会比不切分时差超过 0.5 个百分点,如果差值超过这个数,说明切分把成词语流截断了。
把这三个技巧处理完,zip 里的源码才算真正进入可用状态。一开始跑不动没关系,先让模型在你自己的三句话上出结果,再逐步替换成真实数据。
本文还有配套的精品资源,点击获取