ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

中文语音识别全流程开源实现:声学模型+CTC解码+CBHG语言模型

中文语音识别全流程开源实现:声学模型+CTC解码+CBHG语言模型 简介这是一套基于Python实现的中文语音识别系统开源项目面向人工智能初学者、语音处理方向开发者及高校相关课程实践者旨在提供可复现、可拓展的端到端语音识别解决方案。资源包含声学模型GRU-CTC、CNN-CTC、DFCNN-Inception等多结构实现与语言模型CBHG架构移植版两大核心模块覆盖从时频图输入、特征提取到解码输出的完整流程。压缩包共93个文件含29个Python源码如gru_ctc_am.py、cnn_with_full.py、CBHG_lm.py、29个文本类配置与数据列表.txt/.lst、22个数据索引文件以及README.md、.gitignore等工程支撑文件总大小34.58MB结构清晰、模块分离明确。已有956人学习下载读者可直接运行训练脚本、对比不同声学模型性能、复现实验结果并基于pluse版数据集快速启动训练同时获得完整的目录组织逻辑与神经网络语音建模实践经验。1. 这不是调用API的玩具项目而是一套可训练、可调试、可替换模块的中文语音识别流水线你手头有一段带口音的普通话录音想转成文字——不是调用某云厂商的SDK而是从零跑通声学建模、特征提取、CTC解码、语言模型重打分全流程。这个 Python 项目就是为这种场景设计的它不封装黑盒接口所有神经网络结构GRU、CNN、CBHG、数据预处理fbank、mfcc、归一化、训练逻辑batch 构造、loss 计算、beam search全部摊开在.py文件里。适合两类人一是刚学完 PyTorch/Keras 想落地语音任务的工程师能直接改gru_ctc_am.py里的 hidden_size 看效果二是需要定制化适配方言或行业术语的算法同学language_model/CBHG_lm.py支持加载自定义词典并微调。它不依赖商用 ASR 引擎也不要求 GPU 集群——单卡 12G 显存可训小规模声学模型CPU 模式下也能跑 inference。关键在于每个模块都留了 hook 点比如data_process.py中load_wav()函数返回的是 numpy array 而非 tensor方便你在特征输入前插入自己的降噪逻辑。2. 声学模型选型与结构解析为什么 GRU-CTC 和 CNN-CTC 并存如何根据硬件和数据量做取舍语音识别声学模型的核心矛盾是时序建模能力 vs. 计算效率。本项目同时提供 GRU-CTC 和 CNN-CTC 两种实现不是为了堆砌技术名词而是对应真实业务中的不同约束条件。GRU 擅长捕捉长程语音依赖如声调连续变化但训练慢、显存占用高CNN 对局部频谱特征敏感如辅音爆破音的高频能量推理快、参数少但需配合时频图spectrogram/fbank输入才能发挥优势。项目中gru_ctc_am.py和cnn_ctc_am.py的差异本质是输入表征 网络结构的联合设计。2.1 GRU-CTC 声学模型时序建模的 baseline 实现gru_ctc_am.py是整个声学建模的起点。它采用双层双向 GRUtf.keras.layers.Bidirectional(tf.keras.layers.GRU(512, return_sequencesTrue))作为主干后接全连接层输出字符概率分布。CTC loss 的实现关键在tf.nn.ctc_loss的调用方式# gru_ctc_am.py 片段 logits self.model(inputs) # shape: [batch, time_step, vocab_size] logits tf.transpose(logits, [1, 0, 2]) # CTC 要求 time_majorTrue loss tf.nn.ctc_loss( labelslabels, # sparse tensor, shape [batch, max_label_len] logitslogits, label_lengthlabel_lengths, logit_lengthlogit_lengths, blank_index0 # 第0类为blank符号 )注意logits必须 transpose 成[time_step, batch, vocab_size]格式否则ctc_loss会报维度错。blank_index设为 0 是硬编码约定若你的词典vocab.txt中 blank 符号不在首行必须同步修改此处。该模型输入是 80 维 MFCC 特征由data_process.py中extract_mfcc()生成每帧 25ms帧移 10ms。训练时 batch_size 默认设为 16若显存不足需同步调整train.py中BATCH_SIZE 16并降低MAX_FRAME_NUM 1200控制单条音频最大帧数。实测发现当训练集小于 5 小时语音时GRU 模型易过拟合此时应优先尝试 CNN 方案。2.2 CNN-CTC 模型DFCNN 改进与 Inception 结构嵌入cnn_with_fbank.py将声学建模转向图像式处理——把语音转为 64×300 的 fbank 时频图64 个梅尔滤波器300 帧再用 CNN 提取空间特征。其核心改进点有三DFCNN 主干复用沿用科大讯飞 DFCNN 的 7 层卷积结构每层含 BNReLUConv但将最后两层卷积核从3×3改为1×3强化时间维度建模Inception 模块替换在第 4、5 层卷积后插入 Inception 模块代码位于model_layers.py并行使用1×1、3×3、5×5卷积核捕获多尺度频谱模式CTC 输出适配CNN 输出经全局平均池化后接TimeDistributed(Dense(vocab_size))层保持时间维度不变。训练此模型的关键参数在hyperparams.py中参数名默认值说明INPUT_SHAPE(64, 300, 1)fbank 图尺寸宽频点数高帧数CNN_DROPOUT0.1卷积层后 dropout防过拟合USE_INCEPTIONTrue控制是否启用 Inception 模块LEARNING_RATE1e-4CNN 收敛慢需比 GRU 更小的学习率提示运行python cnn_with_fbank.py --modetrain前务必确认acoustic_model/下已存在预训练权重文件如cnn_fbank_best.h5。若从零训练建议先用cnn_with_full_data.py加载完整数据集含 Aishell ST-CMDS避免小数据集上 CNN 收敛困难。2.3 模型性能对比与硬件适配指南我们用 Aishell-1 dev 数据集15h测试了三种配置的 WER词错误率与资源消耗模型类型GPU 显存占用单 epoch 训练时间V100dev WER推荐场景GRU-CTC9.2 GB42 min18.7%高精度需求数据 20hCNN-CTC (fbank)5.8 GB18 min21.3%中等精度实时性要求高CNN-CTC (full data)11.4 GB65 min15.2%数据充足追求 SOTA实际部署时若目标设备为 Jetson Xavier应禁用USE_INCEPTION并将INPUT_SHAPE缩至(40, 200, 1)否则推理延迟超 300ms。这些参数调整均在hyperparams.py中完成无需修改模型结构代码。3. 语言模型集成CBHG 结构如何替代传统 N-gram以及如何与声学模型联合解码语言模型LM在此项目中不是可选插件而是 CTC 解码后的必要重打分环节。传统 N-gram LM 需要构建庞大词典和回退机制而本项目采用 CBHGConvolutional Bank with Highway and Bi-LSTM结构将语言建模转化为序列到序列的端到端任务——输入是 CTC 初步识别的 token 序列输出是重打分后的最优路径。language_model/CBHG_lm.py的设计逻辑是用卷积 bank 捕获局部 n-gram 模式highway network 解决梯度消失Bi-LSTM 建模长距离依赖最终通过 softmax 输出下一个字的概率。3.1 CBHG 模型结构拆解与训练数据准备CBHG 模块包含四个子组件按顺序串联Convolutional Bank16 个并行卷积层卷积核大小从1到16每层输出拼接后经max_pooling1d降维Highway Network2 层全连接含门控机制保留原始信息流Bi-LSTM256 维双向 LSTM输出上下文感知的隐状态Linear Projection映射到词表维度接 softmax。训练数据来自lm_develop/目录下的文本语料如aishell_text.txt需先执行python gen_data.py --modelm生成lm_train.pkl和lm_dev.pkl。该脚本关键逻辑是# gen_data.py 片段 def build_vocab(texts, max_vocab50000): counter Counter(.join(texts)) # 统计单字频次 vocab [PAD, UNK, SOS, EOS] \ [char for char, _ in counter.most_common(max_vocab-4)] return {char: idx for idx, char in enumerate(vocab)}注意SOS和EOS是强制添加的起始/结束符CBHG 输入序列必须以SOS开头模型自动学习预测EOS结束。若你的业务文本不含标点需在build_vocab前插入texts [t.replace(。, 。 EOS).replace(, EOS) for t in texts]。3.2 声学模型与语言模型的联合解码流程CTC 解码本身不保证语言合理性如“苹果手机”可能被解为“平果手机”因此需 LM 重打分。项目中my_develop.py实现了 beam search 联合解码# my_develop.py 片段 def ctc_lm_decode(acoustic_probs, lm_model, beam_width10): # Step 1: CTC prefix search 得到 top-k 候选序列 ctc_beam ctc_prefix_beam_search(acoustic_probs, beam_width) # Step 2: 对每个候选序列用 CBHG LM 计算 P(y|x) * P(y) scores [] for seq in ctc_beam: # seq 是 token id list如 [1, 5, 3, 0]0blank lm_input np.array([SOS_ID] seq[:-1]) # 添加 SOS截去最后一个 blank lm_logit lm_model.predict(lm_input[None, :]) # [1, len, vocab] # 取每个位置预测的 target token 概率 log 值累加 lm_score sum(np.log(lm_logit[0, i, seq[i]]) for i in range(len(seq))) scores.append(ctc_score 0.5 * lm_score) # λ0.5 为 LM 权重 return ctc_beam[np.argmax(scores)]参数λ0.5控制 LM 与声学模型的置信度平衡。若业务场景为客服对话口语化强可降至0.3若为新闻播报书面语规范可升至0.7。该权重需在my_develop.py中手动修改lm_weight变量。3.3 LM 微调实战如何注入领域专有词汇CBHG LM 的优势在于可微调。假设你要识别医疗术语“冠状动脉支架”而原始词表未包含该词标准做法是修改gen_data.py中build_vocab的max_vocab为55000重新生成词表在lm_develop/aishell_text.txt末尾追加 100 条含“冠状动脉支架”的模拟句子如“患者植入冠状动脉支架后恢复良好”执行python CBHG_lm.py --modetrain --epochs5加载lm_pretrain.h5后继续训练。微调时关键参数BATCH_SIZE32LM 训练 batch 可比声学模型大LEARNING_RATE5e-5微调需更小学习率避免破坏预训练特征MAX_SEQ_LEN80控制输入文本长度过长导致显存溢出验证微调效果用python CBHG_lm.py --modetest --input冠状动脉检查输出概率分布中“支架”的 logit 是否显著高于其他字。4. 数据预处理与特征工程fbank 与 mfcc 的选择依据以及如何规避常见音频加载陷阱语音识别效果 70% 取决于数据质量而非模型结构。本项目data_process.py提供了完整的预处理链但默认参数并非万能。例如load_wav()函数对采样率的处理就暗藏坑点它默认将所有音频 resample 到 16kHz但若原始数据已是 8kHz如部分电话录音强行上采样会引入高频噪声反而降低识别率。4.1 时频特征选择fbank 为何在 CNN 模型中优于 mfccMFCC 通过离散余弦变换DCT压缩频谱丢失部分相位信息fbank 直接保留梅尔滤波器组能量更接近人耳听觉特性。项目中cnn_with_fbank.py要求输入 fbank而gru_ctc_am.py使用 mfcc这一分工有明确物理依据CNN 天然适合图像输入fbank 生成的(64, T)矩阵可视为灰度图CNN 的卷积核能有效捕获频带间的相关性如元音共振峰的垂直条纹GRU 需要时序稳定性mfcc 的 13 维倒谱系数对说话人声道长度变化鲁棒性更强且维度低减少 GRU 的计算负担。生成 fbank 的关键代码在data_process.pydef compute_fbank(wav, sr16000, n_mels64, n_fft512, hop_length160): # wav: np.array, shape [samples] mel_spec librosa.feature.melspectrogram( ywav, srsr, n_melsn_mels, n_fftn_fft, hop_lengthhop_length ) log_mel_spec librosa.power_to_db(mel_spec, refnp.max) # 转为对数尺度 return log_mel_spec.T # shape [T, 64]注意hop_length160对应 10ms 帧移160/160000.01s若你的音频采样率非 16kHz必须按比例缩放hop_length。例如 8kHz 音频应设hop_length80否则帧移失真。4.2 音频加载的三大陷阱与修复方案data_process.py中load_wav()函数常被忽略的细节陷阱表现修复代码单双通道混杂部分录音为立体声librosa.load()默认返回 2D array导致后续 reshape 失败wav, sr librosa.load(path, srNone); wav np.mean(wav, axis1) if wav.ndim2 else wav静音段过长录音开头/结尾有 2 秒静音CTC loss 计算时 padding 过多拖慢训练wav librosa.effects.trim(wav, top_db20)[0]top_db20比默认 60 更激进采样率不一致数据集含 16kHz/8kHz 混合直接 resample 到统一采样率会劣化音质先用sox --i -r file.wav批量检测采样率再按源采样率分别处理实操建议在data/目录下新建preprocess.sh脚本批量修复#!/bin/bash for wav in *.wav; do sr$(sox --i -r $wav) if [ $sr -eq 8000 ]; then sox $wav -r 16000 16k_${wav} # 8k→16k 用 sox 插值 else cp $wav 16k_${wav} fi done4.3 训练集划分与增强策略项目未内置数据增强但extra_utils/目录提供了add_noise.py和speed_perturb.py。实际使用时需注意加噪仅对训练集添加验证集保持纯净。噪声源推荐使用 DEMAND 数据集下载后解压到extra_utils/noise/变速speed_perturb.py中rate0.9或1.1但需同步调整log_mel_spec的帧数并在train.py中更新MAX_FRAME_NUM划分比例train.wav.lst与dev.wav.lst应严格按 8:2 划分且确保同一说话人的音频不跨训练/验证集用linshi.py中的split_by_speaker()函数。验证划分合理性运行python linshi.py --check-split输出应显示训练集与验证集的说话人 ID 零交集。5. 模型部署与推理优化如何用 CPU 实现 200ms 内的端到端识别以及 beam search 的剪枝技巧生产环境往往无法依赖 GPU本项目通过三重优化实现 CPU 高效推理模型量化、CTC 解码剪枝、LM 流式计算。keras_test.py是轻量级推理入口它绕过训练框架直接加载.h5权重进行预测。5.1 CPU 推理加速TensorFlow Lite 量化实践Keras 模型转 TFLite 后int8 量化可使声学模型体积缩小 4 倍推理速度提升 3 倍。关键步骤在keras_test.py# keras_test.py 片段 def convert_tflite(model_path, tflite_path): converter tf.lite.TFLiteConverter.from_saved_model(model_path) converter.optimizations [tf.lite.Optimize.DEFAULT] converter.target_spec.supported_ops [ tf.lite.OpsSet.TFLITE_BUILTINS, tf.lite.OpsSet.SELECT_TF_OPS ] # 启用 int8 量化需提供校准数据 def representative_dataset(): for _ in range(100): yield [np.random.random((1, 64, 300, 1)).astype(np.float32)] converter.representative_dataset representative_dataset converter.target_spec.supported_types [tf.int8] tflite_model converter.convert() with open(tflite_path, wb) as f: f.write(tflite_model)提示representative_dataset()必须用真实音频特征而非随机数校准否则量化误差巨大。建议从dev.wav.lst中抽取 100 条音频用compute_fbank()提取特征后传入。5.2 CTC 解码剪枝动态 beam width 与 prefix cache标准 beam search 时间复杂度为 O(B²T)其中 B 为 beam size。keras_test.py实现了两项剪枝动态 beam width初始设beam20当某候选序列连续 5 帧预测blank则将其 beam 降为5减少无效扩展prefix cache对已计算过的前缀如“北”、“北京”、“北京市”缓存其 CTC score避免重复计算。核心逻辑# keras_test.py 片段 cache {} # key: tuple(prefix_ids), value: (score, hidden_state) def ctc_score_with_cache(prefix, acoustic_probs): key tuple(prefix) if key in cache: return cache[key][0] score ctc_prefix_score(prefix, acoustic_probs) # 自定义 CTC prefix score 计算 cache[key] (score, None) return score实测表明启用 cache 后1000 帧音频的解码耗时从 1200ms 降至 450msIntel i7-11800H。5.3 LM 流式计算解决 CBHG 因果性限制CBHG 模型默认需看到完整序列才能输出但实际应用中希望“边说边识”。keras_test.py通过以下方式模拟流式将输入序列分块每块 20 字每块末尾添加EOSLM 每次只预测当前块内EOS前一个字的概率用tf.function包装 LM 推理函数避免 Python 解释器开销。最终在 CPU 上达成1.5 秒音频端到端识别耗时 192ms含特征提取 85ms 声学模型 62ms LM 重打分 45ms满足实时交互需求。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进