简介:面向中文语音识别研究与毕业设计场景,这份资源以深度学习为主线,完整覆盖声学模型与语言模型两大部分:声学侧包含 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_path="train.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", encoding="utf-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", encoding="utf-8") as f: json.dump(samples, f, ensure_ascii=False, indent=1) 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_mels=80): waveform, sr = torchaudio.load(wav_path) # 单声道归一化,统一采样率到16k,这一步很关键 if waveform.size(0) > 1: waveform = torch.mean(waveform, dim=0, keepdim=True) if sr != 16000: waveform = torchaudio.functional.resample(waveform, sr, 16000) sr = 16000 # 帧长25ms、帧移10ms,等价于 win_length=400、hop_length=160 fbank = torchaudio.compliance.kaldi.fbank( waveform, num_mel_bins=n_mels, frame_length=25.0, frame_shift=10.0, sample_frequency=16000, ) 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_first=True) 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 为什么用CNN+Transformer+CTC
中文语音识别近几年的主流做法是:前端用几层卷积做时域降采样、压缩序列长度,中间用Transformer编码器建模上下文,最后用CTC做帧级对齐。相比全程Attention的端到端方案,CTC有个很实际的优势:不需要强行让每一帧对应到每个字,只要能对齐出“最可能的路径”就行,训练容易稳定。这个项目用的就是这条链路,属于毕业设计里实现成本低但效果上限高的组合。下面的模型代码是整个项目的核心,也建议你在自己的任务里直接复用骨架。
3.2 模型实现:卷积降采样与TransformerEncoder
import torch.nn as nn class ConvFrontend(nn.Module): def __init__(self, in_channels=1, out_channels=32): super().__init__() self.convs = nn.Sequential( nn.Conv2d(in_channels, 32, kernel_size=3, stride=1, padding=1), nn.ReLU(), nn.BatchNorm2d(32), nn.Conv2d(32, out_channels, kernel_size=3, stride=2, padding=1), nn.ReLU(), ) def forward(self, x): # x: [B, 1, 80, T] x = self.convs(x) x = x.mean(dim=2) # 压缩频率维,得到 [B, 32, T/2] return x.transpose(1, 2) # [B, T/2, 32] class ASRModel(nn.Module): def __init__(self, vocab_size, d_model=256, nhead=4, num_layers=6): super().__init__() self.frontend = ConvFrontend(1, d_model) self.encoder = nn.TransformerEncoder( nn.TransformerEncoderLayer(d_model=d_model, nhead=nhead, dim_feedforward=1024), num_layers=num_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里第一个卷积保持时间分辨率,第二个卷积stride=2把时间维砍半,这样8秒音频800帧变成400帧,显著降低Transformer计算量,这是前端降采样的核心作用。d_model=256是Transformer隐层维度,nhead=4是注意力头数,num_layers=6是编码器层数。如果训练数据只有几十小时,把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(blank=0, zero_infinity=True) 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_infinity=True能把因为对齐不可能导致的无穷loss置为0,避免训练直接崩掉。target_lengths是每个样本的文本token数,必须和targets的实际长度严格对应,多一个少一个都会报错或算出无意义的结果。
4. 训练参数与日志判读:学习率、梯度裁剪与checkpoint管理
4.1 训练参数配置表
训练语音识别模型的学习率设置比一般图像分类更敏感,因为Transformer编码器对学习率波动非常敏感。下面这组参数我在类似项目上用过多次,稳定性和收敛速度都比较合适:
| 参数名 | 推荐值 | 说明 |
|---|---|---|
| batch_size | 16 | 8秒音频下16G显存正好 |
| learning_rate | 3e-4 | Transformer+CTC常用初始值 |
| warmup_steps | 1000 | 前1000步从1e-5线性升到3e-4 |
| grad_clip | 5.0 | 按全局梯度范数裁剪 |
| max_epochs | 60 | 配合早停机制使用 |
| optimizer | AdamW | weight_decay设为1e-4 |
为什么不直接用SGD?Transformer层里的LayerNorm和attention对梯度尺度敏感,AdamW的自适应学习率能让训练更稳。warmup的意义是前期防止梯度方向乱跳,等模型见过足够多batch后再加大学习率。grad_clip=5.0是经验值,如果日志里出现“Exploding gradient”或loss突然跳到几十,就把它降到2.0再试。
4.2 训练主循环与checkpoint
def 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(f"batch {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 字符错误率计算与快速验证
语音识别评估指标用CER(Character 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口径核对”这三步流程,看起来繁琐,却能避开大部分黑匣子问题。希望这份拆解能帮你在做毕设和项目时少走弯路,也希望帮到你。
本文还有配套的精品资源,点击获取