电子病历NER实战:基于BERT的命名实体识别源码解析与避坑指南
简介这份源码面向医疗信息化开发者、自然语言处理学习者与科研人员提供一套基于BERT模型的电子病历命名实体识别完整实现用于从病历文本中抽取疾病、药物、治疗手段等关键实体支撑临床决策与医疗数据分析。资源包共38个文件约395KB以21个Python源码文件为核心覆盖模型定义、数据加载、训练与预测全流程另含txt数据与说明、xml配置、md文档、Jupyter Notebook实验笔记及许可证等目录结构清晰便于按模块阅读与二次开发。目前已有320人学习下载。读者可据此掌握BERT在医疗NER任务中的落地方式理解数据预处理、特征提取、模型训练与性能评估的完整链路并借助conlleval评价工具与预训练脚本快速复现实验适合作为课程设计、毕业项目或医疗AI研究的参考基线。1. 电子病历 NER 为什么绕不开 BERT一份能跑通的源码长什么样电子病历里塞满了疾病、药物、手术、检查这类实体但写法极其随意——“2型糖尿病”可能写成“T2DM”“阿司匹林”可能写成“拜阿司匹林”同一个词在不同科室还有不同缩写。传统 CRF 或者 BiLSTM-CRF 靠词向量和人工特征遇到这种缩写、嵌套、长距离依赖就翻车。这份源码把 BERT 预训练语言模型直接接到 NER 任务上用run_classifier.py的思路改造成序列标注配合conlleval做标准评估从train.txt、dev.txt、test.txt三份数据一路走到predict.py输出实体。它适合两类人一是想拿电子病历做科研或课程设计的学生需要一份结构完整、能改能跑的基线二是医疗 NLP 方向的一线开发想快速验证 BERT 在自己数据上的上限。整个工程 39 个文件核心逻辑集中在model.py、utils.py、loader.py、train.py、predict.py这五个 Python 文件里BERT 官方代码作为子模块放在根目录不依赖 pip 安装的 transformers改起来更直接。2. 拆开工程看数据流从loader.py到conlleval的完整链路2.1 数据格式与loader.py的读取逻辑电子病历 NER 的数据通常按 CoNLL 格式组织每行一个 token 加标签空行分隔句子。这份源码的data目录下train.txt、dev.txt、test.txt就是这种结构。loader.py负责把文本转成 BERT 需要的input_ids、input_mask、segment_ids同时把标签映射成 id。关键点在于标签体系常见做法是B-DIS、I-DIS、B-DRU、I-DRU、O源码里用label2id和id2label两个字典维护。我一般会先跑一遍loader.py里的read_data函数确认句子数和标签分布避免后面训练时发现某个实体类别样本极少导致 F1 为 0。# loader.py 中读取 CoNLL 格式的核心逻辑示意 def read_data(file_path): sentences [] labels [] with open(file_path, r, encodingutf-8) as f: tokens [] tags [] for line in f: line line.strip() if not line: # 空行表示句子结束 if tokens: sentences.append(tokens) labels.append(tags) tokens, tags [], [] continue parts line.split() tokens.append(parts[0]) tags.append(parts[-1]) return sentences, labels这段代码的逻辑很直白按空行切句每行取第一个字段为 token、最后一个字段为标签。参数上要注意encodingutf-8电子病历里常有中文和特殊符号用默认编码会直接报UnicodeDecodeError。另外如果数据里用空格分隔而 token 本身含空格需要改成制表符分隔否则标签会对齐错位。2.2model.py里的 BERT 微调结构model.py定义了一个BertNER类继承 BERT 的BertModel在[CLS]对应的输出后面接一个全连接层输出维度等于标签数量。前向传播时取output_layer[:, 1:-1, :]去掉[CLS]和[SEP]再算交叉熵损失。这里有个容易忽略的参数max_seq_length。电子病历句子长度差异大设 128 可能截断长句设 512 又吃显存。我一般先统计训练集 token 长度的 95 分位数再定这个值。源码里默认在train.py的FLAGS里配置改的时候要同步改loader.py里的 padding 逻辑否则 mask 对不上。# model.py 中 NER 头部的关键代码示意 class BertNER(nn.Module): def __init__(self, bert_config, num_labels): super().__init__() self.bert BertModel(bert_config) self.classifier nn.Linear(bert_config.hidden_size, num_labels) self.num_labels num_labels def forward(self, input_ids, input_mask, segment_ids, labelsNone): outputs self.bert(input_ids, attention_maskinput_mask, token_type_idssegment_ids) sequence_output outputs[0] # 去掉 [CLS] 和 [SEP] 对应的位置 sequence_output sequence_output[:, 1:-1, :] logits self.classifier(sequence_output) if labels is not None: loss_fct nn.CrossEntropyLoss() loss loss_fct(logits.view(-1, self.num_labels), labels.view(-1)) return loss, logits return logits参数说明bert_config.hidden_size通常是 768base 版num_labels等于实体标签总数。labels.view(-1)把 batch 和序列维度拉平这是 PyTorch 交叉熵的常规操作。如果显存不够把 batch size 降到 8 或 16同时梯度累积步数调大效果接近大 batch。2.3 训练入口train.py与评估工具conllevaltrain.py是主入口里面用tf.flags或argparse定义了一堆超参learning_rate、num_train_epochs、batch_size、do_train、do_eval。BERT 微调一般用 2e-5 到 5e-5 的学习率太大直接发散太小收敛慢。源码里默认 3e-5我试过在电子病历数据上 2e-5 更稳。训练完在dev.txt上跑评估conlleval脚本会输出 precision、recall、F1按实体类别分别统计。注意conlleval要求输入格式是token predicted_label true_label源码里utils.py有个write_eval_file函数负责拼这个格式如果自己改数据流这一步别漏。# 训练命令示例 python train.py \ --data_dir./data \ --bert_config_file./bert/bert_config.json \ --init_checkpoint./bert/bert_model.ckpt \ --output_dir./output \ --do_traintrue \ --do_evaltrue \ --max_seq_length128 \ --train_batch_size16 \ --learning_rate2e-5 \ --num_train_epochs3参数含义init_checkpoint指向预训练权重没有这个就是随机初始化F1 会低得离谱output_dir存 checkpoint 和评估结果num_train_epochs在电子病历小数据集上 3 到 5 就够再多容易过拟合。跑完看output/eval_results.txt如果某个实体类 F1 明显低于其他先查训练样本里该类数量再查标签是否写错。3. 避坑与排查电子病历 NER 训练中常见的五个翻车点3.1 标签体系不统一导致 F1 虚低现象训练 loss 正常下降但conlleval输出的 F1 只有 0.3 左右且B-DIS和I-DIS的召回率差异巨大。原因训练数据里同一实体在不同文件用了不同标签名比如B-Disease和B-DIS混用label2id把它们当成两个类。解决在loader.py里加一层标签归一化把所有别名映射到统一标签再重新生成label2id。我一般会先跑一个set(labels)把所有标签打印出来肉眼确认没有同义不同名的情况。3.2max_seq_length截断长病历导致实体丢失现象预测时某些长句里的实体完全识别不出来但短句正常。原因max_seq_length设了 128而电子病历里一段病程记录可能 300 多个 token截断后实体落在截断部分。解决统计训练集长度分布把max_seq_length调到 256 或 512同时把 batch size 减半。如果显存实在不够可以用滑动窗口切分长句预测时再合并结果但要注意实体跨窗口的边界处理。3.3 学习率过大导致 loss 震荡不收敛现象训练前几个 step loss 从 2.3 跳到 8.7 再跳回来一直不下降。原因BERT 微调对学习率极其敏感源码默认 3e-5 在某些数据上偏大。解决降到 2e-5 或 1e-5同时加 warmup前 10% 的 step 线性增加学习率。如果还震荡检查init_checkpoint是否加载成功有时候路径写错但代码不报错实际是随机初始化在跑。3.4conlleval格式不对导致评估报错现象训练完跑评估conlleval输出Error: invalid format或者直接空结果。原因utils.py里拼评估文件的顺序是token true_label predicted_label而conlleval默认期望token predicted_label true_label。解决打开conlleval脚本看它读的是哪两列调整write_eval_file里的写入顺序。这个坑很隐蔽因为训练本身不报错只有评估阶段才暴露。3.5 中文 token 被拆成单字导致实体边界模糊现象模型把“阿司匹林肠溶片”识别成“阿司匹林”和“肠溶片”两个实体或者只识别出“阿司匹林”。原因BERT 中文版按字切分tokenization.py里FullTokenizer对中文逐字处理实体边界依赖模型自己学。解决在数据预处理阶段加入词典特征或者换用 BERT-wwm 整词掩码版本。源码里tokenization.py是官方实现改的时候注意do_lower_case参数中文场景设 False。4. 从predict.py到自定义数据把模型用起来的三个关键操作4.1 用predict.py跑单句推理predict.py加载训练好的 checkpoint对输入句子做 tokenize、padding、前向传播再根据id2label还原标签序列。关键参数是--output_dir要指向训练时保存的目录里面得有model.ckpt或pytorch_model.bin。我一般会先用sample_text.txt里的样例跑一遍确认输出格式是token label对再换自己的病历文本。# predict.py 中推理的核心步骤示意 def predict(text, model, tokenizer, id2label, max_seq_length128): tokens tokenizer.tokenize(text) tokens [[CLS]] tokens [[SEP]] input_ids tokenizer.convert_tokens_to_ids(tokens) input_mask [1] * len(input_ids) segment_ids [0] * len(input_ids) # padding while len(input_ids) max_seq_length: input_ids.append(0) input_mask.append(0) segment_ids.append(0) input_ids torch.tensor([input_ids]) input_mask torch.tensor([input_mask]) segment_ids torch.tensor([segment_ids]) with torch.no_grad(): logits model(input_ids, input_mask, segment_ids) preds torch.argmax(logits, dim-1).squeeze().tolist() # 去掉 [CLS] 和 [SEP] 对应的预测 preds preds[1:len(tokens)-1] return list(zip(tokens[1:-1], [id2label[p] for p in preds]))逻辑说明先 tokenize 再加特殊符号padding 到固定长度前向传播后取 argmax。注意preds要截掉[CLS]和[SEP]的位置否则标签和 token 对不齐。如果预测结果全是O先检查 checkpoint 是否加载成功再检查id2label映射是否和训练时一致。4.2 替换成自己的电子病历数据源码自带的data目录是示例数据换成自己的病历需要三步第一把病历文本按 CoNLL 格式标注每行token 标签空行分句第二更新label2id和id2label确保标签集合和标注一致第三重新跑train.py把--data_dir指向新数据目录。标注工具常见的是 brat 或 doccano导出后写个脚本转 CoNLL。我一般会留 10% 做测试集不参与训练只在最后评估用。4.3 用conlleval做类别级评估conlleval的输出不只是总体 F1还有每个类别的 precision、recall、F1。电子病历 NER 里疾病和药物通常 F1 较高手术和检查因为样本少可能偏低。看评估结果时先关注召回率低的类别再查训练样本是否足够。如果某个类别 F1 为 0大概率是标签写错或者样本数为 0。源码里conlleval是 Perl 脚本Linux 和 macOS 直接跑Windows 需要装 Perl 环境或者用 Python 版conlleval.py替代。评估指标含义关注点Precision预测为某类的实体中真正属于该类的比例低 precision 说明误报多Recall真实某类实体中被正确预测的比例低 recall 说明漏报多F1Precision 和 Recall 的调和平均综合指标低于 0.7 需排查Support该类真实实体数量数量少于 50 时 F1 波动大5. 进阶技巧用extract_features.py做实体嵌入与错误分析extract_features.py是 BERT 官方代码里用来提取隐层向量的脚本很多人直接忽略但它对 NER 错误分析非常有用。具体做法把dev.txt里预测错误的句子挑出来用extract_features.py提取每个 token 的最后一层向量然后算错误实体和正确实体之间的余弦相似度。如果相似度很高但标签不同说明模型学到了相近表示但分类边界没学好可以考虑加 CRF 层或者调整标签权重。如果相似度很低说明模型根本没学到该类实体的特征需要检查训练样本是否足够或者 tokenization 是否把关键字符切碎了。# 提取 dev 集特征向量 python extract_features.py \ --input_file./data/dev.txt \ --output_file./output/dev_features.jsonl \ --vocab_file./bert/vocab.txt \ --bert_config_file./bert/bert_config.json \ --init_checkpoint./output/model.ckpt \ --layers-1 \ --max_seq_length128 \ --batch_size8参数--layers-1表示取最后一层也可以取-2或-3看不同层的表示质量。输出是 JSONL 格式每行一个 token 的向量。我一般会写个 Python 脚本读这个文件把预测错误的实体对找出来算平均余弦相似度。如果某个类别的错误实体相似度普遍高于 0.9说明模型区分不开这时候加 CRF 或者换用BERT-wwm往往能提升几个点。另一个技巧是用predict.py的输出做混淆矩阵看哪些类别之间互相误判最多比如“疾病”和“症状”经常混那就需要在标注规范里把边界定义清楚。从那以后我每次跑 NER 实验都会先用extract_features.py抽一遍 dev 集特征把错误样本的向量相似度算出来再决定下一步是调模型还是调数据。这个习惯帮我省了很多盲目调参的时间。希望帮到你。本文还有配套的精品资源点击获取

