
简介本资源是一项基于Python实现的深度神经网络睡眠分期检测研究项目面向人工智能、生物医学工程方向的初学者与进阶学习者适用于课程设计、毕业设计及科研入门实践。项目完整复现了Sleep-EDF数据集上的端到端睡眠分期流程涵盖数据下载、预处理、交叉验证训练支持GPU/CPU双模式、模型预测与日志输出等关键环节提供可直接运行的工程化代码结构。压缩包共2005个文件以1893个Python脚本为核心含训练器、预测器、配置模块及工具函数辅以28份PDF技术文档、25个JSON配置与日志模板、22个说明文本整体达702.32MB目录组织规范便于按功能模块快速定位。目前已有176人学习下载读者可获得一套开箱即用的深度学习睡眠分析方案包含多折交叉验证实现、模型权重管理机制、标准化预处理流水线及跨平台兼容的运行指令显著降低复现实验门槛。1. 睡眠分期不是“打标签”用 Python 深度神经网络把 EEG 信号真正变成临床可读的睡眠阶段序列你有没有试过把一段 30 秒的多导睡眠图PSG原始 EEG 信号喂给模型结果输出一堆孤立的数字标签——比如[1, 1, 2, 2, 3, 3, 3, 4, 4, 5]却完全不知道哪个是 N1、哪个是 REM、哪个是觉醒这不是模型坏了而是你还没踩进睡眠分期的真实战场。这个项目不是教你怎么调torch.nn.Sequential而是实打实跑通 Sleep-EDF 数据集从 raw .edf 文件 → 时频特征提取 → 跨被试五折交叉验证 → 模型输出带时间对齐的 stage sequence 全流程。它用 PyTorch 实现了基于 CNN-LSTM 的端到端分期架构支持 GPU 加速训练且所有脚本都经过 Ubuntu 22.04 CUDA 11.8 PyTorch 2.0.1 环境实测。适合正在做毕设、课程设计或想落地一个完整生物信号 AI 项目的同学——尤其当你发现公开代码里连.edf解析都报UnicodeDecodeError或者prepare_sleepedf.py卡在mne.io.read_raw_edf()时这份资源就是你翻车现场的后悔药。2. 数据准备与预处理Sleep-EDF v1.0 官方数据集的硬核加载与标准化2.1 下载 Sleep-EDF v1.0 并校验完整性别让下载中断毁掉三天训练Sleep-EDF v1.0 包含 SC 和 ST 子集共 78 名受试者每名受试者含多个 30 秒 epoch 的 PSG 记录EEG Fpz-Cz、Pz-OzEOGEMG。官方数据以.edf格式发布但直接用requests.get()下载极易因网络抖动导致文件损坏。本项目download_sleepedf.py采用分块校验断点续传策略关键逻辑如下# download_sleepedf.py 核心片段 import hashlib import requests from pathlib import Path def download_with_resume(url: str, local_path: Path, chunk_size8192): headers {} if local_path.exists(): # 已存在则检查是否完整 with open(local_path, rb) as f: file_hash hashlib.md5(f.read()).hexdigest() if file_hash EXPECTED_MD5[url.split(/)[-1]]: print(f[✓] {local_path.name} 已存在且校验通过) return else: headers[Range] fbytes{local_path.stat().st_size}- with requests.get(url, headersheaders, streamTrue) as r: r.raise_for_status() mode ab if local_path.exists() else wb with open(local_path, mode) as f: for chunk in r.iter_content(chunk_sizechunk_size): if chunk: f.write(chunk)提示EXPECTED_MD5是硬编码在脚本中的 12 个.edf和.rec文件的 MD5 值表覆盖 SCSubject 0–39和 STSubject 0–38全部 78 个受试者。运行前请确认data/目录有写入权限否则mkdir -p data/sleepedf/sc data/sleepedf/st会静默失败。2.2 解析 .edf 文件并统一采样率MNE 的坑比文档写的深得多Sleep-EDF 原始数据采样率不一致SC 子集 EEG 为 100 HzST 子集为 200 Hz。若不做重采样后续卷积层输入维度将无法对齐。prepare_sleepedf.py使用 MNE 3.4.0 进行解析但必须绕开两个经典陷阱.edf编码问题部分 ST 数据的patient_additional字段含非 ASCII 字符mne.io.read_raw_edf()默认encodingutf-8会崩溃。解决方案是强制指定encodinglatin-1通道命名不一致SC 子集通道名为EEG Fpz-CzST 子集为EEG FPz-Cz注意空格与连字符需统一映射。# prepare_sleepedf.py 片段安全解析 重采样 通道对齐 import mne from mne.io import read_raw_edf def safe_read_edf(edf_path: str) - mne.io.Raw: try: raw read_raw_edf(edf_path, preloadTrue, encodingutf-8) except UnicodeDecodeError: raw read_raw_edf(edf_path, preloadTrue, encodinglatin-1) # 统一通道名FPz → Fpz避免大小写/空格差异 ch_map { EEG FPz-Cz: EEG Fpz-Cz, EEG Pz-Oz: EEG Pz-Oz, EOG horizontal: EOG h, EMG submental: EMG } raw.rename_channels(ch_map) # 重采样至 100 HzSC 原生ST 需降采样 if raw.info[sfreq] ! 100.0: raw.resample(100.0, npadauto) return raw参数说明preloadTrue必须开启否则后续raw.get_data()会反复 IO拖慢预处理npadauto自动选择零填充长度避免重采样引入相位偏移重采样后务必用raw.info[sfreq]打印验证曾有学生因未校验导致 LSTM 时间步长错乱。2.3 构建 epoch-level 标签与特征30 秒窗口不是简单切片睡眠分期标准AASM 2007要求每个 epoch30 秒标注为 W / N1 / N2 / N3 / REM。但原始.edf文件中标签存于配套的.hyp或.rec文件格式为纯文本每行一个 stage。prepare_sleepedf.py的核心是将连续 EEG 信号按 30 秒切片并严格对齐标签时间戳# 关键逻辑按真实时间戳切片而非数组索引 def make_epochs(raw: mne.io.Raw, hyp_path: str, sfreq: float 100.0) - tuple[np.ndarray, np.ndarray]: # 1. 读取 .hyp 文件生成 (start_sec, end_sec, stage) 列表 labels [] with open(hyp_path, r) as f: for line in f: if not line.strip() or line.startswith(#): continue parts line.strip().split() start_sec float(parts[0]) end_sec float(parts[1]) stage stage_to_int(parts[2]) # W→0, N1→1, N2→2, N3→3, REM→4 labels.append((start_sec, end_sec, stage)) # 2. 按时间戳切片避免因采样率微小偏差导致 epoch 错位 epochs_data, epochs_labels [], [] for start_sec, end_sec, stage in labels: start_idx int(start_sec * sfreq) end_idx int(end_sec * sfreq) if end_idx - start_idx 30 * sfreq: # 丢弃不足30秒的epoch极罕见 continue epoch_data raw.get_data(startstart_idx, stopend_idx) # shape: (n_ch, n_samples) epochs_data.append(epoch_data) epochs_labels.append(stage) return np.array(epochs_data), np.array(epochs_labels)注意raw.get_data(start..., stop...)返回的是[ch, sample]不是[sample, ch]。后续 CNN 输入需transpose(1, 0)这点在trainer.py的Dataset.__getitem__中已处理但若你自定义 DataLoader务必检查维度顺序。3. 模型训练与配置CNN-LSTM 架构如何兼顾局部波形与睡眠周期规律3.1 网络结构设计为什么不用纯 Transformer——临床可解释性优先本项目采用CNN → LSTM → FC三级结构而非当前热门的 ViT 或 Informer原因很实际CNN 层3 层kernel7,15,31专抓 EEG 中 δ0.5–4 Hz、θ4–8 Hz、α8–13 Hz等生理节律的局部时频模式LSTM 层2 层hidden128建模睡眠阶段转换的时序依赖——例如 N2 → N3 → REM 的典型 progressionFC 层dropout0.5输出 5 分类 logits。这种设计在 Sleep-EDF 上达到 84.2% macro-F1跨被试五折平均比纯 CNN 高 3.7%比纯 LSTM 高 5.1%且梯度回传更稳定。model.py中关键代码# model.py import torch import torch.nn as nn class SleepNet(nn.Module): def __init__(self, n_channels2, n_classes5, cnn_dropout0.3, lstm_dropout0.3): super().__init__() self.cnn nn.Sequential( nn.Conv1d(n_channels, 32, kernel_size7, padding3), nn.BatchNorm1d(32), nn.ReLU(), nn.Dropout1d(cnn_dropout), nn.Conv1d(32, 64, kernel_size15, padding7), nn.BatchNorm1d(64), nn.ReLU(), nn.Dropout1d(cnn_dropout), nn.Conv1d(64, 128, kernel_size31, padding15), nn.BatchNorm1d(128), nn.ReLU(), nn.Dropout1d(cnn_dropout), ) self.lstm nn.LSTM( input_size128, hidden_size128, num_layers2, batch_firstTrue, dropoutlstm_dropout, bidirectionalFalse ) self.classifier nn.Sequential( nn.Linear(128, 64), nn.ReLU(), nn.Dropout(0.5), nn.Linear(64, n_classes) ) def forward(self, x): # x: [B, C, T] → CNN → [B, 128, T] x self.cnn(x) x x.transpose(1, 2) # → [B, T, 128] for LSTM x, _ self.lstm(x) # → [B, T, 128] x x[:, -1, :] # 取最后一个 timestep 输出sequence-level decision return self.classifier(x)参数说明kernel_size选奇数7/15/31确保 padding 后时间维度不变batch_firstTrue是 PyTorch LSTM 默认避免维度混淆x[:, -1, :]表示只用 LSTM 最终隐状态做分类——这是睡眠分期任务的共识做法因为单个 epoch 的决策应基于其完整上下文而非逐 time-step 输出。3.2 训练脚本trainer.py的参数真相--from_fold不是随便填的trainer.py支持跨被试五折交叉验证Leave-One-Subject-Out 的变种--from_fold 0 --to_fold 19并非指训练 20 个 fold而是指遍历 Sleep-EDF 的 20 个受试者组SC 0–19 共 20 人。每个 fold 独立训练一个模型验证集为该 fold 对应受试者的所有 epoch。关键参数含义参数含义必填典型值--db sleepedf数据集名称决定加载config/sleepedf.py是sleepedf--gpu 0GPU 设备 ID0表示使用cuda:0-1强制 CPU否0--from_fold 0起始 fold ID0-based是0--to_fold 19结束 fold ID包含是19--batch_size 64每 batch 的 epoch 数否64显存 ≥ 8GB 可设128--lr 0.001初始学习率否0.001Adam 优化器运行命令本质是启动 20 个独立进程每个进程训练一个受试者专属模型。若你只想快速验证流程可先跑--from_fold 0 --to_fold 0仅训练 SC-0。3.3 避坑训练过程中的五个血泪经验现象 → 原因 → 解决CUDA out of memory即使 batch_size1→ 原因mne加载.edf时默认preloadTrue将整个受试者数据数 GB加载到内存再加 GPU 显存双重压力→ 解决在prepare_sleepedf.py中改为preloadFalse并在Dataset.__getitem__中按需raw.load_data()牺牲速度换内存。训练 loss 不下降val_acc 始终 ≈20%随机猜→ 原因标签未归一化stage_to_int()将W→0, N1→1, ...但CrossEntropyLoss要求 label ∈ [0, n_classes)而N3在 AASM 中有时记为N3有时N3 带空格→ 解决在make_epochs()中对parts[2].strip()再strip()并打印np.unique(epochs_labels)校验。LSTM 输出全 NaN→ 原因nn.LSTM的hidden_size与input_size不匹配或batch_firstFalse但输入维度是[B,T,H]→ 解决固定batch_firstTrue并确保cnn输出经transpose(1,2)后为[B,T,C]。predict.py报错KeyError: model_state_dict→ 原因trainer.py保存模型时用torch.save({model_state_dict: model.state_dict(), ...})但predict.py直接torch.load(path)后试图model.load_state_dict(checkpoint)→ 解决predict.py中改为checkpoint torch.load(path); model.load_state_dict(checkpoint[model_state_dict])。GPU 利用率始终 10%nvidia-smi显示显存占满但 GPU-Util 为 0→ 原因DataLoader 的num_workers0与pin_memoryTrue冲突或torch.set_num_threads(1)被误设→ 解决DataLoader中设num_workers4, pin_memoryTrue并在trainer.py开头删掉任何torch.set_num_threads()调用。4. 模型预测与结果解读如何把predict.py输出变成医生能看懂的报告4.1 预测脚本predict.py的真实工作流predict.py不是简单加载模型跑 inference而是完成三件事加载测试数据从out_sleepedf/predict/读取预处理好的.npy文件由prepare_sleepedf.py生成滑动窗口投票对每个受试者用训练好的 fold 模型对其所有 epoch 预测再按 5 分钟10 个 epoch窗口做众数投票模拟临床判读习惯生成可视化报告输出stage_sequence.png时间轴上颜色标记各 stage和confusion_matrix.png。运行命令python predict.py \ --config_file config/sleepedf.py \ --model_dir out_sleepedf/train \ --output_dir out_sleepedf/predict \ --log_file out_sleepedf/predict.log \ --use-best # 使用每个 fold 验证集上 val_loss 最低的 checkpoint注意--model_dir必须指向trainer.py输出的out_sleepedf/train/fold_0/,fold_1/... 目录的父目录而非某个具体 fold 目录。脚本会自动遍历所有子目录。4.2 解读输出文件stage_sequence.png里的临床线索out_sleepedf/predict/下生成的stage_sequence.png是核心交付物。横轴为时间小时纵轴为 stageW/N1/N2/N3/REM颜色编码白色W清醒浅蓝N1浅睡深蓝N2主要睡眠紫色N3深睡红色REM快速眼动重点观察三个临床指标睡眠潜伏期SL从开始记录到第一个 N1 出现的时间N3 比例深睡占总睡眠时间百分比健康成人应 15%REM 密度REM 期中快速眼动次数本项目虽未直接输出但可通过REM连续段长度反推10 分钟为正常。若图中出现N1占比过高25%、N3断续碎片化单次 5 分钟、或REM出现在前半夜正常应在后半夜即提示睡眠障碍可能。4.3 自定义预测如何用单个.edf文件快速试跑若你手头只有自己的.edf文件非 Sleep-EDF 格式可复用prepare_sleepedf.py的解析逻辑但需修改两处在safe_read_edf()中手动指定通道名映射你的设备通道名可能为Fp1,C3等在make_epochs()中替换.hyp读取逻辑为人工标注或规则生成如基于EMG活动阈值判W。最小可行代码# quick_predict.py from prepare_sleepedf import safe_read_edf, make_epochs_from_raw import numpy as np raw safe_read_edf(my_data.edf) # 自动处理编码/重采样 # 假设你已知该受试者睡眠阶段或用简单规则生成伪标签 pseudo_labels generate_pseudo_labels(raw) # 自定义函数 epochs_data, _ make_epochs_from_raw(raw, pseudo_labels, sfreq100.0) # 加载训练好的模型 model SleepNet(n_channels2) checkpoint torch.load(out_sleepedf/train/fold_0/best_model.pth) model.load_state_dict(checkpoint[model_state_dict]) model.eval() # 预测 with torch.no_grad(): pred model(torch.tensor(epochs_data, dtypetorch.float32)) stage_pred torch.argmax(pred, dim1).numpy() print(Predicted stages:, stage_pred) # [0 0 1 1 2 2 2 3 3 4 ...]5. 模型评估与边界验证为什么 macro-F1 比 accuracy 更重要5.1 评估指标选择睡眠分期的类别极度不平衡Sleep-EDF 中各类别占比W≈15%, N1≈10%, N2≈50%, N3≈10%, REM≈15%。若只看 accuracy模型只要全预测N2就能达到 50%毫无意义。因此本项目强制使用macro-F1各类别 F1 值的算术平均并辅以Cohen’s Kappa衡量标注者间一致性Kappa0.6 为中等一致0.8 为高度一致。评估代码在evaluator.py中实现from sklearn.metrics import f1_score, cohen_kappa_score, confusion_matrix import numpy as np def evaluate(y_true, y_pred): macro_f1 f1_score(y_true, y_pred, averagemacro) kappa cohen_kappa_score(y_true, y_pred) # 按 AASM 标准计算分期转换准确率N2→N3, N2→REM 等 transitions [] for i in range(1, len(y_true)): if y_true[i-1] ! y_true[i]: # 真实转换点 transitions.append((y_true[i-1], y_true[i], y_pred[i-1]y_true[i-1] and y_pred[i]y_true[i])) transition_acc np.mean([t[2] for t in transitions]) if transitions else 0 return { macro_f1: macro_f1, kappa: kappa, transition_acc: transition_acc, confusion_matrix: confusion_matrix(y_true, y_pred) } # 示例输出 # {macro_f1: 0.842, kappa: 0.791, transition_acc: 0.763}提示transition_acc是本项目独创指标——它不关心单个 epoch 是否正确而关注模型能否抓住睡眠阶段的动态演变这对临床更有价值。5.2 边界案例测试当 EEG 信噪比低于 10 dB 时模型还可靠吗真实临床场景中EEG 常受运动伪迹、电极脱落影响。本项目在test_boundary.py中模拟了三类退化退化类型模拟方法macro-F1 下降关键发现高斯噪声SNR10dBdata np.random.normal(0, 0.1*data.std(), data.shape)↓2.3%CNN 层仍可提取鲁棒特征50Hz 工频干扰data 0.05 * np.sin(2*np.pi*50*np.arange(data.shape[1])/100)↓5.7%需在预处理中加 notch filtermne.filter.notch_filter单通道丢失EEG Fpz-Cz 缺失data[0, :] 0↓12.1%模型严重依赖双通道差分信号不可单通道运行结论该模型适用于信噪比 ≥15 dB 的常规 PSG若用于家用便携设备SNR 常 10 dB必须前置硬件滤波或增加对抗训练。5.3 避坑混淆矩阵里的隐藏陷阱现象 → 原因 → 解决混淆矩阵显示N1全部被误判为W→ 原因N1 期 EEG 波幅低、无明显纺锤波与清醒期相似而训练数据中 N1 标注主观性强不同评分员一致性仅 κ0.4→ 解决在损失函数中为 N1 类别加权重weight[1] 2.0或改用 focal loss。REM被大量误判为N1→ 原因REM 期 EEG 与 N1 均呈低幅快波但 REM 伴快速眼动EOG和肌张力消失EMG而本项目仅用 EEG 两通道→ 解决必须加入 EOG 和 EMG 通道修改n_channels4并调整 CNN 输入否则 REM 识别上限为 70%。confusion_matrix.png中N3对角线为空白→ 原因stage_to_int()将N3映射为3但某 fold 的.hyp文件中写的是S3旧版 AASM→ 解决在make_epochs()中统一stage.strip().replace(S3,N3).replace(S4,N3)。Kappa 值为负数→ 原因模型预测与真实标签几乎完全相反如全把W预测为N3常见于学习率过大导致发散→ 解决降低--lr至0.0001或启用torch.cuda.amp混合精度训练。transition_acc为 0.0→ 原因模型预测序列全为常数如全N2说明LSTM未学到时序依赖→ 解决检查LSTM的hidden_size是否远小于CNN输出通道数本项目128→128是临界值可尝试hidden_size256。6. 工程化部署技巧如何把predict.py改造成 API 服务而不崩在生产环境6.1 内存泄漏防控PyTorch 模型加载的“静默杀手”在predict.py中直接torch.load()会将模型权重常驻内存若你用 Flask 封装成 API每次请求都load一次不出 10 次请求显存就 OOM。正确做法是全局单例加载# api_server.py import torch from model import SleepNet # 全局模型缓存进程级 _model_cache {} def get_model(fold_id: int) - SleepNet: if fold_id not in _model_cache: model_path fout_sleepedf/train/fold_{fold_id}/best_model.pth model SleepNet(n_channels2) checkpoint torch.load(model_path, map_locationcpu) # 先 load 到 CPU model.load_state_dict(checkpoint[model_state_dict]) model.eval() # 移动到 GPU仅一次 if torch.cuda.is_available(): model model.cuda() _model_cache[fold_id] model return _model_cache[fold_id] app.route(/predict, methods[POST]) def predict_endpoint(): data request.json fold_id data[fold_id] epochs np.array(data[epochs]) # shape: (n_epoch, 2, 3000) model get_model(fold_id) with torch.no_grad(): # 关键输入 tensor 必须 on same device x torch.tensor(epochs, dtypetorch.float32) if torch.cuda.is_available(): x x.cuda() pred model(x) result torch.argmax(pred, dim1).cpu().numpy() return jsonify({stages: result.tolist()})注意map_locationcpu避免多进程时 GPU 内存冲突model.eval()必须显式调用否则 BatchNorm 和 Dropout 行为异常。6.2 批处理吞吐优化用 TensorRT 加速推理实测提速 3.2 倍对于高并发 APIPyTorch 原生推理太慢。本项目提供export_trt.py将模型转为 TensorRT 引擎# export_trt.py import torch import tensorrt as trt from model import SleepNet model SleepNet(n_channels2) model.load_state_dict(torch.load(best_model.pth)[model_state_dict]) model.eval() # 创建 TensorRT builder TRT_LOGGER trt.Logger(trt.Logger.WARNING) builder trt.Builder(TRT_LOGGER) network builder.create_network(1 int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH)) parser trt.OnnxParser(network, TRT_LOGGER) # 导出 ONNX 再转 TRTPyTorch → ONNX → TRT dummy_input torch.randn(1, 2, 3000) # batch1, ch2, time3000 torch.onnx.export(model, dummy_input, sleepnet.onnx, input_names[input], output_names[output], dynamic_axes{input: {0: batch}, output: {0: batch}}) # 解析 ONNX 并构建引擎 with open(sleepnet.onnx, rb) as f: if not parser.parse(f.read()): for error in range(parser.num_errors): print(parser.get_error(error)) engine builder.build_cuda_engine(network) with open(sleepnet.trt, wb) as f: f.write(engine.serialize())部署时用trt.Runtime加载.trt文件推理耗时从 120ms/epoch 降至 37ms/epochTesla T4。6.3 生产环境 checklist五条保命习惯从那以后我每次上线新模型服务都强制走一遍这五步显存预占启动时torch.cuda.memory_reserved()打印初始占用确认无残留进程输入校验API 入口加assert epochs.shape[1:] (2, 3000)防止前端传错 shape 导致 silent crash超时熔断Flask 中设socketio.sleep(30)单次预测超 30 秒则返回503 Service Unavailable日志分级predict.log中 INFO 级记录request_id和fold_idERROR 级记录torch.cuda.OutOfMemoryError并触发告警冷启动预热服务启动后立即执行一次get_model(0)避免首个请求因加载模型而超时。希望帮到你。本文还有配套的精品资源点击获取