第二章 生物医学文本挖掘基础:命名实体识别(NER)简介

做药物靶点关系抽取,第一步得先看懂文本里哪些词是“实体”。说白了,你得教会计算机认出“BRAF V600E”、“吉非替尼”、“非小细胞肺癌”这些词分别属于基因、药物还是疾病。

命名实体识别(NER)就是干这个的。它的任务是从非结构化文本中定位出命名实体,并分类到预定义的类别。在生物医学领域,我们通常关注基因/蛋白质、化学物/药物、疾病、症状、细胞系、物种、突变等类型。

我个人习惯把NER看作“文本中的地标识别”——你要画地图,总得先标出哪里是山、哪里是河、哪里是城市吧?NER就是给文本画地标的。

2.1 为什么生物医学NER这么难?

你不要拿通用NER(比如识别人名、地名)来对标生物医学NER。基因名字能长成什么样?你想想看:

  • 别名爆炸:同一个基因可能有十几个名字。比如“EGFR”也叫“ErbB-1”、“HER1”、“c-erbB-1”。你写成“EGFR”还是“epidermal growth factor receptor”?模型全得认识。
  • 跨界混淆:“BRAF”是个基因,但“B-RAF”是一种激酶抑制剂?还有“c-KIT”既是基因也是受体。有时候一个词在PubMed摘要里同时指代蛋白质和疾病名。
  • 嵌套实体:“BRAF V600E突变”里,“V600E”是“突变实体”,嵌套在“BRAF”这个基因实体后面。模型需要能捕捉这种层级。
  • 缩写与口语化表达:“5-FU”代表“5-氟尿嘧啶”,“Met”可能指“甲氨蝶呤”或“甲硫氨酸”。上下文决定一切。

我在项目里遇到过最头疼的情况:一篇文献里“Akt”出现了50次,其中48次是“蛋白激酶B”的别名,还有2次是某个化合物缩写。你用规则词典匹配?词典里根本没收录那个化合物缩写。后来我意识到,必须用上下文建模才能搞定这种歧义。

注意: 不要迷信词典匹配。我曾经过度依赖CTD(Comparative Toxicogenomics Database)的词典,结果在非标准缩写词上召回率不到30%。一定要搭配序列标注模型。

2.2 NER的主流方法演化

从最早的规则匹配到现在的大模型微调,NER走过了三个阶段。为了方便对比,我整理了一个表格:

阶段典型方法优点缺点
规则词典基于正则、词典(如UMLS)精准、可解释覆盖差、维护成本高
统计机器学习CRF、HMM、BIO标注泛化能力强需要大量特征工程
深度学习BiLSTM-CRF、BERT-CRF端到端、上下文感知计算资源高,小样本差

现在工业界主流是用预训练语言模型(比如BioBERT、PubMedBERT)做编码,然后接CRF层做序列标注。我自己最常用的是BioBERT + CRF,在化学物和疾病数据集上F1能到90%以上。

2.3 标注格式:BIO/BIESO

NER模型一般用序列标注。每个token被分配一个标签。最常用的是BIO格式:

  • B- (Begin):实体的第一个词
  • I- (Inside):实体的内部词
  • O (Outside):非实体

还有更细的BIESO格式,增加了E(End)和S(Single)标签。对于药物靶点任务,我推荐用BIESO,因为论文摘要里经常出现“表皮生长因子受体”这样的长实体,结束位置准确很重要。

举个例子,句子“吉非替尼抑制EGFR活性”的BIO标注:

吉  B-药物
非  I-药物
替  I-药物
尼  I-药物
抑  O
制  O
E  B-基因
G  I-基因
F  I-基因
R  I-基因
活  O
性  O

为什么会这样标注?因为“吉非替尼”是一个药物实体(4个字),“EGFR”是一个基因实体(4个字母)。注意字母“E”对应汉字“E”是因为英文token化?这里展示中文场景,英文同理。实际上在数据集里,英文单词一般被拆成subword,但为了演示我用了整词。

我的小技巧: 标注时注意一致性。同一个实体在不同句子中出现,类别必须一致。我见过一个数据集里“紫杉醇”有时标B-药物有时标B-化学物,这种歧义会严重降低模型性能。

2.4 实战:快速搭建一个BioBERT NER管道

这里我用HuggingFace Transformers + PyTorch给大家演示。假设我们已经有一个标注好的数据(BIO格式,每行一个token+标签,空行分隔句子)。

