ARTICLE DETAIL

资讯详情

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

单通道脑电睡眠分期Python工程实战:从数据预处理到Flask网页端部署

单通道脑电睡眠分期Python工程实战:从数据预处理到Flask网页端部署 简介这份资源面向计算机、人工智能、通信工程、自动化、电子信息等专业的在校学生与教师以及希望入门生理信号处理的企业员工提供一套基于单通道脑电信号实现自动睡眠分期的完整Python项目。项目围绕EEG信号预处理、数据集构建、深度网络建模与预测推理展开包含GRU等模型实现、Focal Loss损失函数、Sleep-EDF数据下载脚本以及Web端可视化服务适合作为毕业设计、课程设计、作业或项目初期立项演示也便于在现有代码基础上二次修改扩展功能。压缩包共22个文件约10.67MB以12个py源码文件为核心辅以3个txt说明、2个pt模型权重、1个sh运行脚本、1个html页面、1个md文档和1个png示意图覆盖训练、测试、预测与部署全流程。目前已有118人学习下载代码均经测试运行成功答辩评审平均分达96分下载后可按README说明快速复现实验并理解睡眠分期建模思路。1. 单通道脑电睡眠分期的工程化落地从一份能跑通的 Python 源码说起睡眠分期这件事真正卡住大多数人的不是模型结构而是数据怎么读、标签怎么对齐、单通道信号怎么切窗。这份资源给的是一个完整可跑的 Python 工程单通道 EEG 输入深度网络做 5 类分期W/N1/N2/N3/REM带预处理脚本、训练脚本、预测脚本还有一个 Flask 网页端能直接上传信号看结果。它适合三类人做毕设/课设需要一份能答辩的完整工程的学生、想入门生理信号深度学习但不想从零搭数据管线的工程师、以及需要快速验证单通道方案可行性的算法同学。下面按「数据怎么进 → 模型怎么训 → 网页怎么跑 → 坑在哪」的顺序拆一遍。2. 数据管线拆解从 eeg_signal.txt 到可训练张量2.1 单通道睡眠分期的数据形态与选型理由睡眠分期公开数据集里Sleep-EDF 是最常被拿来练手的因为它提供了 Fpz-Cz 和 Pz-Oz 两路 EEG单通道方案通常取 Fpz-Cz。这份工程的数据文件是data/eeg_signal.txt本质是把一段或多段整夜信号按采样点存成文本配套的标签按 30 秒一帧对齐——这是睡眠分期的行业惯例AASM 标准就是 30 秒一帧、5 类。为什么用单通道而不是多通道一是可穿戴设备现实约束二是单通道已经能拿到 0.75 以上的准确率Sleep-EDF 上三是工程复杂度低预处理链路短。代价是 N1 期识别率天然偏低这是信号本身的信息量决定的不是模型能完全补回来的后面避坑章节会专门讲。preprocessing.py和prepare_data.py是两个关键脚本。前者做滤波和归一化后者做切窗和标签对齐。常见做法是 0.5–40 Hz 带通滤波去掉工频和基线漂移再按 30 秒窗、不重叠切分。下面是我一般会先跑一遍的数据检查代码确认信号长度和标签数量对得上import numpy as np # 读取整夜单通道 EEG假设每行一个采样点 signal np.loadtxt(data/eeg_signal.txt) print(信号总点数:, signal.shape) print(采样率假设 100Hz 时总时长(小时):, signal.shape[0] / 100 / 3600) # 30 秒一帧100Hz 采样率 每帧 3000 点 fs 100 epoch_len 30 * fs n_epochs signal.shape[0] // epoch_len print(可切出的帧数:, n_epochs) # 这个数字必须和标签文件的行数一致否则后面训练会错位逻辑说明先确认信号总长度再按采样率和帧长算出理论帧数。参数说明fs必须和采集时一致Sleep-EDF 是 100 Hzepoch_len是 30 秒对应的点数。如果这里算出来的帧数和标签数不一致别急着改模型先回去查数据对齐这是最常见的翻车点。2.2 prepare_data.py 的切窗与标签映射prepare_data.py干的事是把长信号切成帧、把每帧对应的标签取出来、做 z-score 归一化最后存成.npy或直接喂给 Dataset。标签映射一般是W0, N11, N22, N33, REM4。这个顺序不要随意改因为focal_loss.py里的类别权重是按这个顺序配的。import numpy as np def make_epochs(signal, labels, fs100, epoch_sec30): epoch_len fs * epoch_sec n len(signal) // epoch_len X np.zeros((n, epoch_len), dtypenp.float32) y np.zeros((n,), dtypenp.int64) for i in range(n): seg signal[i*epoch_len:(i1)*epoch_len] # 逐帧 z-score消除个体幅值差异 seg (seg - seg.mean()) / (seg.std() 1e-8) X[i] seg y[i] labels[i] return X, y逻辑说明逐帧归一化而不是整夜归一化是因为整夜信号幅值会漂移整夜归一化会让某些时段的特征被压平。参数说明1e-8是防止除零epoch_sec30是 AASM 标准改成别的值标签就对不上了。这一步做完建议打印一下各类别样本数N1 通常只有其他类的三分之一到一半这就是后面要用 focal loss 的原因。2.3 dataset.py 与 DataLoader 的工程细节dataset.py里封装的是 PyTorch 的Dataset返回(x, y)。训练时用DataLoader打乱、分批。这里有个容易被忽略的点睡眠分期是时序任务如果模型里有 GRU/LSTM理论上应该按整夜序列喂但这份工程是逐帧独立分类为主、GRU 做时序建模为辅。model_GRU.pt这个权重文件名说明主模型带 GRU 层。from torch.utils.data import DataLoader, TensorDataset import torch X, y make_epochs(signal, labels) ds TensorDataset(torch.from_numpy(X), torch.from_numpy(y)) # batch 不要太大单通道帧特征维度低64 足够 loader DataLoader(ds, batch_size64, shuffleTrue, num_workers0)逻辑说明shuffleTrue在逐帧分类下没问题但如果改成整夜序列输入就不能 shuffle否则时序断裂。参数说明num_workers0在 Windows 上更稳Linux 可以开到 4。batch_size64是经验值显存小就降到 32。3. 模型与训练network.py、focal_loss.py 和 train.py 怎么串3.1 network.py 的网络结构与 GRU 的位置network.py定义主模型。单通道 EEG 常见结构是一维卷积做局部特征提取 → 池化降维 → GRU 做时序依赖 → 全连接分类。卷积核一般取 50 左右对应 0.5 秒因为睡眠纺锤波、K 复合波都在这个时间尺度上。GRU 放在卷积之后输入是卷积输出的序列。import torch.nn as nn class SleepNet(nn.Module): def __init__(self, n_classes5): super().__init__() self.conv nn.Sequential( nn.Conv1d(1, 32, kernel_size50, stride6, padding25), nn.BatchNorm1d(32), nn.ReLU(), nn.MaxPool1d(8), nn.Conv1d(32, 64, kernel_size8, stride1, padding4), nn.BatchNorm1d(64), nn.ReLU(), nn.MaxPool1d(4), ) self.gru nn.GRU(64, 64, batch_firstTrue) self.fc nn.Linear(64, n_classes) def forward(self, x): # x: (B, 1, 3000) x self.conv(x) # (B, 64, T) x x.permute(0, 2, 1) # (B, T, 64) 给 GRU x, _ self.gru(x) x x[:, -1, :] # 取最后时刻 return self.fc(x)逻辑说明stride6是为了快速降采样减少后续计算量permute是把通道维换到特征维符合 GRU 的batch_first约定。参数说明kernel_size50对应 0.5 秒n_classes5对应 5 类。如果换成二分类睡/醒把n_classes改成 2 即可但标签也要重映射。3.2 focal_loss.py 解决类别不平衡N1 期样本少普通交叉熵会让模型直接放弃 N1。focal_loss.py用 focal loss 降低易分类样本的权重让模型关注难分的 N1。这是这份工程比较实用的一个点。import torch import torch.nn as nn import torch.nn.functional as F class FocalLoss(nn.Module): def __init__(self, alphaNone, gamma2.0): super().__init__() self.alpha alpha # 各类别权重张量 self.gamma gamma def forward(self, logits, target): logp F.log_softmax(logits, dim1) logp_t logp.gather(1, target.unsqueeze(1)).squeeze(1) p_t logp_t.exp() loss -((1 - p_t) ** self.gamma) * logp_t if self.alpha is not None: loss loss * self.alpha[target] return loss.mean()逻辑说明gamma2.0是原论文默认值越大越关注难样本alpha按类别频率倒数设置。参数说明如果 N1 还是学不出来把gamma调到 3或者把alpha里 N1 的权重再调高。注意 focal loss 对学习率敏感通常要比交叉熵小一档。3.3 train.py 的训练循环与验证策略train.py是主训练脚本。常见做法是按受试者划分训练/验证集而不是随机划分帧——因为同一受试者的帧高度相关随机划分会虚高准确率。这一点很多开源代码没做对是答辩时容易被问的点。import torch from torch.optim import Adam from focal_loss import FocalLoss from network import SleepNet device torch.device(cuda if torch.cuda.is_available() else cpu) model SleepNet(n_classes5).to(device) # alpha 按类别频率倒数N1 给高权重 alpha torch.tensor([1.0, 3.0, 1.0, 1.5, 1.5]).to(device) criterion FocalLoss(alphaalpha, gamma2.0) opt Adam(model.parameters(), lr1e-3) for epoch in range(50): model.train() for x, y in loader: x, y x.to(device), y.to(device) x x.unsqueeze(1) # (B, 1, 3000) opt.zero_grad() loss criterion(model(x), y) loss.backward() opt.step()逻辑说明unsqueeze(1)是补通道维因为 Conv1d 要求(B, C, L)。参数说明lr1e-3配 focal loss 偏大如果 loss 震荡就降到 3e-4epoch50是上限实际看验证集 macro-F1 早停。验证时重点看 N1 的 F1不要只看总体准确率。4. 网页端与预测server.py、predict.py 怎么跑起来4.1 predict.py 的推理流程predict.py加载models/model_GRU.pt对输入信号做同样的预处理然后逐帧输出类别。关键是要保证推理时的预处理和训练时完全一致否则结果会莫名其妙地差。import torch import numpy as np from network import SleepNet def predict(signal, fs100): model SleepNet(n_classes5) model.load_state_dict(torch.load(models/model_GRU.pt, map_locationcpu)) model.eval() epoch_len fs * 30 n len(signal) // epoch_len X np.zeros((n, 1, epoch_len), dtypenp.float32) for i in range(n): seg signal[i*epoch_len:(i1)*epoch_len] X[i, 0] (seg - seg.mean()) / (seg.std() 1e-8) with torch.no_grad(): logits model(torch.from_numpy(X)) pred logits.argmax(1).numpy() return pred逻辑说明map_locationcpu保证没有 GPU 也能加载逐帧归一化必须和训练一致。参数说明fs要和训练时一致不一致的话帧长就错了。返回的pred是 0–4 的类别序列对应 W/N1/N2/N3/REM。4.2 server.py 与 Flask 网页端web/目录下是 Flask 应用server.py提供上传接口templates/放页面。跑起来的步骤# 1. 安装依赖 pip install -r requirements.txt # 2. 启动服务 python web/server.py # 3. 浏览器打开 http://127.0.0.1:5000逻辑说明requirements.txt里主要是 torch、numpy、flask。参数说明如果端口被占用改server.py里的app.run(port5000)。上传的信号文件格式要和eeg_signal.txt一致否则解析会失败。4.3 run.sh 与整体复现顺序run.sh是把预处理、训练、预测串起来的脚本。我一般会按这个顺序手动跑一遍确认每步都通python prepare_data.py # 切窗、对齐标签 python preprocessing.py # 滤波、归一化 python train.py # 训练产出 model_GRU.pt python predict.py # 单文件推理验证 python web/server.py # 起网页端逻辑说明先数据后模型再服务任何一步报错都先解决再往下。参数说明如果prepare_data.py报标签数量不匹配回去查eeg_signal.txt和标签文件的行数关系。5. 避坑与排查这份源码跑不起来时先看这几条5.1 信号长度和标签数量对不上现象prepare_data.py切出来的帧数和标签数不一致训练时报 shape 错误。原因eeg_signal.txt可能包含多段信号拼接或者采样率假设错了。解决先打印信号总点数和标签行数用总点数 / (fs * 30)算理论帧数和标签数比对不一致就检查数据来源和采样率。5.2 推理结果全是同一类现象predict.py输出全是 N2 或全是 W。原因推理时预处理和训练不一致最常见的是忘了逐帧 z-score或者fs设错导致帧长不对。解决把推理的预处理代码和prepare_data.py逐行对照确保归一化方式、帧长、采样率完全一致。5.3 focal loss 导致 loss 不下降现象训练几个 epoch 后 loss 卡住不动。原因alpha权重设得太极端或者学习率配 focal loss 偏大。解决先把alpha全设成 1.0 跑一遍确认能下降再逐步加 N1 权重学习率从 1e-3 降到 3e-4 试。5.4 网页端上传后无响应现象Flask 页面能打开上传信号后一直转圈。原因信号太长逐帧推理耗时或者文件格式不对解析卡住。解决先用短信号几分钟测试检查上传文件的列数和eeg_signal.txt一致在server.py里加日志打印解析进度。5.5 GPU/CPU 加载权重报错现象torch.load报RuntimeError: Attempting to deserialize object on a CUDA device。原因权重是在 GPU 上存的当前环境没 GPU。解决torch.load(..., map_locationcpu)这份工程的predict.py已经这么写了如果你自己改过要注意。6. 进阶技巧把单通道方案迁移到自己的数据上拿到这份源码后最有价值的用法不是原样跑一遍而是把它当成模板迁到自己的数据。迁移的核心就三件事采样率对齐、标签映射对齐、预处理对齐。我一般会先写一个适配层把自己的数据转成eeg_signal.txt同款格式再走原管线。import numpy as np from scipy.signal import butter, filtfilt def adapt_my_data(raw, fs_in, fs_out100): # 1. 重采样到 100Hz如果原始不是 if fs_in ! fs_out: ratio fs_out / fs_in n_out int(len(raw) * ratio) raw np.interp(np.linspace(0, len(raw)-1, n_out), np.arange(len(raw)), raw) # 2. 0.5-40Hz 带通去掉基线漂移和工频 b, a butter(4, [0.5/(fs_out/2), 40/(fs_out/2)], btypeband) raw filtfilt(b, a, raw) return raw逻辑说明重采样用线性插值够用要求高就上scipy.signal.resample带通滤波的截止频率按 AASM 推荐。参数说明fs_out100是为了和原工程对齐如果你要保留原始采样率就得同步改prepare_data.py里的fs。标签映射也要对齐成 W0/N11/N22/N33/REM4否则focal_loss.py的alpha就错位了。验证迁移是否成功我习惯先跑一个 sanity check拿自己数据里一段已知标签的信号走完预处理和推理看预测序列和真实标签的 macro-F1。如果低于 0.5八成是预处理没对齐而不是模型不行。这个习惯是从一次血泪经验来的——当时折腾了两天以为是模型问题最后发现是滤波截止频率设错把睡眠纺锤波滤掉了。从那以后我每次迁移数据都强制先跑一遍 sanity check确认预处理链路通了再动模型。希望帮到你。本文还有配套的精品资源点击获取
返回列表