☰
Python皮肤电信号情绪识别源码包:从数据预处理到模型评估
2026/10/3 2:54:46 网站建设 项目流程

简介:面向情绪识别与人机交互研究场景,这份以Python实现的皮肤电信号(EDA/GSR)情绪识别算法包,适合机器学习初学者、高校学生及从事生理信号分析的研究者,用于解决从皮肤电原始数据中提取特征、训练情绪分类模型并完成效果评估的完整流程。源码均经本地编译验证,可稳定运行,整体难度适中,经助教审定,评审分达95分以上,可直接用于课程设计、毕业设计或算法复现。 包体共102个文件,大小仅1.91MB,其中78个csv为分段标注后的情绪皮肤电数据集,7个py为核心训练与预测脚本,6个xml保存工程运行配置,4个m为MATLAB辅助分析脚本,3个pyc为已编译模块,1个pptx用于结果汇报,1个md记录使用说明,另有iml工程文件与mat矩阵数据;数据类别覆盖happy、normal、sad等情绪标签,便于对照实验。目前已有217人学习下载,附带模型、PPT及详细文档,利于从数据读取、特征工程到模型评估全程跟练,适合希望快速搭建情绪识别基线方案的学习者。

1. 皮肤电信号情绪识别:这个 Python 源码包把从 CSV 到模型的链路全跑通了

做情绪识别的人拿到皮肤电信号(GSR/EDA)之后的第一反应,大多是直接扔给分类器,结果准确率常常低到没法看。这份基于 Python 的皮肤电信号情绪识别源码包,把从 CSV 原始数据到模型评估的完整链路都跑通了,源码本地编译可运行,评审分 95 以上,压缩包里还带着模型文件、答辩 PPT 和详细文档。它适合两类人:一是做课设或毕设、需要完整可复现项目的人,包括刚入门 Python 的初学者;二是接触生理信号处理、想看看数据清洗和特征工程在代码层面怎么落地的工程师。我拆完这个包最大的体会是,它的价值不在模型多高级,而在把最脏最累的预处理和特征提取做扎实了,下载之后改改路径就能复现整套结果。

2. 数据与预处理:从 10 个 CSV 到干净的可训练样本

2.1 文件构成与标签划分:先搞清楚手里有什么

压缩包里数据文件的名字其实已经把设计意图说了一半。happy_other.csv 和 normal_sad.csv 是按情绪标签组织的样本段文件,前者对应积极情绪,后者对应中性加消极情绪,这是典型的两分类情绪识别设定。剩下 1.csv、2.csv、9.csv、18.csv、19.csv、20.csv、21.csv、22.csv 这类数字命名的文件,按常见实验范式看,应该是分受试者或分 session 的原始记录,每一行一般是时间戳和皮肤电导值,单位通常是 uS(微西门子),有些记录还会带事件标记列。

文件内容数据来源用途
happy_other.csv积极情绪标签样本按情绪整理训练正样本
normal_sad.csv中性/消极情绪标签样本按情绪整理训练负样本
1.csv / 2.csv / 9.csv / 18~22.csv分段原始记录分受试者或分 session验证/测试

我拿到数据的第一件事不是跑模型,而是先画一条时间序列看看。直接 pd.read_csv 之后把 GSR 列画出来,能一眼看出有没有明显的基线漂移、毛刺和掉线段。这个包选择两分类是有道理的:皮肤电信号本质上对唤醒度敏感,对效价不敏感,让它区分「激动对平静」比区分「高兴对难过」可靠得多,所以 happy_other 和 normal_sad 这种标签设计,在生理信号上比四分类现实很多。标签映射关系在训练脚本里通常写成 normal_sad=0、happy_other=1,后面所有脚本都用这套映射,别在某个环节单独改。

2.2 滤波与滑动窗口:GSR 信号处理的第一个坎

皮肤电导反应(SCR)的生理特性决定了它的频谱范围:一个典型的 SCR 上升沿持续 1 到 3 秒,加上恢复期,有效成分基本都在 2Hz 以下。高于这个频率的,多半是工频干扰、运动伪迹和设备底噪。常见做法是先低通滤波再切窗。切窗用滑动窗口,窗口长度取 5 到 10 秒,步长取 2 到 5 秒,让相邻窗口有重叠,本质上是一种数据扩增,一段 5 分钟的记录能切出七八十个样本。

