第四章:数据获取与探索:公开数据集介绍与格式解析
做深度学习模型,数据就是燃料。没有好的数据,模型再花哨也是白搭。
睡眠分期这块,有几个公开数据集是绕不开的。我当年刚入行时,就是靠这几个数据集练的手。今天咱们就一个一个掰开揉碎了讲清楚。
4.1 三大主流公开数据集
目前学术界用得最多的,主要是这三个:Sleep-EDF、SHHS 和 MASS。我个人的习惯是,新手先从 Sleep-EDF 入手,因为它数据量适中,格式也相对友好。
| 数据集 | 全称 | 样本量 | 通道数 | 采样率 | 特点 |
|---|---|---|---|---|---|
| Sleep-EDF | Sleep European Data Format | 约 200 条记录 | 2 (EEG Fpz-Cz, Pz-Oz) | 100 Hz | 最经典,入门首选 |
| SHHS | Sleep Heart Health Study | 约 6000 条记录 | 多通道 (EEG, EOG, EMG) | 125 Hz / 250 Hz | 样本量大,适合大规模训练 |
| MASS | Montreal Archive of Sleep Studies | 约 200 条记录 | 20 通道 (高密度 EEG) | 256 Hz | 通道多,适合多模态研究 |
我的建议:如果你只是想快速验证模型能不能跑通,用 Sleep-EDF 就够了。但如果你想发论文,SHHS 的大样本量更有说服力。MASS 嘛,适合做多通道分析的进阶玩家。
4.2 数据下载与格式解析
4.2.1 Sleep-EDF 数据集
这个数据集最早来自欧洲的睡眠研究项目。数据格式是 EDF(European Data Format),说白了就是一种二进制存储格式,头文件里存着元信息,后面跟着信号数据。
下载地址在 PhysioNet 上。我当年第一次下载时,差点被那个网站搞晕。其实很简单,直接搜 "Sleep-EDF PhysioNet" 就能找到。
数据文件命名规则是这样的:SC4001E0-PSG.edf 是脑电信号,SC4001EC-Hypnogram.edf 是人工标注的分期结果。
小技巧:下载时注意区分 "Sleep-EDF" 和 "Sleep-EDF Expanded"。后者是前者的扩展版,多了几十条记录。我个人习惯用 Expanded 版,数据量更足。
解析 EDF 文件,我推荐用 mne 库。代码很简单:
import mne
# 读取 EDF 文件
raw = mne.io.read_raw_edf('SC4001E0-PSG.edf', preload=True)
# 查看基本信息
print(raw.info)
print(raw.ch_names)
# 提取数据
data, times = raw[:, :]
print(f"数据形状: {data.shape}")
print(f"采样率: {raw.info['sfreq']} Hz")
运行这段代码,你会看到类似这样的输出:
通道名: ['EEG Fpz-Cz', 'EEG Pz-Oz', 'EOG horizontal', 'Resp oro-nasal', 'EMG submental', 'Temp rectal', 'Event marker']
采样率: 100.0 Hz
数据形状: (7, 2176000) # 7个通道,约6小时的数据
嗯,这里要注意。Sleep-EDF 的标注文件是独立的。你需要单独读取 hypnogram 文件:
import numpy as np
# 读取标注文件
annot = mne.read_annotations('SC4001EC-Hypnogram.edf')
# 查看标注
print(annot.onset[:10]) # 前10个事件的起始时间
print(annot.duration[:10]) # 前10个事件的持续时间
print(annot.description[:10]) # 前10个事件的分期标签
分期标签的编码规则是:W(清醒)、N1、N2、N3(非快速眼动睡眠)、R(快速眼动睡眠)。
避坑指南:我曾经遇到过一个问题——标注文件里的时间戳和信号数据的时间轴对不上。后来发现是 EDF 文件头里的记录时长信息有偏差。解决办法是:用 raw.first_time 和 raw.last_time 来校准时间轴。
4.2.2 SHHS 数据集
SHHS 的数据量是真的大。6000 多条记录,每条都有完整的整晚睡眠数据。但它的格式有点特殊——用的是 EDF+ 格式,其实就是 EDF 的扩展版。
下载需要去 NSRR(National Sleep Research Resource)网站申请。流程不复杂,填个申请表,说明研究用途就行。我当年等了两周才拿到权限。
SHHS 的数据文件命名规则:shhs1-200001.edf 是信号文件,shhs1-200001-nsrr.xml 是标注文件(XML 格式)。
解析 SHHS 的 XML 标注文件,我写了个小工具:
import xml.etree.ElementTree as ET
def parse_shhs_annotation(xml_path):
tree = ET.parse(xml_path)
root = tree.getroot()
stages = []
for event in root.findall('.//ScoredEvent'):
stage = event.find('EventConcept').text
onset = float(event.find('Start').text)
duration = float(event.find('Duration').text)
stages.append({
'stage': stage,
'onset': onset,
'duration': duration
})
return stages
# 使用示例
stages = parse_shhs_annotation('shhs1-200001-nsrr.xml')
print(f"共 {len(stages)} 个分期事件")
print(stages[:5])
经验之谈:SHHS 的标注文件里,分期标签用的是 SleepStage 命名空间。比如 SleepStageW 代表清醒,SleepStageN1 代表 N1 期。别搞混了。
4.2.3 MASS 数据集
MASS 数据集来自蒙特利尔大学。它的特点是通道多——20 个 EEG 通道,适合做空间特征分析。
下载需要联系作者,发邮件申请。格式也是 EDF,但采样率是 256 Hz,比 Sleep-EDF 高不少。
MASS 的标注文件是 CSV 格式的,每行对应一个 30 秒的 epoch:
import pandas as pd
# 读取 MASS 标注文件
annot_df = pd.read_csv('MASS_SS1_annotations.csv')
print(annot_df.head())
# 分期标签分布
print(annot_df['stage'].value_counts())
输出示例:
epoch stage
0 0 W
1 1 W
2 2 N1
3 3 N2
4 4 N2
stage
W 120
N1 80
N2 200
N3 100
R 100
小提示:MASS 数据集的分期标准是 AASM 标准,和 Sleep-EDF 的 R&K 标准略有不同。主要区别在于 N3 期的定义。如果你要混合使用多个数据集,记得先统一分期标准。
4.3 数据预处理流水线
拿到原始数据后,不能直接扔给模型。你需要做几步预处理。我总结了一个标准流程:
- 重采样:统一到 100 Hz 或 128 Hz
- 滤波:0.5-40 Hz 带通滤波
- 分段:按 30 秒一个 epoch 切分
- 归一化:每个 epoch 独立做 z-score 归一化
- 标签对齐:确保每个 epoch 都有对应的分期标签
代码实现:
def preprocess_sleep_edf(raw, annot, target_sfreq=100):
# 1. 重采样
raw.resample(target_sfreq)
# 2. 滤波
raw.filter(0.5, 40, fir_design='firwin')
# 3. 提取数据
data, times = raw[:, :]
# 4. 分段 (30秒一个epoch)
epoch_duration = 30 # 秒
epoch_samples = int(target_sfreq * epoch_duration)
n_epochs = data.shape[1] // epoch_samples
epochs = []
for i in range(n_epochs):
start = i * epoch_samples
end = start + epoch_samples
epoch = data[:, start:end]
# 归一化
epoch = (epoch - epoch.mean()) / epoch.std()
epochs.append(epoch)
# 5. 对齐标签
labels = []
for onset, duration, desc in zip(annot.onset, annot.duration, annot.description):
# 将标注映射到epoch
epoch_idx = int(onset / epoch_duration)
labels.append((epoch_idx, desc))
return np.array(epochs), labels
# 使用示例
epochs, labels = preprocess_sleep_edf(raw, annot)
print(f"Epochs shape: {epochs.shape}")
print(f"Labels count: {len(labels)}")
注意:分段时,最后一个 epoch 可能不足 30 秒。我通常的做法是直接丢弃不足的部分,或者用零填充。但零填充会影响模型训练,建议直接丢弃。
4.4 数据可视化探索
在训练模型之前,我强烈建议你先可视化一下数据。看看不同睡眠分期的脑电波形长什么样。
import matplotlib.pyplot as plt
def plot_sleep_stages(epochs, labels, n_samples=5):
fig, axes = plt.subplots(n_samples, 1, figsize=(12, 8))
stage_names = ['W', 'N1', 'N2', 'N3', 'R']
colors = ['red', 'blue', 'green', 'purple', 'orange']
for i, ax in enumerate(axes):
epoch = epochs[i]
label = labels[i][1]
# 提取EEG通道 (假设第一个通道是EEG)
eeg_signal = epoch[0, :]
ax.plot(eeg_signal, color=colors[stage_names.index(label)])
ax.set_title(f'Epoch {i}: Stage {label}')
ax.set_xlabel('Time (samples)')
ax.set_ylabel('Amplitude')
plt.tight_layout()
plt.show()
# 可视化前5个epoch
plot_sleep_stages(epochs, labels[:5])
你会发现,不同分期的波形差异很明显。W 期波形杂乱,N3 期有典型的慢波,R 期则类似清醒但眼动活跃。
核心要点:数据探索不是走过场。我见过太多人拿到数据就直接训练,结果模型效果差,回头才发现是数据预处理出了问题。花 30 分钟看看数据,能省你 3 天的 debug 时间。
4.5 本章小结
今天咱们聊了三个主流数据集的特点和用法。Sleep-EDF 适合入门,SHHS 适合大规模训练,MASS 适合多通道研究。数据下载和格式解析是基本功,预处理流水线要标准化。
嗯,数据这块就讲这么多。记住一句话:数据质量决定模型上限。花时间把数据搞明白,比调参重要得多。