微表情识别实战:多特征融合从LBP-TOP到深度特征的完整指南
2026/9/24 22:21:54 网站建设 项目流程

简介:基于Python的多特征融合微表情识别项目,面向计算机视觉入门者与正在准备课程设计、毕业设计的学生,目标是帮助快速掌握微表情识别中的多特征融合方法。项目基于TensorFlow 2.2.0构建,完整覆盖了人脸裁剪配准、欧拉视频放大、时序插值、特征提取与分类评估等流程,能够以较小成本实现一个可运行的微表情识别实验。压缩包共8个文件,主体为6个Python脚本、1个说明文档和1个配置文件,整体大小仅21KB;Python脚本按功能模块拆分,人脸配准模块负责标准人脸对齐与裁剪,时序处理模块用于微表情运动信息的增强与插值,特征学习与分类模块则实现多特征融合和最终评估,结构清晰,方便读者逐段阅读与二次开发。说明文档给出了Python、OpenCV、dlib、TensorFlow等环境依赖版本,降低部署门槛。资源已有195人学习,适合希望通过轻量级代码系统理解微表情识别技术路线和工程细节的学习者。

1. 微表情识别,卡在多特征融合这根独木桥上

那天下午我把一段不到半秒的嘴角抽搐反复放了三遍,人眼几乎看不出情绪,CNN 给到的七个类别置信度差距也拉不开——这正是微表情识别的常态。微表情持续时间只有普通表情的十分之一左右,面部肌肉位移可能只差几个像素,依靠单帧画面或者单一特征很难抓住信号。基于 python 实现的多特征融合微表情识别,本质上就是同时要时空纹理、运动方向和语义信息三只手,才能把微弱信号从噪声里捞出来。这篇内容面向做行为分析、心理学辅助评估和情感计算的研究者与工程师,从特征选择、数据划分到落地代码和翻车记录,一次讲透。

2. 为什么单一特征搞不定微表情:三个维度的信息各自半盲

2.1 微表情和普通表情的本质差异:不是算法问题,是信号强度问题

普通表情识别可以靠单帧完成,因为表情幅度大、人脸的几何形变明显,一个训练充分的 CNN 在 CK+ 这类数据集上拿高准确率是很自然的事。但微表情是被抑制后的表情,持续时间普遍在 1/25 到 1/5 秒之间,运动幅度往往只有几个像素,单帧里的差异甚至不如光照噪声引人注目。

此时算法面对的不再是“分类问题”,而是弱的信号检测问题。帧与帧之间的时间连续性反而成了最可靠的信息源。我见过不少直接把图片分类模型搬过来的项目,结果无一例外停在 50% 左右的准确率上——不是模型不努力,是输入里根本没有足够信号。想让模型看见微表情,必须把多个视图的信息都喂进去,让每种特征各自补上盲区。

2.2 三个并行抽取向:LBP-TOP、光流、深度语义特征

多特征融合不是越多越好,而是希望三种特征各自覆盖不同的信号维度。我一般固定选三种:LBP-TOP 负责时空纹理,稠密光流负责运动方向和幅度,预训练深度特征负责语义抽象。它们之间高度互补,又不会在计算上互相抢占资源。

特征提取视角擅长捕捉明显弱点
LBP-TOP时空纹理光照变化下的局部纹理和边缘变化对运动幅度不敏感
稠密光流帧间运动场微小位移的方向和强度低纹理区域噪声大
预训练深度特征高层语义面部结构、动作单元的抽象表示单帧输入,缺少时间维度

LBP-TOP 把传统的 LBP 从单帧扩展到三个正交平面:XY 平面刻画空间纹理,XT 和 YT 平面刻画像素随时间的运动轨迹,相当于把整个视频卷成一个纹理描述子。它对光照变化比原始灰度更宽容,这是它在传统方法里能一直活到今天的原因。

光流则直接计算每一帧里每个像素的位移矢量。微表情的运动幅度虽然小,但方向是有序的——嘴角上翘和眼角收缩的光流方向不一样,光流拿到的正是 LBP-TOP 最不敏感的运动方向信息。深度特征则来自 ImageNet 预训练模型,它知道“嘴在动”和“眉毛在抬”在语义上不是一回事,这是手工特征不具备的抽象能力。三者一起用,才能覆盖“长什么样、怎么动、在表达什么”三个问题。

2.3 融合粒度决定上限:为什么特征级融合是最省事的起点

