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

资讯详情

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

从零搭建语音识别系统:TensorFlow+CNN+BiLSTM+CTC实战

从零搭建语音识别系统:TensorFlow+CNN+BiLSTM+CTC实战 简介基于Python与TensorFlow构建的语音识别系统完整工程面向毕业设计、课程设计及项目开发场景适合具备一定Python基础的初学者或开发者参考。项目实现了从音频数据读取、MFCC梅尔倒频谱特征提取、文本向量化到BiRNN双向循环神经网络训练与识别的全流程能够完成高效语音识别。资源共9个文件以5个Python源码文件为核心分别承担模型构建、训练、测试及工具函数等功能另含配置文件、开发说明文档与附属压缩包整体仅498KB结构紧凑。目前已有169人学习下载源码经过严格测试可直接运行验证并便于在此基础上进行算法改进与功能扩展。配套开发文档对项目流程与关键步骤做了说明适合作为课程报告、毕业设计论文撰写的参考资料。 从零搭建一个能跑的语音识别系统这个话题我在毕业设计那会儿就折腾过后来带实习生、帮学弟学妹调代码又踩过不少坑。用Python和TensorFlow这条路优点是生态成熟资料多遇到问题基本都能搜到答案缺点是如果你只知道“用TensorFlow”却不知道音频该怎么处理、标签该怎么对齐很容易卡在数据准备这一步模型还没见到数据就被预处理劝退了。这篇就把我做语音识别系统的完整思路、关键代码、调参心得和踩坑记录一次性梳理出来。无论你是做毕业设计、课程设计还是单纯想搞一个能用的语音转文字工具这篇文章都能帮你省下大量试错时间。1. 系统整体设计与方案选型1.1 核心需求拆解这门课/毕设到底要你做什么先别急着写代码先搞清楚一个语音识别系统拿出去给老师验收核心指标是什么。基本绕不开三件事能不能把音频转成文字转得准不准以及你能不能讲清楚“为什么这么做”。很多同学第一次做语音识别第一反应是去调用百度、讯飞或者Google的云识别API。这确实能“高效识别”但作为毕业设计或课程设计这基本属于踩红线。老师要的是你把“识别”这件事做出来而不是把别人家的“识别结果”拿过来。所以我们在设计方案时目标定成模型自己学、自己推理本地能跑通完整的训练和预测链路。我选定的技术路线是Python TensorFlow 2.x 卷积神经网络(CNN) 双向LSTM CTC损失函数。CNN负责提取音频的局部特征模式双向LSTM负责建模时序依赖CTC负责解决“音频帧和文字长度对不齐”的问题。这条路线的好处是不依赖外部语法模型和语言模型就能做出一个基础可用的识别器而且每一层都有清晰的原理可以写进论文或者说明书。1.2 为什么选TensorFlow而不是PyTorch或其他框架• 对于一个功能型毕设来说TensorFlow的完整生态能让你少写很多底层代码比如音频的IO处理、波形到频谱的转换、TFRecord数据集的制作都有成熟方案可以直接借鉴。• 相对PyTorchTensorFlow在模型导出和部署上更顺手训练好的模型能保存为SavedModel格式后续要接Web服务或者打包成小工具都比较方便。• 当然现在PyTorch在学术界更流行如果你熟悉PyTorch换成PyTorch实现也完全没问题。语音识别领域核心的“CTC损失函数”“特征提取”“序列建模”思想是通用的换框架只影响写法不影响原理。选型确定后就要开始准备语料数据了。这一步会花掉你整个项目大约一半的时间绝对不能跳过。2. 数据准备与特征提取细节2.1 开源数据集选择别一上来就自己录音很多人想训练一个属于自己的中文语音识别系统第一反应是拿手机录几百条语音再手动转写。我劝你趁早打消这个念头。个人录音的问题在于背景噪声不一致、说话人单一、时间长短参差不齐而且你自己转录的文字对齐质量很难保证。最稳妥的方式是直接使用开源中文语音数据集网上常见的免费数据集有THCHS-30、Free ST Chinese Mandarin Corpus等。如果你做英文识别可以选用LibriSpeech。这些数据集通常自带wav格式的音频文件和对应的文本标注文件。你需要按照数据集的划分方式训练集、验证集、测试集把音频路径和文本内容整理成一个列表作为训练的输入清单。2.2 音频预处理三步走采样率、分帧加窗、梅尔频谱模型读不懂原始的wav波形数据我们需要把音频转换成“特征图”。我这里采用的方案是提取80维梅尔频谱特征Mel Spectrogram。梅尔频谱的核心逻辑是模拟人耳对不同频率声音的非线性感知。频率越高人耳分辨能力越差因此梅尔刻度在低频区域给更多分辨率、高频区域给更少分辨率。这一步相当于把音频从“声波形状”转换成了“时间-频率的灰度图”CNN就可以像处理图像一样处理它了。具体处理流程分三步统一音频格式所有wav文件重采样到16000Hz、单声道、16bit编码。分帧与加窗每帧长度设为25毫秒相邻两帧起始位置间隔10毫秒避免帧与帧之间发生剧烈跳变。计算梅尔频谱对每一帧做快速傅里叶变换再通过梅尔滤波器组得到80个频带的能量值。在TensorFlow中可以直接调用tf.signal模块完成上述操作。以下是核心代码雏形import tensorflow as tf def load_and_preprocess_audio(file_path, sample_rate16000): # 读取音频文件并解码为波形 audio_binary tf.io.read_file(file_path) audio, sr tf.audio.decode_wav(audio_binary, desired_channels1, desired_samplessample_rate) audio tf.squeeze(audio, axis-1) audio audio * 32767.0 # 还原振幅范围 # 计算梅尔频谱 stfts tf.signal.stft(audio, frame_length400, frame_step160, fft_length512) spectrograms tf.abs(stfts) num_spectrogram_bins stfts.shape[-1] # 转为梅尔刻度 linear_to_mel_weight_matrix tf.signal.linear_to_mel_weight_matrix( num_mel_bins80, num_spectrogram_binsnum_spectrogram_bins, sample_ratesample_rate, lower_edge_hertz0.0, upper_edge_hertz8000.0 ) mel_spectrograms tf.tensordot(spectrograms, linear_to_mel_weight_matrix, 1) log_mel_spectrograms tf.math.log(mel_spectrograms 1e-6) return log_mel_spectrograms这段代码最常见的坑是desired_samples参数。如果某个wav文件实际时长超过1秒decode_wav返回的数据会超过desired_samples指定的长度吗实际规则是如果音频比desired_samples长它会截断到该长度如果比它短会补零。所以这个参数并不能完成“把所有音频统一到等长”的任务你还得靠后续的padding或batch padding来解决长度不一致的问题。2.3 标签编码与字符表音频变成了特征序列文本标签也得变成模型能算的形式。中文字符集不大我们只需要把训练语料中出现过的所有汉字、字母、数字收集起来建立一个“字符到索引”的映射表。比如“你好世界”就编码成[6, 12, 3, 25]这样的整数序列。这里有个关键点CTC的预测结果会引入一个空白符blank通常用索引0表示。这个空白符的作用是分隔相邻的相同字符。比如模型连续输出了“我我[blank]爱爱[blank]中中中”经过CTC解码合并后得到“我爱中”。字符表里实际索引编号时要记得把0号位留给blank其余字符从1开始编号。我通常会把字符表保存成一个txt文件每行一个字符第一行固定留空。这样训练和预测时加载同一个文件避免因字符排序不一致导致预测结果错乱。3. 模型构建与训练策略3.1 模型结构设计CNN BiLSTM CTC我采用的模型结构可以拆解成三段第一段是卷积特征提取层。输入梅尔频谱图形状为[batch, time_steps, 80]。我们给它增加一个通道维度变成[batch, time_steps, 80, 1]然后经过几层Conv2D。这里使用CNN的目的是沿着频率轴和时间轴进行局部特征提取把80维的原始特征浓缩成更高层、更鲁棒的表征。常见做法是使用Conv2D(32, 3, strides2, paddingsame, activationrelu)这样的结构让时间轴逐步压缩。第二段是双向循环神经网络。在语音识别中当前帧的发音不仅受之前内容影响也受之后内容影响。所以使用双向LSTM每个时间步同时看前文和后文的信息。我经验上至少使用两层Bidirectional(LSTM(units128, return_sequencesTrue))units大小根据数据集量调整数据集越大units可以越大。第三段是分类输出层。用一个全连接Dense层输出每个时间步上属于每个字符包括blank的概率维度是[batch, time_steps, vocab_size]。训练时在这个输出上计算CTC损失预测时在这个输出上做解码。模型核心代码如下import tensorflow as tf from tensorflow.keras import layers, Model def build_model(input_dim, vocab_size): input_layer layers.Input(shape(None, input_dim), namespectrogram) x layers.Lambda(lambda t: tf.expand_dims(t, -1))(input_layer) # CNN特征提取 x layers.Conv2D(32, 3, strides2, paddingsame, activationrelu)(x) x layers.BatchNormalization()(x) x layers.Conv2D(32, 3, strides2, paddingsame, activationrelu)(x) x layers.BatchNormalization()(x) # 转换回时间序列格式 # 经过两次stride2后时间维约变为原来的 1/4 _, t_dim, freq_dim, ch_dim x.shape x layers.Reshape((t_dim, freq_dim * ch_dim))(x) # 双向LSTM x layers.Bidirectional(layers.LSTM(128, return_sequencesTrue))(x) x layers.Bidirectional(layers.LSTM(128, return_sequencesTrue))(x) # 分类输出 output layers.Dense(vocab_size, activationsoftmax, nameoutput)(x) model Model(inputsinput_layer, outputsoutput) return model注意这里CNN的strides和kernel size选择需要结合输入特征长度来调整。假设输入音频时长为2秒16000Hz采样率下总帧数为200帧经过两层strides2的CNN后时间维度大约变成50。如果你的音频特别短比如不足0.5秒卷积之后的时间步可能只有几个LSTM就学不到什么东西了。3.2 使用CTC损失函数对齐音频与文本CTC全称是Connectionist Temporal Classification专门用来解决“输入序列很长、输出序列很短”的序列对齐问题。举个例子“你好”两个字读出来大约0.6秒对应梅尔频谱大约60帧。模型要为这60帧中的每一帧都预测一个字符或blank。假设真实目标是“你好”但模型并不知道“你”具体对应哪一帧、“好”对应哪一帧。CTC允许模型把所有可能的帧对齐方式都考虑进来在训练时计算“所有能通过合并得到目标文本的对齐路径的总概率”并最大化这个总概率。实际使用TensorFlow的CTC损失函数时需要构造ctc_batch_cost的输入# 伪代码示意 loss tf.keras.backend.ctc_batch_cost( y_truelabel_sequences, # SparseTensor 或 padded mask y_predmodel_outputs, # shape [batch, time, vocab] input_lengthinput_lengths, # 每个样本的实际帧数 label_lengthlabel_lengths # 每个样本的标签长度 )CTC允许模型把所有可能的帧对齐方式都考虑进来在训练时计算“所有能通过合并得到目标文本的对齐路径的总概率”并最大化这个总概率。3.3 训练细节学习率、Batch Size与Early Stopping语音识别训练最难伺候的不是模型结构而是“怎么训得动”。基于我的实操经验有几个参数值得重点关注Batch Size建议从16或者32开始。Batch太小梯度震荡剧烈loss曲线很难看Batch太大会导致显存溢出。如果GPU显存只有6G左右建议用16。学习率建议初始化1e-3并配合ReduceLROnPlateau回调当验证集loss连续3轮不降时学习率自动乘以0.5。不要从头到尾用一个固定学习率死磕。Early Stopping监控验证集loss连续5轮不下降就停止训练同时保存最优模型权重。还有一个很重要的点数据padding不能污染计算。由于音频时长不一我们在组batch时会把短音频的特征尾部补零到和最长音频等长。但CTC要求输入长度必须真实有效所以补零部分的帧数不能计入input_length。训练时务必把补零部分的输出概率在计算loss前排除掉最方便的方式是使用tf.keras.backend.ctc_batch_cost并传入真实的输入长度。4. 模型解码与识别测试4.1 两种解码方式贪心解码与束搜索训练完模型之后要把概率序列转成文字这一步叫解码。最简单的是贪心解码每一步都取概率最大的字符然后把相邻重复的字符合并再删掉blank。例如模型在10个时间步上输出的概率最大值索引为[3, 3, 0, 0, 7, 7, 7, 0, 9, 9]贪心解码过程如下先做合并去重连续重复的3合并为一个3连续重复的7合并为一个7连续重复的9合并为一个9结果为[3, 0, 7, 0, 9]。再删掉blank索引0最终得到[3, 7, 9]对应字符表即“你”“好”“啊”。贪心解码速度快但精度有限因为每一步只考虑当下的最优忽略了整体路径。如果想要更高准确率可以使用束搜索Beam Search保存前N条候选路径最后取整体概率最高的路径。TensorFlow Addons库提供了tfa.seq2seq.decoder相关工具也可以用第三方实现的ctcdecode库解码。考虑到毕设场景如果你不需要参加比赛冲SOTA贪心解码完全够用。我用贪心解码在自测音频上准确率大约在85%左右而引入束搜索后能提升到88%左右提升有限但耗时增加明显。4.2 完整识别函数实现下面是项目中最核心的“加载音频-预测-输出文字”完整代码import numpy as np import tensorflow as tf class SpeechRecognizer: def __init__(self, model_path, char_list_path, sample_rate16000): self.model tf.keras.models.load_model(model_path, compileFalse) self.sample_rate sample_rate self.char_list self._load_char_list(char_list_path) def _load_char_list(self, path): with open(path, r, encodingutf-8) as f: chars [line.strip() for line in f.readlines()] # 保证第一个是blank return chars def _preprocess(self, audio_path): audio_binary tf.io.read_file(audio_path) audio, sr tf.audio.decode_wav( audio_binary, desired_channels1, desired_samplesself.sample_rate ) audio tf.squeeze(audio, axis-1) audio audio * 32767.0 stfts tf.signal.stft(audio, frame_length400, frame_step160, fft_length512) spectrograms tf.abs(stfts) linear_to_mel_weight_matrix tf.signal.linear_to_mel_weight_matrix( num_mel_bins80, num_spectrogram_binsspectrograms.shape[-1], sample_rateself.sample_rate, lower_edge_hertz0.0, upper_edge_hertz8000.0 ) mel_spectrograms tf.tensordot(spectrograms, linear_to_mel_weight_matrix, 1) log_mel_spectrograms tf.math.log(mel_spectrograms 1e-6) # 增加batch维度 log_mel_spectrograms tf.expand_dims(log_mel_spectrograms, 0) return log_mel_spectrograms def recognize(self, audio_path): features self._preprocess(audio_path) predictions self.model.predict(features, verbose0) # shape [1, time, vocab] decoded self._greedy_decode(predictions[0]) text .join([self.char_list[idx] for idx in decoded if idx ! 0]) return text def _greedy_decode(self, pred): # pred shape [time, vocab] seq np.argmax(pred, axis-1).tolist() # 合并重复并去除blank decoded [] previous None for idx in seq: if idx ! previous: decoded.append(idx) previous idx return decoded4.3 推理性能优化从模型到部署训练完成后我们会发现每次预测都要重新加载整个模型速度尚可但不够优雅。如果想让识别系统响应更快有两个优化方向模型转TensorFlow Lite格式如果最终要部署到移动端或嵌入式设备可以将模型转换为.tflite格式推理速度会快不少。转换时需要注意动态时间维度问题。使用ONNX Runtime加载模型将keras模型导出为ONNX格式可以用CPU上的ONNX Runtime加速。实测在普通笔记本上对1秒音频的识别耗时能从300ms下降到150ms左右。不过对于课程设计和毕设验收来说模型能正确加载、能稳定输出结果已经足够。真正要花心思优化的是识别准确率。5. 常见问题与排查技巧实录5.1 问题速查表如果你照着我前面的流程做大概率会遇到以下几个典型问题。我把它们整理成一张速查表方便对照排查。常见问题典型表现可能原因解决办法音频加载失败tf.io.decode_wav报错或shape不对音频采样率不是16000Hz或文件是双声道统一用ffmpeg转成单声道16000Hz模型loss不降训练几轮loss值一直停在某个高位学习率过大、字符表损坏、标签长度与输入长度差距过大调低学习率检查标签编码序号是否正确预测出来全是空字符串识别结果返回空或大量乱码解码时blank索引和字符索引错位确认字符表第一个是blank且模型输出维度等于字符表长度显存不足OOM程序崩在模型训练阶段Batch Size太大或音频padding太长减小Batch Size限制最大音频时长模型预测速度极慢单条音频耗时十几秒padding过多LSTM计算了太多无效帧按实际音频长度分批预测去掉尾部无效padding中文识别有谐音错字“你”识别成“里”“是”识别成“四”缺少语言模型修正加入N-gram语言模型或对结果做纠错后处理5.2 经验心得先跑通一条样本再谈准确率我见过太多人死在“想一步到位”上。刚装好TensorFlow就想着直接用大数据集训练一个超高准确率的模型结果数据加载环节耗时几小时最后在训练阶段发现loss出问题又得回头查数据。白白浪费大量时间。我的建议是第一次跑通全流程时只取数据集中的100条音频训练1个epoch。目标是验证“数据加载-模型前向传播-计算loss-反向传播-解码输出-文字显示”整条链路是否通畅。只要这100条音频能跑完、能输出文字再逐步增加数据和训练轮次。这样每一轮操作都有反馈排错成本更低。5.3 针对毕业设计的加分项建议如果你做的是课程设计或毕业设计想拿高分有几个加分项不需要太多额外成本加一个简单的Web识别页面用Flask写一个上传音频的接口把训练好的模型包成一个类用户上传wav文件后返回识别文字。整个代码量不大但演示效果非常好。加一个实时录音识别功能通过pyaudio录音后保存为临时wav再走正常识别流程。这样答辩时可以直接现场说一句话系统立刻显示文字视觉冲击力远胜于放一段预录音频。画出训练过程的loss曲线用matplotlib把训练集和验证集loss画出来插入到设计文档中同时配合几个测试音频的识别结果对比表格逻辑上就非常完整了。做一个模型对比实验比如“只使用CNN特征提取”和“CNNBiLSTM”两个模型的准确率对比或者“不同学习率下loss曲线的对比”这些实验工作量不大但对于论文写作和答辩准备非常有利。6. 最后的实操总结与个人体会从零搭建一个语音识别系统核心难处从来不在“模型结构”上而在“数据怎么喂进模型”和“训练出了问题时怎么定位”。TensorFlow生态把很多底层算法都封装好了我们要做的就是把音频处理、字符映射、CTC损失这三条线串在一起形成一个闭环。我个人的体会是遇到问题先从最小单元排查单独把音频读出来看看波形单独把标签打印出来看看字符编号单独用一条音频做前向传播看看输出的shape是否合理。养成这种“拆步骤验证”的习惯后你会发现所谓复杂项目其实就是一个一个简单步骤的拼接。这次分享的整套流程我已经在多个项目里验证过你按照上面的步骤操作大概率能在半天内跑通训练链路。后续你还可以自行扩展替换成更先进的Conformer模型、加入注意力机制、引入语言模型重打分或把系统做成可实时交互的语音助手。语音识别是一个容易“越做越深”的方向但先把基础的跑通、跑稳才有资本去谈后面的优化和扩展。本文还有配套的精品资源点击获取
返回列表