news 2026/9/10 23:42:18

中文电子病历NER实战:CCKS 2019数据集与BERT-BiLSTM-CRF基线

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
中文电子病历NER实战:CCKS 2019数据集与BERT-BiLSTM-CRF基线

简介:面向中文医学自然语言处理研究者与竞赛学习者,这份数据集源自CCKS 2019中文电子病历命名实体识别评测任务,包含1379例真实病历样本,每份均提供原始文本与实体标注,覆盖手术、解剖部位、药物、疾病和诊断、影像检查、实验室检验等五类实体,可直接用于训练和评估中文电子病历NER模型,解决非结构化病历文本的信息抽取问题。压缩包共8个文件,以xlsx标注表、txt训练/未标注语料、json带标注测试集及docx任务说明为主,整体仅1.18MB,轻量便于下载与复现。借助这些语料,研究者可复现官方评测流程,对比不同模型在手术、用药、诊断等实体抽取上的表现,也能用于构建术语词典或医疗问答系统的底层抽取模块。目前已有1400余人浏览/学习,数据来自官方赛题,质量可靠,适合做学术基准测试、模型调优或入门医学文本挖掘实践。

1. 为什么用 CCKS 2019 中文电子病历数据集做 NER 基线测试

拿到这份.rar先别急着解压跑模型——CCKS 2019 发布的中文电子病历命名实体识别(NER)评测数据集,名义上只有 1379 例病历样本、五个实体类型,但真正让它成为医学 NLP 试金石的是标注细节:overlap字段、跨行文本、以及"手术"与"疾病和诊断"之间高度嵌套的实体关系。很多团队用通用领域预训练模型直接跑,F1 值看起来很高,换个科室的病历立刻崩盘,原因就在于没有吃透这个数据集的标注边界。它适合两类人:一是想快速验证 BERT、RoBERTa、ERNIE 在医学文本上的表现差异;二是要给真实临床文本处理系统做领域适配,需要一份带标准偏移量(start_pos / end_pos)的干净标注数据来评估解码策略。接下来我会按"看数据 → 转标签 → 建模型 → 踩坑验证"的顺序把它拆开。

2. 解析病历实体标注的 JSON Schema 与五个标签类型

2.1 标注结构:为什么start_posend_pos是字符级偏移

样例里的entities直接给到字符偏移,这比常见的 BIO 标注文件更适合复现。originalText是一整段脱敏后的出院记录或病程记录,entities是标注列表,每个实体包含label_typeoverlapstart_posend_pos。要注意start_posend_pos是左闭右开区间,即text[start_pos:end_pos]恰好切出实体原文。以下是该数据集中一个典型样本的字段构成:

字段类型说明示例值
originalTextstring去标识化后的病历原始文本患者3月前因“直肠癌”于在我院于全麻上行直肠癌根治术
label_typestring五类实体之一疾病和诊断
overlapint是否与其他实体在字符区间上重叠0 或 1
start_posint实体开始的字符索引(从0计)8
end_posint实体结束的字符索引(开区间)11
entitieslist当前文本的全部实体标注见代码块

示例里rectal cancer被标为"疾病和诊断",overlap为 0,说明该实体在区间上不与任何其他实体交叉。但同一份病历里"直肠癌根治术"如果被同时标为"手术",就和"直肠癌"在字符区间上产生重叠——这就是overlap字段的价值:它明确告诉你这份数据集允许嵌套实体存在,不是一个纯扁平的序列标注问题。

{ "originalText": ",患者3月前因“直肠癌”于在我院于全麻上行直肠癌根治术(DIXON术),手术过程顺利,术后给予抗感染及营养支持治疗,患者恢复好,切口愈合良好。,,术后病理示:直肠腺癌(中低度分化),浸润溃疡型,面积3.5*2CM,侵达外膜。双端切线另送“近端”、“远端”及环周底部切除面未查见癌。肠壁一站(10个)、中间组(8个)淋巴结未查见癌。,免疫组化染色示:ERCC1弥漫(+)、TS少部分弱(+)、SYN(-)、CGA(-)。", "entities": [ {"label_type": "疾病和诊断", "overlap": 0, "start_pos": 8, "end_pos": 11}, {"label_type": "手术", "overlap": 1, "start_pos": 18, "end_pos": 24} ] }

