尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

中文电子病历NER实战:CCKS 2019数据集解析与模型评估

发布时间:2026/9/26 16:21:18

资讯中心
01
ARTICLE

中文电子病历NER实战:CCKS 2019数据集解析与模型评估

中文电子病历NER实战:CCKS 2019数据集解析与模型评估
简介CCKS 2019 中文电子病历数据集是一份面向中文医疗命名实体识别NER任务的评测数据包适用于自然语言处理研究者、竞赛选手及医学信息抽取方向的学生。包内共含8个文件涵盖任务描述文档docx、原始文本与标注的txt、表格型训练/测试数据xlsx以及带答案的JSON测试集压缩包整体仅1.18MB结构紧凑便于直接下载使用。数据源自CCKS 2019评测任务包含1379例真实病历样本每份样本由originalText原始文本和entities实体标注构成覆盖手术、解剖部位、药物、疾病和诊断、影像检查、实验室检验等实体类型并标注了实体起止位置和重叠标记。使用这套数据读者可以复现比赛baseline、训练并评估自己的命名实体识别模型也可用于中文临床文本信息抽取教学与实验目前已有1434人学习下载是入门中文医学NLP的实用资源。1. CCKS 2019 中文电子病历数据集先弄清楚它到底能干什么很多人下载 CCKS 2019 中文电子病历数据集是冲着“中文医疗数据”来的结果解压后看到一堆电子病历片段反而不知道怎么下手。这个数据集的真实用途是命名实体识别NER评测给定一段病历原文模型要从里面抽出疾病、症状、检查、治疗、身体部位五类实体并且部分实体还要预测属性。它是中文医疗 NLP 里少见的公开、带标注、贴近真实临床文本的数据之一。想做中文医疗文本结构化、病案质控、辅助诊断的研发人员或者想找一个有明确评测口径的 NER 基准数据的同学这份数据都值得认真用起来。但前提是你得先把它加载成模型能吃的格式这篇文章就沿着这条路走一遍。2. 数据集内部长什么样从 .rar 解压到五类实体标注2.1 解压与目录文件原文、标注、说明三类文件的组织这个数据以 .rar 压缩包形式分发这类压缩格式在中文评测数据里很常见主要是为了减小体积并完整保留中文文件名。拿到压缩包后第一步是确认解压工具能正确处理中文名常见做法是在 Windows 上用 WinRAR 或 7-Zip在 Linux 服务器上用 unrar 或 7z 命令行。解压命令一般是这样unrar x CCKS2019_ChineseEHR.rar ./data/如果你的环境里没有 unrar可以用 7z7z x CCKS2019_ChineseEHR.rar -o./data/参数说明x表示保留压缩包内目录结构解压-o指定输出目录注意 7z 的-o后面不要有空格。解压完成后你通常会在data目录下看到按训练集、测试集划分的子目录内部组织一般遵循评测任务发布的固定结构原文文件、标注文件和一个说明文档。我用表格总结一下常见的三类文件文件类型作用典型格式原始病历文本供模型读取的输入内容是未分段或半分段的病历记录txt / 无扩展名纯文本实体标注文件记录每个实体在原文中的起止位置、类别、属性txt / json / xml任务说明文档说明实体类别定义、标注规范、评测方式pdf / docx / md拿到数据后我建议先把说明文档完整读一遍重点看两件事实体标注是“字符偏移量”还是“BIO 序列”评测指标是“实体级严格匹配”还是“宽松匹配”。这两点直接决定你后面的数据解析代码怎么写。2.2 中文电子病历文本形态字段化结构与命名实体位置中文电子病历的文本特点和新闻、微博语料差别很大。它通常是半结构化的每一份病历里有固定字段比如主诉、现病史、既往史、体格检查、诊断等。字段之间用标题或分隔符隔开但字段内部又完全是自然语言。举个例子一段现病史可能长这样主诉反复咳嗽、咳痰3年加重伴胸闷1周。 现病史患者3年前无明显诱因出现咳嗽、咳痰为白色粘痰冬春季加重每年持续约3个月近1周因受凉后上述症状加重伴胸闷、气促无胸痛、咯血无发热。这段文本里“咳嗽”“咳痰”“胸闷”“气促”都是症状类实体“受凉”是诱因但在标注规范里可能归属到其他类别而“冬春季”这类时间词一般不标。注意这里的字数是按字符算的所以像表格里常见的实体起始位置 “5-7” 也是按字符偏移来算这一点在后面对齐时尤其重要。实体在原文中的位置通常用“起始偏移 结束偏移”表示左闭右开。也就是说start5, end7表示包含第 5 和第 6 两个字符不包含第 7 个字符。这个约定几乎所有评测数据都遵守但仍有少数标注会用闭区间所以解析前快速抽几条样本人工验证一遍比任何“通用解析工具”都靠谱。2.3 五类实体与属性标注规范里的边界问题CCKS 2019 中文电子病历评测任务定义的实体类别常见的是下面这五类我把每类的含义和典型例子列出来实体类别含义典型例子身体部位人体解剖学部位肺、左肺上叶、淋巴结症状患者主观不适或客观体征咳嗽、发热、肺部啰音疾病明确的诊断名称肺炎、慢性阻塞性肺疾病检查实验室或影像学检查及其结果胸部CT、血常规、白细胞计数治疗治疗方式或用药阿莫西林、吸氧、手术切除真正让标注变复杂的是属性。症状类实体可以带属性比如“咳嗽”可能有“程度”属性轻、中、重、“发作频率”属性阵发性、持续性、“诱因”属性受凉、劳累。这部分标注信息通常和实体标签放在一起表示成一个“实体 属性名 属性值”的三元组。这是这个数据集区别于普通 NER 语料的地方也意味着严格匹配时抽出了实体但属性判断错误仍然算错。因此建议在写代码之前先把实体边界和属性边界分开。实体标注解决“在哪儿”的问题属性标注解决“这个实体的性质是什么”的问题。很多初学的人把属性值和实体混在一起做序列标注结果类别极度不均衡F1 上不去。正确做法是先做五类实体抽取再做基于实体上下文的属性分类或者对属性做独立的多分类任务。3. 把标注读进代码加载与向量化的完整 Python 路径3.1 读取原文与标注头尾偏移量的映射原理评测数据集常见的标注文件格式是每行一个实体字段包括实体文本、类别、起始偏移、结束偏移。下面是一份虚构的样本格式但结构上非常接近 CCKS 2019 的真实标注文件咳嗽 症状 5 7 咳痰 症状 8 10 慢性阻塞性肺疾病 疾病 23 31 胸部CT 检查 35 39拿到这种格式第一步就是写一个加载函数把原文和标注同时读进内存并转成方便处理的统一结构。常见做法是使用 Python 的数据类和列表来组织这些数据from dataclasses import dataclass from typing import List, Tuple dataclass class Entity: text: str type: str start: int end: int def load_annotations(ann_path: str) - List[Entity]: entities [] with open(ann_path, r, encodingutf-8) as f: for line in f: parts line.strip().split(\t) if len(parts) ! 4: continue entity Entity( textparts[0], typeparts[1], startint(parts[2]), endint(parts[3]) ) entities.append(entity) return entities核心逻辑是按制表符切分每行然后分别映射到 Entity 的四个字段。split(\t)能正确切分“实体文本 类别 起始 结束”四列因为实体文本本身不会包含制表符。这里要注意len(parts) ! 4的过滤已经过滤掉了空行和异常行但如果某行的实体文本中间混入了空格而不是制表符过滤条件是拦不住的需要额外检查。同时要把原文读取进来并验证标注偏移量是否越界def verify(entities: List[Entity], text: str) - bool: for ent in entities: if ent.start 0 or ent.end len(text): print(f越界: {ent}) return False if text[ent.start:ent.end] ! ent.text: print(f不匹配: {ent.text} - {text[ent.start:ent.end]}) return False return True这个验证函数的参数说明entities是上一步加载的实体列表text是对应病历的完整原文。检查内容包括偏移量是否落在文本长度范围内以及按偏移量截取出来的文本和标注实体文本是否一致。这一步能拦截绝大多数“标注文件与原文版本不匹配”的问题。3.2 从偏移标注到 BIO 标签序列神经网络 NER 模型通常吃的是“每个字符一个标签”的序列所以要把偏移量转成 BIO 标记。BIO 规则是实体首字符标 B-类型内部字符标 I-类型非实体标 O。以下是标准转换代码def offset_to_bio(text: str, entities: List[Entity]) - List[str]: tags [O] * len(text) for ent in entities: if ent.end len(text): continue tags[ent.start] fB-{ent.type} for i in range(ent.start 1, ent.end): tags[i] fI-{ent.type} return tags参数说明text是原始字符串entities是已经通过 verify 的实体列表。tags初始化为全 O长度等于文本字符数。然后遍历实体起始位置写 B-类型后续位置写 I-类型。这里存在一个隐患如果两个实体重叠后遍历的实体会覆盖先遍历的标签。CCKS 2019 的标注规范不允许嵌套或重叠实体但如果你的预处理代码里实体有重叠可以用if tags[ent.start] ! O: print(重叠)加一层告警。BIO 序列生成后下一步通常要做标签到 ID 的映射因为模型只接受数值输入label_list sorted(set(tags)) label2id {label: i for i, label in enumerate(label_list)} id_seq [label2id[t] for t in tags]参数说明label_list是从当前数据里抽出的所有标签集合label2id建立了标签字符串到整数索引的字典id_seq是最终的整数序列。这一步看起来简单但有一个坑如果在训练集和测试集上分别执行这套代码两个集合的label2id可能不一致导致测试时标签编号对不上。正确做法是先用训练集生成完整的 label2id然后用同一个字典映射测试集。3.3 类别统计与长文本截断策略病历文本的特点是字段多、正文长一份病历动辄上千字符。如果直接送入 BERT 这类长度限制为 512 的模型必须截断或切窗。截断不能只用“前 512 个字符”这种粗暴方式因为病历前半部分往往是主诉和现病史实体密度高而后半部分的诊断和治疗也同样是关键信息。常见做法是统计实体在全文中的位置分布然后决定窗口如何滑动。下面这段代码统计每个字符位置被实体覆盖的次数import matplotlib.pyplot as plt coverage [0] * len(text) for ent in entities: for i in range(ent.start, ent.end): coverage[i] 1 # 统计实体密度最高的阈值区间 high_density_segments [] current [] for i, c in enumerate(coverage): if c 0: current.append(i) else: if current: high_density_segments.append(current) current [] print(f实体覆盖字符数: {sum(1 for c in coverage if c 0)})这段代码的用途是快速判断实体是否集中分布在文本开头或者均匀散布全文。如果你的分析结果显示实体集中在前 40% 的位置那么采用“头部保留 尾部截断”的策略是合理的如果实体均匀散布则应该用滑窗切分窗口大小 400、步长 300让相邻窗口有 100 字符的重叠这样能降低实体被从中间切断的概率。截断策略的另一个参数是“窗口内最少实体数”。我一般设置 3如果某个滑动窗口内含实体数少于 3则跳过这个窗口减少无实体样本对模型训练的干扰。这个阈值你可以根据数据密度调整但不要设太高否则测试集上一些实体稀疏的困难样本会被跳过导致你评估时过于乐观。4. 评估模型实体级 F1 怎么算才对4.1 基于序列标签还原实体边界模型输出的是每个字符的预测标签序列比如B-症状 I-症状 O O B-疾病。要从这个序列还原出实体核心逻辑是按标签前缀分组遇到 B 就开启一个新实体遇到 I 就续接当前实体遇到 O 就关闭当前实体。以下是标准还原代码def bio_to_entities(tags: List[str]) - List[Entity]: entities [] i 0 n len(tags) while i n: if tags[i].startswith(B-): etype tags[i][2:] start i i 1 while i n and tags[i] fI-{etype}: i 1 end i entities.append(Entity(text, typeetype, startstart, endend)) else: i 1 return entities逻辑说明外层 while 遍历每个字符当遇到 B- 前缀时记录类型和起始位置内层 while 连续消费相同类型的 I- 标签直到遇到非 I- 标签或序列结束。这里的关键判断是内层循环的tags[i] fI-{etype}它保证了 I-症状 不会错误续接到 B-疾病 后面。如果模型用的是 BIOES 标签体系还需要额外处理 E- 和 S- 标签。BIOES 的还原逻辑会在每个独立实体结尾标 E单字实体标 S这样模型能更清楚地学习实体边界但还原代码要多一个分支遇到 E 就结束当前实体遇到 S 就直接把单字作为独立实体。4.2 实体级评估脚本与常见指标对照实体级评估和字符级评估是两套标准。字符级 F1 看的是每个字符的标签预测是否正确实体级 F1 看的是整个实体是否被完整抽出且类型正确。同一个模型字符级 F1 可能 96%实体级 F1 只有 88%这是很正常的现象。实体级严格匹配strict要求预测实体的起始、结束、类别全部正确才算 1 个正例。宽松匹配partial则允许边界有部分重叠。评测任务里常用的是严格匹配所以提交结果之前一定要确认官方脚本用的是哪一种。下面是一段实体级严格匹配评估的伪代码用集合交集实现def evaluate(pred_entities, gold_entities): gold_set set((e.start, e.end, e.type) for e in gold_entities) pred_set set((e.start, e.end, e.type) for e in pred_entities) correct len(pred_set gold_set) precision correct / len(pred_set) if pred_set else 0 recall correct / len(gold_set) if gold_set else 0 f1 2 * precision * recall / (precision recall) if (precision recall) else 0 return {precision: precision, recall: recall, f1: f1}参数说明这里把实体转成三元组元组利用 Python 集合的去重和交集能力一行代码完成匹配计算。这样计算出的 F1 是严格匹配口径适合作为团队内部的统一标准。如果你要对比不同模型我建议所有实验都用这段代码生成最终数值避免不同脚本间口径差异导致对比失效。还有一个细节评测脚本应该忽略实体文本内容只依赖起止位置和类型。因为模型输出的文本和原文已经对齐不需要再用字符串匹配。如果你基于还原出的实体文本去原文里找位置一旦同一个实体在文中出现两次位置匹配会错乱F1 值也会失真。5. 避坑与排查解压、编码、偏移量不一致等 4 个高频问题5.1 .rar 文件解压报错或提示需要密码现象解压到一半提示文件损坏或者弹窗让你输入密码。很多人第一反应去找各种“rar密码移除”工具其实多数情况是工具版本太老不支持高版本 RAR 压缩格式。原因WinRAR 从 5.0 版本开始改变了压缩算法旧版解压工具无法识别。另外部分从网盘下载的文件下载过程中损坏也会表现为需要密码或 CRC 校验失败。解决更新解压工具到最新版Linux 下卸载 unrar-free安装 official unrar 或 p7zip-rar。再不行重新下载一次压缩包并在下载完成后对比文件的字节大小是否和下载页面标注一致。5.2 读取病历文件中文乱码现象用 Python 的open(path, r, encodingutf-8)读取文件后中文显示成乱码或者直接抛UnicodeDecodeError。原因这份数据集的文本文件可能不是 UTF-8 编码而是 GBK 或 GB18030。不要假设所有中文数据都是 UTF-8特别是由医院信息系统导出的数据生成时所在系统的编码环境往往是 GBK。解决先尝试 GB18030 读取这个编码是 GBK 的超集能覆盖大多数中文字符def read_text(path): for enc in [utf-8, gb18030, gbk]: try: with open(path, r, encodingenc) as f: return f.read() except UnicodeDecodeError: continue raise ValueError(f无法解码: {path})参数说明enc依次尝试三种编码GB18030 放在优先位置是因为它兼容繁体字和生僻字。这段代码能自动处理编码格式但会掩盖问题因此建议在处理完后把所有文件统一转成 UTF-8 保存一份后续实验全部基于转换后的文件。5.3 标注偏移量与原文对不上现象验证脚本输出大量“不匹配”告警实体文本和按偏移量截取出来的文本不一致。原因这是这个数据集最棘手的问题常见诱因有两个。一是原始病历文本里含有不可见字符比如\u3000全角空格、\xa0不间断空格人工查看时看不见但计算字符串长度时会计入。二是标注文件的原始版本和发布版本在预处理时对回车换行做了替换导致全部偏移量后移。解决先做全文归一化把全角空格换成普通空格统一换行符text text.replace(\u3000, ).replace(\xa0, ).replace(\r\n, \n)注意归一化操作必须在加载标注之前执行否则会破坏原文和标注的对齐。如果做了这层处理仍然对不上那就只能统计偏移量误差的分布看是否所有错误实体都偏移同一个量是的话做整体修正。5.4 训练集与测试集的实体分布差异现象模型在训练集上 F1 达到 90%提交到测试集只有 70%。原因病历数据不像新闻语料那样类别均匀。训练集可能以呼吸系统疾病为主测试集里却混入了大量心血管系统术语模型没见过这些词汇自然抽不出。解决拿到数据后先分别统计训练集和测试集的实体类型分布与实体长度分布。如果差异明显有两种处理策略一是把训练集按测试集分布做重采样二是放弃纯单模型方案针对常见实体类别补充外部词典做后处理。这是数据层面的问题调模型超参数无法解决这一点要尽早意识到。6. 进阶把这份数据集用出更好的效果6.1 从 BIO 到 BIOES边界信息更明确BIO 标注对实体内部和外部做了区分但实体结束位置没有显式标记。BIOES 在每个实体尾字符标 E-类型单字实体标 S-类型给模型提供了更明确的边界信号。如果你的模型是 BERT CRF把标签体系从 BIO 换成 BIOES通常实体级 F1 能提升 1 到 2 个点几乎不需要额外代价。转换规则是实体长度为 1 时标 S长度大于 1 时首字符 B、尾字符 E、中间字符 I。代码上只需要改第 3 章的offset_to_bio函数增加一个判断分支。6.2 词典与特征增强的边界电子病历里的实体有很多是直接可查的比如“阿莫西林胶囊”这种药物名“胸部 CT”这种检查名。准备一份医学词典在模型预测后用最大匹配法做一次词典修正能稳定提升实体召回。但这个技巧有边界一旦涉及“咳嗽伴喘息”这种症状组合词典匹配会切分出两个实体而标准答案可能是一个联合实体反而拉低精确率。所以词典增强只建议用于检查类、治疗类实体症状和疾病这两类要谨慎。6.3 正确地做实验划分很多人拿到这份数据直接按官方划分训练集、测试集跑实验这没问题但要注意如果你要做模型调参不要反复用测试集验证很容易过拟合到测试集。我的做法是从官方训练集里再切出 10% 作为开发集专门用来调参和早停最后才在官方测试集上跑一次最终提交。开发集的切分要保证实体类别分布与原训练集一致用 stratified split而不是随机切分。最后这条建议来自我自己的经验教训这份数据集最有价值的不仅是那几百份标注病历而是它提供了一个可复现的中文医疗 NER 评测口径让你改任何模型之前都能先跑出基线数字。拿到数据先建立加载、评估、简单基线的完整流程再谈算法优化。整个过程里最耽误时间的不是模型训练而是各种编码和偏移量问题。希望你跑通这套流程时比我省去那些磨合的时间希望帮到你。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。