简介:CHB-MIT脑电数据集是麻省理工学院与波士顿儿童医院合作建立的癫痫EEG公开资源,广泛用于癫痫发作检测、特征提取与机器学习算法验证。这份压缩包正是围绕该数据集的实用代码包,共145个文件,以141个MATLAB脚本为主、4个Python脚本为辅,整体仅159KB,轻量易部署。MATLAB脚本针对CHB01/CHB03等多位受试者提供样本熵等特征计算与发作期对比实现,可直接复现脑电信号分析流程,便于横向比较不同受试者的发作特征;Python脚本则可衔接预处理或分类建模,适合生物医学信号处理、模式识别方向的研究者与中高阶开发者。包内脚本覆盖多位受试者与多种发作状态,可在此基础上调整参数、扩展至癫痫检测分类任务。资源面向需要在实际EEG数据上验证算法的人群,已有832人学习下载,是进入癫痫脑电分析、开展跨学科研究的实用起点。
1. CHB-MIT 数据集到底是什么:为什么做癫痫检测的人都绕不开它
第一次在论文里看到 CHB-MIT 这四个字母,很多人以为是什么国产工具包,实际上它是脑电(EEG)领域流传最广的公开癫痫头皮数据集,全称可理解为某儿童医院与某理工学院合作的记录成果,收录了几十位癫痫患者长期监测的头皮脑电信号。凡是做发作检测、发作预测、脑电分类的研究者,几乎都拿它当基准数据跑实验。标题里的「CHB-MIT数据集.rar」,解压后就是一批 EDF 原始信号文件和配套的发作标注文件。
但我要先泼一盆冷水:这个数据集与其说是开箱即用,不如说是一个需要仔细处理的原始仓库。它的标注和信号是分离存储的,采样率不统一、导联顺序不统一、发作时间长短悬殊,直接按图像分类那套流程处理会踩一连串坑。这篇笔记的意义正在于此:从解压rar开始,一步步把数据变成能喂给模型、能给出可信指标的训练样本。
如果你只是想快速验证一个模型结构,或者需要跑论文里的对比实验,这篇笔记能帮你少走弯路;如果你打算直接把它当标准数据集开训,我建议先读完第 5 章的避坑清单再动手。下面的内容覆盖数据组织方式、Python 读取流程、训练集制作和评估口径,按顺序读就能直接跟着操作。
2. 数据集的目录结构与标注体系:从 EDF 文件到 .summary 的元数据
2.1 目录组织与命名规则:chb01 到 chb24 的作用
解压后你会看到一系列以chb开头、两位数字结尾的文件夹,这是按患者编号组织的,常见编号从 01 到 24,每个文件夹代表一个病例,里面包含若干.edf文件和至少一个以summary命名的文本文件。EDF 是 European Data Format 的缩写,是脑电记录设备保存信号的标准格式之一,Python 里的 MNE 和 pyedflib 都能直接识别这种格式。
文件命名有固定规律,比如chb01_01.edf表示该病例的第一个记录片段,chb01_03.edf就是第三个片段。同一个病例的片段之间通常没有严格的时间连续性,每个片段是一段独立的采集记录,长度从几十分钟到几小时不等。长记录被拆成多个 EDF 文件更多是为了控制文件大小,而不是信号本身有中断,这一点会影响后面标签对齐的方式。
有些病例文件夹下除了.edf还会出现带.seizures或.txt后缀的文件,最常见的是chb01-summary.txt这种命名。不同病例的摘要文件格式存在细节差异,这也是为什么后面解析代码要写容错分支,而不是死板地按固定行数读取。
2.2 .summary 文件:发作标注到底怎么读
打开 summary 文件看到的内容有点像表格,核心行由三列组成:文件名、发作开始时间、发作结束时间,单位是秒,坐标是相对该 EDF 文件第一秒的距离。比如下面这一行表示在chb01_03.edf的第 1466 秒到第 1524 秒之间存在一次发作:
chb01_03.edf 1466 1524解析这段文本要注意两点。一是行首的文件名可能带路径也可能不带,按.edf结尾判断最稳妥;二是有的 summary 文件头部带统计说明,例如Number of Seizures这类文字行,解析时要跳过不是三列格式的行。用 Python 读取时按空白字符切分,判断字段数量即可,split()默认会吃掉多余空格和制表符。
还有一类变体是独立汇总文件,把整个病例的发作情况统一记录在一个地方,内容格式和逐文件的方式一致。我一般会写一个通用函数,只认「三列且第三列能转成浮点数」的行,其余全部忽略,这样无论文件头怎么写都能稳定解析。解析完成后建议立刻打印与原始文件做的预检,确认标注区间是否落在信号时间范围内,这个检查能在后续节省大量排查时间。
2.3 元数据清单:采样率、通道数与信号单位
这是动手写代码前必须确认的三项元数据。CHB-MIT 的主干记录采样率是 256 Hz,但部分病例或部分片段是 512 Hz,极少数文件头给出的采样率数值与实际信号长度并不完全吻合。通道数也不是固定的,常见配置有 23 导、24 导,个别文件里混入了心电或温度等非脑电通道。
信号单位以微伏(μV)为主,但不同设备导出 EDF 时可能把单位写成其他形式,MNE 读取后raw.info["units"]未必一致。建议在预处理阶段统一把这些通道映射到一个标准名称列表,否则后面做通道选择和特征计算时,同样的通道在不同文件里可能出现在不同位置。下面列一个解析 EDF 后需要核对的信息清单:
| 项目 | 对应字段 | 注意点 |
|---|---|---|
| 采样率 | raw.info["sfreq"] | 同一病例不同片段可能不同 |
| 通道名 | raw.ch_names | 顺序与数量会变,先统一顺序 |
| 总时长 | raw.times[-1] | 与 summary 秒数比较时用 |
| 通道单位 | raw.info["chs"] | 提取特征前统一单位 |
表格里前三项是每次加载文件都要记录的基础信息,第四项影响后续特征数值的量级。把这些信息打印出来核对一遍,相当于给数据管线做了体检。很多情况下模型指标异常,根源就在于某几个文件的采样率没有被动态读取,而是被写死成了固定值。
3. 用 Python 把 EDF 读取流程跑通:MNE 的最小可运行方案
3.1 安装与加载:mne、numpy、scipy 够用了
MNE 是处理脑电和脑磁图的 Python 库,安装时会自动带上 numpy 和 scipy。我建议用虚拟环境安装,避免和系统 Python 里的其他包互相干扰。安装命令很简单,三行就能把环境准备好:
python -m venv eeg_env source eeg_env/bin/activate pip install mne numpy scipyMNE 读取 EDF 走的是内置解析器。先读单个文件验证环境是否正常,代码里打开chb01_01.edf并打印采样率、通道数和总时长:
import mne raw = mne.io.read_raw_edf("chb01/chb01_01.edf", preload=False, verbose="ERROR") print("采样率:", raw.info["sfreq"]) print("通道数:", len(raw.ch_names)) print("时长(秒):", raw.times[-1])逻辑说明:preload=False表示只读文件头和数据元信息,不把信号加载进内存,后续真正取数时才载入,好处是打开大文件快、内存占用可控;verbose="ERROR"让输出干净,不然 MNE 会打印大段 EDF 头解析结果。raw.times[-1]是最后一个采样点对应的时间,等于总时长减一个采样周期,用这个值和 summary 里的秒数做对照,能第一时间发现单位问题。
如果 MNE 读不了某个文件,我会直接用 pyedflib 兜底,它是专门读写 EDF 的底层库,对格式兼容性更宽松:
import pyedflib f = pyedflib.EdfReader("chb01/chb01_01.edf") print(f.getSignalLabels()) print(f.getSampleFrequency(0)) f.close()pyedflib 返回的通道标签是设备原始名称,不像 MNE 会做部分标准化,但好处是极少被头文件格式问题卡住。遇到解析异常时,先用 pyedflib 试试能不能打开,就能判断问题出在文件本身还是 MNE 的解析逻辑上。
3.2 读入单个 EDF 并核对时间轴
拿到raw之后,第一步不是急着切数据,而是把时间轴和 summary 对齐。MNE 里raw.n_times是信号总采样点数,raw.times是每个采样点的时间数组。核对总时长的标准写法是用采样点数除以采样率:
total_sec = raw.n_times / raw.info["sfreq"] print("总时长(秒):", total_sec)如果raw.times[-1]和total_sec之差在一个采样周期内,属于正常现象,因为索引从 0 开始;如果差很多,就得回头检查采样率读取是否正确。另一个常见情况是某些 EDF 文件头记录的是整数采样率,但实际信号长度与之相除后不是整数,这种文件建议直接按实际长度计算,不要过度信任文件头。
时间轴核对通过后,把 summary 里读到的发作区间在raw上做一次换算,用采样点索引表示:
seizures = load_seizure_marks("chb01/chb01-summary.txt") marks = seizures.get("chb01_03.edf", []) print("发作区间(秒):", marks) for s, e in marks: print(f"发作占用采样点: {int(s * raw.info['sfreq'])} - {int(e * raw.info['sfreq'])}")这里把秒换算成采样点,是为了后续滑窗时直接用整数索引。换算公式是sample = int(second * sfreq),没有别的花样,但很多人在这里把采样率写成 256,遇到 512 的文件就全部错位。换算时一定从raw.info动态取数,这也是代码里反复强调sfreq的原因。
3.3 用 summary 在信号上标注发作区间
现在把整个病例的所有文件遍历一遍,建立「文件名 -> 发作列表」的完整映射,为下一步切窗做准备。这个函数会自动寻找 summary 文件,不写死文件名,因为不同病例的 summary 命名有差异:
import os def build_seizure_map(folder): seizure_map = {} summary_path = None for name in os.listdir(folder): if name.endswith("summary.txt"): summary_path = os.path.join(folder, name) break if summary_path is None: return seizure_map with open(summary_path, "r", encoding="utf-8") as f: for line in f: parts = line.strip().split() if len(parts) == 3 and parts[0].endswith(".edf"): fname = parts[0] if not os.path.isabs(fname): fname = os.path.join(folder, parts[0]) t0, t1 = float(parts[1]), float(parts[2]) seizure_map.setdefault(fname, []).append((t0, t1)) return seizure_map这段代码解决两个问题。一是自动搜索以summary.txt结尾的文件,兼容chb01-summary.txt和chb01seizure-summary.txt这类命名差异;二是发作区间按文件名聚合,后面按文件切窗时直接查表。还要处理路径拼接,如果 summary 里的文件名不带目录前缀,就手动拼接完整路径,带绝对路径的情况直接使用。
实际项目中我还见过标注行里混入制表符或全角空格的情况,split()默认按任意空白切分,正好兼容这类脏数据。解析完成后建议打印映射的键值数量,确认每个文件都对应了正确的发作标注,这一步能提前暴露路径拼接或文件命名不一致造成的静默错误。
4. 制作训练集:滑窗、标签对齐与类别均衡的落地姿势
4.1 滑窗切分:窗口长度与重叠率怎么定
原始信号是连续的长序列,模型不能直接吃整个文件,需要切成短窗口。窗口长度通常选 2 到 10 秒,具体取决于任务目标。做发作检测一般 4 秒比较平衡:太短,频率信息不够;太长,发作边界会被切进多个窗口,标签变得模糊。做发作预测时窗口可以适当缩短,因为要捕捉发作前的短暂变化。
重叠率的常见取值是 50% 到 75%。重叠的作用是增加样本量,但也让相邻窗口高度相关,评估时容易造成数据泄漏。我一般先把重叠设为 0,把数据管线和基线跑通,再逐步增加重叠做数据增强,这样排查问题时维度更少。
import numpy as np def sliding_window_epochs(raw, window_sec=4.0, overlap=0.0): sfreq = raw.info["sfreq"] win = int(window_sec * sfreq) step = int(win * (1 - overlap)) epochs = [] for start in range(0, raw.n_times - win, step): epochs.append(raw.get_data(start=start, stop=start + win)) return np.array(epochs)这个函数返回三维数组,形状是(样本数,通道数,窗口采样点数)。注意用raw.get_data而不是raw[:, start:stop],后者返回的元组处理起来更绕。step的计算有一个细节:当overlap=0.5时,step是win的一半,保证窗口有一半交叠;当overlap=0时,step=win,窗口首尾相接,不会丢中间数据,但最后一个不足窗口长度的尾段会被丢弃。
窗口长度和重叠率其实是一对需要一起调参的量,增大窗口会提高频率分辨率,但降低对较短发作的分辨能力;提高重叠率会增加样本数,同时放大样本间相关性。参数记录里务必同时写下这两个值,只写其一,后面复现时对不上。
4.2 标签对齐:把秒级标注映射成逐样本标签
切窗只是第一步,关键是把每个窗口标成发作或非发作。如果窗口与一个发作区间有任何重叠,就标为发作。这里说的「任何重叠」用的是区间相交判断:窗口开始秒小于发作结束秒,且窗口结束秒大于发作开始秒,两个条件同时成立才算重叠:
def window_is_seizure(window_start, window_end, seizure_intervals): for s, e in seizure_intervals: if window_start < e and window_end > s: return True return False判断逻辑里最容易写错的是边界条件。窗口正好在发作前一秒结束,window_end == s,不算发作;窗口从发作结束的那一秒开始,window_start == e,也不算。所以我用严格小于和严格大于,避免把紧贴发作边界的正常窗口误标为正样本。
还有一种做法是要求窗口有超过一定比例面积落在发作区间内才标为正样本。当发作持续时间比窗口短时,4 秒窗口只覆盖到 0.2 秒的发作也被标成阳性,会引入大量噪声标签。具体阈值选 0.1 还是 0.5,取决于你更容忍漏报还是更容忍标签噪声。我建议先按「任意重叠」跑一版,看模型在发作边界的预测情况,再决定是否收紧标准。
生成标签时还有一个效率问题:如果每个窗口都遍历一遍所有发作区间,数据量大时很慢。可以先按窗口起始秒排序,再对每个文件的发作区间做二分查找,或者直接用一个布尔掩码标记发作段所在的采样点范围,滑窗时查看窗口内是否有被标记的点。后一种做法更直观,也更容易调试。
4.3 发作样本太少:过采样与数据增强的常见做法
CHB-MIT 的现实问题是大多数病例发作次数有限,有的病例只有两三次发作,切窗后发作样本可能只有几十个,而正常样本有几千个。类别不均衡到这个程度,直接训练模型会完全偏向大多数类,准确率虚高但几乎抓不住发作。
常见做法是先统计每个病例的正负样本比例,然后分策略处理。如果只是做二元分类,先试欠采样:把正常样本随机抽到和发作样本接近的数量。缺点是把大量数据丢弃,模型容易欠拟合。如果嫌欠采样浪费数据,用加噪增强:
def add_noise(epoch, snr=1.0): noise = np.random.normal(0, np.std(epoch) * 0.1, epoch.shape) return epoch + noise * snr加噪声增强的假设是脑电信号本身受噪声污染,模型应该对小幅扰动保持鲁棒。另一种常见增强是时间位移,把一个窗口往左或往右平移几个采样点再截断。这两种做法都不会改变信号的频率结构,比直接对原始信号做 SMOTE 更安全,因为脑电通道间相关性很强,插值容易生成极不自然的波形。
如果你坚持用 SMOTE,建议在特征层面做,不要做在原始信号上。原始信号是(通道,时间)矩阵,通道间不是简单的高斯分布,SMOTE 按样本间线性插值会造出许多不可能出现的脑电波形。数据增强的目标是增加特征空间的覆盖,而不是虚构生理信号,这个边界要守清楚。
5. CHB-MIT 避坑指南:5 个让模型翻车的典型问题与排查方法
5.1 发作标注秒数远超信号长度:时间单位与采样率混用
现象:解析 summary 后,发现某次发作的开始时间比 EDF 文件总时长还大,滑窗时直接数组越界,或者生成的标签全部落在文件结尾之外。
原因:summary 里的时间确实是秒,但换算到采样点时容易被当成采样点序号,甚至把其他文件的采样率套到当前文件上。还有一种情况是标注对应的是记录设备内部时钟的绝对秒数,而 EDF 文件从某个相对时刻才开始记录,导致标注区间与文件时间轴整体偏移。
解决:每次读取都用raw.info["sfreq"]动态换算,不要写死 256。读文件后立刻打印raw.times[-1],与 summary 里该文件的最大结束秒对比一次。如果完全对不上,先确认打开的文件名与 summary 里写的是否一致,再看路径拼接有没有指向其他病例的同类文件,最后才考虑单位换算问题。
5.2 同病例不同文件采样率不一样:重采样是必须的预处理
现象:训练时某几个 epoch 的维度比其他 epoch 少一半,或者模型在部分样本上特征计算异常,报错提示数组维度不一致。
原因:CHB-MIT 并不是所有文件都是 256 Hz,部分片段采样率是 512 Hz 或更高,同一个病例的不同记录会话也可能不同。
解决:在加载数据阶段统一重采样到目标采样率。MNE 里用一行raw.resample(256)完成,注意要在构建 epoch 之前执行,否则每个 epoch 的窗口采样点数不一致,后面的特征提取全部乱套。我的习惯是把目标采样率设为全局变量,所有文件加载后先重采样到这个值,再进入切窗流程,这样后续代码里可以安全地假设所有数据采样率一致。
5.3 通道顺序不同:同样名字的导联在不同文件里不在同一列
现象:用固定索引取通道,比如epoch_data[0]取到的不是预期中的前额通道,特征和标签集体错位,训练出来的模型完全不可解释。
原因:EDF 文件的通道顺序由采集设备当时的配置决定,不同会话、不同病例都可能不同。MNE 会标准化部分通道名,但不会改变通道在数组中的位置。
解决:每次读取后,用raw.ch_names建立「通道名 -> 索引」的映射,再按统一顺序重排数据。只保留所有文件都存在的公共通道,通常是一组头皮脑电通道,排除心电、肌电等辅助通道。千万不要用数字索引写死通道位置,否则换一个病例就悄悄出错。
5.4 MNE 读文件报错或警告刷屏:EDF 头字段的兼容性问题
现象:mne.io.read_raw_edf抛出无法解析的异常,或者虽然能读但打印大段警告,严重时直接中断脚本运行。
原因:EDF 文件头中有些字段是设备导出时留空的,或者写了非标准字符,MNE 的解析器对这些情况要求较严格,而 pyedflib 的实现更宽松。
解决:先用 pyedflib 打开文件,打印信号标签和采样频率,确认文件本身没有损坏。如果 pyedflib 能正常读,就用它读出数据,再用 MNE 的RawArray重新包装,手动指定采样率和通道名。这样既保留 MNE 后续的预处理能力,又绕开了解析器对头部字段的严格限制。
5.5 评估指标虚高:同一个病人的相邻窗口同时进了训练集和测试集
现象:测试集准确率超过 0.95,换到其他病人或新数据上直接跌到 0.6 以下,甚至不如随机猜测。
原因:滑窗产生的时间相关性没有被尊重。无论有没有重叠,同一个 EDF 文件切出来的窗口天然高度相似,如果训练集和测试集都从同一段记录里取,模型等于记住了相邻片段的模式,这就是典型的数据泄漏。
解决:严格按病例划分数据。训练集和测试集绝不能包含同一个病例的窗口,跨患者的评估应该用留一患者法,一次把一个病例的全部数据当测试集,其余病例当训练集。做患者内评估时也要按记录文件分组,别让同一个 EDF 切出的窗口跨集合出现,这同样会高估性能。
6. 把 CHB-MIT 用出价值:用 STFT 特征跑通基线并验证训练没白费
6.1 先定评估口径:患者内还是跨患者
做 CHB-MIT 实验前,先问自己一个问题:模型部署时面对的是新患者还是老患者。如果是长期监测同一个患者,做患者内评估合理;如果想验证模型泛化能力,必须做跨患者评估。论文里两个数字都能看到,但跨患者的敏感性、特异性才更能说明模型的实际价值,因为真实场景里你不可能提前录好每个患者的发作样本。
6.2 用 STFT 特征 + 随机森林跑通基线
先用轻量特征跑出基线,再用深度学习模型对比。STFT 特征计算量小,能快速验证数据管线是否正确。下面这段代码把每个 epoch 按四个频带分别取短时傅里叶变换的幅度均值:
from scipy.signal import stft import numpy as np def extract_band_features(epoch, sfreq=256): f, _, Zxx = stft(epoch, fs=sfreq, nperseg=128, noverlap=64) mag = np.abs(Zxx) bands = {"delta": (1, 4), "theta": (4, 8), "alpha": (8, 13), "beta": (13, 30)} feats = [] for band, (lo, hi) in bands.items(): mask = (f >= lo) & (f < hi) feats.extend(mag[mask].mean(axis=0)[:5]) return np.array(feats)这里取每个频带前 5 个时间窗的均值,是为了把特征维度固定下来,不同采样率和窗长下都能对齐。实际使用时特征要按通道堆叠,维度会比较大,先在少量数据上验证特征能区分发作与非发作,再考虑是否降维。然后套一个随机森林分类器,训练测试严格按病例划分:
from sklearn.ensemble import RandomForestClassifier clf = RandomForestClassifier(n_estimators=100, max_depth=8, random_state=42) clf.fit(X_train, y_train) print("训练集准确率:", clf.score(X_train, y_train)) print("测试集准确率:", clf.score(X_test, y_test))看训练集和测试集准确率的差距:如果训练集高而测试集低,是过拟合;如果两者都低,说明特征不够判别性。刚开始复现时先把目标放低,比如只在单个病例上做患者内二分类,能稳定到 0.8 以上再扩大到跨患者场景,这样排错范围小,问题定位更快。
6.3 记录这几个字段,防止自己后面返工
实验跑完别急着关终端,把这几样记下来:病例编号、目标采样率、窗口长度与重叠率、标签对齐策略、特征参数。同一个数据集,这些参数不同的人会得到差很远的指标,不记录的话,下次调参或写论文时就是一场灾难。常见的情况是隔几天再跑,已经忘了当时的配置,指标从 0.9 掉到 0.7,最后发现只是重叠率被无意改掉。我的习惯是每跑一组实验就把参数和结果写进注释,形成一个可复现的配置快照,切换实验时从配置文件读取,而不是靠记忆。这套流程走下来,CHB-MIT 在你手里就不再是一个解不开的压缩包,而是能被你控制、能给出可信结论的数据集。希望帮到你。
本文还有配套的精品资源,点击获取