简介这是一份基于小波分解、主成分分析与支持向量机的情感分类MATLAB实现面向需要完成文本或信号情感识别、模式分类课题的本科生、研究生与算法开发人员。项目将小波的多尺度特征提取、PCA降维去噪以及SVM最优超平面分类相结合形成完整的情感分类流程并达到90%以上的识别准确率适用于情感分析、故障诊断等二分类场景。压缩包共598个文件、大小5.81MB以444个mat数据文件、79个m源码文件为主另有少量c/cpp、java、python及可执行文件等涵盖数据集、核心算法、辅助函数与说明文档。已有252人学习下载。通过M文件可复现从数据预处理、小波特征提取、PCA降维、SVM训练到测试评估的完整链路其中还包含psoSVMcgForClass等参数优化脚本便于进一步调参和改进模型。1. 语音情感分类不一定要上深度学习小波分解PCASVM这条老管线依然能打如果你手里有一批语音片段想按高兴、愤怒、悲伤、中性把它们分开第一反应可能是微调一个预训练模型。但数据量只有一两个小时、还要跑在普通CPU上时基于小波分解做特征、PCA主成分分析做降维、再用SVM支持向量机做分类的传统管线反而是最稳的一条路。这条管线的思路很直接小波分解把语音信号按频带拆开情感差异在高频细节和低频轮廓上的分布会显形PCA主成分分析把高维特征里冗余的相关性去掉SVM支持向量机在小样本下做决策边界既快又可解释。适合两类人一类是不想为几十条样本就拉深度学习环境的研究者另一类是需要在离线设备上做实时推理的工程师。下面的内容都会围绕这条管线展开。2. 三个黑匣子逐个拆开小波分解、PCA、SVM各自在解决什么先说结论这套管线里没有任何一个环节是玄学。小波分解负责“怎么把一段语音变成一堆有物理含义的数字”PCA负责“怎么把这堆数字压缩而不丢主信息”SVM负责“怎么在压缩后的空间里画一条能把不同情感分开的线”。下面逐个拆开讲每一部分都会落到后面要用的参数上。2.1 小波分解把一维语音信号摊开到时间-尺度平面语音情感识别里情感差异不是均匀分布在所有频带上的。愤怒和惊讶的语音里高频细节分量会在短时间内迅速增强悲伤和中性则往往在低频段表现出更强的能量延续。短时傅里叶变换STFT用一个固定窗长去切信号低频和高频共用同一套时间分辨率得不偿失。小波分解的核心优势是它用一组可伸缩的基函数低频用长窗看轮廓高频用短窗看细节把一维信号投射到一个时间-尺度平面上哪个情感在哪一段、哪个频带发力一目了然。这里需要把“去噪”和“特征提取”分开。很多入门文章拿小波做去噪那是把高频细节系数直接置零再重构情感分类里我们恰恰相反细节系数往往是愤怒、惊讶这类激活度高的情感最重要的证据。所以不要照着去噪的思路把高频细节丢掉。常见的做法是保留每一层的近似系数和细节系数从里面提取统计量而不是直接拿原始系数去塞给分类器——原始系数太长直接当特征既浪费维度又让分类器无从学起。在Python里最常用的是PyWavelets库核心函数是pywt.wavedec。调用一次之后会得到一列系数顺序固定为最低频的近似系数cA_n然后是第n层到第1层的细节系数cD_n ... cD_1。这个顺序是后面所有特征提取代码的基础记错顺序会导致特征位置对不上。2.2 PCA主成分分析降维给SVM腾出线性可分空间特征提取之后有个很现实的问题特征维度可能比样本量还大。比如每条语音分成几帧、每帧提取十几二十个统计量一个样本的特征向量很容易上百维而在情感数据集上一条样本往往只有一两千条。高维小样本的场景里SVM虽然比深度学习抗过拟合但特征之间高度相关时训练会变得不稳定决策边界也会被不重要的维度牵着走。PCA主成分分析做的就是找出一组正交方向让数据在这些方向上的方差依次递减前面的少数几个方向就能覆盖绝大部分信息。它的本质是一种无监督的线性变换不关心标签只关心数据的散布结构。这里有一个关键点PCA对量纲极其敏感。能量特征的数值可能到0.1级过零率在0.5级均值接近0如果不先做标准化第一主成分会完全被数值大的特征主导降出来的维度没有任何物理意义。所以在PCA前面必须接一个StandardScaler。和前一类特征选择方法对比一下Lasso属于监督式的特征选择它是带着标签去筛原始特征留下对分类有贡献的那几个维度。PCA则完全不看标签它做的是压缩而非筛选。当你手里特征数量本身不大、比如只有二三十维时用Lasso或直接对特征做重要性排序都是可选的但特征维度高、相关性又强的时候PCA是更省心的默认选择。顺带提一句网上搜PCA经常出来“特征脸”之类的图像降维例子那是把像素当特征语音情感里我们压的是统计特征原理一样但不要照着图像的例子直接套。2.3 为什么是SVM小样本分类器里的老伙计分类器为什么选SVM支持向量机而不是KNN或者随机森林情感分类的训练集通常只有几百到几千条样本SVM在这类场景下有一个理论优势它找的是最大间隔超平面泛化误差只由间隔和支持向量的稀疏程度决定与特征维度没有直接关系。这正好配合PCA降维后的空间——你不需要知道每个特征对分类的贡献只需要把样本映射到一个比较干净的低维空间SVM就会自己找支持向量。SVM原理上分硬间隔和软间隔。硬间隔要求训练样本严格线性可分真实情感数据根本做不到所以实际用的是软间隔版本允许一部分样本越过边界用hinge loss做惩罚。很多人看“硬间隔svm”的文章把它当成SVM的全部其实硬间隔只是软间隔里惩罚系数C为无穷大时的特例。另一个相关的疑惑是“svm的梯度下降”能不能跑SVM的损失函数确实可以拿梯度下降或SGD求解但sklearn里SVC默认用的是SMO优化效率更高、数值更稳——所以不用纠结梯度下降的实现直接用库就好。那为什么不是CNN这一类深度模型深度学习与SVM原理的核心区别在于特征从哪来。CNN用大量数据自动学特征数据不够时容易过拟合而SVM是拿人工设计的特征做边界划分。在一两个小时的语音数据上SVM翻车的概率远小于从零训一个小型CNN。如果哪天数据量膨胀到几万小时再考虑把这里的SVM替换成深度分类头。3. 从WAV文件到情感标签搭一条可复现的DWT-PCA-SVM分类流水线下面这套流程是完整的可运行方案我在多个语音情感数据集上按同样结构跑过。以16kHz采样率、3秒分段为例如果你手头是CASIA这类中文情感语料库或RAVDESS这类英文库只需要改读取路径和标签映射流程骨架不用动。3.1 数据准备先解决采样率不齐和样本长短不一很多公开语音数据集不是统一的16kHz有的是44.1kHz有的是24kHz。分类器要求所有样本特征维度一致所以第一步是把采样率规整到目标值这里统一到16kHz。同时处理两个隐藏问题立体声要合并成单声道16bit PCM读进来是int16幅度范围在-32768到32767不归一化的话数值量纲会直接拖垮后面的标准化。import os import numpy as np import scipy.io.wavfile as wavfile from scipy.signal import resample_poly from math import gcd def load_mono_16k(path, target_fs16000): fs, data wavfile.read(path) # 16bit PCM的int16先转成float32并归一化到[-1, 1] if data.dtype ! np.float64 and data.dtype ! np.float32: data data.astype(np.float32) / 32768.0 # 立体声取平均丢失的空间信息对情感分类影响不大 if data.ndim 1: data data.mean(axis1) # 采样率不齐就重采样up和down先约分防止倍数过大 if fs ! target_fs: g gcd(fs, target_fs) data resample_poly(data, target_fs // g, fs // g) return data.astype(np.float32)这段代码里resample_poly内部自带抗混叠滤波器比直接线性插值靠谱。重采样不是简单的拉伸它会先做低通滤波再抽点或插值避免高频镜像噪声混入。gcd约分的目的是让上采样和下采样系数尽量小既减少计算量也降低重采样造成的高频损失。有了统一格式的语音波形之后还要解决样本长短不一致的问题。常见做法是定长截断或滑窗分段。定长截断简单但会把情绪爆发点拦腰截断滑窗分段能保留更多上下文同时相当于做了数据增强。我一般取3秒窗口、1秒步长重叠2秒既覆盖了短时情绪变化的尺度又不会让帧数膨胀得太厉害。每条原始语音会切出好几段每段当成一条独立样本预测时再按段投票得到整条语音的标签。3.2 小波分解特征提取用 pywt.wavedec 逐层拆解特征提取是整个管线里最体现物理含义的一步。对每一段3秒语音做三层离散小波分解每一层系数都提四个统计量均值反映该频带的静态分量标准差反映波动幅度单位长度能量反映该频带的强度贡献过零率反映信号振荡的快慢。这四个量互相补充比只提能量一个特征稳得多。import pywt def wavelet_feature_vector(signal, waveletdb4, level3): # 返回列表顺序cA_n, cD_n, ..., cD_1 coeffs pywt.wavedec(signal, wavelet, levellevel) feats [] for c in coeffs: c np.asarray(c, dtypenp.float64) feats.append(np.mean(c)) # 本层系数均值 feats.append(np.std(c)) # 本层系数标准差 feats.append(np.sum(c * c) / len(c)) # 单位长度能量 zc np.sum(np.abs(np.diff(np.sign(c)))) / (2 * len(c)) feats.append(zc) # 过零率 # 补一个原始信号RMS保留总能量信息防止分解后能量被分散 feats.append(np.sqrt(np.mean(np.square(signal)))) return np.array(feats, dtypenp.float32)理解coeffs的顺序至关重要coeffs[0]是最低频近似系数承载语音的基频和低频轮廓越往后系数越细致coeffs[-1]是第一层细节系数对应4kHz到8kHz的高频分量。在情感分类里悲伤和中性样本的低频近似系数通常更平滑标准差偏小愤怒样本的高频细节系数能量明显偏高。判断代码有没有写对可以打印每层特征向量如果数值全部接近0往往是信号没有被归一化或者重采样时把值压到了极小范围。3.3 PCA降维先标准化再降维别让量纲抢戏特征提取完后每条样本有13维特征三层分解共12维加1维RMS看似不多但如果改了滑窗参数或者后面换成小波包分解特征维度会迅速上升。PCA在这里的作用是两方面的一是压缩冗余二是把特征去相关。要注意的是PCA必须接在标准化后面否则能量维度的数值会把其余特征全挤到后面。from sklearn.preprocessing import StandardScaler from sklearn.decomposition import PCA scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) pca PCA(n_components0.95, whitenTrue) X_train_pca pca.fit_transform(X_train_scaled) # 测试集只能transform不能fit否则等于把测试集分布泄露进训练 X_test_pca pca.transform(scaler.transform(X_test))这里n_components0.95的含义是保留95%的方差具体保留多少维由PCA自动判断。whitenTrue会把降维后的各个主成分再缩放到单位方差对SVM这类对特征尺度敏感的模型有正向作用。很多第一次做这个管线的人会把scaler和pca在全部数据上fit一遍再划分训练集这就造成了数据泄露十有八九会在真实场景翻车。上面的写法把fit和transform分开训练集fit测试集只用transform是最基本但最容易被忽略的规矩。如果你的代码里到目前为止还没有单独划分测试集建议现在补上。用一个分层采样划分按情感类别比例切分保证训练集和测试集里各类别占比一致。3.4 SVM训练与网格搜索怕漏调参数就交给GridSearchCVSVM最怕的不是数据量少而是参数没调。RBF核有两个关键参数C控制对误分类的惩罚力度gamma控制单个样本的影响半径。这两个参数哪个不匹配都会让结果崩掉所以实际训练时不要手动试直接把参数空间交给网格搜索。from sklearn.pipeline import Pipeline from sklearn.svm import SVC from sklearn.model_selection import GridSearchCV, StratifiedKFold pipeline Pipeline([ (scaler, StandardScaler()), (pca, PCA(n_components0.95)), (svm, SVC(kernelrbf, class_weightbalanced, probabilityTrue)) ]) param_grid { svm__C: [0.1, 1, 10, 100], svm__gamma: [0.001, 0.01, 0.1, 1], pca__n_components: [0.90, 0.95, 0.99] } cv StratifiedKFold(n_splits5, shuffleTrue, random_state42) grid GridSearchCV(pipeline, param_grid, cvcv, scoringf1_macro, n_jobs-1, refitTrue) grid.fit(X_train, y_train) print(grid.best_params_) print(classification_report(y_test, grid.predict(X_test)))把PCA放进Pipeline是刻意的写法交叉验证的每一折内部重新做标准化和PCA再从训练折里学参数这样网格搜索的过程也不会泄露测试信息。scoringf1_macro比默认的accuracy更稳因为情感数据里中性样本往往占大头只看准确率会让模型变成“全部预测中性”的复读机。class_weightbalanced让模型自动对少样本类别加权在愤怒样本只有中性样本一半的场景下这一行能直接挽回十几个点的召回率。如果数据量特别少比如总样本不足500条把param_grid里的C和gamma候选值缩减到两个同时把交叉验证的n_splits从5改成3否则每一折的训练集太小网格搜出来的参数方差很大。4. 避坑情感分类管线上最常翻车的五个点这一章的每一条都是真实跑数据时撞出来的按“现象 → 原因 → 解决”的顺序写。看完这部分再回头看第3章的代码你会发现那些写法都不是随意定的。4.1 小波层数选太高PCA一压全白做现象把分解层数从3调到6特征维度翻倍PCA降完维度之后准确率反而比3层时低了5个点。原因层数越高最底层的近似系数覆盖频带越窄几乎退化成一条缓慢变化的基线PCA按方差找主成分时会把大量方差分配给高频噪声细节而真正区分情感的中低频能量反而被压到后面。解决分解层数不要超过信号奈奎斯特频率的一半所能支撑的层数16kHz采样率下3到4层就够第5章会给出具体对照表。如果发现PCA的可解释方差曲线前几个维度异常平缓先怀疑小波层数选高了。4.2 样本不等长导致特征矩阵对不齐SVM直接报错现象训练时SVC.fit()抛异常提示输入样本数不一致或者特征矩阵里出现NaN。原因不同语音切出来的分段数不同直接拼成一个np.array时维度对不上。解决要么统一每条样本的窗口数和特征拼接方式要么在生成X矩阵之前用np.vstack强制对齐。更稳的做法是每条语音只保留固定数量的分段特征比如滑窗后取所有分段特征的平均值这样每条样本对应一个定长向量SVM只管盯着这个向量学。4.3 PCA放错位置造成数据泄露测试集指标虚高现象交叉验证时准确率95%把模型扔到新录音上立刻掉到60%。原因把StandardScaler和PCA在完整数据集上fit了一遍再去切测试集等于测试集的均值、方差、主成分方向全部泄露给了训练过程。解决严格用Pipeline或者手动保证scaler和pca只在训练集上fit。判断有没有这个问题的快捷方法是看训练和测试的准确率差差值超过10个点先查数据泄露再怪模型。4.4 类别不平衡让SVM变成“复读机”现象F1分数看着还行但翻混淆矩阵发现某一类召回率是0模型把所有样本都预测成多数类。原因情感数据集里中性样本往往占一半以上SVM的默认目标函数是最小化整体错误率把全部样本判给多数类就能拿低错误率。解决加class_weightbalanced是最直接的办法如果还不够考虑对少数类做SMOTE过采样或者干脆把混淆最严重的两个类别合并成一个“激活度”类。4.5 标签本身有噪声模型F1死活上不去现象网格搜索参数已经收敛特征提取也换了三种小波基F1始终卡在0.55上下。原因语音情感标注的主观性很强同一条音频有人听是悲伤有人听是平静数据集的硬标签本身就不一致。解决如果原始数据集提供多个标注者结果优先用多数投票后的标签只保留标注一致性高的样本做训练。另一个思路是放弃五分类改成回归预测效价和唤醒度两个连续值再用阈值映射回情感类别这条路径的稳定性往往更高。5. 参数怎么设才不玄学小波层数、PCA保留率与SVM的C、gamma调优参数是这套管线里看起来最像“玄学”的部分其实每个参数都有可推导的选择依据。下面给出一套可复制的参数设定逻辑照着这套逻辑去调比盲目试组合节省很多时间。5.1 小波基和小波层数怎么配对小波基的选择会影响频带分离的干净程度。db4是计算量和频带定位的均衡点大多数情感分类场景直接用db4就行db8频率分辨率更高但边界伪影更重sym8对称性好对语音这类非对称波形引入的相位失真更小。三种我都跑过准确率差距通常不超过2个点所以不要在这里花太多时间。小波基特点适用场景db4计算快频带重叠较小默认首选数据量大时优先db8频带隔离更干净滤波阶数高高频情感愤怒、惊讶占比高sym8近似对称边界失真小短样本分段多边界的截断影响较大分解层数取决于采样率。16kHz采样率对应奈奎斯特频率8kHz每分解一层低频近似系数的覆盖带宽减半。下表给出16kHz下的频带分布分解层数最终低频近似覆盖范围保留的主要信息10 - 4000 Hz全频带粗轮廓20 - 2000 Hz中低频段30 - 1000 Hz基频与低频共振峰40 - 500 Hz极低频段情感识别里基频和低频共振峰是悲伤、中性分类的关键所以3到4层比较合适。如果采样率是44.1kHz可以适当加一层因为奈奎斯特频率翻倍了如果采样率只有8kHz三层以内的层数就足够。一个实用的验证方法分别提取2、3、4层特征各跑一次SVM看验证集F1变化稳定后就不再往上加层。5.2 PCA保留率95%是起点不是标准答案PCA保留率在情感分类里常用三个值90%、95%、99%。90%丢的信息偏多适合特征本身噪声很大的场景95%是安全起手式99%几乎保留了全部信息但降维效果减弱噪声也可能一并保留。在第3章的代码里我把pca__n_components放进了网格搜索空间让交叉验证替你选。一个更细的实操办法是画出可解释方差曲线对标准化后的特征跑PCA看累计方差百分比随维度上升的曲线找到曲率从陡峭变平缓的拐点。拐点对应的维度就是“再加维度也没多少信息”的位置。如果你发现曲线到第3维已经超过95%说明前面提取的小波特征之间相关性太强可以适当减少每层统计量的个数如果第10维还没到90%说明特征提取过于分散PCA把噪声也当成方差保留下来回去检查小波层数和滑窗长度。5.3 SVM核函数与C、gamma怎么搜核函数的选择先看数据量。特征维度在20维以内、样本量几百条时RBF核通常是首选因为它能拟合任意形状的决策边界如果特征维度升到上百维线性核反而更稳。情感分类这套管线经PCA降维后大多落在10到30维RBF核没错。RBF核的参数对结果影响远大于核函数本身。C越大模型越不愿意误分类训练集上表现好但容易过拟合C越小决策边界越平滑。gamma越大每个样本的影响范围越小边界越弯曲gamma越小决策边界越接近线性。实用组合参考参数搜索范围出现最优值的常见区间C0.1, 1, 10, 1001 - 10gamma0.001, 0.01, 0.1, 10.01 - 0.1class_weightbalanced固定用balanced如果网格搜索结果显示最优gamma在边界值比如总是取到0.001或1说明搜索范围没覆盖到位把范围向对应方向扩展一层。不要迷信网格搜索给出来的最优解它只是在离散候选点里选最好的最优解出现在搜索边界时基本等于提示你调整范围。5.4 评估准确率会骗人要看混淆矩阵模型训练完第一个要看的不是准确率而是混淆矩阵。情感分类里愤怒和惊讶容易互相混悲伤和中性容易互相混这符合情感维度理论里“激活度”和“效价”两个轴的直觉——同一轴上相邻的情感本来就难分。如果混淆集中在某个特定类别上优先处理那一类的特征而不是调全局参数。按类别计算F1宏平均比准确率可靠得多。还有一个隐藏问题如果数据集的同一位说话人的多条语音同时出现在训练集和测试集模型学到的是说话人的音色而不是情感本身。测试集里换成新说话人准确率立刻跳水。这种情况很常见评估时必须按说话人分组划分数据具体做法在第6章末尾给出。6. 从DWT升级到小波包分解识别率还能再上一个台阶DWT只对低频部分反复分解高频细节从第二层开始就不再细分。对于愤怒、惊讶这类情感它们的信息大量集中在3kHz以上的高频细节里DWT在这几个频段上一锤子买卖细节粒度不够。小波包分解WPD的不同之处在于它对每一个子带都继续往下拆高频部分也能获得多分辨率的描述。6.1 用小波包分解替换DWT的代码改法PyWavelets里对应的接口是WaveletPacket核心改法如下from pywt import WaveletPacket def wp_feature_vector(signal, waveletdb4, maxlevel3): wp WaveletPacket(signal, wavelet, maxlevelmaxlevel) nodes wp.get_level(maxlevel, natural) # 按频带顺序取所有叶子节点 feats [] for node in nodes: c np.asarray(node.data, dtypenp.float64) feats.append(np.std(c)) feats.append(np.sum(c * c) / len(c)) return np.array(feats, dtypenp.float32)这段代码和DWT版本最大的区别是特征量翻了两倍多3层WPD有8个叶子节点每个节点提两个统计量加上原始RMS总共17维4层就是33维。替换后PCA的作用会更加明显因为相邻子带的能量天然相关PCA正好把这些相关分量合并。我实际对比过在愤怒和惊讶样本占比高的数据集上WPD比DWT的宏平均F1能高出3到5个点但中性占比高时提升很小所以不必无脑升级。6.2 留一说话人验证判断模型是否在偷懒最后一个技巧也是我踩过最大的坑验证模型是否真的在学情感而不是在记说话人。做法是按说话人分组做交叉验证每个说话人单独留作测试集其余全部说话人做训练。如果成绩比随机划分低了10个点以上说明模型大量依赖音色线索。from sklearn.model_selection import LeaveOneGroupOut, cross_val_score groups speaker_ids # 每条样本对应的说话人编号 logo LeaveOneGroupOut() scores cross_val_score(grid.best_estimator_, X_train, y_train, groupsgroups, cvlogo, scoringf1_macro) print(scores.mean())这个验证方式同样适用于深度学习模型。我最早做这套管线时没按说话人划分随机划分下F1到了0.82换成留一说话人验证直接掉到0.61。那之后我把说话人分组当成和数据清洗同等重要的固定步骤每次训练前先看一眼这个数。希望帮到你。本文还有配套的精品资源点击获取