相关新闻

YashanDB落地实践:避开部署、迁移、备份与性能调优的6个坑

YashanDB落地实践:避开部署、迁移、备份与性能调优的6个坑

YashanDB最近在技术社区里的讨论热度一直在涨,身边陆续有朋友开始做POC,有些团队甚至已经把核心业务跑在上面了。去年我深度参与了一套业务系统的YashanDB落地项目,从版本选型、架构评审开始,到迁移上线和后续的持续调优&#xff…

2026/10/9 8:54:18 阅读更多 →
生产级Coding Agent调优实战:Harness工程从Vibe Coding到可交付

生产级Coding Agent调优实战:Harness工程从Vibe Coding到可交付

1. 从 Vibe Coding 到生产可用:一个 Coding Agent 调优项目的完整复盘Vibe Coding 这个词从去年火到现在,很多人已经用它写了不少小工具、脚本、甚至完整的 Demo 项目。但真正把 Coding Agent 丢进生产环境跑起来的人都知道,Demo 能跑通和线上…

2026/10/9 8:54:18 阅读更多 →
网页右键被禁?从JavaScript事件到浏览器扩展彻底解除限制

网页右键被禁?从JavaScript事件到浏览器扩展彻底解除限制

你打开某个网站,想复制一段特别有用的内容,右键一点,弹出的不是你熟悉的“刷新、检查、另存为”,而是一句冷冰冰的“该页面禁止右键”或者干脆什么反应都没有。再按一下F12,浏览器底部连影子都不出,或者弹个…

