☰
Python实现中文语音识别:离线可定制热词的深度学习系统详解
2026/10/27 2:36:58 网站建设 项目流程

简介:这是一套面向深度学习开发者与语音技术初学者的中文语音识别系统实战源码,基于Python实现端到端ASR建模,解决汉语语音转文本的核心任务,适用于智能客服、会议记录、方言适配等实际场景。资源包共65个文件,含26个核心Python模块(覆盖声学模型定义、语言模型构建、训练/评估/推理全流程)、13个文本配置与语料列表文件、10个备份文件(.zbak),以及Dockerfile、Proto协议定义、README文档和模型配置JSON等,整体压缩后仅6.51MB,结构清晰、模块解耦,便于理解各组件职责并快速二次开发。已有52人学习下载,代码采用Keras与PyTorch双后端支持,集成梅尔频谱特征提取、CNN-LSTM混合声学建模、注意力机制语言模型及维特比解码,附带ST-CMD、THCHS-30等主流中文数据集加载脚本与预处理工具,开箱即可运行训练与实时语音识别任务。 做中文语音识别,最容易被低估的不是模型有多深,而是整条流水线有多碎。我一开始也是直接调现成API,直到有个项目要求在内网离线环境部署、还要能随时加自定义热词,才不得不自己从零搭一套基于Python的深度学习识别系统。前前后后折腾了小半年,源码、训练脚本、解码逻辑全在里面。如果你正打算干同样的事,这篇把我踩过的坑、验证过有效的方案,以及最后沉淀下来的代码组织方式,一次说清楚。

1. 动手写语音识别之前,先想清楚这几个前提

1.1 为什么不直接用现成的语音识别API

先说结论:能直接调API就调,这不是什么丢人的事。但当你遇到这三个需求时,API方案基本就没法用了:

  • 离线部署:很多应用现场不允许数据出内网,尤其涉及企业会议记录、医疗问答、工业语音指令这些场景,音频是敏感数据,不可能传到云端。
  • 可定制词表:通用API对"磐石"、"Stable Diffusion"、"甲卡西酮"这种人名、品牌、专业术语经常识别成同音错字,你没法给云端模型注入词表。
  • 长期成本:语音识别一旦成为核心功能的调用大头,按分钟计费很快会吃掉利润。自家有GPU服务器的话,自己训的模型单次推理成本几乎可以忽略。

当然,自己搞的代价是:数据要自己找、模型要自己调、坑要自己填。但换来的是整个系统的可控性和可扩展性。我这套源码的定位就是这样——一套离线可跑、模块解耦、方便改词表和模型结构的完整中文语音识别训练与推理系统。

1.2 中文识别难在哪:难的不是"听",是"选"

中文语音识别和英文有个本质区别。英文的发音单元(音素)可以直接映射到字母串,而中文的音节和汉字是多对多的关系。普通话里,无调音节大概400多个,带上声调也就1300多个,但常用汉字是3000多个。这意味着:

  • 声学模型即使完美地识别出"zhong1 guo2"这个拼音串,也还是无法确定到底是"中国"还是"种过"。
  • 汉字级别的语言模型成了绕不开的一环,它要负责从拼音流里选出最合理的字序列。
  • 中文里数字、英文、口语语气词("嗯"、"那个")大量混入,如果建模单元和词表设计得不好,这几类内容会互相干扰。

所以我在设计系统时,把声学模型和语言模型拆成了独立的两个部分——声学模型只负责把音频变成带声调的拼音序列,语言模型负责把拼音序列变成有意义的汉字序列。这个拆分让整个系统好维护得多:你想提升热词命中率,改语言模型就行;你想换口音适应能力,重新训练声学模型就行,两边互不影响。

1.3 这套源码的整体架构与代码目录规划

我的实现完全是Python写的,深度学习框架用PyTorch。训练和推理链路包含以下五个模块:

模块职责对应目录
音频前端采样、分帧、提取Log-Mel特征、归一化audio/
声学模型CNN + BiGRU + CTC,输出拼音概率model/
训练调度数据加载、增强、学习率调度、训练循环train/
解码器拼音序列生成(贪心/Beam Search)、汉字转换decode/
工具集词表管理、音频切分、指标计算、模型导出utils/

