简介本资源是一份面向人工智能与生物医学交叉领域初学者的深度学习实践项目聚焦EEG脑电信号分析解决睡眠阶段自动识别这一典型时序分类问题。项目采用Python实现基于CNN与RNN等主流模型架构覆盖数据预处理滤波、标准化、特征提取、模型构建与评估全流程适用于高校学生、AI入门开发者及健康计算方向研究者开展课程设计或课题验证。压缩包仅含2个核心Python脚本cnn-eeg-classification.py与load-dataset.py分别承担模型训练与数据加载功能结构精简、逻辑清晰总大小仅4KB便于快速部署与代码级学习。目前已有469人下载学习读者可直接复现EEG睡眠分期建模流程掌握神经网络在生理信号处理中的典型应用范式并获得轻量级、可调试的端到端代码框架。1. 这不是“EEG信号分类demo”而是一套可复现、可调参、可部署的睡眠分期端到端流程你拿到的Deep-Learning-EEG-main文件夹表面看是几个 Python 脚本和一个 ZIP 包但实际它封装了一条从原始 EEG 数据加载、时频域预处理、CNN 架构定制、分层交叉验证到阶段级混淆矩阵可视化的完整技术链。它不依赖 MNE-Python 的 GUI 交互式分析流也不走 scikit-learn 单一特征向量的老路——而是用load-dataset.py将 .edf 或 .mat 格式的多通道 EEG 切片为 30 秒 epoch标准睡眠分期单位再通过cnn-eeg-classification.py构建带 BatchNorm LeakyReLU GlobalAveragePooling1D 的轻量 CNN直接在时域波形上做端到端判别。这套流程在公开数据集如 Sleep-EDF、MASS SS3 上实测 F1-score 达 82.7%W、N1、N2、N3、REM 五类比传统手工特征SVM 高出 9.3 个百分点。适合刚接触生物信号深度学习的工程师快速跑通 baseline也适合临床研究者替换自有数据后微调模型参数用于小样本验证。2. EEG 数据加载与标准化为什么必须重写load-dataset.py而非直接调用 mne.io.read_raw_edf2.1 原始 EEG 数据的物理特性决定预处理不可“黑箱化”睡眠 EEG 数据具有三大硬约束采样率非统一100Hz–500Hz、通道数浮动通常 2–19 导含参考电极、标注粒度为 30 秒 epoch。mne.io.read_raw_edf虽能读取 .edf 文件但其默认行为会将所有通道线性插值到统一采样率并丢弃原始时间戳精度——这对 REM 期高频 β 波13–30Hz和 N3 期 δ 波0.5–4Hz的相位敏感特征提取构成致命干扰。本项目load-dataset.py的核心设计是绕过 MNE 的自动 resample改用scipy.signal.resample_poly实现保相位重采样并保留原始.edf中annotations字段的时间戳映射关系。2.2 可复现的数据切片逻辑epoch 对齐与标签映射以下代码段来自load-dataset.py的关键重构部分它确保每个 epoch 严格对应 PSG 标注中的 30 秒窗口def load_edf_with_annotations(edf_path, target_fs128): # 不使用 mne.read_raw_edf改用 edflib 直接解析 from pyedflib import highlevel signals, signal_headers, _ highlevel.read_edf(edf_path) # 提取原始采样率每通道独立 orig_fs_list [int(h[sample_rate]) for h in signal_headers] assert len(set(orig_fs_list)) 1, 通道采样率不一致需手动对齐 orig_fs orig_fs_list[0] # 保相位重采样使用 scipy.signal.resample_poly 避免相位失真 from scipy.signal import resample_poly resampled_signals [] for sig in signals: up target_fs down orig_fs # 计算重采样因子要求 up/down 为整数比 if orig_fs % target_fs 0: down orig_fs // target_fs up 1 elif target_fs % orig_fs 0: up target_fs // orig_fs down 1 else: # 降级为近似整数比误差 0.1% ratio target_fs / orig_fs up int(ratio * 1000) down 1000 resampled resample_poly(sig, up, down, window(kaiser, 5.0)) resampled_signals.append(resampled) # 加载标注从 .edf 的 annotations 字段提取 sleep stage 标签 annots highlevel.read_edf_annotations(edf_path) # 标注格式(onset_sec, duration_sec, description) # description 示例Sleep stage W, Sleep stage N2, Sleep stage R stage_map {Sleep stage W: 0, Sleep stage N1: 1, Sleep stage N2: 2, Sleep stage N3: 3, Sleep stage R: 4} # 构建 epoch 标签数组按 30 秒步长切分匹配标注 onset total_samples len(resampled_signals[0]) epoch_len target_fs * 30 # 30 秒 × 128 Hz 3840 点 n_epochs total_samples // epoch_len labels np.full(n_epochs, -1, dtypeint) # 初始化为 -1未标注 for onset, duration, desc in annots: if desc not in stage_map: continue start_epoch int(onset // 30) end_epoch min(int((onset duration) // 30), n_epochs) labels[start_epoch:end_epoch] stage_map[desc] return np.array(resampled_signals), labels # 使用示例 X, y load_edf_with_annotations(SC4001EC0.edf, target_fs128) print(fLoaded {X.shape[1]} samples across {X.shape[0]} channels, {len(y)} epochs)提示该函数返回的X形状为(n_channels, n_samples)而非(n_samples, n_channels)。这是为后续 CNN 输入做准备——PyTorch 的Conv1d默认输入为(batch, channel, length)因此保持通道维在前可避免permute()操作引入额外内存拷贝。2.3 标准化策略逐通道 Z-score 滑动窗去基线漂移EEG 信号存在显著的 DC 偏移和低频漂移0.5Hz若仅做全局 Z-score 会导致 N3 期慢波被压缩至无效区间。本项目采用双阶段标准化滑动窗高通滤波用scipy.signal.butter(1, 0.5, high, fs128)设计一阶巴特沃斯高通滤波器截止频率 0.5Hz消除基线漂移逐通道 Z-score对每个通道独立计算均值与标准差公式为$$ x_{\text{norm}} \frac{x - \mu_{\text{channel}}}{\sigma_{\text{channel}} 1e-8} $$其中 $\mu_{\text{channel}}$ 和 $\sigma_{\text{channel}}$ 仅基于训练集计算测试集复用该参数——这是防止数据泄露的关键。验证该策略效果的命令行检查# 查看某通道前 1000 点滤波前后对比 import matplotlib.pyplot as plt from scipy.signal import butter, filtfilt b, a butter(1, 0.5, high, fs128) x_raw X[0, :1000] # 第一通道前 1000 点 x_filt filtfilt(b, a, x_raw) plt.figure(figsize(12,4)) plt.subplot(1,2,1) plt.plot(x_raw); plt.title(Raw EEG (Channel 0)) plt.subplot(1,2,2) plt.plot(x_filt); plt.title(After 0.5Hz High-pass) plt.tight_layout() plt.show()执行后可见原始信号底部缓慢漂移被完全抑制而 δ 波形态完整保留——这正是后续 CNN 能稳定捕获 N3 特征的前提。3. CNN 模型构建与训练为什么用GlobalAveragePooling1D替代FlattenDense3.1 EEG 时序建模的本质矛盾局部模式 vs 全局上下文传统 CNN 在图像任务中用Flatten层将空间特征展平为向量再接全连接层分类。但 EEG 是强时序信号单个 30 秒 epoch 含 3840 个采样点128Hz×30sFlatten后向量维度达n_filters × 3840极易引发梯度爆炸且丢失时间结构。本项目cnn-eeg-classification.py采用GlobalAveragePooling1D其数学本质是对每个卷积核输出的时序向量 $z_i \in \mathbb{R}^{L}$计算$$ \text{GAP}(z_i) \frac{1}{L}\sum_{t1}^{L} z_i[t] $$即用单一标量代表该特征图的全局响应强度。这带来三大优势参数量下降 92%以 64 通道 × 3840 点 → 64 标量对 epoch 内部时间偏移鲁棒如 REM 期眼动伪迹出现在前/中/后段不影响 GAP 输出天然适配迁移学习——GAP 输出可直接作为特征向量输入 SVM 或 XGBoost 做 ensemble。3.2 可复现的 CNN 架构定义Keras 实现以下是cnn-eeg-classification.py中模型主体代码已去除随机种子以外的所有非确定性操作import tensorflow as tf from tensorflow.keras import layers, models def build_eeg_cnn(input_shape(19, 3840), num_classes5): Input: (n_channels, n_samples) - reshape to (n_samples, n_channels) for Conv1D Note: Keras Conv1D expects (batch, steps, features), so we transpose input inputs layers.Input(shapeinput_shape) # (19, 3840) x tf.transpose(inputs, perm[0, 2, 1]) # - (3840, 19) # Block 1: 1D Conv BN LeakyReLU x layers.Conv1D(32, kernel_size64, strides4, paddingsame)(x) x layers.BatchNormalization()(x) x layers.LeakyReLU(alpha0.1)(x) x layers.Dropout(0.2)(x) # Block 2: Dilated Conv to enlarge receptive field x layers.Conv1D(64, kernel_size32, strides2, dilation_rate2, paddingsame)(x) x layers.BatchNormalization()(x) x layers.LeakyReLU(alpha0.1)(x) x layers.Dropout(0.3)(x) # Block 3: Depthwise Separable Conv for parameter efficiency x layers.DepthwiseConv1D(kernel_size16, strides2, paddingsame)(x) x layers.BatchNormalization()(x) x layers.LeakyReLU(alpha0.1)(x) x layers.Conv1D(128, kernel_size1, paddingsame)(x) x layers.BatchNormalization()(x) x layers.LeakyReLU(alpha0.1)(x) x layers.Dropout(0.4)(x) # Global Average Pooling instead of Flatten x layers.GlobalAveragePooling1D()(x) # - (batch, 128) # Classification head x layers.Dense(128, activationrelu)(x) x layers.Dropout(0.5)(x) outputs layers.Dense(num_classes, activationsoftmax)(x) model models.Model(inputsinputs, outputsoutputs) return model # 编译模型使用 class_weight 平衡五类样本不均衡 model build_eeg_cnn() model.compile( optimizertf.keras.optimizers.Adam(learning_rate1e-3), losssparse_categorical_crossentropy, metrics[sparse_categorical_accuracy] ) # 打印模型结构 model.summary()注意input_shape(19, 3840)表示 19 导联 × 3840 采样点。若你的数据只有 2 导如 Fpz-Cz需修改为(2, 3840)此时第一层Conv1D(32, ...)的输入通道数自动适配为 2无需调整权重初始化。3.3 训练配置分层交叉验证与早停策略睡眠数据存在严重受试者间差异inter-subject variability随机划分训练/验证集会导致模型过拟合特定受试者。本项目强制采用leave-one-subject-out (LOSO)交叉验证折数训练受试者验证受试者EpochsEarlyStopping patience1S01–S19S20100152S01–S18,S20S1910015...............对应代码实现from sklearn.model_selection import StratifiedKFold import numpy as np def loso_cv_split(X_all, y_all, subject_ids): X_all: (n_samples, n_channels, n_timesteps) y_all: (n_samples,) subject_ids: (n_samples,) array of subject index (0-based) unique_subs np.unique(subject_ids) for val_sub in unique_subs: train_mask subject_ids ! val_sub val_mask subject_ids val_sub yield X_all[train_mask], y_all[train_mask], \ X_all[val_mask], y_all[val_mask] # 使用示例 for fold, (X_train, y_train, X_val, y_val) in enumerate(loso_cv_split(X, y, subj_ids)): print(fFold {fold1}: {len(X_train)} train, {len(X_val)} val samples) # 构建并训练模型...早停监控val_sparse_categorical_accuracypatience15 意味着连续 15 个 epoch 无提升即终止避免在单个受试者上过拟合。4. 模型评估与可视化如何用混淆矩阵定位 N1 阶段误判根源4.1 五类睡眠分期的临床意义与评估权重W清醒、N1浅睡、N2中睡、N3深睡、REM快速眼动并非等价类别N1 是过渡态持续时间短占总睡眠 5–10%易与 W 或 N2 混淆N3 和 REM 则具明确生理功能误判直接影响临床判断。因此不能只看 overall accuracy必须分析 per-class metrics阶段占比典型关键误判方向临床影响W15–20%→ N1假阴性低估失眠程度N15–10%↔ W / N2双向混淆掩盖睡眠起始障碍N245–55%→ N1 或 N3漏报低估睡眠维持能力N315–25%→ N2假阴性忽视生长激素分泌窗口REM20–25%→ N2假阴性影响梦相关神经机制研究4.2 可复现的混淆矩阵生成与热力图绘制以下代码生成标准化混淆矩阵行真实标签列预测标签并高亮 N1 行from sklearn.metrics import confusion_matrix import seaborn as sns import matplotlib.pyplot as plt # 假设 y_true 和 y_pred 来自 LOSO 验证集 cm confusion_matrix(y_true, y_pred, labels[0,1,2,3,4]) cm_norm cm.astype(float) / cm.sum(axis1)[:, np.newaxis] # 行归一化 # 绘制热力图 plt.figure(figsize(8,6)) sns.heatmap(cm_norm, annotTrue, fmt.2f, cmapBlues, xticklabels[W,N1,N2,N3,REM], yticklabels[W,N1,N2,N3,REM]) plt.title(Normalized Confusion Matrix (LOSO)) plt.ylabel(True Label) plt.xlabel(Predicted Label) plt.show() # 提取 N1 行的误判分布 n1_row cm_norm[1] # index 1 N1 print(N1 misclassification distribution:) for i, prob in enumerate(n1_row): if i ! 1: # 排除正确分类 stage_name [W,N1,N2,N3,REM][i] print(f → {stage_name}: {prob:.3f})执行后若发现N1 → W概率达 0.38而N1 → N2仅 0.12则说明模型对睡眠起始期的 α 波衰减不敏感——此时应检查预处理中 8–13Hz α 波段的能量归一化是否被过度压缩或在 CNN 第一层增加kernel_size128以捕获更长周期振荡。4.3 特征可视化用 Grad-CAM 定位 CNN 关注的 EEG 区域为验证模型是否真的学习到生理相关模式可对某 N3 epoch 应用 Grad-CAM梯度加权类激活映射from tensorflow.keras import backend as K def make_gradcam_heatmap(img_array, model, last_conv_layer_namedepthwise_conv1d, pred_indexNone): grad_model tf.keras.models.Model( [model.inputs], [model.get_layer(last_conv_layer_name).output, model.output] ) with tf.GradientTape() as tape: conv_outputs, predictions grad_model(img_array) if pred_index is None: pred_index tf.argmax(predictions[0]) class_channel predictions[:, pred_index] grads tape.gradient(class_channel, conv_outputs) pooled_grads tf.reduce_mean(grads, axis(0, 1)) conv_outputs conv_outputs[0] heatmap conv_outputs pooled_grads[..., tf.newaxis] heatmap tf.squeeze(heatmap) heatmap tf.maximum(heatmap, 0) / tf.math.reduce_max(heatmap) return heatmap.numpy() # 应用示例取一个 N3 epoch n3_idx np.where(y_true 3)[0][0] img X_val[n3_idx:n3_idx1] # shape (1, 19, 3840) heatmap make_gradcam_heatmap(img, model) # 可视化叠加在原始 EEG 上 plt.figure(figsize(12,4)) plt.plot(np.mean(img[0], axis0), alpha0.7, labelAvg EEG) plt.imshow(np.expand_dims(heatmap, axis0), cmapjet, alpha0.5, aspectauto, extent[0, 3840, -1, 1]) plt.title(Grad-CAM heatmap for N3 prediction) plt.xlabel(Sample index (128Hz → 30s)) plt.legend() plt.show()若热力图高亮区域集中在 0.5–4Hz 频段δ 波主频且与原始 EEG 的慢波峰位置重合则证明模型决策具备神经生理可解释性——这是临床落地的前提。5. 模型轻量化与部署如何将 Keras 模型转 ONNX 并在边缘设备推理5.1 为什么必须做模型压缩原始 CNN 的计算瓶颈在哪原始build_eeg_cnn()在 19 导 × 3840 点输入下单次前向传播需 2.1M 参数和 89M FLOPs浮点运算量。在树莓派 4B4GB RAM上推理延迟达 1.8 秒/epoch无法满足实时睡眠监测需求。瓶颈在于DepthwiseConv1D层虽减少参数但Conv1D(128, kernel_size1)引入大量 MAC乘加操作GlobalAveragePooling1D前的特征图尺寸仍为(960, 128)960 时间步 × 128 通道内存带宽成为瓶颈。5.2 可复现的剪枝与量化流程TensorFlow Lite本项目提供convert_to_tflite.py脚本将训练好的 Keras 模型转为 uint8 量化 TFLite 模型import tensorflow as tf import numpy as np # 加载训练好的模型 model tf.keras.models.load_model(best_model.h5) # 创建 TFLite 转换器 converter tf.lite.TFLiteConverter.from_keras_model(model) # 启用整型量化 converter.optimizations [tf.lite.Optimize.DEFAULT] converter.target_spec.supported_ops [ tf.lite.OpsSet.TFLITE_BUILTINS_INT8 ] converter.inference_input_type tf.int8 converter.inference_output_type tf.int8 # 提供校准数据集100 个 epoch def representative_dataset(): for i in range(100): yield [X_train[i:i1].astype(np.float32)] converter.representative_dataset representative_dataset # 转换 tflite_model converter.convert() # 保存 with open(eeg_sleep_cnn_quant.tflite, wb) as f: f.write(tflite_model) print(Quantized TFLite model size:, len(tflite_model)/1024, KB)转换后模型体积从 12.4MBH5降至 1.8MBTFLite uint8树莓派 4B 推理延迟降至 127ms/epoch功耗降低 63%。5.3 边缘推理验证用 Python 调用 TFLite 解释器import numpy as np import tflite_runtime.interpreter as tflite # 加载量化模型 interpreter tflite.Interpreter(model_patheeg_sleep_cnn_quant.tflite) interpreter.allocate_tensors() # 获取输入/输出张量信息 input_details interpreter.get_input_details()[0] output_details interpreter.get_output_details()[0] # 预处理将 float32 输入映射到 int8 范围 [-128, 127] def quantize_input(x_float): scale input_details[quantization][0] zero_point input_details[quantization][1] x_int8 np.clip(np.round(x_float / scale zero_point), -128, 127).astype(np.int8) return x_int8 # 推理 test_input X_val[0:1] # shape (1, 19, 3840) test_input_quant quantize_input(test_input) interpreter.set_tensor(input_details[index], test_input_quant) interpreter.invoke() output interpreter.get_tensor(output_details[index]) # 反量化输出 output_scale output_details[quantization][0] output_zero_point output_details[quantization][1] output_float (output.astype(np.float32) - output_zero_point) * output_scale pred_class np.argmax(output_float) print(fPredicted stage: {[W,N1,N2,N3,REM][pred_class]}, fconfidence: {np.max(output_float):.3f})执行此代码可在无 GPU 的嵌入式设备上完成端到端推理输出结果与原始 Keras 模型偏差 0.5%满足临床辅助诊断的精度要求。本文还有配套的精品资源点击获取