from transformers import AutoTokenizer, AutoModelForTokenClassification
import torch

# 加载BioBERT预训练模型(专门在PubMed上训练)
model_name = "dmis-lab/biobert-base-cased-v1.1"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForTokenClassification.from_pretrained(
    model_name, 
    num_labels=9  # 假设有9个类别:B-药物、I-药物、B-基因、I-基因、B-疾病、I-疾病、B-突变、I-突变、O
)

# 单句推理
sentence = "Imatinib inhibits BCR-ABL activity."
inputs = tokenizer(sentence, return_tensors="pt")
outputs = model(**inputs)
logits = outputs.logits
predictions = torch.argmax(logits, dim=-1)[0]

# 解码标签
id2label = {0: "B-药物", 1: "I-药物", 2: "B-基因", 3: "I-基因", 4: "B-疾病", 5: "I-疾病", 6: "B-突变", 7: "I-突变", 8: "O"}
tokens = tokenizer.convert_ids_to_tokens(inputs["input_ids"][0])
for token, pred in zip(tokens, predictions):
    print(f"{token}\t{id2label[pred.item()]}")

输出示例(忽略特殊标记[CLS]和[SEP]):

Imatinib	B-药物
inhibits	O
BC	B-基因
##R	I-基因
-	I-基因
AB	I-基因
##L	I-基因
activity	O
.

注意“BCR-ABL”被拆成了多个subword。这是BERT的WordPiece分词导致的,但没关系,因为我们用BIO标注,模型会自动学习边界。

曾经踩过的坑: 忘记处理subword对齐。如果你的训练数据是用空格分词的,而模型用subword分词,要确保标签正确传播。我写过一个对齐工具,把原始实体的标签映射到subword上,重复父标签给每个子token。

2.5 评价指标:精确率、召回率、F1

NER的评价通常按实体级别(entity-level)计算。也就是看预测的实体span和类别是否完全匹配。常用的:

  • 精确率(Precision):预测出的实体中有多少是正确的。
  • 召回率(Recall):真实标注的实体中有多少被预测出来了。
  • F1分数:两者的调和平均,综合指标。

解释一下“严格匹配”:只有当预测实体的起始位置和类别都与标注完全一致时,才算正确。比如真实是“EGFR”(位置5-8,类别基因),你预测“EGFR”没问题;但如果预测了“EGFR gene”(包含额外token),就算错了。

小经验: 在药物靶点关系抽取的上游,NER的阈值可以设低一些(比如F1达到85%以上就够了),因为关系抽取会利用span特征,一点小错误可以通过下游模型修正。但如果NER准确率低于80%,关系抽取基本没戏。

2.6 本章知识框架

下面这张图概括了生物医学NER的核心流程和关键概念。我画的是从输入句子到输出实体列表的完整路线:

生物医学NER核心流程 输入句子 分词与子词化 预训练编码器(BioBERT) CRF 序列标注层 实体列表(span+类别) 难点: • 别名混淆 • 嵌套实体 • 跨肿瘤命名 • 分词不对齐

嗯,流程大致就是这样。从一句话开始,经过分词、生物医学预训练模型编码,再到CRF解码,最后得到一个个实体。每个环节都可能引入错误,所以实际项目中我会在每步后做质控检查。

2.7 常用工具与数据集

如果你准备动手做生物医学NER,这里列几个我常用的资源:

资源说明适用场景
BioBERT预训练于PubMed,生物医学SOTA通用生物医学NER
PubMedBERT微软发布,更充分预训练药物-靶点相关论文摘要
BC2GM基因/蛋白质标注数据集基因命名实体评估
BC5CDR化学物-疾病关系,含NER标注药物-疾病NER/关系抽取
JNLPBA生物医学实体标注(5类)多类型NER评测
建议: 先拿BC5CDR数据集跑一遍,这个数据集质量高、标注一致。我当年入门NER就是用它练手,跑通后再迁移到自己的药物靶点上。

好了,这一章我们把NER的基础过了一遍。记住:没有准确的NER,根本谈不上关系抽取。后面几章我会带你一步步搭建端到端的药物靶点关系抽取系统。现在,你可以先找一个PubMed摘要,手动标几个实体,感受一下标注的难处——你会理解为什么我们需要模型来做这件事。