2023/4/5 -4/17 脑机接口学习内容一览:
这一篇文章主要对DEAP数据集转化为python可以处理的格式,并且进一步使用LSTM网络进行分类工作。
一、数据集分析
详情见于官网:DEAPdataset
若想使用数据集可自行在官网申请下载。
以下是对该数据集的简要介绍:
文件夹界面:
mat文件的解释如下:
dataset key_word:
(1)labels
shape (40, 4)
解释:
labels用于标记被试看到每一个视频时的状态
第二维度中的四个数据代表四个评价标准,即arousal(唤醒度), valence(愉悦度), dominance(支配度), like(喜爱度)
(2)data
shape (40, 40, 8064)
解释:
第一个维度代表40段观看视频产生的脑电数据
第二个维度代表每段数据存在40个通道
第三个维度为采样点,长度在63s左右
根据官方网站得采样率为128hz
数据已经经过预处理,预处理手段如下:
1、降低采样率至128hz;
2、移除眼动伪迹;
3、设置平均参考;
4、重新排序脑电通道;
5、数据被分割为60秒试验和3秒的基线;
6、试验从演示顺序重新排序为视频(Experiment_id)顺序;
7、应用了4至45hz的带通滤波。
由于第33至第40通道的数据与EEG信号用不上,稍后在预处理阶段移除,只保留前32个通道进行处理。
二、了解情感轮(emotion wheel)
在每个labels中存在四个数值,即arousal(唤醒度), valence(愉悦度), dominance(支配度), like(喜爱度),前三个指标构成了情感轮。对于情感轮我不是很了解,故下面借助chatgpt来解释:
1、如何分析与Valence,Arousal,Dominance相关的情感轮?
情感轮通常会将情感分为三个维度:Valence,Arousal,Dominance(VAD)。这三个维度可以帮助人们更好地理解和表达情感的复杂性。
Valence:Valence表示情感的积极或消极性。积极的情感通常被认为是愉悦、高兴、爱和满意,而消极的情感通常被认为是悲伤、恐惧、愤怒和厌恶。在情感轮中,通常将情感分为“正向”和“负向”,从左到右分别表示积极和消极的情感。
Arousal:Arousal表示情感的强度或活跃程度。高度的情感通常表示兴奋、兴趣和激动,而低度的情感通常表示冷静、平静和沉闷。在情感轮中,从中心向外的距离表示情感的强度,中心表示低度的情感,外围表示高度的情感。
Dominance:Dominance表示情感的控制程度。高度的情感控制通常表示自信、主动和支配,而低度的情感控制通常表示无助、被动和受控。
在情感轮中,从底部到顶部的距离表示情感的控制程度,底部表示低度的控制,顶部表示高度的控制。 通过将情感分为这三个维度,情感轮可以帮助人们更好地理解和表达情感的复杂性。例如,一个积极的情感可以是高度的兴奋和控制(如自信和决心),也可以是低度的兴奋和控制(如放松和满足)。对于情感分析,这些维度可以帮助人们更好地识别和分类情感,并为情感分类提供更准确和精细的方法。
2、将这三个指标量化为1到9这几个分级后,如何通过分级判断情感状态,举出具体例子
通过将Valence、Arousal、Dominance这三个指标量化为1到9这几个分级,情感状态可以根据不同的数值组合来进行判断。以下是一些具体例子:
1. Valence=1,Arousal=1,Dominance=1:这表示情感是消极、低度兴奋和控制。这可能代表着一种沮丧和无助的情感状态。
2. Valence=9,Arousal=9,Dominance=1:这表示情感是积极、高度兴奋和低度控制。这可能代表着一种兴奋和激动的情感状态,但无法完全掌控。
3. Valence=5,Arousal=1,Dominance=9:这表示情感是中性的、低度兴奋和高度控制。这可能代表着一种平静和冷静的情感状态,但同时也感到有掌控力。
4. Valence=3,Arousal=9,Dominance=3:这表示情感是消极的、高度兴奋和中度控制。这可能代表着一种情绪激动和不安的情感状态。
通过这些示例,可以看出Valence、Arousal、Dominance这三个指标的不同数值组合可以代表不同的情感状态。将这些指标量化为1到9这些分级,可以更加精细地表达和描述情感状态。
3、like指标的作用
在情感轮中,Valence、Arousal和Dominance用来描述情感的基本属性,而Like则是一种情感的表达方式。Like可以表示喜欢、欣赏、支持、赞赏等情感,通常与积极的情感相关。 在分析与Valence、Arousal、Dominance、Like相关的情感轮时,可以考虑如下几点:
1. Like与Valence的关系:Like通常与积极的情感相关,因此高Valence的情感通常更容易引起Like的表达。例如,一个高度积极的情感,如喜悦或爱,更容易引起Like的表达。
2. Like与Arousal的关系:Like通常与低度兴奋的情感相关,因为低度兴奋的情感更容易引起人们的喜欢和赞赏。但是,在某些情况下,高度兴奋的情感也可以引起Like的表达,例如在一场比赛或者一个演出中,高度兴奋的情感也可以引起人们的喜欢和赞赏。
3. Like与Dominance的关系:Like通常与低度控制的情感相关,因为低度控制的情感更容易引起人们的喜欢和赞赏。但是,在某些情况下,高度控制的情感也可以引起Like的表达,例如在领导和支配的情境中,高度控制的情感也可以引起人们的喜欢和赞赏。
4. Like与情感的表达方式:Like是一种情感的表达方式,它可以通过语言、行为、肢体语言等方式表达出来。在情感轮中,Like通常在外圈表示,因为它是一种情感的表达方式,而不是情感本身。
通过考虑这些因素,可以更好地理解和分析与Valence、Arousal、Dominance、Like相关的情感轮。例如,一个积极的情感,如喜悦,通常具有高Valence和低Arousal,同时可能具有低度控制和Like的表达。
4、EEG信号中代表arousal(唤醒度), valence(愉悦度), dominance(支配度), like(喜爱度)的特征有哪些?
EEG信号中代表arousal(唤醒度), valence(愉悦度), dominance(支配度), like(喜爱度)的特征有以下几种:
1. 频谱特征:EEG信号的频谱特征可以反映出脑电活动的能量分布和频率特征。例如,alpha波的增加与愉悦度的增加有关系,而beta波的增加则与唤醒度的增加有关系。
2. 时域特征:EEG信号的时域特征可以反映出脑电活动的振幅、周期和相位。例如,P300波的振幅与注意力的集中程度有关系。
3. 熵特征:EEG信号的熵特征可以反映出脑电活动的复杂程度和不规则程度。例如,样本熵(Sample Entropy)可以反映出脑电活动的复杂程度和唤醒度的变化。
4. 聚类特征:EEG信号的聚类特征可以反映出脑电活动的空间分布和相关性。例如,相干性(Coherence)可以反映出不同脑区之间的相关性和支配关系。
5. 事件相关电位(ERP):ERP是一种脑电信号的特殊形式,是由于特定的刺激或任务产生的脑电响应。例如,N170波可以反映出面孔识别的加工过程,而P300波可以反映出注意力的集中程度和识别的准确性。
6. 睡眠特征:睡眠是一种自然的状态,可以通过EEG信号的睡眠特征来反映。例如,睡眠深度和REM睡眠可以通过EEG的频谱和时域特征来判断。 需要注意的是,不同的特征可能对应着不同的脑区和神经环路,因此在分析和识别中需要考虑到多种特征的综合作用。
根据PAD三维情感模型理论,我们可能将进行一个将情感进行16(?)分类的任务。
根据arousal(唤醒度), valence(愉悦度), dominance(支配度), like(喜爱度)这四个维度,可以将人的情绪分为以下16种:
1. 高愉悦、高唤醒、高支配、高喜爱:这种情绪状态通常被称为“狂喜”、“兴奋”等。
2. 高愉悦、高唤醒、高支配、低喜爱:这种情绪状态通常被称为“自恋”、“傲慢”等。
3. 高愉悦、高唤醒、低支配、高喜爱:这种情绪状态通常被称为“热情”、“激动”等。
4. 高愉悦、高唤醒、低支配、低喜爱:这种情绪状态通常被称为“激昂”、“冷漠”等。
5. 高愉悦、低唤醒、高支配、高喜爱:这种情绪状态通常被称为“自豪”、“自信”等。
6. 高愉悦、低唤醒、高支配、低喜爱:这种情绪状态通常被称为“得意”、“傲慢”等。
7. 高愉悦、低唤醒、低支配、高喜爱:这种情绪状态通常被称为“放松”、“愉悦”等。
8. 高愉悦、低唤醒、低支配、低喜爱:这种情绪状态通常被称为“宁静”、“平静”等。
9. 低愉悦、高唤醒、高支配、高喜爱:这种情绪状态通常被称为“愤怒”、“恼怒”等。
10. 低愉悦、高唤醒、高支配、低喜爱:这种情绪状态通常被称为“愤慨”、“不满”等。
11. 低愉悦、高唤醒、低支配、高喜爱:这种情绪状态通常被称为“焦虑”、“紧张”等。
12. 低愉悦、高唤醒、低支配、低喜爱:这种情绪状态通常被称为“悲伤”、“失落”等。
13. 低愉悦、低唤醒、高支配、高喜爱:这种情绪状态通常被称为“嫉妒”、“惊讶”等。
14. 低愉悦、低唤醒、高支配、低喜爱:这种情绪状态通常被称为“羞愧”、“尴尬”等。
15. 低愉悦、低唤醒、低支配、高喜爱:这种情绪状态通常被称为“温柔”、“感动”等。
16. 低愉悦、低唤醒、低支配、低喜爱:这种情绪状态通常被称为“沮丧”、“无聊”等。
需要注意的是,这些情绪状态的划分也不是非常明确,而且在不同的文化和语境中可能存在差异。因此,在具体应用中,需要根据实际情况进行调整和适配。
三、LSTM神经网络
对于脑电信号这种时序信号,使用LSTM神经网络进行处理能够得到较好的效果。
LSTM神经网络的具体细节不再赘述。
四 、处理流程
1、重整数据集为mne格式;
2、重整labels,根据二中分类标准进行编码,编码为0~15,一共16个类别;
3、根据对情感轮的分析,本次主要采用频谱特征提取的方式提取频谱功率的时序变化特征;
4、将提取特征输入LSTM网络进行预测;
5、根据预测值和loss绘制tensorboard图像。
探索部分:
(1)查找deap数据集相关资料,了解数据集结构;(这一步花费时间较多)
(2)查找LSTM相关资料,了解LSTM的工作原理;
(3)代码实践。
五、代码部分
import numpy as np
import matplotlib.pyplot as plt
import mne
import scipy.io as scio
import warnings
warnings.filterwarnings("ignore", category=RuntimeWarning)
import torch
import torch.nn as nn
from torchvision import transforms
import os
from torch.utils.tensorboard import SummaryWriter
from sklearn.model_selection import train_test_split
# 超参数
num_epochs = 500 # 训练轮数
num_sample = 60 # 时间轴上时间点数量,num_sample + t_max < 63(数据持续时间)
learing_rate = 0.0003 # 学习率
t_max = 3 # 每一个mne.epoch的持续长度
output_types = 16 # LSTM输出种类数量
class my_lstm(nn.Module):
def __init__(self):
super(my_lstm, self).__init__()
self.rnn = nn.LSTM( # if use nn.RNN(), it hardly learns
input_size=160, # 输入维度
hidden_size=50, # 隐藏层神经元节点个数
num_layers=2, # 神经元层数
batch_first=True, # input & output will has batch size as 1s dimension. e.g. (batch, time_step, input_size)
bidirectional=True,
)
self.out = nn.Sequential(
nn.Dropout(p=0.5),
nn.Linear(100, 64),
nn.ReLU(),
nn.Linear(64, output_types)
)
def forward(self, x):
# x shape (batch, time_step, input_size)
# r_out shape (batch, time_step, output_size)
# h_n shape (n_layers * n_directions, batch, hidden_size)
# h_c shape (n_layers * n_directions, batch, hidden_size)
r_out, (h_n, h_c) = self.rnn(x, None)
# choose r_out at the last time step
out = self.out(r_out[:, -1, :])
return out
def train():
# 这里初始空值维度设置为第一个文件第一个视频中的维度,便于拼接,之后需要去掉
features = np.empty((40, num_sample, 160))
labels = []
# 要遍历的文件夹路径
folder_path = "data/data_preprocessed_matlab"
# 遍历文件夹下的文件名
file_names = [f for f in os.listdir(folder_path) if os.path.isfile(os.path.join(folder_path, f))]
# print(file_names)
test_file = ['s01.mat', 's02.mat', 's03.mat', 's04.mat', 's05.mat', 's06.mat', 's07.mat', 's09.mat']
for i in file_names:
path = folder_path + "/" + i
input_features, y = dataset(path)
features = np.concatenate([features, input_features], axis=0)
labels = np.append(labels, y)
# print(features.shape)
# print(labels.shape)
# print(labels)
labels = labels.astype(np.int32)
# print(labels)
print(labels.shape)
features = features[40:]
# print(features)
print(features.shape)
# 将特征数组和labels转换成PyTorch tensor
train_features, test_features, train_labels, test_labels = train_test_split(features, labels, test_size=0.2,
random_state=1)
train_features = torch.from_numpy(train_features).float()
train_labels = torch.from_numpy(train_labels).long()
test_features = torch.from_numpy(test_features).float()
test_labels = torch.from_numpy(test_labels).long()
for epoch in range(num_epochs):
# 前向传播
train_outputs = lstm(train_features)
print(train_outputs.shape)
train_loss = criterion(train_outputs, train_labels)
# 反向传播和优化
optimizer.zero_grad()
train_loss.backward()
optimizer.step()
# 在训练集上计算准确率
train_predicted = torch.argmax(train_outputs, dim=1)
print(train_labels)
print(train_predicted)
train_accuracy = (train_predicted == train_labels).sum().item() / train_labels.shape[0]
# 在测试集上计算准确率
test_outputs = lstm(test_features)
test_predicted = torch.argmax(test_outputs, dim=1)
test_accuracy = (test_predicted == test_labels).sum().item() / test_labels.shape[0]
# 打印损失值和准确率
print('Epoch [%d/%d], Train Loss: %.4f, Train Accuracy: %.4f, Test Accuracy: %.4f' % (
epoch + 1, num_epochs, train_loss, train_accuracy, test_accuracy))
# 将训练集的loss和accuracy写入TensorBoard
writer.add_scalar('Train/Loss', train_loss, epoch)
writer.add_scalar('Train/Accuracy', train_accuracy, epoch)
writer.add_scalar('test/Accuracy', test_accuracy, epoch)
def eeg_power_band(epochs):
"""
该函数根据epochs的特定频段中的相对功率来创建eeg特征
"""
# 特定频带
FREQ_BANDS = {"delta": [0.5, 4.5],
"theta": [4.5, 8.5],
"alpha": [8.5, 11.5],
"sigma": [11.5, 15.5],
"beta": [15.5, 30]}
spectrum = epochs.compute_psd(method='welch', picks='eeg', fmin=0.5, fmax=30., n_fft=256, n_overlap=16)
psds, freqs = spectrum.get_data(return_freqs=True)
# 归一化 PSDs
psds /= np.sum(psds, axis=-1, keepdims=True)
X = []
for fmin, fmax in FREQ_BANDS.values():
psds_band = psds[:, :, (freqs >= fmin) & (freqs < fmax)].mean(axis=-1)
X.append(psds_band.reshape(len(psds), -1))
return np.concatenate(X, axis=1)
def label_trans(raw_label):
# 对值进行二进制编码(小于5.5为0, 大于为1)
# 该函数在调整分类类别的时候需要跟随目标类别进行调整
binary_arr = np.where(raw_label < 5.5, 0, 1) # 小于 5.5 的值设置为 0,大于等于 5.5 的值设置为 1
decimal_arr = binary_arr.dot([8, 4, 2, 1]) # 将二进制数组转化为十进制数组
# print(decimal_arr) # 输出生成的数组
return decimal_arr
def dataset(file):
"""
dataset key_word:
labels.shape (40, 4)
解释:
labels用于标记被试看到每一个视频时的状态
第二维度中的四个数据代表四个评价标准arousal(唤醒度), valence(愉悦度), dominance(支配度), like(喜爱度)
data.shape (40, 40, 8064)
解释:
第一个维度代表40段观看视频产生的脑电数据
第二个维度代表每段数据存在40个通道
第三个维度为采样点,长度在63s左右
根据官方网站得采样率为128hz
"""
real_feature = np.empty((0, 160))
# print(real_feature.shape)
original_data = scio.loadmat(file)
# print(original_data.keys())
sample_data = original_data['data']
sample_labels = original_data['labels']
sample_data = sample_data[:, :32, :]
k = sample_data.shape[0]
# print(sample_labels)
# 根据官方文档设置通道
channel_names = ['Fp1', 'AF3', 'F7', 'F3', 'FC1', 'FC5', 'T7', 'C3',
'CP1', 'CP5', 'P7', 'P3', 'Pz', 'PO3', 'O1', 'Oz',
'O2', 'PO4', 'P4', 'P8', 'CP6', 'CP2', 'C4', 'T8',
'FC6', 'FC2', 'F4', 'F8', 'AF4', 'Fp2', 'Fz', 'Cz']
# 设置采样率
sfreq = 128
info = mne.create_info(channel_names, sfreq)
# 设置所有通道种类为eeg
channel_types = {}
for i in channel_names:
channel_types[i] = 'eeg'
# 将观看者看第i个视频的感受提取出来创建raw
for i in range(0, k):
slice_data = sample_data[i, :, :]
raw = mne.io.RawArray(slice_data, info)
raw.set_channel_types(channel_types)
"""
查看EEG信号图
raw.plot(title="The "+str(i)+" raw", bgcolor='pink', color='steelblue', n_channels=10, duration=10)
plt.pause(0)
"""
# 构建事件数组
events = np.zeros((num_sample, 3))
for i in range(num_sample):
events[i][0] = i*sfreq
# print(events)
events = events.astype(int)
# 每一个epoch长度为从事件开始的采样点到 t_max 秒后的采样点,这里就不设置基线了
epochs = mne.Epochs(raw=raw, events=events, tmin=0, tmax=t_max, preload=True, baseline=None)
# print(epochs)
features = eeg_power_band(epochs)
# features = features.reshape((160, 10))
# print(features.shape)
"""
features.shape = (60, 160)
解读:在此段raw中一共取得60个epoch,每一个epoch都有32个通道,每一个通道中都会提取5个功率谱特征,因此得到32*5=160个数据
"""
real_feature = np.concatenate((real_feature, features), axis=0)
# print(real_feature.shape)
"""
该文件中所有数据特征读取完之后重整维度,与40个标签数量配合
"""
# print(real_feature.shape)
input_feature = real_feature.reshape((-1, num_sample, 160))
y = label_trans(sample_labels) # 重整labels
# print(input_feature.shape)
# train(input_feature, y)
# 返回当前文件下的40个视频中的特征以及labels
return input_feature, y
# 创建LSTM模型
lstm = my_lstm()
# 定义损失函数和优化器
class_weight = torch.ones(output_types)
class_weight[0] = class_weight[-1] = 0.7 # 为0和15这两个类别分配不一样的权重,其它类别权重为1.0,避免0和15过多的问题
criterion = nn.CrossEntropyLoss(weight=class_weight)
optimizer = torch.optim.Adam(lstm.parameters(), lr=learing_rate)
# 添加TensorBoard的配置信息
writer = SummaryWriter('logs')
train()
# 关闭TensorBoard的输出
writer.close()
六、结果分析
在本次实验中取得的准确率并不高,在16分类方面取得的准确率大概在0.2到0.3左右,而在2分类方面正确率基本上也在0.6左右,与预期差距较大。此篇文章拖的时间较久且长时间进展微小,故现在发布,省出时间进行下一个工作。
16分类正确率低的原因大部分在于被试打分的主观性,且每一个情感层级的差别其实并没有想象中的大,最重要的是各个类别之间的占比极不均匀,0和15占比过高,就算为各个类别设置权重仍然达不到较好的效果。另一个原因主要在于对lstm模型的不熟练,不知道如何调整参数、如何增加和设置隐藏层以达到最好的训练效果。
下一个目标主要在于找一篇论文进行简单的复现,体会一下别人解决问题的思路。