2026/10/9 8:53:18 阅读更多 →

最新新闻

t3code 深度解析:Electron + CLI + Homebrew/winget 跨平台工具链实战

t3code 深度解析:Electron + CLI + Homebrew/winget 跨平台工具链实战

1. 从 t3code 这个标题说起:它到底想解决什么问题第一次看到 “t3code” 这个标题,我脑子里蹦出来的第一反应是:这大概率是一个围绕命令行工具链做整合的项目,而且名字里的 “t3” 很可能对应着某种技术栈缩写或者版本代号。结合热…

2026/10/9 9:52:12 阅读更多 →
可靠性工程师:从失效分析到全生命周期质量保障

可靠性工程师:从失效分析到全生命周期质量保障

1. 先把这个岗位说清楚说实话,十年前我刚入行的时候,别人问我是干嘛的,我说做可靠性,十个人有九个会反问:“那是啥?”现在好多了,起码大家知道这个岗位跟产品质量沾边,但理解依然很有…

2026/10/9 9:52:12 阅读更多 →
opencode 工具系统深度解析:从设计到实战集成

opencode 工具系统深度解析:从设计到实战集成

1. 从“工具”这个词说起:opencode 的定位到底特殊在哪聊 opencode 的工具系统之前,得先把一个容易混淆的概念掰扯清楚。很多人第一次接触 opencode,看到“工具”两个字,脑子里第一反应是插件市场里那种装完就多一个按钮的东西。但…