融合一般分三个粒度。像素级融合要把三路特征对齐到同一个像素坐标系,计算开销大,而且微表情本身信号弱,叠加噪声容易把有效信息埋得更深,我在小样本场景里基本不碰。

决策级融合是对每种特征单独训练一个分类器,再用投票或平均概率做最终预测,虽然鲁棒,但三个分类器之间没有信息交互,每个弱模型只能依据单视角决策,上限不高。

真正适合作为起点的是特征级融合:把三种特征各自向量化、标准化、降维,然后拼接成一个完整向量,交给一个分类器。公式很朴素: $$F = concat(\phi_{lbp}, \phi_{flow}, \phi_{deep})$$

拼完之后有没有效果,看的是每种特征在融合之后是否真的贡献了区分度。特征级融合的调参路径清晰:先单独评估每个特征的效果,再拼起来看涨跌,哪个环节出了问题一眼能定位到,这是后面所有踩坑分析的基础。

3. 先把底子打牢:数据准备、预处理和评价协议

3.1 微表情数据集的正确打开方式

微表情数据集比普通表情少两个数量级。目前学术圈最常被提到的是 CASME II、SAMM 和 SMIC 三个,样本量都在百段量级,类别数也少,获取方式通常是向数据集作者提交学术申请并签署使用协议,论文署名要求也各有不同。

数据量小直接决定了实验设计不能照搬普通表情识别。最关键的规矩是:必须按受试者划分训练集和测试集,也就是 leave-one-subject-out(LOSO)协议。同一个人的不同视频不能同时出现在训练集和测试集里,否则模型会偷学个人身份特征,指标虚高得毫无意义。

用 scikit-learn 实现 LOSO 只需要两行,但注意 group 参数要传受试者编号,不是传样本编号。

from sklearn.model_selection import LeaveOneGroupOut logo = LeaveOneGroupOut() for train_idx, test_idx in logo.split(feature_matrix, labels, groups=subject_ids): X_train, X_test = feature_matrix[train_idx], feature_matrix[test_idx] y_train, y_test = labels[train_idx], labels[test_idx] # 每一折训练一个分类器,最后汇总所有折的预测结果再算指标

这段代码里subject_ids是每个视频样本对应的受试者 id 数组。不能和普通分类任务的train_test_split混用,也不能用GroupShuffleSplit按固定比例切,因为有的受试者表情样本多、有的少,固定比例容易造成某些类别在一折里消失。评估指标也要跟着换:只看 accuracy 在小样本上很容易虚高,需要同时报告加权 F1 和 UAR(unweighted average recall),后者对少数类别更敏感。

3.2 预处理管道:人脸对齐与 ROI 裁剪

微表情识别的预处理比普通表情更挑剔,因为输给特征的区域必须是同一个人的同一块脸。我推荐先用 MTCNN 做人脸检测,再根据左右眼坐标做仿射对齐,这样头部轻微摆动就不会在 ROI 里引入无关运动。

from facenet_pytorch import MTCNN import cv2 import numpy as np def align_clip(frames): mtcnn = MTCNN(keep_all=False, thresholds=[0.6, 0.7, 0.7]) aligned = [] for frame in frames: rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) boxes, probs, landmarks = mtcnn.detect(rgb, landmarks=True) if landmarks is None: continue left_eye = landmarks[0][0] right_eye = landmarks[0][1] dx, dy = right_eye[0] - left_eye[0], right_eye[1] - left_eye[1] angle = np.degrees(np.arctan2(dy, dx)) center = (left_eye + right_eye) / 2.0 mat = cv2.getRotationMatrix2D(tuple(center), angle, 1.0) warped = cv2.warpAffine(frame, mat, (frame.shape[1], frame.shape[0])) aligned.append(warped) return aligned

MTCNN 第一次运行会下载权重到缓存目录,这个行为依赖网络环境但不需要手动干预。thresholds是三个级联网络的置信度阈值,微表情视频常出现低分辨率,建议每级都比默认值放宽 0.05 左右,否则容易整段视频检测不到人脸。

对齐后只裁脸的中下部区域(眼睛到下巴)就够用,因为微表情主要集中在眉、眼、嘴三个区域。统一缩放到 64×64 或 96×96,再转灰度,后续的 LBP-TOP 计算才承受得起。

3.3 序列归一化:把长度不一致的视频变成等长张量

数据集的视频长度参差不齐,最短的可能只有 8 帧,长的有 30 多帧,而且不同设备采样率也不一样。直接把序列 padding 到等长会让模型把无效帧里的光流当真值。我一般做法是统一线性插值到固定长度,比较相对稳定的帧数窗口是 16 或 32,由原始视频的最短长度决定。