这是一个非常典型的"数据-模型-解码"三段式结构。我强烈建议你在动手写代码之前,先把目录结构和接口定义好,因为语音识别的代码最忌讳揉成一团——特征提取、模型训练、解码分属不同的优化周期,耦合在一起后面会非常痛苦。

2. 音频链路:从麦克风信号到特征矩阵,每一步都是经验值

2.1 采样率、分帧加窗这些参数,为什么要这样定

音频是波形信号,深度学习模型不能直接吃波形,得先把一长串采样点切成有重叠的短帧,再转成频谱特征。核心参数有这么几个:

  • 采样率:我用16kHz。人声有效频谱范围其实超过8kHz,但语音识别对4kHz以上的信息依赖很小,16kHz是业界的标准选择,几乎所有开源语音数据集都是这个采样率。如果遇到22.05kHz或48kHz的音频,要先重采样。
  • 帧长25ms,帧移10ms:25ms的窗能保证足够的频率分辨率,10ms的移动保证相邻帧有重叠,不至于丢失音节间的过渡信息。1秒音频大约产生100帧。
  • 窗函数:用汉明窗。不干啥花活,就是为了减少频谱泄漏。
  • n_fft = 512:对应16kHz下约31.25Hz的频率分辨率,够用,分辨率再高就是浪费计算量。

这套参数是从语音识别几十年的工程经验里沉淀下来的,不要随便改。我见过有人觉得帧移小一点信息更全,改成5ms,结果训练时间翻倍,效果几乎没提升,纯粹浪费GPU。

2.2 特征选择:为什么是Log-Mel而不是MFCC

早期语音识别喜欢用MFCC(梅尔倒谱系数),因为它在传统的GMM-HMM框架下能去冗余、抗噪声。但到了深度学习时代,MFCC的倒谱变换反而会丢掉一些对模型有用的细节信息。

我这里直接用80维Log-Mel特征:

  • Mel刻度是一种模拟人耳听觉的非线性频率刻度,低频分辨细、高频分辨粗,这一步是必须的。
  • 取log是为了压缩动态范围,让特征分布更接近高斯,利于网络训练。
  • 80维是如今主流ASR系统公认的甜点值,40维稍嫌信息不足,128维提升有限但计算量上涨明显。

有一点要注意:Mel特征提取之后一定要做均值方差归一化。因为不同录音设备、不同录音距离导致的音量差异非常大,归一化可以抹平这种全局差异。我在系统里默认对每个音频文件单独做归一化,这和图像领域的per-image归一化一个道理。

2.3 特征提取代码:一个可以直接落地的实现

下面这段是我在audio/feature.py里特征提取的简化版,实测可以直接用:

import torch import torchaudio def extract_log_mel(wav_path: str, sample_rate: int = 16000, n_mels: int = 80, n_fft: int = 512, hop_length: int = 160, win_length: int = 512) -> torch.Tensor: # 1. 读取音频 waveform, sr = torchaudio.load(wav_path) # 2. 统一采样率:遇到44.1kHz、22.05kHz的录音一律重采样到16k if sr != sample_rate: resampler = torchaudio.transforms.Resample(sr, sample_rate) waveform = resampler(waveform) # 3. 预加重:补偿高频能量衰减,让辅音更清楚 # y[n] = x[n] - 0.97 * x[n-1] waveform = torch.cat( (waveform[:, :1], waveform[:, 1:] - 0.97 * waveform[:, :-1]), dim=1 ) # 4. 提取Log-Mel mel_transform = torchaudio.transforms.MelSpectrogram( sample_rate=sample_rate, n_fft=n_fft, hop_length=hop_length, win_length=win_length, n_mels=n_mels, window_fn=torch.hamming_window, power=2.0, ) mel = mel_transform(waveform) # [batch, n_mels, time] log_mel = torch.log(mel + 1e-8) # 加极小值防log(0) # 5. 均值方差归一化 log_mel = (log_mel - log_mel.mean()) / (log_mel.std() + 1e-8) # 转成 [time, n_mels],方便后续按时间维度操作 return log_mel.squeeze(0).transpose(0, 1)

