
1. 数据集来龙去脉为什么CHB-MIT这么多年来始终绕不开如果你是做癫痫EEG相关研究的人有一个数据集你迟早会碰到那就是CHB-MIT。这个来自波士顿儿童医院Childrens Hospital Boston的公开脑电数据集几乎是癫痫检测、发作预测、EEG分类方向入门必跑的benchmark。它由MIT与波士顿儿童医院联合发布多年来一直是国内外论文里出现频率最高的公共数据集之一很多经典算法、深度学习模型的验证都跑在它上面。这套数据采集自22位难治性癫痫患儿其中包含5例男性和17例女性年龄从1.5岁到22岁不等共记录了24例病例的连续头皮EEG信号部分患者因随访时间跨度大被当作多个独立病例记录。所有信号均采用国际标准10-20导联系统放置电极采样率统一为256Hz16位分辨率22到23个通道不等部分记录额外包含心电等生理信号通道。简单来说这就是一份质量相当高、标注相对规范、规模适中的长时程癫痫脑电记录特别适合用来做发作检测和发作起始区分析。这一系列文章我计划从数据集的底层格式解析开始一步步带你走通数据读取、标注转换、预处理、坏道处理、发作片段提取再到特征工程和深度学习建模的完整链路。今天这一篇先解决最基础也最容易让人卡住的问题EDF文件到底是什么结构、怎么用Python正确读出来、标注文件怎么解析以及拿到手后第一步该做什么。适合什么人来读两类人。第一类是刚接触癫痫EEG分析、打算用CHB-MIT跑通第一个实验的研究生或工程师你可能之前只会调用现成工具但没搞懂底层数据长什么样。第二类是已经跑过一些代码但对数据集细节知其然不知其所以然的人比如为什么有的文件是23通道有的24通道为什么标注时间不能直接用这些问题你需要一个明确答案。2. 标准数据集背后的采集细节与通道布局2.1 病例组成与文件命名规则先花点时间把数据集的物理结构讲清楚因为这个结构直接影响后续所有代码逻辑。下载解压后你会看到以chb01、chb02这类命名的子文件夹每个文件夹对应一名患者。每个患者文件夹里包含若干EDF格式的脑电记录文件文件命名通常是这样的chb01_01.edf chb01_02.edf chb01_03.edf ... chb01_29.edf每个EDF文件代表一段独立的连续记录时间时长从几分钟到数小时不等。以chb01为例全部记录的累积时长约40小时其中的发作事件分布在6个文件里。除EDF文件本身外每个患者文件夹里还有一个名为chb01-summary.txt的文本文件也可能是.txt格式这个summary文件记录了该患者全部EDF文件中癫痫发作起止时间是整个数据集唯一可靠的标注来源。22位患者中注意一个特殊情况chb12和chb13来自同一位患者该患者在不同时间点接受了两轮记录chb14到chb24也是按不同时间段的随访检查划分的所以论文里常说“22个受试者24个病例记录”。这个细节在写论文时如果表述不严谨容易被审稿人挑毛病。2.2 通道配置与10-20系统这套数据绝大部分采用的是双极参考导联导联名称在EDF文件头里直接标注常见的有FP1-F7、F7-T7、T7-P7、P7-O1这类经典双极链每个文件的通道数量差异主要取决于是否额外录制了心电或其它生理信号。以chb01为例它的标准通道如下FP1-F7, F7-T7, T7-P7, P7-O1, FP1-F3, F3-C3, C3-P3, P3-O1, FP2-F4, F4-C4, C4-P4, P4-O2, FP2-F8, F8-T8, T8-P8, P8-O2, FZ-CZ, CZ-PZ, P7-T7, T7-FT9, FT9-FT10, FT10-T8, T8-P8最后两个导联FT9-FT10和FT10-T8是颞叶下方的电极组合对颞叶癫痫的棘波检测非常重要。实际处理时你会发现部分文件会多出一个心电图通道也就是ECG或者因为记录设备的问题出现通道缺失、坏道。这些都是预处理阶段必须处理到的不能拿到手就直接丢进模型。2.3 为什么选择EDF格式存储EDFEuropean Data Format是医学信号领域最常见的标准格式之一由欧洲多个睡眠实验室在上世纪90年代推动建立后来在脑电、心电、多导睡眠监测等领域广泛使用。EDF的精髓在于一个二进制文件前半部分用ASCII码写了固定的文件头后半部分是纯数字的信号数据块通过文件头里的元信息解释每一段数据对应哪个通道、多少个样本点。这种设计的优势非常明显文件自包含不依赖外部数据库就能完整还原记录现场而且EDF格式是公开标准任何语言都能实现解析。代价是文件头结构略显老旧有些字段比较“死板”比如患者名、记录编号等字段长度固定为80字节信号个数、采样率等字段长度固定为8或10字节。旧格式带来的这些问题实际读取时经常引发小麻烦后面我会逐一提到。3. EDF文件结构与Python读取实战3.1 文件头字段逐项拆解EDF文件头主要分成三大部分第一部分是固定长度的全局头信息第二部分是每个信号通道的局部头信息第三部分是各通道的数据正文。全局头信息的关键字段如下表所示偏移量长度(字节)字段含义08版本号通常为0880患者ID可包含姓名、性别等8880记录ID1688记录开始日期1768记录开始时间1848文件头总字节数19244保留字段2368数据记录段数量2448每个数据记录段的时长秒2524通道数信号个数每个通道的局部头信息有256字节包括通道名称16字节、转换因子8字节、物理量纲8字节、幅值范围、采样点数等。数据正文则按“数据记录段”为单位存放每段包含所有通道在特定时长内通常是1秒的样本通道顺序和局部头信息的顺序一一对应。3.2 pyedflib和mne的选择Python生态里读取EDF文件主要有两个选择pyedflib和MNE-Python。pyedflib是底层的EDF读写库功能直接适合对数据细节有清晰要求的开发者MNE-Python则是一套完整神经影像分析框架内置了EDF读取器并且会顺手做很多数据处理比如自动提取通道类型、生成Raw对象更适合直接用现成流程做预处理。我的建议是第一个实验用MNE就够了它对EDF的兼容性好几行代码就能把数据读进内存。但如果你打算在服务器上做大规模离线处理或者不想引入过重的依赖直接基于pyedflib写一套轻量级读取函数也很方便。下面给出两种方式的示例。3.3 Python读取代码与细节说明用MNE读取一个EDF文件并查看基本信息只需这几行import mne import numpy as np # 读取EDF文件 raw mne.io.read_raw_edf(chb01_03.edf, preloadTrue, verboseERROR) # 查看基本属性 print(raw.info) print(raw.ch_names) print(采样率:, raw.info[sfreq]) print(数据矩阵形状:, raw.get_data().shape)preloadTrue会把数据全部加载进内存适合文件不大的情况单个EDF几十分钟的256Hz数据大概几十到两百兆不等。如果只是先做元数据检查建议preloadFalse快速扫描文件头避免内存开销。用pyedflib读取的写法是import pyedflib import numpy as np with pyedflib.EdfReader(chb01_03.edf) as f: n_channels f.signals_in_file ch_names f.getSignalLabels() sfreq f.getSampleFrequency(0) sample_count f.getNSamples()[0] duration sample_count / sfreq print(通道列表:, ch_names) print(采样率:, sfreq) print(记录时长(秒):, duration) # 读取第0个通道的全部数据 signal f.readSignal(0).astype(np.float64)实际使用中我的建议是如果不用MNE后面那套滤波、ICA、epoch等高级功能pyedflib足够轻量顺手如果你本来就要做完整EEG预处理那就直接用MNE少写很多胶水代码。3.4 EDF文件头解析的踩坑记录EDF格式有一些经典坑我当年第一次用的时候踩了几次列出来供你排雷。第一患者ID字段、记录时间字段都是定长文本内容是带空格的直接用字符串切片没问题如果试图解析成日期时间对象注意去掉首尾空格。第二全局头中的通道数和后面的nSignals字段可能不一致少数文件里有“幽灵信号”就是头信息里声明了通道但数据段里根本没有对应采样点遇到这种情况pyedflib读出来的通道数和文件头声明对不上需要根据实际采样点数做修正。第三转换因子的问题EDF里每个通道的单位不一定是µV可能是mV甚至直接是ADC码值读数据时一定要用文件头里的物理量纲和转换因子换算否则后续幅值全部出错。最简单的方法还是用pyedflib的readSignal函数它会自动完成换算。4. 标注体系与发作事件提取4.1 summary文件结构与发作时间解析拿到CHB-MIT数据后真正花时间的不是读EDF本身而是把summary文件里的标注对到具体的EDF文件上。以chb01-summary.txt为例其内容片段如下File Name: chb01_03.edf File Start Time: 15:56:29 File End Time: 15:59:57 Number of Seizures in File: 1 Seizure Start Time: 2967 seconds Seizure End Time: 3036 seconds这里的美妙之处在于一个summary里出现多个File Name块每个块对应一个EDF文件的全面信息块内部有“Number of Seizures in File”字段如果大于0就继续读下面每一个“Seizure Start Time”和“Seizure End Time”。时间单位是秒以该EDF文件的起始时刻为0点。比如上面这个文件从EDF文件开始记录起第2967秒起第3036秒止一共持续69秒的发作。注意有些文件同时包含多次发作summary里会依次列出每一段的起止时间。4.2 一个可以复用的标注解析脚本我写过一个精简的解析函数每次处理一个患者文件夹时直接调用返回一个字典键是文件名值是该文件中所有发作事件的起止时间列表import re from collections import defaultdict def parse_chb_summary(summary_path): with open(summary_path, r) as f: content f.read() # 按File Name分块处理 file_blocks re.split(rFile Name:, content)[1:] out defaultdict(list) for block in file_blocks: lines block.strip().split(\n) fname lines[0].strip() for line in lines[1:]: line line.strip() if line.startswith(Seizure Start Time:): start float(line.split(:)[1].strip()) # 下一个Seizure End Time才对应 end_match re.search(rSeizure End Time:\s*([0-9.]), lines[lines.index( line)1] if lines.index( line)1 len(lines) else ) if end_match: end float(end_match.group(1)) out[fname].append((start, end)) return dict(out)需要注意原始summary文件里有的行首带空格有的不带直接用split经历不同版本数据时可能出现index错误所以我这个版本里严格按行匹配更稳妥。另外还有一个需要注意的地方就是某患者比如chb10的summary里开头几行有一个“Number of Seizures: ”的全局总数字那个只管整个文件的总和不影响我们按文件块解析。实际上更稳妥的做法是直接用状态机按行解析遇到“File Name”切文件遇到“Seizure Start”记录start遇到“Seizure End”补上end这样不依赖行数和换行格式。我测试过不同患者文件夹里的summary格式有少量细节不一致比如chb24的文件名后缀是chb24_07.edf.chb24_07.edf这种重复命名不影响解析正确处理的时候按路径找文件即可。4.3 发作段与正常段的划分策略有了起止时间下一步就是把这些时间点转成样本索引。假设EDF的采样率是256Hz那么第2967秒对应第2967*256759552个样本点第3036秒对应第777216个样本点。虽然CHB-MIT里发作标注的时间粒度是秒级但这个秒其实并非整数有些是带小数的直接用整数乘采样率会丢掉部分精度建议保留小数乘完再取整。这里通常有两种策略。一种是把发作段完整截下来作为正样本再从非发作区域随机截取等长片段作为负样本另一种是固定窗口法比如每个窗口2秒滑窗时保持发作起止时间的边界对齐窗口与发作段交集超过某个阈值就算正类。前者实现简单后者更细致适合做逐窗口分类任务。实操中我倾向于后者因为发作的起止边界往往是模型最容易错分的位置把边界附近窗口也纳入训练有助于模型学习过渡态。5. 预处理链条坏道检测、滤波与降采样5.1 EEG坏道检测到底怎么做EEG坏道检测是预处理中非常关键的一环。CHB-MIT虽然整体数据质量尚可但毕竟记录对象是儿童儿童在长时间监测中头部活动多有些通道的电极松动或脱落很常见采集出来的信号要么是一段幅度极高的平直或饱和波形要么是持续的无规则高频毛刺。如果不处理这些通道会把后面所有频段分析全带偏。坏道检测的方法核心思路就是找“和其他通道显著不同的时间序列”。我常用的检测流程分为三步第一步逐通道计算时域统计量比如方差、峰峰值、绝对幅度中位数。如果一个通道的方差比其它通道高出20倍以上基本可以判定是坏道或异常饱和如果一个通道的峰峰值长期接近采集器满量程说明电极可能短路或脱落。第二步做频域检测计算每个通道在全频段、高频段比如35-100Hz和低频段0.5-4Hz的功率占比。坏道通常在中高频段表现出异常的高功率因为系统里混入设备干扰或接触不良的噪声。第三步检查与相邻通道的相关系数。正常EEG信号在相邻电极之间往往有较高的空间相关性坏道与周围通道的相关系数一般会显著下降。把相关系数低于某个阈值的通道标记出来再结合前两步结果做投票判定。下面是一段基于MNE的坏道自动识别参考代码import numpy as np def detect_bad_channels(raw, threshold_variance20.0, threshold_corr0.4): data raw.get_data() n_ch data.shape[0] bad_ch set() # 1. 方差检测 variances np.var(data, axis1) median_var np.median(variances) for i, v in enumerate(variances): if v threshold_variance * median_var: bad_ch.add(raw.ch_names[i]) # 2. 高频功率占比检测 from scipy.signal import butter, filtfilt high_freq_power {} fs raw.info[sfreq] b_high, a_high butter(5, 35/(fs/2), btypehighpass) b_low, a_low butter(5, 4/(fs/2), btypelowpass) for i in range(n_ch): sig data[i] - np.mean(data[i]) high_f filtfilt(b_high, a_high, sig) low_f filtfilt(b_low, a_low, sig) high_power np.sum(high_f**2) low_power np.sum(low_f**2) high_freq_power[raw.ch_names[i]] high_power / (low_power 1e-8) median_ratio np.median(list(high_freq_power.values())) for ch_name, ratio in high_freq_power.items(): if ratio 5 * median_ratio: bad_ch.add(ch_name) # 3. 相关检测与相邻通道 for i in range(1, n_ch): corr np.corrcoef(data[i-1], data[i])[0, 1] if np.abs(corr) threshold_corr: bad_ch.add(raw.ch_names[i]) return list(bad_ch)这个版本偏向工程化未必完美但检测结果可以用MNE的raw.info[bads]属性记录下来后续无论做ICA、伪迹剔除还是画图都会自动跳过这些通道。手动审查时我会把自动检测出的坏道叠加在脑电波形图上逐段确认避免误杀有效通道。5.2 滤波参数的确定和分频段分析基础脑电信号的有效频率范围大约在0.5到100Hz之间超过100Hz的成分基本都是肌电或电磁干扰。CHB-MIT的采样率是256Hz按奈奎斯特定理其可分析的最高频率是128Hz实际工程中使用低通滤波器上限一般设为70或100Hz。对癫痫发作检测而言最关注的频段通常是0.5-4Hzdelta波慢波活动部分局灶性发作后期会出现明显的delta节律4-8Hztheta波某些颞叶癫痫发作期会出现theta节律演化8-13Hzalpha波静息态背景节律13-30Hzbeta波发作放电的快波活动30-70Hzgamma波高频振荡与癫痫发生区有较强关联。我的推荐流程是先做0.5Hz高通和70Hz低通滤波再看看50Hz工频干扰严重与否。如果频谱图上50Hz处有明显的尖峰需要加50Hz陷波滤波器如果50Hz处没有明显峰值则尽量不做陷波因为陷波器会连带损失一部分邻频信息这对后面高频振荡分析不利。下面是一个典型预处理管线把滤波、重参考和坏道标记都做了import mne raw mne.io.read_raw_edf(chb01_03.edf, preloadTrue, verboseERROR) # 重采样可选如果后续要降采样到128/200Hz # raw.resample(200) # 带通滤波注意先做高通再做低通 raw.filter(0.5, 70, fir_designfirwin, verboseERROR) # 50Hz陷波观察频谱后决定是否启用 # raw.notch_filter(50, verboseERROR) # 设置参考常用平均参考 raw.set_eeg_reference(average, projectionFalse, verboseERROR) # 统一通道类型保证后面MNE的处理能识别 raw.set_channel_types({ECG: ecg} if ECG in raw.ch_names else {})有一点要特别说明CHB-MIT多数记录本身就是双极导联双极导联下相邻通道的差值已经把共模干扰抑制掉了一部分这时候再做一次平均参考效果有限但也不是完全没意义。平均参考在数据质量波动大的文件里能进一步压平基线漂移。具体做不做建议针对每个文件目检后再决定。5.3 降采样和内存优化的工程考量256Hz的采样率在很多任务场景下其实是偏高的。比如做癫痫发作检测时脑电的主要特征集中在0.5-40Hz过高的采样率只会增加计算负担和内存占用。如果目标是快速跑通分类实验往往降到128Hz甚至100Hz就够用计算量能减少一半以上信息损失在可接受范围内。MNE的resample函数很方便但要注意重采样前必须做好低通滤波防止混叠。MNE的resample内部会自动选择合适的方法如果不放心可以先用lowpass滤波到40Hz再resample到128Hz。内存方面一个256Hz、23通道、1小时的数据float64存储大约是256360023*8约170MB多个文件同时载入会很吃内存。如果做全数据集实验强烈建议按窗口切分后使用数据管道不要一次性把所有EDF全部读进来。用一个生成器逐文件读取、逐窗口产生样本是工程实践里更稳的方式。6. 训练集验证集划分与数据泄漏预防6.1 为什么要按患者划分而不是按文件划分做癫痫检测模型时最常见的错误之一是把同一患者的多个文件随机分进训练集和测试集。理论上同一患者的EEG极具个体特异性模型记住了这个人特征测试时看到同一个人的另一段数据就能轻松做对导致在测试集上效果虚高而换到新患者身上效果暴跌。正确的做法是患者划分比如22位患者的完整记录中选17位作为训练集5位作为验证集或测试集保证训练和测试时见不到同一个人的任何数据。这样评测出来的跨患者泛化能力才是临床上有意义的指标。如果想做半泛化实验也可以采用患者中一部分文件训练、一部分文件测试但这需要明确标注并谨慎质疑模型学到了多少患者特异性特征。更严格的做法是采用双交叉验证或leave-one-subject-out只是计算量会成倍增加不能作为日常快速实验的首选。6.2 序列切分时防止相邻窗口信息混叠另一个容易被忽略的泄漏源是数据增广或滑窗切分时相邻窗口之间高度重叠。如果训练集和测试集来自同一个患者的连续记录且窗口的起始点只差几十毫秒那么训练集和测试集的实际内容几乎重合评测指标会严重失真。打断信息混叠的方法很简单切窗口时给训练和测试各留一段独立时间区域或者让窗口间隔大于窗口长度保证两个集合之间没有重叠覆盖。另外在做归一化、z-score标准化时必须用训练集统计量并把同样的均值和标准差应用到测试集上。这一点做特征工程时很容易翻车如果直接从整个数据集上统计均值和方差相当于把测试集的信息提前泄漏进了训练阶段得到的效果同样不值得信任。我在组织CHB-MIT实验时通常会先按患者划分数据再在训练集内部切窗、计算统计量全部处理完再对测试集做同样的变换处理。虽然代码上稍微复杂但结果要可靠得多用来发论文也不会被审稿人抓数据泄漏的把柄。7. 关于数据增强和波形可视化的一点实操心得7.1 发作样本过少怎么办CHB-MIT虽然总计包含上百次发作事件但是分散到20多个患者后每个患者的发作次数并不多多的几十次少的只有几次。直接做有监督分类时正负样本数量往往严重不平衡哪怕正负样本按1:1采样训练出来的模型还是容易过拟合发作形态。常用的增强手段有三种加噪声、幅值扰动和时域缩放。加噪声用高斯白噪声叠加模拟采集设备和周围环境干扰幅值扰动对每个样本随机乘以0.8到1.2的系数模拟电极接触阻抗变化导致的增益漂移时域缩放稍微麻烦一点需要保持时间轴和标签对齐不能随意拉伸窗口。实际操作中我发现组合使用噪声和幅值扰动收益最大时域缩放则慎用因为过度的缩放会破坏棘波形态导致模型学到失真特征。如果想要更稳定的样本均衡方法可以优先尝试随机下采样正常段让正常段和发作段数量比维持在5:1到10:1之间。发作段本来就是一个比例极低的事件盲目追求1:1反而会让模型看到太多重复的发作形态对背景脑电特征的泛化能力变差。7.2 用Topomap和波形图辅助判断预处理是否达标很多人做完滤波、参考和坏道处理后直接就把数据丢进模型这其实不够负责。建议每个患者至少保留一张预处理前后的对比图一个通道挑选FP1-F7或其它前头部导联用matplotlib画一段包含发作起止的波形查看滤波后棘波形态是否清晰是否出现大幅脉冲伪迹。MNE里画Topomap能直观看到每个通道的空间分布和异常电压对判断预处理质量很有帮助。比如预处理后某通道的Topomap看起来颜色极度饱和说明这个通道可能仍有坏的残余信号需要回到坏道检测阶段重新标记。我每次处理完一批文件后都会随机抽查3到5个文件做目检这个习惯帮我发现过很多自动流程无法捕捉的问题。8. 常见错误与排查清单根据我自己跑CHB-MIT一年的经验把新手最容易踩的坑整理成一个速查表容易出问题的地方具体表现排查思路EDF文件头解析读取通道数比实际多/少先打印所有通道名逐个和标准10-20通道对照采样频率设置错误时频图横轴单位不对用raw.info[sfreq]确认勿手动假设256发作时间解析起止时间完全对不上波形检查summary中单位是否为秒注意文件起始时间不是0点坏道未处理模型训练不收敛或性能极差画出通道方差和高频功率自动加人工确认滤波器相位失真发作起始边界偏移使用filtfilt做零相位滤波勿用causal filter训练测试数据重叠模型看似性能极佳但泛化差按患者严格划分杜绝同源文件出现在两个集合幅值范围不一致上下限差异大导致特征失效用训练集统计做z-score勿用测试集统计文件解压损坏read失败或通道数异常通过文件哈希校验是否完整下载这套清单是通用的排查时从最底层的数据文件格式开始一层层往上走往往能找到根本原因。EEG数据预处理没有一步到位的神器多花时间做质量审查比多调几个模型参数重要得多。9. CHB-MIT处理完成后的下一步规划这篇作为系列第一篇主要解决的是“拿到数据后怎么正确读出来并看懂”的问题。到了这一步你已经能完成从EDF文件到带标注发作区间样本矩阵的转换这是整个CHB-MIT分析链路中最基础也最关键的一步。后续内容我会陆续展开发作检测模型的基线搭建、短时傅里叶变换与小波变换的特征对比、深度学习模型如EEGNet、TSception在CHB-MIT上的表现复现以及如何利用该数据集做患者特异性迁移学习。CHB-MIT的好处在于它的标注规范性和采样一致性便于研究者专注于算法本身而不必花大量时间清洗数据。但它也存在一些局限比如受试者都是儿童、发作类型和脑区覆盖面有限、记录时间并非全天候这些局限会在你后续做通用模型评估时暴露出来需要结合其它数据集比如TUH EEG Seizure Corpus做交叉验证。TUH数据集规模更大、来源更杂、包含更多不同的记录环境是CHB-MIT之外非常值得关注的补充资源我后面也会专门讲它的下载格式和标注体系。不过在深入研究模型之前先把数据基础打牢保证每一步处理都经得起复查。信号质量决定了特征上限标注对齐决定了标签可靠性这两点做到位之后模型实验才有真正的意义。下一篇我会继续沿着数据管道往下走写清楚从原始信号到训练样本的具体转化细节。