def temporal_resample(arr, target_len=16): if arr.shape[0] == target_len: return arr axis = np.linspace(0, arr.shape[0] - 1, arr.shape[0]) interp_axis = np.linspace(0, arr.shape[0] - 1, target_len) out = np.zeros((target_len,) + arr.shape[1:], dtype=np.float32) for i in range(arr.shape[-1]): out[..., i] = np.interp(interp_axis, axis, arr[..., i]) return out

这段代码把特征序列按原有时间分布重新采样,不会改变运动的先后顺序。arr可以是图像序列,也可以是抽好的一维特征序列,接口通用。

数据增强在这个方向上是把双刃剑。水平翻转和轻微亮度扰动对大多数样本有效,但随机遮挡、强模糊这类增强会把本来就弱的微表情运动直接盖掉,我建议不做。时间方向的轻微缩放可以做,但要控制在 0.9 到 1.1 倍之间,否则会扭曲表情的起止节奏。

4. 多特征融合的完整落地:从特征提取到分类器

4.1 环境依赖与基础代码骨架

跑这套流程需要的核心库不多:opencv-python、numpy、scikit-learn、torch、torchvision,再加上 facenet-pytorch 用来做检测。装依赖时最常遇到的是下载慢,直接把 pip 源切到国内镜像站能省下大量时间,然后再一次装齐:

pip install opencv-python numpy scikit-learn torch torchvision facenet-pytorch

装完先验证 cv2 和 numpy 的版本,我遇到过 numpy 2.x 与旧版 opencv 出现接口不兼容的情况,处理办法是统一升级到新版而不是降级。IDE 报找不到 cv2 模块时,先确认当前解释器和 pip 装到了同一个环境里;VSCode 里选错解释器是新手最常踩的坑。

准备一个统一的视频读取函数,后面每个特征提取步骤都从它拿灰度帧和 RGB 帧:

import cv2 import numpy as np def load_clip(video_path): cap = cv2.VideoCapture(video_path) frames_gray, frames_rgb = [], [] while True: ok, frame = cap.read() if not ok: break frames_rgb.append(frame) frames_gray.append(cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)) cap.release() return frames_gray, frames_rgb

读取时不要在原视频上直接做预处理,因为后续对齐、裁剪需要多次回看原始帧。视频解码失败时先检查文件路径是否包含中文字符,OpenCV 对中文路径支持不好,重命名为纯英文路径是最快的解决方案。

4.2 LBP-TOP:把时空纹理卷成一个特征向量

LBP-TOP 的实现思路是从三个正交平面分别提取局部二值模式。XY 平面就是普通 LBP 直方图,重点关注每一帧的空间纹理;XT 平面固定某个像素行,横轴是时间,描述的是这一行像素沿时间的变化;YT 平面同理,固定像素列描述的是“列随时间”的变化。三者直方图拼接后得到时空纹理描述子。

from skimage.feature import local_binary_pattern def lbp_top(frames_gray, P=8, R=1, sample_step=2): T = len(frames_gray) H, W = frames_gray[0].shape hist_size = P + 2 hist_xy = np.zeros(hist_size) hist_xt = np.zeros(hist_size) hist_yt = np.zeros(hist_size) for t in range(0, T, sample_step): g = frames_gray[t] lbp_xy = local_binary_pattern(g, P, R, method='uniform') hist_xy += np.bincount(lbp_xy.ravel(), minlength=hist_size) / (H * W) for h in range(0, H, sample_step): plane_xt = np.stack([frames_gray[t][h, :] for t in range(T)], axis=0) lbp_xt = local_binary_pattern(plane_xt, P, R, method='uniform') hist_xt += np.bincount(lbp_xt.ravel(), minlength=hist_size) / (T * W) for w in range(0, W, sample_step): plane_yt = np.stack([frames_gray[t][:, w] for t in range(T)], axis=0) lbp_yt = local_binary_pattern(plane_yt, P, R, method='uniform') hist_yt += np.bincount(lbp_yt.ravel(), minlength=hist_size) / (T * H) num_row_samples = H // sample_step num_col_samples = W // sample_step hist_xt /= max(num_row_samples, 1) hist_yt /= max(num_col_samples, 1) return np.concatenate([hist_xy, hist_xt, hist_yt])