import numpy as np import pandas as pd from scipy.signal import butter, filtfilt, detrend def preprocess_gsr(raw, fs=20.0, cutoff=2.0, order=4): """ 皮肤电信号预处理:低通滤波 + 去趋势。 raw: 原始 GSR 一维数组,单位 uS fs: 采样率,Hz,包里没写的话要先从数据推断 """ # 低通滤波:2Hz 以上基本是噪声,保住 SCR 上升沿即可 b, a = butter(order, cutoff / (fs / 2), btype='low') filtered = filtfilt(b, a, raw) # 去趋势:扣掉缓慢的基线漂移,防止滑动窗口均值被基线带着走 filtered = detrend(filtered) return filtered

参数说明:cutoff 取 2.0Hz 是保守做法,取太低会把 SCR 的幅值削平,特征里就只剩波动趋势;filtfilt 是零相位滤波,不像普通 filter 会把波形整体滞后,这对后面提取上升沿斜率很关键。order 取 4 足够,再高阶会引入数值抖动。采样率 fs 如果包里没写,最简单的办法是看两个相邻时间戳的差,取倒数就是实际采样率,常见 GSR 设备是 4Hz、8Hz、20Hz 或 128Hz,预处理参数要跟着 fs 一起改。

2.3 特征提取:把一段波形压成一行数字

切完窗之后,每个窗口是一个一维数组。特征提取的目标是把这段波形压成一行数字特征,之后交给分类器。时域特征包括均值、标准差、极差、线性拟合斜率、偏度和峰度;频域特征可以取功率谱在 0 到 2Hz 频段的积分;另外还有 SCR 事件统计,比如窗口内超过阈值的峰个数和平均幅值,这类特征对唤醒度识别特别有效。

