
简介本资源是一个基于PyTorch实现的单通道脑电信号EEG睡眠分期系统面向高校人工智能、生物医学工程及计算机相关专业高年级本科生与研究生解决神经科学中自动化睡眠阶段判读这一典型时序分类问题。压缩包共26个文件含7个核心Python源码如model.py、train.py、preprocess.py、4个Markdown/README备份文件、4个XML配置与IDE设置文件、3个编译缓存pyc文件以及LICENSE、requirements.txt等关键文档整体仅25KB轻量紧凑且模块划分清晰——涵盖数据预处理、混合CNN-RNN建模、Lightning封装训练与评估全流程。已有133人学习下载提供完整可运行代码、技术文档及标准化接口定义支持直接复现实验结果或快速迁移至多模态生理信号分析任务是开展毕业设计、课程实践与科研原型开发的高复用性参考实现。1. 项目概述从单通道脑电到睡眠分期睡眠分期或者说睡眠阶段划分是睡眠医学和神经科学研究中的一个基础但至关重要的任务。传统的多导睡眠图需要同时记录脑电、眼电、肌电等多个生理信号并由专业技师进行人工分期这个过程耗时耗力且存在主观差异。近年来随着可穿戴设备和家庭健康监测的兴起使用更少的传感器、甚至单通道脑电信号来实现自动睡眠分期成为了一个极具吸引力的研究方向。这不仅能降低设备成本和佩戴复杂度也为大规模、长期的睡眠健康监测铺平了道路。这个项目的核心目标就是利用PyTorch这一强大的深度学习框架构建一个能够仅凭单通道脑电信号就自动、准确地将整夜睡眠划分为清醒、快速眼动睡眠以及非快速眼动睡眠的N1、N2、N3期的系统。听起来像是从一片嘈杂的“脑电海洋”里精准地捞出代表不同睡眠状态的“鱼”而我们的“渔网”就是深度学习模型。选择PyTorch是因为它在研究社区和工业界都享有极高的声誉其动态计算图、直观的API设计以及对自定义模型和损失函数的友好支持使得我们能够快速地将前沿的论文思路转化为可运行的代码并进行灵活的调试和优化。对于处理像脑电信号这样的时序数据PyTorch的torch.nn模块提供了丰富的循环神经网络和卷积神经网络组件而DataLoader和Dataset类则能优雅地处理信号切片、数据增强等繁琐的预处理流程。2. 核心思路与方案选型要实现单通道脑电的睡眠分期我们面临的挑战是信息维度的显著减少。多导睡眠图可以利用不同通道信号如额区脑电、眼电、下颌肌电之间的关联性来辅助判断而单通道则失去了这些交叉验证的信息。因此我们的模型必须更加“聪明”能够从单一通道的时域和频域特征中挖掘出足够深层次、具有判别性的模式。2.1 模型架构的演进与选择早期的自动睡眠分期多依赖于手工提取的特征如功率谱密度、非线性动力学指标等再结合传统的机器学习分类器。但深度学习特别是卷积神经网络和循环神经网络的结合展现出了更强大的端到端特征学习能力。一个经典的架构是CNN-LSTM混合模型CNN层通常是1D卷积负责从原始的或简单预处理后的脑电信号片段中提取局部时空特征比如检测特定的脑波节律随后LSTM层则负责捕捉这些特征在时间序列上的长期依赖关系理解睡眠阶段之间的转换规律。然而近年来基于纯卷积的模型如SleepEEGNet、U-Sleep以及基于Transformer的模型也开始崭露头角。Transformer的自注意力机制能够直接建模信号中任意两点之间的全局依赖关系理论上比RNN更能捕捉长程关联。考虑到计算效率和实现的简洁性本项目选择以一个中等复杂度的CNN-LSTM混合模型作为基线。它结构清晰易于理解和调试并且为后续引入更复杂的模块如注意力机制、残差连接留下了充足的扩展空间。2.2 数据处理流水线设计数据是模型的“粮食”。公开的睡眠数据集如Sleep-EDF、SHHS等是我们的起点。但原始数据不能直接喂给模型。我们的数据处理流水线需要精心设计信号读取与通道选择从PSG记录文件中读取多通道数据并提取出我们选定的单通道通常是C4-A1或Fpz-Cz这些是临床常用的位置。重采样与滤波将信号统一重采样到相同的频率如100Hz或128Hz。然后进行带通滤波如0.3-35Hz以去除工频干扰、肌电伪迹和直流漂移保留与睡眠相关的生理频段。分段与标注对齐睡眠分期通常以30秒为一个“时期”。我们需要将连续的脑电信号切割成一个个30秒长的片段。同时将专家标注的睡眠阶段标签W, N1, N2, N3, REM与这些片段精确对齐。这里要特别注意处理标注中的移动、缺失或“未知”阶段。标准化对每个样本或整个训练集进行标准化使其均值为0标准差为1。这能加速模型收敛并提高泛化能力。数据集划分务必按“受试者”划分训练集、验证集和测试集而不是随机打乱所有样本。这是为了评估模型的跨受试者泛化能力避免因为同一个人的数据同时出现在训练和测试中而得到过于乐观的结果。数据增强对于睡眠数据简单的时间翻转或裁剪可能不合适。我们可以采用添加高斯噪声、轻微的时间扭曲、随机缩放幅度等方法来增加数据的多样性这对于防止过拟合、尤其是处理类别不平衡问题N1期样本通常很少很有帮助。注意数据预处理的每个步骤都需要保存相应的参数如滤波器的系数、标准化的均值和标准差。在推理预测新数据时必须使用与训练时完全相同的预处理流程和参数否则模型性能会严重下降。3. 核心模块实现与PyTorch技巧接下来我们深入到代码层面看看如何用PyTorch实现这个系统的核心部分。3.1 自定义Dataset类这是连接数据和模型的桥梁。一个好的Dataset类能让我们高效地加载和预处理数据。import torch from torch.utils.data import Dataset, DataLoader import numpy as np class SleepEEGDataset(Dataset): def __init__(self, eeg_signals, stage_labels, transformNone): Args: eeg_signals: list of numpy arrays, 每个元素是一个30秒的EEG片段 (seq_len,) stage_labels: list of integers, 对应的睡眠阶段标签 (0:W, 1:N1, 2:N2, 3:N3, 4:REM) transform: 可选的数据增强变换 self.signals eeg_signals self.labels stage_labels self.transform transform def __len__(self): return len(self.signals) def __getitem__(self, idx): signal self.signals[idx].astype(np.float32) label self.labels[idx] # 转换为PyTorch张量 signal_tensor torch.from_numpy(signal).unsqueeze(0) # 形状: (1, seq_len) 增加通道维 label_tensor torch.tensor(label, dtypetorch.long) # 应用数据增强 if self.transform: signal_tensor self.transform(signal_tensor) return signal_tensor, label_tensor使用DataLoader可以方便地进行批处理、打乱和并行加载train_dataset SleepEEGDataset(train_signals, train_labels, transformadd_gaussian_noise) train_loader DataLoader(train_dataset, batch_size32, shuffleTrue, num_workers4, pin_memoryTrue)这里pin_memoryTrue在GPU训练时能显著加速数据从CPU到GPU的传输。3.2 CNN-LSTM混合模型构建下面是一个简化但完整的模型定义示例import torch.nn as nn import torch.nn.functional as F class SleepStageClassifier(nn.Module): def __init__(self, input_size3000, num_classes5): # 假设30秒100Hz采样共3000点 super(SleepStageClassifier, self).__init__() # CNN特征提取部分 self.conv1 nn.Conv1d(in_channels1, out_channels64, kernel_size50, stride6, padding25) self.bn1 nn.BatchNorm1d(64) self.pool1 nn.MaxPool1d(kernel_size8, stride8) self.dropout1 nn.Dropout(0.5) self.conv2 nn.Conv1d(64, 128, kernel_size8, stride1, padding4) self.bn2 nn.BatchNorm1d(128) self.pool2 nn.MaxPool1d(kernel_size4, stride4) self.dropout2 nn.Dropout(0.5) # 计算经过CNN后的序列长度用于LSTM输入 # 可以手动计算也可以用forward一次来获取这里我们手动估算或动态获取 self.cnn_output_size self._get_cnn_output_size(input_size) # LSTM时序建模部分 self.lstm nn.LSTM(input_size128, hidden_size128, num_layers2, batch_firstTrue, bidirectionalTrue, dropout0.3) # 双向LSTM输出特征维度为 hidden_size * 2 # 全连接分类层 self.fc nn.Linear(128 * 2, num_classes) # 双向所以是128*2 def _get_cnn_output_size(self, input_size): # 一个辅助函数用于计算CNN输出的序列长度 # 实际项目中可以写一个前向传播来动态计算 x torch.randn(1, 1, input_size) x self.pool1(F.relu(self.bn1(self.conv1(x)))) x self.pool2(F.relu(self.bn2(self.conv2(x)))) return x.shape[2] # 返回序列长度 def forward(self, x): # x shape: (batch_size, 1, seq_len) # CNN部分 cnn_out F.relu(self.bn1(self.conv1(x))) cnn_out self.pool1(cnn_out) cnn_out self.dropout1(cnn_out) cnn_out F.relu(self.bn2(self.conv2(cnn_out))) cnn_out self.pool2(cnn_out) cnn_out self.dropout2(cnn_out) # 此时 cnn_out shape: (batch_size, 128, cnn_seq_len) # 为LSTM准备输入: (batch_size, cnn_seq_len, 128) lstm_input cnn_out.transpose(1, 2) # LSTM部分 lstm_out, _ self.lstm(lstm_input) # lstm_out shape: (batch_size, cnn_seq_len, 256) # 我们取最后一个时间步的输出或者对所有时间步的输出做平均/最大池化 # 这里取最后一个时间步 lstm_last_out lstm_out[:, -1, :] # 分类 out self.fc(lstm_last_out) return out关键点解析1D卷积nn.Conv1d的in_channels对应信号的通道数单通道就是1。kernel_size,stride,padding的选择会影响感受野和下采样率需要根据脑电信号的频率特性来设计目标是让卷积核能覆盖到有意义的节律如α波、δ波。批归一化nn.BatchNorm1d在卷积层后使用可以加速训练并提高模型稳定性。Dropout是防止过拟合的利器尤其在数据量有限的医疗数据上。双向LSTM睡眠阶段具有前后依赖性双向LSTM能同时利用过去和未来的上下文信息通常比单向LSTM效果更好。输出处理对于序列分类任务常见策略有1) 取LSTM最后一个时间步的输出2) 对所有时间步的输出做平均或最大池化3) 使用注意力机制加权求和。本项目采用第一种简单策略。3.3 损失函数与类别不平衡处理睡眠分期的一个老大难问题是类别极度不平衡。通常N2期占整夜睡眠的50%以上而N1期可能只占5%。如果使用标准的交叉熵损失模型会倾向于把所有样本都预测为N2期来获得一个不错的整体准确率但这对于识别罕见的N1期和REM期是灾难性的。解决方案加权交叉熵损失为每个类别赋予一个权重权重与类别的样本数成反比。from sklearn.utils.class_weight import compute_class_weight import numpy as np classes [0,1,2,3,4] class_weights compute_class_weight(balanced, classesclasses, ytrain_labels_list) class_weights torch.tensor(class_weights, dtypetorch.float).to(device) criterion nn.CrossEntropyLoss(weightclass_weights)Focal Loss这是一种在目标检测中流行起来的损失函数它通过降低易分类样本的权重让模型更关注难分类的样本。对于睡眠分期N1期通常是“难样本”。class FocalLoss(nn.Module): def __init__(self, alpha1, gamma2, reductionmean): super(FocalLoss, self).__init__() self.alpha alpha self.gamma gamma self.reduction reduction def forward(self, inputs, targets): BCE_loss F.cross_entropy(inputs, targets, reductionnone) pt torch.exp(-BCE_loss) # pt p if target1 else 1-p F_loss self.alpha * (1-pt)**self.gamma * BCE_loss if self.reduction mean: return torch.mean(F_loss) elif self.reduction sum: return torch.sum(F_loss) else: return F_loss在实践中可以尝试将加权交叉熵和Focal Loss结合使用。3.4 训练循环与评估指标训练循环是PyTorch的标准流程但有一些细节需要注意def train_epoch(model, dataloader, criterion, optimizer, device): model.train() running_loss 0.0 all_preds [] all_labels [] for batch_idx, (signals, labels) in enumerate(dataloader): signals, labels signals.to(device), labels.to(device) optimizer.zero_grad() outputs model(signals) loss criterion(outputs, labels) loss.backward() # 可以添加梯度裁剪防止梯度爆炸在RNN/Transformer中尤其有用 # torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() running_loss loss.item() * signals.size(0) _, preds torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_labels.extend(labels.cpu().numpy()) epoch_loss running_loss / len(dataloader.dataset) return epoch_loss, np.array(all_preds), np.array(all_labels)对于睡眠分期不能只看整体准确率。因为即使模型把所有样本都猜成N2准确率也可能有50%以上但这毫无意义。我们必须看每个类别的性能。核心评估指标混淆矩阵直观展示每个类别被预测成其他类别的情况。每类精确率、召回率、F1分数这是最重要的指标。特别是N1期和REM期的召回率敏感度直接反映了模型识别这些关键阶段的能力。总体准确率作为参考。Cohen‘s Kappa系数衡量模型预测与专家标注之间的一致性排除了随机同意的影响是睡眠分期研究中公认的指标。Kappa 0.8 表示几乎完美一致0.6-0.8表示高度一致。可以使用sklearn.metrics方便地计算这些指标。4. 实战中的挑战与调优策略纸上得来终觉浅绝知此事要躬行。在实际编码和训练过程中你会遇到一系列教科书上不会细讲的问题。4.1 过拟合与泛化能力医疗数据通常样本量有限过拟合是头号敌人。策略一更强的正则化除了Dropout可以尝试在卷积层和全连接层后都加入Dropout并适当提高丢弃率。还可以为模型参数添加L2正则化权重衰减。策略二数据增强的学问对于脑电信号哪些增强是有效的我的经验是添加高斯噪声和随机幅度缩放是比较安全且有效的。时间扭曲如轻微拉伸或压缩需要谨慎因为这会改变信号的频率成分。也可以尝试在频域进行增强比如随机扰动某个频段的功率。策略三早停法持续监控验证集上的损失或F1分数当其在连续多个周期内不再提升时就停止训练并回滚到验证集性能最好的那个模型参数。策略四简化模型如果模型在训练集上表现很好但在验证集上很差首先应该考虑是不是模型太复杂了。尝试减少卷积层的通道数、减少LSTM的隐藏单元数或层数。4.2 超参数调优这是一个需要耐心和一定经验的过程。学习率最关键的参数。可以从1e-3或3e-4开始尝试使用学习率预热和余弦退火等调度策略能带来稳定提升。torch.optim.lr_scheduler.CosineAnnealingLR或OneCycleLR都是不错的选择。批大小较小的批大小如32有时能带来更好的泛化性能但训练可能更不稳定。较大的批大小训练更快、更稳定但可能会损害泛化能力。需要根据你的GPU内存来权衡。优化器Adam或AdamW是默认的首选。AdamW通常对权重衰减的处理更好能获得更优的泛化性能。序列长度我们默认使用30秒。但也可以尝试使用更长的上下文窗口如5个连续的30秒时期作为模型输入让LSTM看到更长的依赖关系。这需要调整模型输入和数据处理逻辑。4.3 处理标注噪声与不确定性即使是专家标注睡眠分期也存在一定的主观性不同评分员之间的一致性组内相关系数并非100%尤其是N1期和REM期的区分。这意味着我们的训练数据本身就有“噪声”。标签平滑在计算交叉熵损失时不使用硬标签如[0,0,1,0,0]而使用软标签如[0.05, 0.05, 0.8, 0.05, 0.05]。这可以防止模型对“绝对正确”的标签过于自信提高泛化性。PyTorch的交叉熵损失直接支持软标签。集成学习训练多个模型可以是相同架构不同初始化也可以是不同架构然后对它们的预测进行平均或投票。这能有效平滑掉单个模型可能犯的错误。5. 从实验到部署构建完整系统模型训练好只是第一步我们要的是一个可以使用的“系统”。5.1 推理流程封装我们需要一个predict函数它接收原始的、一整夜的单通道脑电信号输出每个30秒时期的睡眠阶段。def predict_whole_night(model, raw_eeg_signal, sample_rate, preprocess_params, devicecuda): 预测整夜睡眠阶段 Args: raw_eeg_signal: 一维numpy数组整夜EEG信号 sample_rate: 采样率 preprocess_params: 字典包含训练时用的滤波器系数、标准化参数等 Returns: stages: 预测的睡眠阶段列表 probas: 每个阶段对应的概率向量可选 # 1. 应用与训练时相同的预处理滤波、分段 processed_signal apply_filter(raw_eeg_signal, preprocess_params[filter_coeff]) epochs segment_into_epochs(processed_signal, epoch_length30*sample_rate) # 2. 标准化 epochs_normalized (epochs - preprocess_params[mean]) / preprocess_params[std] model.eval() all_preds [] all_probs [] with torch.no_grad(): # 可以批量处理以提高速度 for i in range(0, len(epochs_normalized), batch_size): batch epochs_normalized[i:ibatch_size] batch_tensor torch.from_numpy(batch).float().unsqueeze(1).to(device) # (batch, 1, seq_len) outputs model(batch_tensor) probabilities F.softmax(outputs, dim1) _, preds torch.max(outputs, 1) all_preds.extend(preds.cpu().numpy()) all_probs.extend(probabilities.cpu().numpy()) # 3. 可选的后期处理例如应用睡眠阶段转换规则如REM期不会直接跳到N3期 # all_preds apply_sleep_rules(all_preds) return all_preds, all_probs5.2 可视化与结果分析一个良好的系统应该提供直观的结果展示。睡眠结构图绘制整夜的睡眠阶段序列与专家标注的金标准进行对比。这是最直接的评估方式。概率趋势图对于每个时期绘制模型预测为各个睡眠阶段的概率。这可以帮助我们识别模型不确定的时期这些时期往往是分期困难或存在伪迹的片段。性能报告自动生成包含总体准确率、每类F1分数、Kappa系数和混淆矩阵的文本或HTML报告。5.3 性能优化与部署考虑模型轻量化研究级的模型可能参数量较大。为了部署到资源受限的边缘设备如便携式睡眠监测仪可以考虑模型剪枝、量化或知识蒸馏来压缩模型。实时处理如果用于实时监测需要考虑模型的推理速度。可以使用PyTorch的torch.jit.trace或torch.jit.script将模型转换为TorchScript以提高推理效率。对于更极致的性能可以探索使用TensorRT或ONNX Runtime进行部署。持续学习当有新数据时我们可能希望在不遗忘旧知识的情况下更新模型。这涉及到持续学习或在线学习的技术是一个更高级的话题。6. 常见问题排查与调试心得在开发过程中你肯定会遇到各种“坑”。这里记录一些典型问题和我的解决思路。问题1模型根本不学习训练损失几乎不下降。检查数据首先确保你的数据加载和预处理是正确的。打印几个样本和标签看看信号是正常的脑电图吗标签范围对吗尝试过拟合一个极小的数据集比如几十个样本如果模型连这么小的数据都学不好那肯定是模型或代码有问题。检查损失函数确认你传入的标签是torch.long类型的索引而不是one-hot编码。检查类别权重是否计算正确如果某个类别的权重极大可能会导致训练不稳定。检查学习率学习率可能太高或太低了。尝试一个经典的学习率如1e-4或1e-3。检查梯度在训练循环中打印模型某一层如第一个卷积层的权重的梯度范数。如果梯度是0或接近0可能是网络结构或激活函数导致梯度消失。问题2训练集表现很好但验证集表现极差严重过拟合。增加正则化这是第一反应。加大Dropout比率增加L2权重衰减的系数。简化模型减少网络宽度通道数和深度层数。数据增强增强方式是否足够多样尝试更激进的数据增强。早停务必使用早停法。问题3N1期和REM期的召回率特别低。这是常态这两个阶段本身就难分甚至专家也容易混淆。首先接受这个事实。聚焦于这两个类别可以尝试为N1和REM设置更高的损失权重。或者在训练后期使用一种“课程学习”的策略先让模型学好区分大类别如清醒、NREM、REM再精细区分N1、N2、N3。检查特征可视化模型中间层的特征看看对于N1和REM期模型提取的特征是否真的有区别。也许单通道EEG本身在这两个阶段的信息就不够需要考虑是否真的需要引入其他微弱的特征如基于原始信号计算的心率变异性。问题4推理速度慢。增大批处理大小在GPU推理时批量处理能极大提升吞吐量。使用半精度如果GPU支持使用model.half()和torch.cuda.amp进行混合精度推理可以几乎不损失精度地提升速度并减少内存占用。优化数据加载确保数据预处理和传输不是瓶颈。使用DataLoader的num_workers和pin_memory。一个重要的调试习惯始终在训练开始时运行一个完整的训练和验证周期并打印出损失、准确率以及一个小的混淆矩阵。这能帮你快速确认整个流程是否基本通畅。在PyTorch中善用torchsummary库来可视化模型结构和参数量也是一个好习惯。构建一个鲁棒、准确的单通道脑电睡眠分期系统是一个迭代的过程需要不断地在模型架构、数据处理和训练技巧之间进行权衡和实验。PyTorch提供的灵活性和丰富的生态系统让我们能够相对快速地进行这些探索。记住没有一劳永逸的“最佳模型”只有针对你的特定数据和任务通过反复实验和调试找到的“最适合的模型”。从这个项目出发你可以进一步探索更先进的模型如Transformer、多任务学习同时预测睡眠阶段和睡眠事件甚至是不依赖人工标注的自监督学习方法这些都是当前睡眠分析领域非常活跃的研究方向。本文还有配套的精品资源点击获取