基于CRF的医疗实体识别:Python+Jupyter实战教程
2026/9/14 3:29:35 网站建设 项目流程

简介:基于Python+Jupyter的医疗实体识别模型项目,面向期末大作业、课程设计或医疗NLP项目开发人群,帮助开发者快速掌握实体识别任务的完整落地流程。项目包含词典构造、语料标注与识别模型,基于词典通过最大匹配算法获取实体位置并标注类型,源码经过严格测试,附完整说明文档,便于直接复现和二次扩展。压缩包共147个文件,大小581.14MB,主要文件包括Jupyter Notebook源码、Python脚本、txt/dic词典与标注语料、训练数据、TensorFlow模型checkpoint以及评估结果文档。词典覆盖疾病、症状、身体部位三类,疾病词典39615条、症状词典7457条、身体部位词典1929条,数据来源包括互联网爬取与ICD10疾病名称,标注语料和评测指标同步提供。已有90人学习下载,适合具备一定Python基础、希望深入医疗实体识别或用于课程实战项目的学习者。

1. 医疗实体识别,为什么在 Python+Jupyter 里做最顺

在病历和检查报告里,“持续胸痛三天”和“心电图提示ST段抬高”这类信息是藏在一句话里的。医疗实体识别的任务,是把“胸痛”“ST段抬高”自动找出来并标成“症状”“检查”。这个任务非常适合作为课程设计,数据规模可控、问题边界清楚、效果能直观看到。

用 Python 加 Jupyter 来做,最直接的好处是每个步骤都能看到中间结果。原始语料读进来什么样、词典匹配错在哪里、标注文件哪一行有问题,都能在 Notebook 里调试。下面从环境搭建开始,把医疗词典、BIO 语料标注、CRF 模型、评估和文档组织一次讲完。

2. 医疗 NER 数据准备:Jupyter 环境、实体定义与词典自动标注

2.1 先装好 Python 并启动 Jupyter Notebook

医疗实体识别依赖的第三方库不算多,核心是 pandas、scikit-learn 和 python-crfsuite。为了不污染系统 Python,我一般会先用 Anaconda 建一个独立环境。python-crfsuite 在 Python 3.10 以上偶尔会遇到编译不起的情况,因此把 Python 固定在 3.9 是省时间的做法。

conda create -n medical_ner python=3.9 -y conda activate medical_ner pip install jupyter pandas scikit-learn python-crfsuite seqeval jupyter notebook

四条命令的作用分别是:创建独立环境;激活环境;安装 Jupyter、Pandas、Scikit-learn、CRF 工具库和实体级评估库 seqeval;启动 Jupyter Notebook 网页版。启动之后终端会打印一条带 token 的 URL,浏览器打开这个地址就是登录入口。如果 8888 端口被占用,可以改成jupyter notebook --port=8889重新指定端口。

Pandas 负责读取原始语料和标注表格,Scikit-learn 用来做数据集切分,python-crfsuite 提供 CRF 训练器,seqeval 在评估阶段计算实体级别的精确率、召回率和 F1。这些库不需要最新版本,安装时如果遇到依赖冲突,多数情况下是环境混用导致,重新建一个干净的 conda 环境通常能解决。

2.2 定义实体类型和 BIO 标注格式

在开始动手之前,必须先确定要识别哪些实体类别。医疗场景常见的类别包括疾病、症状、药物和检查。对期末课程设计,四类已经足够覆盖典型病历;如果原始语料中检查描述较多,可以在后面加一类“体征”,但不要一开始就设计十类,类别过多会加重标注成本和模型分类难度。

BIO 标注是序列标注任务最常用的标签体系,B 表示实体的第一个字,I 表示实体中间的字符,O 表示非实体。例如“肺炎”应该被标成B-DiseaseI-Disease,而“患者”属于背景内容,标O。下面是用在标注文件里的标签约定:

标签含义示例
B-Disease / I-Disease疾病实体开始/内部肺炎、冠心病
B-Symptom / I-Symptom症状实体开始/内部咳嗽、胸痛
B-Drug / I-Drug药物实体开始/内部阿莫西林、硝酸甘油
B-Check / I-Check检查实体开始/内部心电图、血常规
O非实体患者、三天