2026/10/9 9:52:12 阅读更多 →
私域团购区域招商实战:平台与供应链联合模式及团长运营指南

私域团购区域招商实战:平台与供应链联合模式及团长运营指南

1. 一场招商大会背后,私域团购正在发生什么变化私域团购这个词,这两年从朋友圈里的零星拼单,一路演变成了一个有着完整上下游的渠道体系。我关注这个领域大概有三年多,从最早的社群接龙、快团团工具,到后来各种区域平台…

2026/10/9 9:52:12 阅读更多 →
改进的多目标差分进化算法在电力系统环境经济调度中的应用(Python代码实现)【电气期刊论文复现】

改进的多目标差分进化算法在电力系统环境经济调度中的应用(Python代码实现)【电气期刊论文复现】

💥💥💞💞欢迎来到本博客❤️❤️💥💥 🏆博主优势:🌞🌞🌞博客内容尽量做到思维缜密,逻辑清晰,为了方便读者。 &#x1f381…

2026/10/9 9:52:12 阅读更多 →
MiFlashi专业刷机工具深度解析:fastboot兼容与小米OEM指令支持

MiFlashi专业刷机工具深度解析:fastboot兼容与小米OEM指令支持

1. 刷机不是“点一下就完事”:为什么小米用户需要真正专业的刷机工具“MiFlashi”这个名字一出现,老米粉心里基本就有数了——它不是那种点开就弹窗、点下一步就报错的“一键傻瓜式”工具。我接触过太多案例:某位A同学想给闲置的小米Note 3刷…

