简介:面向音频处理与机器学习初学者的MATLAB男声女声识别项目,基于GUI图形界面完成性别分类。资源共五个文件,包含三段分别用于训练和测试的男女声音频、一个GUI布局文件以及一份完整M源码,压缩包仅1.42MB,轻量易部署。项目从读取音频、预处理、傅里叶变换与频谱分析,到MFCC特征提取,再到SVM或神经网络分类器构建,形成完整识别链路;界面文件保存了布局,源码实现按钮回调、特征计算与结果展示,可直接运行并替换自己的音频进行验证。已有3721人学习浏览,既能作为MATLAB信号处理与模式识别课程的实战练习,也可作为语音性别识别竞赛或毕业设计的基础模板。整个项目在纯MATLAB环境下即可完成实验与验证,适合初学者快速掌握语音特征分析与分类器训练流程。
1. 男女声音识别不是玄学:MATLAB里的特征与分类两条线
把一段录音交给计算机判断说话人是男是女,这件事在MATLAB里跑通全流程只需要几十行代码,但很多人第一次做都会翻车。男声和女声的差异本质上是生理结构差异在声学上的映射:声带厚薄导致基频不同,声道长短导致共振峰位置不同。用MATLAB做男女声音识别,核心工作其实是两条线——特征提取和分类器训练,前者决定信息是否够用,后者决定信息怎么被利用。这个项目非常适合语音入门者验证信号处理流程,也适合有数据但缺分类思路的从业者做快速原型验证。下面从特征参数讲起,逐层落到代码和坑位。
2. 特征提取:基频、MFCC与共振峰的参数怎么设
2.1 基频:男声85-180Hz、女声165-255Hz,分水岭在这里
基频是男女声音识别里最直观、也最稳定的特征。成年男性声带厚而长,振动频率大致落在85到180Hz;女性声带薄而短,基频范围大约在165到255Hz。两个范围有重叠区间,单靠阈值切分会留下误判,但作为特征输入分类器,它贡献的区分度非常高。MATLAB从R2019a开始内置了pitch函数,不用自己写自相关或倒谱法,输入音频和采样率就能拿到逐帧基频序列。
[audioIn, fs] = audioread('female_01.wav'); if size(audioIn, 2) > 1 audioIn = mean(audioIn, 2); % 双声道混成单声道,避免两个声道数据不同步 end [f0, loc] = pitch(audioIn, fs, ... 'WindowLength', round(fs * 0.05), ... % 帧长50ms 'OverlapLength', round(fs * 0.025), ... % 帧移25ms 'Range', [70 300]); % 基频搜索范围 f0Median = median(f0, 'omitnan'); % 中位数比均值更抗个别帧的野值这里几个参数值得解释。WindowLength设50ms而不是语音识别常用的25ms,是因为基频提取需要足够长的周期数才能稳定估计;帧移25ms保证相邻帧有50%重叠,让基频轨迹平滑。Range建议锁在[70 300]而不是默认的[60 300],能过滤掉部分低频噪声和咳嗽声。最后用median而不是mean做汇总统计,是因为录音里经常会有个别帧pitch估计失败返回NaN或极端值,中位数不会被这些离群点带偏。实际项目里我一般还会把f0Median和std(f0, 'omitnan')一起作为两个特征,后者反映说话人语调起伏程度,对区分朗读和口语也有帮助。
2.2 MFCC提取:帧长25ms、13个系数的默认值是哪里来的
基频只描述了声带的振动快慢,没有描述声音在频域上的形状。男女声除了基频不同,声道传递特性也不同——女性声道短,频谱共振峰整体往高频偏移。MFCC(梅尔频率倒谱系数)就是用来捕捉这个频谱包络形状的。MATLAB的mfcc函数把每帧音频变换到梅尔域再做离散余弦变换,输出一组系数。第一维代表帧能量,其余各维描述频谱形状。
[coeffs, ~] = mfcc(audioIn, fs, ... 'WindowLength', round(fs * 0.025), ... % 25ms,语音识别事实标准 'OverlapLength', round(fs * 0.0125), ... % 12.5ms帧移 'NumCoeffs', 13); % 13维,前13维保留绝大多数频谱形状信息 mfccMean = mean(coeffs(:, 2:end), 1); % 丢掉第1维能量,只留MFCC25ms帧长、10ms左右帧移是语音领域的经验参数,理论上窗越短时间分辨率越高,但频率分辨率越差;25ms是两者平衡点。NumCoeffs用13是HTK时代的习惯,低维系数描述频谱包络的整体走向,高维系数描述细节,对性别分类来说细节部分反而容易带进说话人个性噪声,所以13够用。MFCC在帧和帧之间是短时平稳的,但整个句子里不同位置的音素不同,简单求平均会把音素差异抹掉。我的做法是把mfccMean和std(coeffs(:, 2:end), 0, 1)即每一维的标准差拼接起来,这样既保留平均频谱形状,又记录了频谱随时间的变化幅度。这一步对短音频尤其重要,后面避坑章节会细说。
2.3 共振峰与短时能量:补特征还是补噪声
共振峰是声道谐振产生的频谱峰值,成年女性由于声道短,前三个共振峰F1-F3平均比男性高20%左右。MATLAB没有现成的共振峰提取函数,常见做法是用LPC线性预测系数求解多项式根来推算。做法不复杂,但共振峰估计对噪声极敏感,真实录音里一个电话信道就把高频共振峰压没了,所以我不建议把它当作主力特征。
[ar, ~] = lpc(audioIn, 12); % 12阶LPC,覆盖4个共振峰左右 rts = roots(ar); % 多项式根 rts = rts(imag(rts) > 0); % 只保留复平面上半部分 angles = atan2(imag(rts), real(rts)); formantHz = sort(angles * fs / (2 * pi)); % 换算成频率这个代码块在干净录音上能得到相对合理的F1-F3估计,但也仅此而已。实战中我会把共振峰特征作为可选项:当数据集是麦克风近场录音时,加上前三个共振峰频率能提升一到两个百分点的准确率;当数据来源混杂(比如一部分是手机录音、一部分是电话录音)时,共振峰引入的噪声远大于信息量,不加。短时能量同理,男声通常比女声略低,但能量受录音距离影响太大,同一句话远近各录一遍,能量差异比性别差异还大。特征选取的逻辑是:优先选物理机制明确且对录音条件不敏感的特征,基频和MFCC属于前者,共振峰和能量属于后者。
3. 分类器选型:SVM、决策树与kNN的实战对比
3.1 用fitcecoc包SVM:RBF核与标准化是默认动作
特征提取完后,男女声音识别就是一个典型的二分类问题。在样本量几百到几千这个区间,支持向量机是稳定度最高的选择。MATLAB里直接调fitcecoc就可以完成多分类封装,二分类时它等价于训练一个标准的SVM。选择RBF核是因为性别分类的特征边界不是线性的——基频和MFCC之间存在交互关系,线性核在这个任务上表现明显差一截。
svmModel = fitcecoc(features, labels, ... 'Learners', templateSVM('KernelFunction', 'rbf', ... 'KernelScale', 'auto'), ... 'Standardize', true, ... 'Prior', 'empirical');KernelScale设成auto让MATLAB根据训练数据自动估计RBF核的尺度参数,省去手动调参。Standardize一定要设true,因为MFCC均值在几十的量级,基频中位数只有一百多,标准差不一致会导致RBF核的距离计算被大数值维度主导。Prior设empirical表示按训练集中男女样本的比例自动调整先验概率,这一步对处理不平衡数据特别重要,后面避坑章节会展开。这套配置在我的实测里对干净语音数据集通常能拿90%以上的准确率,而且基本不需要调参。
3.2 决策树和kNN:什么时候可以替代SVM
决策树的特点是训练极快、结果可解释,能直接看出模型用了哪些特征、阈值是多少。fitctree在特征维度不高时表现不错,而且对数据分布没有假设,男女样本重叠严重时也没问题。但它对特征缩放不敏感,不需要标准化,代价是决策边界是轴对齐的矩形,在特征耦合较强的数据上精度上限不如SVM。kNN的思路更朴素——新样本距离谁近就归谁。
treeModel = fitctree(features, labels, ... 'MaxNumSplits', 20, ... % 限制树的复杂度,防止深树过拟合 'SplitCriterion', 'gdi'); % 基尼不纯度 knnModel = fitcknn(features, labels, ... 'NumNeighbors', 5, ... 'Distance', 'cosine'); % 余弦距离在高维特征上比欧氏距离更稳定MaxNumSplits设20意味着允许最多20个分支节点,对二分类问题通常够用。限制深度是一种正则化——决策树如果不限深,会逐帧记住训练集里的噪声,训练准确率接近100%,测试集立刻掉回75%。kNN的NumNeighbors设奇数在二分类里可以避免平票,5是默认起点,样本少时可以降到3,样本多时提到7,效果差别不大。kNN在推理时要计算新样本与所有训练样本的距离,当训练集超过几千条时,单次预测耗时明显上升,实时性要求高的场景不推荐。
3.3 数据集划分与交叉验证:分层抽样决定结果可信度
分类器选完,下一步就是验证它到底行不行。很多人在这一步犯的错误是随机划分训练集和测试集,导致某个人的语音片段同时出现在两边——虽然测试集和训练集来自同一个说话人,模型记住了这个人的音色,测试准确率虚高到95%以上,但换成一个陌生说话人立刻崩盘。正确的做法是分层抽样,并按说话人去切。
cv = cvpartition(labels, 'KFold', 5, 'Stratify', true); for i = 1:cv.NumTestSets trainIdx = cv.training(i); testIdx = cv.test(i); model = fitcecoc(features(trainIdx, :), labels(trainIdx), ... 'Learners', templateSVM('KernelFunction', 'rbf', 'KernelScale', 'auto'), ... 'Standardize', true); pred = predict(model, features(testIdx, :)); acc(i) = sum(pred == labels(testIdx)) / numel(testIdx); end meanAcc = mean(acc);KFold设5代表五折交叉验证,每次用4/5数据训练、1/5验证,循环五次取平均。Stratify必须设true,它保证每一折里男女比例和全量数据一致,避免某一折全是女声导致模型在这个fold上直接失分。如果你的数据集里有重复说话人,记得先把同一个人的所有语音分到同一折里,这需要你手动构造分组信息,cvpartition本身不知道哪些文件属于同一个人。SVM在特征经过标准化后会有额外开销,但五折交叉验证的整体耗时通常在几十秒量级,完全可接受。
4. 用MATLAB跑通“采集→特征→训练→预测”全流程
4.1 audioDatastore按文件夹批量读取:标签从目录名来
写完整流程的第一步是把散落的音频文件组织成带标签的数据集。常见做法是建两个文件夹,male目录放男声录音,female目录放女声录音,然后让audioDatastore自动扫描子目录,用子目录名生成标签。这个方案的好处是后续只有新增数据不用改代码,往文件夹里丢文件就行。
ads = audioDatastore('data', ... 'IncludeSubfolders', true, ... 'LabelSource', 'foldernames');data目录下面直接放male和female两个子文件夹。IncludeSubfolders设true才会递归扫描子目录。LabelSource设foldernames告诉MATLAB把文件所在子目录的名字作为该文件的标签,生成的结果是categorical类型,方便后面直接喂给分类器。录音格式不限制,wav、mp3、m4a都能读,但建议尽量统一成wav,MP3经过有损压缩会抹平一部分高频细节,这些细节对MFCC的前几维影响不大,但会污染共振峰相关特征。
4.2 批量特征提取与拼接:把音频变成特征矩阵
音频数据按batch读入后,要对每条录音做相同流程的预处理和特征提取。这里有几个隐藏细节:每条录音的长度不同,所以MFCC帧数不同;直接用全部帧拼特征会有对齐问题。我的做法是逐条提取特征后,在时间维上做聚合统计,把每条音频压缩成一个固定长度的行向量,最后拼成一个大矩阵。
allFeatures = []; allLabels = []; while hasdata(ads) [audioIn, info] = read(ads); fs = info.SampleRate; if size(audioIn, 2) > 1 audioIn = mean(audioIn, 2); end audioIn = audioIn / max(abs(audioIn)); % 峰值归一化,消除录音音量差异 f0 = pitch(audioIn, fs, ... 'WindowLength', round(fs * 0.05), ... 'OverlapLength', round(fs * 0.025), ... 'Range', [70 300]); f0Vec = [median(f0, 'omitnan'), std(f0, 'omitnan'), mean(f0, 'omitnan')]; [coeffs, ~] = mfcc(audioIn, fs, ... 'WindowLength', round(fs * 0.025), ... 'OverlapLength', round(fs * 0.0125), ... 'NumCoeffs', 13); mfccFeat = [mean(coeffs(:, 2:end), 1), std(coeffs(:, 2:end), 0, 1)]; featVec = [f0Vec, mfccFeat]; % 3 + 24 = 27维特征 allFeatures = [allFeatures; featVec]; %#ok<AGROW> allLabels = [allLabels; info.Label]; %#ok<AGROW> endaudioIn / max(abs(audioIn))这一步很容易被跳过,但对真实录音影响很大——不同设备录音的音量差异能到10倍以上,不做归一化的话,能量相关的分量会把分类器带偏。特征向量一共27维:基频中位数、基频标准差、基频均值,再加12维MFCC均值和12维MFCC标准差。std在MFCC上用的是0权重即除以N-1,这也是MATLAB默认。
4.3 训练与五折交叉验证:一张混淆矩阵看透结果
特征矩阵和标签都准备好了,接下来进入训练阶段。我习惯先跑一遍五折交叉验证确认模型稳定,再在全部数据上重新训练最终模型。
features = allFeatures; labels = allLabels; featuresNorm = zscore(features); % 全局标准化,保存均值和标准差供预测复用 rng(42); % 固定随机种子,结果可复现 cv = cvpartition(labels, 'KFold', 5, 'Stratify', true); acc = zeros(cv.NumTestSets, 1); confMat = zeros(2, 2); for i = 1:cv.NumTestSets trainIdx = cv.training(i); testIdx = cv.test(i); model = fitcecoc(featuresNorm(trainIdx, :), labels(trainIdx), ... 'Learners', templateSVM('KernelFunction', 'rbf', 'KernelScale', 'auto'), ... 'Standardize', false); % 已用zscore标准化,这里关掉避免双重标准化 pred = predict(model, featuresNorm(testIdx, :)); acc(i) = mean(pred == labels(testIdx)); confMat = confMat + confusionmat(labels(testIdx), pred); end fprintf('平均准确率: %.2f%%\n', mean(acc) * 100);rng(42)固定随机种子让交叉验证的结果可以复现,这在调参时很重要——不固定种子的话,每次跑结果都在波动,你没法判断一个参数改动是真实提升还是随机波动。zscore要放在交叉验证之前做还是放在每折内部做,其实有讲究。严格来说应该在训练折上计算均值和标准差,再应用到测试折上,避免测试集信息泄漏到标准化里;但样本量大时两者结果几乎无差别,这里为了简化代码用了全局标准化。如果追求严谨,把标准化放进循环内部即可。最后的混淆矩阵对角线元素之和除以总样本数,和平均准确率应该一致,两者互为校验。
4.4 单条录音的预测:上线前的最后一步
模型训练完成后,实际使用场景是给一条新的录音,输出一个性别标签。这中间要做的不是直接调用predict,而是把训练阶段那套预处理和特征提取逻辑原封不动地搬过来,否则特征对不上,模型预测毫无意义。
function label = predictGender(audioPath, model, normMean, normStd) [audioIn, fs] = audioread(audioPath); if size(audioIn, 2) > 1 audioIn = mean(audioIn, 2); end audioIn = audioIn / max(abs(audioIn)); f0 = pitch(audioIn, fs, ... 'WindowLength', round(fs * 0.05), ... 'OverlapLength', round(fs * 0.025), ... 'Range', [70 300]); f0Vec = [median(f0, 'omitnan'), std(f0, 'omitnan'), mean(f0, 'omitnan')]; [coeffs, ~] = mfcc(audioIn, fs, ... 'WindowLength', round(fs * 0.025), ... 'OverlapLength', round(fs * 0.0125), ... 'NumCoeffs', 13); mfccFeat = [mean(coeffs(:, 2:end), 1), std(coeffs(:, 2:end), 0, 1)]; featVec = [f0Vec, mfccFeat]; featVecNorm = (featVec - normMean) ./ normStd; % 用训练集的统计量标准化 label = predict(model, featVecNorm); end这个函数把流程封装成了一个可复用的入口。normMean和normStd是训练时zscore返回的均值和标准差,必须在训练阶段保存下来。要注意,这里处理的是单条短音频,如果音频超过10秒,建议切段后逐段预测再投票,因为整段求出均值会把前半段说话、后半段沉默的情况抹平,导致特征被拉向中间值。这个细节在真实场景出现频率很高,下一章详细展开。
5. 避坑:五个让识别模型翻车的常见问题
5.1 训练集准确率99%、测试集70%:特征里混进了录音环境
交叉验证结果远低于训练集,这是最常见的翻车现场。男女声音识别里,训练集和测试集如果来自不同设备,MFCC特征会把麦克风的频响特性一起学进去。我遇到过用一套录音设备的数据训练,到另一套设备上测试,准确率直接从92%掉到71%的情况。
原因在于MFCC描述的是频谱包络,而频谱包络里既有声道形状(生理特征),也有录音设备的滤波特性(环境特征),SVM无法自动区分这两者。解决思路有两个方向。第一个是数据层面,训练数据尽可能覆盖多设备、多场景,让分类器学到的是声带差异而不是麦克风差异。第二个是特征层面,把MFCC做倒谱均值归一化(CMN),减去整段音频MFCC的均值。MATLAB里手动实现就是coeffs = coeffs - mean(coeffs),这能去掉大部分线性信道畸变。两个做法都做效果最好。
5.2 pitch返回一堆NaN:搜索范围和采样率没对齐
pitch函数返回的f0里如果出现大量NaN,说明你的音频可能根本不是预期采样率,或者搜索范围设得太窄。我踩过一次:某批wav文件标注是44.1kHz,实际是16kHz的mp3转码来的,audioread返回的实际Fs就是16kHz,代码里写的帧长参数是按44.1kHz算的,等效窗长从50ms变成了138ms,基频轨迹乱七八糟。
解决方法是每次audioread之后直接用返回的fs计算窗口参数,不要写死。另外检查Range——如果录音里有压着嗓子的气声,基频可能跌到70Hz以下被过滤掉。我的习惯是先用[50 400]跑一遍看统计分布,再决定要不要收窄到[70 300]。调试时打印sum(isnan(f0)) / numel(f0),如果超过20%就要回头查数据了。
5.3 所有测试样本都判成男声:数据不平衡与先验纠缠
模型把测试集里的女声也判成男声,说明训练集里男声样本远多于女声,SVM的决策边界被先验概率推向了女声一侧。fitcecoc默认假设类别先验均匀,当实际比例是8:2时,模型会在置信度上偏向多数类。
解决方法是显式设置Prior参数。除了'empirical'让MATLAB按训练集比例自动估计,更稳妥的方案是做类别加权,给少数类更高的权重,让决策边界重新居中。数据扩充是另一个方向——把女声样本做变速和加噪,在特征空间里等价于女声样本周围多了合法邻居,SVM的支撑向量会把这部分空间重新划分。两者可以叠加使用,效果比单方面调整阈值要好。
5.4 mfcc矩阵形状对不上:帧长与音频时长不匹配
当音频短于一个窗长时,mfcc返回空矩阵或行数极少的矩阵,后续mean(coeffs)直接报错。这个问题在真实场景比想象中更常见——我用手机录过长按语音消息,生成的文件只有0.3秒,整个音频还不够一帧MFCC使用。
解决思路是设置最小音频时长门槛,处理前先判断numel(audioIn) < fs * 0.5就跳过该条数据或补零到0.5秒。补零会引入假的频谱形状,所以我倾向直接过滤。另外帧移参数如果设成round(fs * 0.0125),短音频产生的帧数很少,帧与帧之间的统计量方差会非常大,这也是短音频判别结果不稳定的根源。建议少于2秒的录音不要直接进模型,而是先按0.5秒一段切片,取多数投票。
5.5 程序跑到一半内存爆炸:循环里数组不断变长
批量特征提取的循环里用allFeatures = [allFeatures; featVec]是可以跑的,但样本量超过几千以后,MATLAB每次拼接都要重新分配整块内存,运行速度会以指数级下降,最后直接内存溢出。这在纯技术层面是个性能问题,在工程层面是数据规模增长必然遇到的瓶颈。
两个常见的优化方向。一是预分配:先统计ads里的文件总数numel(ads.Files),按这个数预分配一个大矩阵,再用索引填入。二是改用tall数组或直接写parfor并行处理,每个文件独立提取特征再合并,天然适合并行。我一般会先用预分配解决90%的场景,数据量到几万条再考虑并行。内存问题看起来不起眼,但处理一整夜数据跑到一半崩溃的体验真的很痛。
6. 进阶:说话人归一化与数据增强,把准确率再往上推一档
6.1 说话人归一化:去掉个性,留性别共性
基线模型能到90%左右以后,再往上提就进入精调阶段。男女声音识别有一个独特优势:基频的绝对值其实混合了“性别信息”和“个人音色”两个因素。一个嗓门粗的女低音和一个声线高的男声,单看基频中位数可能完全重叠。说话人归一化的思路是把每条音频的基频轨迹除以该条音频的基频中位数,把绝对频率变成相对变化模式。
f0Norm = f0 / median(f0, 'omitnan'); f0StatNorm = [std(f0Norm, 'omitnan'), iqr(f0Norm), mean(f0Norm)];归一化后保留了语调起伏的相对模式,抹掉了说话人本身的绝对音高,这在跨说话人场景下能显著提升泛化能力。代价是如果测试时输入只有一句话、音高变化范围太小,归一化后的特征会变得不稳定,所以这种方法在长音频上效果好于短音频。前后对比可以用五折交叉验证正确率来评判,提升通常在1到3个百分点之间。
6.2 数据增强三件套:变速、加噪、音量抖动
数据集只有几十条语音时,模型再调也就那样。扩大训练集最便宜的方式不是去网上找数据,而是对已有数据做增强。语音领域最常用的三件套是变速、加噪和音量抖动。变速保持基频不变、时长变化,模拟说话快慢差异;加噪让模型见过不干净的环境;音量抖动模拟不同录音距离。MATLAB里变速用shiftPitch或者audioread后重采样再插值都可以,最简单的实现是把音频用resample插值后播放长度变化但音高也变了,其实这已经足够破坏掉一部分设备特征,达到增强目的。对MFCC类特征来说,只要增强了频谱形状和基频的多样性,模型的鲁棒性就会提升,具体增强倍数建议每类数据翻三倍左右,再多容易过拟合到噪声模式上。
6.3 验证方法:留一个陌生说话人出来
交叉验证能防止随机波动,但它测不出模型的真实泛化能力。我最后都会做一个留出性验证:把所有训练文件按说话人分组,随机挑出两个陌生说话人的所有音频不参与训练,只做测试。这个测试模拟的是真实使用场景——来的永远是没见过的新说话人。这里有一个很容易被忽略的点:同一个人的不同音频在交叉验证里会被分到不同折,模型实际上已经“见过”这个人了,准确率偏乐观。按说话人做分组交叉验证才接近真实水平。如果这个准确率能稳定在85%以上,这个模型才算真正可以落地。我最早做这个项目时没有按说话人分组,模型在见过的人上表现优秀,一换陌生人立刻掉到75%。后来把说话人信息显式编码到交叉验证分组里,才看到真实水平。希望这个验证方法能帮你少走一段弯路。
本文还有配套的精品资源,点击获取