手工标注时最容易犯的错误是把“阿莫西林胶囊”整体标成药物,实际上“胶囊”只是剂型,不包含在药物实体中;“上呼吸道感染”应该作为一个完整疾病实体,不能拆成“上呼吸道”和“感染”两个实体。BIO 标签的这种连续性要求,直接影响后续 CRF 的转移特征。标注数据建议保存成一行一个句子的格式,字和标签用空格或 Tab 分隔,一句结束后空一行,这种格式可以直接被 python-crfsuite 读取。

2.3 用词典自动预打标,再人工修正

如果直接打开文本开始手标几百句,工作量非常大。常见做法是先准备一份词典,把高频的疾病、症状、药品名放进去,然后写一个自动打标函数生成初步的 BIO 标签,最后在 Jupyter 里逐句核对和修正。词典来源可以是公开的医学词表,也可以从课程资料里手工整理 50 到 100 个关键名词,重点是覆盖训练语料中的高频实体。

# sentence: 原始句子,term_map: 词典,例如 {"咳嗽": "Symptom"} def auto_tag(sentence, term_map): tags = ["O"] * len(sentence) # 长词优先,防止“心房颤动”被拆成“心房”和“颤动” for term in sorted(term_map, key=len, reverse=True): tag_type = term_map[term] start = 0 while True: idx = sentence.find(term, start) if idx == -1: break # 只覆盖未标注区域,避免短词破坏长词结果 if tags[idx] == "O": tags[idx] = "B-" + tag_type for pos in range(idx + 1, idx + len(term)): tags[pos] = "I-" + tag_type start = idx + 1 return list(zip(list(sentence), tags))

代码的核心是先按词条长度从长到短排序,再逐条调用find定位词条在句子里的位置。长词优先能避免“心房颤动”被先匹配成“心房”和“颤动”两个实体。对句子的每个字符,程序会维护一个标签数组,初始全为 O,命中词条后把起始字改成B-实体类型,词条内部的其他字改成I-实体类型。返回结果是一个字符和标签的配对列表,方便后续打印检查。

在 Jupyter 单元格里,可以这样批量看效果:

sentences = [ "患者反复咳嗽两周,伴发热", "用法用量:阿莫西林胶囊,一次0.5g", ] term_map = { "咳嗽": "Symptom", "发热": "Symptom", "阿莫西林": "Drug", } for sent in sentences: print(sent) print(auto_tag(sent, term_map))

这段代码主要用来验证词典覆盖情况。如果句子里的“胸痛”没有被打上标签,说明词典里没有这个词,需要补充词条;如果“阿莫西林胶囊”因为词条“阿莫西林”而只标好“阿莫西林”,剩下的“胶囊”会保持 O,这是预期的。自动标注的结果不会一次完美,但已经能省掉一半以上的手工工作量。检查无误后,把标注结果转成(字符, 标签)的序列并保存成 JSON 或 TSV,作为 CRF 模型的训练语料。

这里要注意词典匹配和分词是两个独立操作。不要先做分词再标注,因为分词一旦切错,实体边界就再也找不回来。这个方案里直接在原始句子上做字符级匹配,是最稳妥的。

3. 从词典基线到 CRF 模型:特征模板与训练代码

3.1 为什么课程设计里 CRF 比深度学习更稳

词典匹配能解决已经见过的高频词,但碰到“患者出现干咳,尤其夜间明显”中的“干咳”时,如果词典没有收录,模型就无法识别。深度学习模型如 BiLSTM-CRF 或 BERT 可以应付这类未登录词,但需要足够多的标注数据和较长的训练时间。对于只有几百句语料的期末项目,CRF 是更合适的折中方案。

CRF 会学习两个层面的规律:一是当前字符的特征是否指向某个实体标签,二是相邻标签之间的转移是否合法。例如模型能学到B-Disease后面更可能接I-Disease而不是I-Symptom,也能通过“发热待查”这类上下文推测出“发热”是症状。CRF 训练速度快、特征可解释,答辩时把特征模板讲清楚,评分老师很容易理解。

3.2 把字符转成模型需要的特征字典

CRF 不能直接读汉字,需要把每个字符转成一个字典形式的特征向量。特征的设计直接决定模型上限,常用的包括当前字符、前后各两个字符、是否数字、是否英文,以及词典先验位置标记。词典先验来自前面自动标注时的匹配结果:1 表示该字符是某个词典词的起始字,2 表示在词典词内部,0 表示没有命中。

特征名取值示例作用
char“胸”当前字符本身
prev_char“痛”左侧上下文
next_char“三”右侧上下文
is_digitFalse区分数字信息,如“0.5g”
is_alphaFalse区分英文缩写,如“CT”
flag1词典先验开始/内部/无

