news 2026/9/28 15:09:00

CCKS 2019中文电子病历数据集:从解压到NER基线的完整实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CCKS 2019中文电子病历数据集:从解压到NER基线的完整实践

简介:CCKS 2019 中文电子病历数据集是一份面向自然语言处理与医疗信息抽取研究者的公开评测数据,可用于中文医学命名实体识别、关系抽取等任务的训练与验证。资源包含1379例真实病历样本,每个样本同时提供原始文本和实体标注,字段覆盖手术、解剖部位、药物、疾病和诊断、影像检查、实验室检验六类实体,并给出实体起止位置与重叠标记,便于直接构造序列标注实验。压缩包共8个文件,以xlsx、txt、json、docx格式为主,分别存放训练集、测试集、未标注样本、评测任务说明及结果标注文件,整体约1.18MB,轻量易用。目前已有1434人学习下载,适合作为医疗NLP入门或评测基线搭建的练习数据。

1. 为什么对着 CCKS 2019 中文电子病历数据集.rar 折腾是值得的

拿到 CCKS 2019 中文电子病历数据集.rar 这类压缩包的人,大概率已经在做医疗文本的命名实体识别、关系抽取,或者正在给医院项目搭知识图谱。CCKS 2019 的电子病历评测任务,把中文临床文本里最难啃的部分摆到了台面上:病程记录长、句式随意、实体跨行、术语写法和日常叫法混在一起。这个 rar 包之所以到现在还被反复下载,是因为它提供了一套带标注的原始病历文本,能用来训练基线模型,也能用来验证自己的实体抽取方案在中文临床场景下到底行不行。

我见过不少人栽在第一步:下载完发现解压乱码,或者解压到一半报 CRC 错误,更常见的是一上来就用 Excel 打开标注文件,结果全角符号和制表符把列搞得乱七八糟,还没开始建模就劝退了。这篇笔记按我实际处理这类资源的顺序来写:先做压缩包的完整性和编码确认,再把病历文本切成 BIO 序列,跑一个最小可复现的 NER 基线,最后把评估口径和踩过的坑讲清楚。适合刚接触临床 NLP 的工程师,也适合被病历文本折磨过的熟手用来补边界。

2. 打开这个 rar 之前:先做完整性检查、编码探测和目录盘点

2.1 别急着“解压到当前文件夹”,先用 7-Zip 看一眼清单

很多人拿到 .rar 的第一反应是双击解压,这在 Windows 上最容易埋雷。中文文件名在压缩包里用 GBK 编码保存,老的 WinRAR 版本可能没问题,但系统默认终端和某些国产压缩软件会按 UTF-8 去解释,解压出来就是“锟斤拷”。我的习惯是先用 7-Zip 的命令行只做列表查看,不真正解压:

7z l ccks2019_emr.rar

这条命令只列出压缩包内的文件名、原始大小和压缩后大小,不会把文件写到磁盘。先看清单有几个目的:第一,确认文件名是不是完整的中文,而不是乱码;第二,判断里面是单个文件夹还是散落的多个文件,为后续建目录做准备;第三,如果压缩包里混进了 .exe、.bat 之类的可执行文件,列表里也能一眼发现。正常的数据集压缩包应该只有文本文件或表格文件,出现可执行文件本身就是危险信号。

确认清单无误后,再做一次完整性校验:

7z t ccks2019_emr.rar

t是 test 的简写,会对压缩包内每个文件做 CRC 校验,输出里出现Everything is Ok才是安全的。只要有一个文件包损坏,后面解压出来的标注数据就可能少几行,而少了行这种错误在 NER 任务里很难被发现,只会让评估分数静悄悄地往下掉。这一步属于成本极低、收益极高的操作,和数据本身无关,但几乎所有数据集事故都出在这个环节。

2.2 用 Python 解压后做编码探测,别让 UTF-8 梦游

解压我一般也交给 7-Zip,不推荐 Python 的 rarfile 库直接处理受保护的压缩包,它会引入一堆系统依赖。解压完成后,先用 Python 遍历目录,把文件扩展名、文件数量、总体积统计出来:

import os from collections import Counter data_root = "./ccks2019_emr/" ext_counter = Counter() total_size = 0 for root, dirs, files in os.walk(data_root): for name in files: ext = os.path.splitext(name)[1].lower() ext_counter[ext] += 1 total_size += os.path.getsize(os.path.join(root, name)) print("扩展名分布:", dict(ext_counter)) print("总大小: %.1f MB" % (total_size / 1024 / 1024))