这份 JSON 结构没有采用嵌套标注的复杂表示,而是用扁平列表加overlap标记来记录交叉关系。我一般会先统计overlap为 1 的实体占比,如果超过 5%,就不要直接套用 CRF 那套"每个 token 一个标签"的解码逻辑,否则模型天然无法输出重叠实体。

2.2 五类实体的边界特征与混淆点

CCKS 2019 中文电子病历数据集的五个实体类型并不均衡:疾病和诊断解剖部位数量最多,手术次之,影像检查实验室检验最少。这种不均衡会直接放大模型在少数类上的 F1 波动。下面按实际标注中的易混淆程度排序讲解。

疾病和诊断是最容易和手术混淆的类型。"直肠癌根治术"整体是手术,但其中嵌套的"直肠癌"是疾病和诊断;"胃癌术后"里的"胃癌"是疾病,"术后"不构成实体。标注规则通常以疾病名称本身为准,不管它出现在手术名称还是病程描述里。药物类别坑在复合名称上,比如"奥沙利铂150MG D1"——实体是"奥沙利铂"还是整串用药方案?本数据集按最小药物成分名标注,剂量和用药频次不进实体。解剖部位影像检查的区分要留意"部位+检查"连写的情况:"腹部CT"是影像检查,"腹部"单独出现时是解剖部位。实验室检验的边界更细,它只覆盖检验项目名词,如"白细胞计数",而"白细胞"本身解剖部位或成分名则按上下文判别。

提示:拿到数据先跑一个实体长度分布,疾病和诊断往往出现 2~8 个字的跨度,手术则普遍在 4~12 字。这会影响后面序列标注的标签字典设计和解码时的约束规则。

标签不均衡的另一个后果是:如果直接按label_type做五分类微调,影像检查的召回会显著偏低,因为样本里手术记录远多于影像报告。处理办法有两种,一是在损失函数里按类别频率加权,二是先用一个二分类判断"是否是实体",再在多分类里判断类型。后一种在医学文本上通常更稳定,因为实体区域和背景区域的判别难度远低于五类之间的判别难度。

3. 数据集解压后的文件结构与 BIO/BIOES 标签转换

3.1subtask1subtask2的差异:从实体识别到关系抽取

压缩包中包含的subtask1_training.txtsubtask1_test_set_with_answer.json对应的是第一子任务(命名实体识别),一个样本一行文本,标注以 JSON 形式附加。subtask2_training_part1.xlsxsubtask2_training_part2.xlsxsubtask2_test.xlsxsubtask2_unlabeled.txt则是第二子任务的"属性抽取"或"关系抽取"组织方式,Excel 里通常包含实体对及其关系类别。如果你只做 NER 基线,直接读取 subtask1 的 JSON 即可;subtask2_unlabeled.txt是无标注病历,适合做领域自适应的无监督预训练,或者用弱监督方式生成伪标签。

readme-subtask1.txt里说明了实体类型的标注规范,但注意它不保证每个文本文件都与测试集 JSON 的originalText完全对应。subtask2 的 Excel 文件里,实体偏移量字段可能与 subtask1 不一致,需要先按文件名或病历 ID 对齐。我一般会写一个统一的数据加载层,把 xlsx 和 txt 统一映射成上面的 JSON 结构,再进入下游处理。

3.2 从 entities 列表生成字级别的 BIOES 序列

不管用什么模型,第一步都是把 JSON 标注转成模型可读的标签序列。中文医学 NER 推荐按单字切分,不按词切分,因为分词错误会直接传导到实体边界上。下面这段代码把一条病历文本和它的 entities 列表转成字级标签:

def build_bioes_tags(text: str, entities: list): tags = ["O"] * len(text) for ent in entities: s, e = ent["start_pos"], ent["end_pos"] label = ent["label_type"] if e - s == 1: tags[s] = "S-" + label else: tags[s] = "B-" + label for i in range(s + 1, e - 1): tags[i] = "I-" + label tags[e - 1] = "E-" + label return tags def load_ccks_json(json_path: str): data = [] for line in open(json_path, "r", encoding="utf-8"): line = line.strip() if not line: continue item = json.loads(line) text = item["originalText"] tags = build_bioes_tags(text, item["entities"]) data.append({"text": text, "tags": tags}) return data

