资讯动态

基于PyTorch和单通道EEG的睡眠分期:从信号预处理到深度学习实战

发布时间:2026/9/16 19:07:26 来源:尧图企业网站定制
简介基于PyTorch框架的单通道EEG睡眠分期Python源代码面向计算机相关专业正在做毕业设计、课程设计或需要项目实战练习的学习者主要解决睡眠脑电信号的自动分期识别问题涵盖浅睡、深睡、REM等阶段分类。资源包共21个文件以7个Python脚本为核心包含EmbedSleepNet模型定义、preprocess.py数据预处理、train.py训练流程、benchmark.py基准测试以及lightning_wrapper.py封装等模块同时提供配置文件、说明文档、依赖清单和许可证等辅助材料压缩包整体仅19KB轻量易部署。已有98人学习下载适合作为深度学习入门与生物医学信号处理方向的完整项目参考。代码完整且经过导师指导与高分评审可直接运行目录结构清晰便于读者快速理解项目架构、复现睡眠分期实验并在此基础上进行二次开发或课程设计展示。1. 睡眠分期为什么值得用单通道 EEG 做我见过不少做健康监测的团队一开始都想上多导睡眠监测PSG几十路信号、专业设备、技师整夜值守成本一下来就劝退了。而单通道 EEG 只需要一个前额或耳后电极戴上就能睡数据量小传输和处理压力也低天然适合做家用睡眠监测和分期。但要靠一条通道把睡眠分期做准传统信号处理和规则判定在个体差异面前非常吃力深度学习恰好能从大量样本里自己学出跨受试者的时序规律。这篇博文围绕一个可落地的项目展开用 PyTorch 搭建深度学习模型输入单通道 EEG 原始波形输出 Wake / N1 / N2 / N3 / REM 五类分期结果。我会按“信号预处理 - 滑窗切片 - 模型选型与训练 - 评估与部署”的路径走一遍所有代码基于 Python 和 PyTorch读者在 CPU 上也能完成小数据集的迭代。适用人群是有 Python 基础、想上手时间序列深度学习或生物信号处理的工程师。2. 睡眠分期与 EEG 信号从波形规律到模型输入2.1 睡眠分期的标准规则与单通道挑战临床上常用 Rechtschaffen KalesRK或 AASM 标准把睡眠分为清醒期Wake、非快速眼动期NREM的 N1/N2/N3以及快速眼动期REM。对单通道 EEG 来说分期主要依赖频谱特征和瞬态波形Wake 期以 beta 和 alpha 波为主N1 期 alpha 波减弱、theta 波出现N2 期出现睡眠纺锤波和 K 复合波N3 期以高幅慢波delta为主REM 期 theta 波混有 alpha 波且肌电干扰少。传统自动化分期方法多采用手工特征加分类器提取频带能量、熵、过零率等特征再喂给 SVM、随机森林或隐马尔可夫模型。这类方法在单一数据集上效果尚可但跨受试者泛化能力弱因为手工特征很难覆盖个体间的 EEG 形态差异和电极位置偏差。深度学习模型可以从原始波形中自动学习时域和频域特征免去人工设计特征的过程并且在多中心数据上有更强的泛化潜力。对于单通道输入模型还必须能够处理信号的强非平稳性同一受试者不同睡眠周期的频谱分布差异不同受试者之间的基线漂移这些都需要在预处理阶段做针对性处理。2.1.1 单通道 EEG 的典型获取方式与采样率项目里最常见的是 Fpz-Cz 或 C4-A1 导联使用公开数据集比如 Sleep-EDF时原始采样率通常是 100 Hz 或 256 Hz。实际开发时建议将采样率统一到 100 Hz 或 128 Hz主要目的是减小计算量同时保留睡眠相关频段0.3~30 Hz的完整信息。采样率选择不能随意。过高的采样率会带来大量冗余数据点过低的采样率则会丢失纺锤波等高频瞬态。抗混叠滤波必须在重采样前完成否则高频噪声会折叠到低频段干扰分期。2.2 为什么单通道比多通道更容易落地多通道 PSG 中的 EMG 和 EOG 对 REM 分期非常关键单通道 EEG 少了这两个辅助信号单靠一条通道区分 REM 和 N1 往往是最困难的。实际项目中通常会在预处理阶段加上“上下文”信息来缓解比如对 30 秒窗口前后相邻窗口也做特征提取让模型看到时序上下文。从部署角度看单通道设备对硬件要求低功耗小适合做穿戴式长期监测。芯片端推理时模型参数量通常控制在几 MB 以内单次推理时间小于 200 ms这对于实时睡眠监测是可接受的范围。这也意味着模型结构需要相对轻量为后续的端侧部署留出余量。2.3 深度学习模型的时间序列视角把单通道 EEG 看成时间序列每 30 秒一个分期标签那么问题就变成一个典型的时间序列分类任务。常用的网络结构有三类网络类型核心思想单通道 EEG 适用性1D-CNN局部卷积核提取短时形态特征高适合捕捉纺锤波、K 复合波LSTM门控循环结构建模长程依赖中能利用整夜时序上下文但训练慢Transformer自注意力捕捉全局关系较高需要较多数据参数量偏大对睡眠分期这种具有层次时序结构的任务来说混合结构通常效果更好先用 CNN 从原始波形中提取睡眠形态特征再用 LSTM 或 Transformer 建模跨窗口的时序依赖。这也是本文推荐的技术选型路径。3. 数据预处理与滑窗切片搞不干净数据模型再深也没用3.1 数据清洗与重采样步骤拿到原始 EEG 数据后需要依次做以下几件事去噪、降采样、滤波、标准化。这个顺序不能乱特别是去噪必须放在滤波之前否则工频干扰会先被滤波放大或折叠进有效频段。import numpy as np import scipy.signal as signal def preprocess_eeg(raw_signal, orig_fs256, target_fs128): # 1. 去除基线漂移高通滤波截止频率 0.3 Hz b_high, a_high signal.butter(4, 0.3, btypehigh, fsorig_fs) filtered signal.filtfilt(b_high, a_high, raw_signal) # 2. 去除 50 Hz 工频干扰欧洲标准或 60 Hz美洲标准 b_notch, a_notch signal.iirnotch(50 if orig_fs 60 else 60, 30, fsorig_fs) filtered signal.filtfilt(b_notch, a_notch, filtered) # 3. 低通滤波到 30 Hz避免高频肌电干扰混叠 b_low, a_low signal.butter(4, 30, btypelow, fsorig_fs) filtered signal.filtfilt(b_low, a_low, filtered) # 4. 重采样到目标采样率 resampled signal.resample_poly(filtered, target_fs, orig_fs) return resampled代码里用filtfilt是零相位滤波可以避免lfilter带来的相位偏移影响特征时序。iirnotch的 Q 值设为 30以保证陷波带宽适中不会过度削除邻近频段能量。重采样用resample_poly而非resample原因是resample_poly会先做抗混叠滤波再采样能显著减少频谱混叠。3.2 分段策略使用滑窗切割 30 秒样本睡眠分期以 30 秒为最小标注单位预处理后的信号也要按 30 秒切段。如果是 128 Hz 采样率每段就是 3840 个采样点。实际操作里要注意两点一是当窗口没有对齐到时需要处理标签边界二是数据不足时要用重叠滑窗做数据增强。def sliding_window(signal_array, labels, window_sec30, fs128, stride_sec10): win_len window_sec * fs stride_len stride_sec * fs windows, label_list [], [] for i in range(0, len(signal_array) - win_len 1, stride_len): win signal_array[i:i win_len] label labels[i // fs // window_sec] # 对应原始标签段 windows.append(win) label_list.append(label) return np.array(windows), np.array(label_list)训练阶段设置stride_sec10可以让相邻窗口覆盖 2/3 的重叠相当于把数据量放大三倍。但推理阶段我不建议用重叠窗口否则同一睡眠阶段会被反复预测影响后续对整夜睡眠结构的统计。验证集和测试集也要按原始 30 秒窗口切保持标签独立。3.2.1 标签处理的边界情况睡眠数据常伴随未分期的片段比如“运动伪影”或“未评分”。这类样本如果不剔除会让模型学到噪声。我通常的处理方式是直接丢弃同时把相邻的不确定性变成硬约束如果某段标签是未知则该段前后各 5 分钟内不做滑窗重叠防止同一段数据的上下文泄漏到训练集。3.3 标准化与通道数折腾对于单通道 EEG标准化方式推荐逐样本 z-score而不是全局标准化。原因是不同受试者、不同夜间的信号幅度差异很大全局均值会拉偏幅度特征。def zscore_window(windows, eps1e-6): mean windows.mean(axis1, keepdimsTrue) std windows.std(axis1, keepdimsTrue) return (windows - mean) / (std eps)尽管项目名义上是“单通道”在输入模型之前可以把一维信号通过连续小波变换CWT转成二维时频图这样就能直接使用 PyTorch 里的 2D-CNN。这种做法能显式地把时频分布交给模型更符合睡眠分期的专业知识但代价是计算量成倍增加。我在做项目验证时通常准备两个版本的输入一是原始波形加短时傅里叶变换STFT得到的 2D 特征图二是原始波形加多频带能量特征的一维输入。先在二维版本上预训练再用一维输入做微调收敛速度和最终准确率都比较理想。4. PyTorch 模型构建与训练实操从定义到迭代4.1 网络结构设计CNN LSTM 的混合结构模型结构上我推荐三阶段设计感受野逐渐扩大语义层级逐渐升高。第一阶段用 1D-CNN 从原始波形提取短时形态特征第二阶段用 LSTM 建模窗口内的时间依赖第三阶段用全连接层输出五个类别的概率。import torch import torch.nn as nn class SleepStageCNNLSTM(nn.Module): def __init__(self, n_classes5, input_channels1, lstm_hidden64): super().__init__() # 第一层卷积感受野较小捕捉纺锤波等瞬态特征 self.conv1 nn.Sequential( nn.Conv1d(input_channels, 32, kernel_size50, stride2, padding25), nn.BatchNorm1d(32), nn.ReLU(), nn.MaxPool1d(2) ) # 第二层卷积感受野扩大提取慢波分段特征 self.conv2 nn.Sequential( nn.Conv1d(32, 64, kernel_size25, stride1, padding12), nn.BatchNorm1d(64), nn.ReLU(), nn.MaxPool1d(2) ) self.lstm nn.LSTM(input_size64, hidden_sizelstm_hidden, num_layers1, batch_firstTrue, bidirectionalTrue) self.classifier nn.Linear(lstm_hidden * 2, n_classes) def forward(self, x): # x: (batch, channels, time_steps) x self.conv1(x) # (batch, 32, time/4) x self.conv2(x) # (batch, 64, time/8) x x.permute(0, 2, 1) # (batch, length, features) lstm_out, _ self.lstm(x) # 取 LSTM 最后时间步的输出 out self.classifier(lstm_out[:, -1, :]) return outkernel_size50配合 128 Hz 采样率约覆盖 0.39 秒的波形这个长度对捕捉约 11~16 Hz 的睡眠纺锤波比较合适。LSTM 用双向结构可以让每一时刻的特征同时参考前后窗口信息对识别 N1-REM 这种边界模糊的阶段有明显帮助。4.2 损失函数与类别不均衡处理睡眠分期的标签天然不均衡N2 占比接近 40%-50%N1 可能只有 5%-10%。如果不做处理模型会倾向于把所有样本都预测为 N2。这里我常用的两种做法一是加权交叉熵损失二是Focal Loss。加权交叉熵简单有效权重可以按各类别样本数的反比设置。def get_class_weights(label_list, n_classes5): counts np.bincount(label_list, minlengthn_classes).astype(np.float32) weights np.sum(counts) / (n_classes * counts 1e-6) return torch.tensor(weights, dtypetorch.float32)注意权重平方根效果往往比原始反比更好。完全的反比权重会把样本量极少的 N1 放大到“矫枉过正”的程度预测结果里 N1 的输出概率偏高实际误判增多。一般我会在反比基础上开 0.5 次方作为最终权重。4.3 训练流程与验证策略训练配置分三个阶段推进先是学习率 warm-up然后稳态训练最后用 reducelronplateau 降低学习率直到收敛。优化器首选 AdamW权重衰减设置 1e-4比 Adam 多一个解耦的权重衰减能更有效防止过拟合。optimizer torch.optim.AdamW(model.parameters(), lr1e-3, weight_decay1e-4) scheduler torch.optim.lr_scheduler.ReduceLROnPlateau( optimizer, modemin, factor0.5, patience5 ) criterion nn.CrossEntropyLoss(weightclass_weights.to(device)) for epoch in range(n_epochs): model.train() for batch_x, batch_y in train_loader: batch_x, batch_y batch_x.to(device), batch_y.to(device) optimizer.zero_grad() logits model(batch_x) loss criterion(logits, batch_y) loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() val_loss evaluate(model, val_loader, criterion) scheduler.step(val_loss)梯度裁剪的max_norm1.0对 LSTM 极其重要。睡眠信号里的尖峰伪影很容易让梯度暴涨不做裁剪训练就会发散。ReduceLROnPlateau的 patience 设 5意思是最少等 5 个 epoch 再降低学习率太少会让训练提前饱和太多则会浪费计算资源。4.3.1 按受试者划分数据睡眠分期项目最关键的验证纪律是必须按受试者划分训练集和测试集。如果按样本切分同一受试者的睡眠数据会同时出现在训练和测试集合里跨受试者泛化能力全被高估。我遇到过跨样本评估准确率 92%换到按受试者划分后直接掉到 76% 的情况这种差距才是真实表现。4.4 训练超参数配置参考参数推荐值说明输入长度384030s 128Hz使用滑窗重叠增强时要有上下文的约束Batch size64显存不足时可降到 16同时降低 lr初始学习率1e-3AdamW 配合 warm-up 会更稳定权重衰减1e-4正则化防止特征维度高原过拟合Max epochs80-100早停 patience15早停条件验证损失 15 轮不降避免过拟合保留最佳模型5. 评估与部署从准确率到真实场景可用性5.1 评估指标的选择睡眠分期不能只看整体准确率。因为类别极不均衡一个只预测 N2 的模型可能也能拿到 50%-60% 的准确率。一般重点看三样每类的 F1-score、Cohen‘s Kappa 系数、混淆矩阵。其中 Kappa 对不均衡数据更稳健项目上通常要求 Kappa 0.7 才算临床应用可参考。from sklearn.metrics import cohen_kappa_score, classification_report # preds: 所有测试样本的预测标签, trues: 真实标签 kappa cohen_kappa_score(trues, preds, weightsquadratic) report classification_report(trues, preds, target_names[Wake, N1, N2, N3, REM])Kappa 的weightsquadratic是常规做法。因为 N1 误判为 Wake 和误判为 N3 的实际临床含义完全不同二次加权可以适当容忍相邻阶段的误判。5.2 模型导出与推理流水线PyTorch 模型在端侧部署前要导出为推理格式。如果只需要 CPU 推理torch.jit.script是个快速方案如果后续要跑到手机或单片机上建议转成 ONNX 再优化。导出时要注意把预处理逻辑也封装进去不然部署端还得重新实现滤波和标准化。model.eval() example_input torch.randn(1, 1, 3840) traced_model torch.jit.trace(model, example_input) traced_model.save(sleep_stage_128hz.pt)推理流水线上实时系统每 30 秒出一个分期结果但更实用的做法是每次输出前把当前 30 秒窗口和前后各 2 个窗口拼起来做一个“上下文投票”可显著平滑单帧误判。代价是响应时间延迟到 90 秒左右但对整夜分期结果来说完全可接受。5.3 训练过程中的高频坑第一个坑是 BatchNorm 与 LSTM 混用时的 batch size 过小。BatchNorm 在 batch size 小于 16 时统计偏差大训练和推理时的均值/方差差异会引起明显的性能滑坡。改用 LayerNorm 或在卷积层之间减少 BatchNorm 可以缓解。第二个坑是数据的 label 泄漏。很多人做滑窗增强时没有把重叠部分从验证集排除导致验证集里有大量与训练集窗口高度重叠的样本验证指标虚高。严格做法是先用原始 30 秒窗口切出验证集再对训练集单独做滑窗增强。第三个坑是类别的顺序问题。睡眠分期标签的编码在不同数据集中标准不一比如有的 Sleep-EDF 转写的标签是 0-4有的数据集编码为 5 个阶段但顺序不同。代码里要显式做映射表不要在数据加载时假设编码顺序一致。6. 进阶技巧利用上下文与迁移学习提升分期表现如果项目已经跑通但 Kappa 卡在 0.7 以下通常不是换更大模型能解决的而是没有充分利用睡眠分期的领域知识。我发现最有效的三个技巧是引入相邻窗口注意力、预训练与微调、以及整夜阶段转移矩阵约束。相邻窗口注意力很好理解人类专家在标记一个 30 秒片段时一定会参考前一段和后一段的情况因为睡眠状态在时间上天然连续。工程上可以做一个轻量的上下文模块把当前窗口的前后各 2 个窗口的特征图拼接经过一个线性层投影后和当前窗口特征做注意力融合。这个模块加在 LSTM 之后能显著改善 N1 和 REM 这种容易跳变的类别。class ContextAttention(nn.Module): def __init__(self, feature_dim128): super().__init__() self.query nn.Linear(feature_dim, feature_dim) self.key nn.Linear(feature_dim, feature_dim) self.value nn.Linear(feature_dim, feature_dim) self.softmax nn.Softmax(dim1) def forward(self, center_feat, context_feats): # center_feat: (batch, feature_dim) # context_feats: (batch, num_context, feature_dim) q self.query(center_feat).unsqueeze(1) k self.key(context_feats) v self.value(context_feats) attn self.softmax(torch.bmm(q, k.transpose(1, 2))) out torch.bmm(attn, v).squeeze(1) return center_feat out注意这里context_feats的抽取和分段要和当前窗口保持天然的先后顺序打乱顺序会破坏睡眠周期的连续性语义注意力学到的是错误的时间关系。如果有条件拿到多个公开数据集先在大数据集上做自监督预训练也是一种高效选择。用带标签的数据预训练一个公开的睡眠分期模型再把权重迁移到自己的单通道数据上微调在数据量只有几百小时的情况下比从零训练能提升 8%-12% 的 Kappa。微调时把学习率调到 1e-5 以下只更新后面两层分类头收敛快且不容易过拟合。还有一个常被忽视的稳定化技巧是整夜阶段的转移约束。睡眠分期本质上是一个马尔可夫链Wake - N1 - N2 - N3 - REM 是常态路径反向跳跃极不合理。推理阶段可以在后处理时加入一个简单的一阶转移矩阵惩罚比如把“REM 后直接接 N3”和“Wake 后直接接 N2”的概率显著压低。这种后处理不会增加模型参数量但能过滤掉不少令人哭笑不得的时序跳变。至于要不要直接用维特比算法进行全局最优解码看你的需求整夜离线分析场景用维特比很合适实时流式场景就只能做窗口级平滑。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价