这段代码用了 P=8、R=1 的 uniform LBP。uniform模式会把 256 种原始编码压缩成 P+2 个 bin,样本量只有几百的时候,这个压缩非常关键,否则直方图维度会膨胀到 256×3,基本必过拟合。sample_step是我用来控制计算量的参数,对小尺寸 ROI 取 1 或 2 都行,视频帧多时取 2 可以明显加速而不损失太多信息。

LBP-TOP 对灰度变化和中低幅度的边缘运动敏感,但它有个天生缺陷:对运动方向不敏感。所以下一节的光流是它的互补搭档。

4.3 稠密光流:捕捉微表情的微小形变

微表情运动幅度小,但方向是有规律可循的。Farneback 稠密光流在低分辨率 ROI 上计算速度快,参数调整空间也足够应对微表情这种弱运动场景。

def optical_flow_features(frames_gray): mag_list, ang_hist_acc = [], np.zeros(36) prev = frames_gray[0] for i in range(1, len(frames_gray)): curr = frames_gray[i] flow = cv2.calcOpticalFlowFarneback( prev, curr, None, pyr_scale=0.5, levels=3, winsize=15, iterations=3, poly_n=5, poly_sigma=1.2, flags=0 ) mag, ang = cv2.cartToPolar(flow[..., 0], flow[..., 1]) mag = mag[mag > 0.1] # 滤掉噪声级别的微小位移 if mag.size == 0: continue mag_list.extend(mag.tolist()) hist, _ = np.histogram(ang[ang != 0], bins=36, range=(0, 2 * np.pi)) ang_hist_acc += hist / ang[ang != 0].size prev = curr if len(mag_list) == 0: return np.zeros(39, dtype=np.float32) mag_arr = np.array(mag_list) feat = np.concatenate([ [mag_arr.mean(), np.median(mag_arr), mag_arr.std()], ang_hist_acc / (len(frames_gray) - 1) ]) return feat.astype(np.float32)

Farneback 四个关键参数决定了它能不能看清微表情:winsize是积分窗口,取得太大会把微弱运动抹平,取 15 左右比较平衡;poly_n是多项式展开邻域,5 在细节和噪声抑制之间比较均衡;levels是金字塔层数,3 层能让大位移和小位移都能被追踪到;poly_sigma控制平滑程度,1.2 是默认值,微表情场景不用改。阈值 0.1 是关键技巧,把光流幅度低于 0.1 的像素当作静止噪声丢掉,能明显减少融合后的抖动。需要特别注意ang里可能混入无效角度,所以直方图统计前要过滤。

4.4 深度特征:用预训练模型补上语义维度

手工特征缺少语义信息。一个皱眉和一个嘴角下撇在光流上都表现为向下运动,但表达的意向完全不同。这里接入预训练 ResNet18 提取深层抽象特征,不重新做微调,因为样本量不支持在微表情数据上端到端训练深层网络。

import torch import torchvision.models as models from torchvision import transforms device = 'cuda' if torch.cuda.is_available() else 'cpu' model = models.resnet18(weights=models.ResNet18_Weights.DEFAULT) model.fc = torch.nn.Identity() # 去掉最后的分类层,只留特征 model = model.to(device).eval() preprocess = transforms.Compose([ transforms.ToPILImage(), transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) def deep_features(frames_rgb): feats = [] with torch.no_grad(): for frame in frames_rgb[::2]: # 每两帧抽一帧,降低计算量 inp = preprocess(frame).unsqueeze(0).to(device) feat = model(inp).cpu().numpy().flatten() feats.append(feat) if len(feats) == 0: return np.zeros(512, dtype=np.float32) feats = np.array(feats) # 时间维上同时做均值池化和最大池化,再拼接 return np.concatenate([feats.mean(axis=0), feats.max(axis=0)]).astype(np.float32)

每帧过一遍 ResNet18 得到 512 维向量,然后沿着时间维度做均值池化,保留整体趋势;做最大池化,保留最强响应的那一帧。两个池化结果一拼接就是 1024 维,比单独用均值丰富得多。这里frame[::2]是抽帧,如果视频本身就只有十几帧,不应该再抽,直接全量计算。

4.5 特征级融合、降维与分类评估

把三路特征拼接起来只是第一步。拼接后的维度在 1100 左右,而样本只有两三百段,直接丢给分类器必然过拟合,所以必须做标准化加 PCA,再用径向基 SVM 或 LightGBM 执行 LOSO 评估。

