第四章:数据获取与探索:公开数据集介绍与格式解析

做深度学习模型,数据就是燃料。没有好的数据,模型再花哨也是白搭。

睡眠分期这块,有几个公开数据集是绕不开的。我当年刚入行时,就是靠这几个数据集练的手。今天咱们就一个一个掰开揉碎了讲清楚。

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_timeraw.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 数据预处理流水线

拿到原始数据后,不能直接扔给模型。你需要做几步预处理。我总结了一个标准流程:

  1. 重采样:统一到 100 Hz 或 128 Hz
  2. 滤波:0.5-40 Hz 带通滤波
  3. 分段:按 30 秒一个 epoch 切分
  4. 归一化:每个 epoch 独立做 z-score 归一化
  5. 标签对齐:确保每个 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 适合多通道研究。数据下载和格式解析是基本功,预处理流水线要标准化。

嗯,数据这块就讲这么多。记住一句话:数据质量决定模型上限。花时间把数据搞明白,比调参重要得多。


专注资料整理