文本预处理:分词、词性标注、命名实体识别基础

拿到一篇PubMed摘要的原始文本,第一件事是什么?我个人习惯先做文本预处理。说白了,就是把机器看不懂的自然语言,拆成它更容易处理的结构。你想想看,一条药物靶点的句子长什么样——比如“Dasatinib inhibits BCR-ABL tyrosine kinase”——如果我们不做处理,让程序直接去匹配“BCR-ABL”,很容易因为空格、大小写或者连字符而出错。

4.1 分词:把句子切成最小单元

分词是NLP最基础的一步。英文句子按空格和标点切基本上就够用了,但PubMed里大量出现复合词和特殊命名,比如“T-cell”、“2.5-fold”、“IL-2Rα”。直接按空格切会丢信息。

我在处理药物靶点项目时,就踩过一个坑:dasatinib 后面接了个括号的写法,比如“dasatinib (Sprycel)”,简单切分把“dasatinib”和“(Sprycel)”分开了,导致后续匹配不到标准的药物名。后来我改用 NLTK 的 word_tokenize 加上 正则预处理,效果才稳下来。

实用经验: 如果只是对 PubMed 摘要做批量处理,直接用 spaCy 的 nlp.tokenizer 就行,它内置了英文缩略词、复合词的分词规则,省去自己写正则的时间。

下面是一个简单的分词示例:

import spacy

nlp = spacy.load("en_core_web_sm")
text = "Dasatinib inhibits BCR-ABL tyrosine kinase (TKI) in CML patients."
doc = nlp(text)
tokens = [token.text for token in doc]
print(tokens)
# ['Dasatinib', 'inhibits', 'BCR-ABL', 'tyrosine', 'kinase', '(', 'TKI', ')', 'in', 'CML', 'patients', '.']

注意看,BCR-ABL 被保留成了一个整体,没有拆成 BCR 和 ABL。这种处理对识别药物靶点名称很关键。

4.2 词性标注:给每个词打上语法标签

分词之后,我们需要知道每个词的词性——是名词、动词还是形容词。为什么要做这个?因为药物靶点通常是名词短语,动词往往描述作用关系(inhibits, activates)。

举个例子:“dasatinib inhibits BCR-ABL”——我们更关心“BCR-ABL”这个名词对象,而不是“inhibits”这个动词。如果你只拿所有词去匹配,会把“inhibits”当成靶点候选,那就会闹笑话。

我在一个实体抽取脚本里,曾经因为没做词性过滤,结果把所有带 - 的连字符名词都当成了蛋白名,抽到一堆“drug-resistant”这种其实是形容词的结果。后来加上了词性标签过滤,只保留 名词(NN/NNS/NNP/NNPS),精度马上从 67% 涨到了 81%。

核心观点: 词性标注不是必须步骤,但在基于规则的初步筛选中,它能过滤掉大量噪声,让后续的命名实体识别轻松不少。

用 spaCy 做词性标注非常方便:

for token in doc:
    print(f"{token.text:15}  {token.pos_:6}  {token.dep_}")
# Dasatinib       PROPN   nsubj
# inhibits        VERB    ROOT
# BCR-ABL         PROPN   dobj
# tyrosine        NOUN    compound
# kinase          NOUN    attr
# (               PUNCT   punct
# TKI             PROPN   appos
# )               PUNCT   punct
# in              ADP     prep
# CML             PROPN   pobj
# patients        NOUN    appos
# .               PUNCT   punct

看到没?BCR-ABL 被标注为 PROPN(专有名词),inhibits 是 VERB。我们可以很轻松地把动词过滤掉,只捕捉 PROPN + NOUN 组成的短语。

4.3 命名实体识别(NER)基础

NER 才是药物靶点抽取的主战场。它的目标是从文本中识别出 药物名、靶点/基因名、疾病名 等实体。PubMed 摘要里,最常见的实体类型就是这些。

我试过两种方式:

  • 基于规则 + 词典: 从 DrugBank、UniProt 下载标准词典,用 Aho-Corasick 算法匹配。优点是快,缺点是词典覆盖不全(变体名、缩写、拼写错误抓不住)。
  • 基于预训练模型: 用 BioBERT 或 PubMedBERT,开箱即用就能识别绝大多数生物实体。缺点是需要 GPU,推理时间比词典长。

我个人建议在课程项目的早期阶段,先用 spaCy 的预训练 NER 模型(en_ner_bc5cdr_md 或 en_ner_jnlpba_md)做个快速原型。等整体流程跑通之后,再考虑要不要换成 BERT 模型提高召回。

避坑指南: 我曾经直接用通用英文 NER(en_core_web_sm)去抽药物靶点,结果它把“EGFR”识别成了组织器官类型,把“imatinib”识别成了一个人的名字(PER)。所以一定得用生物医学领域的专用 NER 模型。

下面是用 en_ner_bc5cdr_md 抽取的例子:

import spacy

nlp_bc5cdr = spacy.load("en_ner_bc5cdr_md")
text = "Gefitinib selectively inhibits EGFR tyrosine kinase in NSCLC cells."
doc = nlp_bc5cdr(text)
for ent in doc.ents:
    print(f"{ent.text:20}  {ent.label_}")
# Gefitinib             CHEMICAL
# EGFR                  GENE
# NSCLC                 DISEASE

完美:药物(Gefitinib)、靶点基因(EGFR)、疾病(NSCLC)全部正确识别。如果你手头有 PubMed 全文或摘要,用这个模型就能拿到 85% 以上的基础精度。

4.4 本章知识体系关系

我把三者之间的关系用一张图串起来:

文本预处理流程示意 原始 PubMed 文本 分词 (Tokenization) 词性标注 (POS Tagging) 命名实体识别 (NER) 药物 + 靶点 + 疾病 (结构化实体列表) 关键产出 分词 → 索引词序列 词性标注 → 过滤动词 NER → 药品/靶点/疾病

这张图展示了从原始文本到结构化实体列表的流动。分词是入口,词性标注是过滤器,NER 是最终输出模块。三者缺一不可,顺序也不能颠倒。

注意: 很多初学者会跳过分词或词性标注,直接跑 NER 模型。结果发现模型识别出的实体虽然对,但上下文关系抓不住——比如“drug A inhibits target B”和“drug B inhibits target A”两句话,如果没有分好句子,模型可能会把跨句的词混在一起。

4.5 实操:小规模流水线测试

我建议你下载 10 篇乳腺癌相关的 PubMed 摘要,用下面的脚本跑一遍,看看效果:

import spacy

nlp = spacy.load("en_ner_bc5cdr_md")

texts = [
    "Trastuzumab binds to HER2 receptor and inhibits downstream signaling.",
    "Tamoxifen acts as an antagonist of estrogen receptor (ER) in breast tissue.",
    "Lapatinib is a dual inhibitor of EGFR and HER2 tyrosine kinases.",
]

for t in texts:
    doc = nlp(t)
    drugs = [e.text for e in doc.ents if e.label_ == "CHEMICAL"]
    genes = [e.text for e in doc.ents if e.label_ == "GENE"]
    print(f"药物: {drugs}\n靶点: {genes}\n")

你会看到类似以下的输出:

药物: ['Trastuzumab']
靶点: ['HER2']

药物: ['Tamoxifen', 'estrogen receptor']
靶点: ['ER']

药物: ['Lapatinib']
靶点: ['EGFR', 'HER2']

嗯,其实 estrogen receptor 被识别成了 CHEMICAL,有点奇怪——但整体上主要药物和靶点都被抓住了。这种小规模测试能让你快速理解预处理的效果,也能帮你发现模型在特定领域的偏差。