2026/10/9 9:51:10 阅读更多 →

日新闻

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API实战:LocalDate、Date与ZonedDateTime的转换与避坑指南

Java时间API这个话题,隔三差五就会在群里被翻出来讨论一次。上周还有个同事线上处理一个订单超时问题,排查到最后发现是ZonedDateTime序列化后时区丢了,用户在下单当天晚上看到的时间整整差了8个小时。这类问题几乎每个做Java开发的人都遇到过…

2026/10/9 0:00:49 阅读更多 →
EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

EasyTier实践:从NAT穿透到子网代理的异地组网部署与排错

前几个月我手头有好几台机器需要互相访问:办公室台式机、家里 NAS、还有一台云主机。如果只是偶尔传个文件倒还好,问题是工作场景经常要在几处环境之间来回切换,每次都先登录跳板机再层层代理,实在折腾。我先后试过端口映射、自建…

2026/10/9 0:00:49 阅读更多 →
AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent工程实战:从七要素到七个决策点的系统设计指南

AI Agent 这个词在过去一年里被反复提及,但真正动手搭过一套能跑起来的 Agent 系统的人都知道,从"知道它是什么"到"让它稳定干活"之间隔着一整套工程决策。我前后参与过几个 Agent 项目的落地,从最初用现成框架拼装&…

2026/10/9 0:01:50 阅读更多 →

周新闻

KT148A语音芯片外挂8002D功放的工程实践指南

KT148A语音芯片外挂8002D功放的工程实践指南

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

2026/10/8 15:26:32 阅读更多 →
LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

LLC谐振变换器增益公式推导:从FHA等效到完整归一化表达式

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

2026/10/8 15:26:40 阅读更多 →
ARM架构深度解析:从RISC设计理念到交叉编译实战

ARM架构深度解析:从RISC设计理念到交叉编译实战

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

2026/10/8 10:10:36 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

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

2026/10/8 21:13:17 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

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

2026/10/8 15:26:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

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

2026/10/9 6:17:20 阅读更多 →