ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

GMM-UBM声纹识别:从特征提取到打分判决的完整项目解析

GMM-UBM声纹识别:从特征提取到打分判决的完整项目解析 简介一套基于GMM-UBM模型的声纹识别Python实现源码及文档说明面向语音信号处理、机器学习相关课程的高分大作业场景。资源基于TIMIT数据集完整覆盖静音消除、FBANK/MFCC/PNCC三种特征提取、UBM训练、说话人模型训练与评分测试等关键环节适合需要快速复现声纹识别流程的学生和开发者参考。压缩包共10个文件包含9个Python脚本和1个Markdown文档脚本分别负责特征提取、模型训练、测试集与注册集生成、打分评估等功能文档说明则给出清晰的数据准备和运行步骤便于对照源码理解GMM-UBM的完整建模思路。已有224人学习/下载代码体积虽小但模块划分明确可帮助读者掌握声纹识别原理并作为课程设计或毕业设计的可扩展框架也方便针对不同特征或数据集进行二次调参与改进。1. GMM-UBM声纹识别这份Python课程项目把全链路跑通了声纹识别在课程大作业里最常见的翻车方式是直接搬一个深度学习分类网络训完拿测试集一跑准确率还行就交差。这份基于GMM-UBM的Python实现走的是另一条路它不训练神经网络而是用高斯混合模型对每个说话人的声学特征分布建模用通用背景模型UBM做全局兜底再通过最大后验概率自适应得到每个说话人自己的模型。项目把FBANK、MFCC、PNCC三种特征提取前端全部实现配套了静音消除、数据列表生成、UBM训练、说话人注册、打分判决的完整脚本和文档说明。适合正在做说话人识别课程设计、需要一条能逐步复现并到讲台答辩的完整路径的同学也适合想把传统声纹识别“特征→模型→打分”全链路看清楚、不想把模型当黑匣子的技术从业者。2. 项目结构与时序十个脚本谁先谁后捋一遍才敢动手这个项目最大的特点是不藏代码整套流水线被拆成十个左右的Python脚本每个脚本只做一件事顺着文档说明.md能一步步跑完。不像很多深度学习项目一个train.py跑到底中间发生了什么全靠猜。动手跑之前先把脚本之间的依赖关系捋清楚后面翻车概率能少一半。2.1 工程文件拆解与运行顺序先把zip里的脚本挨个看一遍文件清单如下脚本文件职责在流水线中的位置yu_pos.py静音消除可选数据准备最前段train_spk_model.py生成训练列表训练说话人模型列表阶段建模阶段train_test_scp.py生成或调整训练/测试列表辅助工具val_enrollment_scp.py生成注册列表与验证列表列表阶段feature_ex_Fbank.py提取FBANK特征特征阶段feature_extract_MFCC.py提取MFCC特征特征阶段feature_extract_pncc.py提取PNCC特征特征阶段train_UBM.py训练通用背景模型建模阶段eval_score.py打分与判决评估阶段按照项目文档给出的流程完整执行顺序是这样的。注意train_spk_model.py会出现两次第一次负责生成列表第二次负责训练说话人模型这是这类课程脚本常见的功能分支设计# 0. 可选对数据集做静音消除VAD去掉每段音频前后的静音 python yu_pos.py # 1. 第一次运行 train_spk_model.py生成训练UBM用的 ubm_wav.scp 和测试用的 test.scp python train_spk_model.py # 2. 生成注册列表 enrollment.scp 与验证列表 var.scp python val_enrollment_scp.py # 3. 分别提取三种特征这一步的输出和前面生成的scp一一对应 python feature_ex_Fbank.py python feature_extract_MFCC.py python feature_extract_pncc.py # 4. 训练UBM运行时用chooes()输入 fbank / mfcc / pncc 选定特征类型 python train_UBM.py # 5. 第二次运行 train_spk_model.py用chooes()选定同一特征类型生成说话人模型 python train_spk_model.py这个顺序本身就是传统声纹识别系统的标准流水线先有数据列表再提特征再训UBM再自适应出说话人模型最后才是打分。我第一次拿到这类项目时会先按这个顺序空跑一遍即使中途报错也不急着改先看它停在哪一步再回头翻文档说明.md比从读代码开始快得多。跑的时候每一步都要回头确认输出存在scp文件是文本直接打开看特征文件通常是二进制矩阵看文件大小是否为0。这里有个习惯值得养成——把每个脚本的输出路径提前记下来比如fbank特征存在哪个目录、mfcc特征又存在哪个目录后面很多报错都源于“我以为它生成了其实没有”。2.2 静音消除yu_pos.py为什么是可选步骤yu_pos.py在流水线最前面但文档说它是可选的。原因在于TIMIT这类数据集本身在安静录音室环境下采集句首句尾的静音段很短对GMM建模的影响往往可以忽略。但如果未来换自己录的数据或者环境里有很多无语音片段静音消除就很必要。常见做法是基于短时能量和过零率的双门限VAD把音频切成25毫秒左右的帧计算每帧的短时能量和过零率能量低于阈值且过零率接近静音水平的帧标记为静音再用前后几帧做平滑避免把清音和弱音也切掉。这个项目的yu_pos.py就是这类思路的轻量实现# yu_pos.py 的核心逻辑课程项目的典型VAD实现 def vad_frame(frame, energy_thresh, zcr_thresh): # 帧能量对帧内采样点求平方和取对数数值越大代表这一帧越“响” energy np.log(np.sum(frame ** 2) 1e-10) # 过零率采样点符号发生变化的次数语音浊音段过零率低清音和静音段高 zcr np.sum(np.abs(np.diff(np.sign(frame)))) / 2 # 双门限能量和过零率同时超过设定范围才判为静音 if energy energy_thresh and zcr zcr_thresh: return True # 静音帧 return False这段代码里energy_thresh和zcr_thresh是经验参数。对16kHz采样、16bit的音频能量阈值一般在0到10之间手动调过零率阈值在10到20之间。调参的依据是看VAD输出后保留的语音段能否完整覆盖一句话里所有成音部分。阈值设太高韵母的弱收尾会被切成碎块设太低静音段残留下来后面提取的特征里混入大量低能量帧UBM训练的收敛速度会变慢。实际跑的时候我一般会先对一段音频做可视化把能量曲线画出来再定阈值而不是一上来就套默认值。2.3 scp列表制度训练、注册、验证为什么必须分开scp是Speech Corpus格式的列表文件每一行对应一段音频典型格式是“音频路径 标签”标签可以是说话人ID或句子ID。这个项目里train_spk_model.py生成ubm_wav.scp和test.scpval_enrollment_scp.py生成enrollment.scp和var.scp四份列表各有各的用途列表文件用途内容来源ubm_wav.scp训练UBM用覆盖尽量多说话人的语音训练集全体说话人test.scp测试/评估用包含身份未知的待识别语音测试集说话人enrollment.scp注册用每个说话人一段或多段语音用来建模型验证集说话人var.scp验证用与注册说话人一一比对打分与enrollment配对把注册和验证分开是实验纪律里很重要的一条。如果同一个人的同一段录音既用来注册又用来验证打分结果会虚高等错误率好看到不真实这在课程答辩时是最容易被问倒的问题之一。正确做法是enrollment.scp里每人取前几段语音注册模型var.scp里每人另取不同句子做验证确保注册语音和验证语音没有重叠。TIMIT数据集本身提供了训练集与测试集的说话人划分项目是按这个约定来生成列表的。如果你要换自己的数据集至少要自己保证训练UBM的说话人、注册的说话人和验证的说话人互不重叠否则UBM吸收了目标说话人的分布后面MAP自适应就失去意义了。顺带一提train_test_scp.py在文件清单里是辅助工具通常用来做列表的拆分和重排比如把ubm_wav.scp按说话人随机打乱或者从test.scp里抽一部分做调试。它不是主流程必需脚本但如果数据集很大想先拿一小部分验证代码能跑通用它比手动改列表方便。3. 三种特征提取的取舍MFCC、FBANK与PNCC特征提取是把音频变模型输入的环节。这个项目一次实现了三个前端不是堆功能而是三种特征分别适合不同条件。搞清楚它们各自做了什么、在什么场景下选哪个是复用这份资源时最值得花时间的一步。3.1 MFCC提取流程与参数含义MFCC是语音识别里最经典的声学特征。feature_extract_MFCC.py做的事情可以拆成四步预加重、分帧加窗、Mel滤波器组、对数与DCT。预加重用一阶高通滤波器把高频分量抬起来弥补语音发声时高频能量天然衰减的问题分帧加窗用25毫秒的汉明窗把连续信号切成帧帧间重叠50%保证特征序列在时间轴上是平滑过渡的。# feature_extract_MFCC.py 中MFCC提取的简化流程 def extract_mfcc(wav, sr16000): # 预加重y[n] x[n] - 0.97 * x[n-1]提升高频分量 pre_emph np.append(wav[0], wav[1:] - 0.97 * wav[:-1]) # 分帧加窗25ms帧长10ms帧移汉明窗 frame_len int(sr * 0.025) # 400个采样点 frame_shift int(sr * 0.010) # 160个采样点 frames framing(pre_emph, frame_len, frame_shift) frames * np.hamming(frame_len) # FFT后过Mel滤波器组取对数 power_spec np.abs(np.fft.rfft(frames, n512)) ** 2 mel_energy np.dot(power_spec, mel_filterbank.T) log_energy np.log(mel_energy 1e-10) # DCT得到倒谱系数通常取前13维 mfcc scipy.fftpack.dct(log_energy, axis1)[:, :13] return mfcc这里几个参数值得留意。预加重系数0.97是经验值接近1但不等同于1目的是在提升高频的同时不引入低频失真。帧长25毫秒、帧移10毫秒是语音特征提取的默认配置因为语音在20到30毫秒内可近似看成平稳信号。Mel滤波器组的个数在这个项目里一般设26或40MFCC维度取13维。如果做说话人识别13维通常偏少常见做法是拼接13维静态系数加13维一阶差分加13维二阶差分凑成39维或者直接用FBANK的40维特征训练GMM而不做DCT压缩。这个项目后续训练UBM时会读取mfcc特征文件你要确认维度设置与train_UBM.py里配置的输入维度一致不然EM迭代第一步就会因为维度对不上而报错。3.2 FBANK与MFCC的差别少一步DCT多一分相关性FBANK特征其实是MFCC去掉最后一步DCT之后的中间产物。feature_ex_Fbank.py保存的就是经过Mel滤波器组和对数运算之后的滤波器组能量没有做倒谱变换。少了一步变换FBANK到底好在哪关键在DCT的作用是去相关和降维。MFCC通过DCT把高维的滤波器组能量压缩成低维倒谱系数丢掉的是滤波器组输出中彼此相关的部分。对传统GMM来说特征维数低、各维相关性低协方差矩阵更接近对角阵训练更稳定。但FBANK保留了相邻Mel频带之间的能量相关性配合GMM的协方差建模反而能捕捉到更多频谱细节。在语音内容识别任务里两者差不多但在说话人识别里频谱细节正是区分不同人发声习惯的关键。这个项目同时提供fbank和mfcc两条路线的特征提取正确用法是训练UBM和训练说话人模型时选择同一个特征类型。很多人第一次跑会踩这个坑训练UBM时选了mfcc训练说话人模型时又选了fbank两者特征维度都不同加载模型时要么报错要么默默训练出一个没用的模型。实际体验下来FBANK配合GMM-UBM的识别率通常会略高于MFCC特别是在训练数据量充足的情况下。PNCC则主要在噪声环境下优势明显。三者的适用场景差异通常在干净语音上MFCC和FBANK差距不大在有背景噪声或信道失真的数据上PNCC的鲁棒性会明显体现出来。3.3 PNCC的鲁棒性来源从频谱增强到幂律压缩PNCC的全称是Power-Normalized Cepstral Coefficients设计目标就是对抗噪声和混响。它和MFCC在结构上相似都是先做频谱分析再经过滤波器组和压缩最后做DCT。差别在中间的处理环节。MFCC的经典流程里FFT功率谱直接经过Mel滤波器组然后取对数对数压缩是固定底数的非线性变换。PNCC的做法要复杂几层先用带通滤波器和中值滤波估计每个频带的噪声基底从功率谱中减去噪声成分得到增强后的频谱然后做幂律非线性压缩通常取1/15的幂次这个幂律是从人耳听觉的响度感知模型推出来的比对数压缩更接近听觉神经对语音强度的映射再加一个时域掩蔽步骤用邻近帧的能量做平滑抑制脉冲噪声。最后再走DCT得到倒谱系数。# feature_extract_pncc.py 中PNCC的简化处理链 def extract_pncc(wav, sr16000): # 1. 短时傅里叶变换得到功率谱 power_spec stft_power(wav, sr) # 2. 中值滤波估计噪声基底并从功率谱中扣除 noise_floor median_filter(power_spec, kernel_size5) enhanced np.maximum(power_spec - noise_floor, 0) # 3. 幂律非线性压缩gamma 1/15 compressed np.power(enhanced, 1.0 / 15.0) # 4. 时域掩蔽对时间轴做指数平滑抑制脉冲突变 masked temporal_mask(compressed, coeff0.5) # 5. 过Mel滤波器组、取对数、DCT return dct(np.log(np.dot(masked, mel_filterbank.T)))[:, :13]参数上最值得关注的是中值滤波的kernel_size和时域掩蔽的平滑系数。kernel_size取5意味着每个频带取相邻5帧的中值作为噪声估计窗口太大反应慢会误把语音段能量当成噪声估计进去窗口太小又挡不住持续性的背景噪声。平滑系数0.5是保守默认值如果面对的噪声是短促的开关声、键盘声可以把系数调到0.3以下让掩蔽更敏感。PNCC在干净语音上不一定比MFCC强但在有噪声的测试条件下优势非常明显。我拿到这个项目后实际体验是同样的GMM-UBM管线把特征从MFCC换成PNCC在加了环境噪声的测试集上等错误率下降明显。代价是提取速度慢不少中值滤波和时域掩蔽都是逐帧逐频带的操作特征提取耗时大约是MFCC的2到3倍。如果你的课程设计没有噪声鲁棒性要求直接用FBANK性价比最高如果答辩题目里带“鲁棒说话人识别”这类字眼PNCC就是加分项。4. GMM-UBM训练与打分从EM迭代到似然比判决特征提取完之后进入整个系统的核心训练UBM、自适应说话人模型、打分判决。这三个环节分别对应train_UBM.py、train_spk_model.py和eval_score.py每一步都有值得展开的参数细节。4.1 UBM训练train_UBM.py的EM迭代与分量数选择UBM是通用背景模型本质上是一个用大量非目标说话人的语音训练出来的大GMM。它扮演的是“普通人声音分布”的基准。train_UBM.py读取ubm_wav.scp对应的特征用期望最大化算法迭代拟合GMM参数。GMM的数学形式是多个高斯分量按权重叠加每个说话人的声学特征分布用M个高斯分量描述每个分量有自己的均值向量、协方差矩阵和权重。EM迭代的每一步分两步E步计算每一帧特征落在每个高斯分量上的后验概率M步用这些后验概率作为软标签重新估计权重、均值和协方差。迭代到对数似然的变化量小于某个阈值才停。# train_UBM.py 中GMM-UBM训练的EM思路 def gmm_em(features, n_components32, max_iter50): # 初始化用KMeans给特征聚类用聚类中心初始化各个高斯分量的均值 gmm initialize_with_kmeans(features, n_components) for i in range(max_iter): # E步计算每个样本在每个高斯分量上的后验概率责任度 responsibilities gmm.predict_proba(features) # M步用责任度加权重估权重、均值、协方差 gmm.update_parameters(features, responsibilities) # 判断收敛对数似然变化小于1e-4就停止 if abs(gmm.log_likelihood_change()) 1e-4: break return gmm高斯分量数n_components是最关键参数常见做法是32、64或128。分量越多对训练数据的拟合越精细但也更容易过拟合。UBM由于是大量说话人数据训练出来的收敛性相对好可以用128个分量到了说话人模型自适应阶段如果每个说话人的注册语音只有几十秒用64个分量就够了。一个经验规则是每个高斯分量至少要有足够帧数支撑均值估计注册语音总帧数除以分量数不要小于几十帧。如果注册语音只有10秒左右对应特征大约是1000帧分量数超过64就有点危险。4.2 说话人模型自适应train_spk_model.py的MAP更新UBM训练好以后下一步为每个说话人生成自己的模型。train_spk_model.py在这一阶段做的是最大后验自适应不是从零训练一个新GMM而是把UBM的参数作为先验用该说话人的少量语音把UBM的均值拉向说话人自己的特征分布。MAP的核心是自适应“力度”的控制。每个高斯分量的均值更新时并不完全等于该说话人语音在这个分量上的均值而是新旧信息的加权平均权重由该说话人语音落在该分量上的帧数和预先设定的自适应因子共同决定。帧数越多越信任新数据帧数越少越依赖UBM的先验。# train_spk_model.py 中MAP自适应说话人模型的示意代码 def map_adapt(ubm, spk_features, relevance_factor16.0): # 计算说话人特征在UBM各分量上的后验概率 gamma ubm.predict_proba(spk_features) for c in range(ubm.n_components): n_c gamma[:, c].sum() # 落在分量c上的等效帧数 data_mean (gamma[:, c].dot(spk_features)) / max(n_c, 1e-10) # 权衡系数alpha n_c / (n_c relevance_factor) alpha n_c / (n_c relevance_factor) ubm.mean_[c] alpha * data_mean (1 - alpha) * ubm.mean_[c] return ubmrelevance_factor是MAP自适应里最重要的参数常用范围在8到32之间。它越大说话人模型越接近UBM个性化程度越低它越小说话人模型越依赖注册语音对注册语音的质量也越敏感。如果注册语音里有明显的环境噪声或误切出来的静音段relevance_factor又设得很小说话人模型会被这些噪声帧带偏后面打分时对同一个人其他干净语音的得分反而很低。课程项目里一般不会主动让你调这个参数但答辩老师很可能会问“自适应因子为什么这样设”所以至少要能说出relevance_factor对模型偏置和方差的影响。4.3 打分与阈值eval_score.py的似然比判决最后一个关键环节是打分。eval_score.py加载说话人模型和UBM对每一段待验证语音计算两个对数似然一个是这段语音被该说话人模型生成的概率一个是它被UBM生成的概率。两个对数似然的差就是对数似然比分数。分数越高说明这段语音越不像普通人的声音、越像目标说话人。这个设计是GMM-UBM体系最巧妙的地方。如果不做差、只看说话人模型的绝对似然不同语音时长会让分数标准不一致。而UBM作为“普通人基准”可以把说话人模型的分数归一化到“相对普通人的偏离程度”上分数跨语音时长的可比性更强。打分时通常还会对帧得分做平均用平均对数似然比而不是累加值进一步消除时长影响。训练完以后判断“是不是本人”需要一个阈值。低于阈值判为冒认者高于阈值判为目标说话人。项目里通常的做法是把var.scp的验证分数画成分布图取两个分布的交叉点或计算等错误率阈值。等错误率的意思是误接受率和误拒绝率相等时的点在课程答辩里是最直观的评估指标。如果文档里没有推荐阈值先用所有验证分数的均值附近当作初始阈值再根据期望偏安全还是偏方便来调整。到这里整个GMM-UBM系统的工作链闭环特征提取把音频变成特征矩阵训练用EM训UBM、用MAP得到说话人模型评估用似然比打分。5. 避坑与常见问题跑这个项目最常翻车的五个地方5.1 scp文件路径写错或带空格特征读取阶段直接失败现象运行特征提取脚本时报文件找不到但文件明明就在scp里列出的路径下。原因scp文件里写的是绝对路径但数据集换过目录或者Windows系统上路径分隔符是反斜杠脚本里却用正斜杠去拼接。此外目录名里有空格的话按行split会把路径拆成多段。解决先检查scp文件里每一行的格式确认路径分隔符和实际环境一致。我一般会在提取特征前写一个简短的Python脚本遍历scp里的每一条路径用os.path.exists逐一验证把不存在的路径打印出来。路径换成绝对路径最省心别依赖脚本里默认的相对路径拼接。5.2 静音消除过猛特征矩阵变成空文件现象yu_pos.py跑完后某些音频的帧数变成0对应的特征文件也是空的训练UBM时加载进来直接报维度错误。原因双门限VAD的阈值设得太高把清音段和弱音段全部判成静音并切掉了。尤其是一些音量偏小的说话人整个句子都可能被误判为静音。解决用VAD之前先做一次音量归一化把每段音频的峰值幅度统一缩放到一个固定范围再设定能量阈值。阈值不要卡死留出余量宁可多留一点静音帧也不要切掉语音。跑完VAD后检查被切掉的部分占原音频的比例如果超过10%基本可以断定阈值偏高。5.3 特征类型不统一训练出的说话人模型是废的现象训练UBM时选fbank训练说话人模型时也选了fbank但打分阶段eval_score.py报特征维度不匹配。原因eval_score.py里加载的是mfcc特征文件而模型是fbank特征训练的。这类错误出现在chooes()这种交互式选择设计的项目里特别常见每次运行都要手动输入参数一不留神就选岔了。还有一种更隐蔽的情况三个特征脚本保存的路径或文件名不同打分脚本默认去读mfcc的特征目录导致模型和特征对不上。解决把特征类型当成全局配置管理而不是每次交互输入。我拿到这类项目后的第一个改动就是把train_UBM.py和train_spk_model.py里的chooes()替换成读取一个config变量比如feature_type mfcc并统一设置输出路径的前缀。训练完看一眼生成的模型文件名带的是哪种特征后缀再跑打分能省掉很多莫名其妙的报错。5.4 训练集测试集说话人有重叠识别分数虚高现象模型在var.scp上的分数高得不正常等错误率压到百分之零点几答辩时老师一追问就露馅。原因数据列表生成时没有严格执行说话人水平的划分同一个人的语音既出现在ubm_wav.scp里用于训练UBM又出现在enrollment.scp里注册了模型还在var.scp里当了验证语音。三重重叠等于开卷考试。解决重新生成列表确保UBM训练集、注册说话人集合、验证说话人集合三者互不重叠。如果换了自己的数据集先按说话人ID分组把说话人整体划分到集合里而不是按音频文件随机切。检查方法很简单写一段脚本提取每个列表里的说话人ID用集合的intersection看一下有没有交集。5.5 打分阶段没有做时长归一化长音频天然占便宜现象同一个人的短语音分数很低长语音分数高但阈值对长短语音都很公平实际却一直在误判。原因eval_score.py如果直接累加每帧对数似然比而不是用帧数做平均长音频的分数天然更高。GMM的逐帧似然都是负值累加后数值会随帧数线性增大直接把长短语音的分数拉到不同量级。解决改成累加后除以总帧数用平均对数似然比作为分数。如果不想改打分脚本也可以在测试时把音频统一截断到固定长度再打分。这个问题在课程项目里经常被忽视因为大部分演示数据时长都差不多一旦自己录几段长短不一的语音来测试立刻就会暴露。6. 验证与进阶让识别结果可解释的三个习惯把系统跑通以后重要的不是急着调参而是先建立一套能解释实验结果的验证习惯。我从这个项目里总结出三个习惯在答辩和后续换数据集时反复用上了。6.1 用等错误率描述性能而不是只报一个识别准确率GMM-UBM的输出是分数所谓识别准确率依赖阈值选择换个阈值就成了另一个数。而等错误率不依赖阈值它关注注册说话人自身分数和冒认者分数两个分布的交叉点是这套系统最稳定的评价指标。每次调完特征或模型参数把这个数记下来比“准确率95%”更有说服力。6.2 给特征做倒谱均值减MFCC和FBANK在特征提取完后把每一维特征在整个句子上减掉均值再归一化能有效去除信道和麦克风响应带来的加性偏移。我一般把CMS作为说话人识别的默认预处理它几乎不增加计算量但对跨设备录音的识别效果改善明显。6.3 固定随机种子让每次训练可复现EM初始化和KMeans聚类都带有随机性不固定种子的话连续两次训练UBM得到的分量均值可能不同实验结果无法复现。在train_UBM.py开头加一行随机种子固定是最容易做、也最容易加分的小改动。从那以后我每次跑这类项目都强制自己先做这三步固定随机种子、确认注册语音和验证语音不重叠、用平均对数似然比打分。希望帮到你。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表