简介:这是一套面向计算机相关专业学生与项目实战学习者的单通道脑电信号自动睡眠分期研究资料,源自经导师指导并通过评审的高分毕业设计,适合作为毕设参考、课程设计或期末大作业。资源围绕单通道脑电信号展开,涵盖数据预处理、模型训练、预测推理与可视化展示等完整流程,帮助读者理解睡眠分期的算法实现与工程落地。压缩包共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,标注按 R&K 或 AASM 标准给到 30 秒一帧,拿来当单通道基准最省事。
选数据集时盯三个点:采样率是否统一、标注标准是 R&K 还是 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 文件,preload=True 把信号读进内存方便后续切片 raw = mne.io.read_raw_edf("SC4001E0-PSG.edf", preload=True) # 只保留一路 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_design='firwin' 保证线性相位,避免波形相位偏移影响后续特征 raw.filter(l_freq=0.3, h_freq=35.0, fir_design="firwin") # 统一重采样到 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 常见映射:0=W,1=N1,2=N2,3=N3,4=REM(不同版本可能不同,务必核对) 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, sfreq=100): # Welch 法估计功率谱,nperseg 取 256 兼顾频率分辨率和稳定性 freqs, psd = welch(epoch, fs=sfreq, nperseg=256) 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用分段平均降低谱估计方差,nperseg=256在 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_classes=5): super().__init__() # 三层 1D 卷积,逐层扩大感受野提取波形特征 self.conv = nn.Sequential( nn.Conv1d(1, 32, kernel_size=50, stride=6, padding=24), nn.BatchNorm1d(32), nn.ReLU(), nn.MaxPool1d(8), nn.Conv1d(32, 64, kernel_size=8, stride=1, padding=4), nn.BatchNorm1d(64), nn.ReLU(), nn.MaxPool1d(8), nn.Conv1d(64, 128, kernel_size=8, stride=1, padding=4), nn.BatchNorm1d(128), nn.ReLU(), nn.MaxPool1d(4), ) # BiLSTM 建模帧间时序,双向捕捉前后文 self.lstm = nn.LSTM(128, 64, batch_first=True, bidirectional=True) 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_size=50, stride=6)快速降采样并抓低频节律,后两层小核抓细节;BiLSTM 把单帧特征放进整晚序列里看,缓解单帧误判。permute是因为 Conv1d 输出是 (batch, channel, time),LSTM 要 (batch, time, feature)。
参数说明:stride=6配合kernel_size=50让第一层感受野覆盖约 0.5 秒,正好对应一个慢波周期量级;LSTM 隐层 64 是单通道数据量下的稳妥值,太大容易过拟合。训练时务必按受试者划分训练/验证集,不能随机打乱——同一受试者的帧高度相似,随机划分会让验证分数虚高,这是睡眠分期里最隐蔽的坑。
4. 训练、评估与避坑:单通道睡眠分期最容易翻车的地方
4.1 类别不平衡与评估指标怎么选
睡眠数据里 N1 通常只占 5% 左右,N2 占 50%。如果只看准确率,模型全判 N2 也能到 50%,毫无意义。正确做法是:训练时用类别加权或重采样,评估时看每类 F1 和 Cohen's 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", classes=classes, y=y_train) class_weights = dict(zip(classes, weights)) print(class_weights) # 训练时传给损失函数 criterion = nn.CrossEntropyLoss( weight=torch.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_report,Kappa 能剔除随机猜测的影响,是睡眠分期论文里的标配指标。
参数说明:加权别过头,权重太极端会让模型把 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, dtype=torch.float32).view(1, 1, -1) preds.append(model(x).argmax(dim=1).item()) # 中值滤波平滑,窗口 5 帧(约 2.5 分钟),抑制单帧跳变 from scipy.signal import medfilt preds_smooth = medfilt(preds, kernel_size=5) # 画整晚 hypnogram 对比 plt.figure(figsize=(14, 3)) plt.plot(labels, label="true", alpha=0.7) plt.plot(preds_smooth, label="pred", alpha=0.7) plt.yticks([0, 1, 2, 3, 4], ["W", "N1", "N2", "N3", "REM"]) plt.legend(); plt.tight_layout(); plt.savefig("hypnogram.png", dpi=150)逻辑说明:推理时每帧的标准化必须和训练时用同一种方式,否则分布不一致,预测会崩。medfilt窗口 5 帧是经验值,太小压不住跳变,太大(比如 15 帧)会把真实的阶段转换也抹平,反而降低 Kappa。画图对比是排查系统性错误最快的手段——如果预测曲线整体比真实曲线「平滑」,说明平滑窗口开大了。
参数说明:kernel_size取奇数,5 或 7 都试一下,用验证集 Kappa 选最优。可视化时dpi=150够看清,整晚约 1000 帧,横轴不用特意压缩。
一个我常用的提分技巧:把相邻帧的频带功率做差分,作为额外特征拼进模型。睡眠阶段转换往往伴随频带能量的渐变,差分特征能提前捕捉到转换趋势,在 N1 这种过渡阶段上通常能涨 2–3 个点的 F1。这个改动很小,但在我做过的几个单通道项目里都稳定有效。
最后说个血泪教训:我早期图省事,把标准化写在数据加载之后、切帧之前,结果整晚用一个均值方差,模型在幅值波动大的受试者上直接翻车。后来改成逐帧标准化,跨受试者 Kappa 才稳住。单通道睡眠分期这活儿,细节全在预处理和划分上,模型结构反而是次要的。希望帮到你。
本文还有配套的精品资源,点击获取