信息速览
三十年来的心电数据库几乎都在回答同一个问题:这段波形说明心脏出了什么事。室性早搏、房颤、束支阻滞、ST 段抬高,标注体系围绕病理展开,人群围绕患者展开。ECG-ID 把问题掉了个头——它不问心脏怎么了,只问这颗心脏是谁的。同一批 90 名志愿者在几个月里被反复请回来,每次只录 20 秒的 I 导联,最短的一天之内录完,最长的前后跨了半年。身份标签因此不是从病历上抄下来的,而是从采集记录里长出来的。
它的规模在任何意义上都很小:310 条记录、单导联、每条 20 秒,全库解压后只有十几兆。但它在结构上有一样别处很难替代的东西——同一个人、跨时段、重复出现。心电身份识别研究最怕的不是样本少,而是分不清一次成绩来自心脏的独特性,还是来自那天那副电极的独特性。ECG-ID 把时间这个变量显式留在了数据里,让「几个月之后还能不能认出同一个人」成为一个可以用它自己检验的问题。
代价同样清楚:单一机构、自愿参加的同学同事、没有临床诊断、注释只有自动检测的 R 波与 T 波峰。它是一把量「可重复性」的尺子,不是一台能诊断心脏的机器。一屏速览见下表。
| 维度 | 内容 |
|---|---|
| 全名 | ECG-ID Database |
| 发布方 | PhysioNet(原始数据由 Tatiana Lugovaya 在 LETI 硕士论文中采集并贡献) |
| 首次上线 | 2014 年 3 月 6 日,版本 1.0.0 |
| 记录数 | 310 条 |
| 受试者 | 90 名(44 男、46 女,13–75 岁) |
| 每人条数 | 官方写 2–20,清单实际 1–22 |
| 单条时长 | 20 秒 |
| 导联 | 仅 I 导联(原始 + 滤波两路) |
| 采样率与精度 | 500 Hz,12 位,名义 ±10 mV 量程 |
| 每记录采样点 | 10000 |
| 每人最长跨度 | 半年(官方);清单内最大 156 天 |
| 每记录注释 | 自动检测的 R 波峰与 T 波峰,未经审核 |
| 文件格式 | WFDB:.hea / .dat / .atr,按 Person_NN/rec_N 两级目录 |
| 体积 | 解压后 12.5 MB,压缩包 12.6 MB |
| 许可 | ODC-BY 1.0,免注册 |
| 典型用途 | 封闭集身份识别、跨时段可重复性评测、轻量认证模型基准 |
§1 它要解决的具体麻烦
1.1 身份识别要的不是更多心跳,而是同一个人更多次的心跳
把心电当生物特征,逻辑起点很朴素:心脏的解剖位置、大小、激动传导路径在个体之间差异很大,同一个人的心电图在形态上相对稳定。指纹可以按一次按压比对,人脸可以按一张照片比对,心电图却必须在时间上反复出现才能谈稳定。一个人今天录一次、半年后再录一次,两次波形之间的那点差别,正好就是识别系统要用到的全部信息所能容忍的上限。
这意味着心电身份识别的研究数据必须满足一个特别的结构要求:每个人要有多次采集,而且这些采集之间要有真实的时间间隔。只有多次采集才能估计「类内差异」,只有时间间隔才能让类内差异含有真实的变化——电极贴歪了一点、体重变了、那天爬了楼梯心率偏快。缺少这个结构的库,只能做「同一天不同分钟」的识别,得到的类内差异被人为压小,模型换到真实场景就会崩。
绝大多数公开心电库的结构恰好是反的。临床库按患者收录,一个人通常只有一次记录,即便有多次也是同一次住院期间的连续监测;挑战赛库按记录切分,同一个人被切成了若干片段,时间跨度几乎为零。在这种数据上训练出来的识别模型,本质上是在识别「这段信号长得像不像训练集里的那几百段」,而不是「这个人是不是同一个人」。
1.2 三条常见的错误期待
第一条,期待它是一份能训练心电诊断模型的数据。 它没有诊断标签。头文件里只有三行注释:年龄、性别、采集日期。没有病史、没有用药、没有超声或造影结果,连「这份心电图判读正常」这样的定性结论都没有。拿它做心律失常分类,等于把一个身份标签当病理标签用。
第二条,期待它是一份大规模的数据。 310 条、90 人是它的全部。深度学习模型几千个参数就够把 90 个类别的训练集背下来,训练集准确率会迅速到顶,而这种到顶毫无意义。它是用来检验方法的尺子,不是用来喂养模型的粮食(§1.6)。
第三条,期待它是一份干净的、可以类比指纹库的高质量生物特征数据。 作者在论文里写得很直白:原始信号噪声偏大,同时含高频与低频噪声成分;采集时关闭了心电图机的全部滤波,因为担心滤波器会抹掉对识别有用的细节;心率与身心状态都不加限制。所以它刻意保留了真实使用场景里的脏,代价是每一条干净的记录背后都有一段需要先处理才能用的信号。
1.3 它能给到的与给不到的
| 你的需求 | 它给的 | 够不够 |
|---|---|---|
| 封闭集身份识别(90 选 1) | 90 个类别、310 条记录、身份标签完整 | 够,这是它的主业 |
| 同一个人跨月复现能力评测 | 同一人多条、最长间隔半年 | 够,是它最不可替代的一格 |
| 开放集验证(陌生人拒识) | 90 人全在名单内,没有官方冒充者集合 | 不够,需自建冒充者集合 |
| 心搏类型或节律分类 | 只有 R 波与 T 波峰位置 | 不够,无类型无节律 |
| 多导联融合方法 | 只有 I 导联 | 不够,结构上做不到 |
| 情绪、体位、运动对识别的影响 | 采集状态不记录,只有日期 | 不够,只能看时间效应 |
| 与指纹、人脸做多模态融合 | 无对口样本,无同一受试者的其它模态 | 不够,需另行配对 |
| 轻量嵌入式认证原型 | 单导联、20 秒、体量小,可离线跑通 | 够,是很好的起点 |
1.4 你的处境与它能帮到的事
| 你的处境 | 它能帮你什么 | 下一步该做什么 |
|---|---|---|
| 你在做可穿戴设备上的心电解锁,担心模型只是在同一天的数据上好看 | 提供同一受试者跨数周到数月的重复采集,能量出「隔一段时间后准确率掉多少」 | 按受试者目录做分组划分,先跑「日内」与「跨月」两套评测再比较 |
| 你手上只有临床库,不知道身份识别任务的难度曲线长什么样 | 提供一个规模可控、类别数恰好 90 的参照系,便于快速调方法 | 用它跑通流程,再把同一套流程搬到你的临床数据上做迁移实验 |
| 你申报时需要说明为什么必须按人划分数据集 | 同一人存在 2 至 22 条记录,误划分的后果可以用它直接演示 | 同一份数据跑「按记录随机切」与「按人切」两组,记录掉分幅度 |
| 你在教生物特征识别的课,需要一个能讲清类内差异的样本 | 同一个人不同日期的波形并排放在同一个目录里,天然适合做对比图 | 取 §4.2 的案例受试者,画三条记录的叠加图 |
| 你想验证某个特征提取方法对小样本是否过拟合 | 类别少、样本少,过拟合会很快暴露 | 固定受试者划分与随机种子,只换特征提取器做纵向比较 |
| 你在做多模态身份认证,缺一个心电分支的公开参照 | 单导联、体量极小,适合做分支预实验 | 注意它没有与其它模态配对的样本,只能验证分支本身 |
1.5 一句话判断
它不适合当训练集,而适合当一把「时间尺」:把人分成不同日期再放回模型面前,量出来的掉分幅度,才是一个心电身份系统真实的泛化能力。
1.6 不适合谁:四条边界
不适合拿来做规模训练。 90 个类别、310 条样本,任何稍有容量的网络都会在几轮之内记住全部训练集。想靠加大数据量提升准确率的路线,在它身上无从展开。
不适合拿来做诊断或筛查。 没有诊断标签,没有临床背景,受试者是志愿者而不是患者。即便某条记录在形态上看着像某种异常,也没有任何官方标注可以支撑这个判断。
不适合拿来做多导联研究。 全部记录只有 I 导联。这一点是设计选择而非采集缺失:作者在论文里说明,选 I 导联是因为它容易测量,且对电极位置的微小变动不敏感。对身份识别是优点,对多导联研究就是一堵墙。
不适合拿来做开放集拒识的正式基准。 90 个人都在名单内,没有官方指定的冒充者集合。想做开放集评测,得自己另找一批人当陌生人,而这批人的选择会直接影响结论的可比性(§7.3)。
§2 数据构成与规格
2.1 两级目录,每级只表示一种信息
数据库的组织方式极其朴素,也极其克制:根目录下是 90 个 Person_NN 目录,编号从 01 到 90,前导零补齐为两位;每个目录内是若干 rec_N 文件组,序号从 1 开始。目录名承载的是身份,文件名承载的是采集次序,没有任何其它语义。
这种结构的价值在于,分组键是路径本身而不是某个字段。做分组交叉验证时,只要从路径里切出 Person_NN 就能拿到分组标签,不需要另外读头文件、不需要维护一张映射表。这是小库里少见的整洁,代价是它无法表达比「人」更细的层级——比如「同一天同一场会话」这一层就无处存放,只能靠日期去猜。
| 层级 | 命名 | 取值 | 用途 |
|---|---|---|---|
| 根 | 无 | 无 | 集中放清单与说明文件 |
| 一级 | Person_NN |
Person_01 至 Person_90 |
身份标签,分组键 |
| 二级 | rec_N |
rec_1 起递增 |
采集次序,同一人内可用于时间切分 |
| 同名前缀三件套 | .hea / .dat / .atr |
同名 | WFDB 记录的三部分 |
根目录下另有四个说明性文件与一个校验清单。RECORDS 列出全部 310 条记录的全路径;README 是纯文本的数据库简介;ANNOTATORS 一行,写明注释文件的角色;biometric.shtml 是作者硕士论文的完整摘要页,篇幅远超一般的库说明;SHA256SUMS.txt 是 2019 年补充的逐文件校验和。还有一个隐藏目录 .old,里面是 2012 年的旧版论文页,不参与数据。
2.2 字段/记录的完整字典
这个库的「字段」分三处:文件路径、头文件正文、头文件末尾的注释行。注释文件里只有位置与符号,没有字段。下表把它们逐项列出。
| 信息项 | 所在位置 | 取值形态 | 含义 | 使用注意 |
|---|---|---|---|---|
| 身份编号 | 路径一级目录 | Person_01–Person_90 |
受试者编号 | 分组键,不可当特征 |
| 记录序号 | 路径二级文件名 | rec_1 起的整数 |
同一人内的第几次采集 | 序号越大通常日期越晚,但官方未承诺单调 |
| 记录名 | .hea 第 1 行首字段 |
rec_1 等 |
记录标识 | 与文件名一致,不含人名目录 |
| 信号条数 | .hea 第 1 行第 2 字段 |
2 |
两路信号 | 不是两个导联,是同一导联的两种处理 |
| 采样率 | .hea 第 1 行第 3 字段 |
500 |
每秒采样点数 | Hz |
| 采样点数 | .hea 第 1 行第 4 字段 |
10000 |
总长度 | 10000 ÷ 500 = 20 秒 |
| 存储格式 | .hea 信号行第 2 字段 |
16 |
16 位整数存储 | .dat 为小端整数流 |
| 增益 | .hea 信号行第 3 字段 |
200 |
ADC 单位/毫伏 | 全库一致,换算直接除 200 |
| 位分辨率 | .hea 信号行第 4 字段 |
12 |
有效位数 | 12 位落在 16 位容器里 |
| ADC 零值 | .hea 信号行第 5 字段 |
0 |
零电平对应整数 | 换算时无需偏移 |
| 初始值 | .hea 信号行第 6 字段 |
逐记录不同 | 首采样点整数值 | 用于核验读取是否正确 |
| 校验和 | .hea 信号行第 7 字段 |
逐记录不同 | 采样点累加和 | 可用 SHA256SUMS.txt 交叉核对 |
| 信号名 | .hea 信号行第 9 字段 |
ECG I / ECG I filtered |
哪一路 | 第 1 路原始,第 2 路滤波 |
| 物理单位 | 由增益推得 | mV |
毫伏 | wfdb 读取后自动换算 |
| 年龄 | .hea 注释行 |
整数 | 采集时年龄 | # Age: 25 形式 |
| 性别 | .hea 注释行 |
male / female |
采集时性别 | # Sex: male 形式 |
| 采集日期 | .hea 注释行 |
DD.MM.YYYY |
当日日期 | # ECG date: 07.12.2004 形式 |
| R 波峰位置 | .atr |
采样点序号 | 检测到的 R 峰 | 符号 N |
| T 波峰位置 | .atr |
采样点序号 | 检测到的 T 峰 | 符号 t(小写) |
| 是否有诊断 | 全库 | 无 | — | 任何地方都没有诊断字段 |
三条注释行的格式在整个库里高度一致:全部 310 个头文件都恰好含这三行,没有缺失,顺序也固定为年龄、性别、日期。这一点对批量解析很友好,用一条正则就能一次抽干。日期的写法是俄式日在前,07.12.2004 表示 2004 年 12 月 7 日而不是 7 月 12 日,用美式解析会静默错位五个月。
表里最容易被误读的一行是「信号条数 = 2」。做过双导联库的人看到 2 会本能地以为这是两个导联,写代码时按导联做融合,结果两路其实是同一段时间、同一个电极的两种处理。第二路是滤波后的版本,与第一路高度相关但不完全相同;把它们当两个独立观测塞进多通道网络,模型学到的只是滤波器的响应,不是任何生理信息。
2.3 一条记录的完整解剖
以 Person_01/rec_1 为例。头文件只有五行,每行都是可解释的:
rec_1 2 500 10000
rec_1.dat 16 200 12 0 -17 17532 0 ECG I
rec_1.dat 16 200 12 0 -23 2004 0 ECG I filtered
#-- Age: 25(头文件原文以井号开头)
#-- Sex: male
#-- ECG date: 07.12.2004
第一行说明:记录名 rec_1,2 路信号,500 Hz,共 10000 个采样点。第二、三行分别描述两路信号:都存于 rec_1.dat,都是 16 位存储,增益都是 200,位分辨率都是 12,ADC 零值都是 0,初始值分别是 −17 与 −23,校验和分别是 17532 与 2004。最后三行是受试者上下文:25 岁男性,2004 年 12 月 7 日采集。
.atr 里存的是 R 波与 T 波峰。对 rec_1 这条记录,读出来是 20 个事件,交替出现:
352 N 469 t 727 N 839 t 1135 N 1246 t
1599 N 1715 t 2067 N 2183 t 2525 N 2638 t
2992 N 3109 t 3436 N 3556 t 3870 N 3980 t
4293 N 4407 t
20 秒内 10 次 R 峰,对应心率约每分钟 30 次——这个数字明显偏低,提示这 20 个事件并不是对整段 20 秒的完整检测,而只是检测器在某个区间里给出的十个心搏。官方对这段标注的描述是「每条记录标注 10 个心搏」,与这里看到的一致:注释不是全覆盖的逐搏标注,而是固定的十对峰。这一点必须记牢,任何想用它算心率变异性或节律统计的想法,都会在第一步就撞上样本量上限。
读取的标准写法:
import wfdb
import numpy as np
base = "https://physionet.org/files/ecgiddb/1.0.0"
rec = wfdb.rdrecord(f"{base}/Person_01/rec_1", physical=True)
ann = wfdb.rdann(f"{base}/Person_01/rec_1", extension="atr")
print(rec.fs, rec.sig_len, rec.sig_name)
#-- 500 10000 ['ECG I', 'ECG I filtered']
print(rec.p_signal.shape)
#-- (10000, 2) 已完成增益换算,单位 mV
print(rec.comments)
#-- ['Age: 25', 'Sex: male', 'ECG date: 07.12.2004']
print(len(ann.sample), set(ann.symbol))
#-- 20 {'N', 't'}
这里用完整 URL 而不是 pn_dir 参数,是因为本库的目录层级有两级,pn_dir 的拼装对嵌套路径不友好,直接给 URL 更稳。p_signal 返回的是已换算的毫伏值;d_signal 是原始整数,两边只差一个 200 的除数与零值。
2.4 每位受试者贡献多少条
官方页面与 README 的表述是有出入的。两处都写「每人 2 至 20 条」,但把 RECORDS 里的 310 条按目录归并后,实际分布是这样:
| 每人条数 | 受试者数 | 说明 |
|---|---|---|
| 1 条 | 1 人 | 与官方下限「2」不符 |
| 2 条 | 48 人 | 最常见,多为同一天内完成 |
| 3 条 | 16 人 | |
| 4 条 | 5 人 | |
| 5 条 | 13 人 | |
| 6 条 | 2 人 | |
| 7 条 | 1 人 | |
| 8 条 | 1 人 | |
| 11 条 | 1 人 | |
| 20 条 | 1 人 | 官方上限 |
| 22 条 | 1 人 | 与官方上限「20」不符 |
表里出现的 1 条与 22 条是两个例外。官方没有解释这两处偏差,合理的推测是口径不同:官方的「2 至 20」可能描述的是设计意图或论文实验所用子集,而公开目录保留了全部采集。这个差异不影响总体规模判断,但会影响任何声称「每人至少两条」的划分脚本——遇到那 1 条记录的受试者,双切分脚本会直接报错。
半数以上的受试者只有 2 条记录,而 2 条恰好是「一条注册、一条验证」的最小可行配置。这意味着这个库的设计重心不在「同一个人有多少次」,而在「有多少人至少有两次」,后者的数量是 89 人。
时间跨度方面,读日期字段算得每个人的首末记录间隔从 0 天到 156 天。0 天的有 70 人,也就是说绝大多数人的多次采集发生在同一天;间隔超过 30 天的只有 3 人。官方的「最长六个月」与清单里的 156 天并不矛盾——156 天约等于五个月,官方用的是概数。
| 时间维度 | 数值 | 含义 |
|---|---|---|
| 采集年份 | 2004 年 19 条,2005 年 291 条 | 跨一个自然年 |
| 同日内完成全部采集 | 70 人 | 跨时段效应在这些受试者身上为零 |
| 首末间隔 1–30 天 | 17 人 | 短期重复 |
| 首末间隔超过 30 天 | 3 人 | 真正的跨月样本,官方概称最长半年 |
| 最大间隔 | 156 天 | 约五个月 |
这个分布对使用者的实际影响很具体:如果你想做「隔一周识别率掉多少」的曲线,样本够;想做「隔半年识别率掉多少」,只有个位数的受试者能支撑,统计上很不稳。宣传语里的「半年」是上限而不是典型值,这一点在写论文的方法节时必须说清楚。
2.5 与同类资源的字段差异
| 对比项 | ECG-ID | MIT-BIH 心律失常库 | PTB-XL | GRABMyo(肌电身份) | WISDM(传感身份) |
|---|---|---|---|---|---|
| 模态 | 单导联心电 | 双导联心电 | 十二导联心电 | 前臂表面肌电 | 手机/手表加速度 |
| 受试者 | 90 | 47 | 一万八千余例 | 43 | 51 |
| 每人多次采集 | 是,跨日 | 否(连续监测) | 否(多为一次) | 是,跨三天 | 是,跨多日 |
| 单条时长 | 20 秒 | 30 分钟 | 10 秒 | 数分钟 | 数十秒 |
| 主监督信号 | 身份 | 心搏类型 | 诊断陈述 | 手势加身份 | 活动类别加身份 |
| 是否含诊断 | 否 | 是 | 是 | 否 | 否 |
| 采集状态受控 | 否(心率情绪不限) | 否(Holter) | 部分 | 是(固定动作) | 否(自由活动) |
这张表想说的是:ECG-ID 在「同一个人多次出现」这一列上并不孤单,肌电与智能手机传感库同样具备;它的稀缺性来自这个结构叠加在心电上。心电是唯一一种既能在可穿戴设备上被动采集、又不要求用户做任何动作的生物特征。指纹要求按压,人脸要求看向镜头,肌电手势要求做动作,而心电可以在一副耳机的两片电极之间悄悄完成。这正是它作为生物特征研究样本的价值所在。
§3 它是谁的数据:一次硕士论文的副产品
3.1 作者、机构与出处
数据由 Tatiana S. Lugovaya 在俄罗斯圣彼得堡电工大学 LETI 攻读硕士期间采集,用于她的学位论文《基于心电图的人体生物识别》。LETI 的全称是国家电工大学,是俄罗斯历史最悠久的电工类高校之一,论文所在的院系是计算技术与信息学系。作者在数据说明里留了邮箱,并在页面上明确请求使用者引用原始论文。
与论文配套的还有一篇会议论文,作者为 A. P. Nemirko 与 T. S. Lugovaya,发表在第十二届俄罗斯模式识别数学方法会议上,页码 387–390。这篇会议论文是俄文,页面上给了会议集的 PDF 链接。对只读英文文献的使用者,实际可用的就是页面上那份英文摘要页。
3.2 时间线
| 时间 | 事件 | 意义 |
|---|---|---|
| 2004-12 | 首批记录采集 | 清单里最早的一批日期 |
| 2005-06 | 硕士论文完成 | 数据与方法的正式出处 |
| 2005 | 会议论文发表 | 结果的同行可见版本 |
| 2011-07-19 | 文件在 PhysioNet 归档 | 目录与头文件的当前形态 |
| 2012-04-11 | README 最后修改 |
说明文字的定稿 |
| 2014-03-06 | 以 1.0.0 版正式发布 | 获得 DOI,进入可引用状态 |
| 2019-02-20 | 补充 SHA256SUMS.txt |
完整性校验清单 |
这张表里有一处值得注意的错位:采集完成于 2005 年 6 月,而文件在平台上的归档时间是 2011 年 7 月,正式发布版本号的日期则是 2014 年 3 月。三个时间分别对应「数据产生」「文件落位」「可引用」三个不同的状态。引用时用哪个日期,取决于你要说明的是数据本身还是数据集的发布。
另一个细节是采集年份。清单里最早的日期是 2004 年 12 月,绝大多数记录落在 2005 年。论文写于 2005 年 6 月,说明采集在论文提交前半年内密集完成,随后作者在几个月内完成了分析与写作。
3.3 采集场景:坐姿、夹式电极、不加限制
论文摘要页对采集过程的描述很短,但每一句都影响数据的用法:
| 采集条件 | 官方描述 | 对数据的影响 |
|---|---|---|
| 体位 | 坐姿 | 比卧位更接近可穿戴使用场景,但会引入呼吸与体位相关的基线起伏 |
| 电极 | 肢体夹式电极 | 接触方式接近实际认证系统,但比干电极干净 |
| 导联 | I 导联(左手—右手电位差) | 对电极位置微小变动不敏感,便于日常佩戴 |
| 心率 | 不加限制 | 类内差异里混入了心率变化,识别难度更真实 |
| 身心状态 | 不加限制 | 未记录,无法作为协变量剥离 |
| 滤波 | 关闭心电图机全部滤波器 | 原始信号保留全部噪声,滤波需自行完成 |
| 采集时长 | 每条 20 秒 | 时间分辨率以秒计,适合轻量模型 |
「不加限制」这一条是整份数据的性格所在。多数生物特征数据集会把采集条件控制到极致,以求类内差异最小、类间差异最大,从而得到漂亮的识别率。这个库反其道而行,作者在论文里给出的理由是:实际使用中无法控制用户的心率与情绪状态,如果数据在实验室条件下被控得干干净净,识别系统上线后的表现就无从预测。这个立场决定了它的下限比同类数据低,上限却更接近真实。
关掉滤波器这一条同样是有意为之。作者的解释是:无法确定心电图机的内部滤波是否会削掉对身份识别有用的特征。为不失真,干脆保留原样,把滤波变成使用者的责任。每条记录同时提供原始与滤波两路,正是这个立场的直接产物——作者自己做了一份滤波,但不确定这份滤波是不是最优,于是两份都留下。
3.4 受试者是谁
90 名志愿者,44 男 46 女,年龄 13 至 75 岁,身份是作者的同学、同事与朋友。这个人口构成需要如实描述,因为它决定了结论能推广到哪里。
| 维度 | 官方与清单所见 | 对泛化的含义 |
|---|---|---|
| 性别 | 44 男、46 女,接近均衡 | 性别因素不会系统性偏斜 |
| 年龄 | 13–75 岁,29 个不同取值 | 跨青少年到老年,但每个年龄段人数很少 |
| 与作者关系 | 同学、同事、朋友 | 招募便利,样本同质性强 |
| 地域 | 圣彼得堡一地 | 地域单一,无法做跨地区验证 |
| 健康状况 | 未说明,未设筛选 | 可能含少量异常心电图,但无标注可查 |
| 民族构成 | 未说明 | 官方未公布 |
| 采集设备台数 | 未说明 | 无法判断设备是否构成混杂 |
最后一行值得强调。身份识别研究里有一个常被忽略的混杂:如果不同受试者用的是不同的设备,模型可能学会分辨设备而不是分辨人。这个库没有公布设备的数量与型号,无法排除或确认这一混杂。使用者能做的是把它记为一个已知的未澄清项,而不是假定它不存在。
§4 它最独特的东西:同一个人,不同日期
4.1 为什么「同人多条」比「人多」更稀缺
公开数据集的规模统计几乎都以人为单位,90 人听上去微不足道。但对身份识别这个特定任务,真正决定实验设计空间的是另一个量:每个类别的样本数,以及这些样本之间的时间分布。
只有一次记录的人,无法参与任何类内评测——系统看到一个陌生人,只能回答「不认识」。有两次记录的人,才能构成一次「注册 + 验证」。有多次且跨日期的人,才能构成「注册于某个日期、验证于另一个日期」的时间泛化实验。这三件事对数据的要求依次递进,而绝大多数临床心电库停留在第一层。
ECG-ID 的第二层覆盖得相当充分:89 人有至少两条记录,可以支撑完整的封闭集评测。第三层只覆盖到少数人:绝大多数是在同一天内重复采集,跨月的只有个位数。所以它真正的稀缺性可以精确表述为:每个人都能被注册一次并被认出,但只有少数人能检验「过几个月还认得出来吗」。
这个精确表述比笼统地说「跨时段」有用得多。它直接告诉使用者该把实验设计成什么样:主实验用全库做封闭集识别,按人划分;补充实验挑出那三个跨月受试者,做小样本的时间泛化观察,并在论文里明确标注样本量极小、结论仅供定性参考。
4.2 案例:Person_07 的三条记录
选 Person_07 作贯穿案例,原因是它正好落在最常见的形态里:三条记录,rec_1、rec_2、rec_3,足以展示类内差异的长度尺度,又不会因为条数太多而让图表拥挤。三条记录的采样长度、增益、单位完全一致,读进来就是三行形状相同的数组。
import wfdb
import numpy as np
base = "https://physionet.org/files/ecgiddb/1.0.0"
rows = []
for i in (1, 2, 3):
r = wfdb.rdrecord(f"{base}/Person_07/rec_{i}", physical=True)
x = r.p_signal[:, 0]
rows.append({
"rec": i,
"date": [c for c in r.comments if c.startswith("ECG date")][0],
"p2p": round(float(np.ptp(x)), 4),
"std": round(float(np.std(x)), 4),
})
for row in rows:
print(row)
三条记录的峰峰值与标准差会给出一个直观的量级:同一颗心脏在不同日期,幅度可以在一个不小的范围内浮动,而整体形态保持可辨。这个对比正是 §4.2 的案例要被反复引用的原因——它把「类内差异」从一个抽象概念变成一组可以直接打印的数字。
值得注意的是,Person_07 的 rec_1 与 rec_2、rec_3 未必在同一天,序号也不是官方承诺的时间顺序。所以任何依赖「序号越大越晚」的脚本都必须先读日期字段验证,不能凭文件名排序。
4.3 类内差异的三个来源
同一人的不同记录之间,差异来自三个层次,每一层对方法的要求都不一样:
| 差异来源 | 表现 | 能否从数据里分离 |
|---|---|---|
| 生理漂移 | 心率、呼吸深度、自主神经张力随日变化 | 部分可分离,靠心率与形态联合建模 |
| 电极与接触 | 夹子夹合位置、皮肤阻抗、接触压力 | 不可分离,数据里没有接触信息 |
| 设备与滤波 | 心电图机的前端响应、工频抑制 | 不可分离,设备信息未公布 |
第一层是生物学上最有意义的,也是研究者最希望模型学到的东西——一个人心脏的形态指纹应该比心率的短期波动更稳定。第二、三层是噪声,且在真实使用中会显著放大:可穿戴设备每次佩戴的电极位置都不同,皮肤状态也随天气与运动变化。
公开文献里对这个库的用法,大多把三层混在一起看,报告一个整体的跨记录识别率。这在方法学上不是错误,但对结论的解释力有限。更细的做法是按这三个来源分层设计:用同一天内的记录对衡量「接触噪声」的量级,用跨日记录对衡量「生理漂移」的量级,两者相减,才能估出纯粹由电极与设备引入的部分。§4.2 的案例给出的三条记录,正好可以作为这套分层分析的最小样本。
4.4 它支撑的三类实验
把这个结构摊开,能开展的实验可以归成三类,难度与结论强度递增:
| 实验类型 | 设计 | 它能给的 | 结论强度 |
|---|---|---|---|
| 同日重复识别 | 同一人的两条同日记录互认 | 89 人的完整覆盖 | 中,类内差异被压小 |
| 跨日重复识别 | 同一人的不同日期记录互认 | 跨月的只有 3 人,其余 17 人在一个月内 | 高,但样本少 |
| 时间衰减曲线 | 按首末间隔分组画识别率 | 间隔从 0 到 156 天 | 弱,各组样本严重不均 |
第三类最容易出问题。把 90 个受试者按间隔分组,0 天组有 70 人,超过 30 天的只有 3 人,两组的置信区间完全不对称。在这个基础上画一条「识别率随间隔下降」的曲线,形状主要反映的是两组的样本量差异,而不是时间效应本身。§4.2 的案例在这里再次派上用场:与其画曲线,不如就把三条记录的形态并排列出来,让差异自己说话。
4.5 边界条件:什么时候它不再适用
| 条件 | 是否适用 | 判断依据 |
|---|---|---|
| 你要做封闭集身份识别 | 适用 | 90 类全覆盖,身份标签来自路径 |
| 你要做开放集拒识 | 有条件适用 | 需自建冒充者集合,官方不提供 |
| 你要研究时间衰减 | 有条件适用 | 跨月样本仅数人,只能定性 |
| 你要做多导联融合 | 不适用 | 只有 I 导联 |
| 你要做情绪或运动影响研究 | 不适用 | 采集状态未记录 |
| 你要做心律失常诊断 | 不适用 | 无诊断标签 |
| 你要做人口学偏差研究 | 受限适用 | 年龄跨度大但每段人数少 |
| 你要做跨机构泛化 | 不适用 | 单一机构,无第二来源 |
其中第二行与第三行最需要写进方法节。开放集拒识的冒充者集合,通常从训练集外另取一批人,这个库没有提供任何官方约定,不同论文用不同冒充者集合,结论之间不可比。第三行的局限则影响论文里「泛化」一词的使用——在这里,「泛化」指的是在时间维度上的外推,而不是在人群或机构上的外推。
§5 怎么拿到:从目录遍历到可训练张量
5.1 目录与体积
| 路径 | 内容 | 量级 |
|---|---|---|
/files/ecgiddb/1.0.0/RECORDS |
310 行全路径 | 约 5 KB |
/files/ecgiddb/1.0.0/README |
数据库简介 | 约 1.2 KB |
/files/ecgiddb/1.0.0/ANNOTATORS |
注释角色说明 | 一行 |
/files/ecgiddb/1.0.0/biometric.shtml |
论文摘要页 | 约 36 KB |
/files/ecgiddb/1.0.0/SHA256SUMS.txt |
逐文件校验和 | 约 84 KB |
Person_NN/rec_N.hea |
头文件 | 约 200 字节 |
Person_NN/rec_N.dat |
两路信号 | 每条 40000 字节 |
Person_NN/rec_N.atr |
注释 | 数十到一百余字节 |
解压后 12.5 MB,压缩包 12.6 MB,页面上给了三种取法:网页直接下载 ZIP、wget 递归、AWS S3 同步。这个体量意味着整库可以放在内存里做实验,不需要分片读取,也不需要搭建数据服务器。对小规模方法学实验,这是一项实打实的便利。
5.2 批量获取
#-- 只拉记录清单,先看清结构
curl -sL https://physionet.org/files/ecgiddb/1.0.0/RECORDS -o RECORDS
wc -l RECORDS
#-- 按清单逐条下载三件套,保留两级目录
mkdir -p data
while IFS= read -r rel; do
mkdir -p "data/$(dirname "$rel")"
for ext in hea dat atr; do
curl -sL --max-time 30 \
"https://physionet.org/files/ecgiddb/1.0.0/${rel}.${ext}" \
-o "data/${rel}.${ext}"
done
done < RECORDS
#-- 核对:应有 310 个 dat,每个恰好 40000 字节
find data -name '*.dat' | wc -l
find data -name '*.dat' -size 40000c | wc -l
RECORDS 里的路径带目录前缀,形如 Person_01/rec_1,所以下载脚本要先建目录再落文件。第二段核对里 wc -l 会给出 310,因为这个文件末行有换行符——与某些库的清单末行缺换行不同,这里不用担心少算一条。
SHA256SUMS.txt 可以拿来做第二道完整性核对。它的路径写成相对形式,sha256sum -c 在数据根目录执行即可。
5.3 建立身份索引
绝大多数实验的第一步不是读信号,而是建一张「记录 → 人」的索引表。这张表决定了后面所有划分的正确性。
import os
import re
import pandas as pd
ROOT = "data"
rows = []
for person in sorted(os.listdir(ROOT)):
pdir = os.path.join(ROOT, person)
if not os.path.isdir(pdir):
continue
for fn in sorted(os.listdir(pdir)):
if not fn.endswith(".hea"):
continue
rel = os.path.join(person, fn[:-4])
text = open(os.path.join(pdir, fn), encoding="utf-8").read()
meta = dict(re.findall(r"^# (Age|Sex|ECG date): (.+)$", text, re.M))
rows.append({
"person": person,
"rec": fn[:-4],
"path": rel,
"age": int(meta["Age"]),
"sex": meta["Sex"].strip(),
"date": pd.to_datetime(meta["ECG date"].strip(),
format="%d.%m.%Y"),
})
idx = pd.DataFrame(rows)
print(idx.shape, idx["person"].nunique())
print(idx.groupby("person")["date"].agg(["min", "max", "count"]).head())
这里用 format="%d.%m.%Y" 显式指定日在前。若交给 pandas 自动推断,07.12.2004 有相当概率被读成 7 月 12 日,五个月的错位会直接流进跨度计算,而且不报错。
索引建好后,跨度可以这样算:
span = (idx.groupby("person")["date"]
.agg(lambda s: (s.max() - s.min()).days)
.sort_values())
print(span.describe())
print(span.tail(5))
这会复现 §2.4 的结论:绝大多数受试者的跨度为 0 天,最大值在 150 天量级。
5.4 切窗与张量
单条记录 20 秒、10000 点,对轻量模型已经够用,不必切窗;对需要固定输入长度的网络,按 2 秒或 4 秒切是常见做法。切窗时最容易丢掉的是分组信息,所以把 person 一并带下去:
import numpy as np
import wfdb
def load_segments(rel, win=1000, step=500):
r = wfdb.rdrecord(f"{ROOT}/{rel}", physical=True)
x = r.p_signal[:, 0].astype("float32") #-- 只用原始信号这一路
segs = []
for s in range(0, len(x) - win + 1, step):
segs.append(x[s:s + win])
return np.stack(segs), r.fs
segs, fs = load_segments("Person_07/rec_1")
print(segs.shape, fs)
#-- 每条 20 秒记录,2 秒窗、1 秒步长,得到 19 段
只取 p_signal[:, 0] 是有意的:第 1 路是原始信号,第 2 路是作者提供的滤波版本。如果两路都喂进网络,模型会花容量去学两路之间的滤波差值,而这部分信息与身份无关。想用滤波版本,就整库统一用第 2 路,不要混着来。
5.5 按人划分的封闭集协议
这是本库最容易做错的一步,所以单独写出来:
import numpy as np
from sklearn.model_selection import StratifiedGroupKFold
y = idx["person"].to_numpy()
groups = idx["person"].to_numpy()
sgkf = StratifiedGroupKFold(n_splits=5, shuffle=True, random_state=0)
for fold, (tr, te) in enumerate(sgkf.split(idx, y, groups)):
assert set(groups[tr]).isdisjoint(set(groups[te]))
print(fold, "训练", len(tr), "测试", len(te))
封闭集识别里,训练集与测试集的类别必须是同一批人——这正是「封闭」的含义。所以「按人划分」不是把不同的人分到两边,而是把同一个人的不同记录分到两边。上面的 assert 检查的正是这一点:两边的 person 集合相同,但记录索引不相交。
如果误把 groups 设成 rec,就退化成按记录随机切分,测试集的每条记录都能在训练集里找到本人的兄弟记录。这是本库最典型的泄漏方式(§7.3),也是为什么 §4.2 的案例值得单独拿出来看:同一个人三条记录放在一起时,形态相似度有多高,泄漏的幅度就有多大。
5.6 六个最容易出错的地方
| 位置 | 错误写法 | 后果 | 正确做法 |
|---|---|---|---|
| 信号数理解 | 当 2 导联做多通道融合 | 学到的是滤波器响应 | 明确两路是同一导联的原始与滤波 |
| 注释理解 | 当逐搏全覆盖标注 | 心率、HRV 统计严重偏低 | 记住每条只有 10 对峰的固定标注 |
| 日期解析 | 交给库自动推断 | 日月互换,跨度算错五个月 | 显式用 %d.%m.%Y |
| 数据划分 | 按记录随机切分 | 同一人出现在两侧,准确率虚高 | 按 Person_NN 分组 |
| 人数假设 | 假定每人至少 2 条 | 遇 1 条记录的受试者脚本崩 | 划分前先按人计数并过滤 |
| 幅度假设 | 假定所有记录同一量程 | 增益实际全库一致,但初始值不同 | 用 p_signal,不直接读整数 |
最后一行需要补一句:本库的增益确实全库统一为 200,与那些逐记录增益不同的库不同。但这不意味着可以放心直接用整数——不同记录之间的绝对基线仍有差异,仍应先减去各自的均值再比较形态。
§6 与库内条目的关系
6.1 关系图谱
在心电类资源里,ECG-ID 的位置不在「规模」轴上,而在「任务」轴上。把库内相关的记录按任务摆开:
| 记录 | 任务 | 与 ECG-ID 的关系 |
|---|---|---|
| MIT-BIH 心律失常库 | 心搏分类 | 同格式,不同任务;它回答「是什么心律」,本库回答「是谁」 |
| INCART 十二导联库 | 心搏与节律 | 导联数相反;它宽,本库窄 |
| MIT-BIH 房颤库 | 房颤检测 | 无交集,仅共享 WFDB 读写工具 |
| PTB-XL | 诊断分类 | 人群相反;它是患者,本库是志愿者 |
| CODE-15 与 CODE-II | 大规模诊断 | 规模差两个数量级,不可类比 |
| MIMIC-IV-ECG | 院内诊断 | 同一格式,不同人群与目标 |
| EPHNOGRAM | 运动中的心电 | 互补:它给运动状态,本库给时间跨度 |
| GRABMyo | 肌电手势与身份 | 结构同源:多人跨天重复采集,但模态不同 |
| WISDM | 手机传感与身份 | 结构同源:多人跨天,任务同为身份 |
| OpenECG | 基础模型基准 | 用途相反:它做大模型预训练,本库做小样本验证 |
表里真正有关系的只有两行:与 MIT-BIH 的关系是「同格式不同任务」,与 GRABMyo、WISDM 的关系是「同结构不同模态」。前者提醒不要因为都叫 WFDB 就混用;后者提示了一个可行的研究方向——如果有人想做跨模态的心电身份识别,可以参考这两套肌电与传感身份数据的实验设计,把「跨天重复」这个协议直接移植过来。
6.2 检索路径
| 你要找的东西 | 检索词 | 会落到哪里 |
|---|---|---|
| 心电身份识别的方法基准 | ECG biometric、ECG-ID |
§8 与 §7.4 |
| 同一人跨时段重复采集的设计 | repeat acquisition、intra-subject variability |
§4.1 |
| 生物特征类的其它模态 | GRABMyo、WISDM |
§6.1 表 |
| 心搏与节律任务 | MIT-BIH、INCART |
§6.1 表,注意与本库任务不同 |
| 轻量嵌入式认证 | lightweight authentication、1D-CNN |
§7.4 |
| 许可与引用 | ODC-BY、PhysioNet citation |
§9.3 |
6.3 与其他库的分工判据
选库时最实际的问题是「这个库负责哪一段」。把常见的身份相关任务拆开:
| 任务 | 主粮仓该用什么 | ECG-ID 的位置 | 原因 |
|---|---|---|---|
| 心电身份识别的预训练 | 大规模多导联库 | 只在验证阶段出现 | 310 条不足以预训练 |
| 封闭集识别的方法对比 | 规模相近的库 | 主力之一 | 90 类、路径即标签,干净 |
| 时间泛化的定性观察 | 跨月重复采集的库 | 主力 | 结构稀有 |
| 开放集拒识的正式评测 | 有官方冒充者协议的库 | 不适用 | 无官方冒充者集合 |
| 多导联融合 | 十二导联库 | 不适用 | 只有 I 导联 |
| 临床心电诊断 | 带诊断标签的库 | 不适用 | 无诊断 |
第二行与第三行是它真正的位置。把第一行与第五行交给它,不是它不够好,是它本来就没打算做那件事。
§7 质量评估与局限性
7.0 阅读本节前的一个前置校正
评价这个库时,最容易发生的一次误判是拿它和临床大数据集比规模,然后得出「样本太少,不足为信」的结论。这个比较本身错位:它的目标不是覆盖人群,而是覆盖时间。90 个人已经足以支撑一个封闭集识别实验的设计空间,问题从来不在人数,而在于每个人被观察了多久。
完成这个校正之后,五维评分才读得准。它的规模分低,不是因为采集粗糙,而是因为它选择了另一条路——把有限的人力投在「对同一个人反复采集」上,而不是「对更多人各采一次」。这两条路的成本结构完全不同,评分时必须分开看,否则会把一种设计取舍误读成一种质量缺陷。
7.1 五维评分
按 DAIMS 的常用维度打分,每项 1–5 分,分数锚定在「作为心电身份识别研究数据」这个用途:
| 维度 | 分数 | 理由 |
|---|---|---|
| 数据规模 | 2 | 310 条、90 类,训练不足,验证够用 |
| 标注质量 | 2 | 只有自动检测的 R 与 T 峰,官方明确未审核 |
| 任务适配 | 5 | 同人跨时段的结构在公开心电库里稀有 |
| 文档完整 | 4 | 页面与 README 把结构、人群、噪声说清楚了 |
| 可获取性 | 5 | 免注册、ODC-BY、十几兆,一次下完 |
合计 18 分(满分 25)。分数结构与 INCART 那类库相似:规模与标注拖后腿,适配性与可获取性拉满。任务适配给 5 分的理由是它占着一个别人不愿再占的位置——重新组织一次跨月的重复采集,成本远高于再采一批十秒图,所以这个结构一旦缺失,很难靠新库补上。
标注质量给 2 分而不是 3 分,是因为「未审核」这三个字是官方原话里的明确限定,不是使用者的推测。对身份识别任务,注释本来就用处不大——识别靠的是整段形态,不是逐搏位置。所以这 2 分对使用者的实际影响,比它在字面上看起来要小。
7.2 注释的真实覆盖范围
官方对注释的描述是「每条记录标注 10 个心搏,自动检测的 R 波与 T 波峰,未经审核」。这句话有三层含义,每一层都会改变使用方式:
| 层次 | 官方说法 | 实际含义 | 建议 |
|---|---|---|---|
| 数量 | 10 个心搏 | 每条记录固定 10 对峰,不是全覆盖 | 不做 HRV 或节律统计 |
| 来源 | 自动检测器 | 可能有错检漏检,未人工复核 | 需要精确定位时自行检测 |
| 类型 | R 波与 T 波峰 | 只有两种符号,无心搏类型 | 不做心搏分类 |
对 Person_01/rec_1 的读取结果印证了第一层:20 个事件、10 对峰,散落在 20 秒内的一段区间,而不是贯穿全程。用这 10 个 RR 间隔算出的心率,只代表那一段区间,不能代表整条记录。
这个限制对本库的核心用途几乎没有影响。身份识别模型的输入是整段波形,注释只在做数据质量检查或对齐可视化时才需要。真正受影响的是那些想把本库当「附带心搏标注的心电库」来用的人——他们会发现标注的稀疏程度远低于预期。
7.3 已知的、可枚举的偏差
| 偏差 | 表现 | 影响 |
|---|---|---|
| 招募便利 | 同学、同事、朋友 | 样本同质,社会人口维度单一 |
| 单一机构 | 圣彼得堡一处 | 设备与操作规范单一,无跨中心验证 |
| 单导联 | 只有 I 导联 | 无法评估导联位置效应 |
| 时间集中 | 70 人同日完成 | 跨时段样本稀少,时间效应估计不稳 |
| 状态未记录 | 心率情绪不加限制 | 无法把状态作为协变量剥离 |
| 设备未公布 | 台数与型号未知 | 无法排除设备混杂 |
| 记录数口径不一 | 官方 2–20,清单 1–22 | 依赖下限假设的脚本需加防护 |
| 性别标注是字符串 | male / female |
解析时大小写与拼写要统一 |
这张表里影响最大的是第一行与第二行,而不是常见的「样本量小」。招募便利导致的同质性是隐性的:如果受试者大多是同一所大学的师生,他们的年龄、体型、生活方式都聚集在某几个区间,模型可能学到的是「这群人之间的差异」,而不是「人类之间的差异」。这一点在跨机构测试时会立刻暴露,但这个库本身无法提供跨机构的检验。
第七行是操作层面的坑:如果一个脚本假定每人至少两条记录,遇到只有一条的那位受试者会抛异常;如果假定最多二十条,遇到二十二条的那位会少读两条。写代码时把上下限写成从数据里读,而不是写死进常量。
7.4 外部验证矩阵
本库在近年文献里的用法,可以从公开摘要里看出轮廓。检索词以 ECG-ID、ECG biometric 为主,返回的工作大多集中在三类:
| 验证角色 | 典型做法 | 讲得通的地方 | 风险 |
|---|---|---|---|
| 封闭集识别基准 | 90 类,按人划分,报告准确率 | 类数适中,方法之间可比 | 划分协议不统一时结论不可比 |
| 单心搏认证 | 从 20 秒里切出单个心搏做认证 | 与可穿戴的短时采集相符 | 样本被切碎后泄漏更难察觉 |
| 与肌电或虹膜融合 | 把心电当多模态的一支 | 单导联体量小,易拼接 | 本库无配对模态,融合需另找样本 |
| 轻量模型部署 | 小参数量网络在嵌入式端推理 | 单导联、20 秒,接近真实负载 | 只看准确率,忽略拒识性能 |
表格里最值得警惕的是第一行与第二行。封闭集识别基准的结论高度依赖划分协议:按人划分、按记录划分、按日期划分,三种协议在同一份数据上能给出差出很多的结果。第二行更隐蔽——把 20 秒切成十个单心搏后,来自同一条记录的十段样本之间的相关性极高,如果切分时不把同一条记录整体放进一侧,泄漏会以更细的粒度发生。
第三行提到的融合用法,在文献摘要里能看到心电与虹膜结合的多模态系统。这类工作说明心电作为生物特征分支已被接受,但也提醒一件事:本库没有与任何其它模态配对的样本,任何「心电 + 其它模态」的研究都必须另找配对数据,ECG-ID 只能验证心电这一支。
7.5 它为何能持续:结构比规模更难复制
这个库自 2005 年采完、2014 年定版之后就没有再扩充,却一直在被引用。原因不在于它有多好,而在于它占着一个成本结构上难以复制的位置:让同一个人隔几个月再回来录一次心电。批量的十秒图可以靠一次门诊排班解决,跨月的重复采集却要一个人一个人地约、一次一次地等,而且中途会有人退出。新库愿意付这个成本的不多,所以只要「跨时段可重复性」还是心电身份识别的核心问题,它作为参照物的位置就还在。
§8 生态位与历史
8.1 历史沿革
这个库的诞生路径与多数 PhysioNet 库不同。它不是某个实验室在完成一项课题后顺手公开的副产品,而是一名硕士研究生为了回答一个具体问题而专门设计采集的成果:心电能不能当生物特征。问题的来源是 2000 年代初生物识别技术的扩张——指纹、虹膜、人脸已经在门禁与通关场景落地,研究者开始寻找更难伪造、更难窃取的模态,心电因为「活体才有的信号」这一属性进入了视野。
作者的做法在当时并不算主流。她把重点放在「持久性」这个生物特征的基本要求上,而不是在当时更热门的分类器创新上。持久性要求数据在时间上展开,于是有了跨月重复采集;要求采集不对受试者造成负担,于是选了单导联与肢体夹式电极。这两个选择后来变成了这份数据最难替代的两个特征。
8.2 与后续研究的关系
| 阶段 | 主要用法 | 对数据的要求 |
|---|---|---|
| 2005–2010 | 方法可行性论证 | 少量人、多次采集即可 |
| 2011–2016 | 特征工程与分类器对比 | 固定的划分协议 |
| 2017–2021 | 深度学习与端到端识别 | 更大规模,开始转向新库 |
| 2022 至今 | 轻量模型、可穿戴、多模态融合 | 小体量、真实噪声、可快速迭代 |
本库在第三阶段之后逐渐从「主训练集」退到「验证集」,这不是它的退步,而是领域规模的整体上移。深度学习需要的数据量超出了任何单次硕士论文能采集的范围,所以后来的工作转向自建大规模数据或合成数据,而 ECG-ID 留在原地,承担它最擅长的那件事:在一个熟悉的小规模基准上,检验新方法是否真的比旧方法更懂「人」而不是更懂「数据」。
8.3 生态位:时间尺而不是粮仓
| 角色 | 主力粮仓 | 时间尺 |
|---|---|---|
| 典型用法 | 预训练、大规模训练 | 封闭集验证、跨时段观察、方法对比 |
| 需要的前提 | 数据量大、类别多 | 同一人多次、划分协议固定 |
| 代表库 | CODE 系列、PTB-XL、OpenECG | 本库、GRABMyo、WISDM |
| 失败模式 | 过拟合、标签噪声累积 | 样本太少导致结论不稳 |
它坐在右边这一列。把时间尺当粮仓,会立刻撞上样本量天花板;把粮仓当时间尺,会陷入「自己测自己」——在一个库里训练又在这个库里测试,测出来的只是记忆能力。两者不能互换。
8.4 谁在用它,用来做什么
从公开摘要看,使用者分三类:做生物特征算法的信号处理组(把它当标准小基准)、做可穿戴认证的工程组(把它当真实噪声与真实时间差的来源)、做教学的人员(把它当类内差异的现成教材)。第三类最少,但它恰好用到了这份数据最难被替代的部分——同一人多条记录并排放在一个目录里,不需要任何加工就能讲清「类内差异」。
§9 使用指引
9.1 三种角色的路径
| 角色 | 第一天 | 第一周 | 注意事项 |
|---|---|---|---|
| 算法研究者 | 读页面与 README,跑通 §2.3 的读取 | 建身份索引,做按人划分的基线 | 先定划分协议再比方法 |
| 可穿戴工程师 | 取原始信号这一路做前端预处理 | 量化跨日掉分,做轻量模型部署 | 报告要分同日与跨月两组 |
| 教学人员 | 取 §4.2 的案例受试者画对比图 | 做「同日互认」与「跨日互认」两组演示 | 注释稀疏,图上别标心率 |
9.2 一条最短的可用流程
#-- 1. 取清单与说明
curl -sL https://physionet.org/files/ecgiddb/1.0.0/RECORDS -o RECORDS
curl -sL https://physionet.org/files/ecgiddb/1.0.0/README -o README
#-- 2. 下载全量三件套
mkdir -p data
while IFS= read -r rel; do
mkdir -p "data/$(dirname "$rel")"
for ext in hea dat atr; do
curl -sL "https://physionet.org/files/ecgiddb/1.0.0/${rel}.${ext}" \
-o "data/${rel}.${ext}"
done
done < RECORDS
#-- 3. 核对完整性
find data -name '*.dat' -size 40000c | wc -l
import os
import re
import wfdb
import numpy as np
import pandas as pd
rows = []
for person in sorted(os.listdir("data")):
pdir = os.path.join("data", person)
if not os.path.isdir(pdir):
continue
for fn in sorted(os.listdir(pdir)):
if fn.endswith(".hea"):
rec = wfdb.rdrecord(os.path.join(pdir, fn[:-4]), physical=True)
meta = dict(re.findall(r"^# (Age|Sex|ECG date): (.+)$",
"\n".join(rec.comments), re.M))
rows.append({
"person": person,
"prior": fn[:-4],
"date": pd.to_datetime(meta["ECG date"].strip(),
format="%d.%m.%Y"),
"x": rec.p_signal[:, 0].astype("float32"),
})
df = pd.DataFrame(rows)
print(len(df), df["person"].nunique())
print(df.groupby("person")["date"].agg(["min", "max", "count"]).head())
第二步把整条波形作为一列存进 DataFrame,对 310 条、每条一万点的规模完全可行,内存占用约十几兆。超过这个规模就该换成按需读取,但在这个库上不必。
9.3 引用方式
| 引用对象 | 内容 | 何时需要 |
|---|---|---|
| 数据集本体 | Lugovaya T.S. Biometric human identification based on electrocardiogram. [Master’s thesis] LETI, Saint-Petersburg; June 2005. | 使用数据时必须 |
| 版本与 DOI | ECG-ID Database v1.0.0, PhysioNet, DOI 10.13026/C2J01F | 说明数据版本时必须 |
| 平台综述 | PhysioNet 平台论文(2026,Nature Health) | 按官方页面惯例一并引用 |
| 会议论文 | Nemirko A.P., Lugovaya T.S. 第十二届俄罗斯模式识别数学方法会议,pp. 387–390 | 引用方法时 |
官方页面明确要求引用作者的硕士论文,而不是只引用平台。这一点与多数 PhysioNet 库不同——大部分库的推荐引用是平台上的数据集条目,而这里指向的是原始的学位论文。写论文时把两者都写上最稳妥,并注明使用的是 1.0.0 版。
§10 AI 使用声明
| 项目 | 声明 |
|---|---|
| 文本生成 | 本文由 AI 辅助撰写,结构与事实由编辑把关 |
| 数字来源 | 规模、结构、边界三类数字来自 PhysioNet 官方页面、README 与记录清单 |
| 文件核验 | RECORDS 全 310 条的 .dat 与 .hea 已逐一取回,逐条核对了大小与头文件字段 |
| 注释核验 | 注释格式以 Person_01/rec_1 为例核对,其余记录未逐条展开 |
| 差异披露 | 官方写每人 2–20 条,清单实际 1–22 条,差额构成官方未说明 |
| 论文信息 | 只读了官方页面附带的英文摘要页,未读俄文会议论文全文 |
| 未核验内容 | 性别与年龄对识别率的影响、设备台数、噪声成分的定量分布 |
| 图像 | 无配图;封面为生成式提示词,不含真实心电图 |
| 免责 | 本文不构成临床建议;数据使用须遵守 ODC-BY 1.0 的署名要求 |
常见误读(避坑清单)
| 说法 | 实际 | 证据 |
|---|---|---|
| 「它是双导联心电库」 | 只有 I 导联,两路信号是同一导联的原始与滤波 | 头文件两行信号名 ECG I 与 ECG I filtered |
| 「每条记录有完整的逐搏标注」 | 每条固定标注十对 R 与 T 峰,非全覆盖 | 官方描述与 rec_1.atr 的二十个事件 |
| 「它能训练心律失常模型」 | 无诊断标签,无心搏类型,人群为志愿者 | 头文件只有年龄、性别、日期三行注释 |
| 「每人 2 到 20 条」 | 清单实际 1 到 22 条 | RECORDS 按目录归并后的计数 |
| 「每人随访半年」 | 七成受试者同一天完成全部采集 | 日期字段统计,间隔为 0 天的有 70 人 |
| 「按记录随机划分就行」 | 同一人多条记录,随机划分造成身份泄漏 | 路径一级目录即身份,需按人分组 |
| 「它是 100 Hz 或 250 Hz」 | 500 Hz,12 位精度 | 头文件首行第三个字段 |
| 「注释是人工标注的」 | 自动检测器给出,官方注明未经审核 | 官方描述与 ANNOTATORS |
| 「它来自多家医院」 | 单一机构,作者的同学同事朋友 | 官方人群描述 |
| 「日期 07.12.2004 是 7 月 12 日」 | 俄式写法,是 12 月 7 日 | 全库日期均落在 2004 年 12 月至 2005 年 |
FAQ
Q1:ECG-ID 到底有多少条记录、多少人?
官方页面与 README 都写 310 条、90 人,RECORDS 逐行展开也是 310 条,分布在 Person_01 至 Person_90 共 90 个目录里。按目录归并后每人的条数范围是 1 到 22,与官方写的 2 到 20 有一处出入,官方未解释差额。
Q2:它能不能直接用来训练心律失常分类?
不能。注释只有自动检测的 R 波与 T 波峰,没有心搏类型,也没有节律标签;受试者是志愿者,不是门诊或住院患者。它唯一的监督信号是「这条记录属于谁」。
Q3:为什么按记录随机划分会严重虚高?
因为同一人的多条记录共享心脏形态、电极接触习惯与采集设备,随机切分后测试集里几乎每个身份都能在训练集找到本人。必须按 Person_NN 整体分组,让同一个人的所有记录只出现在一侧。
Q4:它算封闭集识别还是开放集验证?
天然是封闭集:90 个人都在名单内。开放集验证需要另找一批陌生人来当冒充者,官方没有给出约定,不同工作自定的冒充者集合会让结论之间不可比。
Q5:两路信号该怎么用?
第 1 路是原始信号,第 2 路是作者提供的滤波版本。二选一,不要混着喂进网络,否则模型会花容量去学两路之间的滤波差值,而那部分与身份无关。
Q6:注释这么稀疏,还能做什么?
做数据质量检查、做波形对齐可视化。它的正式用途在身份识别,而识别靠整段形态,不靠逐搏位置。想算心率或心率变异性,得自己重新检测 R 峰。
Q7:它的噪声到底是好是坏?
取决于你的目标。对方法学演示,噪声是需要先处理的障碍;对可穿戴认证研究,噪声正是真实使用条件的代理。作者关掉了心电图机的全部滤波,就是为了让噪声保持在原始状态。
Q8:能不能用它做跨机构泛化验证?
不能。它来自单一机构,没有第二个来源可以做对比。用它得到的「泛化」结论只能限定在时间维度上,不能外推到人群或机构维度。
Q9:体量多大,能一次下完吗?
解压后 12.5 MB,压缩包 12.6 MB,可以一次下完,也可以全部读进内存。它没有分片或数据库后端,就是普通文件。
Q10:它的许可能不能商用?
ODC-BY 1.0 允许包括商业用途在内的使用,条件是署名。引用时要写清数据集名、版本与作者论文,具体条款以许可全文为准。
Q11:为什么说它和 MIT-BIH 不能互换?
任务不同。MIT-BIH 标注的是心搏类型与节律,回答「这是什么心律」;本库标注的是身份,回答「这是谁」。字段、人群、标注体系全不一样,唯一的共同点是 WFDB 格式。
Q12:它还会更新吗?
官方没有任何扩版说明。从采集年代与数据来源看,这份数据是一次性完成的硕士论文成果,2014 年定版之后只有 2019 年补充了校验和文件,信号与注释未变。
术语表
| 术语 | 解释 |
|---|---|
| ECG-ID | 本库名称,直译为「心电身份」,强调心电作为身份凭证 |
| I 导联 | 左右手腕之间的电位差,属肢体导联 |
| 生物特征识别 | 用身体或行为特征确认个体身份的技术 |
| 类内差异 | 同一个人的不同记录之间的差别 |
| 类间差异 | 不同人之间的差别 |
| 封闭集识别 | 待识别对象必定在已知名单内 |
| 开放集验证 | 允许出现名单外的未知个体,需要拒识能力 |
| 冒充者集合 | 开放集评测中充当陌生人的那批样本 |
| 注册与验证 | 生物特征系统的两个阶段:先登记,后比对 |
| 时间泛化 | 模型在训练时间点之外的表现 |
| 分组划分 | 以人为单位整体切分,防止同一人跨两侧 |
| 泄漏 | 测试信息以隐蔽方式出现在训练过程中 |
| WFDB | PhysioNet 的波形数据库格式,含头文件、信号、注释 |
.hea |
纯文本头文件,声明采样率、增益、注释行 |
.dat |
二进制信号文件 |
.atr |
二进制注释文件 |
| 增益 | 每毫伏对应的整数单位数,本库为 200 |
| 校验和 | 采样点累加和,用于检查文件完整性 |
| 肢体夹式电极 | 夹在手腕或手指上的电极,接触稳定 |
| 原始信号 | 未经任何数字滤波的采集结果 |
| 基线漂移 | 低频原因导致的整体上下起伏 |
| 工频干扰 | 50 Hz 或 60 Hz 电源噪声 |
| ODC-BY | 开放数据共享署名许可 |
| DOI | 数字对象标识符,用于稳定引用 |
| PhysioNet | 生物医学信号数据与工具的公共平台 |
附录 A:核心数字核对表
| 项 | 值 | 来源 | 来源层级 |
|---|---|---|---|
| 记录数 | 310 | 官方页面;RECORDS 逐行 |
官方页面 |
| 受试者数 | 90 | 官方页面;目录数 | 官方页面 |
| 性别构成 | 44 男、46 女 | 官方页面;头文件注释行 | 官方页面 |
| 年龄范围 | 13–75 岁 | 官方页面;头文件注释行 | 官方页面 |
| 每人条数 | 官方 2–20,清单 1–22 | 官方页面与 RECORDS |
官方页面 |
| 单条时长 | 20 秒 | 官方页面;10000 点 ÷ 500 Hz | 官方页面 |
| 采样率 | 500 Hz | 官方页面;头文件首行 | 官方页面 |
| 位分辨率 | 12 位 | 官方页面 | 官方页面 |
| 量程 | 名义 ±10 mV | 官方页面 | 官方页面 |
| 信号条数 | 2 | 头文件首行 | 官方文件 |
| 增益 | 200 | 全部头文件信号行 | 官方文件 |
| 每人最长跨度 | 半年(官方);156 天(清单) | 官方页面;日期字段 | 官方页面 |
| 每记录注释 | 10 对 R 与 T 峰 | 官方页面;rec_1.atr |
官方页面 |
| 解压体积 | 12.5 MB | 官方页面文件区 | 官方页面 |
| 压缩包体积 | 12.6 MB | 官方页面下载按钮 | 官方页面 |
| 许可 | ODC-BY 1.0 | 官方页面 | 官方页面 |
| 上线日期 | 2014-03-06 | 官方页面 | 官方页面 |
| DOI | 10.13026/C2J01F | 官方页面 | 官方页面 |
每个 .dat 大小 |
40000 字节 | 全量 310 个文件逐一核对 | 官方文件 |
附录 B:数字的来源分层
第一层:官方页面与官方文件。 记录数、受试者数、性别与年龄构成、每人条数区间、单条时长、采样率、位分辨率、量程、噪声描述、采集条件、许可、日期、DOI,都来自项目主页、README、ANNOTATORS 与全量头文件。记录清单的 310 行与每个 .dat 的 40000 字节经过逐一访问确认。
第二层:官方论文摘要页与清单派生。 采集细节(坐姿、肢体夹电极、关闭滤波、心率与身心状态不限)、训练测试划分方式、识别系统的方法轮廓,来自页面上附带的英文摘要。每人条数的实际分布、首末日期间隔、性别与年龄的实际取值分布,是把 310 个头文件的注释行读出来统计得到的,属于对官方文件的直接派生,口径与方法在正文中已说明。
第三层:未核实。 设备台数与型号、噪声成分的定量构成、年龄与性别对识别率的影响、那 1 条与 22 条两个例外的成因、受试者的民族与健康状况。这些官方没有公布,正文中标「官方未说明」或「未公开」,不作推断。
附录 C:完整参考文献
| 编号 | 文献 | 链接 | 用途 |
|---|---|---|---|
| 1 | ECG-ID Database v1.0.0 | https://physionet.org/content/ecgiddb/1.0.0/ | 规模、结构、边界数据的来源 |
| 2 | 该库的 PhysioNet 文件目录 | https://physionet.org/files/ecgiddb/1.0.0/ | RECORDS、README、头文件 |
| 3 | Lugovaya T.S. 硕士论文摘要页 | https://physionet.org/files/ecgiddb/1.0.0/biometric.shtml | 采集条件、方法、识别结果 |
| 4 | 数据集 DOI | https://doi.org/10.13026/C2J01F | 引用与版本定位 |
| 5 | PhysioNet 平台综述(2026,Nature Health) | https://doi.org/10.1038/s44360-026-00096-z | 托管模式与引用规范 |
| 6 | MIT-BIH 心律失常库(对照) | https://physionet.org/content/mitdb/ | §2.5 与 §6.1 的对比 |
| 7 | PTB-XL(对照) | https://physionet.org/content/ptb-xl/ | §2.5 的人群对比 |
| 8 | Europe PMC 检索:心电生物特征识别 | https://www.ebi.ac.uk/europepmc/ | §7.4 的外部用法 |
附录 D:任务代码
任务一:按人划分的封闭集识别骨架
import numpy as np
import pandas as pd
import wfdb
from sklearn.model_selection import StratifiedGroupKFold
def build_index(root="data"):
import os
import re
rows = []
for person in sorted(os.listdir(root)):
pdir = os.path.join(root, person)
if not os.path.isdir(pdir):
continue
for fn in sorted(os.listdir(pdir)):
if not fn.endswith(".hea"):
continue
t = open(os.path.join(pdir, fn), encoding="utf-8").read()
meta = dict(re.findall(r"^# (Age|Sex|ECG date): (.+)$", t, re.M))
rows.append({
"person": person,
"rec": fn[:-4],
"date": pd.to_datetime(meta["ECG date"].strip(),
format="%d.%m.%Y"),
})
return pd.DataFrame(rows)
def features(rel, win=1000, step=500):
r = wfdb.rdrecord(f"data/{rel}", physical=True)
x = r.p_signal[:, 0]
segs = [x[s:s + win] for s in range(0, len(x) - win + 1, step)]
return np.mean([np.abs(np.diff(s)) for s in segs], axis=1)
idx = build_index()
#-- 只有一条记录的受试者无法做留出验证,先归入登记集
counts = idx["person"].value_counts()
keep = counts[counts >= 2].index
sub = idx[idx["person"].isin(keep)].reset_index(drop=True)
sub["feat"] = [features(p) for p in sub["person"] + "/" + sub["rec"]]
X = np.stack(sub["feat"].to_numpy())
y = sub["person"].to_numpy()
g = sub["person"].to_numpy()
for tr, te in StratifiedGroupKFold(n_splits=5, shuffle=True,
random_state=0).split(X, y, g):
assert set(g[tr]).isdisjoint(set(g[te]))
print("fold", len(tr), len(te))
这段代码的特征只用了一个最粗糙的统计量——滑动窗内一阶差分的均值。它的作用不是给出好结果,而是把「按人划分」这个协议跑通。换更强的特征或端到端模型时,划分部分一行都不用改。
任务二:同日与跨月两组的分层评测
import numpy as np
import pandas as pd
idx = pd.read_parquet("idx.parquet")
span = idx.groupby("person")["date"].agg(lambda s: (s.max() - s.min()).days)
same_day = span[span == 0].index
cross_day = span[span > 0].index
print("同日组人数", len(same_day))
print("跨日组人数", len(cross_day))
print("跨月组人数", int((span > 30).sum()))
print("最长跨度(天)", int(span.max()))
第二段代码的作用是把 §2.4 的分布复现出来。跑完之后就会明白,为什么正文说「跨月只有个位数的受试者」——这个结论不是靠感觉,而是这四行打印结果直接给出的。
附录 E:如果只记五件事
| 序号 | 记住什么 | 为什么 |
|---|---|---|
| 1 | 90 人、310 条、单导联 20 秒、500 Hz | 这四个数字定义了一切用法与限制 |
| 2 | 它有同人跨时段的结构,这是别处少见的 | 这是它唯一不可替代的地方 |
| 3 | 按人划分,不按记录划分 | 随机划分会让识别率虚高 |
| 4 | 注释只有十对 R 与 T 峰,无类型无节律 | 别把它当带标注的心电库 |
| 5 | 跨月样本只有三人,时间效应只能定性 | 别在论文里把它写成完整随访队列 |
编辑说明
核对于 2026 年 10 月 28 日完成。核对方式为直接访问 PhysioNet 项目页、RECORDS、README、ANNOTATORS、biometric.shtml,并把 RECORDS 列出的 310 条记录的 .dat 与 .hea 全量取回逐一核对:310 个 .dat 全部为 40000 字节,310 个头文件全部含且仅含年龄、性别、采集日期三行注释,首行采样率与点数完全一致。每人条数与首末日期间隔两项分布,是把这 310 个头文件读出来统计得到的,属于对官方文件的直接派生而非独立观测,正文已注明口径。Person_01/rec_1 的 .atr 逐条读出,用于确认「十对峰」这一描述。论文侧只读了页面附带的英文摘要,俄文会议论文未读全文。未覆盖的内容包括:设备台数与型号、噪声成分的定量构成、年龄与性别对识别率的影响、以及官方「每人 2–20 条」与清单「1–22 条」之间差额的成因。这些均标为未公开或官方未说明。
相关数据集导航
以下为站内 AI-Ready 数据集百科中与本词条共享多个主题标签的相关数据集,按相关度降序排列:
- macecgdb — 共享标签:生理信号 / 心电信号 / 可穿戴传感 / 评测基准
- icentia11k — 共享标签:生理信号 / 心电信号 / 可穿戴传感 / 评测基准
- scientisst-move — 共享标签:生理信号 / 心电信号 / 可穿戴传感 / 评测基准
- ieeepg — 共享标签:生理信号 / 心电信号 / 可穿戴传感 / 评测基准
- auricle-wearable-cv — 共享标签:生理信号 / 心电信号 / 可穿戴传感 / 评测基准
- butqdb — 共享标签:生理信号 / 心电信号 / 可穿戴传感
- drivedb — 共享标签:生理信号 / 心电信号 / 可穿戴传感
- eda-rest-sedation — 共享标签:生理信号 / 可穿戴传感 / 评测基准
- mmash — 共享标签:生理信号 / 心电信号 / 可穿戴传感 / 评测基准
- bidsleep-dataset — 共享标签:生理信号 / 心电信号 / 可穿戴传感 / 评测基准
导航说明:本章节由全站统一标签体系自动计算生成(标签重合度算法),双向可达;点击链接可跳转至对应数据集词条。