这段代码里最容易踩坑的就两个点,我说一下:

  • 预加重必须放在分帧特征之前。我犯过把预加重放在波形读取后、归一化之前的错误,导致波形数值被压缩,特征波动几乎消失,模型一直训不上去。
  • log(mel + 1e-8)这个1e-8很小,但必须有。Mel频谱在静音段会出现0值,直接取log会得到负无穷,网络就学崩了。

3. 声学模型设计:CNN + BiGRU + CTC 的组合逻辑

3.1 建模单元:中文为什么用"带声调的拼音"做输出

这是整个系统最关键的设计决策之一。可选方案有四种:

建模单元优点缺点
汉字直接建模端到端,不需要拼音转字汉字类别3000+,训练数据需求大,易混淆
音素建模类别少,跨语言迁移好需要音素标注,中文弱音节现象严重
无调拼音建模类别少,易于训练同音字问题被放大,语言模型压力大
带调拼音建模区分度好,同音字问题部分缓解类别数中等,约1300个,需要声调标注

我最终选了带调拼音建模。1300多个类别对分类器来说完全不是问题,而且声调信息能让"妈"和"马"这些声调不同的字从声学上就被区分开,减轻了后面语言模型的选字压力。这个设计和现在很多开源中文ASR系统的选择一致,是一条被验证过的成熟路线。

3.2 网络结构逐层拆解:CNN怎么用、BiGRU怎么堆

声学模型的输入是[batch, time, 80]的特征序列,输出是对每个时间帧预测的拼音概率。我用的结构大致如下:

import torch.nn as nn class ConvBiGRUASR(nn.Module): def __init__(self, n_mels=80, hidden_size=256, num_layers=4, n_classes=2186): super().__init__() # 第一段:2层CNN,时间维降采样4倍 self.conv = nn.Sequential( nn.Conv2d(1, 32, kernel_size=3, padding=1), nn.BatchNorm2d(32), nn.ReLU(), nn.MaxPool2d(kernel_size=2, stride=2), # time / 2 nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(), nn.MaxPool2d(kernel_size=2, stride=2), # time / 4 ) # 第二段:4层双向GRU,捕捉上下文 self.gru = nn.GRU( input_size=64 * (n_mels // 4), hidden_size=hidden_size, num_layers=num_layers, batch_first=True, bidirectional=True, dropout=0.3, ) # 输出层:映射到拼音类别数 self.ctc_out = nn.Linear(hidden_size * 2, n_classes) def forward(self, x): # x: [batch, time, n_mels] x = x.unsqueeze(1) # [batch, 1, time, n_mels] x = self.conv(x) # [batch, 64, time/4, n_mels/4] x = x.permute(0, 2, 1, 3).contiguous() b, t, c, f = x.shape x = x.view(b, t, c * f) # 展平频率维 x, _ = self.gru(x) # [batch, time/4, hidden*2] return self.ctc_out(x)

拆开说:

  • 为什么用CNN开头:音频特征相邻帧之间有很强的局部相关性,CNN的局部感受野正好能提取这些局部声学模式,同时通过池化把时间维降下来,让后面的RNN在更粗的时间尺度上建模,训练速度更快。这里降采样4倍,意味着740毫秒的音频输入(74帧),输出是18帧的概率序列,CTC就是要在这18帧上做对齐。
  • 为什么用BiGRU而不是LSTM或Transformer:双向GRU对一个音节的前后文都能感知,参数量比LSTM小、训练速度更快,对中文这种语句内强依赖的建模非常合适。Transformer虽然效果上限更高,但对数据量和训练技巧要求也高,我在这个项目里选择了更稳妥的BiGRU。后续如果数据量上来,可以考虑换成Conformer。
  • 为什么不直接上当前最流行的Conformer:Conformer确实在开源基准上表现更好,但它需要更多训练数据、更长训练时间,而且对学习率极其敏感。对于想在有限数据上快速跑通、看完整条链路的人来说,CNN+BiGRU是一个性价比极高的起点。

