简介:这套资源是基于PyTorch框架实现的中文语音识别系统,包含完整源码、预训练模型与配套使用说明,面向毕业设计、课程设计及深度学习语音识别入门人群。系统支持对wav格式音频进行识别并自动播放识别文字,预置了多个测试音频,可直接运行验证效果。资源包共85个文件,约93.44MB,核心内容包括7个Python源码文件(模型、注意力机制、位置编码、特征提取等模块)、预训练模型文件,以及中文语音数据集样本文本与音频;同时附带录音库安装包、格式转换说明、环境依赖列表等辅助文件,便于快速搭建运行环境。包内以音频、文本、脚本、配置等类型为主,目录结构清晰。目前已有615人学习使用。借助这套资源可完整体验中文语音识别的数据准备、模型推理与结果输出流程,还能基于源码开展二次开发,适合作为算法研究或项目演示的基础。
1. 基于深度学习的中文语音识别毕设项目包:能跑和识别是两个门槛
这类 zip 解压之后通常有两种走向:改两条路径就听到测试 wav 被转成汉字,或者环境依赖连环报错,卡在装 torchaudio 之前连模型文件都没见到。我理解为什么很多人把它当成开箱即用的软件,但中文语音识别系统本质是一个最小可复现的教学工程,你得先问对问题:用 Fbank 还是 MFCC,声学模型是 CTC 还是 Attention,解码时 beam 开多大,语言模型权重给多少。中文跟英文最大的差别在同音字、多音字和没有空格的分词边界,所以不是把音频丢进深度学习模型就能出文本,后面还有解码、纠错和标点处理。这篇文章写给要做毕设、要跑通源码、要改参数做演示的人,路径从环境到解码逐层拆开。
2. 端到端框架与 Fbank 特征:中文语音识别模型的选型和特征流程
2.1 DNN-HMM 已是过去式:CTC / Attention 为什么是这类包的主流
在中文语音识别这个题目下,项目包选哪条技术路线,基本决定了你后面要折腾多少东西。早期的 Kaldi 方案是 GMM-HMM 对齐,再用 DNN 做音素状态的后验概率,需要音素集、词典、决策树状态绑定、强制对齐这一整条链路,每一样都在消耗毕设时间。而现在把建模对象换成汉字之后,整个声学模型可以只用音频文件和汉字序列两个目录就端到端训练起来,开源实现多,排错也容易,所以近几年 Python 源码包里出现的基本都是 PyTorch 写出来的端到端模型。
| 声学模型方案 | 训练链路复杂度 | 直接对汉字建模 | 典型实现 |
|---|---|---|---|
| DNN-HMM(Kaldi 式) | 高,需要音素状态绑定和对齐 | 否,需音素词典 | Kaldi thchs30 脚本 |
| CTC 字模型 | 低,帧级自动对齐 | 是 | PyTorch + warp-ctc |
| Attention Encoder-Decoder | 中,自回归解码慢 | 是 | Transformer、Conformer |
| CTC + Attention 联合 | 中,两个损失同时训练 | 是 | 开源中文语音项目最常见 |
CTC 最大的好处是训练时不需要音素级的标签对齐,它自己学会在 blank 符号和重复字之间做 collapse,这让源码包的预处理简单很多。Attention 单独用来生成汉字时,长句会出现注意力漂移,很多包的做法是把 CTC 作为辅助损失加上,解码时再选择 CTC 贪心输出或注意力自回归输出。这个框架你不用改,但要能看懂 loss 里的ctc_loss + alpha * attention_loss是在干什么,答辩时被问到是大概率事件。
2.2 词表设计:中文语音识别为什么以“字”为基本单位
英文语音识别的词表是 subword,中文项目包常见做法是用汉字表建模,而不是词。原因很直接:中文常用汉字 3000 到 6000 个,词表却需要几十万到上百万,普通数据量下 OOV 问题比英文严重得多。字级建模把词典固定住,多音字问题则抛给语言模型和后处理去解决。常见的开源数据集是 THCHS-30 和 AISHELL-1,前者约 30 小时适合跑通流程,后者约 178 小时适合认真训练一版,毕设项目包里多数用的是前者。
python -c "open('dict.txt','w',encoding='utf-8').write('\n'.join(sorted(set(open('train.txt',encoding='utf-8').read()))))"这条命令把训练文本里的所有字符取出去重后写入 dict.txt,生成的词表还需要手动补充<blank>、<sos>、<eos>、<pad>、<unk>这几个特殊 token。注意要把空格、换行和英文标点过滤掉,否则模型会浪费概率去建模无意义的符号。特殊 token 在解码时不会输出,所以使用说明里一般会强调“词表和 checkpoint 配套”,换词表基本等于换模型。
2.3 Fbank 特征:80 维 Mel 滤波器的参数和采样率选择
特征部分在源码包里一般是一个compute_fbank脚本,它负责把所有 wav 批量转成 numpy 数组或 .ark 文件。为什么选 Fbank 而不是 MFCC,很多教科书解释是 MFCC 的 DCT 压缩丢掉了相邻滤波器组之间的相关性,而深度学习可以自己从全量能量中学特征,保留完整信息往往更有利。参数上,中文语音识别的开源实现基本是一套统一配置:16k 采样率、25ms 窗长、10ms 帧移、80 维 Fbank,再加 mean-variance 归一化。
import torch import torchaudio def extract_fbank(wav_path, sample_rate=16000, num_mel_bins=80): waveform, sr = torchaudio.load(wav_path) if sr != sample_rate: waveform = torchaudio.functional.resample(waveform, sr, sample_rate) fbank = torchaudio.compliance.kaldi.fbank( waveform, num_mel_bins=num_mel_bins, frame_length=25.0, frame_shift=10.0, dither=0.0, energy_floor=0.0, ) return fbankdither=0.0是关闭抖动,因为后面通常还要做速度扰动和 SpecAugment 数据增强,前置加噪没有意义;energy_floor=0.0避免过低能量被强制拉高。torchaudio.load读进来如果是双声道,经验做法是先转单声道,很多源码包没有做这一步,导致训练输入尺寸对不上时的报错信息非常隐蔽。批量处理前,我会先用 ffmpeg 统一转成 16k、16bit、单声道 wav,再进特征提取,这个操作能省掉至少一小时查 bug 的时间。归一化时注意是按全局均值和方差做,而不是按每句话做,后者会把不同音量的差异抹掉。
3. Python 环境、源码结构与推理最小验证:把“包运行”落实
3.1 PyTorch 版本组合与 conda 环境:先跑 CPU 再上 GPU
拿到任何标注“包运行”的 Python 项目,别急着pip install -r requirements.txt。先创建一个独立 conda 环境,避免把你日常环境炸了。Python 版本有个经验判断:代码里还有distutils、直接import librosa的老接口,就选 3.8 或 3.9;源码出现torch.compile、dataclass、zipfile.Path,选 3.10/3.11 更省事。中文语音识别项目包的代码风格通常滞后于框架版本,所以我一般默认 Python 3.9。
conda create -n asr python=3.9 -y conda activate asr conda install pytorch torchaudio cpuonly -c pytorch -y pip install librosa editdistance tqdm tensorboard webrtcvad先用 CPU 版把模型加载和推理跑通,再装 CUDA 版本。CUDA 版本的问题集中在 torch 与显卡驱动的匹配上,conda install cudatoolkit会自带 toolkit,不需要单独装系统驱动。torchaudio的 CPU 版和 GPU 版要一起统一,否则会出现libtorch_cuda.so缺失这种进程直接崩溃的问题。
| Python 版本 | 兼容性 | 常见报错 |
|---|---|---|
| 3.8~3.9 | 兼容大部分开源语音项目 | 无明显坑 |
| 3.10 | 多数可运行 | 老代码偶发 warning |
| 3.11 | 需要检查依赖版本 | torchaudio旧版装不上 |
| 3.12 | 不推荐 | distutils缺失、编译型依赖安装失败 |
3.2 目录结构与三跳运行:先看懂源码包怎么组织
好的源码包一般都遵循 data / models / tools / exp 的分层。不要直接点运行脚本,先打开目录树,把数据和 checkpoint 的路径全部弄清楚:
. ├── data/ │ ├── thchs30/ # 源音频和标注 │ ├── list/ # train/dev/test 文件列表 │ └── dict.txt # 字表 ├── models/ │ ├── encoder.py │ ├── decoder.py │ └── asr_model.py ├── tools/ │ ├── prepare_data.py │ └── compute_fbank.py ├── exp/ │ └── checkpoints/ # 训练产物 └── infer.py在这类包里跑最小流程有三个命令,顺序固定。prepare 负责生成数据列表,遍历原始文件夹,输出 train.list 和 dev.list,并把 wav 路径与标注文本对齐;compute_fbank 把每个 wav 转成特征文件,这一步通常最慢,看到进度条卡住先检查是否有样本损坏;infer 加载已有 checkpoint 做单条推理。
python tools/prepare_data.py --data_root data/thchs30 --output_dir data/list python tools/compute_fbank.py --config configs/train.json python infer.py --checkpoint exp/checkpoints/model.pt --wav data/test.wavprepare 这步报错最多的原因是 THCHS-30 原始目录下放了 .wav 和 .trn 两个文件,如果数据集只有 .wav,列表生成后标注会变成空行。compute_fbank 里常见参数是--num_workers,在 Windows 上必须设置为 0,否则多进程在数据处理阶段会卡死,这个坑在 PyTorch 语音项目里非常经典。infer 的输出如果全是空白,先看 checkpoint 路径对不对,再看词表和模型头文件是否匹配。
3.3 环境自检:五分钟定位是缺包还是路径问题
python -c "import torch, torchaudio; print(torch.__version__, torchaudio.__version__)" python -c "import sys; sys.path.insert(0, '.'); from models.asr_model import ASRModel; print('ok')"第一行确认 torch 与 torchaudio 的版本配对,第二行确认模型代码本身能导入。能走到这两步,报错就不可能再出现在框架层面,接下来按 traceback 查路径和文件编码。Windows 上还要注意中文路径导致的 UnicodeDecodeError,运行前把项目放在纯英文路径下是最省事的做法。
4. 模型加载、Beam Search 与中文热词纠偏:解码侧的关键参数
4.1 加载本地模型 checkpoint:cpu 还是 gpu,先解决键名不匹配
加载本地模型值得单独讲,因为按经验八成人卡在这一步。下载好的模型文件一般以 .pt 或 .pth 结尾,里面不一定只放 state_dict,也可能是打包了model_state_dict、optimizer 和 epoch 的字典。不要直接model = torch.load(path),除非你有把握它是完整序列化的 Module。稳妥做法是手动取出状态字典再加载:
import torch from models.asr_model import ASRModel checkpoint = torch.load("exp/checkpoints/model.pt", map_location=torch.device("cpu")) state = checkpoint.get("model_state_dict", checkpoint) model = ASRModel(vocab_size=6000, encoder_dim=256) model.load_state_dict(state) model.eval()map_location决定加载发生在哪个设备。CPU 机器上忘写这个参数,一个几百 MB 的 checkpoint 可能触发 CUDA 初始化然后崩溃;GPU 机器上写map_location="cuda:0"则更省内存。如果报size mismatch,八成是vocab_size对不上,数一下 dict.txt 的行数改掉就行。如果报missing key(s),把model.state_dict().keys()和 checkpoint 的 keys 打出来对比,常见原因是保存时套了nn.DataParallel,需要去掉前缀module.再加载:
state = {k.replace('module.', ''): v for k, v in state.items()}4.2 贪心解码到 Beam Search:beam 开多大,lm_weight 给多少
模型输出的 log_probs 经过 softmax 后是一条帧级标记序列,要得到中文句子还需要合并连续相同 token、去掉 blank。经典 CTC 贪心解码实现很短:
def ctc_greedy_decode(log_probs, blank=0): tokens = log_probs.argmax(dim=1).tolist() result, prev = [], blank for idx in tokens: if idx != prev and idx != blank: result.append(idx) prev = idx return result这个逻辑体现了 CTC 最重要的 collapse 规则:相邻重复的标记只保留一个,blank 作为分隔符但不能出现在输出里。blank=0对应词表中的<blank>,不同项目可能放在索引 0 或最后,不对齐就表现为整段识别结果里频繁出现<blank>字样。
贪心只有一条路径,遇到同音字、多音字时后验概率接近,一次 argmax 就决定了胜负,所以生产中常用 Beam Search 保留候选序列。一个便于理解的最小实现如下,工程上可以换用 pyctcdecode 这类库:
import heapq def beam_search_decode(log_probs, beam_size=10, blank=0): seqs = [(0.0, [], blank)] for t in range(len(log_probs)): new_seqs = [] for score, seq, prev in seqs: for idx, p in enumerate(log_probs[t]): if idx == blank: new_seqs.append((score + p.item(), seq, idx)) elif idx == prev: new_seqs.append((score + p.item(), seq, idx)) else: new_seqs.append((score + p.item(), seq + [idx], idx)) seqs = heapq.nlargest(beam_size, new_seqs, key=lambda x: x[0]) return seqs[0][1]这里每一步只保留累计概率最高的 beam_size 条候选,idx == prev时不做 append,对应 CTC 的重复合并近似。完整实现还要考虑路径合并和语言模型分数,但参数直觉已经够用了。毕设演示一般把 beam_size 设在 10 到 30 之间,CPU 上开到 30 会明显变慢。
| 解码参数 | 推荐值 | 作用 |
|---|---|---|
beam_size | 10~30 | 保留候选路径数,越大越慢 |
lm_weight | 0.2~0.6 | 语言模型打分权重 |
length_penalty | 1.0~1.2 | 长句截断补偿 |
blank_threshold | 0.9 | blank 概率阈值,很少动 |
语言模型权重不是越大越好。有的项目包内置了 KenLM 加载的 n-gram 模型,lm_weight过高时声学概率被语言模型盖过,输出的是语言模型里最高频的句子而不是音频内容。调试时从 0.0 开始以小步长加,每次观察同一段音频的输出变化,比一次设 0.5 更有效。
4.3 热词纠偏:不重训模型,用后处理把姓名和品牌拉回来
中文识别即使在字级做得不错,姓名、品牌、专业术语也常常因为语料里出现少被识别成同音常见字。立竿见影的方法是热词纠偏,它不是重新训练,而是对解码结果做替换。如果模型输出字级,拼音差异是最直接的匹配键,先给热词建拼音表,再对候选文本转拼音,用编辑距离判断是否命中,最后替换。
def apply_hotwords(text, hotwords): for name in hotwords: for seg in hotwords[name]: if seg not in text: text = text.replace(seg, name) return text hotwords = { "商汤科技": ["商汤科技", "商汤科技公司"], "李现": ["李现"], } print(apply_hotwords("现在和李现一起参加会议", hotwords))这个例子说明两点:热词表不是简单一个词替换,要写出常见错句;替换顺序重要,先长词后短词,否则“商汤科技”会被“科技”拆掉。注意不要对全文做全局 replace,万一错句和正句同时存在,替换会把原本正确的部分改坏。落地时我会把“命中拼音但文本不同”和“文本本就相同”分开处理,前者用候选概率对比决定是否替换,后者不动。
这一节的本质是中文语音系统里最容易被忽视的字到句兜底逻辑。源码包里很可能没有这个模块,你补一份热词表,并在使用说明里写清楚替换阈值的作用,答辩时就是实实在在的改进点,比只贴一个识别率数字更有说服力。
5. 长音频切分兜底技巧:用 VAD 和重拼接保住中文语音识别演示效果
5.1 为什么长音频整句识别会越到后面越差
CTC 模型对长序列并不友好,logits 序列超过几百帧时,重复合并和 blank 的累积误差会让输出断句错乱;Attention 解码器也有位置漂移,说话人停顿稍长就可能在句尾重复或漏字。演示时不要直接跑整段三分钟音频,先做语音活动检测,把静音段切开再逐段识别。
5.2 基于 webrtcvad 的切分与重拼接
import webrtcvad def split_wav_vad(pcm, sample_rate=16000, aggressiveness=2, min_speech_ms=250, min_silence_ms=600): vad = webrtcvad.Vad(aggressiveness) frame_ms, frame_len = 20, int(sample_rate * 0.02) frames = [pcm[i:i+frame_len] for i in range(0, len(pcm), frame_len)] active = [vad.is_speech(f, sample_rate) for f in frames] segments, start = [], -1 silence_cnt = 0 for i, a in enumerate(active): if a: if start < 0: start = i silence_cnt = 0 elif start >= 0: silence_cnt += frame_ms if silence_cnt >= min_silence_ms: segments.append((start * frame_ms / 1000, i * frame_ms / 1000)) start, silence_cnt = -1, 0 return segmentsaggressiveness=2是稳定的档位,1 会放过更多静音导致切得太碎,3 在背景噪声下容易把语音尾部吃掉;min_silence_ms=600控制断句,低于这个值的停顿不会切开,防止一句话因为换气被斩成两半。webrtcvad 接收 16bit PCM 字节流,调用前要把 float 数组转成 int16 再 tobytes()。没有 webrtcvad 时用能量阈值也可以,但在空调声、翻书声存在的环境里,WebRTC 的误判明显更少。
切分后的识别结果不要直接拼成一个大字符串,按段记录时间和文本更合理:每段单独过模型,按时间顺序拼成带时间戳的字幕式文本。我这里说的拼接是句级拼接,不是字级直接拼,否则会在切割处出现吞字或重复。演示前我会拿一段三分钟录音,先跑整句,再跑 VAD 切分后的版本,把两个文本连同对齐时间戳打在同一张页面里,这个对比本身就是最有说服力的稳定性说明。
本文还有配套的精品资源,点击获取