这段代码不解决任何模型问题,但它能在 30 秒内告诉你手里到底是什么。扩展名分布能看出是纯文本文件、CSV 还是带 Excel 格式的文件;总大小能用来核对下载是否完整。比如压缩包显示原始文件有 50MB,解压出来只有 5MB,那肯定中途出问题了,这时候别急着往下走,先回下载源头重新拿文件。

接下来是编码探测。中文电子病历数据集常见的编码是 UTF-8、带 BOM 的 UTF-8,或者 GB18030。直接读 UTF-8 文件遇到 GBK 内容时会抛UnicodeDecodeError;反过来用 GBK 读 UTF-8 文件则会出现乱码但不报错,危害更大。我用chardet做快速探测,但只把结果当成参考,最终以实际解析能否成功为准:

import chardet def detect_encoding(path, sample_size=4096): with open(path, "rb") as f: raw = f.read(sample_size) result = chardet.detect(raw) print(f"{path}: {result['encoding']} (置信度 {result['confidence']:.2%})") return result["encoding"]

注意chardet对短样本的判断不稳定,所以采样前 4KB 而不是只读第一行。如果多个文件探测结果不一致,统一转码之后再进入处理流程。我一般会把所有原始文件先转换为 UTF-8 无 BOM 的副本,保留原文件不动,这样后续所有工具链都只面对一种编码,少掉一大类玄学问题。

2.3 给原始数据做只读快照,不然后悔药都买不到

预处理之前,先把整个解压目录复制一份,改成只读属性。听起来很傻,但真实情况是:你写着写着脚本,可能把标注文件原地修改了,然后跑完基线才发现测试集已经被污染。我吃过这个亏,后来定了死规矩:原始解压出来的目录一律放在raw/下,所有派生数据放在processed/下,脚本里只允许读raw/,永远不写。如果磁盘够,把解压后的目录打包成同名.tar留一份,标注文件里的一个全角空格错误,就够你排查一个下午。

3. 把电子病历切成 BIO 序列:预处理脚本、标签映射与数据集划分

3.1 从标注文本到 Token 序列:一个可直接跑的解析脚本

CCKS 2019 电子病历评测的标注数据,常见组织方式是:每个样本一段病历文本,实体标注覆盖症状、体征、检查、疾病、药物等类型,所有标注写在另一份文件里。为了能喂给序列标注模型,第一步是把文本和标签对齐成每行一个 token、用制表符分隔的格式。我一般先写一个解析函数,把原始标注读成文档列表:

def parse_annotation(path): docs = [] tokens = [] labels = [] with open(path, encoding="utf-8-sig") as f: for line in f: line = line.strip() if not line: if tokens: docs.append({"tokens": tokens, "labels": labels}) tokens, labels = [], [] continue parts = line.split("\t") if len(parts) < 2: continue tokens.append(parts[0]) labels.append(parts[1]) if tokens: docs.append({"tokens": tokens, "labels": labels}) print(f"共解析 {len(docs)} 个文档, {sum(len(d['tokens']) for d in docs)} 个 token") return docs

这段代码有几个容易被忽略的细节。第一,用utf-8-sig而非utf-8,因为 Windows 下产生的文本文件会带 BOM,utf-8-sig会自动吃掉头部的\ufeff,避免第一个 token 变成奇怪字符。第二,strip()同时去掉\r\n和\n,兼容不同换行符。第三,跳过没有制表符的行,这类行往往是空说明或表格格式标记,强行解析只会混入噪声。

解析完成后,建议立刻打印一个文档的长度分布,看看 max_len 应该怎么设置。电子病历经常出现整段病程记录堆在一起的情况,一个“文档”可能包含几百个 token,直接塞进 BERT 会超出 512 限制,所以后面需要用滑动窗口或按句子切分,这个改动的参数差异非常大。

3.2 标签系统怎么选:BIO 和 BIOES 的差别不只是字母

序列标注里最常见的标签方案是 BIO:B 表示实体开始,I 表示实体内部,O 表示非实体。它实现简单,绝大多数模型都能直接处理。但我更建议在电子病历场景用 BIOES 或 BIOE:B 是开始,I 是内部,E 是结尾,S 是单个字符实体,O 是外部。

