基于皮肤电信号EDA的情绪识别实战:从CSV数据到分类模型

发布时间:2026/10/18 21:43:23
基于皮肤电信号EDA的情绪识别实战:从CSV数据到分类模型 简介本资源是一套基于Python与皮肤电信号EDA实现情绪识别的完整项目源码面向心理学、人机交互及智能系统方向的研究者与开发者帮助其快速理解并复现从生理信号到情绪分类的全流程。压缩包共102个文件以78个csv数据文件、7个py脚本、6个xml配置、4个m文件及mat、pptx等为主整体约1.92MB涵盖数据采集、信号预处理、特征提取、情绪分类与结果评估等模块并涉及Numpy、Pandas、Scipy、Matplotlib及Scikit-learn等常用库。目前已有815人学习下载。通过研读源码读者可掌握EDA信号的去噪与特征工程方法理解非平稳性、个体差异等实际问题的处理思路并借助实验案例对照验证算法效果适合作为情绪识别入门与项目实战的参考素材。1. 情绪识别项目上手从一堆 CSV 到能跑通的 EDA 分类器第一次拿到这个项目压缩包解压后看到的是一堆命名随意的 CSVhappy_other.csv、normal_sad.csv、1.csv、2.csv、18.csv……没有 README没有 requirements没有目录分层。很多人到这一步就卡住了——数据到底怎么对应标签哪个文件是训练集其实这正是生理信号类项目的常态数据采集阶段往往是先录信号、后补标签文件名本身就是标签线索。这个项目用 Python 把皮肤电信号EDA从原始波形一路处理到情绪分类覆盖采集、预处理、特征提取、分类、评估五个环节适合做心理生理计算、人机交互方向的学生和工程师拿来当练手底稿也适合想快速验证 EDA 特征有效性的研究者。它不依赖昂贵设备核心逻辑全在代码里跑通之后你能清楚看到一段皮肤电波形是怎么变成「开心 / 平静 / 悲伤」这类标签的。2. 项目结构与数据组织先搞清楚每个 CSV 在链路里的位置2.1 目录分层与模块职责这个项目没有用复杂的工程脚手架属于典型的「脚本 数据」结构。按常见做法解压后大致会形成下面这样的布局我按功能把文件归了类方便你对照自己手里的压缩包eda_emotion/ ├── data/ │ ├── raw/ # 原始采集数据 │ │ ├── happy_other.csv │ │ ├── normal_sad.csv │ │ ├── 1.csv 2.csv 9.csv │ │ ├── 18.csv 19.csv 20.csv │ │ └── 21.csv 22.csv │ └── processed/ # 预处理后的中间数据 ├── src/ │ ├── preprocess.py # 滤波、去基线、分段 │ ├── features.py # 时域/频域特征提取 │ ├── train.py # 模型训练与交叉验证 │ └── evaluate.py # 混淆矩阵、指标输出 ├── models/ # 保存的模型文件 └── requirements.txtdata/raw里那些数字命名的 CSV 通常是按被试编号或采集批次命名的happy_other.csv和normal_sad.csv则直接带了情绪标签。这里有个血泪经验不要假设所有 CSV 的列结构一致采集设备不同、导出软件不同列名可能是EDA、GSR、conductance甚至中文。第一步永远是先统一列名和采样率。2.2 数据加载与标签映射标签怎么来文件名就是最直接的来源。下面这段代码是我一般会先写的「数据盘点」脚本跑一遍就能知道每个文件多少行、采样率大概多少、有没有缺失import os import pandas as pd RAW_DIR data/raw # 文件名到情绪标签的映射按项目实际命名调整 LABEL_MAP { happy_other.csv: happy, normal_sad.csv: sad, } def inspect_csv(path): df pd.read_csv(path) # 统一列名把常见 EDA 列名归一到 conductance rename_dict {} for col in df.columns: low col.lower() if eda in low or gsr in low or conduct in low: rename_dict[col] conductance elif time in low or timestamp in low: rename_dict[col] time df df.rename(columnsrename_dict) return df for fname in os.listdir(RAW_DIR): if not fname.endswith(.csv): continue df inspect_csv(os.path.join(RAW_DIR, fname)) label LABEL_MAP.get(fname, unknown) print(f{fname:20s} rows{len(df):6d} cols{list(df.columns)} label{label})这段代码做了三件事遍历原始目录、把不同来源的列名统一成conductance和time、根据文件名推断标签。LABEL_MAP是唯一需要你手动确认的地方——如果数字命名的 CSV 也有对应标签表就把它补进这个字典。跑完这一步你会得到一张「文件 → 行数 → 列名 → 标签」的清单后面所有处理都基于这份清单避免中途发现某个文件列名对不上导致整个 pipeline 崩掉。注意如果某个 CSV 只有一列数据没有时间列说明采样率是固定的需要在加载时手动补一个时间轴否则后续按秒切窗会出错。3. 信号预处理与特征提取EDA 去噪、分解与特征工程3.1 滤波与去基线皮肤电信号很「娇气」采集时稍微动一下、呼吸重一点都会引入伪迹。原始 EDA 通常包含两部分缓慢变化的 tonic 成分基础皮电水平和快速变化的 phasic 成分皮肤电反应SCR。情绪识别真正有用的是 phasic 部分所以预处理的核心是「去趋势 分离」。常见做法是用 Butterworth 低通滤波去掉高频噪声再用平滑先验或高通滤波去掉 tonic 漂移import numpy as np from scipy.signal import butter, filtfilt def bandpass_filter(signal, fs4.0, low0.05, high1.0, order4): 对 EDA 信号做带通滤波保留 0.05-1.0 Hz 的 SCR 频段 nyq 0.5 * fs b, a butter(order, [low / nyq, high / nyq], btypeband) # filtfilt 零相位滤波避免波形时移影响后续特征对齐 return filtfilt(b, a, signal) def decompose_eda(signal, fs4.0, cutoff0.05): 用低通滤波估计 tonic 成分相减得到 phasic nyq 0.5 * fs b, a butter(2, cutoff / nyq, btypelow) tonic filtfilt(b, a, signal) phasic signal - tonic return tonic, phasicbandpass_filter里的fs是采样率EDA 常用 4Hz 到 32Hz如果你不知道自己的数据采样率可以用时间列相邻两点的差值倒数估算。low和high控制保留频段0.05Hz 以下基本是漂移1Hz 以上多是运动伪迹。decompose_eda用低通估计 tonic再用原始信号减掉它得到 phasic这是最轻量的分解方式比 cvxEDA 那种凸优化方法快很多适合快速迭代。3.2 时域与频域特征提取特征决定了分类器的上限。EDA 情绪识别里常用的特征分三类统计特征均值、标准差、偏度、峰度、SCR 形态特征峰值个数、上升时间、幅度、频域特征功率谱密度。下面这个特征提取函数把一段 phasic 信号压成一个特征向量from scipy.stats import skew, kurtosis from scipy.signal import find_peaks def extract_features(phasic, fs4.0): feats {} # 统计特征 feats[mean] np.mean(phasic) feats[std] np.std(phasic) feats[skew] skew(phasic) feats[kurt] kurtosis(phasic) # SCR 峰值特征幅度阈值设为 0.01 微西门子按设备量纲调整 peaks, props find_peaks(phasic, height0.01, distanceint(fs * 1.0)) feats[scr_count] len(peaks) feats[scr_amp_mean] np.mean(props[peak_heights]) if len(peaks) else 0.0 # 频域特征0.05-0.5 Hz 频段能量占比 freqs np.fft.rfftfreq(len(phasic), d1.0 / fs) psd np.abs(np.fft.rfft(phasic)) ** 2 band (freqs 0.05) (freqs 0.5) feats[psd_ratio] psd[band].sum() / (psd.sum() 1e-8) return featsfind_peaks的height和distance是两个关键参数height决定多小的波动算一次 SCR太小会把噪声当峰值太大会漏掉弱反应distance限制两个峰值的最小间隔一般设 1 秒因为生理上 SCR 不会比这更快。psd_ratio用 FFT 粗略估计频段能量占比比 Welch 法快适合数据量大的场景。提取完所有样本的特征后你会得到一个(n_samples, n_features)的矩阵直接喂给 sklearn 就行。提示个体差异是 EDA 最大的坑。同一种情绪不同人的皮电幅度可能差一个数量级。常见做法是对每个被试做 z-score 标准化再合并训练否则模型学到的可能是「谁」而不是「什么情绪」。4. 分类模型训练与评估从特征矩阵到情绪标签4.1 模型选择与交叉验证特征矩阵有了标签也有了接下来是分类。这个项目规模不大用 SVM 或随机森林就够没必要上深度学习。SVM 在小样本高维特征上表现稳定随机森林则能直接输出特征重要性方便你判断哪些特征真正有用from sklearn.svm import SVC from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import cross_val_score, StratifiedKFold from sklearn.preprocessing import StandardScaler from sklearn.pipeline import Pipeline def build_model(kindsvm): if kind svm: clf SVC(kernelrbf, C1.0, gammascale, probabilityTrue) else: clf RandomForestClassifier(n_estimators200, max_depth6, random_state42) # 标准化必须放进 Pipeline避免交叉验证时数据泄漏 return Pipeline([(scaler, StandardScaler()), (clf, clf)]) def evaluate(X, y): cv StratifiedKFold(n_splits5, shuffleTrue, random_state42) model build_model(svm) scores cross_val_score(model, X, y, cvcv, scoringf1_macro) print(fF1 macro: {scores.mean():.3f} ± {scores.std():.3f}) return scores这里有个容易翻车的地方标准化一定要放进Pipeline不能先对全量数据fit_transform再交叉验证。否则验证折的统计量泄漏到了训练折准确率会虚高上线就露馅。StratifiedKFold保证每折里各类情绪比例一致样本少的时候尤其重要。f1_macro比准确率更适合情绪识别因为「平静」样本往往远多于「开心」和「悲伤」用准确率会被多数类带偏。4.2 结果评估与混淆矩阵训练完不能只看一个数字要看混淆矩阵搞清楚模型到底把哪种情绪认错了from sklearn.metrics import confusion_matrix, classification_report from sklearn.model_selection import train_test_split def detailed_report(X, y): X_tr, X_te, y_tr, y_te train_test_split( X, y, test_size0.3, stratifyy, random_state42 ) model build_model(svm) model.fit(X_tr, y_tr) y_pred model.predict(X_te) print(classification_report(y_te, y_pred)) print(confusion_matrix(y_te, y_pred)) return modelclassification_report会给出每类的 precision、recall、f1confusion_matrix则直观显示错分方向。如果发现「悲伤」经常被认成「平静」大概率是这两类的 phasic 特征区分度不够需要回到特征工程补 SCR 上升时间、恢复时间这类形态特征。如果所有类都表现平平先检查预处理是不是把有效信号也滤掉了——把bandpass_filter的high从 1.0 放宽到 2.0 再试一次很多时候问题就出在这里。5. 避坑与常见问题排查EDA 情绪识别里最容易翻车的几件事5.1 采样率不一致导致时间窗错位现象训练时准确率正常换一批数据后断崖式下跌。原因不同 CSV 的采样率不同代码里按固定点数切窗实际时间长度不一致特征量纲全乱。解决加载时先估算fs 1 / np.median(np.diff(time))统一重采样到 4Hz 再切窗别用固定行数。5.2 标签泄漏文件名和特征里的隐藏信息现象交叉验证 F1 高得离谱换测试集就崩。原因特征里混入了被试编号、采集批次这类元信息模型直接背下了标签。解决特征矩阵只保留生理特征列被试编号单独存一份用于分组交叉验证GroupKFold别让它进模型。5.3 基线漂移没去干净现象同一段情绪前半段和后半段预测结果相反。原因tonic 成分缓慢漂移phasic 里还残留趋势。解决在decompose_eda之后再做一次去趋势或者把窗长缩短到 10 秒以内减少漂移影响。5.4 类别极度不平衡现象模型把所有样本都预测成「平静」准确率还有 70%。原因normal_sad.csv这类文件样本量远大于其他类。解决用class_weightbalanced或者对少数类做 SMOTE 过采样评估指标换成f1_macro。5.5 峰值检测参数一刀切现象有的人数据一个 SCR 都检不出有的人满屏都是峰。原因find_peaks的height是绝对值没考虑个体幅度差异。解决先对每个被试的 phasic 做 z-score再用相对阈值如height0.5检测或者用prominence替代height。6. 二次开发与改造思路把 demo 变成能用的模块跑通之后这个项目最值得改的地方有三个。第一是特征工程现在的统计特征太粗可以补上 SCR 的上升时间、半恢复时间、峰值间期变异性这些形态特征在情绪唤醒度区分上比均值标准差有效得多。第二是模型层面如果样本量能到几千条可以试试把特征序列直接喂给 1D-CNN 或 LSTM让网络自己学时序模式但前提是做好被试分组划分否则个体泄漏会让指标虚高。第三是工程化把preprocess.py、features.py、train.py串成一个sklearn的Pipeline用joblib保存对外只暴露predict(eda_signal)一个接口这样别人拿到的就是一个能直接调用的情绪识别模块而不是一堆脚本。改造时我一般会先写一个最小验证脚本固定随机种子跑三遍确认指标稳定再动特征和模型。下面这个表格是我常用的改造优先级参考改造方向预期收益风险建议顺序补 SCR 形态特征中高低1被试内标准化高低2换时序模型高中3增加传感器融合高高4最后说个习惯每次改完特征或模型我都会把混淆矩阵打出来看一遍确认错分方向没有变奇怪。有一次我加了一堆频域特征F1 涨了 5 个点结果混淆矩阵显示模型把「开心」和「悲伤」全认反了——原来是标签映射写错了。从那以后我每次动标签相关代码都强制走一遍「打印前 10 个样本的标签和文件名」的检查。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询