用Python处理清华大学SSVEP脑电数据集:从.mat文件到PyTorch数据加载器的保姆级教程
用Python处理清华大学SSVEP脑电数据集:从.mat文件到PyTorch数据加载器的保姆级教程
当你第一次打开清华大学SSVEP数据集时,那个神秘的4-D矩阵(64, 1500, 40, 6)可能会让你感到困惑。别担心,这正是大多数脑机接口(BCI)研究者都会经历的"数据迷茫期"。本文将带你一步步拆解这个"数据魔方",从理解每个维度的物理意义开始,到最终构建出可以直接喂给PyTorch模型的数据管道。无论你是刚接触BCI的研一学生,还是想快速上手脑电数据分析的工程师,这篇实战指南都能让你在3小时内完成从原始数据到训练就绪状态的全流程。
1. 环境准备与数据初探
工欲善其事,必先利其器。我们需要配置一个专门用于生物信号处理的Python环境:
conda create -n bci python=3.8 conda activate bci pip install numpy scipy matplotlib pandas pip install torch torchvision torchaudio数据集下载后,你会看到类似这样的文件结构:
SSVEP_Data/ ├── S01.mat ├── S02.mat ... ├── Freq_phase.mat ├── 64通道.loc └── Sub_info.txt关键文件说明:
SXX.mat:主体数据文件,每个文件对应一个受试者64通道.loc:电极位置信息(国际10-20系统)Freq_phase.mat:刺激频率与相位参数Sub_info.txt:受试者元数据(年龄、性别等)
用Python加载第一个MAT文件看看数据结构:
import scipy.io as sio data = sio.loadmat('S01.mat')['data'] # 注意MATLAB变量名 print(f"数据维度: {data.shape}") print(f"数据类型: {data.dtype}")你应该会看到输出:
数据维度: (64, 1500, 40, 6) 数据类型: float642. 理解数据维度的物理意义
这个4-D矩阵就像俄罗斯套娃,每一层都有明确的物理含义:
| 维度索引 | 维度大小 | 物理含义 | 备注 |
|---|---|---|---|
| 0 | 64 | 电极通道数 | 对应64个EEG采集电极 |
| 1 | 1500 | 时间点 | 6秒数据@250Hz采样率 |
| 2 | 40 | 目标刺激类型 | 对应40种不同频率的视觉刺激 |
| 3 | 6 | 实验重复次数(Block) | 每种刺激重复6次 |
重要细节:
- 时间维度包含500ms提示期+5.5秒刺激期
- 40个目标刺激对应8-15.8Hz范围内的40个频率(间隔0.2Hz)
- 每个Block中刺激呈现顺序是随机的
用代码验证下时间轴是否正确:
import matplotlib.pyplot as plt # 绘制Pz电极在第1个刺激、第1次实验中的波形 plt.plot(data[47, :, 0, 0]) # 47号电极通常是Pz plt.axvline(x=125, color='r', linestyle='--') # 标记0.5秒处(提示结束) plt.xlabel('时间点(250Hz)') plt.ylabel('电压(μV)') plt.title('单次试验的原始EEG信号') plt.show()3. 数据重塑与预处理
原始4-D结构不适合直接输入深度学习模型,我们需要进行维度重组:
3.1 数据扁平化处理
目标是将(64,1500,40,6)转为(样本数, 通道, 时间点):
import numpy as np # 方法1:使用reshape保持内存连续 data_reshaped = data.transpose(2, 3, 0, 1) # (40,6,64,1500) samples = data_reshaped.reshape(-1, 64, 1500) # (240,64,1500) # 方法2:更安全的逐元素重组 samples = np.zeros((240, 64, 1500)) idx = 0 for target in range(40): for block in range(6): samples[idx] = data[:, :, target, block] idx += 1为什么是240个样本?
- 40种刺激 × 6次重复 = 240 trials
- 每个trial是64电极 × 1500时间点
3.2 添加通道维度(CNN适配)
大多数CNN期望输入有通道维度,我们添加一个伪维度:
samples = samples[:, np.newaxis, :, :] # (240,1,64,1500)3.3 标签处理
从Freq_phase.mat加载刺激频率作为标签:
freq_phase = sio.loadmat('Freq_phase.mat') frequencies = freq_phase['freqs'][0] # 40个刺激频率 # 创建标签向量 (重复6次) labels = np.repeat(np.arange(40), 6) # [0,0,...,39,39] # 可选:转换为one-hot编码 one_hot = np.eye(40)[labels]4. 数据集划分与标准化
4.1 训练集/测试集分割
from sklearn.model_selection import train_test_split X_train, X_test, y_train, y_test = train_test_split( samples, labels, test_size=0.2, random_state=42, stratify=labels)分层抽样(stratify)的重要性:
- 确保每个频率类别在训练/测试集中比例一致
- 避免某些频率在测试集中完全缺失
4.2 数据标准化
EEG数据通常需要按电极进行标准化:
# 计算训练集的均值和标准差 mean = X_train.mean(axis=(0, 2, 3), keepdims=True) std = X_train.std(axis=(0, 2, 3), keepdims=True) # 应用相同的变换到训练和测试集 X_train = (X_train - mean) / std X_test = (X_test - mean) / std为什么不全局标准化?
- 不同电极的信号幅度差异有生理意义
- 保持电极间关系对空间特征提取很重要
5. 构建PyTorch数据管道
5.1 自定义Dataset类
from torch.utils.data import Dataset class SSVEPDataset(Dataset): def __init__(self, X, y): self.X = torch.from_numpy(X).float() self.y = torch.from_numpy(y).long() def __len__(self): return len(self.y) def __getitem__(self, idx): return self.X[idx], self.y[idx] train_dataset = SSVEPDataset(X_train, y_train) test_dataset = SSVEPDataset(X_test, y_test)5.2 创建DataLoader
from torch.utils.data import DataLoader batch_size = 32 train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True) test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False)关键参数建议:
- Batch Size:32-64之间效果较好
- shuffle:训练集必须打乱,测试集保持有序
- num_workers:根据CPU核心数设置(I/O密集型任务)
5.3 验证数据流
# 检查第一个batch for batch_X, batch_y in train_loader: print(f"Batch shape: {batch_X.shape}") # 应为[32,1,64,1500] print(f"Labels: {batch_y[:5]}") # 显示前5个标签 break6. 高级技巧与优化建议
6.1 内存映射处理大文件
当处理多个受试者数据时,可以使用内存映射避免内存爆炸:
# 使用numpy.memmap data = np.memmap('S01.mat', dtype='float64', mode='r', shape=(64,1500,40,6))6.2 实时数据增强
在DataLoader中集成数据增强:
class AugmentedDataset(SSVEPDataset): def __getitem__(self, idx): x, y = super().__getitem__(idx) if random.random() > 0.5: x = add_gaussian_noise(x, std=0.01) return x, y6.3 多被试数据整合
处理多个受试者数据时的建议结构:
all_subjects = [] for subj in range(1, 36): data = load_subject(f'S{subj:02d}.mat') all_subjects.append(data) # 沿样本维度拼接 mega_dataset = np.concatenate(all_subjects, axis=0)7. 常见问题排查
问题1:加载MAT文件时报错"Not a MAT-file"
- 解决方案:检查文件路径,确保使用
scipy.io.loadmat而非普通文件读取
问题2:内存不足错误
- 尝试方案:逐被试处理或使用
memmap - 备用方案:降低采样率或截取时间窗口
问题3:模型训练时loss不下降
- 检查点:数据标准化是否正确实施
- 检查点:标签是否从0开始连续编号
- 检查点:输入张量形状是否符合模型预期
问题4:测试集准确率远低于训练集
- 可能原因:数据泄露(标准化时使用了全局统计量)
- 可能原因:被试间差异(考虑按被试划分数据集)
