尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

中文语音识别系统实战:FBank特征、CTC解码与语言模型优化

发布时间:2026/9/23 19:21:42

资讯中心
01
ARTICLE

中文语音识别系统实战:FBank特征、CTC解码与语言模型优化

中文语音识别系统实战:FBank特征、CTC解码与语言模型优化
简介这是一份基于深度学习的中文语音识别系统实现面向具备Python与神经网络基础的开发者覆盖了声学模型与语言模型两大核心模块。声学模型部分提供了多种网络结构实现包括循环神经网络与卷积神经网络的CTC变体以及基于端到端CNN框架的设计语言模型则引入了CBHG结构便于对比不同网络在中文语音识别任务上的表现。资源共87个文件以Python源码、说明文档、数据列表文件为主同时包含CNN与CTC的示例教程压缩包约32MB目录划分清晰可以直接定位到对应模型代码。通过源码和说明文档可以掌握中文语音识别的完整流程包括声学特征处理、模型训练与结果调优思路适合作为学习和二次开发的参考。目前已有535人学习下载值得语音识别入门者实践参考。1. 中文语音识别难在哪序列对齐与声调建模决定系统天花板拿到这套 Python 实现的中文语音识别系统源码时我第一反应不是看模型结构而是先去找vocab.txt、训练脚本和声音特征处理这三样东西。原因很简单中文录音转文字这类任务真正决定能不能交付的是序列对齐稳不稳、多音字和同音词被语言模型救回来多少而不是单纯把网络层数堆高。用深度学习做中文语音识别工程上一般会拆成四段音频特征处理、声学模型训练、CTC 解码、外部语言模型重评分这套系统源码刚好把四步串成了一条可复现的流水线。适合做会议纪要、呼叫中心质检、字幕生成的工程师参考。对已经写了三五年代码的熟手来说最值得读的是解码配置和训练时的数据增强参数那才是一个系统是否经过真实业务打磨的痕迹。2. 声学特征与数据管线FBank、CMVN 和过拟合的源头2.1 为什么选 80 维 FBank 而不是 MFCC特征模块没有特别花哨的地方但如果第一步做错后面模型再复杂也只能学到噪声。常见的中文语音识别系统里输入不是把 16k WAV 直接扔进网络而是先切成 25ms 一帧、10ms 一移的短时窗再做 FBank 或 MFCC。源码默认走的是 FBank CMVN SpecAugment 这条路线原因是中文的声调信息主要由低频基频承载而 FBank 保留的能量分布更适合深度网络自己做非线性组合。特征保留内容适合模型使用场景MFCC离散余弦变换压缩后的静态系数GMM-HMM、轻量 DNN低算力设备、噪声强相关FBankMel 滤波器组的对数能量CNN、Transformer 等深层模型ASR 主流程搭配 CMVN原始波形未压缩的音频采样自监督预训练模型数据量大、需要长时上下文工程上我一般用 80 维 FBank而不是常见的 40 维。16k 采样率下80 维 Mel 能覆盖到 7.6kHz 左右中文里的 zh/ch/sh、z/c/s 这类声母高频能量比较集中降到 40 维后这些辅音容易被混成同一个状态最终 CER 会闷声涨上去。特征提取可以直接用librosa也可以换成torchaudio的在线特征层区别只是计算速度和是否支持 batch 并行。import numpy as np import librosa SAMPLE_RATE 16000 def build_fbank(wav_path: str, n_mels: int 80) - np.ndarray: wav, _ librosa.load(wav_path, srSAMPLE_RATE) mel librosa.feature.melspectrogram( ywav, srSAMPLE_RATE, n_fft400, hop_length160, n_melsn_mels, fmin0, fmax7600, power2.0, ) log_mel np.log(mel 1e-6) return log_mel.T # [T, n_mels]这段代码里的n_fft400对应 25ms 窗长hop_length160对应 10ms 帧移。fmax7600是特意留出来的因为电话信道的有效带宽一般到 3400Hz但麦克风采集的会议录音高频信息远不止这些如果源头数据是大量 VAD 切碎的短句保留更高频段会让声母部分更容易学。返回的矩阵形状是[T, 80]T 等于音频总帧数后续送入 CNN 或 Transformer 时不需要再手工展开成二维图。2.2 CMVN 按说话人统计避免把信道偏差学进网络深度学习声学模型对输入特征的均值和方差很敏感。同一个汉字会议室麦克风和手机录音在频谱上的整体偏移完全不同。如果直接把每句话单独做标准化会把不同语速、不同响度变成伪特征模型学到的可能是“谁在说话”而不是“说了什么”。源码里做 CMVN 时应该按说话人聚合统计而不是按文件逐个计算。尤其是多说话人会议数据同一段音频里说话人切换频繁局部归一化反而会破坏声道稳定性。常见做法是在训练前先扫描训练集对每个speaker_id累加一阶矩和二阶矩然后统一计算全局均值与方差。def apply_cmvn(feat: np.ndarray, speaker_stats: dict) - np.ndarray: mean speaker_stats[mean] std speaker_stats[std] return (feat - mean) / (std 1e-10) # 使用示例 spk { mean: np.zeros(80), std: np.ones(80), } fbank_feat build_fbank(test.wav) normed apply_cmvn(fbank_feat, spk)这里std加1e-10是防止静音段特征方差为 0 时产生除零。实际跑训练时不要把 CMVN 放进每个 batch 内计算因为一个 batch 里往往混杂多个说话人先离线算好整份特征再在数据加载器里读normed.npy能省去大量重复 IO。2.3 SpecAugment 与速度扰动是少样本场景最稳的正则化中文语音识别系统最常见的过拟合信号是训练集 CER 很低、验证集 CER 很高。语音领域的通用解法是 SpecAugment在时间维和频率维随机遮挡一部分区域强迫模型不依赖固定频段和固定时长。def apply_spec_augment(feat: np.ndarray, freq_mask10, time_mask20): T, F feat.shape f0 int(np.random.uniform(0, freq_mask)) feat[:, f0:f0 freq_mask] 0 t0 int(np.random.uniform(0, time_mask)) if t0 time_mask T: feat[t0:t0 time_mask, :] 0 return feat频率掩码宽度建议从 10 开始调时间掩码从 20 开始调。掩码太大会把整个声母段抹掉模型只能靠语言习惯猜反而让训练不稳定。除了 SpecAugment还要加语速扰动常见做法是把 WAV 重采样到 0.9 到 1.1 倍速后再提取特征。这个操作等价于让模型见过更多音素时长分布对中文语速变化带来的吞音问题有实际帮助。3. 深度神经网络声学模型从 CNN 到 Transformer 的混合编码3.1 卷积前端做时间降采样Transformer 不擅长处理 800 帧长序列中文语音识别里10 秒音频在 10ms 帧移下会产生 1000 帧特征。如果直接把 1000 帧序列送进 6 层 Transformer显存占用和自注意力计算量都会快速膨胀。源码里的做法通常不是用完整 Conformer而是先用两层卷积把时间维降到原来的四分之一再把压缩后的序列送进 Transformer Encoder。这套设计有两个好处第一CNN 局部感受野能比较好地建模声母与韵母之间的短时依赖第二向量化后的序列长度只有原来的 25%Transformer 可以堆到更深而不爆显存。工程上想要更快收敛可以把第一层卷积的步长设成(2, 2)同时在频率维也做一次降采样。假设输入特征是 80 维 FBank经过两层频率维减半后展平后的特征维度是64 * 20这个值要跟后续d_model匹配。3.2 PyTorch 模型骨架与 CTC 损失配置搭建这类声学模型不需要很复杂的代码核心是把卷积前端、位置编码、Transformer Encoder 和 CTC 输出层接好。标签索引从 1 开始0固定留给 CTC 的 blank。import torch import torch.nn as nn class ConvTransformerASR(nn.Module): def __init__(self, feat_dim80, vocab_size4333, d_model256, nhead8, num_layers6, dropout0.1): super().__init__() self.d_model d_model self.conv1 nn.Conv2d(1, 32, kernel_size3, stride2, padding1) self.conv2 nn.Conv2d(32, 32, kernel_size3, stride2, padding1) self.conv3 nn.Conv2d(32, 64, kernel_size3, stride1, padding1) self.bn nn.BatchNorm2d(64) self.act nn.ReLU() self.proj nn.Linear(64 * (feat_dim // 4), d_model) self.ln nn.LayerNorm(d_model) encoder_layer nn.TransformerEncoderLayer( d_modeld_model, nheadnhead, dim_feedforward2048, dropoutdropout, batch_firstTrue ) self.encoder nn.TransformerEncoder(encoder_layer, num_layersnum_layers) self.ctc_linear nn.Linear(d_model, vocab_size) def _conv_lens(self, seq_lens: torch.Tensor) - torch.Tensor: x (seq_lens 1) // 2 x (x 1) // 2 return x def forward(self, feats: torch.Tensor, seq_lens: torch.Tensor): # feats: [B, T, feat_dim] x feats.unsqueeze(1) # [B, 1, T, F] x self.act(self.bn(self.conv1(x))) x self.act(self.bn(self.conv2(x))) x self.act(self.bn(self.conv3(x))) b, c, t, f x.shape x x.permute(0, 2, 3, 1).reshape(b, t, -1) x self.proj(x) x self.ln(x) new_lens self._conv_lens(seq_lens) pad_mask torch.arange(t, deviceseq_lens.device).unsqueeze(0) new_lens.unsqueeze(1) x self.encoder(x, src_key_padding_maskpad_mask) logits self.ctc_linear(x) log_probs logits.log_softmax(-1) return log_probs, new_lens训练时的 CTC loss 写法如下criterion nn.CTCLoss(blank0, zero_infinityTrue) log_probs, out_lens model(feats, feat_lens) loss criterion(log_probs.transpose(0, 1), target_ids, out_lens, target_lens) loss.backward()CTCLoss的第一个参数要求是[T, B, V]所以需要把模型输出从 batch_first 转回来。zero_infinityTrue很重要它会把 padding 导致的无穷 loss 置零否则一批里只要有一条过短音频整个训练就容易南辕北辙。feat_dim // 4表示频率维经过两层 stride2 卷积后的展平维度换用 40 维 FBank 时这里要改成64 * 10不然 linear 层维度对不上。3.3 标签单元纯汉字、带调拼音还是混合标签单元词汇量优点缺点汉字4000-6000解码直接得到中文工程最简单多音字依赖语言模型纠正带调拼音1300 左右声调信息显式建模音近错误少汉字还原需要额外映射模型汉字拼音混合9000可同时利用字和音上下文词典和实现复杂度高源码里如果提供的是vocab.txt大概率是纯汉字加上常见标点符号。纯汉字单元对短句质检项目足够用如果业务里大量出现生僻人名和地名则建议在词表里补充热词并重新训练而不是在解码后再做字符串替换。想要兼顾声调信息可以在输入侧额外拼接一个基频特征但不要轻易把输出单元改成带调拼音因为拼音到汉字的转换会引入第二层错误。4. CTC 解码与中文语言模型重评分让输出不再“啥都像”4.1 为什么单独 CTC 贪心输出不满足中文业务交付CTC 贪心解码只是把每一帧概率最大的标签拼起来再做 blank 去重。这种输出对纯字符模型来说经常出现“服务气”“对不起请稍候”这类音近字错误因为声学模型只学习了声学特征到单个汉字的映射没有足够强的语言先验。中文里“服务器”“服务期”“服务气”发音完全相同只有靠语言模型或热词表才能选出真正符合句义的词。所以成熟的中文语音识别系统都不会只跑贪心搜索而是在解码阶段使用 CTC beam search并引入外部 n-gram 语言模型对候选路径重打分。重评分阶段的输入不再是单条文本而是多条带声学分数和语言模型分数的候选序列最终选择综合分数最高的路径输出。4.2 pyctcdecode KenLM 重评分在 Python 项目里把 CTC 输出交给语言模型重评分的常见方案是pyctcdecode配合kenlm。先安装依赖pip install pyctcdecode kenlm然后加载词表和语言模型构建解码器import kenlm import numpy as np from pyctcdecode import build_ctcdecoder # 第 0 个一定是 blank with open(vocab.txt, encodingutf-8) as fr: labels [blank] [line.strip() for line in fr if line.strip()] lm kenlm.Model(models/zh_char_4gram.arpa) decoder build_ctcdecoder( labels, kenlm_modellm, alpha0.6, beta1.0, unk_score_offset-10.0, ) def ctc_beam_decode(logits: np.ndarray) - str: # logits 形状 [T, V]可以直接传 log_softmax 的结果 return decoder.decode(logits, beam_width16)这一段里的labels顺序必须和训练时vocab.txt完全一致否则识别结果会错乱。alpha是语言模型权重值越大越信任语言模型通常从 0.5 开始调beta是标签插入奖励中文长句经常出现漏字适当调高 beta 可以减少过度吞字。unk_score_offset-10用来惩罚 OOV 字符避免中文模型把生僻字硬凑进结果。具体调参经验如下参数作用调参方向alpha语言模型权重噪声环境调低纯安静对话调高beta插入奖励漏字严重时调高多字乱出时调低beam_width解码束宽16 够用32 能挽回长句但推理变慢unk_score_offset未登录词惩罚生僻字场景不要设太低4.3 音近字纠错把结果交给文本后处理即使有了 n-gram 语言模型长尾专有名词依然会错。一般做法是准备一份hotwords.txt每行一个热词和对应权重。解码完成后把原始字符串与热词表做前缀匹配属于专有名词的词条如果出现在输出附近就用 Hotword Weight 把该子串的分数上调。这个操作不改变声学模型只影响最终排序因此可以热更新不用重新训练。def apply_hotwords(text: str, hotwords: dict[str, int]) - str: for w, score in hotwords.items(): if w in text: # score 越大保留该词的可能性越高 text text.replace(w, w, 1) return text这里替换逻辑只是一个示意真正生产环境会把 hotword 融合进 beam search 或作为二遍纠错模型的输入。规则越往后走越容易误伤所以热词表要按业务维度划分不同质检项目只加载对应版本。5. 训练收敛与部署调优CER 指标、流式推理和并发缓存5.1 用动态规划算 CER别只盯 Loss训练过程中验证集 loss 下降不代表字符错率一定下降中文 ASR 要看 CER。CER 是把参考文本和识别文本逐字符对齐后用编辑距离除以参考字符数得到的比例。不要直接用jieba分词后再算词错率字符级 CER 对工程验收更直观。import numpy as np def cer(ref: str, hyp: str) - float: r_len, h_len len(ref), len(hyp) if r_len 0: return 0.0 if h_len 0 else 1.0 dp np.zeros((r_len 1, h_len 1), dtypenp.int32) for i in range(r_len 1): dp[i, 0] i for j in range(h_len 1): dp[0, j] j for i in range(1, r_len 1): for j in range(1, h_len 1): if ref[i - 1] hyp[j - 1]: dp[i, j] dp[i - 1, j - 1] else: dp[i, j] min(dp[i - 1, j] 1, dp[i, j - 1] 1, dp[i - 1, j - 1] 1) return dp[r_len, h_len] / r_len这个函数在测试集上批量跑时注意要先过滤标点差异否则标点分号会被算成插入错误建议统一用re.sub(r[。、], , text)清洗后再计算。模型训练收敛后为了减少 CPU 部署时的响应时间我一般会把torch.set_num_threads调低到 2 到 4而不是默认占用所有核。多线程推理在 GPU 上容易造成显存带宽竞争在 CPU 上则会导致解码线程互相抢锁所以宁可让一个请求独占少数线程也不要让每个并发请求都去抢全部核心。流式识别还有一个容易忽略的细节不要把长音频切成固定 1 秒的小块分别过模型。正确做法是保留尾部 640ms 左右的上下文叠进下一段让语音识别系统在句首和句尾都能看到完整的声学边界。服务器部署时可以在外部包一层带热词版本号的缓存函数同一段音频和同一版热词重复请求时直接返回缓存结果能显著降低质检系统里的重复转写消耗。缓存 key 一定要加上hotword_version否则热词表更新后客户端还会拿到旧识别结果。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。