十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

单通道脑电睡眠分期实战:Python实现五分类自动分期

单通道脑电睡眠分期实战:Python实现五分类自动分期 简介这是一套面向计算机相关专业学生与项目实战学习者的单通道脑电信号自动睡眠分期研究完整资料源自经导师指导并通过评审的高分毕业设计适合作为毕设参考、课程设计或期末大作业。资源包共22个文件约10.85MB以12个Python源码文件为核心涵盖数据预处理、模型训练、预测与Web服务等模块另含2个pt模型权重、3个txt说明、1个docx手册、1个sh运行脚本及html、png等辅助文件结构清晰便于按模块查阅。项目围绕单通道脑电信号展开包含数据下载、特征准备、网络定义、训练测试与可视化展示等环节并配有项目说明文档帮助读者理解从数据到模型再到应用的完整流程。目前已有122人学习下载可作为睡眠分期方向入门与实战练习的参考素材。1. 单通道脑电睡眠分期为什么一个电极也能跑出可用的五分类睡眠分期是睡眠医学里最基础的一项判读工作传统做法靠多导睡眠图PSG采集整晚的脑电、眼电、肌电、心电再由技师按 30 秒一帧逐帧打标签。问题在于设备贵、导联多、贴片麻烦普通人根本没法在家做长期监测。而单通道脑电single-channel EEG只用一个电极通常放在额区或枕区就能拿到整晚的神经电活动信号硬件成本能压到很低。基于 Python 的单通道脑电自动睡眠分期要解决的就是拿到这一路信号后怎么用代码把它自动分成 W、N1、N2、N3、REM 五个阶段并且准确率能到可用水平。这套东西适合谁一是做可穿戴睡眠监测的嵌入式/算法工程师二是睡眠方向的研究生需要一套能复现的 baseline三是想入门生理信号处理的 Python 开发者。它不追求替代医院 PSG而是解决「长期、居家、低成本」场景下的粗分期需求。整条链路的核心难点有三个单通道信息量少导致 N1 和 REM 容易混、类别极度不平衡N2 占一半以上、以及个体差异让跨被试泛化很难。后面几章我会按「数据怎么准备 → 特征/模型怎么搭 → 怎么训练 → 坑在哪 → 怎么验证」的顺序把一套能跑通的方案讲清楚源码结构、数据集组织、文档说明该写什么也一并交代。2. 数据集准备与预处理从原始 EDF 到模型能吃的片段2.1 单通道睡眠数据集怎么选、怎么读做这个方向绕不开公开数据集。最常用的是 Sleep-EDF含 Sleep-EDF-20 和 Sleep-EDF-78 两个子集它本身就是单通道 Fpz-Cz 或 Pz-Oz 脑电采样率 100Hz每 30 秒一个 epoch标签按 RK 标准给出。另一个常见的是 SHHS但它是多导的需要自己抽单通道。选数据集时重点看三件事通道是不是单通道、标签是不是按 30 秒 epoch 对齐、采样率是否统一。Sleep-EDF 这三条都满足所以是首选。读取用 MNE 最省事它能直接解析 EDF 并拿到注释里的睡眠标签。下面是最小读取代码import mne import numpy as np # 读取一条记录preloadTrue 把信号载入内存 raw mne.io.read_raw_edf(SC4001E0-PSG.edf, preloadTrue) # 只保留单通道脑电Sleep-EDF 里通常是 EEG Fpz-Cz raw.pick_channels([EEG Fpz-Cz]) raw.resample(100) # 统一到 100Hz避免不同记录采样率不一致 # 注释里存的是睡眠分期标签 annot raw.annotations print(annot.description[:10], annot.onset[:10])逻辑说明pick_channels保证只留一路信号这是「单通道」的前提resample是为了后续所有记录长度对齐。参数上100Hz 是 Sleep-EDF 原生采样率如果你用别的数据集建议重采样到 100 或 128Hz太高会增加计算量太低会丢高频特征比如纺锤波在 12-16Hz100Hz 采样足够。标签映射要注意RK 标准里 W、1、2、3、4、REM、MOVEMENT、UNKNOWN 都有通常做法是把 3 和 4 合并成 N3丢弃 MOVEMENT 和 UNKNOWN最终得到 5 类。这一步不做干净后面训练会莫名其妙多出类别。2.2 30 秒 epoch 切分与滤波归一化拿到连续信号和标签后要按 30 秒切段和标签一一对应。这里有个血泪经验注释的 onset 不一定从 0 开始也不一定严格对齐 30 秒边界直接按索引切会错位。稳妥做法是按注释时间戳去切。from mne import Epochs # 事件id映射把字符串标签转成整数 event_id {Sleep stage W: 0, Sleep stage 1: 1, Sleep stage 2: 2, Sleep stage 3: 3, Sleep stage 4: 3, Sleep stage R: 4} events, _ mne.events_from_annotations(raw, event_idevent_id) # 按 30 秒切 epochtmin0 表示从事件点开始取 epochs Epochs(raw, events, event_idevent_id, tmin0, tmax29.99, baselineNone, preloadTrue) X epochs.get_data() # 形状 (n_epochs, 1, 3000) y epochs.events[:, -1] # 对应标签 # 带通滤波 0.5-40Hz去掉基线漂移和工频 epochs.filter(0.5, 40.0, fir_designfirwin) # 逐 epoch 做 z-score 归一化消除个体幅度差异 X epochs.get_data() X (X - X.mean(axis-1, keepdimsTrue)) / (X.std(axis-1, keepdimsTrue) 1e-8) np.save(X.npy, X); np.save(y.npy, y)逻辑说明tmax29.99而不是 30是因为 MNE 的区间是左闭右开写 30 会多采一个点导致长度 3001训练时形状对不上。滤波范围 0.5-40Hz 是睡眠分期的通用选择0.5Hz 以下有漂移40Hz 以上主要是肌电干扰。归一化用逐 epoch 的 z-score而不是全局归一化原因是不同被试、不同时段的信号幅度差异极大全局归一化会让某些记录被压扁。参数怎么改如果你做的是实时推理滤波要用因果滤波器fir_designfirwin换成在线版本否则会有前瞻延迟。如果内存吃紧可以边切边存不要一次性preload整晚。3. 特征工程与模型选型从手工特征到 1D-CNN 的两条路3.1 手工特征 传统分类器小数据下的稳妥 baseline单通道数据量不大时比如只有几十个被试手工特征加树模型往往比深度学习更稳。睡眠分期的经典特征分四类时域均值、方差、偏度、峰度、Hjorth 参数、频域各频带功率delta 0.5-4Hz、theta 4-8Hz、alpha 8-12Hz、sigma 12-16Hz、beta 16-30Hz、时频小波能量、非线性样本熵、排列熵。频带功率是最有判别力的尤其 sigma 功率和 N2 的纺锤波强相关。from scipy.signal import welch from scipy.stats import skew, kurtosis def extract_features(epoch, fs100): # epoch 形状 (3000,) feats [] feats [epoch.mean(), epoch.std(), skew(epoch), kurtosis(epoch)] # 频带功率 f, psd welch(epoch, fsfs, npersegfs*2) bands {delta: (0.5, 4), theta: (4, 8), alpha: (8, 12), sigma: (12, 16), beta: (16, 30)} for lo, hi in bands.values(): idx (f lo) (f hi) feats.append(np.trapz(psd[idx], f[idx])) # 频带绝对功率 # 频带相对功率消除整体幅度影响 total np.trapz(psd, f) for lo, hi in bands.values(): idx (f lo) (f hi) feats.append(np.trapz(psd[idx], f[idx]) / (total 1e-8)) return np.array(feats)逻辑说明welch用 2 秒窗做功率谱估计平衡频率分辨率和方差。绝对功率和相对功率都保留是因为绝对功率受电极阻抗影响大相对功率更稳但绝对功率对 N3 的 delta 判断有用。分类器我一般先用 LightGBM 或随机森林特征维度几十维几千个 epoch 就能训几分钟出结果作为深度学习前的 sanity check 非常值。参数怎么改nperseg越大频率分辨率越高但方差越大2 秒是常用折中。如果你发现 N1 识别率极低通常都低可以额外加 theta/alpha 比值、以及 alpha 功率的爬升斜率特征。3.2 1D-CNN BiLSTM端到端方案的结构与理由数据量够几百个被试、上万 epoch时端到端深度模型能省掉手工特征且能自动学到时序上下文。单通道睡眠分期最经典的架构是「1D-CNN 提局部波形 BiLSTM 抓 epoch 间时序 全连接分类」。为什么用 BiLSTM 而不是单向因为睡眠阶段有强上下文N2 后面大概率还是 N2REM 往往在 N2 之后出现双向能看到前后文实测比单向高 2-3 个百分点。import torch import torch.nn as nn class SleepNet(nn.Module): def __init__(self, n_classes5): super().__init__() # 三层 1D 卷积逐层增大感受野 self.cnn nn.Sequential( nn.Conv1d(1, 32, 50, stride6, padding25), nn.BatchNorm1d(32), nn.ReLU(), nn.MaxPool1d(8, stride8), nn.Conv1d(32, 64, 8, stride1, padding4), nn.BatchNorm1d(64), nn.ReLU(), nn.MaxPool1d(4, stride4), nn.Conv1d(64, 128, 8, stride1, padding4), nn.BatchNorm1d(128), nn.ReLU(), ) self.lstm nn.LSTM(128, 128, batch_firstTrue, bidirectionalTrue) self.fc nn.Linear(256, n_classes) def forward(self, x): # x: (batch, seq_len, 3000) - (batch, 1, seq_len*3000) 不行 # 正确做法逐 epoch 过 CNN再按序列送 LSTM b, s, t x.shape x x.view(b * s, 1, t) x self.cnn(x) # (b*s, 128, L) x x.mean(dim-1) # 全局平均池化 - (b*s, 128) x x.view(b, s, 128) x, _ self.lstm(x) # (b, s, 256) return self.fc(x) # (b, s, 5)逻辑说明第一层卷积核 50、步长 6是为了在 100Hz 下覆盖约 0.5 秒的波形抓纺锤波和 K 复合波后面两层核 8 抓更细的局部模式。全局平均池化把每个 epoch 压成 128 维向量再按序列送 LSTM。输入组织成(batch, seq_len, 3000)seq_len 通常取 20-30即用前后各若干 epoch 的上下文。参数怎么改seq_len是关键太小抓不到上下文太大显存吃不消且引入无关历史20 左右是常见值。类别不平衡要在 loss 里处理用带权重的交叉熵权重按类别频率的倒数设。4. 训练、评估与类别不平衡处理让 N1 不再被吞掉4.1 加权损失与按被试划分睡眠分期最大的坑是类别不平衡。Sleep-EDF 里 N2 能占 50% 以上N1 常常不到 5%。如果直接训模型会把所有难分的都预测成 N2总体准确率看着有 80%但 N1 的 F1 接近 0这种模型在临床上没用。解决办法是加权交叉熵权重取1 / 类别频率再归一化。from sklearn.utils.class_weight import compute_class_weight import numpy as np classes np.unique(y_train) weights compute_class_weight(balanced, classesclasses, yy_train) weights torch.tensor(weights, dtypetorch.float32) criterion nn.CrossEntropyLoss(weightweights)逻辑说明balanced模式自动按n_samples / (n_classes * count)算权重少数类权重高。注意权重别设太极端否则模型会过度预测少数类N3 和 REM 的精确率掉得厉害。我一般会在 balanced 权重基础上做一次开方平滑比如weights ** 0.5实测更稳。划分数据集必须按被试划分不能按 epoch 随机划分。同一被试的不同 epoch 高度相关随机划分会让训练集和测试集泄漏准确率虚高十几个点。正确做法是留出若干被试完全不参与训练只做测试。4.2 评估指标别只看准确率评估要看三样总体准确率ACC、宏平均 F1macro-F1、以及混淆矩阵。macro-F1 对少数类敏感是判断模型是否可用的核心指标。一个能用的单通道模型在 Sleep-EDF 上通常能做到 ACC 82-86%、macro-F1 0.75-0.80。如果 ACC 很高但 macro-F1 低于 0.7基本是类别不平衡没处理好。from sklearn.metrics import classification_report, confusion_matrix preds model.predict(X_test) print(classification_report(y_test, preds, target_names[W,N1,N2,N3,REM])) print(confusion_matrix(y_test, preds))逻辑说明classification_report直接给出每类的 precision/recall/F1重点看 N1 那一行。混淆矩阵能看出错误集中在哪通常 N1 和 N2、N1 和 REM 之间混淆最多这是单通道的固有局限不是模型问题。参数怎么改如果 N1 的 recall 太低可以调低 N1 的判定阈值后处理或者用 focal loss 替代加权交叉熵。如果 N3 和 N2 混检查滤波是否把 delta 频段削掉了。5. 避坑与排查单通道睡眠分期最常见的五个翻车点现象一训练 loss 正常下降但验证集准确率一直 20% 左右等于瞎猜。原因标签映射错了或者 epoch 和标签错位。最常见的是events_from_annotations返回的事件顺序和get_data的 epoch 顺序没对齐或者把 UNKNOWN 类也当成了有效类。 解决打印前 20 个 epoch 的标签分布和原始注释对照确认event_id里没有把不同阶段映射成同一个整数除了 3 和 4 合并成 N3。现象二换一个被试测试准确率暴跌到 60%。原因个体差异。不同人的脑电幅度、阻抗、甚至电极位置都有差异模型学到了训练被试的特定模式。 解决逐 epoch z-score 归一化必须做训练时做被试级的数据增强随机幅度缩放、加高斯噪声有条件的用对抗训练或域自适应。现象三N1 的 F1 始终接近 0。原因N1 是过渡期本身就和 W、N2 边界模糊加上样本极少模型直接放弃。 解决加权损失 后处理阈值调整把 N1 和 REM 的区分特征眼动相关但单通道没有眼电用频域特征近似接受 N1 的 F1 在 0.3-0.4 是单通道的正常水平不要强求。现象四显存爆了或者训练极慢。原因把整晚信号一次性送进 LSTM序列长度上千。 解决按 seq_len20 切窗口每个窗口独立送模型用DataLoader的num_workers并行加载信号用 float32 而不是 float64。现象五推理时结果和训练时对不上。原因训练用了双向 LSTM推理时如果按流式逐 epoch 输入没有未来信息性能会掉。 解决要么推理时也攒够一个窗口再出结果有延迟要么训练时就用单向 LSTM 或因果卷积牺牲一点精度换实时性。6. 源码结构与文档说明让这套东西能被别人跑起来一套能交付的单通道睡眠分期项目目录结构我一般这么组织data/放原始 EDF 和切好的 npysrc/放预处理、特征、模型、训练脚本configs/放 YAML 参数checkpoints/存权重docs/放说明。文档说明里最该写清楚的不是模型多牛而是三件事数据集从哪下、怎么放、跑哪条命令能复现论文里的数字。# 典型复现流程 python src/preprocess.py --data_dir data/raw --out_dir data/processed python src/train.py --config configs/sleepnet.yaml --gpu 0 python src/evaluate.py --ckpt checkpoints/best.pt --split test逻辑说明预处理和训练解耦预处理只跑一次把 EDF 转成 npy后续训练直接读 npy速度快很多。configs里把学习率、batch size、seq_len、滤波范围都参数化换数据集只改配置不改代码。文档里必须写的一个表是「参数默认值与调参范围」参数默认值可调范围影响采样率100Hz100-128高频特征完整性滤波带0.5-40Hz0.3-45Hz漂移与肌电抑制seq_len2010-30上下文长度与显存学习率1e-31e-4~5e-3收敛速度类别权重balanced^0.50.5-1.0 次方少数类召回验证方法上我习惯做两件事一是留一被试交叉验证LOSO最能反映泛化二是把模型输出和原始 hypnogram 画在一起肉眼看错误是不是集中在阶段切换处。如果错误都堆在切换点说明模型对过渡期不敏感可以加一个「阶段转移矩阵」的后处理用维特比解码平滑预测序列通常能再涨 1-2 个点。最后说个我自己的习惯每次改完模型先在一个被试上跑通全流程再上全量数据别一上来就训 78 个被试等两小时发现标签错了那才叫后悔药没处买。这套方案值不值得做取决于你的场景——如果是要做居家长期监测的粗分期单通道加这套流程完全够用如果是要做临床级判读还是老老实实上多导。希望帮到你。本文还有配套的精品资源点击获取
返回列表