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模型的不熟练,不知道如何调整参数、如何增加和设置隐藏层以达到最好的训练效果。

        下一个目标主要在于找一篇论文进行简单的复现,体会一下别人解决问题的思路。