病历里单字实体特别多,比如“痛”一个字就能表示症状,“热”一个字表示发热。BIO 方案里这两种情况都标成B-症状或I-症状,解码时边界完全靠 CRF 或模型推断。BIOES 里单字实体标S-症状,这让边界判断直接从“猜”变成“读标签”,测试集 F1 往往能涨一到两个点。

选择标签方案时,有三个参数需要同步定下来:

参数建议值影响
max_seq_len128-256太长显存不够、训练慢;太短长实体全被截断
label_schemeBIOES 优先单字实体多的数据收益明显
是否过滤无标注句子保留全 O 的句子能增强模型对反例的判别,但会拉长训练时间

我见过有人为了省时间把全 O 的句子全部删掉,结果模型在医院数据上把“体温正常”这种完全中性的句子也识别出了疾病实体,这就是典型的正负样本失衡。

3.3 数据集划分:按病历分,别按句子随机分

划分训练集和验证集时,最容易犯的错误是直接对 token 列表做随机切分。随机切分会把同一个病人的病历片段同时送进训练集和验证集,模型在验证阶段等于开卷考试,分数虚高但没有实际意义。正确做法是先给每个文档分配一个唯一 ID,然后按 ID 划分:

import random from collections import defaultdict doc_groups = defaultdict(list) for idx, doc in enumerate(docs): doc_id = "case_" + str(idx // 3) doc_groups[doc_id].append(idx) doc_ids = list(doc_groups.keys()) random.seed(42) random.shuffle(doc_ids) split_point = int(len(doc_ids) * 0.8) train_ids = doc_ids[:split_point] valid_ids = doc_ids[split_point:] train_docs = [docs[i] for cid in train_ids for i in doc_groups[cid]] valid_docs = [docs[i] for cid in valid_ids for i in doc_groups[cid]]

这里的idx // 3是我临时的分组策略,实际使用时应替换成病历中的真实病人标识,比如门诊号后三位或病历 ID。随机种子固定后,每次跑出来的数据划分一致,这对后续调参至关重要。如果你换了随机种子分数明显变化,说明数据量不够或样本分布太偏,这时候与其调模型,不如想想怎么扩充样本。

4. 跑通一个最小 NER 基线并算对指标:BERT 还是词典、严格还是宽松

4.1 第一步用 CRF 还是 BERT:选型理由不是“哪个新选哪个”

中文电子病历的实体抽取,核心难点是命名不规范和上下文依赖。比如“患者咳嗽三天”里的“咳嗽”是症状,“咳嗽带血丝”里的“咳嗽带血丝”是更细的症状描述,词典匹配在这种场景下漏召会非常严重。所以我不推荐直接拿词典方案当正式基线,词典只适合做预标注,用来给 BERT 补充外部特征。

真正的最小可行基线,要么是条件随机场搭配手工特征,要么是预训练语言模型加一个线性输出层。如果手头只有 CPU、数据量在一万条以内、还要在内网保密环境交付,CRF 是更务实的选择;如果有一张入门级 GPU、想要可复用的流程,我会直接上 BERT 类模型微调,输出层换成一个简单的 token 分类器。

方案适用条件优势劣势
词典+规则快速验证、特定字段抽取可解释、零样本泛化差、维护地狱
CRF+字符特征CPU 环境、小数据量轻量、可控特征工程耗时
BERT 微调GPU 环境、数据量足够效果好、迁移强显存消耗大、黑匣子

无论选哪个,第一阶段的目标是跑通流程,而不是刷最高分。选定了方案就别中途频繁换,先用一套固定参数跑一周,积累对这个数据集难度的直觉,再去优化。

4.2 实体级 F1 的评测脚本:严格匹配、宽松匹配都算

NER 评估不像分类任务只有一个口径。同一段文本,模型预测的实体边界比标准答案多一个字符或少一个字符,严格匹配下算错,宽松匹配下可能算对。实际业务里,边界差一个字可能不影响下游检索,但研究论文和评测里通常看严格匹配。我习惯两个都算,然后把严格匹配当主指标:

def eval_ner(trues, preds): """trues/preds 是列表,元素为 (start, end, entity_type)""" true_set = set(trues) pred_set = set(preds) strict_tp = len(pred_set & true_set) precision = strict_tp / len(pred_set) if pred_set else 0 recall = strict_tp / len(true_set) if true_set else 0 f1 = 2 * precision * recall / (precision + recall) if (precision + recall) else 0 # 宽松匹配:只看起始位置和类型一致就算正确 true_loose = {(s, e_type) for s, e, e_type in true_set} pred_loose = {(s, e_type) for s, e, e_type in pred_set} loose_tp = len(pred_loose & true_loose) loose_f1 = 2 * loose_tp / (pred_loose + true_loose) return precision, recall, f1, loose_f1

这段代码的输入要求是实体边界(start, end, type),在模型输出后需要把 BIOES 标签序列解码成实体片段,再转换成字符偏移量。这里的陷阱是偏移量必须统一:是按字符数偏移,还是按 token 数偏移。BERT 的 tokenizer 把中文切成字粒度还好,但如果加了词表,同一个字符可能被拆成多个子词,偏移量就对不上了。我的做法是统一在原始文本上计算字符偏移,模型预测的每个 token 往回映射到原始文本的字符区间。

4.3 别被整体 F1 骗了:按实体类型分层评估

整体实体级 F1 不能反映真实痛点。电子病历里“症状”类实体出现频率高、相对好识别;“检查”类实体包含大量缩写和英文混排,F1 会明显偏低;“疾病”类实体受上下文影响大,误报率往往很高。把评估结果按实体类型拆开看,才能决定下一步优化方向。

我习惯在跑完评估后打印一张分层报表,列出每个类型的 TP、FP、FN 和 F1。如果发现某个类型预测量远大于标注量,说明存在系统性误报,常见原因是模型把否定表述里的实体也抽了出来,比如“无恶心呕吐”里的“恶心呕吐”被当成阳性症状。这种问题整体 F1 看不出来,分层报表一眼就能定位。

5. 避坑:解压乱码、标注错位、压缩包里的广告子程序与三个血泪教训

5.1 现象:解压后所有文件名变成乱码,病历内容打开是空的

原因:压缩包在 Windows 下用 GBK 编码保存文件名,而解压工具按 UTF-8 解析,文件名乱码后文件内容实际上没有损坏,但路径对不上,导致读取失败。

解决:不要用系统自带的“全部解压”按钮,改用 7-Zip,在“选项”里把编码设置为“本地代码页”。如果你已经解压乱了,把压缩包重新用 7-Zip 解压到新目录,注意目标目录不要包含中文,减少路径编码干扰。

5.2 现象:rar 解压到一半报 CRC 错误,或者解压出来只有几十 KB

原因:网盘下载不完整、浏览器断点续传出错、或存储介质坏道。rar 格式对完整性要求很高,只要有一个分卷位错,后面所有文件都解不出来。

解决:回原发布页重新下载,下载完成后先用7z t做完整性校验,再进下一步。不要尝试用“恢复记录”强行修复并继续用数据,标注数据只要错一行,模型的 CRF 解码就可能集体错位,这种错误没法自动发现,只能浪费几天时间。

5.3 现象:拿到一个课程资料.rar 忘记解压密码,网上找的密码移除工具一打开就弹广告

原因:密码恢复和“破解版”工具是广告子程序的重灾区。这类工具通常以小体积、免安装为幌子,解压后自带另一个可执行文件,运行后弹广告、静默安装推广程序,甚至篡改浏览器首页。

解决:先查压缩包属性里的注释页和发布页说明,作者通常会把密码写在标题或配套说明文档里,而不是真的为了保密。找不到密码就回到正规渠道重新获取数据包,不要为了一个数据集去冒这种风险。对已经下载的解压工具,先看文件图标和发行方签名,再用杀毒引擎全盘扫描,那种刚解压就引发多个告警的程序直接删掉最省事。

5.4 现象:BERT 模型预测的实体边界总是差一个字,比如“胸廓畸形”识别成“胸廓”

原因:预训练模型的分词器把“胸廓畸形”切成“胸廓”和“畸形”两个词,而标注规范里它是一个完整实体,CRF 或线性层在训练时可能没学会合并这两个词的边界。另一个常见原因是原始标注和模型预处理之间发生了字符错位,比如全角括号被转成了半角,导致索引对不上。

解决:在模型输出层加边界后处理,用一个院内词汇表做最长匹配回溯;同时检查训练数据和预测输入用的预处理函数必须完全一致,只要有一端多做了全角转半角,边界就全部错了。我在工程里把预处理统一成一个函数,数据清洗和模型推理都调同一个入口。

5.5 现象:按 token 随机划分训练集后,验证集分数比测试集高 10 个点

原因:随机划分把同一份病历的不同片段同时分到训练和验证里,模型在验证时“见过”了相似句子,评估结果虚高。这也是很多论文效果难以复现的重要原因。

解决:按病历 ID 或按病人维度划分,逻辑见 3.3 节;如果原始数据里没有明显的病人标识,用文档标题行生成伪 ID,只要保证同一病历文件的所有内容不出现在两个集合里,就比随机 token 划分严谨得多。

6. 把 CCKS 2019 这套数据用到位:一页纸验证清单与多源病历合并经验

反复用这个 rar 包做实验之后,我把验收流程固定成了一页纸清单,每次拿到新数据源都走一遍:第一,7z t验证压缩包完整性;第二,统计扩展名分布和文件大小;第三,编码探测并统一转为 UTF-8;第四,解析样本文件,打印 token 数和实体类型分布;第五,按病历维度划分训练验证集;第六,用实体级严格 F1 评估,并输出分层报表。这六步做完,数据集能给你提供什么样的基线,基本心里有底了。

关于把多个来源的病历合并成统一语料,我用一个很简单的 Python 脚本解决,不需要任何 Excel 插件。手头分发到 Excel 里的病历摘要、标注片段,统一导出 CSV 后按文件名批量合并:

import glob import csv merged_path = "merged_emr.tsv" with open(merged_path, "w", encoding="utf-8", newline="") as out_f: writer = csv.writer(out_f, delimiter="\t") for path in glob.glob("./extra/*.csv"): with open(path, encoding="utf-8-sig") as in_f: reader = csv.reader(in_f) for row in reader: if row and len(row) >= 2: writer.writerow([path.split("/")[-1], row[0], row[1]])

合并后的文件第一列保留来源文件名,相当于一个粗粒度的域标签,训练时可以用来按来源分组,避免不同医院的病历书写风格互相干扰,这比把所有数据混在一起训要稳得多。混合来源时还有一个容易被忽略的坑:不同文件的编码可能是 GBK 和 UTF-8 混用,utf-8-sig只能处理 BOM,遇到 GBK 文件需要先按gb18030转码,否则 CSV 解析会在第 N 行突然崩掉。

这套数据我从 2019 年盯到现在,最大的体会是:医疗文本的标注质量和预处理稳定度,比模型结构对最终效果的影响大得多。很多人把时间花在改模型上,却忽略了原始数据已经存在边界错位、编码污染和划分泄漏,最后得到的分数根本不可信。我一直保留着“先验证再训练”的习惯,把一页纸清单放在手边,每换一个数据源就从头核对一遍,希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 15:09:00

JEV模型实战:从申请密钥到接入Codex的完整指南

最近在开发者圈子里&#xff0c;JEV 这个词出现的频率明显变高了。从技术群里的讨论&#xff0c;到各种模型评测榜单的评论区&#xff0c;再到 Codex 这类 Agent 工具的配置教程里&#xff0c;到处都能看到有人在问“JEV 模型官网在哪”“JEV 怎么接入”“JEV 开源了吗”。我也…

作者头像 李华
网站建设 2026/9/28 15:07:38

离散小波变换MATLAB实战:原理、参数与避坑全解析

做小波变换的MATLAB代码&#xff0c;网上随便一搜就是一堆&#xff0c;但多数人只是把dwt、wavedec这几行命令抄下来跑通就完事了。等真正用起来&#xff0c;选小波基、定层数、处理边界、挑阈值&#xff0c;每一步都可能翻车。我刚上手那段时间就吃过不少亏&#xff1a;系数长…

作者头像 李华
网站建设 2026/9/28 15:07:33

Python+Unet图像语义分割实战:从环境配置到模型训练与调优

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 15:07:26

C# WinForms 2D游戏骨架:Game Loop与对象池实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 15:06:11

HslCommunication收费版深度解析:FX5U与MC协议工业级通讯实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 15:03:22

JEV 模型实测:从申请接入到代码重构与 Agent 实战

最近一段时间&#xff0c;JEV 这个词在开发者圈子里出现得越来越频繁。群里有人问"JEV 模型官网在哪"&#xff0c;有人问"JEV 怎么接入 Codex"&#xff0c;还有人直接晒出用 JEV 跑完一轮重构的截图。我本来以为又是一波蹭热度的营销&#xff0c;直到自己花…

作者头像 李华