简介:面向自然语言处理与医学信息学研究者的中文电子病历命名实体识别数据集,源自CCKS 2019评测任务,共含1379例真实病历样本。每份样本包含原始文本与实体标注,实体类型覆盖手术、解剖部位、药物、疾病和诊断、影像检查、实验室检验六类,并给出实体类别与起止位置,可直接用于医疗实体识别模型的训练与评测。压缩包共8个文件,以txt训练文本、xlsx测试表格、json标注答案及docx任务说明为主,整体仅1.18MB,轻量易用。目前已有1400余人学习参考,适合入门医疗NLP或参加同类评测的开发者快速获取标准数据,开展模型训练与效果对比。
1. 一个 .rar 背后的中文医学 NLP 起点
CCKS 2019 中文电子病历数据集是中文医疗信息抽取里绕不开的评测语料,整个包以 .rar 形式分发,打开后是一批脱敏电子病历文本和对应的实体、关系标注。很多 NLP 工程师第一次接触医疗领域,第一反应是解压直接跑 BERT,结果在 rar 解压和标注解析上就耗掉半天。这篇按解压、解析、基线和验证的顺序,把从压缩包到可训练样本的完整链路过一遍。内容只依赖压缩包本身的信息,不需要去对接医院系统,也不需要额外申请接口。适合刚入行中文医疗 NLP 的算法工程师,也适合有通用 NER 经验、想了解电子病历标注结构的人做迁移参考。
2. 从 rar 解压到文件完整校验:命令、编码与密码坑
2.1 为什么医疗文本数据经常用 RAR 分发
这类评测数据用 .rar 而不是 .zip 的原因不复杂:RAR 对大量中文小文件的压缩率更高,支持分卷、恢复记录和文件名编码指定。电子病历数据集由几百个 txt 文件组成,文本重复度高,RAR 的压缩算法通常能压到原始体积的三分之一以下。另一个现实原因是出题方当初在 Windows 环境打包,WinRAR 是默认工具,所以“CCKS 2019 中文电子病历数据集.rar”这个文件名才会普遍流传。
拿到包之后不要用 unzip 硬解,先确认系统里有没有支持 RAR 的解压工具。Linux 服务器经常只装了 zip,直接执行 unzip 会报unknown file type,容易误判文件损坏。
2.2 用 unar 解压:最少依赖的命令行方式
在 Linux 服务器上,常见做法是安装 unar 而不是 rar 命令行。unar 基于 libarchive,对非 UTF-8 文件名编码的兼容性比 rar 好,能自动处理中文文件名,不会解出一堆乱码目录。安装完成后,先列出压缩包内容:
lsar "CCKS 2019 中文电子病历数据集.rar"这个命令只列目录不解压,输出里会显示压缩包内的文件路径。看到中文文件名后,再执行真正解压:
unar -e GB18030 -o ./ccks2019_ehr "CCKS 2019 中文电子病历数据集.rar"-e GB18030让 unar 用 GB18030 解码文件名;-o指定输出目录;-f可以在目标目录非空时强制覆盖。如果文件名本来就是 UTF-8 编码,-e参数可以省略,建议先用lsar输出判断,避免解压出乱码目录。
如果服务器上已经装了官方 unrar,也可以用unrar x:
unrar x "CCKS 2019 中文电子病历数据集.rar" ./ccks2019_ehr/注意unrar x保留压缩包内目录结构,unrar e会把所有文件平铺到当前目录。电子病历数据通常按编号文件组织,平铺后同名文件容易互相覆盖,不建议对多文件语料使用e参数。Windows 用户可以用 7-Zip 打开 RAR,但 7-Zip 对 GBK 文件名的处理不如 unar 稳定,解压后如果出现乱码,再换 unar 重试。
| 工具 | 命令示例 | 适用场景 |
|---|---|---|
| unar | unar -e GB18030 -o out file.rar | Linux 下中文文件名兼容最好 |
| unrar | unrar x file.rar | 官方格式支持完整,可做完整性测试 |
| 7-Zip | 右键7-Zip打开压缩包 | Windows 快速查看和部分解压 |
| rar | rar x file.rar | 需要创建分卷或加密包时使用 |
2.3 遇到密码和损坏包:先确认来源,再谈破解
数据集压缩包有时会带密码,尤其从网盘或群聊转发的版本。命令行解压遇到密码会提示Enter password,手动输入即可。如果不知道密码,正确做法是回到原始发布页确认密码是否公开,不要第一时间想到 rar password cracker 或“rar 密码移除”这类工具。
这类工具的原理是逐字尝试候选口令,针对用 AES-256 加密的 RAR5 包,速度取决于 GPU 和字典,普通文件可能要跑几天。更重要的是,评测数据通常不是强加密,设密码只是为了限制传播,真正的授权要联系发布组织。团队协作时,我一般会先检查压缩包是不是分卷格式,比如文件名末尾带.part1.rar,此时必须用rar x而不是unrar x,前者会按顺序读取后续分卷。
解压时报Unexpected end of archive,多半是传输过程中文件被截断。这时先重新下载,不要尝试修复工具:
# 网络差时用 rsync 断点续传,比 wget 重来更实际 rsync -avP user@host:/path/CCKS*.rar .2.4 解压后的完整性验证
解压完成后,一个容易被忽略的步骤是校验文件数量和大小。RAR 包里的文件列表可能与实际解压结果不一致,常见原因是磁盘空间不足或权限问题。先用lsar记录预期数量,再对比实际目录:
lsar "CCKS 2019 中文电子病历数据集.rar" | grep -c "\.txt$" find ./ccks2019_ehr -name "*.txt" | wc -l如果两个数字不一致,优先检查解压过程是否有 I/O 错误。更严格的做法是用unrar t测试压缩包完整性,它会对每个文件做 CRC 校验:
unrar t "CCKS 2019 中文电子病历数据集.rar"输出中OK表示文件校验无误,CRC failed说明源文件损坏。到这里,rar 解压的流程才算完整走通,接下来可以放心解析数据。
3. 解析电子病历标注:从原始 txt 到 BIO 序列
3.1 理解压缩包里的两类文件
解压后常见目录结构大概是这个样子:
ccks2019_ehr/ ├── 原始病历/ │ ├── 0001.txt │ ├── 0002.txt └── 标注数据/ ├── 实体标注/ │ ├── 0001.txt └── 关系标注/ ├── 0001.txt这里的“原始病历”是一段没有标签的出院小结或入院记录,“实体标注”按字符位置记录实体。CCKS 2019 电子病历评测里,实体类型常见为身体部位、症状、疾病、检查、治疗,但具体标签名要解压后看,不要假设所有版本一致。关系标注则记录头实体、尾实体和关系类型,文件结构和实体标注完全不同。
先打开一个文件看前 30 行,比直接写解析器更稳妥:
head -30 ./ccks2019_ehr/标注数据/实体标注/0001.txt这一步能确认分隔符是 Tab、空格还是逗号,有没有空行。BIO 文件通常以空行分隔句子,每行两列:字符和标签。偏移量文件则像下面这样:
急性阑尾炎 病名 5 9意思是原文第 5 到第 9 个字符是“急性阑尾炎”这个实体。这里的偏移量是字符计数还是字节计数,需要验证:中文在 UTF-8 里占三个字节,如果按字节标注,解析前要先编码再定位。
3.2 把偏移量标注转成 BIO 序列
写一个通用转换函数,输入原文和实体列表,输出字符级 BIO 标签。第一版不考虑嵌套实体,只处理非重叠标注:
def offsets_to_bio(text, entities): chars = list(text) tags = ['O'] * len(chars) for start, end, etype in entities: # 防止越界和标签重叠 if start < 0 or end > len(chars): continue if tags[start] != 'O': continue tags[start] = 'B-' + etype for i in range(start + 1, end): tags[i] = 'I-' + etype return list(zip(chars, tags))这个函数有两个设计点:先检查tags[start],遇到重叠实体时保留先出现的,避免B-被后续覆盖成I-导致标签序列不合法。电子病历标注容易在长实体内部出现子实体,比如“左肺上叶”是身体部位,“左肺上叶结节”是症状,两者位置重叠。第一版把这种冲突直接跳过,等后续根据评测要求决定是否支持嵌套。
3.3 BIO 文件解析与标签集对齐
如果是逐行 BIO 文件,解析逻辑更直接:
def load_bio(path): samples = [] chars, tags = [], [] with open(path, encoding='utf-8') as f: for line in f: line = line.rstrip('\n') if not line: if chars: samples.append((chars, tags)) chars, tags = [], [] continue char, tag = line.split('\t') chars.append(char) tags.append(tag) if chars: samples.append((chars, tags)) return samples解析后做两件事:打印所有出现的标签类型,并检查是否只有O、B-、I-三种前缀。
from collections import Counter label_counter = Counter(tag for _, tags in samples for tag in tags) print(label_counter)如果出现S-或E-前缀,说明原数据用的是 BIOES 标签,可以直接保留,也可以在预处理阶段统一映射成 BIO。对 BERT 类模型来说,BIOES 和 BIO 的 F1 差距很小,关键是训练和预测用同一套标签体系。
3.4 统计实体长度和类别分布
拿到标签后,第一版统计能快速发现数据质量问题。实体类别不平衡在电子病历中很常见:症状和检查类实体可能占多数,治疗类很少。统计每个类别的实体数量、平均长度,以及重复实体占比:
def count_entities(samples): cnt = Counter() len_sum = Counter() for chars, tags in samples: current = None start = 0 for i, tag in enumerate(tags): if tag.startswith('B-'): current = tag[2:] start = i elif tag.startswith('I-') and current == tag[2:]: pass else: if current: cnt[current] += 1 len_sum[current] += i - start current = None return {k: (cnt[k], len_sum[k] / cnt[k]) for k in cnt}这段代码在标签从I-跳回O时结算实体。实测中常见的问题是B-后跟了不同类型的I-,比如B-symptom后面跟I-disease,这时统计代码会忽略这段,但模型训练时仍会按标签序列计算损失。最好在预处理阶段就把这种非法序列修正成O,避免模型学到错误边界。
4. 用 BERT 做电子病历 NER 基线:参数设置与长文本策略
4.1 为什么先选 BERT 而不是直接换医疗模型
对中文电子病历数据,第一个可跑通的基线通常用 BERT-base-Chinese。模型在通用中文语料预训练,医学术语覆盖一般,但电子病历句式短、上下文集中,BERT 的字向量能提供稳定起点。相比更大的预训练模型,它的显存占用和推理速度更适合先验证标签设计是否正确。
这不等同于直接跳过词法特征。电子病历里“右肺上叶”这类嵌套器官表达,BERT 能学到部分边界信息,但训练数据太少时依然会切错。所以第二步再换成 RoBERTa-wwm-ext 或医疗领域预训练模型,对比实体级别 F1。先把基线的标签映射准备好:
label_list = sorted({tag for _, tags in samples for tag in tags}) label2id = {tag: i for i, tag in enumerate(label_list)} id2label = {i: tag for tag, i in label2id.items()}标签映射必须保证训练和预测共用一份,否则推理时会出现标签错位,尤其在多进程分布式训练时,每个进程都要加载同一个映射文件。
4.2 把字符级标签对齐到 BERT token
用 Transformers 加载分词器后,把字符标签对齐到模型输入 token。这是电子病历 NER 最容易出 bug 的地方。BERT-base-Chinese 基本按字切分,但词表里仍存在少量多字 token,不能假设全部是单字。
from transformers import AutoTokenizer tokenizer = AutoTokenizer.from_pretrained('bert-base-chinese') def tokenize_and_align(sample): text = ''.join(sample['chars']) char_tags = sample['tags'] encoding = tokenizer( text, truncation=True, max_length=256, return_offsets_mapping=True ) labels = [-100] * len(encoding['input_ids']) for idx, offset in enumerate(encoding['offset_mapping']): if offset == (0, 0): continue char_idx = offset[0] if char_idx < len(char_tags): labels[idx] = label2id[char_tags[char_idx]] encoding['labels'] = labels return encoding-100是 PyTorch 交叉熵损失函数默认忽略的标签,把[CLS]、[SEP]和子词切分产生的额外 token 都设成 -100。这里对多字 token 取了第一个字符的标签,对中文字粒度的 BERT 来说基本够用。如果后续换成带自定义词表的模型,要改成取最后一个字符的标签,或者对 token 内部不同的标签做投票。
4.3 Trainer 训练与超参数选择
用 Trainer 跑微调的代码量最少,但参数要按电子病历短文本的特点调整:
from transformers import AutoModelForTokenClassification, Trainer, TrainingArguments model = AutoModelForTokenClassification.from_pretrained( 'bert-base-chinese', num_labels=len(label2id), id2label=id2label, label2id=label2id ) args = TrainingArguments( output_dir='./ehr_ner', learning_rate=3e-5, per_device_train_batch_size=16, per_device_eval_batch_size=32, gradient_accumulation_steps=2, num_train_epochs=5, evaluation_strategy='epoch', save_strategy='epoch', weight_decay=0.01, warmup_ratio=0.1, logging_steps=50, fp16=True ) trainer = Trainer( model=model, args=args, train_dataset=train_dataset, eval_dataset=eval_dataset, ) trainer.train()电子病历 NER 常用的学习率范围是 2e-5 到 5e-5。数据集只有几千条句子时,5 个 epoch 容易过拟合,建议结合验证集 F1 做早停,或者把 epoch 降到 3。gradient_accumulation_steps在显存不足时等价于增大 batch size,但会拖慢收敛速度,调大后要同步调低学习率。fp16需要 GPU 支持 AMP,否则训练速度反而下降。
评估指标不要用准确率,电子病历中绝大多数 token 是O,准确率会被背景类拉高。要计算实体级别的 Precision、Recall、F1,先把模型输出还原成(实体类型, 起始位置, 结束位置)的片段,再与标注做集合匹配。这部分可以用seqeval库,它直接接受字符串标签列表,不需要手动转换。
4.4 长文本截断策略
电子病历的出院小结经常超过 2000 字,直接截断到 256 会丢掉大量实体。常见做法是滑窗:把原文切成长度 240 的块,相邻块之间保留 80 字重叠,对每块独立预测,最后合并时按重叠区间的实体去重。
def sliding_window(text, window=240, step=160): for start in range(0, len(text), step): end = min(start + window, len(text)) yield text[start:end] if end == len(text): break滑窗重叠的副作用是同一个实体可能在多个窗口出现,合并时按“起始位置优先”的规则保留。更稳妥的方法是预测结果先映射回原文位置,再做重叠区间合并。电子病历中相同实体重复出现很常见,所以不能简单去重,要同时考虑实体类别和具体位置。
5. 数据质量验证与一个增强技巧
5.1 用一致性检查抓标注错误
解析完数据后,先对训练集和验证集做一套交叉一致性检查,能避免模型在脏标注上白跑。最简单的是检查同一实体在相同上下文里是否出现不同标签。电子病历中“肺炎”有时标成疾病,有时标成症状,这种歧义如果比例过高,训练时的梯度会反复横跳。
context_counter = {} for chars, tags in train_samples + eval_samples: text = ''.join(chars) for tag, span in extract_entities(chars, tags): key = (text[max(0, span[0]-2):span[1]+2], span[2]) context_counter.setdefault(key, set()).add(tag)这里extract_entities是把 BIO 标签还原成实体列表的函数。统计后发现同一上下文对应多个类型时,优先按训练集众数修正验证集,而不是反过来。如果 train 和 eval 的风格明显不一致,说明划分方式有问题。
5.2 用 K 折验证看数据分布
电子病历数据集通常按病例编号划分,但一个患者多条记录会同时出现在训练集和验证集里,导致实体重复泄漏。用StratifiedKFold按患者 ID 分组折叠,能看到标注风格差异对 F1 的影响。这个值直接决定你做的模型是真正学到了实体边界,还是记住了训练集里的病例措辞。
5.3 把处理后的语料重新打成 rar
预处理完成后,把转换出的 BIO 数据和标签映射打包备份,方便后续复现。重新压缩时建议保留目录结构,并增加恢复记录:
rar a -ep1 -rr10% -ver -s ccks2019_ehr_processed.rar 原始病历/ 标注数据/-rr10%添加 10% 的恢复记录,能在网络传输损坏时尝试修复;-s开启固实压缩,对大量小体积 txt 能提高压缩率。这样备份下来的包既是可复现实验的原始凭证,也能在换机器时快速迁移,不需要重新解压和解析一遍。对后续接手的人来说,一个带内部目录和标签映射的 rar 包,比零散文件更不容易出错。
本文还有配套的精品资源,点击获取