from sklearn.preprocessing import StandardScaler from sklearn.decomposition import PCA from sklearn.svm import SVC from sklearn.pipeline import make_pipeline from sklearn.metrics import classification_report from sklearn.model_selection import LeaveOneGroupOut def extract_all(frames_gray, frames_rgb): f_lbp = lbp_top(frames_gray) f_flow = optical_flow_features(frames_gray) f_deep = deep_features(frames_rgb) return np.concatenate([f_lbp, f_flow, f_deep]) clf = make_pipeline( StandardScaler(), PCA(n_components=0.95), SVC(C=10, gamma='scale', class_weight='balanced') ) logo = LeaveOneGroupOut() y_pred, y_true = [], [] for train_idx, test_idx in logo.split(all_features, all_labels, groups=subject_ids): X_train, X_test = all_features[train_idx], all_features[test_idx] y_train, y_test = all_labels[train_idx], all_labels[test_idx] clf.fit(X_train, y_train) y_pred.extend(clf.predict(X_test)) y_true.extend(y_test) print(classification_report(y_true, y_pred, zero_division=0))

PCA 参数n_components=0.95表示保留 95% 的方差,比硬编码 300 维更省心。class_weight='balanced'负责处理类别不均衡,微表情数据集中高兴类样本经常只有其他类别的一半,这行设置能明显提高少数类召回。SVM 的 C 取 10、核函数用 RBF,是当前数据集规模下最不用费心的一组参数。

标准化的 fit 必须发生在训练集上,测试集只能 transform,绝不能全量数据先 fit 再切分。PCA 也一样,要放进 pipeline 里而不是在切分前提前做。

5. 落地最常踩的 5 个坑:现象、原因与对策

5.1 随机划分训练集:准确率 89%,其实是数据泄漏

现象:用 train_test_split 随机划分,LOSO 协议没做,测试集指标接近 90%,所有特征看起来都有效。把验证方式换成 LOSO 之后准确率掉到 50%,直接翻车。

原因:同一个受试者的多个视频被拆到了训练集和测试集,模型记住的是这个人长什么样、面部结构有什么特征,而不是表情的类别。微表情强度弱,身份特征占比远高于表情特征,模型优先学身份特征几乎是必然的。

解决:强制按受试者 id 分组做 LOSO,并且把标准化和 PCA 都放进每一折的 pipeline 内部,只允许训练集参与 fit。我在代码评审中发现,即使做了 LOso 的人,也常犯把 StandardScaler 放在全量数据上 fit 的错误,这会让每折的验证数据提前暴露均值与方差,指标同样被污染。

5.2 特征维度爆炸加类别不均衡:F1 惨不忍睹

现象:LBP-TOP 直接用原始 P=16、R=2 的 full 编码,直方图 256 维,再拼接光流和深度特征后总维度接近 1500。样本只有两百多,SVM 训练没有任何报错,但少数类召回率是 0。

原因:维度远大于样本量时,模型很容易找到区分训练集的假模式,测试集上完全失效。同时数据集类别分布天然不均衡,压抑、悲伤这类样本少,普通 SVM 的决策边界会被多数类主导。

解决:LBP 一律用 uniform 模式,P=8 就好,直方图 bin 数从 256 降到 10,三维度拼接后约 30 维;深度特征先做 PCA 保留 0.95 方差;分类器里显式打开 class_weight='balanced'。评估时不要只看 accuracy,同时打印每类的 precision、recall 和 UAR,少数类数值迟早会暴露问题。

5.3 光流把微表情当噪声滤掉了

现象:融合后的结果不如只用 LBP-TOP,拆开看发现光流特征单独评估就是约 30% 准确率,等于往融合向量里掺了一批垃圾特征。

原因:Farneback 的winsize参数如果取到 30 甚至更大,微表情那几像素的位移在积分窗口里被平均成零了;反之winsize取太小,噪声又被当成运动。另外很多实现直接对所有像素的光流做统计,脸部非表情区域的轻微头部晃动、眨眼,都会把统计量带偏。

解决:把 winsize 固定在 15 到 21 之间;计算光流前对灰度帧做一次高斯滤波,核大小 3×3,sigma 0.8;光流幅度小于 0.1 的像素直接置零;统计时只限制在眼睛、眉毛和嘴周裁剪出的 ROI 内,不统计整张脸。按这个配置重新跑单独评估,光流特征的准确率能提升到 40% 上下,融合后才会带来净收益。

5.4 视频长度不齐,直接 padding 带偏分类器

现象:把短视频和长视频都补零到固定长度再进 LSTM,训练时 loss 正常下降,但测试时预测几乎全偏向某几个类。