代码逻辑分两层:build_bioes_tags负责把字符区间映射到 BIOES 标签,长度等于 1 的实体直接用S-处理,避免出现B-后无I-的非法序列;load_ccks_json按行读取一个 JSON 列表文件,把每条病历转成{"text": ..., "tags": ...}结构。转换后检查每个样本len(text) == len(tags),否则就是偏移越界或文本与标注不对齐。

用 BIOES 而不是 BIO 的原因是:医学实体特别是手术名称尾部边界信息量大,E-标签能帮解码器收敛得更快。经过转换后统计标签分布,通常O占比超过 80%,实体标签稀少带来严重的类别不平衡问题,后面需要做类别权重调整,或者对O标签降采样。

3.3 训练集验证集划分:按病历而不是按句子切

很多用户直接把所有样本 shuffle 后按比例切开,这会带来严重的数据泄漏:同一份病历的多个句子可能被同时分进训练和验证集,模型在验证集上见到过同一病人的前后文,F1 虚高 2~4 个百分点。正确做法是按病历维度去重再做划分。subtask1 的originalText通常是一段落一段记录,但一个subtask1_training.txt里可能有多条病史记录属于同一个病人。

import random from collections import defaultdict patient2items = defaultdict(list) for item in loaded_ccks_data: patient2items[item["patient_id"]].append(item) patients = list(patient2items.keys()) random.shuffle(patients) split_idx = int(len(patients) * 0.8) train_patients, valid_patients = patients[:split_idx], patients[split_idx:] train_data = [d for p in train_patients for d in patient2items[p]] valid_data = [d for p in valid_patients for d in patient2items[p]]

这里的关键变量是patient_id,如果原始 JSON 没有提供,就用病历文本的前 20 个字符做哈希近似标识。按病历切分后,验证集上的 F1 才是真实泛化能力的度量,这也是 CCKS 2019 基线测试的常见坑之一。

4. 基于 BERT-BiLSTM-CRF 的中文医学 NER 基线实现

4.1 为什么不直接套通用 BERT 做序列标注

中文电子病历有大量缩略语和特殊符号:ERCC1SYNCGAD2-D6这类药学表达式,通用 BERT 的词表里根本没有完整词,会被 WordPiece 拆成ER##CC##1这样的子词。这对标签对齐是个不小的挑战。推荐的做法是加载一个医学领域预训练模型,如果没有就退而求其次使用bert-base-chinese,但必须自己处理子词标签对齐。

字符偏移与 BERT 子词对齐的映射方式是:先对整段文本调用 tokenizer 并开启return_offsets_mapping,然后遍历每个 token 的偏移区间,带回原来的 BIOES 标签。核心代码如下:

from transformers import BertTokenizerFast def encode_with_labels(text, tags, tokenizer, max_len=128): enc = tokenizer( text, padding="max_length", truncation=True, max_length=max_len, return_offsets_mapping=True, return_tensors="pt" ) offset_mapping = enc.pop("offset_mapping")[0] label_ids = [] label2id = {tag: idx for idx, tag in enumerate(sorted(set(tags)))} id2label = {idx: tag for tag, idx in label2id.items()} for (start, end) in offset_mapping: if start == end == 0: label_ids.append(-100) # CLS / SEP / PAD 位置 else: char_idx = start if tags[char_idx] in label2id: label_ids.append(label2id[tags[char_idx]]) else: label_ids.append(-100) return enc, torch.tensor(label_ids)

这段代码的offset_mapping返回每个 token 在原始文本里的字符区间。中文 BERT 按字切分时每个 token 对应一个字符,标签直接取该字符的标签即可;遇到ERCC1被拆成多段时,我们取起始字符的标签,保证标签序列与子词序列一一对应。-100是 PyTorch CrossEntropyLoss 里的默认 ignore index,用来屏蔽 CLS、SEP 和 padding 位置。这里一个常见的错误是给子词尾部继承I-标签,实际上 CRF 层会把它当成非法转移,导致训练崩掉。

BiLSTM 层的作用是在字向量之上再建模上下文约束,CRF 层则是把"B-疾病后面只能跟 I-疾病或 E-疾病"这类约束直接注入解码过程。因此结构是:BERT 输出字向量 → BiLSTM 编码 → Linear 映射到标签空间 → CRF 解码。在训练时使用负对数似然损失,验证时用 Viterbi 解码后转回标签序列。

