资讯动态

深度特征映射语音增强Python源码全解析:从STFT到PESQ评估

发布时间:2026/10/3 3:07:57 来源:尧图企业网站定制
简介一套基于深度特征映射的语音增强实现方案随附完整Python源码、文档说明与专用数据集面向计算机相关专业学生和语音技术入门者。该方案针对从含噪语音中提取清晰信号的核心问题覆盖数据预处理、模型训练、效果评估等环节既适用于课程设计、毕业设计、实训实验也可为公司项目二次开发提供基础。资源包共144个文件约57.81MB以py脚本、txt配置与说明、wav音频样本为主并包含sh执行脚本、pyc预编译模块以及PESQ语音质量评估工具目录结构清晰便于按流程调用与改造。目前已有53人学习浏览代码完整且经过测试遇到问题可私信交流并获远程指导。通过这套资料可系统理解深度特征映射在语音增强中的落地方法也可快速开始相关研究或工程实践。1. 深度特征映射语音增强这份 python 源码能解决什么问题语音增强这几年被深度学习彻底带火而深度特征映射是里面门槛最低、最不容易翻车的一条路线不碰生成模型直接把带噪语音的幅度谱喂进网络让它学干净语音的幅度谱训练完拿相位一拼就能出增强音频。你手里这份基于深度特征映射的语音增强 python 源码包正是按这条路线组织的数据、预处理、训练、PESQ 评估配成完整一套代码成功跑通的验证已经做过直接拿来当毕业设计、课程设计或者实训项目都够用。如果你正愁毕设没方向或者公司项目里要快速搭一个语音预处理前端这套包能帮你省掉从零写代码、凑数据集的时间。真正会卡住你的不在模型而在数据怎么切、特征怎么对齐、PESQ 为什么老报错。下文我会按流水线顺序把每个脚本的角色、关键参数和踩过的坑逐个拆开讲。2. 先看清脚本与数据整个流水线是怎么串起来的2.1 文件角色对照表我拿到一个压缩包的习惯是先翻文件列表而不是直接解压跑 train.sh。语音增强工程最怕的就是脚本名字看着像那么回事跑起来才发现缺依赖、缺路径。把这份资源里的文件名过一遍能看出它大概分成了数据预处理、训练、评估三块。文件/脚本职责我判断的依据avr_pesq、ca_pesq.sh、pesqPESQ 客观音质评估工具及封装pesq 是语音增强领域最常用的评估指标avr 是 average 的缩写ca 应该是 calculate 的缩写pre_process_data.py训练集预处理把带噪和干净 wav 转成特征对pre_process 加 data职责最重的一个脚本pre_process_test.py测试集预处理生成推理输入和上一个配套参数结构基本一致evaluate.py加载训练好的模型做增强并计算评估指标evaluate 在语音工程里几乎就是这个职责config.py全局参数配置采样率、帧长、路径、训练超参数都集中在这inputs.scp、inputs_nmae输入音频的文件索引scp 是 Kaldi 风格的文件列表格式一行一个文件tr.list、cv.list、test.list训练、验证、测试集划分深度学习里标准的数据划分命名cut_cln_wav.m把长录音切成长度一致的干净语音片段从命名看是 cut clean wavMATLAB 脚本train.sh、ex_trac.sh训练入口和特征提取入口shell 包装脚本把多个 python 模块串成流水线从这张表能看出一个关键设计数据划分被单独抽成了 list 文件特征提取单独拆了脚本说明这个项目是要反复换数据、反复训练的不是一次性 demo。你真正需要动刀的地方只有 config.py 和那三个 list其他脚本基本是围绕它们转的配套。2.2 tr.list、cv.list、test.list 如何决定训练效果这三个文件决定了模型见过什么、验证用什么、考试用什么。常见的格式有两种一种是每行一个 wav 路径另一种是每行一对路径左边干净语音、右边带噪语音。语音增强必须用第二种因为训练需要“噪声版本对应干净版本”的监督标签。文件里大概率长这样/path/to/cln/001.wav /path/to/far_dt/001_dt.wav注意路径要一一对应文件名 base 保持相同只是目录和尾缀不同。far_dt 从命名看像是远场或远端采集的带噪数据具体以包内数据目录为准。这一段还藏着一个很多人忽略的细节cv.list 和 tr.list 的数据不能有交集。如果切音频时把同一段录音的前半截分给训练、后半截分给验证模型验证分数会虚高因为“记忆”过同一段话的声学特征。cut_cln_wav.m 的作用就是把长录音切成短段切完要检查输出目录里音频数量再决定怎么分比例。一般训练、验证、测试按 8:1:1 或者 7:2:1 分这份包里的三个 list 对应的比例可以在文档说明里确认。3. 预处理阶段从波形到带上下文的幅度谱特征3.1 波形读取与 STFT帧长、窗函数选什么预处理是整个工程里最容易出玄学问题的地方。很多人模型写得很标准但训练数据生成得粗糙loss 就是降不下去。语音增强里最主流的做法是把 wav 切成 20ms 左右的帧做短时傅里叶变换拿到幅度谱和相位谱。相位在初版系统里通常直接沿用带噪语音的相位因为人耳对相位误差相对不敏感这是经典 DNN 语音增强的标准偷懒方案。import numpy as np import soundfile as sf def read_wav(path, target_sr16000): # 读取音频并统一到单声道、16k、float32 data, fs sf.read(path, dtypefloat32) if data.ndim 1: data data.mean(axis1) if fs ! target_sr: n int(len(data) * target_sr / fs) x_old np.linspace(0, 1, len(data)) x_new np.linspace(0, 1, n) data np.interp(x_new, x_old, data) return data def stft_mag_phase(wav, n_fft512, win_len320, hop_len160): # 分帧、加窗、FFT返回幅度谱和相位谱 frames [] for start in range(0, len(wav) - win_len 1, hop_len): frame wav[start:start win_len] * np.hanning(win_len) frames.append(frame) if not frames: raise ValueError(wav 长度不足一帧检查音频是否过短) spec np.fft.rfft(np.asarray(frames), nn_fft, axis1) mag np.abs(spec) phase np.angle(spec) return mag, phase这段代码里有三个参数决定了后面所有特征维度win_len 取 320 是因为 16k 采样率下 320 点正好是 20ms这是语音短时平稳假设的常用折中hop_len 取 160 是 10ms 帧移保证相邻帧有 50% 重叠istft 时能自然过渡n_fft 取 512 是补零后的 FFT 长度比 win_len 大可以提升频域分辨率得到的单侧频谱是 257 维。为什么用 np.hanning 而不是矩形窗因为矩形窗的旁瓣泄漏大频谱上会出现明显的“栅栏效应”训练出来的映射会把窗函数的伪影也当成语音特征学进去。hann 窗旁瓣衰减好是语音特征提取的默认选择。如果后续要自己写 istft必须记下窗类型和 hop_len窗不一致是增强音频出现爆破声的头号原因这个坑在第 5 章会展开讲。3.2 拼接上下文与构造标签归一化藏在哪单帧 257 维幅度谱作为输入模型容易把噪声的瞬时尖峰误判为语音。常见做法是把当前帧和前后各几帧拼接起来给网络更多时间上下文。前后各 3 帧是最常见的配置输入维度变成 7 乘 257 等于 1799。def build_context(mag, ctx3): # 把当前帧与前后 ctx 帧拼接成单帧输入 pad np.pad(mag, ((ctx, ctx), (0, 0)), modereflect) feats [] for i in range(ctx, len(pad) - ctx): feats.append(pad[i - ctx:i ctx 1].reshape(-1)) return np.asarray(feats, dtypenp.float32)reflect 填充是给音频开头和结尾补帧用的镜像模式比补零更自然避免开头几帧因为突然出现全零而产生边界伪影。特征处理好之后标签不是直接用干净幅度谱而是取 log。直接回归线性幅度谱大能量帧会把 loss 主导小能量帧的细节学不出来log 操作压缩了动态范围MSE 才能在清音、弱音上均匀发力。归一化参数必须在全部训练数据上统计得到一次训练和测试用同一份。global_max 0.0 for name in train_list: # 假设预处理输出的 npy 以音频名命名 feat np.load(os.path.join(feat_dir, name .npy)) global_max max(global_max, np.abs(feat).max()) # 保存 global_max测试阶段加载同一份 model_input noisy_feat / global_max model_label np.log(np.maximum(clean_feat / global_max, 1e-6))注意 np.maximum 里的小常数 1e-6这是防止干净语音里某些频点能量为 0取 log 后变成负无穷。损失函数在 log 谱上算 MSE对应的是相对误差而不是绝对误差这样模型会更关注语音的结构而不是音量大小。4. 训练与推理config 参数、train.sh 与网络结构4.1 config.py 里值得改的参数config.py 是这份包的心脏。大部分脚本第一行基本就是加载它。不用全部看懂但下面这几个参数是必调的它们直接决定显存占用和训练效果。参数名作用我常用的值sample_rate音频采样率16000n_fftFFT 点数512win_len帧长320hop_len帧移160feature_dim输入特征维度17997 帧乘 257batch_size每批样本数32 或 64看显存learning_rate初始学习率0.0003max_epoch最大训练轮数60 左右model_path模型保存目录建议绝对路径list 文件路径tr/cv/test 三个列表位置必须和实际解压路径一致这里最容易被忽略的是 feature_dim。很多人改小了上下文帧数却忘记同步改网络输入层的维度一跑就报维度不匹配。我的习惯是先改预处理脚本里的 ctx重新生成特征看一眼输出 npy 的形状再回头填 config。4.2 train.sh 到底做了什么train.sh 是一个 shell 包装脚本作用是帮你按顺序调起各个 python 模块。不同版本的包内部调用关系可能不一样但大致的执行流是先切数据、再预处理、再训练。以常见流程为参考执行顺序如下。#!/bin/bash set -e source activate py38 # 激活 python 环境具体环境名按你的实际来 # 1. 用 MATLAB 切好干净音频后检查三个 list 是否齐全 # 2. 预处理器生成训练特征和标签 python pre_process_data.py --config config.py # 3. 特征提取推理用特征 bash ex_trac.sh # 4. 训练主程序如果包里没有独立 train.py按 4.3 的模板新建 python train.py --config config.pyset -e 的作用是只要中间任何一步报错脚本立刻退出避免带病跑完最后给你一堆坏模型。我还是建议不要直接 bash train.sh 一把梭而是先跑 preprocessing 单独验证特征能正常产出再进训练。日志里如果出现 FileNotFoundError八成是 list 里的路径和实际解压路径不一致回到 2.2 节检查。4.3 特征映射网络结构、损失与 checkpoint深度特征映射的核心假设是带噪幅度谱和干净幅度谱之间存在一个稳定的函数映射神经网络就是去拟合这个函数。结构不需要花哨经典配置是两层到三层全连接中间夹 ReLU输出层不用激活函数因为要回归的是连续幅度值。PyTorch 的写法大致如下如果包里是 TensorFlow 版本逻辑完全一样只是 API 不同。import torch.nn as nn class FeatureMappingNet(nn.Module): def __init__(self, in_dim1799, out_dim257): super().__init__() self.fc nn.Sequential( nn.Linear(in_dim, 1024), nn.ReLU(), nn.Linear(1024, 512), nn.ReLU(), nn.Linear(512, out_dim) ) def forward(self, x): return self.fc(x)输入 1799 维、输出 257 维中间 1024 和 512 是经验值。第一层要够宽因为输入是 7 帧拼接需要先把上下文信息融合最后一层必须严格等于 257对应单帧幅度谱的频点数。训练时用 MSE loss 和 Adam 优化器学习率 3e-4 起步。训练循环要打印每个 epoch 的验证损失当验证损失连续 5 个 epoch 不再下降时就保存一份 checkpoint这叫 early stopping防止后期过拟合到训练集的噪声模式。for epoch in range(max_epoch): model.train() for x, y in train_loader: pred model(x) loss mse_loss(pred, y) optimizer.zero_grad() loss.backward() optimizer.step() if (epoch 1) % 5 0: ckpt os.path.join(model_path, fepoch_{epoch1}.pt) torch.save(model.state_dict(), ckpt) print(fepoch {epoch1} done, model saved to {ckpt})推理阶段带噪语音经过相同的 STFT拼接上下文除以训练时的 global_max过网络得到增强幅度谱然后乘回 global_max与相位谱合成复数谱最后用 istft 还原波形。这里最容易出的问题是测试时忘记除以 global_max或者用了自己重新统计的 global_max会导致增强输出音量漂移。正确做法是把 global_max 像模型参数一样保存、加载不要重算。5. 避坑指南五条实测踩坑记录5.1 路径与数据格式的硬坑第一坑PESQ 跑出来的分数是 -0.5或者干脆报错。这是我见过最多的问题。现象是 evaluate.py 或者 ca_pesq.sh 能把 wav 读进去但算出来的分数要么是负的、要么是异常小。原因基本逃不出两个采样率不是 16k 或 8k位深不是 16bit PCM。PESQ 对格式极其严格44.1k 采样率、24bit 位深都会直接翻车。解决方法是预处理阶段强制统一重采样到 16k、单声道、float32写 PESQ 评估文件之前再转成 16bit PCM一劳永逸。第二坑train.sh 提示找不到 inputs.scp 或者 list 文件。原因是脚本里写了相对路径而你从别的目录执行了 train.sh工作目录不对相对路径全部失效。解决方法是先进到解压目录再执行更稳的做法是把 config.py 里的路径全部改成绝对路径。我自己的习惯是写路径时用 os.path.join(os.path.dirname(file), ...) 拼绝对值这样不管从哪里起脚本都不会翻车。5.2 训练与评估阶段的软坑第三坑loss 初始值很高训练了十个 epoch 还在原地震荡。我有一阵子被这个折腾到怀疑模型写错了。后来定位到原因幅度谱没有归一化。不同句子的音量差异很大有的句子幅度谱最大值上千有的只有几十log 之后依然动态范围太大MSE 在这种尺度下很难稳定下降。解决方法是全局 max 归一化再把学习率压到 3e-4。注意这个坑的另一个隐蔽变体只在训练集上统计 global_max在测试集上重算了一遍结果增强音量忽大忽小。第四坑增强后的音频有周期性爆破声频谱图上一道道竖线。原因是 istft 时窗口不匹配或者 overlap-add 没有正确归一化。训练时用 hann 窗推理时如果自己手写了 istft 但没用 OLA 归一化拼接处就会产生不连续的跳变。解决方法是用 hann 窗帧移为窗长一半时OLA 的增益恒为常数istft 时要除以窗平方和的均值。如果你用的是 librosa.istft只要 hop_length、win_length、window 三个参数和 stft 时完全一致就能自动处理好。第五坑cv.list 是空的或者指向的文件根本不存在evaluate.py 跑到一半崩掉。原因是 cut_cln_wav.m 切完的音频没有同步生成验证集列表或者 MATLAB 输出目录和 list 里手写的路径对不上。解决方法是不要手写 list用脚本扫描 cln 目录下实际存在的文件名再按比例生成 tr.list、cv.list、test.list。我习惯在这个脚本里加一个校验逻辑统计三个 list 的文件数并打印出来一眼就能看出划分是否合理。6. 效果验证与进阶PESQ 之外再补两个技巧6.1 用 PESQ 和语谱图双重验证训练结束先别急着看指标先做一次目检。用 ex_trac.sh 对测试集跑增强再调用 PESQ 工具批量打分。包里的 ca_pesq.sh 封装的就是下面这个循环其中 ref 是干净语音enh 是增强后的语音。while read ref other; do enh${ref%.wav}_enh.wav python -m pesq --fs 16000 $ref $enh done test.listPESQ 分数范围在 -0.5 到 4.5 之间一般从 1.5 提升到 2.5 已经算显著改善。但 PESQ 高分不等于听感好它反映的是客观失真。我每次跑完评估都会把测试集里最嘈杂的几段挑出来画出增强前后的语谱图对比再下结论。import matplotlib.pyplot as plt import librosa.display fig, (ax1, ax2) plt.subplots(1, 2, figsize(12, 4)) librosa.display.specshow(librosa.power_to_db(noisy_spec), sr16000, hop_length160, x_axistime, y_axishz, axax1) ax1.set_title(noisy) librosa.display.specshow(librosa.power_to_db(enh_spec), sr16000, hop_length160, x_axistime, y_axishz, axax2) ax2.set_title(enhanced) plt.show()语谱图里语音的谐波结构应该是横条纹状的噪声则表现为均匀分布的颗粒感。如果增强后横条纹连贯、颗粒感明显减少说明训练是有效的如果出现整段能量被压得很低那就是过抑制了。6.2 自备音频测试时沿用训练归一化参数第二个技巧是当你拿自己的录音测试时一定要沿用训练阶段保存的 global_max不要重新统计。你的录音如果比训练集音量小重算的 global_max 会偏小相当于把输入特征整体放大模型输出跟着失真。如果新录音整体偏轻先对波形做一次 RMS 归一化到固定音量再走增强链路。从那以后我每次换测试音频都强制走一遍这套流程先目检语谱图再算 PESQ最后用耳朵听一遍噪声残留量。指标再好看也得先过这三个检查才敢说真的有效。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