自然语言处理入门到进阶:一个医疗软件公司Python开发的血泪实战笔记

轻舟开发记
2025-12-18 04:42
阅读 2317

上周五晚上十点半,我还在工位上死磕一个 NLP 模型——产品经理又在 Slack 上 @ 我:“能不能让这个病历摘要再智能一点?现在输出跟 AI 胡说八道似的。”
我盯着终端里那行 Loss: 2.34 (still not converging...),差点一口老血喷在 Mac 键盘上。

我是某家医疗行业 SaaS 公司的 Python 后端开发,日常除了写 CRUD 接口、怼数据库慢查询,还得兼职搞点 NLP 相关的活儿。毕竟我们做的是电子病历智能分析系统,光靠正则表达式匹配“发烧”“咳嗽”早就糊弄不过去了。更惨的是,最近我还边上班边刷 LeetCode 准备跳槽,每天早上 8 点准时到公司,就为了抢在晨会前跑完一轮模型训练(别问,问就是 GPU 资源被测试团队占着跑 UI 自动化)。

今天这篇水文,就想聊聊我在医疗 NLP 这个坑里摸爬滚打一年多的心得——从连 BERT 是啥都不知道,到现在能搭起一个还算靠谱的临床实体识别 pipeline。如果你也正准备面试 NLP 岗,或者被老板逼着搞“AI+医疗”,希望这篇能帮你少走点弯路。


为啥非得搞 NLP?因为病历真的没法用 SQL 查

我们系统每天要处理上万份医生手写的门诊记录。这些文本长得像这样:

“患者主诉反复咳嗽伴低热1周,夜间加重,无咯血。既往有支气管哮喘史,否认结核接触史。”

产品经理最初的想法很朴素:“能不能把‘咳嗽’‘低热’‘哮喘’这些词抽出来,存成结构化字段?”
听起来简单,对吧?但现实是:

  • 医生缩写千奇百怪(“COPD”、“DM2”、“CHF”)
  • 同义词满天飞(“发烧”=“发热”=“体温升高”)
  • 否定句防不胜防(“无胸痛”≠“有胸痛”)

传统规则方法很快崩了。去年双11期间,系统误把“否认高血压”识别成阳性,触发了一堆错误预警,运维半夜打电话骂我:“你这模型是不是喝假酒了?”

于是,NLP 正式提上日程。


入门:别一上来就啃 Transformer,先搞定基础工具链

我一开始直接去 GitHub 扒了个 BERT 微调脚本,结果本地跑三天都没收敛。后来才明白:NLP 不是换个预训练模型就能起飞的魔法

必读的几本书 & 资源

类型 名称 我的评价
入门书 《Speech and Language Processing》(Jurafsky) 经典!虽然厚,但第8章讲 NER 讲得贼清楚
实战书 《Natural Language Processing in Action》 代码多,适合边看边敲
在线课 Stanford CS224n 免费!但数学有点硬核,建议配合食用李沐的《动手学深度学习》
中文资源 哈工大 LTP / THUNLP 的博客 对中文医疗文本特别友好

血泪教训:别迷信英文教材里的例子。英文分词天然有空格,中文得先过 tokenizer 这一关。我们早期用 jieba 切词,结果把“心肌梗死”切成“心肌/梗死”,实体直接废了。后来换成 LAC(百度开源的中文词法分析工具),准确率立马涨了 15%。


进阶:从 spaCy 到 BERT,再到部署上线

搞定基础分词后,我们开始搞命名实体识别(NER)。目标很明确:从病历中抽 疾病、症状、检查、药物 四类实体。

第一版:spaCy + 规则兜底

import spacy
from spacy.training import Example

# 加载中文模型(需自行训练)
nlp = spacy.load("zh_core_medical_sm")

# 添加否定规则
def negate_entities(doc):
    for ent in doc.ents:
        if "否认" in [token.text for token in ent.sent]:
            ent._.negated = True  # 自定义属性
    return doc

nlp.add_pipe(negate_entities, after="ner")

效果还行,但遇到新药名(比如刚上市的“瑞德西韦”)就抓瞎。而且 spaCy 的 CRF 解码在长病历上速度感人——单份病历平均 800ms,根本扛不住线上 QPS。

第二版:BERT + BiLSTM-CRF

面试题来了:为什么不用纯 BERT 做 NER?
答:BERT 输出的是 token-level logits,直接 argmax 容易出现“B-Disease, I-Symptom”这种非法标签序列。加一层 CRF 能保证标签转移合法,还能引入上下文约束。

我们用 transformers + pytorch-crf 搞了个轻量 pipeline:

from transformers import BertTokenizer, BertModel
from torchcrf import CRF

class MedicalNER(nn.Module):
    def __init__(self, num_labels):
        self.bert = BertModel.from_pretrained('bert-base-chinese')
        self.classifier = nn.Linear(768, num_labels)
        self.crf = CRF(num_labels, batch_first=True)
    
    def forward(self, input_ids, labels=None):
        outputs = self.bert(input_ids)[0]
        emissions = self.classifier(outputs)
        if labels is not None:
            loss = -self.crf(emissions, labels, mask=input_ids.ne(0))
            return loss
        return self.crf.decode(emissions)

关键调优点

  • 学习率:BERT 层 2e-5,CRF 层 1e-3(别全用一样!)
  • Batch Size:医疗文本长,GPU 显存有限,最后定在 16
  • 数据增强:用同义词替换(比如“发烧”→“发热”)缓解标注数据少的问题

训练完在内部测试集上 F1 达到 0.89,上线后误报率降了 60%。最爽的是,现在系统能识别“服用阿司匹林后出现皮疹”中的因果关系了(虽然这用了额外的关系抽取模块,此处略过)。


面试高频题 & 我踩过的坑

最近刷题时发现,NLP 面试题越来越偏工程实践。分享几个真实被问到的:

  1. “如何处理 OOV(Out-of-Vocabulary)问题?”
    → 我们的解法:子词切分(WordPiece)+ 医学术语词典强制加入 tokenizer 词汇表。

  2. “线上模型延迟高怎么办?”
    → 量化!用 onnxruntime 把 BERT 转成 ONNX 格式,推理速度提升 3 倍。顺便吐槽:运维一开始死活不肯给 NLP 服务单独配 GPU,说“你们又不是搞 CV 的”,直到我把 P99 延迟从 2s 降到 300ms...

  3. “怎么评估模型效果?准确率够吗?”
    → 医疗场景下,召回率比准确率重要!漏掉一个“过敏史”可能出人命。所以我们设定了不同的阈值,并人工抽检 500 份病历做黄金标准。


最后几句大实话

NLP 在医疗领域远没到“开箱即用”的程度。就算你 fine-tune 了百亿参数的大模型,面对“患者诉间断性脐周隐痛,饭后为著”这种描述,照样可能翻车。

我的建议:

  • 别追求 SOTA:在业务场景里,一个稳定、可解释、易维护的模型,比刷榜模型有价值得多。
  • 数据 > 模型:花 80% 时间清洗和标注数据,20% 调参。我们团队现在有个专职医学编辑帮我们标数据,效果拔群。
  • 善用开源:别重复造轮子。腾讯的 Medical-BERT、阿里云的 Qwen-Med 都可以直接拿来微调。

写这篇文章的时候,我的模型终于收敛了(Loss: 0.42 🎉)。虽然不知道下个月跳槽面试能不能用上这些经验,但至少——产品经理今晚不会再@我了。

共勉。

评论 0

最热最新
暂无评论
轻舟开发记Lv.1
0
影响力
0
文章
0
粉丝