简介:面向情感分类与机器学习初学者,这是一套基于小波分解、主成分分析与支持向量机(SVM)的完整MATLAB实现方案,可帮助快速掌握多尺度特征提取、降维与分类器协同建模流程。压缩包共含598个文件,以444个mat数据文件、79个m脚本为核心,另有libsvm相关c/java源文件、mexw32/mexw64编译组件、python辅助脚本及pdf说明文档,便于在不同环境中复现实验,整体包体约5.81MB。目前已有251人学习,资源提供svmtrain、svmpredict等成熟SVM接口,还包含粒子群优化SVM参数的psoSVMcgForClass.m、特征提取tezheng_tiqu.m等自定义函数,可支撑从数据预处理到模型评估的完整链路。基于小波与PCA的联合特征工程能有效提升分类精度,实测识别准确率达到90%以上,对研究降噪特征表达与核函数调参的读者具有较高参考价值。
1. 为什么情感分类要同时搬出小波分解、PCA和SVM
情感分类在真实业务里远没有公开数据集上那样体面:一条在线评论可能只有十几个字,一段语音带着环境噪声,一组生理信号里混着基线漂移。特征在时间轴上被拉得很长,维度高、冗余重、类别边界还经常非线性。小波分解、PCA和SVM这条流水线之所以常被放在一起,恰好在意的不是你熟悉哪个模型,而是每一步都对应一个具体的工程设问:拿什么描述信号中的情绪起伏,用什么把上百个特征压成十几个不丢失有效信息的向量,再交给哪个分类器才能扛住高维稀疏和线性不可分。
这套组合在文本情感分类、语音情感识别、脑电与心率变异性情感标注三类任务里都能用,只不过数据形态不同。小波分解负责在时间-频率两个尺度上同时截获特征,比如文本里通过词向量序列找短语级情绪波动,语音里通过小波系数看基频突变与能量衰减,PCA负责把分解后大量存在共线性的特征投影到方差最大的方向上,SVM则利用核函数在高维空间里切分隔断。适合的读者不只是要做实验的算法工程师,还包括那些手上已有标注数据、被“效果卡在 72% 上不去”困扰的工程团队——多数问题并不出在模型,而是前端特征的尺度混乱和后端核函数选择不当。
2. 小波分解第一站:用多尺度特征描述情感波动
2.1 为什么短时傅里叶变换扛不动情感信号
短时傅里叶变换给信号加一个固定长度的窗,窗一短则频率分辨率差,窗一长则时间分辨率差,两个矛盾在情感这种持续时间不稳定的信号上暴露得尤其明显。人的情感表达时长从 200 毫秒到 3 秒不等,愤怒时的语音短促高频,悲伤时语调拖长低频占主导,固定窗函数无法同时照顾这两类片段。小波分解的关键不同在于它对低频段用宽窗、对高频段用窄窗,这从数学上对应一组可缩放可平移的基函数,即母小波的伸缩与移位。分解后得到的近似系数承载低频轮廓,细节系数承载不同尺度的高频扰动,情感信号中的瞬时情绪突变因此可以被单独切出来观察。
import pywt import numpy as np # 以语音信号的某帧为例,采样率 16000,帧长 400ms fs = 16000 t = np.linspace(0, 0.4, int(fs * 0.4), endpoint=False) clean_signal = (np.sin(2 * np.pi * 220 * t) + 0.6 * np.cos(2 * np.pi * 1300 * t)) noise = 0.15 * np.sin(2 * np.pi * 60 * t) signal = clean_signal + noise # db4 小波,4 层分解 coeffs = pywt.wavedec(signal, wavelet='db4', level=4) cA4, cD4, cD3, cD2, cD1 = coeffs print(f"近似系数长度: {len(cA4)}, 各细节系数长度: {len(cD4)}, {len(cD3)}")pywt.wavedec返回的近似系数和一组细节系数,长度逐层减半,这是二抽取(downsampling)操作带来的。实际提取特征时不会直接拿所有系数当输入,因为近似系数 cA4 仍保留较多能量,细节系数在高频噪声较多的数据里往往占主导,所以后面进入 PCA 之前通常要混合统计量,把一个尺度上的数百个系数压成均值、标准差、能量占比和过零率这几个标量。
2.2 小波基选择和分解层数是第一个坑
小波基不是随便挑一个就完事的。dbN 小波族是离散小波变换里应用最广的,N 越大滤波器越长,频域局部性越好,但计算量增加,实时性要求高的任务里首选 db4 或 sym4。sym 小波比 db 对称性好,处理语音或脑电这类对相位敏感的生理信号时失真更小,而 haar 小波虽然只有一阶消失矩,对突变最敏感,但平滑性差,用在文本特征序列上可能更合适。选择方法不靠感官,我一般采用两层交叉验证:先固定分解层数 4 和 SVM 的默认参数,把 db2、db4、sym4、coif3 各跑一遍,比较验证集上的 F1 值,差异不足 0.5 个点时选计算更快的。
层数设置同样需要小心。层数偏低,低频近似系数里仍残留噪声,分类器容易被无关成分带偏;层数偏高,最低层的近似系数被压成几个点,信息损失不可逆。常规做法是先看信号的主频区间,令分解层数满足“最高层近似系数的中心频率接近情绪特征的主频”,拿语音情感举例,若基频在 150Hz~300Hz,采样率 16000,则层数选 4 或 5 恰好让最低层的近似频带落在 500Hz 以下,噪声和清音中的高频毛刺被丢进细节系数,不参与后续 PCA。
# 计算每层细节系数能量占比,辅助决定保留哪几层特征 energy_ratio = [] total_energy = sum(np.sum(c**2) for c in coeffs[1:]) for d in coeffs[1:]: energy_ratio.append(np.sum(d**2) / total_energy) for i, ratio in enumerate(energy_ratio, start=1): print(f"D{i} 能量占比: {ratio:.4f}")如果 D1 的能量占比超过 0.8,说明信号里遍布高频噪声,层数设置没有问题,但需要先对原信号做去噪;如果 D4 和 D3 几乎为零,说明主能量都压在近似系数上,情绪差异不体现在细节层,此时再上 SVM 反而会被噪声层干扰,建议只保留 cA4 和 cD3、cD4。
3. PCA 主成分分析在情感特征压缩里的正确打开方式
3.1 方差从哪个维度压,主成分就往哪边转
小波分解多尺度铺开后,特征空间维度很容易膨胀。语音情感里如果对 4 层系数各取 5 个统计量,特征维度就到了几十维,再叠加上韵律特征(基频、短时能量、共振峰)和频谱特征(MFCC),几百维都很常见。这些特征彼此并不独立,基频均值与基频标准差天然相关,小波细节系数的能量与短时能量也存在共线性。PCA 的核心动作正是找一组新的正交基,使得数据在这组基上的投影方差依次递减,第一主成分对应方差最大的方向,也就是区分样本最剧烈的那个方向。
from sklearn.decomposition import PCA from sklearn.preprocessing import StandardScaler # feature_matrix: shape (n_samples, n_features),由小波统计量和其他特征拼接而成 scaler = StandardScaler() X_scaled = scaler.fit_transform(feature_matrix) pca = PCA(n_components=0.95) # 保留 95% 方差 X_pca = pca.fit_transform(X_scaled) print(f"原始特征维数: {feature_matrix.shape[1]}") print(f"降维后维数: {X_pca.shape[1]}") print(f"各主成分方差解释率: {pca.explained_variance_ratio_}")先标准化再 PCA 是前提顺序,不能倒过来。否则方差大的特征天然主导第一主成分,计算出的载荷系数只反映量纲大小,不是真实信息贡献。选n_components=0.95这种按解释方差总比例截断的方式,比直接写死n_components=10更合理,因为不同情感数据集的固有维度差异很大,固定取值容易在小样本上过降维、在大样本上欠降维。
3.2 主成分的个数和情感类别数之间没有直接公式
常见误区是认为主成分个数应等于情感类别数,比如三分类就有三个主成分。这个说法没有理论根据,PCA 不感知标签,它只看特征方差。类别数少,不代表类内差异小,性别差异、说话人差异、录音环境差异带来的特征方差可能远超情绪差异,这时候取三个主成分等于强行丢掉真实区分情感方向的信息。
正确做法是画累计方差解释率曲线,找拐点。拐点定义是增加一个主成分带来的解释率增量显著变小的位置。工程上通常取累计解释率达到 85%~95% 区间内的最小 k 值,同时确保 k 不超过样本数的五分之一,否则后续 SVM 训练会面临维数灾难的残余风险。若 PCA 前的特征维度只有 20 维,经验上降维到 5~8 维就足够;若原始特征 300 维,降到 25~40 维比较常见。
import matplotlib.pyplot as plt pca_full = PCA().fit(X_scaled) cumsum = np.cumsum(pca_full.explained_variance_ratio_) k = min(range(1, len(cumsum) + 1), key=lambda i: abs(cumsum[i-1] - 0.90)) + 1 plt.plot(range(1, len(cumsum) + 1), cumsum, marker='o') plt.xlabel('主成分个数') plt.ylabel('累计解释方差比例') plt.axvline(x=k, color='red', linestyle='--', label=f'90% 阈值: k={k}') plt.legend()如果这个图中前两个主成分的解释率之和不到 40%,说明数据中情绪信息分散在多个弱相关维度上,靠 PCA 很难聚拢。这时候不要一味加大 k,而要考虑回上游检查小波特征是否提取到位,或者改用核 PCA 对非线性结构而非线性方差做投影。核 PCA 的缺点是降维后的向量无法直接解释主成分方向,所以通常在 SVM 精度提升不超过 1 个点时仍推荐线性 PCA,便于定位具体是哪个原始特征贡献了情感判别力。
3.3 载荷矩阵读法与业务解释
PCA 降维之后要回看载荷矩阵,确认压缩不是瞎压。载荷矩阵的每一列代表一个主成分,数值表明各个原始特征在这个主成分中的权重。语料上做情感分类时,如果第一主成分中权重最高的是小波细节系数的标准差和短时能量,说明情感强度主要由高频扰动和整体能量变化刻画,这与愤怒、兴奋类的情感特性一致;如果权重靠前的是近似系数的均值,则说明情感状态偏向语气平坦的悲伤与平静。
loadings = pd.DataFrame( pca.components_[:k].T, columns=[f'PC{i+1}' for i in range(k)], index=feature_names ) print(loadings.iloc[:10, :3])实际项目中我会把载荷矩阵做成热力图,列是主成分,行是特征名,颜色代表权重正负。一个经验法则:如果某个主成分的所有载荷都在同一方向上且绝对值接近,那它就是总体强度因子,没有类别区分能力,应该在后续筛选时弃用,转而保留载荷正负分化明显的主成分,后者才对应情绪类别间的对抗性差异。
4. SVM 如何在情感分类里完成最后的判决
4.1 不同核函数对应不同情感边界的形状假设
PCA 降维后的特征向量不是天然线性可分的主成分空间,更多情况下两类情感的投影区域相互纠缠,比如“惊喜”和“恐惧”在生理信号特征空间中共享高唤醒度区域,但效价一正一负。线性核在这里容易欠拟合,RBF 核是应用最广的选择,原因是它只有一个 gamma 参数需要调节,就能模拟出足够复杂的决策边界。
from sklearn.svm import SVC from sklearn.model_selection import cross_val_score, StratifiedKFold svm_linear = SVC(kernel='linear', C=1.0, class_weight='balanced') svm_rbf = SVC(kernel='rbf', gamma=0.1, C=1.0, class_weight='balanced') cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) scores_linear = cross_val_score(svm_linear, X_pca, y, cv=cv, scoring='f1_macro') scores_rbf = cross_val_score(svm_rbf, X_pca, y, cv=cv, scoring='f1_macro') print(f"Linear F1: {scores_linear.mean():.4f} (+/- {scores_linear.std():.4f})") print(f"RBF F1: {scores_rbf.mean():.4f} (+/- {scores_rbf.std():.4f})")class_weight='balanced'是情感分类中必须开的参数,公开数据集里的样本分布常常失衡,“中性”样本可能是“愤怒”样本的三倍。SVM 对不平衡数据本身敏感,不设权重时支持向量会全部偏向多数类,少数类边界被压缩得几乎不存在。scoring='f1_macro'比accuracy更严谨,宏平均 F1 能保证每个类别被平等对待,不会出现多数类 95% 准确率、少数类直接全错的假象。
4.2 SMO 与支持向量的关系不需要你手写,但要理解
SVM 的损失函数是合页损失加上 L2 正则项,求解是一个二次规划问题,工程实现普遍使用序列最小优化算法,把大问题拆成两个拉格朗日乘子的小块来迭代求解。scikit-learn 的SVC在 LIBSVM 封装下默认使用 SMO 的改进版本,所以多数场景不需要自己实现。但理解支持向量的含义仍然必要:最终决策函数只依赖落在边界上的那些样本,而 PCA 压缩会影响哪些点落在边界上。如果降维后的数据状态下大量样本都成了支持向量,说明数据本身噪声大或核函数不匹配,此时加入对新样本的预测会显著变慢。
svm_rbf.fit(X_pca, y) # 检查支持向量占比,判断决策边界是否过于复杂 sv_ratio = len(svm_rbf.support_vectors_) / len(X_pca) print(f"支持向量占比: {sv_ratio:.2%}")占比超过 40% 时,我一般先尝试降低gamma值,让每个样本只影响近距离区域,整体决策边界变得平滑;如果占比反而继续增大,再回头增加 PCA 的累计方差阈值,从特征端减少干扰。还有一种做法是换用LinearSVC走 liblinear,它优化的是不同的损失形式,拟合速度快一个数量级,适合上万级样本量的情感语料,但效果上通常要差 1~2 个 F1 点,是否值得用取决于线上推理时延预算。
4.3 C 和 gamma 的取值交互是网格搜索的最优范围
C 控制误分类惩罚力度,gamma 控制单个样本的影响半径。C 过大时边界复杂,容易把噪声也当成分界线;C 过小时欠拟合,在情感分类中尤其容易把愤怒和惊喜合并成同一个高唤醒类。gamma 过大时模型退化成只看最近邻样本的类别,等于失去 SVM 的泛化能力;gamma 过小时所有样本的核函数值都趋近相似,模型近似线性。
from sklearn.model_selection import GridSearchCV param_grid = { 'C': [0.1, 1, 10, 50], 'gamma': [0.001, 0.01, 0.05, 0.1, 0.5] } svm_search = SVC(kernel='rbf', class_weight='balanced') grid = GridSearchCV(svm_search, param_grid, cv=cv, scoring='f1_macro', n_jobs=-1) grid.fit(X_pca, y) print(f"最优参数: {grid.best_params_}") print(f"最优F1: {grid.best_score_:.4f}")注意这里gamma的搜索范围起点要低且跨度要密,常见教训是把网格设在[0.001, 0.01, 0.1, 1]后最优点落在 0.001 边界上,这表示搜索范围没有覆盖到合理区域,应该继续向小方向扩大而非认为 0.001 就是最优。实际项目中我会把C和gamma一起放进对数尺度网格,先用粗网格确定数量级,再在最优值附近加密跑一轮细网格,以免粗网格跳过尖峰点。
5. 从特征到分类器的端到端实验流程,直接照搬可跑通
5.1 数据组织与文件读取
准备好一批带情感标签的文本短句或语音样本,按 6:2:2 划分为训练、验证和测试集,划分时使用分层采样保证每个情感类别在三份中的分布一致。语音情感数据集常见的有 EMO-DB、RAVDESS 等公开数据,文本情感则常用中文酒店评论语料和英文 IMDB 情感子集。数据规模上,情感分类实验最怕样本过少导致 SVM 参数不可信,每个类别至少准备 150 条以上。
import os import numpy as np import pandas as pd from sklearn.model_selection import train_test_split samples = [] labels = [] for emotion_dir in ['angry', 'happy', 'neutral']: for fname in os.listdir(emotion_dir): audio_path = os.path.join(emotion_dir, fname) signal, sr = librosa.load(audio_path, sr=16000) samples.append(signal) labels.append(emotion_dir) X_train, X_temp, y_train, y_temp = train_test_split( samples, labels, test_size=0.4, stratify=labels, random_state=42) X_val, X_test, y_val, y_test = train_test_split( X_temp, y_temp, test_size=0.5, stratify=y_temp, random_state=42)train_test_split两次划分比一次性三划分更灵活,验证集在参数搜索阶段用,测试集只登场一次。如果数据是文本分类语料而非音频,则可以先对每篇文本做分词,用预训练词向量或者自训练的 Word2Vec 把句子映射为句向量,再把这个句向量序列当作信号送入小波分解。
5.2 特征拼接、PCA 压缩与 SVM 分类器的流水线封装
下面用Pipeline把标准化、PCA、SVM 串在一起,避免训练集和测试集分别做标准化引入数据泄漏。
from sklearn.pipeline import Pipeline # X_features: 原始拼接特征,shape (n_samples, n_features) X_features_train = extract_wavelet_features(X_train) X_features_val = extract_wavelet_features(X_val) X_features_test = extract_wavelet_features(X_test) pipeline = Pipeline([ ('scaler', StandardScaler()), ('pca', PCA(n_components=0.95)), ('svm', SVC(kernel='rbf', class_weight='balanced')) ]) pipeline.fit(X_features_train, y_train) val_acc = pipeline.score(X_features_val, y_val) print(f"验证集准确率: {val_acc:.4f}")Pipeline带来一个直接的好处:网格搜索时pca和svm的参数可以一起搜索,包括pca__n_components与svm__gamma之间的搭配。实践中确实存在“PCA 保留 99% 方差配上较小的 gamma 值”好于“95% 方差配上大 gamma”的情况,因为它们本来就是一前一后的联动模块,分阶段调优会错过联合最优。
5.3 模型评估指标选择哪种更稳
情感分类在多数论文里报告准确率,但工程上线更看重各类别的召回率和平均 F1。如果业务需求是避免把“极度负面”的评论误判为中性导致客诉升级,就要关注负面情感类别的召回率。用classification_report可以快速看到每个情感类别的精确率、召回率和 F1 值。
from sklearn.metrics import classification_report, confusion_matrix y_pred = pipeline.predict(X_features_test) print(classification_report(y_test, y_pred, digits=4)) cm = confusion_matrix(y_test, y_pred) print(cm)混淆矩阵里最容易出现的情况是“平静”与“悲伤”相互混淆,以及“愤怒”与“恐惧”混淆。前者在语音特征上低频能量相似,后者在高唤醒维度上相似。解决方向不是继续调 SVM 参数,而是回到小波分解步,检查是否应该单独增强某一频段的细节系数权重,或者在 PCA 之后做特征选择剔除制造混淆的噪声主成分。
6. 最终落地的三个参数细节:让你的小波 PCA SVM 管线从能用变好用
第一个细节是特征顺序对 PCA 载荷的影响。特征矩阵的各维顺序不影响 PCA 的数学结果,但特征命名和排列会影响你读取载荷矩阵时的直觉判断。建议把同源的统计量排列在一起,即第一段是小波近似系数统计量,第二段是小波细节系数统计量,第三段是韵律或频谱特征,这样载荷矩阵转成热力图后,区块结构一眼可见。
第二是SVC的概率输出问题。默认情况下SVC.predict_proba不可用,需要设置probability=True才会启用 Platt 缩放,但这会额外增加一次交叉验证计算量,训练速度明显变慢。若业务只需要类别结果、不需要置信度,就不开这个参数,直接用decision_function的数值符号做判断,效率更高。若确实需要概率输出配合真阳率调节,那么在网格搜索中也要开启probability=True,因为 Platt 缩放的引入本身会轻微改变最终分类边界。
第三个细节是小波分解系数的截断方式对最终精度的影响。我习惯将分解得到的细节系数做绝对值截断,即绝对值小于中位数的一定倍数时置零,然后再计算统计量。这相当于在小波域做了软阈值去噪,减少高频噪声对统计特征均值的拉动,同时保留情绪突变对应的明显尖峰。
def threshold_details(coeffs, factor=1.5): thresholded = [] for c in coeffs[1:]: # 跳过近似系数,或对近似系数保留原样 median_abs = np.median(np.abs(c)) thr = factor * median_abs c_thr = np.where(np.abs(c) < thr, 0, c) thresholded.append(c_thr) return coeffs[:1] + thresholded把阈值化后的细节系数放进特征提取内部替代原始细节系数,通常能带来 1~3 个百分点的 F1 提升,具体幅度取决于录音环境的噪声度。最后建议把最优参数配置、特征提取版本号、数据划分随机种子全部记录在实验配置文件中,避免参数调完之后无法复现线上结果。情感分类模型的天花板不只在分类器里,小波分解的尺度选择与 PCA 的方差阈值,对你最终结果的影响通常比 SVM 的边际调参要大得多,值得多花时间打磨。
本文还有配套的精品资源,点击获取