原因:padding 的 0 被模型理解为“静止画面”,而这个静止画面在长视频样本里占据了大量比例。注意力机制和 LSTM 都会学习到“尾部都是零”这个捷径,而不是表情本身。RGB 图像 padding 用 0 还能看到黑边,特征向量的 0 完全无声无息,极其隐蔽。

解决:首选线性插值到固定长度,16 或 32 帧;如果一定要保留原始长度信息,则在序列进入模型时额外输入每个样本的真实帧数 mask。mask 的具体做法是在特征尾部拼接一个长度掩码向量,让时序模型知道哪些位置是真实帧,哪些不是。特征级融合加 SVM 的场景则完全不需要 padding,这也是先用传统特征打底的原因之一。

5.5 MTCNN 检测抖动导致整个序列特征报废

现象:跑完预处理后某几段视频的特征与其他样本差异明显,画出来看是 ROI 区域内人脸位置一跳一跳的。回看预处理结果,发现同一段视频里 MTCNN 每帧输出的人脸框都不一样。

原因:微表情虽然是弱表情,但伴随的嘴角或眉眼动作依然会触发人脸检测器关键点的轻微漂移;低分辨率和运动模糊也会让某几帧的关键点置信度突然变低。检测框一旦抖动,切出来的 ROI 内容就不稳定,LBP 直方图会跟着大幅跳变。

解决:第一帧用 MTCNN 检测并定位 ROI 后,整段视频沿用同一组人脸框和关键点,不逐帧重新检测;如果视频里头部本身在动,则对连续帧的人脸框做指数平滑,alpha 取 0.6 左右,兼顾跟随速度和抖动抑制;个别检测失败的帧不要直接丢弃,用前后两帧的 ROI 做线性插值补上。

6. 进阶:把融合特征喂给时序模型,再做一次融合体检

6.1 给 LSTM 一个轻量接入点

特征级拼接加 SVM 是可靠的基线,但它把整段视频压缩成了一根向量,时间顺序信息也一起被压没了。想要进一步利用时序关系,可以把逐帧的光流特征、逐帧的深度特征组成序列,喂给 LSTM,让它自己学习哪一帧最关键。

import torch from torch import nn class TemporalNet(nn.Module): def __init__(self, in_dim, hidden=128, num_classes=7): super().__init__() self.lstm = nn.LSTM(in_dim, hidden, bidirectional=True, batch_first=True) self.fc = nn.Linear(hidden * 2, num_classes) def forward(self, x): out, _ = self.lstm(x) return self.fc(out.mean(dim=1))

in_dim是逐帧特征的维度,比如光流的 39 维加浅层深度特征的 128 维。双向 LSTM 能同时看到表情发生前后的上下文,batch_first=True让输入形状为 (batch, time, feature),对齐常规的 PyTorch 输入习惯。这个模型在样本量小的时候容易过拟合,hidden 128 已经偏高,再大建议加 dropout 正则。

6.2 用置换重要性确认融合到底加没加码

融合不是拼完就结束了。我拿到一个新特征,第一件事是单独评估它,评估通过后再拼进去,最后做一次置换重要性检查,确认每个特征块对最终预测的贡献确实存在。

from sklearn.inspection import permutation_importance result = permutation_importance( clf, X_test_scaled, y_test, n_repeats=10, random_state=42 )

permutation_importance会把某一列特征随机打乱,然后看预测指标掉多少。掉得越多说明这个特征越有价值;如果某个特征块打乱后指标基本不动,它就是在做无用功,甚至会稀释其他特征的权重。这个检查在 LOSO 循环里每次都要做,因为不同受试者 fold 的特征依赖不一样。

6.3 一个可复用的工程习惯:先跑通 pipeline 再加特征

我很久以前拿到新项目先调网络结构,后来发现前置工作才是决定成效的关键。现在的习惯是:固定随机种子,固定分类器参数,把预处理流程里最基础的特征拼接加 SVM 先跑通,输出每一类别的 recall 和 UAR 留档。再往上加时序模型,每加一个组件就看它是否让融合整体涨点,而不是只看单模型指标。多特征融合微表情识别这个方向的收益,往往不是某一支特征的爆发,而是三路弱信号叠加后,分类器终于跨过了判读门槛。也谈不上多玄学,唯一能确定的是每换一个数据集,参数都得重新筛一遍。希望这篇内容能帮你少走几个弯路,把精力放到真正有用的特征工程上去。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询