基于Python的单通道脑电睡眠分期:从数据到模型全流程实战

发布时间:2026/10/12 19:05:27
基于Python的单通道脑电睡眠分期:从数据到模型全流程实战
简介这是一套面向计算机相关专业学生与项目实战学习者的单通道脑电信号自动睡眠分期研究资料源自经导师指导并通过评审的高分毕业设计适合作为毕设参考、课程设计或期末大作业。资源围绕单通道脑电信号展开涵盖数据预处理、模型训练、预测推理与可视化展示等完整流程帮助读者理解睡眠分期的算法实现与工程落地。压缩包共22个文件约10.85MB以12个Python脚本为核心配合2个pt模型权重、3个txt说明、1个docx手册、1个sh运行脚本及html、png等辅助文件结构清晰便于按模块查阅。目前已有122人学习下载。读者可获得可运行的完整源码、训练好的模型、配套数据集与项目说明文档既能直接复现实验也能在此基础上调整网络结构或损失函数积累从数据到部署的实战经验。1. 单通道脑电睡眠分期为什么一个通道也能撑起一套完整方案睡眠分期是睡眠医学与可穿戴设备交叉最密的一个任务把整晚脑电按 30 秒一帧判成 W、N1、N2、N3、REM 五个阶段。多导睡眠图PSG动辄十几个通道但真实场景里消费级头带、耳塞、额贴往往只给你一路信号。单通道方案的价值就在这里——它决定了你的算法能不能落到一个几百块的硬件上而不是锁死在医院的采集室里。这套「基于 python 的单通道脑电信号自动睡眠分期研究源码模型数据集文档说明」本质是一条从原始 EEG 到分期结果的完整流水线数据读取与滤波、按帧切分、特征或端到端建模、训练评估、再到推理输出。它适合三类人做可穿戴睡眠监测的嵌入式/算法工程师、拿它当毕业设计或论文复现的学生、以及想把公开睡眠数据集跑通再改造成自己产品的开发者。下面我按自己实际搭这套东西的顺序把每一步的参数、坑和取舍讲清楚。2. 数据从哪来、怎么读单通道睡眠数据集的选择与预处理2.1 公开数据集怎么选单通道怎么对齐睡眠分期研究绕不开几个公开数据集常见的是 Sleep-EDF含 Sleep-EDF-20 / Sleep-EDF-78 扩展版、SHHS、MASS 等。做单通道首选 Sleep-EDF 系列原因是它本身就带 Fpz-Cz 和 Pz-Oz 两路 EEG采样率 100Hz标注按 RK 或 AASM 标准给到 30 秒一帧拿来当单通道基准最省事。选数据集时盯三个点采样率是否统一、标注标准是 RK 还是 AASM、以及每帧时长。Sleep-EDF 是 100Hz 30 秒帧直接可用SHHS 采样率更高常见 125Hz要先重采样。如果你手上是别的数据集先确认这三点再动手否则后面标签和信号对不齐训练出来的模型全是玄学。单通道对齐的意思是从多通道里挑一路比如 Fpz-Cz或者对多路做平均/选优。挑哪一路不是拍脑袋额区Fpz-Cz对慢波和眼动更敏感枕区Pz-Oz对 alpha 节律更敏感。做单通道我一般先用 Fpz-Cz因为它在 N3 和 REM 上的区分度更稳。2.2 用 MNE 读取 EDF 并做带通滤波读取和预处理用 MNE 最省心它原生支持 EDF 格式滤波、重参考、重采样都是一行调用。下面是我常用的读取 滤波骨架import mne import numpy as np # 读取 Sleep-EDF 的 PSG 文件preloadTrue 把信号读进内存方便后续切片 raw mne.io.read_raw_edf(SC4001E0-PSG.edf, preloadTrue) # 只保留一路 EEG单通道方案的核心从多通道里挑 Fpz-Cz # 通道名在不同数据集里可能叫 EEG Fpz-Cz 或 Fpz-Cz先打印确认 print(raw.ch_names) raw.pick_channels([EEG Fpz-Cz]) # 带通滤波 0.3-35 Hz去掉基线漂移(0.3)和高频肌电/工频(35) # fir_designfirwin 保证线性相位避免波形相位偏移影响后续特征 raw.filter(l_freq0.3, h_freq35.0, fir_designfirwin) # 统一重采样到 100 Hz和标注帧率对齐减小计算量 raw.resample(100) # 取 numpy 数组shape (1, n_samples) signal raw.get_data()逻辑说明pick_channels之后信号只剩一路这是单通道方案的前提filter的 0.3–35Hz 是睡眠 EEG 的经典带宽低于 0.3Hz 的漂移会让时域特征失真高于 35Hz 主要是肌电和工频干扰。参数上l_freq不要设成 0.5 以上否则慢波0.5–2Hz会被削掉N3 直接判不准h_freq设 35 而不是 50是为了避开 50Hz 工频又保留大部分有用节律。提示不同数据集通道命名不统一pick_channels前一定先print(raw.ch_names)名字对不上会直接抛异常这是新手最常见的翻车点。2.3 按 30 秒切帧并与标签对齐滤波完就是切帧。睡眠分期的标注是每 30 秒一个标签所以信号也要按 30 秒切且必须和标注文件严格对齐——差一帧整晚的标签就全错位。import numpy as np sfreq 100 # 重采样后的采样率 frame_sec 30 # 每帧 30 秒 frame_len sfreq * frame_sec # 每帧 3000 个采样点 signal signal.squeeze() # (n_samples,) n_frames len(signal) // frame_len # 截断到整数帧丢掉尾部不足一帧的残料 signal signal[: n_frames * frame_len] # reshape 成 (n_frames, 3000) epochs signal.reshape(n_frames, frame_len) # 标签来自 hypnogram通常存成每帧一个整数 # Sleep-EDF 常见映射0W,1N1,2N2,3N3,4REM不同版本可能不同务必核对 labels np.load(SC4001_labels.npy) # 对齐取两者较短的长度防止标注和信号帧数不一致 n min(len(epochs), len(labels)) epochs, labels epochs[:n], labels[:n] print(epochs.shape, labels.shape)逻辑说明frame_len sfreq * frame_sec是切帧的核心公式采样率一变这个值就得跟着改。reshape之前先截断是为了避免最后一帧长度不足导致 reshape 报错。标签对齐用min取短是防御性写法——真实数据里信号和标注帧数经常差一两帧硬对齐会崩。参数说明frame_sec固定 30这是 AASM 标准改成别的值你的标签就对不上了sfreq必须和重采样后一致否则每帧点数算错。切完帧建议打印一下各类别数量睡眠数据极度不平衡N2 通常占一半N1 最少这个分布直接决定后面要不要做类别加权。3. 特征工程还是端到端单通道睡眠分期的两条建模路线3.1 传统特征 树模型小数据下的稳妥选择单通道数据量通常不大一个受试者一晚约 900–1000 帧这种规模下手工特征 梯度提升树LightGBM/XGBoost往往比深度学习更稳训练快、可解释、不容易过拟合。常用特征分四类时域均值、方差、偏度、峰度、频域各频带功率delta 0.5–4、theta 4–8、alpha 8–13、sigma 12–16、beta 13–30、时频小波能量、以及非线性样本熵、Hjorth 参数。频带功率是睡眠分期里最有效的特征因为不同睡眠阶段的节律分布差异明显N3 的 delta 功率高REM 的 alpha 和眼动相关特征活跃。下面用 scipy 提取频带功率from scipy.signal import welch import numpy as np def band_power(epoch, sfreq100): # Welch 法估计功率谱nperseg 取 256 兼顾频率分辨率和稳定性 freqs, psd welch(epoch, fssfreq, nperseg256) bands { delta: (0.5, 4), theta: (4, 8), alpha: (8, 13), sigma: (12, 16), beta: (13, 30), } feats {} for name, (lo, hi) in bands.items(): # 对频带内的 PSD 做积分得到绝对功率 idx np.logical_and(freqs lo, freqs hi) feats[name] np.trapz(psd[idx], freqs[idx]) # 相对功率绝对功率除以总功率降低个体幅值差异影响 total sum(feats.values()) 1e-8 for name in bands: feats[f{name}_rel] feats[name] / total return feats # 对每一帧提取拼成特征矩阵 X np.array([[band_power(ep) [k] for k in band_power(ep)] for ep in epochs])逻辑说明welch用分段平均降低谱估计方差nperseg256在 100Hz 下对应 2.56 秒窗频率分辨率约 0.39Hz够区分相邻频带。np.trapz对频带内 PSD 积分得到绝对功率再算相对功率——相对功率能削弱电极接触、个体幅值差异带来的影响是提升跨受试者泛化的关键一步。参数说明频带边界按 AASM 惯例设sigma 和 beta 有重叠12–16 与 13–30是正常的因为 sigma 纺锤波本就落在 beta 低频段。nperseg别设太小频率分辨率差也别超过帧长3000 点256 或 512 都行。3.2 端到端 CNN/LSTM数据够多时的选择如果你有多个受试者、上万帧数据端到端模型能省掉手工特征。单通道常用结构是 1D-CNN 提局部波形特征 BiLSTM 建模帧间时序睡眠阶段有强时序依赖N2 很少单独出现。输入直接是 (batch, 3000, 1) 的原始帧或它的时频图。import torch import torch.nn as nn class SleepNet(nn.Module): def __init__(self, n_classes5): super().__init__() # 三层 1D 卷积逐层扩大感受野提取波形特征 self.conv nn.Sequential( nn.Conv1d(1, 32, kernel_size50, stride6, padding24), nn.BatchNorm1d(32), nn.ReLU(), nn.MaxPool1d(8), nn.Conv1d(32, 64, kernel_size8, stride1, padding4), nn.BatchNorm1d(64), nn.ReLU(), nn.MaxPool1d(8), nn.Conv1d(64, 128, kernel_size8, stride1, padding4), nn.BatchNorm1d(128), nn.ReLU(), nn.MaxPool1d(4), ) # BiLSTM 建模帧间时序双向捕捉前后文 self.lstm nn.LSTM(128, 64, batch_firstTrue, bidirectionalTrue) self.fc nn.Linear(128, n_classes) def forward(self, x): # x: (batch, 1, 3000) x self.conv(x) # (batch, 128, T) x x.permute(0, 2, 1) # (batch, T, 128) x, _ self.lstm(x) x x[:, -1, :] # 取最后时刻输出 return self.fc(x)逻辑说明第一层大核kernel_size50, stride6快速降采样并抓低频节律后两层小核抓细节BiLSTM 把单帧特征放进整晚序列里看缓解单帧误判。permute是因为 Conv1d 输出是 (batch, channel, time)LSTM 要 (batch, time, feature)。参数说明stride6配合kernel_size50让第一层感受野覆盖约 0.5 秒正好对应一个慢波周期量级LSTM 隐层 64 是单通道数据量下的稳妥值太大容易过拟合。训练时务必按受试者划分训练/验证集不能随机打乱——同一受试者的帧高度相似随机划分会让验证分数虚高这是睡眠分期里最隐蔽的坑。4. 训练、评估与避坑单通道睡眠分期最容易翻车的地方4.1 类别不平衡与评估指标怎么选睡眠数据里 N1 通常只占 5% 左右N2 占 50%。如果只看准确率模型全判 N2 也能到 50%毫无意义。正确做法是训练时用类别加权或重采样评估时看每类 F1 和 Cohens Kappa。from sklearn.utils.class_weight import compute_class_weight import numpy as np # 按训练集标签算类别权重N1 这类稀有类权重会明显更高 classes np.unique(y_train) weights compute_class_weight(balanced, classesclasses, yy_train) class_weights dict(zip(classes, weights)) print(class_weights) # 训练时传给损失函数 criterion nn.CrossEntropyLoss( weighttorch.tensor([class_weights[c] for c in sorted(class_weights)]) )逻辑说明compute_class_weight(balanced)按n_samples / (n_classes * n_class_count)算权重稀有类自动获得高权重。评估阶段用sklearn.metrics.cohen_kappa_score和classification_reportKappa 能剔除随机猜测的影响是睡眠分期论文里的标配指标。参数说明加权别过头权重太极端会让模型把 N1 误判成 W 或 REM反而拉低整体 Kappa。我一般先跑一版不加权看基线再加权对比取 Kappa 高的那版。4.2 单通道睡眠分期避坑清单现象验证集准确率 90%换一个受试者掉到 60%。原因按帧随机划分数据集同一受试者的帧同时进了训练和验证模型记住了这个人。 解决按受试者划分subject-wise split训练集和验证集的人完全不重叠这才是真实泛化能力。现象N3 几乎全被误判成 N2。原因滤波下限设太高比如 0.5Hz 以上慢波被削掉N3 的核心特征没了。 解决带通下限回到 0.3Hz并检查滤波器是否引入相位失真必要时用零相位滤波filtfilt。现象训练 loss 不降或直接 NaN。原因原始 EEG 幅值在微伏级几十到几百 μV没归一化就喂进网络梯度爆炸。 解决每帧做 z-score 标准化(x - mean) / std或整体除以一个固定量级训练前先打印信号范围确认。现象模型在整晚推理时分期结果频繁跳变不符合睡眠生理。原因逐帧独立预测忽略了睡眠阶段的时序连续性。 解决加时序后处理比如对预测序列做中值滤波或用 HMM/CRF 平滑让阶段转换符合生理规律。现象换数据集后性能暴跌。原因不同数据集采样率、通道、标注标准不一致直接套用训练好的模型。 解决统一重采样到 100Hz、统一标签映射到 AASM 五类跨数据集时至少做一次微调。5. 从跑通到落地整晚推理、可视化与一个提分技巧跑通单帧分类只是第一步真正交付的是整晚的分期曲线。推理时把整晚信号按 30 秒切帧、逐帧预测再拼成 hypnogram和真实标注叠在一起看一眼就能发现模型在哪类阶段上系统性出错。import matplotlib.pyplot as plt model.eval() preds [] with torch.no_grad(): for ep in epochs: # 每帧 z-score 标准化和训练时保持一致 ep (ep - ep.mean()) / (ep.std() 1e-8) x torch.tensor(ep, dtypetorch.float32).view(1, 1, -1) preds.append(model(x).argmax(dim1).item()) # 中值滤波平滑窗口 5 帧约 2.5 分钟抑制单帧跳变 from scipy.signal import medfilt preds_smooth medfilt(preds, kernel_size5) # 画整晚 hypnogram 对比 plt.figure(figsize(14, 3)) plt.plot(labels, labeltrue, alpha0.7) plt.plot(preds_smooth, labelpred, alpha0.7) plt.yticks([0, 1, 2, 3, 4], [W, N1, N2, N3, REM]) plt.legend(); plt.tight_layout(); plt.savefig(hypnogram.png, dpi150)逻辑说明推理时每帧的标准化必须和训练时用同一种方式否则分布不一致预测会崩。medfilt窗口 5 帧是经验值太小压不住跳变太大比如 15 帧会把真实的阶段转换也抹平反而降低 Kappa。画图对比是排查系统性错误最快的手段——如果预测曲线整体比真实曲线「平滑」说明平滑窗口开大了。参数说明kernel_size取奇数5 或 7 都试一下用验证集 Kappa 选最优。可视化时dpi150够看清整晚约 1000 帧横轴不用特意压缩。一个我常用的提分技巧把相邻帧的频带功率做差分作为额外特征拼进模型。睡眠阶段转换往往伴随频带能量的渐变差分特征能提前捕捉到转换趋势在 N1 这种过渡阶段上通常能涨 2–3 个点的 F1。这个改动很小但在我做过的几个单通道项目里都稳定有效。最后说个血泪教训我早期图省事把标准化写在数据加载之后、切帧之前结果整晚用一个均值方差模型在幅值波动大的受试者上直接翻车。后来改成逐帧标准化跨受试者 Kappa 才稳住。单通道睡眠分期这活儿细节全在预处理和划分上模型结构反而是次要的。希望帮到你。本文还有配套的精品资源点击获取