ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

基于CRF的医疗实体识别:Python+Jupyter实战教程

基于CRF的医疗实体识别:Python+Jupyter实战教程 简介基于PythonJupyter的医疗实体识别模型项目面向期末大作业、课程设计或医疗NLP项目开发人群帮助开发者快速掌握实体识别任务的完整落地流程。项目包含词典构造、语料标注与识别模型基于词典通过最大匹配算法获取实体位置并标注类型源码经过严格测试附完整说明文档便于直接复现和二次扩展。压缩包共147个文件大小581.14MB主要文件包括Jupyter Notebook源码、Python脚本、txt/dic词典与标注语料、训练数据、TensorFlow模型checkpoint以及评估结果文档。词典覆盖疾病、症状、身体部位三类疾病词典39615条、症状词典7457条、身体部位词典1929条数据来源包括互联网爬取与ICD10疾病名称标注语料和评测指标同步提供。已有90人学习下载适合具备一定Python基础、希望深入医疗实体识别或用于课程实战项目的学习者。1. 医疗实体识别为什么在 PythonJupyter 里做最顺在病历和检查报告里“持续胸痛三天”和“心电图提示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 python3.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 --port8889重新指定端口。Pandas 负责读取原始语料和标注表格Scikit-learn 用来做数据集切分python-crfsuite 提供 CRF 训练器seqeval 在评估阶段计算实体级别的精确率、召回率和 F1。这些库不需要最新版本安装时如果遇到依赖冲突多数情况下是环境混用导致重新建一个干净的 conda 环境通常能解决。2.2 定义实体类型和 BIO 标注格式在开始动手之前必须先确定要识别哪些实体类别。医疗场景常见的类别包括疾病、症状、药物和检查。对期末课程设计四类已经足够覆盖典型病历如果原始语料中检查描述较多可以在后面加一类“体征”但不要一开始就设计十类类别过多会加重标注成本和模型分类难度。BIO 标注是序列标注任务最常用的标签体系B 表示实体的第一个字I 表示实体中间的字符O 表示非实体。例如“肺炎”应该被标成B-Disease和I-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, keylen, reverseTrue): 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, keylen, reverseTrue): 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 featuresget_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( algorithmlbfgs, c10.1, c20.1, max_iterations100, all_possible_transitionsTrue, ) crf.fit(X_train, y_train) import joblib joblib.dump(crf, models/medical_ner_crf.joblib)参数里algorithm指定优化器lbfgs是拟牛顿法收敛稳定比较适合中小规模语料c1和c2是 L1 与 L2 正则化系数默认都从 0.1 开始调整数值偏大时模型更保守适合数据量少的场景max_iterations控制最大迭代次数几百句语料 100 次通常足够。all_possible_transitionsTrue会显式训练所有标签转移概率避免出现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 stylebackground:%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.mdsrc/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 可能通过上下文和字符特征把它识别为药物片段。预测不理想时就在词典中补充该词并修正标注重新训练一轮即可。本文还有配套的精品资源点击获取
返回列表