3.3 CTC损失函数:它是怎么解决"帧和字对不齐"这个难题的

这个点值得花点篇幅讲清楚,因为它是语音识别训练最核心的魔法。

训练时,音频有N帧,文本标注只有M个字,N和M通常不相等,而且你不知道哪几帧对应哪个字。这种"对齐"问题以前用HMM的隐状态来建模,非常复杂。CTC的思路很巧妙:引入一个特殊的blank符号(我用类别0),允许输出序列中出现blank,然后通过动态规划求出所有可能的对齐方式的概率之和。

比如标注是"中 国"两个字,而输入有4帧,CTC允许的对齐路径可能是:

  • blank, 中, blank, 国
  • 中, 中, 国, blank
  • 中, blank, 国, 国

这些路径最终经过"连续相同帧合并、blank删除"之后都能还原成"中 国"。CTC在训练时对所有这些合法路径的概率求和作为目标函数,通过前向后向算法高效计算。这样就不需要人工标注每一帧的标签了。

在代码里使用F.ctc_loss时,最让人头疼的是序列长度要对应上。注意两点:

log_probs = F.log_softmax(logits, dim=-1).transpose(0, 1) # [time, batch, n_classes] loss = F.ctc_loss( log_probs, target, # 拼音类别索引序列,shape [batch, target_len] input_lengths, # 每个样本的帧数,注意CNN降采样4倍 -> 输入帧数//4 target_lengths, # 每个样本的拼音长度 blank=0, )

input_lengths不是音频原始帧数,而是CNN输出后的帧数,也就是原始帧数除以4。我第一次训练时没除以这个数,直接报了"预期长度超出实际长度"的错。这是CTC训练第一号新手杀手。

4. 训练阶段最容易忽略的几件事

4.1 数据集选择、清洗和标注校验

模型效果一大半取决于数据。我用的是公开的中文普通话语料加自己录制的一批领域数据。公开数据集(如AISHELL-1、THCHS-30这些社区里常见的)质量相对干净,但总时长有限,且朗读式语音和真实对话场景差异很大。

数据清洗比数据量还重要。我遇到最离谱的一次,是某个数据集的标注里混了大量"嗯"、"啊"、"那个"这类口语词,模型训练时为了拟合这些词,把大量声学资源浪费在语气词上,导致正式词汇的识别精度下降。我的处理办法是:

  • 统计标注文本的词频,把出现频率异常高的感叹词和语气词单独抽出来检查。
  • 写脚本做标签和音频长度的估算校验:如果标注文本按正常语速估计的时长和音频时长差距超过1.5倍,直接标为可疑样本。
  • 对明显有口音或录音噪声大的样本,宁可丢掉也不要硬塞进训练集。

4.2 数据增强三板斧:速度扰动、加噪、SpecAugment

语音数据的增强不像图像那么花哨,但效果却立竿见影。我用了这三种:

  • 速度扰动:把同一段音频以0.9倍、1.0倍、1.1倍速度重采样,相当于一次数据扩了三倍。这是成本最低、效果最稳定的增强方式。
  • 背景加噪:给干净音频加上随机的环境噪声(白噪声、咖啡馆噪声、机器噪声),信噪比在0到15dB之间随机选。这能让模型在真实嘈杂环境中不至于一下就崩。
  • SpecAugment:在Mel频谱图上随机屏蔽几段时间和几个频率带。这是一种很巧妙的策略——强制模型在部分输入缺失的情况下照样能识别,相当于让人在听不清个别音的情况下靠上下文猜出来,能显著提升泛化能力。

注意增强要有个度。我见过有人把噪声加得过猛,结果模型在安静环境下识别率还下降了。我一般控制加噪增强样本的比例在30%左右,速度扰动和SpecAugment全程使用。

4.3 学习率策略、梯度裁剪和batch组织