下面这一段生成每个字符的特征:

def get_term_flags(sentence, term_map): flags = [0] * len(sentence) for term in sorted(term_map, key=len, reverse=True): start = 0 while True: idx = sentence.find(term, start) if idx == -1: break flags[idx] = 1 for pos in range(idx + 1, idx + len(term)): flags[pos] = 2 start = idx + 1 return flags def sent2features(sentence, term_flags): features = [] for i, ch in enumerate(sentence): features.append({ "char": ch, "prev_char": sentence[i - 1] if i > 0 else "<BOS>", "next_char": sentence[i + 1] if i < len(sentence) - 1 else "<EOS>", "prev2_char": sentence[i - 2] if i > 1 else "<BOS>", "next2_char": sentence[i + 2] if i < len(sentence) - 2 else "<EOS>", "is_digit": ch.isdigit(), "is_alpha": ch.isalpha(), "flag": term_flags[i], }) return features

get_term_flags和前面的自动标注代码类似,但它只保留“是否命中词典”这个信息,不直接生成标签。sent2features返回一个列表,列表长度等于句子字符数,列表里每个元素是一个字典。字典的键是特征名,值是字符串或布尔值,python-crfsuite 原生支持这种格式,不需要做 one-hot。

如果后续发现模型总把带数字的检查项识别错误,可以再加一个has_hyphen特征,判断当前字符左右是否出现“-”或“/”。特征模板不是越大越好,课程设计里 6 到 8 个特征足够,特征超过 20 个反而容易过拟合小语料。

3.3 训练 CRF 模型并保存参数

使用sklearn_crfsuite接口,它封装了 python-crfsuite,调用方式和 scikit-learn 基本一致。模型输入是每个句子的特征列表X,标签是同样长度的标签列表y。训练前先用train_test_split切分,留出 20% 作为测试集。

from sklearn_crfsuite import CRF crf = CRF( algorithm="lbfgs", c1=0.1, c2=0.1, max_iterations=100, all_possible_transitions=True, ) crf.fit(X_train, y_train) import joblib joblib.dump(crf, "models/medical_ner_crf.joblib")

参数里algorithm指定优化器,lbfgs是拟牛顿法,收敛稳定,比较适合中小规模语料;c1c2是 L1 与 L2 正则化系数,默认都从 0.1 开始调整,数值偏大时模型更保守,适合数据量少的场景;max_iterations控制最大迭代次数,几百句语料 100 次通常足够。all_possible_transitions=True会显式训练所有标签转移概率,避免出现B-Drug直接跳到I-Symptom的非法输出。

fit之前,务必检查X_train中每个样本的长度是否和y_train对应样本的长度一致。CRF 对序列长度非常敏感,长度不一致时会直接抛 ValueError。常见做法是提前加一个断言:

for x_seq, y_seq in zip(X_train, y_train): assert len(x_seq) == len(y_seq)

这样的断言能快速暴露标注文件里句子和标签错位的问题。训练结束后用 joblib 保存整个模型,Jupyter 重启后只需要加载模型进行预测,不用重新训练。

4. 评估指标、排错方法与 Jupyter 实体高亮

4.1 用实体级 P/R/F1 而不是字符准确率

很多课程设计会用 accuracy 评价模型,但在医疗实体识别里,一段文本 70% 以上都是 O 标签,即使把所有实体都标成 O,准确率也能很高,这个数字没有意义。应该用实体级别的精确率、召回率和 F1 来评估。精确率关心的是“找出来的有多少是对的”,召回率关心的是“该找出来的找回来多少”,F1 是两者的调和平均。

指标关注点课程设计里的作用
Precision误报看模型是否把“胸痛”三个字多标了一段
Recall漏报看模型是否漏掉了“硝酸甘油”
F1综合期末验收只看这个也可以
Accuracy全字符正确比例不推荐,O 多时虚高

使用seqeval直接计算这些指标:

from seqeval.metrics import precision_score, recall_score, f1_score y_true = y_test y_pred = crf.predict(X_test) print("Precision:", precision_score(y_true, y_pred)) print("Recall:", recall_score(y_true, y_pred)) print("F1:", f1_score(y_true, y_pred))

注意seqeval的参数必须是二维列表,每个元素对应一个句子的标签序列,而不能是单个字符串。报告里最好带上classification_report输出各类别的具体数值,方便写进实验报告。

