YAOTU INSIGHTS

基于Matlab的表情识别源码:LBP+LPQ特征与SVM/PCA分类实现

基于Matlab的表情识别源码:LBP+LPQ特征与SVM/PCA分类实现
简介一份基于Matlab的表情识别源码工程采用LBP局部二值模式与LPQ局部相位量化提取人脸纹理和相位特征结合PCA降维与SVM分类器在Jaffe人脸库上完成快乐、悲伤、愤怒等静态表情的分类测试。工程共包含235个文件其中213个tiff格式的人脸样本图像用于训练与测试18个m文件为实现特征提取、降维、分类及主程序的Matlab代码另有少量txt说明文件整体压缩包仅2.66MB结构紧凑、便于本地复现。已有1200人学习下载适合计算机视觉初学者及研究者作为表情识别入门与算法对比的参考实现。通过阅读和运行这套代码可完整理解从预处理、特征提取到SVM训练与评估的流程掌握LBP/LPQ对纹理与边缘的刻画方式、PCA降维对高维特征的处理作用以及SVM分类器在表情识别中的应用便于在此基础上扩展自己的改进思路。1. 表情识别源码的选型逻辑为什么是 Matlab LBP/LPQ SVM/PCA拿到“表情识别 源码Matlab LBPLPQ SVM PCA”这个标题第一反应是这是一条非常典型的传统机器学习技术路线而不是现在铺天盖地的深度学习方案。放在几年前表情识别的主流做法就是手工特征 经典分类器LBP 和 LPQ 负责从人脸图像里把纹理和局部结构信息抠出来PCA 负责把高维特征压一压、去掉冗余最后 SVM 做分类。这套组合在实验室环境、标准数据集上能跑出相当不错的精度而且对硬件要求极低训练时间以分钟计——这一点是 CNN 路线目前很难比的。这个标题适合谁适合正在做课程设计、毕业设计或者刚接触模式识别、想在 Matlab 里快速搭一套可解释、可修改、能跑的完整系统的开发者。它不像深度学习那样需要 GPU、需要调几个星期的网络结构而是每一步都有明确的数学含义和可视化中间结果。你拿到源码后能清楚看到“特征是什么”“为什么这样降维”“分类器在做什么”这种透明感是黑匣子式的神经网络给不了的。本文会把这五件事——LBP、LPQ、PCA、SVM、Matlab 工程结构——串成一条可复现的流水线并把最容易翻车的细节单独拉出来讲。2. 特征提取LBP 和 LPQ 到底在提取什么以及为什么两个都要2.1 LBP 的原始形式与统一模式从 3x3 邻域到 59 维直方图LBP局部二值模式做的事情很朴素对每个像素取它周围 3x3 邻域里的 8 个邻居跟中心像素比大小。比中心大记 1比中心小记 0然后按固定顺序把这 8 个二进制位拼成一个 0 到 255 之间的整数这个整数就是该像素的 LBP 编码。统计整张图所有像素的编码直方图就得到了这图像的特征向量。但直接用 256 个 bin 的直方图有两个问题维度偏高而且对噪声敏感。所以实践中几乎都用“统一模式”Uniform Pattern。所谓统一模式是指二进制串中 0/1 跳变的次数不超过 2 次。比如 00000000 跳变 0 次01110000 跳变 2 次都是统一模式而 01010101 跳变了 7 次不算。统一模式在自然图像里占了绝大多数约 90% 以上因此可以把 256 种模式压缩成 59 种58 种统一模式 1 种“其他”归类计算量降下来特征更鲁棒。在 Matlab 里如果不依赖第三方工具包手写一个 LBP 提取函数非常直接。关键点在于把图像转成 double 类型以及处理边界像素——常见做法是直接丢弃最外圈。下面是一个可用的实现输入灰度图输出统一模式 LBP 直方图。function hist lbp_uniform(img) img im2double(img); [h, w] size(img); % 映射表256种原始LBP编码 - 0~5859表示非统一模式 mapping get_uniform_mapping(8); lbp_img zeros(h-2, w-2); for i 2:h-1 for j 2:w-1 center img(i, j); code 0; % 按顺时针顺序取8个邻居 neighbors [ img(i-1, j-1), img(i-1, j), img(i-1, j1), ... img(i, j1), img(i1, j1), img(i1, j), ... img(i1, j-1), img(i, j-1) ]; for k 1:8 if neighbors(k) center code code 2^(k-1); end end lbp_img(i-1, j-1) mapping(code 1); end end % 统计直方图固定59个bin hist histcounts(lbp_img(:), 0:59) / numel(lbp_img); end function mapping get_uniform_mapping(num_bits) mapping zeros(1, 256); for code 0:255 bin_str dec2bin(code, num_bits) - 0; transitions sum(abs(diff([bin_str, bin_str(1)]))); if transitions 2 % 为每种统一模式分配连续编号 mapping(code 1) sum(mapping 0) 1; else mapping(code 1) 59; % 非统一模式统一归入第59类 end end end这段代码的逻辑核心是两层循环逐像素计算 LBP 编码再用get_uniform_mapping生成的映射表把 256 种原始模式归并到 59 个 bin。需要注意mapping(code 1)的索引偏移Matlab 数组从 1 开始而 LBP 编码值范围是 0~255。histcounts的区间设置是0:59表示把整数 0~58 各分一个 bin共 59 个最后除以像素总数做归一化避免图像尺寸不同导致特征量纲不一致。如果你用的是别人封装好的 LBP 函数注意确认返回的到底是 59 维还是 256 维这对接下来的 PCA 和 SVM 输入维度影响很大。2.2 LPQ 的频域原理为什么相位信息在光照变化下更稳LBP 对光照变化有一定鲁棒性因为它是中心像素与邻居的比较属于相对关系不是绝对灰度。但遇到光照梯度剧烈变化、或者图像模糊时LBP 的编码会变得很不稳定。LPQ局部相位量化走的是另一条路——在频域里做文章。LPQ 的核心思想是对每个像素的局部邻域做短时傅里叶变换STFT取变换后的相位信息再对四个特定频率点的相位进行二值量化。具体来说对图像中每个像素的 MxM 邻域计算在四个频点水平、垂直、两个对角方向上的傅里叶系数每个系数有实部和虚部分别与 0 比较得到 8 个二进制位组合成一个 0~255 的 LPQ 编码。最后同样统计直方图作为特征。因为相位对光照变化表现为灰度值的乘性或加性变化不敏感所以 LPQ 在光照不均的人脸图像上往往比 LBP 更稳。Matlab 官方没有直接封装 LPQ但实现它并不复杂。核心是用filter2做卷积近似 STFT或者直接构造傅里叶基向量与邻域块做内积。下面是一个简洁的 LPQ 特征提取函数它用了 3x3 邻域和 4 个频点function hist lpq(img, win_size) img im2double(img); [h, w] size(img); % 构造频点对应的卷积核实部虚部 freq_points [1, 0; 0, 1; 1, 1; 1, -1]; % (u, v) 归一化频点 kernels zeros(win_size, win_size, 8); for k 1:4 u freq_points(k, 1); v freq_points(k, 2); for x -floor(win_size/2):floor(win_size/2) for y -floor(win_size/2):floor(win_size/2) val exp(-2i * pi * (u * x v * y) / win_size); kernels(x floor(win_size/2) 1, y floor(win_size/2) 1, k) real(val); kernels(x floor(win_size/2) 1, y floor(win_size/2) 1, k4) imag(val); end end end % 对图像做卷积得到8个响应图 lpq_encoded zeros(h, w); for k 1:8 resp filter2(kernels(:,:,k), img, same); lpq_encoded lpq_encoded (resp 0) * 2^(k-1); end % 边界裁掉 b floor(win_size/2); lpq_encoded lpq_encoded(b1:end-b, b1:end-b); hist histcounts(lpq_encoded(:), 0:256) / numel(lpq_encoded); end这里的freq_points选择决定了四组傅里叶基的方向常见取法是 DC 附近的四个正交频点保证信息不重叠。filter2的same参数让输出与输入同尺寸但边界处卷积不完整所以后面要裁掉floor(win_size/2)的行列。注意一个容易忽略的坑lpq_encoded的累加顺序要和histcounts的 bin 数对齐这里用0:256表示 256 个 bin0~255 各一个不要和 LBP 的0:59混用否则后面拼接特征向量时长度对不上SVM 直接报维度错误。2.3 特征拼接策略串联还是并联维度爆炸怎么处理LBP 和 LPQ 各有优势LBP 擅长描述局部对比结构LPQ 对光照和模糊更鲁棒。把它们结合起来常见做法有两种。第一种是“串联”也就是把 LBP 的 59 维直方图和 LPQ 的 256 维直方图首尾相接拼成一个 315 维的向量后面统一过 PCA。第二种是“并联”分别用 LBP 和 LPQ 训练两个 SVM再用投票或堆叠确定最终结果。对大多数表情识别任务串联更简单而且因为后面有 PCA 降维维度高一点不是大问题所以我推荐先用串联跑通再回头试并联对比精度。不过串联时有一个细节要格外注意两个直方图的归一化方式必须一致。LBP 用了除以像素总数LPQ 同样用了这没问题。但如果一个用 L1 范数、另一个用 L2 范数拼接之后 PCA 的主成分方向会被量纲更大的那个特征主导降维结果就偏了。更稳妥的做法是先分别 z-score 标准化再拼接或者等 PCA 对拼接后的整体特征统一做标准化。我在实际项目里习惯先拼接再做标准化这样可以消除不同特征维度之间的尺度差异SVM 训练时收敛也更快。这种手工特征组合的思路本质上是让不同类型的纹理信息互补。LBP 抓的是“微观对比”LPQ 抓的是“频域相位结构”两者共同作用时对表情这种局部变化大、全局又有结构的图像描述能力比单用任何一个都强。这也是为什么标题里同时出现了 LBP 和 LPQ而不是只挑一个——它们的组合带来的精度提升通常有 3~5 个百分点在传统方法里这个收益已经非常可观了。3. 降维与分类PCA 怎么选主成分SVM 用哪种核函数3.1 PCA 在表情特征上的作用不只是降维更是去噪和防过拟合拿到拼接后的 315 维特征向量直接丢给 SVM 不是不行但效果往往不好。原因有两个一是样本量不够如果你的训练集只有每类几百张图315 维特征很容易过拟合二是 LBP 和 LPQ 的直方图里有大量冗余信息相邻 bin 之间相关性很高这些冗余对分类器来说就是噪声。PCA 的作用就是把原始特征投影到一组正交的主成分方向上保留方差最大的前 K 个方向丢掉方差小的维度。但 PCA 的一个关键认知是它不保证留下的方向对分类最有利。PCA 是无监督方法它只看方差不管类别标签。所以有时会出现这种情况——保留前 50 个主成分精度反而不如保留前 30 个因为第 31~50 个主成分里可能含有区分表情的关键信息但方差不够大被排在了后面。解决办法是交叉验证在训练集上分别试 K 20, 30, 50, 80, 100看验证集精度选最优 K。不要凭经验拍脑袋定一个数。在 Matlab 里PCA 可以用pca函数返回主成分系数和投影后的得分。需要注意pca默认对数据做中心化即减去每列的均值这是对的。但pca不做标准化即不除以标准差如果你的特征维度之间量纲差异很大需要先手动标准化。下面是完整的 PCA 降维流程% train_features: N x 315 矩阵每行一个样本 % train_labels: N x 1 标签向量 % 1. 标准化让每个维度均值0方差1 mu mean(train_features); sigma std(train_features); sigma(sigma 0) 1; % 防止零方差维度除零 train_features_std (train_features - mu) ./ sigma; % 2. PCA 分解得到系数和得分 [coeff, score, ~, ~, explained] pca(train_features_std); % 3. 看累计方差贡献率决定K cum_var cumsum(explained); K find(cum_var 95, 1); % 取累计方差到95%的维度数 % 4. 投影训练集和测试集 train_pca score(:, 1:K); test_features_std (test_features - mu) ./ sigma; % 注意用训练集的mu和sigma test_pca test_features_std * coeff(:, 1:K);这里最关键的一步是第 4 步测试集的标准化和投影必须复用训练集的mu、sigma和coeff绝不能自己重新算。很多新手在跑交叉验证时精度看起来很高一到测试就崩就是因为对测试集单独做了 PCA等于是用测试集自身的信息去拟合测试集属于典型的信息泄漏。explained变量返回的是每个主成分的方差百分比累加到 95% 对应的 K 一般落在 40~80 之间具体看你的特征冗余程度。如果 K 超过 150说明特征本身区分度不够应该回看特征提取环节而不是继续加维度。3.2 线性 SVM 还是 RBF 核小样本场景下的选择依据PCA 降维之后特征维度在 50 左右样本量可能只有几百到一千这种场景下 SVM 是非常合适的选择。但 SVM 的核函数选择直接影响精度。线性核训练快、结果稳定、可解释性强适合特征维度较高且样本量不大的情况。RBF 核高斯核可以拟合非线性边界但多了两个超参数C惩罚系数和 gamma核宽度需要调参。我的经验是如果 PCA 后特征维度在 100 以上优先用线性核——因为高维空间里线性边界往往已经够用如果降维后维度很低低于 30数据本身信息量小RBF 核可能通过非线性映射找到更好的边界但调参成本高。表情识别数据集通常在 1~2 万张以内各类表情开心、悲伤、惊讶、愤怒、恐惧、厌恶、中性7 类属于典型的小样本多分类问题。我自己一般两个都试先跑线性核做基线再用交叉验证搜索 RBF 的 C 和 gamma看提升是否超过 2 个百分点。不超过就果断用线性核省得模型复杂了还不好解释。Matlab 的fitcecoc可以处理多分类 SVM它内部用一对一策略组合多个二分类器。下面是训练和评估的完整代码% train_pca: PCA降维后的训练特征 % test_pca: PCA降维后的测试特征 % 线性SVM模板 svm_linear templateSVM(KernelFunction, linear, BoxConstraint, 1); % 训练多分类模型 model_linear fitcecoc(train_pca, train_labels, Learners, svm_linear, ... Coding, onevsone, Verbose, 0); % 预测并计算准确率 pred_labels predict(model_linear, test_pca); accuracy mean(pred_labels test_labels); fprintf(线性SVM测试准确率: %.2f%%\n, accuracy * 100); % ---- RBF核对比 ---- svm_rbf templateSVM(KernelFunction, rbf, BoxConstraint, 10, ... KernelScale, 5); % KernelScale与gamma呈倒数关系 model_rbf fitcecoc(train_pca, train_labels, Learners, svm_rbf, ... Coding, onevsone, Verbose, 0); pred_rbf predict(model_rbf, test_pca); acc_rbf mean(pred_rbf test_labels); fprintf(RBF SVM测试准确率: %.2f%%\n, acc_rbf * 100);模板SVM中的BoxConstraint就是 C 值控制误分类惩罚的强度。C 太大容易过拟合太小则欠拟合。KernelScale与 gamma 的关系是gamma 1 / (2 * KernelScale^2)所以 KernelScale 越小决策边界越曲折。这里的参数是我个人常用的起点实际需要跑交叉验证微调。onevsone编码方式对 7 类问题会生成 21 个二分类器训练时间略长但精度通常优于onevsall样本不均衡时更明显。3.3 交叉验证参数搜索避免在小数据集上自嗨表情识别数据集规模小随机划分训练集和测试集一次的结果偶然性很大。比如你运气好测试集里全是容易分类的样本精度直接虚高 5 个百分点。所以参数评估一定要用 K 折交叉验证。常见做法是 5 折或 10 折把数据分成 K 份轮流拿 1 份当验证集其余 K-1 份训练最终精度取 K 次平均。下面这段代码演示了如何对 PCA 维数 K 和 SVM 参数做网格搜索。注意这里搜索的空间不能太大否则训练时间会爆炸我一般先粗搜定区间再细搜定最优值。% features_std: 标准化后的原始特征 % labels: 标签 rng(42); % 固定随机种子保证结果可复现 cv_indices crossvalind(Kfold, labels, 5); K_values [20, 30, 50, 80, 100]; C_values [0.1, 1, 10, 100]; best_acc 0; best_params []; for K K_values % 每次CV都用当前K做PCA注意PCA要在训练折内做不能在全体数据上做 for C C_values acc_fold zeros(5, 1); for fold 1:5 train_idx (cv_indices ~ fold); test_idx ~train_idx; % 在训练折内做PCA [coeff_fold, score_fold, ~, ~, ~] pca(features_std(train_idx,:)); train_pca_fold score_fold(:, 1:K); test_pca_fold features_std(test_idx,:) * coeff_fold(:, 1:K); % 训练线性SVM svm_tpl templateSVM(KernelFunction, linear, BoxConstraint, C); model_fold fitcecoc(train_pca_fold, labels(train_idx), ... Learners, svm_tpl, Coding, onevsone); pred_fold predict(model_fold, test_pca_fold); acc_fold(fold) mean(pred_fold labels(test_idx)); end mean_acc mean(acc_fold); if mean_acc best_acc best_acc mean_acc; best_params [K, C]; end fprintf(K%d, C%.1f, CV准确率%.2f%%\n, K, C, mean_acc*100); end end fprintf(最优: K%d, C%.1f, 准确率%.2f%%\n, ... best_params(1), best_params(2), best_acc*100);这段代码里最容易踩坑的点我已经在注释里标出来了PCA 必须在每个训练折内单独计算不能在全部数据上先做一次 PCA。原因还是信息泄漏——如果全体数据做了 PCA测试折的信息已经通过协方差矩阵影响了主成分方向交叉验证结果会偏乐观。这个问题的严重程度在特征维度高、样本量小时尤其明显。你如果看到某些源码在交叉验证之前先对整个数据集做 PCA然后才划分训练测试那精度基本不可信跑出来好看但泛化能力差。4. 把特征提取、降维、分类串成完整流水线主脚本设计与数据组织4.1 标准数据集目录结构与图像预处理顺序要复现这套流程首先得有数据。常见的公开表情数据集比如某高校提供的标准表情库目录结构一般是按类别分子文件夹happy/、sad/、angry/等每类里面是若干张 JPEG 或 PNG 图像。你需要自己写一个脚本读取所有图像、提取特征、存成.mat文件方便后续训练不用重复提取特征。图像预处理这一步非常关键我见过太多在特征提取上折腾半天、最后发现是图像没对齐或者没归一化导致精度上不去的案例。表情识别面对的人脸图像至少要经过下面这几步转灰度、人脸检测裁剪或者数据集本身已经裁剪好、缩放到固定尺寸如 128x128 或 64x64、直方图均衡化可选。其中直方图均衡化对 LBP 的影响是双刃剑——它能增强对比度但也会改变纹理的局部结构有时反而让 LBP 编码变差。我的经验是如果原图光照条件相对一致跳过均衡化如果光照差异大先做均衡化再提取 LBP然后用不做均衡化的原图提取 LPQ两个特征分别提取后拼接。下面是一个从数据集目录到特征矩阵的完整脚本骨架% 读取数据并提取特征 root_path D:/datasets/emotion_dataset; emotion_cats {happy, sad, angry, fear, surprise, disgust, neutral}; img_size [128, 128]; features []; labels []; for i 1:length(emotion_cats) cat_path fullfile(root_path, emotion_cats{i}); img_files dir(fullfile(cat_path, *.jpg)); for j 1:length(img_files) img_path fullfile(cat_path, img_files(j).name); img imread(img_path); if size(img, 3) 3 img rgb2gray(img); end img imresize(img, img_size); % 可选直方图均衡化默认不做 % img histeq(img); % 提取LBP和LPQ特征并拼接 lbp_hist lbp_uniform(img); lpq_hist lpq(img, 3); feat [lbp_hist(:), lpq_hist(:)]; features [features; feat]; labels [labels; i]; % 1~7对应emotion_cats end end % 保存到mat文件 save(emotion_features.mat, features, labels, emotion_cats, img_size);代码里features和labels用动态拼接数据量小时没问题几千张图跑得动。但如果你的数据集超过 1 万张动态拼接会很慢建议预分配矩阵先遍历统计每类图像数量再初始化features zeros(total_samples, 315)。img_size的选择对 LBP 的影响不大但 LPQ 的win_size参数如果大于图像尺寸会直接报错这里固定用 3x3和图像尺寸无关。另外注意dir的通配符只匹配*.jpg如果数据里有 PNG 格式会漏掉最好改成*.jpg和*.png都扫一遍。4.2 单张图像预测从训练好的模型到实时分类流水线搭好之后你可能还想做单张图像的预测——比如给一张新的人脸图判断它是什么表情。这个流程和训练时的特征提取要保持完全一致否则模型直接失效。最容易犯的错误是新图像没有经过和训练数据相同的预处理比如尺寸不一致、没有转灰度导致特征分布完全不同SVM 给出的置信度毫无意义。下面是单张图像预测的完整函数function emotion_label predict_emotion(img_path, model, mu, sigma, coeff, K) % 加载并预处理图像与训练流程严格一致 img imread(img_path); if size(img, 3) 3 img rgb2gray(img); end img imresize(img, [128, 128]); % 提取特征LBPLPQ拼接 lbp_hist lbp_uniform(img); lpq_hist lpq(img, 3); feat [lbp_hist(:), lpq_hist(:)]; % 标准化和PCA投影复用训练集的参数 feat_std (feat - mu) ./ sigma; feat_pca feat_std * coeff(:, 1:K); % SVM预测 emotion_label predict(model, feat_pca); end这里mu、sigma、coeff、K、model都是训练阶段保存下来的结果必须在训练脚本末尾用save(emotion_model.mat, model, mu, sigma, coeff, K, emotion_cats)存盘。预测函数里绝对不能重新计算这些参数。另外如果训练时对图像做了直方图均衡化预测时也必须对输入图做同样的操作这一步极其容易被遗漏。我自己就翻过车训练时开了histeq预测时把这件事忘了结果测试集上 90% 的精度到了单张预测跌到 60% 以下排查了半天才发现是预处理不一致。4.3 把代码组织成可复用的工程结构如果只是跑通实验单个脚本就够了。但如果你要反复调参、对比不同特征组合的效果建议把代码拆成三个文件extract_features.m、train_model.m、predict_emotion.m。第一个负责从原始图像提取特征并缓存到.mat第二个负责加载特征、做 PCA 和 SVM 训练输出模型文件第三个负责加载模型做预测。这样改特征提取参数时只需要重跑第一个文件训练和预测的代码不用动改 SVM 参数时也只需要重跑第二个文件。这个工程结构看起来简单但对实验管理的帮助很大。尤其是当你尝试了七八种特征组合后如果没有这种隔离改来改去很容易把某个环节的参数覆盖掉最后都不知道当前模型是用哪版特征训练出来的。加上固定随机种子rng(42)每次跑出来的结果完全一致排查问题时就少了一个随机性变量。5. 避坑指南LBPLPQSVMPCA 的 5 个常见翻车点5.1 信息泄漏交叉验证前做 PCA 导致精度虚高现象交叉验证精度 95% 以上但换一批真实测试数据精度骤降到 60%。原因在划分训练/测试集之前就对全部数据做了 PCA测试集的信息已经融入了主成分方向。解决PCA 必须在训练折内单独计算测试集只做投影用训练集的coeff。本文第 3.3 节的代码已经演示了正确做法。这个问题在传统机器学习里是老生常谈但实际源码里出现频率非常高拿到别人的源码第一件事就是检查这一步。5.2 特征长度不匹配LBP 是 58 还是 59LPQ 是 255 还是 256现象训练时报维度错误或者在拼接特征时矩阵无法对齐。原因不同实现的统一模式 LBP 映射表定义有差异。有的实现把 58 种统一模式编号为 0~57另一种非统一模式归入一个 bin总数 59有的实现直接把“0~57”当特征不要最后一个“其他”类。LPQ 的 bin 数同理有的实现把 0~255 统计为 256 个 bin有的把 0~255 的直方图再合并掉空 bin。解决在拼接前打印size(lbp_hist)和size(lpq_hist)确认维度符合预期。如果不一致检查histcounts的区间设置或者统一改用 256 个 bin 的完整直方图对 LBP 而言就是去掉统一模式映射直接用原始 256 维。5.3 直方图归一化方式不一致现象LBP 和 LPQ 单独用精度都不错拼在一起精度反而下降。原因两个直方图一个用 L1 归一化除以总和另一个用了 L2 归一化除以平方和的平方根量纲不同导致拼接后 PCA 的主导方向被量纲大的特征占据。解决统一用 L1 归一化或者拼接后整体做 z-score 标准化。我个人推荐后者因为 PCA 之前做标准化对 SVM 的稳定性有额外收益。5.4 测试图像的预处理与训练集不一致现象模型在测试集上精度正常但部署到实际单张图像预测时完全不能用。原因测试脚本里的预处理如直方图均衡化、归一化和训练时不一致或者训练时用了histeq测试时忘了用。解决把预处理封装成一个函数训练和预测都调用同一个函数不要在两处各写一遍。这是工程习惯问题但对精度影响是毁灭性的。5.5 标签类别顺序与 SVM 的编码顺序混淆现象预测结果总是偏向某一类或者混淆矩阵对角线上某一行的精度特别差。原因训练时labels用的是循环里的整数 1~7而emotion_cats的顺序是happy, sad, angry...如果预测时想输出类别名称直接用了emotion_cats(pred_label)但不同脚本里emotion_cats的顺序变了导致标签错位。解决把emotion_cats和模型一起保存到.mat文件里预测时从.mat加载不要在不同脚本里重新定义。曾经有个项目训练和预测分布在两台机器上一个人改了类别顺序另一个人不知道整整调了两天最后发现不是模型问题就是标签索引错位了。6. 提升精度的进阶技巧从 85% 到 92% 我做了什么传统特征工程的上限不是固定不变的通过对细节的打磨精度可以再有明显提升。我自己在标准表情数据集上从 85% 提到 92%主要做了四件事。第一件事是分块特征。前面讲的特征提取是对整张图统计一个全局直方图但表情的细节往往集中在眼睛、嘴巴区域全局直方图把这些局部信息平均掉了。改进方法是把图像分成 4x4 或 3x3 的块对每个块分别提取 LBP 和 LPQ 直方图然后全部拼接起来。比如分成 4x416 块每块提取 59 维 LBP 和 256 维 LPQ最终特征维度是 16 * (59256) 5040 维。维度大幅提升但 PCA 降维后通常只需要 100~150 维就足够。这个改动带来的精度收益通常有 3~5 个百分点。function feat extract_block_features(img, grid_size, win_size) [h, w] size(img); block_h floor(h / grid_size); block_w floor(w / grid_size); feat []; for i 1:grid_size for j 1:grid_size block img((i-1)*block_h1:i*block_h, ... (j-1)*block_w1:j*block_w); lbp_h lbp_uniform(block); lpq_h lpq(block, win_size); feat [feat, lbp_h(:), lpq_h(:)]; end end end分块大小是精度和维度的平衡点。3x3 分块的维度是 9*(59256)28354x4 是 50405x5 是 7875。我测下来 4x4 比 3x3 高约 1~2 个百分点5x5 提升微乎其微但训练时间成倍增加所以 4x4 是甜点值。分块时要注意最后一行的边界用floor取整后图像底部会有一两行像素被丢弃属于可接受的损耗。如果想要更精细可以先padarray填充再分块。第二件事是数据增强。传统方法一样可以做数据增强不需要像深度学习那样复杂只需简单操作水平翻转、小幅旋转±10 度、小幅平移±3 像素、添加少量高斯噪声。注意增强后的样本只能加入训练集不能加入测试集否则会造成测试集污染。我一般把每类样本增强到原来的 4 倍精度提升约 1~2 个百分点主要帮助 SVM 更好地区分边界附近的样本。第三件事是特征选择而非仅 PCA。PCA 是无监督的有时候换了有监督的判别分析方法如 LDA效果更好。LDA 和 PCA 的区别在于PCA 找的是方差最大的方向LDA 找的是类间散布最大、类内散布最小的方向。对小样本高维数据LDA 容易过拟合但特征维度在 100 左右时通常表现稳定。我试过先 PCA 降到 100 维再做 LDA 降到 6 维类别数 7 类LDA 最多 6 维准确率比单独用 PCA 后直接 SVM 高约 2%。代价是 LDA 同样存在信息泄漏风险必须在训练折内计算投影矩阵。第四件事是集成投票。这个稍微重一点训练两个独立的 SVM一个用 LBP 特征一个用 LPQ 特征分别预测后对结果做投票。如果两个模型预测一致直接输出不一致时选择置信度更高的那个。Matlab 的fitcecoc返回的predict默认只输出标签但加上ReturnScore, true选项可以拿到每个类别的分数用分数最高的那个模型作为最终输出。这个方法在 LBP 和 LPQ 互补性强的数据集上效果明显如果两种特征本身精度差不多、相关性又低投票能稳住错误率。做完整套优化后我终于把精度从 85% 推到了 92%。这个提升不是靠某一个魔法参数而是分块特征贡献了 3 个点数据增强贡献了 1.5 个点LDA 替换 PCA 贡献了 1.5 个点投票贡献了 1 个点。回头总结最大的教训是每一项改动都要单独验证——我试过同时改四个东西精度确实涨了但你永远不知道是哪一步起了作用也不知道有没有负优化被其他改动掩盖了。后来改成每改一个变量跑一次交叉验证进度慢一些但每一步都心里有数。传统方法的好处就在于可解释性强每个环节都能单独调试这也是我至今仍然觉得 Matlab 这套组合值得做、值得投入的根本原因。希望这套流程和踩坑经验能帮到你。本文还有配套的精品资源点击获取