简介:这是一套面向计算机相关专业学生与项目实战学习者的中文语音识别毕业设计资源,基于Python与深度学习框架实现,适合用作毕设、课程设计或期末大作业。项目经导师指导并认可,可直接作为毕设使用,也便于学习者理解语音识别从数据到建模的完整流程。压缩包共88个文件,约34.54MB,以py源码、txt说明、lst数据列表为主,另含tsv、pkl、md等文件,涵盖声学模型、语言模型、数据处理与训练脚本等模块,目录结构清晰,便于按模块查阅。资源包含全部项目源码与文档说明,经过严格调试,确保可以运行,读者可据此掌握GRU-CTC、CNN-CTC等声学建模思路,以及CBHG语言模型、特征提取与数据预处理等关键环节,并参考超参数配置与训练流程进行复现实验。目前已有216人学习下载,适合需要完整项目参考与实战练习的读者。
1. 中文语音识别系统:从一段 wav 到一行文字的完整链路
一段 16kHz、单声道、时长 3 秒的中文 wav 文件,丢进一个基于 Python 的深度学习语音识别系统,几秒后屏幕上打出一行汉字——这件事听起来简单,但真正动手做过的人都知道,从「能跑通」到「识别准」中间隔着一堆玄学。很多同学做毕业设计时,第一反应是去 GitHub 找一个现成的中文语音识别系统源码,clone 下来,装依赖,跑 demo,结果卡在环境配置、数据格式、模型加载上,折腾一周连第一个字都没识别出来。
这个方向的核心,是用 Python 把「音频特征提取 + 深度学习声学模型 + 解码输出文本」串成一条可复现的流水线。它适合两类人:一类是计算机相关专业的毕业生,需要一个有技术含量、能写进论文、能演示的系统;另一类是想入门语音方向的工程师,需要一个能跑通、能改参数、能看到中间结果的工程骨架。下面我会按「先搞清楚系统由什么组成 → 再动手搭最小可跑版本 → 然后处理中文特有的坑 → 最后讲怎么验证和调优」的顺序,把这条链路拆开讲清楚。
2. 中文语音识别系统的四个核心模块与选型逻辑
2.1 音频前端:为什么中文场景对采样率和静音切除更敏感
语音识别的第一步不是模型,是音频预处理。中文是声调语言,同一个音节不同声调代表完全不同的字,比如「妈麻马骂」,声调信息主要靠基频(F0)的走向来区分。如果采样率太低,高频的共振峰信息丢失,声学模型就很难区分韵母相近的字。常见做法是统一到 16kHz、16bit、单声道 PCM 格式,这个配置在中文开源数据集里是事实标准。
预处理里最容易被忽略的是静音切除和音量归一化。一段录音前后往往有大段空白,如果直接整段送进模型,不仅浪费算力,还会让模型学到大量「静音对应空白」的无效映射。我一般用能量阈值做 VAD(语音活动检测),把有效语音段切出来,再做峰值归一化,让不同录音设备的音量拉到同一水平。
import numpy as np import librosa def preprocess_audio(wav_path, target_sr=16000): # 统一采样率,librosa 会自动做重采样 waveform, sr = librosa.load(wav_path, sr=target_sr, mono=True) # 峰值归一化,避免不同设备音量差异 waveform = waveform / (np.max(np.abs(waveform)) + 1e-8) # 简单的能量 VAD:帧长 25ms,帧移 10ms frame_len = int(0.025 * target_sr) hop_len = int(0.010 * target_sr) frames = librosa.util.frame(waveform, frame_length=frame_len, hop_length=hop_len) energy = np.sum(frames ** 2, axis=0) threshold = np.mean(energy) * 0.1 # 经验阈值,可调 voiced_idx = np.where(energy > threshold)[0] if len(voiced_idx) == 0: return waveform # 全静音,直接返回 start = voiced_idx[0] * hop_len end = min(len(waveform), (voiced_idx[-1] + 1) * hop_len + frame_len) return waveform[start:end]这段代码做了三件事:重采样到 16kHz、峰值归一化、基于短时能量的静音切除。threshold这个系数 0.1 是经验值,如果录音底噪大,可以调到 0.2 到 0.3;如果语音很轻,调到 0.05。注意librosa.load默认会做重采样,如果你的原始音频是 8kHz,重采样到 16kHz 并不会凭空增加信息,但能保证和预训练模型输入一致。
2.2 特征提取:MFCC 还是 Mel 频谱,中文场景怎么选
特征提取是把波形变成模型能吃的二维矩阵。传统方案用 MFCC(梅尔频率倒谱系数),一般是 13 维或 40 维,加上一阶、二阶差分。深度学习方案更常用 Mel 频谱图,因为卷积网络可以直接在频谱图上滑动,保留更多频域结构信息。
中文语音识别里,我倾向于用 80 维 Mel 滤波器组特征,帧长 25ms,帧移 10ms。原因是中文的声调信息在基频和低次谐波上,Mel 滤波器组在低频段分辨率高,能保留更多声调线索。MFCC 的 DCT 变换会丢掉一部分相位和细节,对声调区分反而不利。
def extract_mel(waveform, sr=16000, n_mels=80, n_fft=400, hop_length=160): # n_fft=400 对应 25ms,hop_length=160 对应 10ms mel = librosa.feature.melspectrogram( y=waveform, sr=sr, n_fft=n_fft, hop_length=hop_length, n_mels=n_mels, fmin=20, fmax=8000 # 中文有效频段,去掉极低频噪声和极高频 ) # 取对数,压缩动态范围 log_mel = librosa.power_to_db(mel, ref=np.max) return log_mel # 形状 (n_mels, time_frames)n_mels=80是常见配置,太小会丢频域细节,太大则增加计算量且容易过拟合。fmin=20去掉直流和极低频噪声,fmax=8000是 16kHz 采样率的奈奎斯特频率。power_to_db做对数压缩,让模型对音量变化更鲁棒。输出的矩阵形状是(80, T),T 是时间帧数,后续送进 CNN 或 RNN 时通常要转置成(T, 80)。
2.3 声学模型:CNN、RNN 还是 Conformer,毕业设计怎么选不翻车
声学模型是系统的核心,负责把特征矩阵映射成音素或汉字序列。常见有三条路线:CNN 加 CTC、RNN(LSTM/GRU)加 CTC、Conformer(卷积加自注意力)加 CTC 或注意力解码。
对于毕业设计,我的建议是:如果算力有限(单卡 8GB 以下),用 CNN + BiLSTM + CTC,参数量控制在 10M 以内,训练数据用 THCHS-30 或 AISHELL-1 的子集,能在几小时内跑出可演示的效果。如果算力充足(单卡 16GB 以上),可以上 Conformer,但要注意 Conformer 对数据量要求高,小数据集上容易过拟合,需要加 SpecAugment 和 Dropout。
import torch import torch.nn as nn class CNN_BiLSTM_CTC(nn.Module): def __init__(self, n_mels=80, n_class=5000, hidden=256): super().__init__() # 卷积层提取局部频域特征 self.cnn = nn.Sequential( nn.Conv2d(1, 32, kernel_size=3, padding=1), nn.ReLU(), nn.MaxPool2d(kernel_size=(2, 1)), # 只压缩频域,保留时间 nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.ReLU(), nn.MaxPool2d(kernel_size=(2, 1)), ) # BiLSTM 建模时间依赖 self.lstm = nn.LSTM(64 * 20, hidden, num_layers=2, bidirectional=True, batch_first=True) # 输出层映射到字符类别 self.fc = nn.Linear(hidden * 2, n_class) def forward(self, x): # x: (B, 1, n_mels, T) x = self.cnn(x) # (B, 64, 20, T) B, C, F, T = x.shape x = x.permute(0, 3, 1, 2).reshape(B, T, C * F) # (B, T, 1280) x, _ = self.lstm(x) # (B, T, 512) logits = self.fc(x) # (B, T, n_class) return logitsn_class是字符类别数,中文常用 5000 到 6000 个常用字,加上空白符和 CTC 的 blank 标签。hidden=256是 LSTM 隐藏层维度,双向拼接后是 512。卷积层里MaxPool2d(kernel_size=(2, 1))只在频域做池化,时间维度保留,这样 CTC 的时间对齐不会被打乱。训练时用CTCLoss,注意输入长度和标签长度要按 batch 传入。
2.4 解码器:CTC 贪心解码和 Beam Search 的差距在哪
模型输出的是每个时间步的字符概率分布,解码器负责把它变成最终文本。最简单的是 CTC 贪心解码:每个时间步取概率最大的字符,去掉重复和 blank。这种做法快,但容易漏字或重复。
Beam Search 保留多条候选路径,结合语言模型打分,能显著降低字错误率。中文场景里,我一般用 Beam Search 加一个字符级 n-gram 语言模型,beam width 设 10 到 20。如果算力紧张,可以用前缀 Beam Search,在 CTC 框架下更高效。
def ctc_greedy_decode(logits, idx2char): # logits: (T, n_class) preds = torch.argmax(logits, dim=-1).cpu().numpy() result = [] prev = -1 for p in preds: if p != prev and p != 0: # 0 是 blank result.append(idx2char[p]) prev = p return ''.join(result)贪心解码的逻辑是:遍历每个时间步,如果当前字符和上一个不同且不是 blank,就输出。idx2char是类别索引到汉字的映射字典。这个函数适合快速验证模型是否学到了东西,但正式评估时要用 Beam Search,否则字错误率会偏高 5% 到 10%。
3. 从零搭一个能跑的中文语音识别系统:数据、训练、推理
3.1 数据准备:THCHS-30 和 AISHELL-1 的目录结构怎么统一
中文开源语音数据集常用的有 THCHS-30(约 30 小时)和 AISHELL-1(约 178 小时)。THCHS-30 适合快速验证,AISHELL-1 适合训练出可用模型。两个数据集的目录结构不同,需要统一成「音频路径 + 对应文本」的清单文件。
常见做法是生成一个manifest.jsonl,每行一个 JSON,包含wav_path、text、duration三个字段。文本要做清洗:去掉标点、数字转汉字、英文转小写或保留。中文里数字「123」要转成「一百二十三」,这个可以用cn2an库处理。
import json import os from cn2an import an2cn def build_manifest(data_dir, output_path): entries = [] for root, _, files in os.walk(data_dir): for f in files: if not f.endswith('.wav'): continue wav_path = os.path.join(root, f) txt_path = wav_path.replace('.wav', '.txt') if not os.path.exists(txt_path): continue with open(txt_path, 'r', encoding='utf-8') as fp: text = fp.read().strip() # 数字转中文,去掉标点 text = an2cn(text) text = ''.join(ch for ch in text if '\u4e00' <= ch <= '\u9fff') if len(text) == 0: continue entries.append({'wav_path': wav_path, 'text': text}) with open(output_path, 'w', encoding='utf-8') as fp: for e in entries: fp.write(json.dumps(e, ensure_ascii=False) + '\n') print(f'共生成 {len(entries)} 条样本')an2cn把阿拉伯数字转成中文读法,'\u4e00' <= ch <= '\u9fff'过滤掉非汉字字符。注意有些数据集文本里带拼音或特殊标记,要按实际情况调整过滤规则。生成的manifest.jsonl后续用 PyTorch 的Dataset类读取,做特征提取和 padding。
3.2 训练循环:CTC Loss 的三个关键参数怎么设
训练中文语音识别模型,核心是 CTC Loss 的三个参数:blank索引、输入长度、标签长度。blank一般设为 0,字符类别从 1 开始。输入长度是特征时间帧数,标签长度是文本字符数。CTC 要求输入长度大于等于标签长度,否则会报错。
import torch from torch.utils.data import DataLoader def train_one_epoch(model, dataloader, optimizer, ctc_loss, device): model.train() total_loss = 0 for batch in dataloader: feats, feat_lens, labels, label_lens = batch feats = feats.to(device) # (B, 1, n_mels, T) labels = labels.to(device) logits = model(feats) # (B, T', n_class) log_probs = torch.log_softmax(logits, dim=-1) # CTC 要求 (T, B, n_class) log_probs = log_probs.permute(1, 0, 2) loss = ctc_loss(log_probs, labels, feat_lens, label_lens) optimizer.zero_grad() loss.backward() # 梯度裁剪,防止 LSTM 梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=5.0) optimizer.step() total_loss += loss.item() return total_loss / len(dataloader)feat_lens是每个样本的实际时间帧数,因为 batch 里做了 padding,要告诉 CTC 哪些是有效帧。label_lens是每个文本的实际字符数。clip_grad_norm_的max_norm=5.0是经验值,LSTM 训练时梯度容易爆炸,不加裁剪 loss 会变成 NaN。学习率一般用 1e-3 配 Adam,训练 50 到 100 个 epoch,观察验证集 loss 是否还在下降。
3.3 推理部署:把模型封装成一个接收 wav 返回文本的函数
训练完之后,推理流程是:读 wav → 预处理 → 提取 Mel → 模型前向 → CTC 解码 → 返回文本。封装成一个函数,方便演示和集成。
def recognize(wav_path, model, idx2char, device='cpu'): model.eval() waveform = preprocess_audio(wav_path) mel = extract_mel(waveform) # (n_mels, T) feat = torch.FloatTensor(mel).unsqueeze(0).unsqueeze(0) # (1, 1, n_mels, T) feat = feat.to(device) with torch.no_grad(): logits = model(feat) # (1, T', n_class) logits = logits.squeeze(0) # (T', n_class) text = ctc_greedy_decode(logits, idx2char) return textunsqueeze(0)两次是为了加上 batch 维度和通道维度,模型输入要求是四维。torch.no_grad()关闭梯度计算,减少显存占用。如果要做 Beam Search,把ctc_greedy_decode换成对应的 beam search 函数即可。这个函数可以直接被 Flask 或 FastAPI 包装成 HTTP 接口,供前端调用。
4. 中文语音识别的五个血泪坑:从环境到解码
4.1 坑一:librosa 和 torchaudio 版本冲突导致音频加载失败
现象:import librosa正常,但调用librosa.load时报soundfile.LibsndfileError或返回全零数组。原因:librosa 底层依赖 soundfile,soundfile 又依赖系统级的 libsndfile 库,不同版本的 torchaudio 可能自带不同版本的 soundfile,导致冲突。解决:统一用 conda 安装librosa和soundfile,或者直接用torchaudio.load替代librosa.load,避免混用。
4.2 坑二:CTC Loss 报「input length is too short」
现象:训练时抛出RuntimeError: input length is too short。原因:特征时间帧数小于标签字符数。中文一句话可能有 20 个字,但音频只有 1 秒,Mel 帧数只有 100 帧,经过卷积池化后时间维度可能压缩到 25 帧,小于 20 就报错。解决:检查卷积层的池化策略,时间维度不要池化太狠;或者在数据预处理时过滤掉时长过短的样本,保证feat_len >= label_len。
4.3 坑三:模型输出全是 blank,解码结果为空
现象:训练 loss 下降到某个值后不再降,推理时解码结果全是空字符串。原因:CTC 的 blank 类别占比过高,模型学会了「全部预测 blank」这个偷懒策略。解决:检查标签里是否有大量空白或异常字符;在 loss 里给 blank 类别加权重惩罚;或者用torch.nn.CTCLoss的zero_infinity=True参数,避免无穷大 loss 导致梯度异常。
4.4 坑四:中文标点和数字没清洗,模型学到无关符号
现象:识别结果里出现「,」「。」「123」等非汉字字符。原因:训练文本没有做清洗,模型把标点和数字也当成类别学习。解决:在构建 manifest 时过滤掉所有非汉字字符,数字用cn2an转成中文读法。如果确实需要标点,可以单独加一个标点恢复模块,不要混在声学模型里。
4.5 坑五:推理时忘记加 batch 维度,模型报维度错误
现象:RuntimeError: Expected 4D input (got 3D input)。原因:模型定义时卷积层要求输入是(B, C, F, T),推理时只传了(C, F, T)。解决:用unsqueeze(0)加 batch 维度,或者用torch.no_grad()配合model.eval()确保 BatchNorm 和 Dropout 行为正确。这个坑新手最容易踩,因为训练时 DataLoader 自动加了 batch 维度,推理时手动传数据就容易忘。
5. 验证与调优:用字错误率和注意力可视化判断模型好坏
模型训练完之后,不能只看 loss,要看字错误率(CER)。CER 的计算方式是:编辑距离(插入、删除、替换)除以参考文本的总字数。中文语音识别里,CER 低于 10% 算可用,低于 5% 算不错。计算 CER 可以用jiwer库,或者自己写编辑距离。
def compute_cer(refs, hyps): # refs 和 hyps 是等长的字符串列表 total_dist = 0 total_len = 0 for r, h in zip(refs, hyps): total_dist += edit_distance(r, h) total_len += len(r) return total_dist / total_len if total_len > 0 else 0 def edit_distance(s1, s2): m, n = len(s1), len(s2) dp = [[0] * (n + 1) for _ in range(m + 1)] for i in range(m + 1): dp[i][0] = i for j in range(n + 1): dp[0][j] = j for i in range(1, m + 1): for j in range(1, n + 1): if s1[i-1] == s2[j-1]: dp[i][j] = dp[i-1][j-1] else: dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1 return dp[m][n]edit_distance是标准的动态规划实现,时间复杂度 O(m*n),对于短文本足够快。如果测试集很大,可以用jiwer的 C 实现加速。计算 CER 时要注意,参考文本和识别结果都要去掉空格和标点,只保留汉字。
除了 CER,还可以用注意力可视化来检查模型是否对齐正确。如果是带注意力解码的模型,可以把注意力权重矩阵画出来,看每个输出字符对应到哪些输入帧。如果注意力对角线清晰,说明对齐良好;如果注意力分散或错位,说明模型还没学好,需要更多数据或更长训练。
调优的方向有几个:加 SpecAugment 做数据增强,对 Mel 频谱做时间扭曲和频率遮蔽;用学习率预热和余弦退火,避免初期震荡;加 Dropout 和权重衰减,防止过拟合。我一般会先跑一个 baseline,记录 CER,然后每次只改一个变量,观察 CER 变化。如果 CER 卡在某个值不动,优先检查数据质量,而不是盲目加模型复杂度。
做这个方向最深的体会是:数据清洗和对齐比模型结构重要得多。我见过太多人花一周调网络结构,CER 只降了 1%,但花半天清洗文本里的异常字符,CER 直接降了 5%。希望帮到你。
本文还有配套的精品资源,点击获取