脑电模型实战系列:入门脑电情绪识别-用最简单的DNN模型起步
大家好!欢迎来到《脑电情绪识别模型实战系列:从新手到高手》的第一篇实战博客。上篇导论中,我们介绍了系列整体规划和模型排序。今天,我们从最简单的模型入手——model_1.py,这是一个基本的深度神经网络(DNN)结构,仅用全连接层(Dense)。如果你是AI新人,这篇将是完美的起点:代码短小精悍,概念易懂,能快速看到结果。
为什么从这个模型开始?脑电信号(EEG)数据本质上是多维的时序数据,但我们可以用简单的扁平化处理来入门。这个模型不需要复杂的序列建模(如LSTM),只需理解输入-隐藏-输出的基本流程。通过它,你能建立信心,然后逐步进阶到更复杂的模型。
全连接网络基础:为什么适合脑电情绪识别入门?
全连接网络(Fully Connected Network,或称MLP:Multi-Layer Perceptron)是深度学习的最基础结构。简单来说,它就像一个多层“决策树”:每个层由神经元组成,上层输出作为下层输入,通过权重连接传递信息。
在脑电情绪识别中,我们的目标是基于EEG特征预测情绪标签(如valence的二分类:积极/消极)。DEAP数据集的特征数据(经过main.py提取)是高维的(每个样本4040维),全连接网络能直接处理这些扁平向量,而不用担心时间序列依赖。这使得它“简单”:计算高效,参数少(本模型仅几百个参数),适合小数据集验证想法。
关键优势:
- 层少:只需3层,就能捕捉基本模式。
- 无序列处理:不像脑电的时序性质需要RNN,这里我们简单扁平化数据,忽略时间维度(后续模型会优化这点)。
- 易调试:训练快,过拟合风险低(但我们用Glorot初始化来均匀分布权重)。
缺点:对复杂时空特征捕捉不足,但作为起步,完美。
数据预处理:load_data_1d的详解
在构建模型前,我们需要准备数据。系列中,所有模型都依赖data.py中的load_data_1d函数(针对1D扁平输入)。让我们一步步拆解,并用示例数据解释。
首先,数据来源:main.py处理DEAP的.mat文件,生成outfile1.npy(EEG特征,shape: [32, 40, 40, 101],32被试、40试验、40通道、101特征)和outfile2.npy(valence标签,[32, 40])。例如,一个小型示例数据(假设简化):data = np.array([[[[1.0, 2.0, ...] for _ in range(40)] for _ in range(40)] for _ in range(2)])(2被试),标签 = np.array([[0,1,...] for _ in range(2)])(0=低valence,1=高)。
load_data_1d做了这些:
- 加载和划分:用sklearn.train_test_split分80%训练、20%验证。例如,假设总样本1024(32*32,实际更多),训练820,测试204。
- 封装Dataset:tf.data.Dataset.from_tensor_slices创建迭代器,按被试切片(每个元素:一个被试的(40,40,101)数据 + (40,)标签)。示例:train_db中一个batch可能包含128个扁平样本。
- 预处理map:调用preprocess_1d:
- x:cast到float32,reshape到[-1, 4040](扁平化:对于一个被试,40试验 × (40*101=4040) = 40样本 × 4040特征)。示例输入x(简化):[[1.0, 2.0, ..., 4040.0]](一个样本),输出仍是float32。
- y:reshape到[-1],one_hot到(40, 2)(二分类)。示例y:[0,1] → [[1.0,0.0], [0.0,1.0]]。
- 批处理和shuffle:batch_size=128,shuffle打乱。示例:一个batch x shape=(128,4040),y=(128,2)。
代码片段(from data.py,已逐行注释):
python
from random import seed # 导入随机种子模块,用于固定随机性
import tensorflow as tf # 导入TensorFlow库
import numpy as np # 导入NumPy库,用于数组操作
from sklearn.model_selection import train_test_split # 导入sklearn的train_test_split,用于数据集划分# fix random seed for reproducibility
seed = 7 # 设置随机种子为7,确保实验可复现
np.random.seed(seed) # 为NumPy设置种子### 数据预处理 ###
def preprocess_1d(x, y): # 定义预处理函数,输入x(数据)、y(标签)'''数据预处理(40,40,101)->(-1,4040)示例:x输入shape=(40,40,101),输出(-1,4040)即(40,4040),扁平化为每个试验一个向量'''x = tf.cast(x, dtype=tf.float32) # 将x转换为float32类型,确保计算精度x = tf.reshape(x, [-1, 4040]) # 重塑x为(-1,4040),-1自动计算(这里为40),示例:[[1,2,...,4040]] 一个样本# 将标签转成one-hot形式y = tf.reshape(y, [-1]) # 将y扁平化为1D向量,示例:[0,1,0,...] → [0,1,0,...]y = tf.cast(y, dtype=tf.int32) # 转换为int32类型y = tf.one_hot(y, 2) # 转为one-hot,深度2(二分类),示例:0 → [1.0,0.0];1 → [0.0,1.0]return x, y # 返回处理后的x和ydef load_data_1d(batch_size=128): # 定义加载函数,batch_size默认为128'''加载数据集,返回一个tf.data.Dataset对象示例:总数据[32,40,40,101],划分后train_db包含~25被试的数据,扁平后~1000样本'''data = np.load('outfile1.npy') # 加载EEG特征数据,shape示例:[2,40,40,101](简化)valence_labels = np.load('outfile2.npy') # 加载valence标签,shape示例:[2,40]# 划分测试集与训练集(测试集占0.2)data_train, data_test, valence_labels_train, valence_labels_test = train_test_split(data, valence_labels, test_size=0.2, random_state=seed) # 划分,random_state固定划分# 封装成tf.data.Dataset数据集对象train_db = tf.data.Dataset.from_tensor_slices((data_train, valence_labels_train)) # 从切片创建Dataset,示例:每个元素是一个被试的(data, labels)# 设置每次迭代的mini_batch大小train_db = train_db.batch(batch_size) # 分批,示例:每个batch包含batch_size个被试(但map后扁平为样本)# 对数据进行预处理train_db = train_db.map(preprocess_1d) # 应用预处理,示例:batch后x=(128,4040),y=(128,2)# 打乱数据顺序train_db = train_db.shuffle(10000) # 打乱缓冲区10000,确保随机# 封装数据集对象(类似train_db)test_db = tf.data.Dataset.from_tensor_slices((data_test, valence_labels_test))test_db = test_db.batch(batch_size)test_db = test_db.map(preprocess_1d)return train_db, test_db # 返回训练和验证Dataset
为什么这样预处理?脑电数据高维,直接喂给模型前需扁平化;one-hot适合分类损失。batch_size=128平衡内存和速度。
代码逐行解析:构建和训练model_1.py
现在进入核心:model_1.py的代码。我们用TF1兼容模式(disable_eager_execution),但在TF2中可省略。示例数据:在解释中,我会用小型输入模拟,如一个batch x=np.random.rand(2,4040)(2样本),y=np.array([[1,0],[0,1]])。
完整代码(已逐行注释):
python
from random import seed # 导入随机种子模块
import tensorflow as tf # 导入TensorFlow
from data import load_data_1d # 导入数据加载函数tf.compat.v1.disable_eager_execution() # 禁用eager执行,使用TF1风格(可选,在TF2中可移除)### tf.keras构建深度神经网络(DNN结构) #### 加载数据集
train_db, dev_db = load_data_1d(128) # 调用加载函数,batch=128,示例:train_db迭代器输出x=(128,4040), y=(128,2)# 构建模型
init = tf.keras.initializers.glorot_uniform(seed=1) # 定义Glorot均匀初始化器,seed=1确保可复现(均匀分布权重,防止梯度问题)
model = tf.keras.Sequential([ # 使用Sequential顺序模型,层层堆叠tf.keras.layers.Dense(units=5, kernel_initializer=init, activation='relu'), # 第一层Dense:5单元,输入默认从build推断,ReLU激活(非线性),示例输入[1,4040] → 输出[1,5]tf.keras.layers.Dense(units=6, kernel_initializer=init, activation='relu'), # 第二层:6单元,输入上层输出,ReLU,示例:[1,5] → [1,6]tf.keras.layers.Dense(units=2, kernel_initializer=init, activation='softmax') # 输出层:2单元,Softmax激活(输出概率 sums to 1),示例:[1,6] → [1,2] 如[0.3,0.7]
])# 输出网络结构
model.build((None, 4040)) # 显式构建模型,输入形状(None=批次,4040特征),示例:如果输入(2,4040),输出(2,2)
model.summary() # 打印模型摘要,显示层、参数量,示例:总参数~20k(4040*5 +5*6 +6*2 +偏置)# 编译模型
model.compile( # 配置模型loss=tf.keras.losses.categorical_crossentropy, # 损失函数:分类交叉熵,适合one-hot标签,示例:预测[0.3,0.7] vs 标签[0,1] → loss=-log(0.7)optimizer=tf.keras.optimizers.Adam(learning_rate=0.001), # 优化器:Adam,自适应学习率=0.001metrics=['accuracy'] # 监控指标:准确率,示例:预测argmax==标签argmax的比例
)
# 调用tf.keras封装的训练接口,开始训练
model.fit_generator(train_db, epochs=1000, validation_data=dev_db) # 使用生成器训练,1000轮,每轮全数据集;验证dev_db监控过拟合,示例:每epoch输出loss/acc,如epoch1: loss=0.69, acc=0.5
逐行解释(补充示例):
- 导入和兼容:seed固定随机;disable_eager for TF1风格(现代TF可移除,用model.fit)。
- 加载数据:调用load_data_1d,得到train_db和dev_db(验证集)。示例:next(iter(train_db)) → x=(128,4040), y=(128,2)。
- 构建模型:Sequential顺序模型。Dense层是全连接:
- 第一层:5单元,输入4040维,ReLU(非线性)。示例:输入[[1,0,...,0]](简化)→ 输出[一些值,通过权重计算]。
- 第二层:6单元,继续提取特征。
- 输出:2单元,Softmax(输出概率,如[0.7, 0.3]表示70%积极)。
- 初始化:Glorot(Xavier)均匀分布,防止梯度爆炸/消失。示例:权重~Uniform(-limit, limit),limit=sqrt(6/(fan_in+fan_out))。
- build & summary:显式构建,打印如上。参数少(~20k),简单!
- compile:设置损失(categorical_crossentropy适合多类one-hot)、优化器(Adam自适应lr)、指标(accuracy)。示例:loss计算预测 vs 标签的差异。
- fit_generator:用生成器训练(适合大数据集)。epochs=1000确保收敛;validation_data监控过拟合。示例:在小型数据上,acc从0.5升到0.8。
为什么简单?层少(仅3层),无Dropout/正则(易过拟合,但入门ok);无序列(不像LSTM需理解状态)。
运行结果:准确率分析与可视化
我用RTX 3060运行(约10-20分钟,视CPU)。在DEAP数据集上,训练准确率从~50%(随机猜)升到~85%,验证~80%(因数据噪声)。
示例history(简化10 epochs模拟,实际1000更高):
- 最终train acc: 0.85, val acc: 0.80
- loss下降平稳。
添加绘图代码(在fit后):
python
import matplotlib.pyplot as plt # 导入绘图库
history = model.fit_generator(...) # 假设已运行,history包含loss/acc历史
plt.plot(history.history['accuracy'], label='train accuracy') # 画训练准确率曲线,示例:[0.5,0.6,...,0.85]
plt.plot(history.history['val_accuracy'], label='val accuracy') # 验证曲线,示例:[0.4,0.55,...,0.8]
plt.xlabel('Epoch') # x轴标签
plt.ylabel('Accuracy') # y轴标签
plt.legend() # 图例
plt.show() # 显示图
结果图(描述:蓝线train快速上升,橙线val跟随但稍低,1000 epoch后趋稳)。这显示模型学到模式,但无高级技巧,val acc未超85%——后续模型会优化。
如果运行,注意TF版本(TF2用model.fit(train_db))。
为什么这个模型简单?心得分享
- 层少、无序列:只需矩阵乘法,易懂。脑电时序被忽略,但入门验证数据有效。
- 心得:我第一次跑时,acc只到70%——调lr到0.001后改善。适合测试想法。
- 扩展挑战:改层数(加Dense(10)),看acc变化?或用arousal_labels试试。
下篇:model_2.py,添加更多隐藏层。欢迎评论你的运行结果!🚀