语音识别模型对学习率很敏感。我采用warmup + 指数衰减方案:

  • 前2000步warmup,学习率从1e-6线性升到1e-3。
  • 之后每1000步按照指数衰减,衰减系数0.95。
  • 优化器用Adam,梯度裁剪阈值5.0。

为什么需要warmup?模型初始权重是随机的,如果一开始就用大学习率,RNN很容易产生梯度爆炸,导致训练loss直接变成NaN。做过NLP的都知道,这是Transformer和RNN家族训练的标准操作。

gradient clipping这个细节也容易被忽略,不裁剪的后果就是偶尔几个异常batch让整个模型参数飞掉,训练进度回到解放前。加上之后,训练稳定得多,几乎不用担心loss突然变成NaN的问题。

另一个影响训练效率的点是batch组织。音频长度差异极大,如果直接把不同长度的样本打成batch,短样本会被大量padding,计算浪费非常严重。我的做法是:

  • 训练前按音频帧数从小到大排序,每个batch只包含长度相近的样本。
  • 这样能把padding比例控制在20%以内,训练速度提升接近两倍。
  • 同时还能减少CTC对齐时的无效计算,属于零成本优化。

4.4 Checkpoint管理:别按loss保存模型,按CER来

这是我想特别强调的一点。很多初学者习惯每个epoch保存一个模型,最后选loss最小的那个。但loss最小不代表识别效果最好,因为loss里包含的很多冗余信息,和最终你关心的字错误率(CER)并不严格相关。

我的做法是:

  • 每个epoch在验证集上跑一次完整解码,计算拼音错误率和字错率。
  • 只保存验证集CER最低的模型,同时保留最近两个epoch的模型作为回退。
  • 每隔5个epoch保存一次全量checkpoint,防止训练中断。

验证集一定要选和训练集完全不重叠的数据,而且最好包含录音条件差异较大的样本。否则你会在验证集上看到虚高的精度,一上真实场景就现原形。

5. 解码与后处理:从声学模型输出到最终中文句子

5.1 贪心解码和Beam Search:原理、取舍和代码

声学模型输出的每个时间帧是所有拼音类别的概率分布。最简单的解码方式是贪心解码:每帧取最大概率的类别,然后把重复的相邻同类合并、删掉blank,得到拼音串。

贪心解码的问题很明显:你只选当前帧的最优,没有考虑整句的连贯性。某个拼音虽然单帧概率最高,但连起来可能是毫无意义的组合。这时候需要Beam Search——在每个时间步保留概率最高的K条候选路径(我一般K=10),逐步扩展,最后保留整句得分最高的那条路径。

Beam Search的加分不只来自声学模型,还可以把语言模型的得分融合进来。我用的打分公式是:

total_score = acoustic_score + alpha * lm_score + beta * word_count
  • acoustic_score是声学模型的CTC log概率。
  • lm_score是语言模型给出的序列概率,用来惩罚不合理的字组合。
  • alpha和beta是权重,需要在验证集上调,我最终用的alpha=0.6,beta=1.0。
  • word_count用来奖励更长的输出,防止模型倾向于输出短序列。

解码速度方面,Beam Search比贪心慢一些,但对中文这种同音字问题突出的场景,增益非常明显。我做了个粗略对比,贪心解码的CER大概在20%左右,加上Beam Search和语言模型后能降到14%上下,这个差距在真实应用中是决定性的。

5.2 拼音到汉字的转换:语言模型的核心作用

Beam Search输出的是拼音序列,比如"ni3 hao3 shi4 jie4",你要把它转成"你好世界"。这一步如果不做,光靠声学模型是解决不了同音字问题的。

我采用的方案是基于统计语言模型的维特比解码。核心思路是:把拼音转汉字的问题看作一个隐马尔可夫链——拼音是观测变量,汉字是隐状态——然后通过语言模型计算汉字序列的转移概率,用动态规划找到最可能的汉字序列。

我的具体实现简化版本:

def pinyin_to_chars(pinyin_seq, lm, char_table): # 每个拼音对应的候选汉字列表(从词典查) candidates = [lookup_cands(p, char_table) for p in pinyin_seq] # 动态规划:dp[i][c] = 处理到第i个拼音、当前字为c的最大概率 dp = [{} for _ in range(len(candidates))] back = [{} for _ in range(len(candidates))] for c in candidates[0]: dp[0][c] = math.log(char_table.start_prob(c)) for i in range(1, len(candidates)): for cur_c in candidates[i]: best_score = -float('inf') best_prev_c = None for prev_c in candidates[i-1]: score = dp[i-1][prev_c] + lm.bigram_prob(prev_c, cur_c) if score > best_score: best_score = score best_prev_c = prev_c dp[i][cur_c] = best_score back[i][cur_c] = best_prev_c # 回溯得到最优汉字序列 ...

实际工程中,我会用trigram或更高阶的语言模型,因为汉字的组合关系更多体现在三字词和四字成语里,只靠bigram会丢失"人工智能"这种常见搭配信息。如果你有精力,可以尝试更大的n-gram模型,效果会更明显。

5.3 标点恢复、数字和英文的规整处理

模型输出往往是裸拼音串,你要让它变成人读着舒服的文本,还得做后处理。我总结了三件必须做的事:

  • 数字规整:训练数据里如果"2023年"标注成了"二零二三年",模型就只会输出后者。我写了一个规整模块,把中文数字转为阿拉伯数字,但要注意"九八五"这种序列到底是"985"还是"九八五",需要上下文判断。最简单可靠的方案是维护一个正则规则库,配合常见的数字表达模式来做。
  • 英文识别:中文语音里混英文单词很常见。我采用了中英混合词表——把常见英文单词直接作为单独的类别加进输出层,让模型可以输出"Python"而不需要经过拼音"pai1 san1"再转回来。这比后期正则变换可靠得多。
  • 标点恢复:声学模型天然不输出标点。我用了另一个轻量级的BERT模型做标点预测,输入是无标点文本,输出加上逗号、句号、问号。这一块依赖了不少外部预训练能力,如果你对标点不敏感,可以暂时不做。

6. 训练过程中遇到的典型问题和完整排查记录

6.1 训练到一半loss突然变成NaN

这个坑我印象太深了。当时模型训练了两万步,loss稳步下降,突然某一步直接变成NaN,整轮训练报废。

排查过程是这样的:

  • 第一反应是学习率太大,但调小之后问题依旧。
  • 接着怀疑是数据里有异常样本。打印出触发NaN的那个batch,发现里面有条音频是静音段特别长的,特征归一化后出现大量0值。
  • 进一步检查,发现是预处理阶段对整段静音做均值方差归一化,导致一个极小的std被除成了一个巨大的数,梯度瞬间爆炸。

最终修复:在特征提取时对纯静音帧做一次能量阈值过滤,如果帧能量小于阈值就直接丢弃;另外在训练循环里加上梯度值检测,发现NaN就跳过当前batch,并打印出样本ID。这个防御性编程挽救了后面无数次训练。

6.2 训练不收敛,loss始终降不下去

还有一次,换了新数据集之后,loss在2.0附近怎么都降不动。首先检查了数据标注和特征提取,都没问题,模型结构也没动过。

后来仔细对比才发现是新数据集的采样率不是16kHz,是22.05kHz,而我的特征提取脚本里虽然写了重采样逻辑,但因为resampler定义在函数外部,只在第一次调用时初始化,导致后续所有音频都按第一次的采样率处理,特征和之前完全对不上。这个bug特别隐蔽,因为程序不会报错,只是默默做错事。

从那之后我把所有数据处理逻辑都写成了纯函数,每个样本都独立初始化变换器,并加了采样率断言,采样率不是16kHz就报错而不是静默转换。有时候多报点错,反而比静默容忍更省时间。

6.3 音频太长,显存直接爆掉

语音识别模型的输入是时序数据,音频越长,中间的特征和隐状态占用的显存就线性增长。一条5分钟的长音频直接喂进去,16G显存根本不够看。

