十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

中文语音识别全流程:Fbank特征、声学模型与语言模型解码

中文语音识别全流程:Fbank特征、声学模型与语言模型解码 简介基于深度学习的Python中文语音识别系统设计源码面向语音识别初学者、算法研究人员及毕业设计开发者覆盖了从音频预处理、特征提取、声学建模、语言模型到解码输出的完整技术链路。资源包共包含87个文件整体约133.82MB其中30个Python源文件承担模型构建与训练逻辑30个文本文件用于配置与说明22个lst列表文件管理训练、测试数据集划分另有pkl词典、tsv标注文件等辅助构建中文发音词典与标注体系。目前已有114人学习浏览。源码内设数据预处理、模型训练、模型评估和解码等模块提供GRU/CNN声学模型、CBHG语言模型等可运行实现并配有语料处理脚本与词典生成脚本便于理解中文语音识别系统模块间的协作关系。对于希望快速搭建中文语音识别原型或进行算法对比的实验者这份资源能够省去大量环境搭建和数据清洗时间直接作为二次开发的基础工程。1. 中文语音识别不是单个模型就能跑通的打开这份源码包第一眼容易集中在gru_ctc_am.py或cnn_ctc_am.py上但真正让它能跑起来的是feature_extract.py、data_load.py、hyperparams.py这些容易被忽略的文件。中文语音识别和英文不同声学模型通常输出拼音或声韵母单元而不是直接输出汉字。这意味着训练完声学模型之后还要经过语言模型解码才能得到可读的中文文本。这套源码恰好把这条链路补齐了从 THCHS-30、AISHELL 等语料读入到 Fbank 特征提取再到 CNN/GRU/FSMN 和 CTC 训练最后用语言模型纠错。如果你正打算做一个 Python 项目来理解深度学习的完整落地流程或者需要一份可改动的系统设计模板这份源码比单模型教程有价值得多。2. 特征提取与数据管道从中文WAV到Fbank特征2.1 语料库选择为什么是四个而不是一个源码里出现 primewords、st-cmds、thchs30、aishell 四个数据目录这不是随意堆砌。THCHS-30 是清华大学录制的短句普通话标注较干净适合做主训练集AISHELL 来自开源语音团队语速偏口语化适合做跨场景验证Primewords 是手机录音环境噪声大用来测模型的鲁棒性ST-CMDS 偏向命令词适合单独做语音命令任务。我一般会把它们合并成一个经过统一预处理的元列表而不是一个数据集训练到底。四个语料库在项目中的典型定位如下语料目录录音环境特点在项目中的用法thchs30安静室内句子规整音素标注齐全主训练集验证集来源aishell相对干净普通话语速自然文本覆盖广扩充训练集测试模型泛化primewords手机录音噪声、混响明显样本增强模拟真实使用st-cmds半封闭环境短命令词口语化命令词识别测试注意四个语料的文本格式并不统一有的带声调标注有的用数字调。源码里的read_data_prime.py、read_aishell.py、gen_thchs_lable.py就是在做这件事把各自的 label 统一转换成模型可用的拼音序列。这个转换直接决定后期的训练文本有多干净我强烈建议先单独运行这些脚本检查dict.txt的覆盖范围再继续下一步。统一成拼音而不是汉字是因为汉字类别有几千个直接预测文本会让模型收敛极慢而拼音单元加上声调大约只有几百个在准确率和复杂度之间更容易取得平衡。2.2 手动实现Fbank特征提取语音信号经过预加重、分帧、加窗、傅里叶变换、Mel滤波器组得到的就是Fbank通常也被称为FilterBank。与MFCC相比Fbank保留更多原始频谱信息在深度学习中表现通常更好。项目里的feature_extract.py核心逻辑可以简化成下面的代码import numpy as np import librosa def extract_fbank(wav_path, sample_rate16000, win_len25, win_step10, num_filter40): y, sr librosa.load(wav_path, srsample_rate) # 预加重抵消发声时低频能量被口腔衰减提升高频 pre_emphasis 0.97 y np.append(y[0], y[1:] - pre_emphasis * y[:-1]) win_length int(sample_rate * win_len / 1000) hop_length int(sample_rate * win_step / 1000) # power2.0表示用能量谱n_mels是Mel频带数量 fbank librosa.feature.melspectrogram( yy, srsr, n_fftwin_length, hop_lengthhop_length, n_melsnum_filter, power2.0) log_fbank librosa.amplitude_to_db(fbank, refnp.max) # 全局均值方差归一化避免不同录音音量差异干扰训练 log_fbank (log_fbank - np.mean(log_fbank)) / np.std(log_fbank) return log_fbank.T这段代码有两个关键参数需要根据你的数据调整。sample_rate必须和录音文件一致四个语料库都是16kHz所以这里写死是安全的num_filter用40和Kaldi默认配置一致你的GPU显存够大也可以改成64特征维度越高模型容量需求越大。win_len25表示每帧25毫秒win_step10表示帧移10毫秒。这是语音识别中主流配置25毫秒能覆盖一个元音的大部分稳态段10毫秒步长让相邻帧有重叠保证时序连续性。如果你用流式模型帧移往往要增大到20~30毫秒来降低计算延迟离线识别保持这个配置即可。关于预加重系数0.97这是电话语音处理流传下来的经验值对16kHz语音依然有效。如果你的数据是8kHz电话语音应该保留如果是麦克风阵列远场录音可以适当减小到0.95。归一化使用全局均值和方差而不是帧内是因为语音不同帧能量差异很大帧内归一化会把静音帧和语音帧无差别放大反而破坏动态范围。2.3 数据列表与批处理读取项目根目录下的train.wav.lst、datalist和多个read_*.py脚本本质上是在做一个事情把音频路径和文本路径组织成训练用的列表。列表文件的每一行一般由wav路径和文本内容用制表符分隔。我习惯的做法是def load_list(lst_path): with open(lst_path, r, encodingutf-8) as f: lines [] for line in f: line line.strip() if not line: continue wav_path, text line.split(\t) lines.append((wav_path, text)) return lines加载列表后配合特征提取函数生成批数据。需要特别注意CTC语音识别中wav经过特征提取后的帧数远大于字符数所以要在训练时用tf.keras.utils.Sequence或自定义数据集来动态填充batch。不要把整个特征矩阵一次性放进内存四个语料库总共几十小时音频内存很容易被占满。调用GetData.py时我通常会限制单次读取样本数并记录当前batch内的真实帧数便于CTC loss计算时传入序列长度。列表文件通常还会包含第三列说话人ID或数据集来源方便做分层采样。源码里datalist目录下的文件就承担这个角色。做验证集切分时要按说话人或录音文件切分而不是按句子随机切分。否则同一说话人的不同句子同时出现在训练集和验证集CER会被低估这个细节很容易踩坑。3. 声学模型选型从FSMN到CNNGRUCTC3.1 为什么是CTC而不是注意力声学模型要解决的是输入语音帧序列和输出文本序列长度不一致的问题。注意力机制可以直接做seq2seq但计算量较大且并行度低训练数据量不足时容易过拟合。CTC则通过在输出序列中插入blank符让模型可以重复或跳过输出从而在时序上对齐。中文拼音序列一般不超过几十个字符而语音帧可能有几百上千CTC天然适合这种场景。CTC还有一个额外优势不需要音频和文本的帧级对齐标注。THCHS-30虽然有音素级别标注但其他几个语料只有句子级文本CTC可以只用句子级文本训练。源码中同时出现了gru_ctc_am.py和cnn_ctc_am.py说明作者在同一份系统中尝试了两种声学模型。GRU模型长于时序建模CNN模型长于局部频谱模式捕捉。在实际工程中我建议把两者串起来先用CNN下采样频谱维度再用GRU聚合上下文最后接全连接层输出概率。3.2 FSMN单元流式识别的备选方案FSMN的全称是Feedforward Sequential Memory Networks它是Mobvoi提出的一种前馈序列记忆网络。和LSTM、GRU不同FSMN没有循环结构只通过显式记忆模块缓存前几帧特征因此前向计算延迟更低更适合流式语音识别。FSMNCell.py的核心思想是把当前帧和相邻帧拼在一起做变换。下面是我按相同思路实现的一个简化FSMN单元class FSMNCell(tf.keras.layers.Layer): def __init__(self, filter_size128, memory_size5): super().__init__() self.filter_size filter_size self.memory_size memory_size self.fc tf.keras.layers.Dense(filter_size, use_biasFalse) def call(self, x): # x: (batch, time, feature) hidden self.fc(x) # 对每个时刻取前memory_size帧的隐藏向量拼接 time_len tf.shape(hidden)[1] memory tf.concat( [hidden[:, i:i self.memory_size, :] for i in range(time_len)], axis-1 ) # 摊平后映射回filter_size output tf.keras.layers.Dense(self.filter_size)(tf.reshape(memory, [tf.shape(x)[0], -1])) return output这里为了演示把记忆构建简化成一串循环实际项目里应该用tf.keras.layers.Conv1D或滑动窗口实现避免循环构造带来的性能问题。memory_size控制上下文窗口长度。一般设置成5时提取约50毫秒的历史上下文适合短时语音如果模型想知道更远的历史信息可以增加到7或9但设备端内存消耗会明显上升。FSMN在实际部署中和CNN混合使用效果更好。CNN负责压缩特征中的噪声FSMN负责记忆时序上下文两者都不需要递归计算在CPU上推理速度很快。如果你的场景是智能音箱、门禁语音建议保留FSMNCell.py附近的实验代码如果只做离线转写优先使用GRU。3.3 CNNGRU混合声学模型实现cnn_with_fbank.py和gru_ctc_am.py的区别主要在主干网络。我给出的实现同时利用两种网络结构import tensorflow as tf def build_cnn_gru_ctc(input_dim40, num_outputs1200): inputs tf.keras.Input(shape(None, input_dim)) # 增加通道维度方便Conv2D处理 x tf.expand_dims(inputs, axis-1) # 两个Conv2D块提取局部频谱模式 x tf.keras.layers.Conv2D(32, (3, 3), paddingsame, activationrelu)(x) x tf.keras.layers.MaxPooling2D((1, 2))(x) # 只压缩频率维 x tf.keras.layers.Conv2D(64, (3, 3), paddingsame, activationrelu)(x) x tf.keras.layers.MaxPooling2D((1, 2))(x) # 转回序列格式 shape tf.shape(x) x tf.reshape(x, (shape[0], shape[1], shape[2] * shape[3])) # 双向GRU建模时间依赖 x tf.keras.layers.Bidirectional(tf.keras.layers.GRU(256, return_sequencesTrue))(x) x tf.keras.layers.Dropout(0.2)(x) # 每个时刻输出拼音概率分布 logits tf.keras.layers.Dense(num_outputs, activationsoftmax)(x) model tf.keras.Model(inputs, logits) return model模型里MaxPooling2D只压缩频率维度不压缩时间维度这样可以保留较高的时间分辨率避免CTC因帧数不足而无法对齐。如果你做实时识别建议把双向GRU改成单向GRU并且使用因果卷积避免未来信息泄漏。双向GRU在中文识别中作用很大因为前后语义都会影响拼音判断比如“西安”的“西”和“安”之间停顿时长不同双向结构能捕捉这种边界。代价是解码延迟高不能用于在线流式识别。num_outputs设置为1200的原因和字典大小直接相关声韵母数量加上汉字、blank、start/end等特殊标记通常不会超过1500。这个值不能拍脑袋必须由gen_dict.py生成的拼音字典决定。如果字典有1500个单元而你设置了1200训练时会直接报错。3.4 超参数配置与训练循环hyperparams.py是整一套系统的总控文件训练前应先检查以下参数参数名建议值作用sample_rate16000与录音采样率一致num_filters40Fbank滤波器个数batch_size32每批训练样本数max_epochs100最大训练轮次learning_rate0.001Adam初始学习率num_units256GRU隐层维度num_outputs1200声学单元字典大小dropout_rate0.2防止过拟合训练循环的骨架在train.py中实现。核心逻辑是读取特征和标签计算CTC loss梯度更新保存checkpoint。下面是一段基于TensorFlow 2的参考实现def train(model, train_loader, optimizer, ctc_fn, max_epochs): for epoch in range(max_epochs): for batch_wav, batch_labels, input_len, label_len in train_loader: with tf.GradientTape() as tape: logits model(batch_wav) # (batch, time, num_outputs) loss ctc_fn(batch_labels, logits, input_len, label_len) grads tape.gradient(loss, model.trainable_variables) optimizer.apply_gradients(zip(grads, model.trainable_variables)) print(fepoch {epoch} loss: {loss.numpy():.4f})注意ctc_fn需要传入batch中每条样本的输入帧长度和标签长度。很多初学者在这里出错以为CTC会自动推断长度实际上TensorFlow的tf.nn.ctc_loss要求明确指定labels、logits、label_length、logit_length。其中label_length是拼音序列的字符数logit_length是卷积池化后的时间帧数。如果你在MaxPooling2D中使用的时间步长不为1别忘记重新计算logit_length否则训练loss会一直停在NaN附近。还有一个容易混淆的概念blank和空格。CTC中的blank是模型学习出来的“占位符”用于分隔连续的重复字符空格是最终输出文本中的一个真实汉字间分隔符。如果把空格也作为字典中的一个普通单元解码时需要额外过滤否则Beam Search会出现大量重复和消失。源码中如果看到lable.txt或dict.txt含有blank和space两种标记就是这个目的。4. 语言模型与解码把拼音序列变成汉字4.1 为什么必须额外接语言模型声学模型输出的是拼音概率序列即使识别准确一个拼音也可能对应几十个汉字。以 “zhong guo” 为例可以有“中国”“忠国”“中果”等候选。语言模型通过统计中文文本中词序列的概率把这几个候选重新排序让“中国”排第一。这也是language_model/CBHG_lm.py存在的意义它就是用来给声学模型候选重新打分的模块。语言模型有统计n-gram和神经网络两种。CBHG属于神经网络形式训练成本比声学模型低得多即使只有几MB的中文文本语料也能训练出可用的模型。如果不想自己训练也可以用开源的中文拼音到汉字映射表但在源码里既然已经有CBHG_lm.py直接用它成本更低。CBHG并不是为语音识别发明的它最早出现在Tacotron语音合成模型中由一维卷积Bank、Highway网络和双向GRU组成。它的优势在于用多组不同宽度的卷积核捕捉n-gram特征再用Highway网络加深非线性最后用GRU输出序列级预测。我把它用在解码阶段时输入是拼音序列输出是每个位置的汉字分布这样语言模型就能和声学模型无缝结合。4.2 用CBHG实现拼音到汉字映射下面是一个面向语言模型任务的简化CBHG模块class CBHGBlock(tf.keras.layers.Layer): def __init__(self, hidden_size128): super().__init__() # 1到6的卷积核宽度相当于n-gram特征 self.conv_bank [ tf.keras.layers.Conv1D(hidden_size, k, paddingsame, activationrelu) for k in range(1, 7) ] self.highway tf.keras.layers.Dense(hidden_size) self.gru tf.keras.layers.Bidirectional(tf.keras.layers.GRU(hidden_size, return_sequencesTrue)) def call(self, x): # 拼接不同卷积核的输出 conv_list [conv(x) for conv in self.conv_bank] conv_out tf.concat(conv_list, axis-1) highway_out tf.nn.relu(self.highway(conv_out)) output self.gru(highway_out) return output这段实现里conv_bank是关键。卷积核宽度从1到6宽度越窄关注相邻音节的局部关系宽度越宽关注更远的上下文。例如k1能判断单字概率k4能捕捉类似“中华人民共和国”这种常见搭配。实际训练中语言模型和声学模型的字典必须一致否则在解码阶段会出现拼音序列中有些字符在LM字典里缺失导致整个beam搜索崩掉。训练CBHG时输入是build_corpus.py生成的拼音序列输出是汉字序列损失函数用交叉熵即可。语言模型训练完成后保存为独立的tensorflow模型或numpy矩阵方便在解码时快速查表。不要把它和声学模型放在同一个checkpoint里训练一般分开训练损失更稳定。项目中的language_model目录下除了CBHG_lm.py还有model_layers.py和vocab.pkl后者就是保存的字典和统计参数。4.3 解码策略贪心搜索和Beam Search解码时将声学模型的输出按帧取 argmax去掉重复和blank得到拼音序列这是贪心搜索。它速度最快但无法纠正单帧错误。Beam Search会在每一步保留概率最高的若干个候选路径结合语言模型重新打分最后选择总分最高的序列。工程中我通常用beam_size20。两种策略的对比策略速度准确率适用场景贪心搜索极快中实时命令词识别Beam Search慢高离线长句转写Beam 语言模型最慢最高中文听写字幕生成在eval.py中常见一个循环对每个测试音频依次调用模型和语言模型。下面是Beam Search的简化实现def beam_search_decode(logits, lm_model, beam_size5): logprobs np.log(logits 1e-7) T logits.shape[0] seqs [([], 0.0)] # (路径, 累计对数概率) for t in range(T): candidates [] for seq, score in seqs: top_k np.argsort(logprobs[t])[-beam_size:] for token in top_k: new_seq seq [token] lm_prob lm_model.predict_line(new_seq) new_score score logprobs[t][token] 0.1 * np.log(lm_prob) candidates.append((new_seq, new_score)) seqs sorted(candidates, keylambda x: -x[1])[:beam_size] return seqs[0][0]注意这里的0.1是语言模型权重在不同数据集上差别很大。THCHS-30上语料干净权重取0.1~0.3效果就不错如果你的数据噪声大权重调高反而会掩盖声学模型的判断。建议在验证集上按0.1间隔扫描选CER最低的权重。如果计算资源比较紧张可以先只用声学模型做贪心解码再用语言模型对整条候选序列做重打分rescoring而不是在beam每一步都融合语言模型。这样beam search只需跑一次速度快很多准确率下降幅度通常在1%以内。5. 评估指标CER和复现时的调优技巧5.1 用CER而不是accuracy评估识别效果中文语音识别的最终结果是一段文本评估时不能直接比较两个字符串是否完全相同因为少一个标点或者错一个多音字都是一个错误。语音识别社区通常用字符错误率CER来衡量计算公式是编辑距离除以参考文本字符数。源码里的eval.py至少会输出CER我们也可以自己实现一个简化版本def compute_cer(reference, hypothesis): # 去掉空格以字符为单位计算编辑距离 ref list(reference.replace( , )) hyp list(hypothesis.replace( , )) len_ref, len_hyp len(ref), len(hyp) dp np.zeros((len_ref 1, len_hyp 1)) for i in range(len_ref 1): dp[i][0] i for j in range(len_hyp 1): dp[0][j] j for i in range(1, len_ref 1): for j in range(1, len_hyp 1): cost 0 if ref[i - 1] hyp[j - 1] else 1 dp[i][j] min(dp[i-1][j] 1, dp[i][j-1] 1, dp[i-1][j-1] cost) return dp[len_ref][len_hyp] / len_ref这个动态规划表格填完就是编辑距离最后再除以参考长度。项目里如果报告accuracy通常指的是“准确率1-CER”不要把两者直接画在同一张图上对比。日志中如果CER从0.8降到0.4说明模型开始学到内容如果在0.5附近抖动先检查特征和标签是否对齐。5.2 复现时最容易翻车的三个环节第一Python和TensorFlow版本。这份源码里有多个模型文件API风格可能混有TensorFlow 1.x的写法。如果你直接用TensorFlow 2.10跑1.x代码tf.contrib这类模块会直接报错。我一般用虚拟环境固定版本先读hyperparams.py看有没有tf.contrib引用有的话就手动替换成TensorFlow 2的对应函数比如用tf.keras.layers代替tf.contrib.rnn。第二CTC的标签长度计算。尤其在使用卷积池化后logit_length往往不等于原始帧数。简单的方法是在模型里加一个shape分支把tf.shape(logits)[1]动态输出到损失函数而不是自己推算。下面的对照表可以帮助定位问题现象可能原因检查位置loss停在-90附近missing blankCTC窗口为空检查字典是否包含blank识别结果全是最后一个字输入长度和标签长度传反打印logit_length和label_length中文乱码读取label时编码不一致统一使用utf-8显存溢出batch_size过大调小或使用梯度累积第三中文文本编码。label.txt、dict.txt是UTF-8编码但Windows下用默认GBK打开会乱码。写数据读取脚本时统一指定encodingutf-8否则训练到一半会莫名崩溃。read_data_prime.py这类转换脚本建议先打印前五条记录确认文本正常再进入训练流程。如果你只是想做一套快速验证我建议先取 THCHS-30 中的50条句子跑10轮确认loss和CER都有下降趋势再扩展到全量数据。这样能在十分钟内暴露配置错误避免全量训练两天后才发现代码问题。本文还有配套的精品资源点击获取
返回列表