资讯动态

Python皮肤电信号情绪识别源码包:从数据预处理到模型评估

发布时间:2026/10/3 2:54:47 来源:尧图企业网站定制
简介面向情绪识别与人机交互研究场景这份以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_sad0、happy_other1后面所有脚本都用这套映射别在某个环节单独改。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, fs20.0, cutoff2.0, order4): 皮肤电信号预处理低通滤波 去趋势。 raw: 原始 GSR 一维数组单位 uS fs: 采样率Hz包里没写的话要先从数据推断 # 低通滤波2Hz 以上基本是噪声保住 SCR 上升沿即可 b, a butter(order, cutoff / (fs / 2), btypelow) 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, fs20.0, peak_thresh0.1): 把一个滑动窗口压成一行特征字典。 peaks, _ find_peaks(window, heightpeak_thresh, distanceint(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_estimators200, max_depth8, random_state42) ) scores cross_val_score(pipe, X, y, cv5, scoringf1_macro) print(5-fold F1: %.3f ± %.3f % (scores.mean(), scores.std()))参数说明scoring 用 f1_macro 而不是 accuracy是因为 happy 和 normal_sad 的样本数通常不平衡准确率会被多数类带偏cv5 对这个量级是合适的样本太少的话改成 StratifiedKFold(n_splits5) 保证每折类别比例一致。n_estimators200、max_depth8 是控制过拟合的保守组合树再多效果不会更好只会更慢。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, cv5) 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_weightbalanced或者对少数类做 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, fs20.0, cutoff2.0) feat extract_features(processed) row np.array([[feat[c] for c in feature_cols]]) # 列顺序必须与训练一致逻辑说明我把特征列名和模型一起序列化保存就是为了避免换环境之后列顺序漂移。这一步看起来多余实际是踩过坑之后的后悔药。新数据的采样率如果和设备不一样先重采样到训练时的 fs再进预处理函数列名对不上时先打印 CSV 的表头确认别默认第一列是时间戳第二列是 GSR。5.2 类别映射与置信度输出预测结果不能只输出一个 0/1要把概率输出出来。包里的情绪类别映射是 normal_sad 对应 0happy_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, hi0.7, lo0.3): if proba[1] hi: return happy_other if proba[1] lo: return normal_sad return uncertain把「不确定」的样本单独收集起来攒够一批之后用增量方式重训模型比每次从零开始省事。包里的详细文档其实也暗示了这条路径数据文件按 session 分开存放本身就是为增量实验设计的。如果你现在正需要一套能直接跑通的皮肤电情绪识别项目这份带着模型、PPT 和全部数据资料的源码包能省下几天的折腾时间。我自己的习惯是每次调完阈值都打印一次概率分布直方图看看阈值该往哪边偏而不是拍脑袋定 0.7。以前我总想一步到位做四分类情绪识别翻车几次之后才明白皮肤电信号能支撑的可靠粒度就是唤醒度层面的二分类先把置信度和不确定性管理好比堆类别数有用得多。从那以后我每次接手生理信号数据都强制自己先跑一遍预处理管道、看波形、打印分类报告最后才碰模型参数。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