我的做法是训练时用短的音频片段(10秒以内),推理时对长音频做分段处理,用VAD(语音活动检测)把长音频切成句子级别的片段,再对每个片段单独识别,最后合并结果。这里有个细节:切分时要在有语音的边界切,不要在静音中硬切,否则会切断词的发音,导致识别语义断裂。

VAD切分的阈值要调好。阈值太灵敏会把"嗯"这类语气词也切出来,阈值太松又会把两句话粘在一起。我最终用了一个带能量和过零率联合判断的VAD逻辑,并在切分点前后各保留0.2秒的上下文,避免切到音节边缘。

6.4 推理时模型表现出乎意料

训练指标很好,CER在验证集上只有8%,结果一上真实环境,识别率惨不忍睹。复盘下来原因是验证集的录音条件太干净,而真实场景有环境噪声、回声、多人说话。

这个问题没有银弹,只能靠数据增强和更多适配场景的数据来缓解。我在真实部署后做了一件事:持续收集bad case,把识别错的音频样本导出来,分析错误类型——是噪声问题、口音问题还是同音字问题——然后针对性地调整增强策略或语言模型权重。语音识别系统不是一个训完就完事的项目,它需要长期的数据闭环迭代。

7. 效果评估、源码使用建议和后续优化方向

7.1 怎么评估一套模型的好坏

语音识别有三个常用指标,我在项目里都实现了:

  • CER(字错误率):识别出的汉字序列和标准标注相比,插入、删除、替换的错误总字数除以标注总字数。这是我最看重的指标,中文场景下用它。
  • WER(词错误率):同上但按词计算,中文分词后才有意义。
  • 拼音错误率:用于单独评估声学模型,因为没做汉字转换,能看到声学侧的真实问题。

举个例子,标注是"今天天气很好",识别成"今天天气真好",替换了一个字,标注4个字(今/天/天/气/很/好其实6个),CER就是1/6。评估脚本需要在解码后做文本归一化,把全角半角、数字格式统一后再比对,否则指标会虚高或虚低。

7.2 源码的完整使用路径

如果你拿到了这套源码(或参考我的结构重写),使用路径是这样的:

  1. 安装依赖:Python 3.10 + PyTorch 2.0 + torchaudio + librosa + numpy。
  2. 准备数据集:按data/下的格式组织,每个样本一个wav文件加一个txt标注文件。
  3. 修改configs/config.yaml里的数据路径、batch size、学习率、词表路径。
  4. 运行python train/train.py启动训练。
  5. 训练完成后运行python decode/infer.py --wav_path xxx.wav --checkpoint xxx.pt进行单条音频识别。
  6. 使用utils/eval_cer.py在测试集上计算CER。

每一步都有单独的脚本和明确的输入输出接口,新手照着跑一遍就能理解整套系统的数据流。

7.3 后续可以怎么做:热词增强、流式识别、模型结构升级

跑通这套系统只是开始,我整理了三个明确可复用的优化方向:

  • 热词增强:给Beam Search加"热词"选项,把用户自定义词表里的词在解码时额外加分。我实现了一个简单的热词干涉——如果最佳候选路径里包含某个热词,就给它额外的分数加成。这个功能在会议场景下尤其好用,人名、产品名命中率大幅提升。
  • 模型结构升级:数据量上来之后,可以把BiGRU替换成Conformer,或者改用RNN-T损失来实现流式识别。流式识别的核心是让模型在说话过程中实时输出,不能等整句说完。这需要改变训练的掩码策略,不是简单替换模型就能搞定的。我目前在做这部分的改造。
  • 说话人自适应:如果系统只在特定的几个人之间使用,可以采集这几个人的少量语音做微调,模型效果会有一个非常明显的跃升。这就涉及增量训练和模型融合的问题。

我自己的体会是,语音识别这一行最忌讳"想一步到位"的心态。先把一套链路跑通、把基本功练扎实,比什么模型都重要。你手里这套Python实现的中文语音识别源码,价值不在于它用了多fancy的模型,而在于它把音频处理、声学建模、解码后处理这条完整的链路给你铺平了。拿着它,你已经比那些只会调API的人走远了一大截。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询