4.2 类别不平衡下的损失函数与超参数设置

医学文本里O标签占绝大多数,直接训练时模型会倾向把所有 token 判为O。我在训练时会对CE_loss的类别权重做调整:统计训练集里每个标签的出现频次,把权重设为max_count / (class_count ** 0.5),而不是max_count / class_count。前者更温和,不会让低频类被过度加权,导致误报暴涨。以下是可复现的参数建议:

参数推荐值说明
max_seq_len256病历文本普遍较长,128 会截断过多实体
batch_size16显存 12G 以下调成 8
learning_rate3e-5BERT 层用 3e-5,BiLSTM+CRF 用 1e-3
warmup_ratio0.1防止训练早期震荡
epochs5~8早停耐心值设 2
crf_learning_rate1e-3CRF 参数要用独立优化器

训练中要每隔 200 步输出一次验证集精确率和召回率,不要只盯着 loss。一个很有效的技巧是:验证时把 CRF 解码结果和argmax结果做对比,如果两者差异超过 3%,说明 CRF 学到了一些先验约束,模型输出不稳定,需要降低学习率继续微调。

5. 重叠实体评估与跨病历验证的排错技巧

5.1 用overlap字段定位模型的结构性短板

我在实际使用中发现,模型在overlap=0的实体上 F1 轻松到 90% 以上,但overlap=1的实体(如被"手术"实体包围的"疾病和诊断")上,F1 可能直接跌到 70% 以下。如果只报整体 F1,你根本意识不到这个问题。建议按overlap分组分别写出一份指标表:

from seqeval.metrics import classification_report def report_by_overlap(orig_items, pred_tag_lists, overlap_key="overlap"): group_true, group_pred = {"0": [], "1": []}, {"0": [], "1": []} for item, pred_tags in zip(orig_items, pred_tag_lists): key = str(item.get(overlap_key, 0)) group_true[key].append(item["tags"]) group_pred[key].append(pred_tags) for k in group_true: print(f"overlap={k}") print(classification_report(group_true[k], group_pred[k]))

report_by_overlap把实体按overlap分桶,再分别用seqeval生成分类报告。这个过程只需要原始数据里有overlap字段即可运行。运行结果通常显示:重叠实体的错误集中在E-手术B-疾病和诊断的边界上。这时可以考虑用"实体级全局优化"替代 token 级 argmax,即在 Viterbi 解码时加入一个约束——如果输出序列中同时出现两个共享开始偏移的实体,只保留置信度更高的一个。

5.2 跨病历验证:嫌疑文本从哪里找

CCKS 2019 的官方测试集带有答案,适合做最终评测;但在开发验证阶段,我建议从subtask2_unlabeled.txt里抽 20 份无标注病历做人工标注,把它当成一个 mini 测试集。这样可以判断模型的泛化能力是否只存在于官方训练分布内。具体做法是让人工标注者按照readme-subtask1.txt的规范标注这 20 份文本,然后跑一遍classification_report。如果训练集 F1 与 mini 测试集 F1 差距超过 8%,说明过拟合或标注规范没有吃透。

另一个低成本的排错方向是检查解码结果的实体长度分布。CCKS 2019 中文电子病历数据集的文本里存在大量 e 描述型长句子,模型容易从中间截断一个过长的实体。一个简单校验规则是:将预测实体长度超过 15 个字符的实例全部挑出来人工核对,大部分是误报。用这个规则过滤,通常能在不损失召回的前提下把精确率提升 1% 到 2%。最后再回到数据层面重新统计每个类别的混淆矩阵,优先补强影像检查实验室检验两类样本的标注质量,因为这两个类别在原始数据集里的标注一致性明显弱于其他三类。

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

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

秒杀架构设计的 7 个锦囊!

今天我们从 7 个不同的维度,讲讲秒杀系统的架构设计,主要知识点如下:Nginx 前后端分离 CDN 缓存 网关(限流熔断)集群的路由层 Redis(缓存热点数据、分布式锁)MQ 集群业务处理层数据库层(读写…

作者头像 李华
网站建设 2026/9/10 23:37:26

基于SpringBoot的校园二手书交易系统的设计与实现毕业设计项目源码

温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台官方提供的学长联系方式的名片! 温馨提示:本人主页置顶文章(点我)开头有 CSDN 平台…

作者头像 李华