最近看到FBCSP的论文,但是不了解CSP是个啥,所以就学习了一下,这里记录一下,以下内容主要来自以下文章:共空间模式算法(CSP);特征提取丨共空间模式 Common Spatial Pattern (CSP);Python中MNE库利用CSP分析运动想象数据。
共空间模式算法(Common Spatial Pattern,CSP)广泛应用于脑机接口(Brain-Computer Interface,BCI)中脑电信号(Electroencephalogram,EEG)的特征提取,因此,在介绍CSP算法前,有必要对脑电接口,脑电信号等相关概念做个简述。
滤波是指通过操作接受或拒绝一定的频率。具体到空间滤波器,指的是通过空间滤波器对矩阵的元素进行修改,达到预期的效果。
空间滤波器由一下两部分组成:
1)一个邻域(通常为规模较小矩阵)
2)对该邻域的覆盖的矩阵元素执行的预定义操作
下图为使用3 × 3的邻域的线性空间滤波举例,过程就是对原矩阵的元素逐个按照预选定义的滤波器的操作进行运算:
共空间模式(CSP): 一种对两分类任务下的空域滤波特征提取算法,能够从多通道的脑机接口数据里面提取出每一类的空间分布成分。
共空间模式算法的基本原理: 利用矩阵的对角化,找到一组最优空间滤波器进行投影,使得两类信号的方差值差异最大化,从而得到具有较高区分度的特征向量。
共空间模式算法旨在: 设计空间滤波器使得两组脑电时空信号矩阵滤波后,方差值差异最大化,从而得到具有较高区分度的特征向量。用于下一步将特征向量送入分类器进行分类。
上图中,脑电时空信号矩阵的维数 N × T N × T N×T , N N N代表脑电通道数(导联数目), T T T代表时间层面的采样点个数。一个矩阵的单位为一个trail,表示一次测试。滤波器的维数是 2 m × N 2 m × N 2m×N ,N同样代表导联数目, 2 m 2m 2m为生成该滤波器时,人为设定的特征选取个数。
假设 X 1 X_1 X1和 X 2 X_2 X2分别为两分类想象运动任务下的多通道诱发响应时-空信号矩阵,他们的维数均为 N ∗ T N∗T N∗T, N N N为脑电通道数, T T T为每个通道所采集的样本数。为了计算其协方差矩阵,现在假设 N < T N<T N<T。在两种脑电想象任务情况下,一般采用复合源的数学模型来描述 E E G EEG EEG信号,为了方便计算,一般忽略噪声所产生的影响。 X 1 X_1 X1和 X 2 X_2 X2可以分别写成:
式中: S 1 S_1 S1和 S 2 S_2 S2分别代表两种类型任务。不妨假设这两个源信号是相互线性独立的;
S M S_M SM代表两种类型任务下所共同拥有的源信号,假设 S 1 S_1 S1是由 m 1 m_1 m1个源所构成的, S 2 S_2 S2是由 m 2 m_2 m2个源所构成。
则 C 1 C_1 C1和 C 2 C_2 C2便是由 S 1 S_1 S1和 S 2 S_2 S2相关的 m 1 m_1 m1和 m 2 m_2 m2个共同空间模式组成的,由于每个空间模式都是一个 N ∗ 1 N∗1 N∗1维的向量,现在用这个向量来表示单个的源信号所引起的信号在 N N N个导联上的分布权重。
C M C_M CM表示的是与 S M S_M SM相应的共有的空间模式。 C S P CSP CSP算法的目标就是要设计空间滤波器 F 1 F_1 F1和 F 2 F_2 F2得到空间因子 W W W。
X 1 X_1 X1和 X 2 X_2 X2归一化后的协方差矩阵 R 1 R_1 R1和 R 2 R_2 R2分别为:
式中: X T X_T XT表示 X X X矩阵的转置, t r a c e ( X ) trace(X) trace(X)表示矩阵对角线上元素的和。然后求混合空间协方差矩阵 R R R:
式中: R i ˉ ( i = 1 , 2 ) \bar{R_i}(i=1,2) Riˉ(i=1,2)分别为任务1,2实验的平均协方差矩阵。
对混合空间协方差矩阵 R R R按式进行特征值分解:
式中: U U U是矩阵 λ λ λ的特征向量矩阵, λ λ λ是对应的特征值构成的对角阵。将特征值进行降序排列,白化值矩阵为:
则有 P R P T = I PRP^T=I PRPT=I,此结论由白化矩阵得到。
I = P R P T = P R 1 ˉ P T + P R 2 ˉ P T = S 1 + S 2 \begin{align} I & = PRP^T \\ & = P\bar{R_1}P^T + P\bar{R_2}P^T \\ & = S_1 + S_2 \\ \end{align} I=PRPT=PR1ˉPT+PR2ˉPT=S1+S2
对 R 1 R_1 R1和 R 2 R_2 R2进行如下变换:
S 1 = P R 1 ˉ P T S 2 = P R 2 ˉ P T S_1=P\bar{R_1}P^T\\ S_2=P\bar{R_2}P^T S1=PR1ˉPTS2=PR2ˉPT
然后对 S 1 S_1 S1和 S 2 S_2 S2做主分量分解,得到:
S 1 = B 1 λ 1 B 1 T S 2 = B 2 λ 2 B 2 T S_1=B_1\lambda_1B_1^T\\ S_2=B_2\lambda_2B_2^T S1=B1λ1B1TS2=B2λ2B2T
其中 S 1 \mathbf{S}_1 S1和 S 2 \mathbf{S}_2 S2具有相同的特征向量(这也是共空间模式名称的由来):
B 1 = B 2 = U B_1=B_2=U B1=B2=U
这里假设 U \mathbf{U} U的每一列是按照 λ 1 \mathbf{\lambda}_1 λ1中的特征值从大到小排列的,可以看出 λ 2 \mathbf{\lambda}_2 λ2中的特征值是从小到大排列的,满足 λ 1 + λ 2 = I \mathbf{\lambda}_1+\mathbf{\lambda}_2=\mathbf{I} λ1+λ2=I的关系。
把 λ 1 λ_1 λ1中的特征值按照降序排列,则 λ 2 λ_2 λ2中对应的特征值按升序排列,根据这点可以推断出 λ 1 λ_1 λ1和 λ 2 λ_2 λ2具有下面的形式:
白化EEG到与 λ 1 λ_1 λ1和 λ 2 λ_2 λ2中的最大特征值对应的特征向量的变换对于分离两个信号矩阵中的方差是最佳的。 投影矩阵 W W W是所对应的空间滤波器为:
Z L = W × X L = B T × P × X L Z R = W × X R = B T × P × X R Z_L=W×X_L=B^T×P×X_L \\ Z_R=W×X_R=B^T×P×X_R ZL=W×XL=BT×P×XLZR=W×XR=BT×P×XR
R L = W × X L × X L T × W T = B T × P × X L × X L T × P T × B R R = W × X R × X R T × W T = B T × P × X R × X R T × P T × B R_L=W×X_L×X_L^T×W^T=B^T×P×X_L×X_L^T×P^T×B \\ R_R=W×X_R×X_R^T×W^T=B^T×P×X_R×X_R^T×P^T×B RL=W×XL×XLT×WT=BT×P×XL×XLT×PT×BRR=W×XR×XRT×WT=BT×P×XR×XRT×PT×B
将训练集的运动想象矩阵 X L 、 X R XL、XR XL、XR 经过构造的相应滤波器 W W W 滤波可得特征 Z L 、 Z R Z_L、Z_R ZL、ZR 为:
Z L = W × X L Z R = W × X R Z_L=W×X_L \\ Z_R=W×X_R ZL=W×XLZR=W×XR
根据 CSP 算法在多电极采集脑电信号特征提取的定义,本研究选取 f L f_L fL 和 f R f_R fR 为想象左和想象右的特征向量,定义如下:
对于测试数据 X i X_i Xi 来说,其特征向量 f i f_i fi 提取方式如下,并与 f L f_L fL 和 f R f_R fR 进行比较以确定第 i i i 次想象为想左或想右。
求出测试的 f i f_i fi 特征,进行下面的分类。
function CSPMatrix = learnCSP(EEGSignals,classLabels)
%
%Input:
%EEGSignals: the training EEG signals, composed of 2 classes. These signals
%are a structure such that:
% EEGSignals.x: the EEG signals as a [Ns * Nc * Nt] Matrix where
% Ns: number of EEG samples per trial
% Nc: number of channels (EEG electrodes)
% nT: number of trials
% EEGSignals.y: a [1 * Nt] vector containing the class labels for each trial
% EEGSignals.s: the sampling frequency (in Hz)
%
%Output:
%CSPMatrix: the learnt CSP filters (a [Nc*Nc] matrix with the filters as rows)
%
%See also: extractCSPFeatures
%check and initializations
nbChannels = size(EEGSignals.x,2);
nbTrials = size(EEGSignals.x,3);
nbClasses = length(classLabels);
if nbClasses ~= 2
disp('ERROR! CSP can only be used for two classes');
return;
end
covMatrices = cell(nbClasses,1); %the covariance matrices for each class
% Computing the normalized covariance matrices for each trial
trialCov = zeros(nbChannels,nbChannels,nbTrials);
for t=1:nbTrials
E = EEGSignals.x(:,:,t)'; %note the transpose
EE = E * E'; % 计算单试次的协方差矩阵
trialCov(:,:,t) = EE ./ trace(EE);
end
clear E;
clear EE;
%computing the covariance matrix for each class
for c=1:nbClasses
covMatrices{c} = mean(trialCov(:,:,EEGSignals.y == classLabels(c)),3); %EEGSignals.y==classLabels(c) returns the indeces corresponding to the class labels
end
%the total covariance matrix
covTotal = covMatrices{1} + covMatrices{2};
%whitening transform of total covariance matrix
[Ut Dt] = eig(covTotal); %caution: the eigenvalues are initially in increasing order
eigenvalues = diag(Dt);
[eigenvalues egIndex] = sort(eigenvalues, 'descend');
Ut = Ut(:,egIndex);
P = diag(sqrt(1./eigenvalues)) * Ut'; % 白化变换矩阵
%transforming covariance matrix of first class using P
transformedCov1 = P * covMatrices{1} * P';
%EVD of the transformed covariance matrix
[U1 D1] = eig(transformedCov1);
eigenvalues = diag(D1);
[eigenvalues egIndex] = sort(eigenvalues, 'descend');
U1 = U1(:, egIndex);
CSPMatrix = U1' * P; % 计算空间滤波器
function features = extractCSP(EEGSignals, CSPMatrix, nbFilterPairs)
%
%extract features from an EEG data set using the Common Spatial Patterns (CSP) algorithm
%
%Input:
%EEGSignals: the EEGSignals from which extracting the CSP features. These signals
%are a structure such that:
% EEGSignals.x: the EEG signals as a [Ns * Nc * Nt] Matrix where
% Ns: number of EEG samples per trial
% Nc: number of channels (EEG electrodes)
% nT: number of trials
% EEGSignals.y: a [1 * Nt] vector containing the class labels for each trial
% EEGSignals.s: the sampling frequency (in Hz)
%CSPMatrix: the CSP projection matrix, learnt previously (see function learnCSP)
%nbFilterPairs: number of pairs of CSP filters to be used. The number of
% features extracted will be twice the value of this parameter. The
% filters selected are the one corresponding to the lowest and highest
% eigenvalues
%
%Output:
%features: the features extracted from this EEG data set
% as a [Nt * (nbFilterPairs*2 + 1)] matrix, with the class labels as the
% last column
%
%initializations
nbTrials = size(EEGSignals.x,3);
features = zeros(nbTrials, 2*nbFilterPairs+1);
Filter = CSPMatrix([1:nbFilterPairs, (end-nbFilterPairs+1):end],:); % 取了CSPMatrix的前nbFilterPairs行和后nbFilterPairs行
%extracting the CSP features from each trial
for t=1:nbTrials
%projecting the data onto the CSP filters
%对通道做空间滤波,得到通道之间的空间关系,这个关系用几维矩阵表示,取决于滤波器的个数
projectedTrial = Filter * EEGSignals.x(:,:,t)'; % [2*F, Ns] = [2*F,Nc] * [Nc, Ns]
%generating the features as the log variance of the projected signals
variances = var(projectedTrial,0,2); % [2*F, 1]
for f=1:length(variances)
features(t,f) = log(1+variances(f)); % 对协方差数值做了处理
end
end
% Extract Common Spatial Pattern (CSP) Feature
close all; clear; clc;
load dataset_BCIcomp1.mat
EEGSignals.x=x_train;
EEGSignals.y=y_train;
Y=y_train;
classLabels = unique(EEGSignals.y);
CSPMatrix = learnCSP(EEGSignals,classLabels);
nbFilterPairs = 1;
X = extractCSP(EEGSignals, CSPMatrix, nbFilterPairs);
EEGSignals.x=x_test;
T = extractCSP(EEGSignals, CSPMatrix, nbFilterPairs);
save dataCSP.mat X Y T
color_L = [0 102 255] ./ 255;
color_R = [255, 0, 102] ./ 255;
pos = find(Y==1);
plot(X(pos,1),X(pos,2),'x','Color',color_L,'LineWidth',2);
hold on
pos = find(Y==2);
plot(X(pos,1),X(pos,2),'o','Color',color_R,'LineWidth',2);
legend('Left Hand','Right Hand')
xlabel('C3','fontweight','bold')
ylabel('C4','fontweight','bold')
上面代码中的dataset_BCIcomp1.mat数据在这里:链接: https://pan.baidu.com/s/1Tu-kFDjRD6EBEZRo-D00KA?pwd=fci5 提取码: fci5
import numpy as np
import matplotlib.pyplot as plt
from sklearn.pipeline import Pipeline
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
from sklearn.model_selection import ShuffleSplit, cross_val_score
from mne import Epochs, pick_types, events_from_annotations
from mne.channels import make_standard_montage
from mne.channels import read_layout
from mne.io import concatenate_raws, read_raw_edf
from mne.datasets import eegbci
from mne.decoding import CSP
print(__doc__)
# #################################################################################################################
# # Set parameters and read data
# avoid classification of evoked responses by using epochs that start 1s after cue onset.
tmin, tmax = -1., 4. #设置参数,记录点的前1秒后4秒用于生成epoch数据
event_id = dict(hands=2, feet=3) #设置事件的映射关系
subject = 1
runs = [6, 10, 14] # motor imagery: hands vs feet
raw_fnames = eegbci.load_data(subject, runs) # 获取想要读取的文件名称,这个应该是没有会默认下载的数据
raw = concatenate_raws([read_raw_edf(f, preload=True) for f in raw_fnames]) # 将3个文件的数据进行拼接
eegbci.standardize(raw) # set channel names,标准化通道位置和名称。
montage = make_standard_montage('standard_1005')
raw.set_montage(montage)
# strip channel names of "." characters
raw.rename_channels(lambda x: x.strip('.')) # 去掉通道名称后面的(.),不知道为什么默认情况下raw.info['ch_names']中的通道名后面有的点
# Apply band-pass filter
raw.filter(7., 30., fir_design='firwin', skip_by_annotation='edge') # 对原始数据进行FIR带通滤波
events, _ = events_from_annotations(raw, event_id=dict(T1=2, T2=3)) # 从annotation中获取事件信息
picks = pick_types(raw.info, meg=False, eeg=True, stim=False, eog=False,
exclude='bads') # 剔除坏道,提取其中有效的EEG数据
# Read epochs (train will be done only between 1 and 2s)
# Testing will be done with a running classifier
epochs = Epochs(raw, events, event_id, tmin, tmax, proj=True, picks=picks,
baseline=None, preload=True) # 根据事件生成对应的Epochs数据
epochs_train = epochs.copy().crop(tmin=1., tmax=2.) # 截取其中的1秒到2秒之间的数据,也就是提示音后1秒到2秒之间的数据(这个在后面滑动窗口验证的时候有用)
labels = epochs.events[:, -1] - 2 # 将events转换为labels,event为2,3经过计算后也就是0,1
# #################################################################################################################
# # Feature extraction and classification
# Define a monte-carlo cross-validation generator (reduce variance):
scores = []
epochs_data = epochs.get_data() # 获取epochs的所有数据,主要用于后面的滑动窗口验证
epochs_data_train = epochs_train.get_data() # 获取训练数据
cv = ShuffleSplit(10, test_size=0.2, random_state=42) # 设置交叉验证模型的参数,生成将数据分成训练集和测试集的指数。
cv_split = cv.split(epochs_data_train) # 根据设计的交叉验证参数,分配相关的训练集和测试集数据
# Assemble a classifier
lda = LinearDiscriminantAnalysis() # 创建线性分类器
csp = CSP(n_components=4, reg=None, log=True, norm_trace=False) ### 创建CSP提取特征,这里使用4个分量的CSP ###
# Use scikit-learn Pipeline with cross_val_score function
clf = Pipeline([('CSP', csp), ('LDA', lda)]) # 创建机器学习的Pipeline,也就是分类模型,使用这种方式可以把特征提取和分类统一整合到了clf中
scores = cross_val_score(clf, epochs_data_train, labels, cv=cv, n_jobs=1) # 获取交叉验证模型的得分
# Printing the results
# 输出结果,准确率和不同样本的占比
class_balance = np.mean(labels == labels[0])
class_balance = max(class_balance, 1. - class_balance)
print("Classification accuracy: %f / Chance level: %f" % (np.mean(scores),
class_balance))
# plot CSP patterns estimated on full data for visualization
# csp提取特征,用于绘制CSP不同分量的模式图(地形图)
# 如果没有这一步csp.plot_patterns将不会执行
csp.fit_transform(epochs_data, labels)
# lay文件的存放路径,这个文件不是计算生成的,是mne库提供的点击分布描述文件在安装路径下(根据个人安装路径查找):
# D:\Program Files (x86)\Anaconda3\envs\pytorch_env\Lib\site-packages\mne\channels\data\layouts
layout = read_layout('EEG1005')
csp.plot_patterns(epochs.info, ch_type='eeg', units='Patterns (AU)', size=1.5)
# plt.show()
# #################################################################################################################
# # Verify the performance of the algorithm
sfreq = raw.info['sfreq'] # 获取数据的采样频率
w_length = int(sfreq * 0.5) # running classifier: window length
w_step = int(sfreq * 0.1) # running classifier: window step size
w_start = np.arange(0, epochs_data.shape[2] - w_length, w_step) # 每次滑动窗口的起始点
scores_windows = [] # 得分列表用于保存模型得分
for train_idx, test_idx in cv_split: # 交叉验证计算模型的性能
y_train, y_test = labels[train_idx], labels[test_idx] # 获取测试集和训练集数据
X_train = csp.fit_transform(epochs_data_train[train_idx], y_train) # 设置csp模型的参数,提取相关特征,用于后面的lda分类
# fit classifier
lda.fit(X_train, y_train) # 拟合lda模型
# running classifier: test classifier on sliding window
score_this_window = [] # 用于记录本次交叉验证的得分
for n in w_start:
X_test = csp.transform(epochs_data[test_idx][:, :, n:(n + w_length)]) # csp提取测试数据相关特征
score_this_window.append(lda.score(X_test, y_test)) # 获取测试数据得分
scores_windows.append(score_this_window) # 添加到总得分列表
# Plot scores over time
w_times = (w_start + w_length / 2.) / sfreq + epochs.tmin # 设置绘图的时间轴,时间轴上的标志点为窗口的中间位置
# 绘制模型分类结果的性能图(得分的均值)
plt.figure()
plt.plot(w_times, np.mean(scores_windows, 0), label='Score')
plt.axvline(0, linestyle='--', color='k', label='Onset')
plt.axhline(0.5, linestyle='-', color='k', label='Chance')
plt.xlabel('time (s)')
plt.ylabel('classification accuracy')
plt.title('Classification score over time (w_length = {0}s)'.format(w_length/sfreq))
plt.legend(loc='lower right')
plt.show()
下面来看一下为什么测试数据集的时间窗口和训练数据集的时间窗口不一致也可以运行呢?lda的输入是经过csp提取后的特征,所以问题的重点是为什么csp的输入时间窗口可以不一致呢?
对于测试数据 X i X_i Xi 来说,其特征向量 f i f_i fi 提取方式如下,并与 f L f_L fL 和 f R f_R fR 进行比较以确定第 i i i 次想象为想左或想右。
f i f_i fi为提取的特征,是由 z z zi计算得来,而 z i z_i zi又是由矩阵 W W W和 X i X_i Xi计算得来,看到这大概明白了, W W W是我们的投影矩阵, X i X_i Xi是我们的输入数据,根据矩阵乘法的运算规则:前一个的列数等于后一个的行数,不难看出即使Xi的列数发生改变也不会影响计算。这里 X i X_i Xi的列数对应的就是时间窗口,我想这也就是为什么测试集的时间窗口和训练集的时间窗口可以不一致的原因吧。
下面分别设置测试集时间窗口为 1 1 1秒(和训练集相同)和 1.5 1.5 1.5秒,和上面 0.5 0.5 0.5秒的时候相比,在这 3 3 3种时间窗口中 1 1 1秒的更加平稳,我想这也是其和训练集时间窗口一致的原因,毕竟测试集和训练集在数据维度上应该保持一致。不过这个特性倒是十分适合EEG这种具有时间序列特点的数据,这种使用方法或许在其他方面有意想不到的效果。
共空间模式算法(CSP)
特征提取丨共空间模式 Common Spatial Pattern (CSP)
Python中MNE库利用CSP分析运动想象数据
共空间模式CSP