中文语音识别毕设级实操:从Fbank特征到CTC解码全流程解析
简介面向中文语音识别研究与毕业设计场景这份资源以深度学习为主线完整覆盖声学模型与语言模型两大部分声学侧包含 CNN-CTC、GRU-CTC、CNN-RNN-CTC 等结构语言侧引入 Transformer 与 CBHG并配套 thchs30、Aishell、stc、primewords 四类数据集可直接用于复现训练流程与识别效果。压缩包共 29 个文件约 108.36MB主要包含 Python 训练与推理代码、多个模型讲解 Notebook、预训练权重、TensorFlow checkpoint 文件、数据集文本列表及说明文档目录按声学模型、语言模型、数据处理等模块划分便于按需取用。目前已有 4158 人学习或浏览适合准备毕业设计、课程报告或入门语音识别的同学作为完整工程参考。资源内提供多种模型对比实现与训练好的权重文件可观察不同结构在中文语音数据上的表现同时涵盖数据处理、模型定义、训练与识别等完整环节便于二次实验与方案扩展。1. 中文语音识别毕设级资源难的不是模型而是数据流和解码做过语音识别的人都有个体会深度学习模型结构倒是其次真正让人反复折腾的是从音频到特征、从特征到序列标注、再从模型输出到可读文本这条完整链路。这套“基于深度学习的中文语音识别系统”是一份完整毕业设计资源把语音识别最常见的工程路径走通了数据预处理、80维Fbank特征提取、CNN下采样加Transformer编码器、CTC对齐、最后还能解码出拼音或汉字。它适合两类人一类是毕设要做语音识别、需要一份能跑通全流程的参考实现另一类是已经知道深度学习基本API、但想知道语音数据怎么进模型、CTC长度对齐到底怎么处理的开发者。我把它拆开跑了一遍下面按实际使用顺序讲清楚每一环怎么用、参数怎么设、坑在哪。2. 数据准备与特征提取80维Fbank与数据加载器的实现2.1 数据集先统一成JSON清单拿到压缩包先别急着训模型第一步是把语音数据整理成“路径文本”的结构。原项目兼容THCHS-30、AISHELL-1这类按发音标注的中文数据集标注文件格式不一有的是.wav.trn有的是纯文本列表。我一般先把所有标注扫一遍写一个转写脚本最终得到一个统一的JSON清单每行是一个样本包含wav_path和text。搜索热词里频繁出现的“语音识别”“语音数据集整理”指的就是这一步数据整理决定了后面所有环节是否顺利。import json import glob import os def build_manifest(data_root, save_pathtrain.json): samples [] for wav_path in glob.glob(os.path.join(data_root, *.wav)): # 假设标注文件与wav同目录、同名但后缀是 .trn label_path wav_path.replace(.wav, .trn) if not os.path.exists(label_path): continue with open(label_path, r, encodingutf-8) as f: lines [l.strip() for l in f if l.strip()] # THCHS-30的trn前两行上一行汉字、下一行拼音这里取汉字 text lines[-2] if len(lines) 2 else lines[0] samples.append({wav_path: wav_path, text: text}) with open(save_path, w, encodingutf-8) as f: json.dump(samples, f, ensure_asciiFalse, indent1) print(f共收集 {len(samples)} 条样本)这段代码做了两件关键事情一是用glob遍历原始wav文件二是把trn标注里的汉字提取出来。标注格式如果不确定可以先打印一两条再定提取规则。参数方面data_root是原始音频目录save_path是输出的manifest文件路径后续所有训练和评估都只依赖这个JSON不再碰原始标注避免每次重复解析。2.2 Fbank特征提取参数直接影响识别率语音识别里最常用的声学特征是Fbank也叫Mel频谱特征它比MFCC保留更多原始信息配合神经网络效果更好。原项目做的是80维Fbank这是当前语音识别的主流选择。特征提取核心逻辑是预加重、分帧、加窗、短时傅里叶变换、Mel滤波、取对数。下面的函数实现了完整流程import torch import torchaudio def compute_fbank(wav_path, n_mels80): waveform, sr torchaudio.load(wav_path) # 单声道归一化统一采样率到16k这一步很关键 if waveform.size(0) 1: waveform torch.mean(waveform, dim0, keepdimTrue) if sr ! 16000: waveform torchaudio.functional.resample(waveform, sr, 16000) sr 16000 # 帧长25ms、帧移10ms等价于 win_length400、hop_length160 fbank torchaudio.compliance.kaldi.fbank( waveform, num_mel_binsn_mels, frame_length25.0, frame_shift10.0, sample_frequency16000, ) return fbank # 形状 [T, n_mels]这段是纯PyTorch实现没有额外依赖。参数上n_mels取80是主流配置取40会让短元音信息损失明显取128则训练变慢且容易过拟合。frame_length 25ms和frame_shift 10ms是语音识别里默认给得最多的组合模拟人耳对短时信号的感知。返回的fbank形状是[T, 80]T是帧数8秒音频大概对应800帧左右。2.3 DataLoader与batch padding深度学习训练必须按batch进模型但语音片段长短不一不能直接stack。解决办法是按时长排序分组同一个batch内padding到一样长并记录原始长度给CTC用。我按原项目思路写了一个标准写法def collate_fn(batch): wavs, texts, feats [], [], [] for wav_path, text in batch: wavs.append(wav_path) texts.append(text) feats.append(compute_fbank(wav_path)) feats torch.nn.utils.rnn.pad_sequence(feats, batch_firstTrue) feats feats.transpose(1, 2) # [B, 80, T]适配CNN输入 input_lengths torch.LongTensor([f.size(0) for f in feats]) # 文本转成index序列并记录目标长度 return feats, texts, input_lengths这里把特征形状转成[B, 80, T]是因为后面卷积层的输入约定是“通道维在第二维”。注意input_lengths存的是每句话的帧数之后计算CTC损失和预测时必须用它不然padding部分会被当成有效语音参与梯度计算导致loss异常。texts先保留原始字符串在训练循环里再转成token索引这样可以灵活处理词典。3. 模型结构与训练循环CNN下采样、Transformer编码器与CTC对齐3.1 为什么用CNNTransformerCTC中文语音识别近几年的主流做法是前端用几层卷积做时域降采样、压缩序列长度中间用Transformer编码器建模上下文最后用CTC做帧级对齐。相比全程Attention的端到端方案CTC有个很实际的优势不需要强行让每一帧对应到每个字只要能对齐出“最可能的路径”就行训练容易稳定。这个项目用的就是这条链路属于毕业设计里实现成本低但效果上限高的组合。下面的模型代码是整个项目的核心也建议你在自己的任务里直接复用骨架。3.2 模型实现卷积降采样与TransformerEncoderimport torch.nn as nn class ConvFrontend(nn.Module): def __init__(self, in_channels1, out_channels32): super().__init__() self.convs nn.Sequential( nn.Conv2d(in_channels, 32, kernel_size3, stride1, padding1), nn.ReLU(), nn.BatchNorm2d(32), nn.Conv2d(32, out_channels, kernel_size3, stride2, padding1), nn.ReLU(), ) def forward(self, x): # x: [B, 1, 80, T] x self.convs(x) x x.mean(dim2) # 压缩频率维得到 [B, 32, T/2] return x.transpose(1, 2) # [B, T/2, 32] class ASRModel(nn.Module): def __init__(self, vocab_size, d_model256, nhead4, num_layers6): super().__init__() self.frontend ConvFrontend(1, d_model) self.encoder nn.TransformerEncoder( nn.TransformerEncoderLayer(d_modeld_model, nheadnhead, dim_feedforward1024), num_layersnum_layers, ) self.proj nn.Linear(d_model, vocab_size) def forward(self, fbank, input_lengths): # fbank: [B, 80, T] fbank fbank.unsqueeze(1) x self.frontend(fbank) # [B, T/2, d_model] # Transformer要求输入[T, B, d_model]需要转置 x x.transpose(0, 1) # [T/2, B, d_model] x self.encoder(x) logits self.proj(x) # [T/2, B, vocab_size] return logits.transpose(0, 1) # [B, T/2, vocab_size]参数含义ConvFrontend里第一个卷积保持时间分辨率第二个卷积stride2把时间维砍半这样8秒音频800帧变成400帧显著降低Transformer计算量这是前端降采样的核心作用。d_model256是Transformer隐层维度nhead4是注意力头数num_layers6是编码器层数。如果训练数据只有几十小时把num_layers降到4或d_model降到192能明显减少过拟合。前端的BatchNorm在batch较小时容易不稳定数据量少时建议用LayerNorm替代。3.3 CTC损失与长度对齐约束CTC不需要提前知道每个字对应哪几帧它把所有可能的帧-字对齐路径都纳入计算。但有一个硬性约束模型输出的时间步必须大于等于文本token数否则该样本无法计算损失。训练时使用torch.nn.CTCLoss需要传入三个关键量logits序列、目标token序列、输入长度和目标长度。第一次跑的人常在这里翻车input_lengths传错导致loss为nan或正无穷。criterion nn.CTCLoss(blank0, zero_infinityTrue) def compute_ctc_loss(logits, targets, input_lengths, target_lengths): # logits: [B, T, V]CTC要求 [T, B, V]所以转置 logits logits.transpose(0, 1) # [T, B, V] loss criterion( logits.log_softmax(2), # 内部允许用logits但log_softmax更稳 targets, input_lengths, target_lengths, ) return loss注意CTCLoss的两个细节blank索引设为0所以词典里第0位必须是空字符zero_infinityTrue能把因为对齐不可能导致的无穷loss置为0避免训练直接崩掉。target_lengths是每个样本的文本token数必须和targets的实际长度严格对应多一个少一个都会报错或算出无意义的结果。4. 训练参数与日志判读学习率、梯度裁剪与checkpoint管理4.1 训练参数配置表训练语音识别模型的学习率设置比一般图像分类更敏感因为Transformer编码器对学习率波动非常敏感。下面这组参数我在类似项目上用过多次稳定性和收敛速度都比较合适参数名推荐值说明batch_size168秒音频下16G显存正好learning_rate3e-4TransformerCTC常用初始值warmup_steps1000前1000步从1e-5线性升到3e-4grad_clip5.0按全局梯度范数裁剪max_epochs60配合早停机制使用optimizerAdamWweight_decay设为1e-4为什么不直接用SGDTransformer层里的LayerNorm和attention对梯度尺度敏感AdamW的自适应学习率能让训练更稳。warmup的意义是前期防止梯度方向乱跳等模型见过足够多batch后再加大学习率。grad_clip5.0是经验值如果日志里出现“Exploding gradient”或loss突然跳到几十就把它降到2.0再试。4.2 训练主循环与checkpointdef train_one_epoch(model, loader, optimizer, scheduler, device): model.train() total_loss 0 for batch_idx, (feats, texts, input_lengths) in enumerate(loader): feats feats.to(device) input_lengths input_lengths.to(device) # 文本转token targets, target_lengths encode_texts(texts, vocab) optimizer.zero_grad() logits model(feats, input_lengths) loss compute_ctc_loss(logits, targets, input_lengths, target_lengths) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), 5.0) optimizer.step() scheduler.step() total_loss loss.item() if batch_idx % 50 0: print(fbatch {batch_idx} loss {loss.item():.4f}) return total_loss / len(loader)这里encode_texts会把每个汉字映射成词典中的索引。scheduler用的是warmup指数衰减warmup阶段按step更新之后按epoch衰减到3e-5。checkpoint保存建议在模型精度最高时保存而不是最后一个epoch因为CTC训练的后期loss下降缓慢但CER还在变好。我习惯每个epoch结束在验证集上算一次字符错误率只保留CER最低的模型再额外保留最近一次checkpoint作为后悔药。4.3 日志判读什么情况算正常训练过程中loss在前5个epoch从几十快速下降到10以下是非常正常的因为CTC一开始的空路径占比很高。第10个epoch以后loss下降到3-5之间说明模型开始真正学习声学映射关系。如果出现两种情况要立刻停loss在几个epoch内完全不降且浮在初始值附近说明学习率过高或前端卷积初始化有问题loss下降很快但验证集CER不降反升说明已经过拟合应该回头减小模型宽度或增加数据增强。语音识别训练是黑匣子里的常见体验日志里的数值比肉眼听觉更可靠。5. 避坑清单数据、显存、解码时的真实翻车点踩坑这一章是我跑这套代码时最想写的内容。很多问题不是代码逻辑错而是细节没说清。5.1 坑一CTC loss直接变成nan或无穷大现象训练一开始loss就是nan或者打印出来是inf。原因最常见的是input_lengths传成了padding后的长度CTC把无效帧也参与对齐路径中score出现无穷值另一个原因是某个样本的文本token数大于卷积降采样后的帧数根本无法对齐。解决在计算loss前加一个断言逐个样本检查output_length target_length。我的做法是在collate_fn里直接过滤掉不合格的样本并把input_lengths设为降采样前的实际帧数除以2之后的值确保对齐可能性存在。5.2 坑二batch里有一句超长音频导致OOM现象batch_size设为16大部分batch正常但隔几个batch就OOM程序直接被杀死。原因语音数据长度方差极大最短的0.5秒最长的有30秒。如果随机组batch这一批8句话的总帧数可能超过其他batch的5倍显存峰值由最长样本决定。解决用bucket sampler按时长分桶把长度相近的样本放一个batch。代码上只用PyTorch官方Sampler做排序即可先按input_lengths从短到长排序每batch内尽量等长。这个改动能让显存占用降低30%-50%训练速度也更快属于血泪经验。5.3 坑三模型输出全是重复字现象训练结束后用贪心解码识别结果出来的是“好好好好好……”这种大量重复。原因编码器对相邻帧输出的分布太接近CTC贪心路径把多个连续相同token输出成重复字。如果连CTC自身的blank都没有正确激活模型会把相同帧连续判定成同一个字。解决不要直接用argmax结果当成最终文本。正确做法是用CTC prefix beam search解码它能利用路径融合机制把连续相同token合并。如果手里的版本只有贪心解码可以把输出中连续重复的token合并再把blank删掉作为快速验证。完整beam search代码写在最后一个章节。5.4 坑四中文标点符号导致词表失控现象loss正常但CER字符错误率高得离谱或评估脚本报KeyError。原因标注文件里有空格、逗号、句号如果直接按字符切分vocab里会带出一堆标点符号导致和标签对照时错位。解决在构建词典和编码文本时统一过滤掉所有非中文和非发音字符。过滤规则要训练集和验证集共用一份不能两套逻辑。评估时同样把预测结果里的标点删掉再算CER只保留汉字。5.5 坑五beam search比贪心解码还差现象好不容易接好CTC beam search解码识别率反而比贪心解码更低。原因有空桶或合并逻辑写的次数不对。常见错误是beam search没有按blank和重复token合并导致路径权重计算重复或者beam_width太小等于白做。解决把beam_width设为10试试然后逐个检查解码函数返回的token是否还有连续重复。可以在验证集上打印几条解码结果和参考文本做人工对比。通常beam search在等于1时退化成贪心越大效果越好但速度越慢。项目里给的是经典CTC decoders实现建议保留原始逻辑不要自己重写合并分支。6. 验证与调优字符错误率、单样本冒烟测试与前后鼻音排查6.1 字符错误率计算与快速验证语音识别评估指标用CERCharacter Error Rate计算的是最小编辑距离除以参考字符数。这里是精简实现的思路工程上我常直接用开源的editdistance库但样本少时手写也够用。项目报告里给出的CER统计就是基于这个口径。跑评估前留意一点删除标点后两者再比较。import editdistance def compute_cer(preds, refs): total_ed 0 total_len 0 for pred, ref in zip(preds, refs): pred clean_text(pred) ref clean_text(ref) total_ed editdistance.eval(pred, ref) total_len len(ref) return total_ed / total_lenclean_text会过滤全角标点、空格和英文符号只保留中文字符和数字。这个口径与原项目的评估逻辑一致CER低于0.3说明模型基本可用低于0.2是及格水平接近0.1则是效果很好的状态。6.2 单样本冒烟测试先跑通一条完整链路每次拿到新数据、新模型或者改了解码代码我建议先跑一个单样本冒烟测试而不是直接上整个验证集。方法如下随便取一条wav打印它的帧数、token数、模型输出logits的shape再走一遍解码函数。如果这条链路能在20秒内跑完且输出文字不是乱码再开完整评估。这个习惯能帮你拦截超过一半的“代码写好了但哪里都报错”的情况。另外排查识别错误时可以关注容易混淆的音频——前后鼻音“in/ing”、平翘舌“z/zh”、“c/ch”、声调“shí/shǐ”。如果模型在某个音上系统性出错不要急着加大模型先在训练集里统计这些字的出现频次很多情况下是“shi4”这类词样本太少导致欠拟合。通过调整数据采样权重或者把对应拼音补充进词典能比换模型结构更快见效。从那以后我每次拿到一套新的语音识别代码都强制走一遍“单样本冒烟长度断言CER口径核对”这三步流程看起来繁琐却能避开大部分黑匣子问题。希望这份拆解能帮你在做毕设和项目时少走弯路也希望帮到你。本文还有配套的精品资源点击获取