中文语音识别全流程开源实现:声学模型+CTC解码+CBHG语言模型
2026/9/21 0:24:53 网站建设 项目流程

简介:这是一套基于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.pyload_wav()函数返回的是 numpy array 而非 tensor,方便你在特征输入前插入自己的降噪逻辑。


2. 声学模型选型与结构解析:为什么 GRU-CTC 和 CNN-CTC 并存?如何根据硬件和数据量做取舍?

语音识别声学模型的核心矛盾是:时序建模能力 vs. 计算效率。本项目同时提供 GRU-CTC 和 CNN-CTC 两种实现,不是为了堆砌技术名词,而是对应真实业务中的不同约束条件。GRU 擅长捕捉长程语音依赖(如声调连续变化),但训练慢、显存占用高;CNN 对局部频谱特征敏感(如辅音爆破音的高频能量),推理快、参数少,但需配合时频图(spectrogram/fbank)输入才能发挥优势。项目中gru_ctc_am.pycnn_ctc_am.py的差异,本质是输入表征 + 网络结构的联合设计。

2.1 GRU-CTC 声学模型:时序建模的 baseline 实现

gru_ctc_am.py是整个声学建模的起点。它采用双层双向 GRU(tf.keras.layers.Bidirectional(tf.keras.layers.GRU(512, return_sequences=True)))作为主干,后接全连接层输出字符概率分布。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_major=True loss = tf.nn.ctc_loss( labels=labels, # sparse tensor, shape [batch, max_label_len] logits=logits, label_length=label_lengths, logit_length=logit_lengths, blank_index=0 # 第0类为blank符号 )

注意logits必须 transpose 成[time_step, batch, vocab_size]格式,否则ctc_loss会报维度错。blank_index设为 0 是硬编码约定,若你的词典vocab.txt中 blank 符号不在首行,必须同步修改此处。

该模型输入是 80 维 MFCC 特征(由data_process.pyextract_mfcc()生成),每帧 25ms,帧移 10ms。训练时 batch_size 默认设为 16,若显存不足,需同步调整train.pyBATCH_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 提取空间特征。其核心改进点有三:

  1. DFCNN 主干复用:沿用科大讯飞 DFCNN 的 7 层卷积结构(每层含 BN+ReLU+Conv),但将最后两层卷积核从3×3改为1×3,强化时间维度建模;
  2. Inception 模块替换:在第 4、5 层卷积后插入 Inception 模块(代码位于model_layers.py),并行使用1×13×35×5卷积核捕获多尺度频谱模式;
  3. 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 --mode=train前,务必确认acoustic_model/下已存在预训练权重文件(如cnn_fbank_best.h5)。若从零训练,建议先用cnn_with_full_data.py加载完整数据集(含 Aishell + ST-CMDS),避免小数据集上 CNN 收敛困难。

2.3 模型性能对比与硬件适配指南

我们用 Aishell-1 dev 数据集(15h)测试了三种配置的 WER(词错误率)与资源消耗:

模型类型GPU 显存占用单 epoch 训练时间(V100)dev WER推荐场景
GRU-CTC9.2 GB42 min18.7%高精度需求,数据 >20h
CNN-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 需要构建庞大词典和回退机制,而本项目采用 CBHG(Convolutional 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 模块包含四个子组件,按顺序串联:

  1. Convolutional Bank:16 个并行卷积层,卷积核大小从116,每层输出拼接后经max_pooling1d降维;
  2. Highway Network:2 层全连接,含门控机制,保留原始信息流;
  3. Bi-LSTM:256 维双向 LSTM,输出上下文感知的隐状态;
  4. Linear Projection:映射到词表维度,接 softmax。

训练数据来自lm_develop/目录下的文本语料(如aishell_text.txt),需先执行python gen_data.py --mode=lm生成lm_train.pkllm_dev.pkl。该脚本关键逻辑是:

# gen_data.py 片段 def build_vocab(texts, max_vocab=50000): 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_width=10): # 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](0=blank) 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 的优势在于可微调。假设你要识别医疗术语“冠状动脉支架”,而原始词表未包含该词,标准做法是:

  1. 修改gen_data.pybuild_vocabmax_vocab55000,重新生成词表;
  2. lm_develop/aishell_text.txt末尾追加 100 条含“冠状动脉支架”的模拟句子(如“患者植入冠状动脉支架后恢复良好”);
  3. 执行python CBHG_lm.py --mode=train --epochs=5,加载lm_pretrain.h5后继续训练。

微调时关键参数:

  • BATCH_SIZE=32(LM 训练 batch 可比声学模型大)
  • LEARNING_RATE=5e-5(微调需更小学习率,避免破坏预训练特征)
  • MAX_SEQ_LEN=80(控制输入文本长度,过长导致显存溢出)

验证微调效果:用python CBHG_lm.py --mode=test --input="冠状动脉",检查输出概率分布中“支架”的 logit 是否显著高于其他字。


4. 数据预处理与特征工程:fbank 与 mfcc 的选择依据,以及如何规避常见音频加载陷阱

语音识别效果 70% 取决于数据质量,而非模型结构。本项目data_process.py提供了完整的预处理链,但默认参数并非万能。例如,load_wav()函数对采样率的处理就暗藏坑点:它默认将所有音频 resample 到 16kHz,但若原始数据已是 8kHz(如部分电话录音),强行上采样会引入高频噪声,反而降低识别率。

4.1 时频特征选择:fbank 为何在 CNN 模型中优于 mfcc?

MFCC 通过离散余弦变换(DCT)压缩频谱,丢失部分相位信息;fbank 直接保留梅尔滤波器组能量,更接近人耳听觉特性。项目中cnn_with_fbank.py要求输入 fbank,而gru_ctc_am.py使用 mfcc,这一分工有明确物理依据:

  • CNN 天然适合图像输入:fbank 生成的(64, T)矩阵可视为灰度图,CNN 的卷积核能有效捕获频带间的相关性(如元音共振峰的垂直条纹);
  • GRU 需要时序稳定性:mfcc 的 13 维倒谱系数对说话人声道长度变化鲁棒性更强,且维度低,减少 GRU 的计算负担。

生成 fbank 的关键代码在data_process.py

def compute_fbank(wav, sr=16000, n_mels=64, n_fft=512, hop_length=160): # wav: np.array, shape [samples] mel_spec = librosa.feature.melspectrogram( y=wav, sr=sr, n_mels=n_mels, n_fft=n_fft, hop_length=hop_length ) log_mel_spec = librosa.power_to_db(mel_spec, ref=np.max) # 转为对数尺度 return log_mel_spec.T # shape [T, 64]

注意hop_length=160对应 10ms 帧移(160/16000=0.01s),若你的音频采样率非 16kHz,必须按比例缩放hop_length。例如 8kHz 音频应设hop_length=80,否则帧移失真。

4.2 音频加载的三大陷阱与修复方案

data_process.pyload_wav()函数常被忽略的细节:

陷阱表现修复代码
单双通道混杂部分录音为立体声,librosa.load()默认返回 2D array,导致后续 reshape 失败wav, sr = librosa.load(path, sr=None); wav = np.mean(wav, axis=1) if wav.ndim==2 else wav
静音段过长录音开头/结尾有 2 秒静音,CTC loss 计算时 padding 过多,拖慢训练wav = librosa.effects.trim(wav, top_db=20)[0]top_db=20比默认 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 done

4.3 训练集划分与增强策略

项目未内置数据增强,但extra_utils/目录提供了add_noise.pyspeed_perturb.py。实际使用时需注意:

  • 加噪:仅对训练集添加,验证集保持纯净。噪声源推荐使用 DEMAND 数据集(下载后解压到extra_utils/noise/);
  • 变速speed_perturb.pyrate=0.91.1,但需同步调整log_mel_spec的帧数,并在train.py中更新MAX_FRAME_NUM
  • 划分比例train.wav.lstdev.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实现了两项剪枝:

  1. 动态 beam width:初始设beam=20,当某候选序列连续 5 帧预测blank,则将其 beam 降为5,减少无效扩展;
  2. 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 降至 450ms(Intel i7-11800H)。

5.3 LM 流式计算:解决 CBHG 因果性限制

CBHG 模型默认需看到完整序列才能输出,但实际应用中希望“边说边识”。keras_test.py通过以下方式模拟流式:

  • 将输入序列分块(每块 20 字),每块末尾添加<EOS>
  • LM 每次只预测当前块内<EOS>前一个字的概率;
  • tf.function包装 LM 推理函数,避免 Python 解释器开销。

最终在 CPU 上达成:1.5 秒音频,端到端识别耗时 192ms(含特征提取 85ms + 声学模型 62ms + LM 重打分 45ms),满足实时交互需求。

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

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

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

立即咨询