from scipy.signal import find_peaks def extract_features(window, fs=20.0, peak_thresh=0.1): """把一个滑动窗口压成一行特征字典。""" peaks, _ = find_peaks(window, height=peak_thresh, distance=int(fs)) feat = { 'mean': np.mean(window), # 均值:受基线影响,配合去趋势使用 'std': np.std(window), # 标准差:波动强度,情绪越激动通常越大 'range': np.max(window) - np.min(window), # 极差:窗口内动态范围 'slope': np.polyfit(range(len(window)), window, 1)[0], # 一阶斜率 'skew': pd.Series(window).skew(), # 偏度:上升沿陡峭会拉高偏度 'kurt': pd.Series(window).kurtosis(), 'n_peaks': len(peaks), # SCR 峰个数:唤醒度的重要指标 'peak_amp_mean': np.mean(np.abs(window[peaks])) if len(peaks) else 0.0, } return feat

逻辑说明:polyfit 拟合斜率比简单取首尾差值更抗噪声,因为窗口内的基线会有微小波动,最小二乘拟合能平滑掉这些扰动。find_peaks 的 height 参数是绝对阈值,不同个体基线不同,这个阈值最值得调;distance 用 int(fs) 表示至少间隔 1 秒才算两个独立的峰,避免同一个反应被重复计数。把每个窗口的特征收集成 DataFrame,再拼上标签列,就是后面训练脚本的输入,特征列的顺序从这一步开始固定下来,后面预测阶段要复用。

3. 模型训练与评估:传统机器学习在这个体量下的胜出理由

3.1 为什么不用神经网络

以这个包的数据量级,十几段实验记录切出来的样本撑死几百到几千条,喂给神经网络很容易过拟合。皮肤电信号不是图像或文本,没有天然的局部结构可以让卷积层去利用,强行搭 LSTM 反而会把简单问题复杂化。在这个体量下,传统机器学习是性价比最高的选择:随机森林和 SVM 对标准化之后的特征表现可靠,训练快,结果可解释,答辩的时候能指着特征重要性说「唤醒度主要由峰个数和标准差贡献」,这一点神经网络很难给出来。我一般的做法是先用随机森林打底,调不动了再试 SVM 和 XGBoost,不要一上来就上深度模型。

3.2 训练流程:标准化 + 交叉验证

训练脚本里的核心流程是:读特征表,切特征和标签,用 Pipeline 把标准化和分类器串起来,再做交叉验证。标准化这一步必须放在交叉验证内部,如果先对整个数据集做 StandardScaler 再切折,缩放统计量里就带进了验证集的信息,属于轻度数据泄露。

from sklearn.model_selection import cross_val_score from sklearn.ensemble import RandomForestClassifier from sklearn.preprocessing import StandardScaler from sklearn.pipeline import make_pipeline feature_cols = ['mean', 'std', 'range', 'slope', 'skew', 'kurt', 'n_peaks', 'peak_amp_mean'] X = df[feature_cols].values y = df['label'].values # 标签已经映射成 0/1 pipe = make_pipeline( StandardScaler(), RandomForestClassifier(n_estimators=200, max_depth=8, random_state=42) ) scores = cross_val_score(pipe, X, y, cv=5, scoring='f1_macro') print("5-fold F1: %.3f ± %.3f" % (scores.mean(), scores.std()))

参数说明:scoring 用 f1_macro 而不是 accuracy,是因为 happy 和 normal_sad 的样本数通常不平衡,准确率会被多数类带偏;cv=5 对这个量级是合适的,样本太少的话改成 StratifiedKFold(n_splits=5) 保证每折类别比例一致。n_estimators=200、max_depth=8 是控制过拟合的保守组合,树再多效果不会更好,只会更慢。

3.3 评估:混淆矩阵比准确率诚实

交叉验证跑完之后,包里配套的评估脚本会打印分类报告和混淆矩阵。情绪识别最怕的不是整体准确率低,而是系统性把某一类全部划到另一类。逻辑很简单:如果混淆矩阵显示几乎全部样本被分到 happy_other,说明模型学到的其实只是先验概率,特征没有提供判别信息;如果两类能互相错分且比例接近,说明特征有效,只是边界模糊。

from sklearn.metrics import classification_report, confusion_matrix from sklearn.model_selection import cross_val_predict y_pred = cross_val_predict(pipe, X, y, cv=5) print(classification_report(y, y_pred, target_names=['normal_sad', 'happy_other'])) print(confusion_matrix(y, y_pred))

这套输出要保留下来,做课设或汇报的时候,分类报告里 precision、recall、f1-score 三项是评委最常盯的东西,比「准确率 90%」这种一句话有说服力得多。我一般会顺手把特征重要性也打出来,确认模型没有依赖某个「运气好」的特征,比如某个文件的编号被当成隐式标签学进去了。

4. 避坑与排查:GSR 数据里五个高频翻车点

4.1 预处理和特征环节的三个坑

坑一:准确率 99%,一换数据就崩。现象:在包里自带数据上交叉验证 F1 接近 1.0,但随便换一个受试者的新记录,预测结果几乎全错。原因:这是典型的数据泄露,同一个受试者的相邻窗口高度相关,如果切分时把同一条记录的窗口同时分进训练集和验证集,模型等于提前看到了答案。解决:按文件(受试者)分组切分,用 sklearn 的 GroupKFold 或直接按文件名留一折验证。这也是数字命名的原始文件存在的意义,1.csv、2.csv 就是天然的分组依据。

坑二:滤波之后特征全钝了。现象:低通滤波后画图,波形是光滑了,但特征提取出来的标准差、极差普遍变小,分类器怎么调 F1 都上不去。原因:截止频率取得太低,0.5Hz 的 cutoff 把 SCR 的上升沿也滤掉了,剩下几乎只是缓慢的基线趋势。解决:把 cutoff 提到 1 到 2Hz,并且观察滤波前后的波形叠加图,确认上升沿还在。GSR 的决策权在时域波形形状,不在高频细节。

坑三:峰个数特征在换人之后失效。现象:用 find_peaks 提取 n_peaks,在训练集上很有区分度,新用户数据上却变成垃圾特征。原因:height 参数是绝对幅度阈值,而个体间皮肤电导基线差异极大,同一阈值对甲是噪声、对乙是有效峰。解决:先用 z-score 对每个受试者的整段信号做标准化,再对窗口提取峰特征,让阈值跟个体基线脱钩。

4.2 训练和评估环节的两个坑

坑四:类别不平衡让模型偷懒。现象:训练过程看起来正常,但分类报告里少数类 recall 只有 0.2,多数类 precision 虚高。原因:happy 样本远多于 normal_sad,决策树默认按多数类偏移。解决:两个手段二选一,给模型传 class_weight='balanced',或者对少数类做 SMOTE 过采样。注意 SMOTE 要在交叉验证内部做,否则会像坑一那样泄露。

坑五:预测脚本报维度错误或结果错乱。现象:新数据跑 predict 时报特征数量不匹配,或者不报错但预测结果明显不对。原因:新数据走了另一套特征提取逻辑,列顺序和训练时不一致。sklearn 的特征完全按位置解析,列名对不上也不报错,这是最隐蔽的坑。解决:把特征提取封装成同一个函数,训练和预测共用;保存模型时同时保存 feature_cols 列表,预测前核对列顺序。

5. 新数据预测:模型加载与情绪分类的完整流程

5.1 加载模型与新数据预处理

包里的模型文件(.pkl 或 .joblib)可以直接加载,不需要重新训练。新数据进来之后,必须走完全一样的预处理和特征提取管道:同一个滤波函数、同一个窗口长度、同一个特征列顺序。很多人以为模型文件是万能钥匙,其实 GSR 模型对输入分布极敏感,预处理不一致等于给自己埋雷。

import joblib model = joblib.load("emotion_model.joblib") feature_cols = joblib.load("feature_cols.joblib") # 训练时保存的特征列 raw = pd.read_csv("22.csv")["gsr"].values # 假设列名为 gsr processed = preprocess_gsr(raw, fs=20.0, cutoff=2.0) feat = extract_features(processed) row = np.array([[feat[c] for c in feature_cols]]) # 列顺序必须与训练一致

逻辑说明:我把特征列名和模型一起序列化保存,就是为了避免换环境之后列顺序漂移。这一步看起来多余,实际是踩过坑之后的后悔药。新数据的采样率如果和设备不一样,先重采样到训练时的 fs,再进预处理函数;列名对不上时先打印 CSV 的表头确认,别默认第一列是时间戳第二列是 GSR。

5.2 类别映射与置信度输出

预测结果不能只输出一个 0/1,要把概率输出出来。包里的情绪类别映射是 normal_sad 对应 0,happy_other 对应 1,这个映射在训练脚本里定义,预测时保持一致即可。

label_map = {0: "normal_sad", 1: "happy_other"} proba = model.predict_proba(row)[0] pred = int(np.argmax(proba)) print("预测情绪:", label_map[pred]) print("置信度: normal_sad=%.3f, happy_other=%.3f" % (proba[0], proba[1]))

这里把置信度打出来,是因为情绪识别的真实场景里,概率在 0.5 附近徘徊的样本根本不该硬判。包里带的文档对这部分有解释,答辩时可以重点强调「我们不止给出类别,还给出置信度」,这是评审愿意给高分的理由之一。

5.3 参数调整建议

预处理和模型里的核心参数,给出我调过的取值范围,方便直接照抄起步:

参数建议值说明
cutoff1.0~2.0Hz保住 SCR 上升沿,别低于 1Hz
窗口长度5~10 秒短了特征噪声大,长了抹平反应
窗口步长2~5 秒重叠越多数据扩增越多
find_peaks height0.05~0.5uS先 z-score 再调,个体差异大
n_estimators100~300超过 300 基本无收益
max_depth6~10防止在窗口噪声上过拟合
cv5 折 GroupKFold按文件分组,避免泄露

这套参数在包里自带数据上能复现评审分 95 以上的结果。换了环境或换了采集设备,第一件事是重画波形图确认预处理效果,而不是调模型参数,数据侧的问题在模型侧永远调不出来。

6. 进阶用法:用置信度阈值把硬分类改成三类输出

二分类模型调稳之后,下一步不是盲目扩类别,而是把置信度当成工具。我的做法是给 predict_proba 的输出加两个阈值:概率大于 0.7 判 happy_other,小于 0.3 判 normal_sad,中间一律归为「不确定」。这样做的直接好处是,误判率最高的中间地带被单独拎出来,在真实场景里比硬分类可靠得多。

def decide(proba, hi=0.7, lo=0.3): if proba[1] >= hi: return "happy_other" if proba[1] <= lo: return "normal_sad" return "uncertain"

把「不确定」的样本单独收集起来,攒够一批之后用增量方式重训模型,比每次从零开始省事。包里的详细文档其实也暗示了这条路径:数据文件按 session 分开存放,本身就是为增量实验设计的。如果你现在正需要一套能直接跑通的皮肤电情绪识别项目,这份带着模型、PPT 和全部数据资料的源码包能省下几天的折腾时间。

我自己的习惯是,每次调完阈值都打印一次概率分布直方图,看看阈值该往哪边偏,而不是拍脑袋定 0.7。以前我总想一步到位做四分类情绪识别,翻车几次之后才明白,皮肤电信号能支撑的可靠粒度就是唤醒度层面的二分类,先把置信度和不确定性管理好,比堆类别数有用得多。从那以后我每次接手生理信号数据,都强制自己先跑一遍预处理管道、看波形、打印分类报告,最后才碰模型参数。希望帮到你。

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

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

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

立即咨询