4.2 三个高频排错点

第一个高频问题是 python-crfsuite 安装失败。Windows 上如果用pip install python-crfsuite遇到报错,通常是因为缺少 C++ 编译工具或 Python 版本过高。最简单的办法是用 conda 安装:conda install -c conda-forge python-crfsuite。第二个问题是标签序列不合法,比如一个“肺炎”被标成B-Disease后,中间夹了一个I-Symptom。这个问题大多来自词典自动标注时的条目覆盖,可以用下面的函数扫描:

def check_label_transition(seq): prev = "O" for lbl in seq: if prev.startswith("B-") and lbl.startswith("I-"): if prev[2:] != lbl[2:]: print("非法转移:", prev, lbl) prev = lbl

第三个问题在特征提取阶段:特征数量多但样本少,模型对训练集过度自信,跑到测试集上 F1 反而下降。遇到这种情况,把c2从 0.1 调到 0.5,或者减少窗口宽度,通常能改善。

4.3 在 Jupyter 里把预测结果渲染成彩色实体

课程设计演示时,光看标签列表不够直观。Jupyter 支持直接输出 HTML,可以用一个简单的函数给句子里的实体上色。

from IPython.display import HTML, display COLORS = { "Disease": "#ffcccc", "Symptom": "#fff2cc", "Drug": "#d9ead3", "Check": "#c9daf8", } def render_entities(sentence, tags): res = [] for ch, tag in zip(sentence, tags): if tag == "O": res.append(ch) else: ent = tag.split("-")[1] color = COLORS.get(ent, "#dddddd") res.append('<span style="background:%s">%s</span>' % (color, ch)) display(HTML("".join(res)))

这个函数按字符给实体加背景色,运行后句子会以“彩色标注”的形式出现在 Notebook 中。它是纯 HTML 渲染,不依赖 matplotlib,也不会因为中文字体出错。如果希望整个实体词连在一起显示为一块颜色,可以把同一个实体的字符串先合并再上色,代码逻辑类似,但要注意合并时不能把相邻的同类型实体合错。

5. 把项目做成可交付的课程设计:源码结构、文档与演示

5.1 一个会让老师多看两眼的源码目录

课程设计常见的扣分点是把所有代码堆在一个 .ipynb 文件里,运行顺序一变就崩。稍微组织一下目录,项目专业度会明显提升。

medical_ner/ ├── data/ # 原始病历与标注文件 ├── dicts/ # 疾病、症状、药物词条 ├── src/ # 数据预处理、特征、模型模块 │ ├── data_prepare.py │ ├── features.py │ ├── model.py │ └── evaluate.py ├── notebooks/ # 演示用 Notebook ├── docs/ # 课程设计报告 └── README.md

src/data_prepare.py 负责读取原始文本并生成标注文件,features.py 负责生成特征字典,model.py 封装训练与加载,evaluate.py 输出评估报告。notebooks 里只做流程演示,调用这些模块而不是重复实现。

5.2 文档里必须写清楚的四个部分

课程设计文档不需要太长,但这四块内容必须说清楚。第一,问题定义和实体类型,让读者知道你要解决什么问题、为什么选这四个实体类别。第二,数据与标注流程,说明原始语料来源、词典从哪里来、BIO 标签如何生成。第三,模型方案与参数,明确写出用了 CRF、特征有哪些、c1/c2 怎么调,以及和词典基线的对比。第四,实验结果与不足,放上实体级 P/R/F1,然后写一句模型对未见过的药品名识别有限,下一步可以补充词典或换预训练模型。这比把代码贴一大段更接近一份合格的项目文档。

5.3 最后一招:用一段新病历做预测演示

答辩时最好准备一个独立的演示单元格,从磁盘加载模型,输入一句训练时没见过的病历,输出高亮结果。这样评委能直接看到模型在工作。

crf = joblib.load("models/medical_ner_crf.joblib") sentence = "患者因持续胸痛三天入院,含服硝酸甘油后症状缓解" flags = get_term_flags(sentence, term_map) X_demo = [sent2features(sentence, flags)] pred_tags = crf.predict(X_demo)[0] render_entities(sentence, pred_tags)

如果“硝酸甘油”不在词典里,而训练语料里存在类似“硝酸异山梨酯”这样的药物词条,CRF 可能通过上下文和字符特征把它识别为药物片段。预测不理想时,就在词典中补充该词并修正标注,重新训练一轮即可。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询