词性标注与命名实体识别:让机器读懂语言的“语法”与“语义”

好,咱们接着聊。上一章我们讲了分词,把句子切成了一个个词。但光切词还不够,机器还是不知道这些词在句子里扮演什么角色。比如“苹果”这个词,它可以是水果,也可以是公司名。怎么区分?这就轮到词性标注和命名实体识别上场了。

我个人习惯把这两项技术看作搜索引擎的“语法课”和“语义课”。词性标注教机器认识词的词性——名词、动词、形容词;命名实体识别则更进一步,教机器识别出具体的实体——人名、地名、组织名。说白了,一个管“词性”,一个管“实体”。

词性标注原理:给每个词贴个“标签”

词性标注,英文叫 Part-of-Speech Tagging,简称 POS Tagging。它的任务很简单:给句子里的每个词,分配一个词性标签。

举个例子:

输入:我 爱 自然 语言 处理
输出:我/代词 爱/动词 自然/形容词 语言/名词 处理/名词

你可能会问:“这有什么难的?不就是查字典吗?”嗯,还真不是。中文里一词多性的情况太常见了。比如“锁”这个词:

  • “我锁了门” —— 这里的“锁”是动词
  • “这把锁坏了” —— 这里的“锁”是名词

同一个词,在不同语境下词性完全不同。机器怎么判断?

常用的方法有两种:

  1. 基于规则的方法:人工编写词性搭配规则。比如“形容词后面通常跟名词”,“动词前面通常是名词或代词”。这种方法在早期很流行,但维护成本高,而且覆盖不全。
  2. 基于统计的方法:用大量标注好的语料训练模型。最经典的就是隐马尔可夫模型(HMM)和条件随机场(CRF)。

我在项目中遇到过这样一个坑:用 HMM 做词性标注时,模型对生僻词的处理特别差。因为 HMM 假设当前词性只依赖前一个词性,这个“马尔可夫假设”太强了,遇到没见过的新词,模型基本就懵了。

我的经验: 实际项目中,我更推荐用 CRF 替代 HMM。CRF 能考虑整个句子的上下文特征,而不是只依赖前一个状态。虽然训练慢一点,但准确率能提升 3-5 个点。

命名实体识别(NER):从词性到实体

词性标注之后,我们知道了每个词的词性。但搜索引擎真正关心的,是那些“有意义的实体”。比如:

  • “张三” —— 人名
  • “北京” —— 地名
  • “阿里巴巴” —— 组织名
  • “2024年1月1日” —— 时间

命名实体识别(Named Entity Recognition,NER)就是干这个的。它从文本中识别出这些预定义的实体类别。

为什么 NER 对搜索引擎这么重要?我举个例子:

用户搜索“苹果发布会”。如果搜索引擎只做分词,得到的是“苹果/名词 发布会/名词”。但通过 NER,搜索引擎能识别出“苹果”是一个组织名(Apple Inc.),而不是水果。这样搜索结果就能精准地返回苹果公司的发布会信息,而不是水果摊的促销活动。

你想想看,如果没有 NER,搜索引擎得多尴尬。

核心要点: NER 是搜索引擎理解用户意图的关键一步。它把“词”提升到了“实体”的层面,让机器知道用户在谈论什么具体的人、事、物。

CRF 与深度学习 NER 方法

好,接下来我们聊聊具体怎么实现 NER。目前主流的方法有两类:CRF 和深度学习。

条件随机场(CRF)

CRF,全称 Conditional Random Field。它是一种概率图模型,专门用来做序列标注任务。

CRF 的核心思想是:给整个标签序列打分。它不只考虑每个位置的标签,还考虑标签之间的转移关系。比如,在 NER 中,“B-Person”(人名开始)后面通常跟着“I-Person”(人名内部),而不会直接跳到“B-Location”(地点开始)。CRF 能学到这种转移约束。

我曾经用 CRF 做过一个中文 NER 系统,效果还不错。但有个问题:CRF 需要人工设计特征。比如词本身、词性、前后词、词的长度等等。特征工程做得好,效果就好;做得不好,效果就拉胯。

# CRF 特征示例(简化版)
def word2features(sent, i):
    word = sent[i]
    features = {
        'word': word,
        'is_first': i == 0,
        'is_last': i == len(sent) - 1,
        'prefix': word[:2],
        'suffix': word[-2:],
        'prev_word': '' if i == 0 else sent[i-1],
        'next_word': '' if i == len(sent)-1 else sent[i+1],
    }
    return features

这段代码展示了 CRF 中常用的特征。你看,每个特征都是人工定义的。这活儿干起来挺累的,而且不同领域(比如医疗、法律)的特征还不一样。

避坑指南: 我曾经在金融领域的 NER 项目里,花了两周时间调 CRF 特征,结果发现效果还不如一个简单的 BiLSTM-CRF 模型。所以,如果你的数据量足够大(比如 10 万条以上),直接上深度学习吧,别在特征工程上死磕。

深度学习 NER 方法

深度学习的出现,彻底改变了 NER 的玩法。最经典的架构就是 BiLSTM-CRF

这个架构分三层:

  1. Embedding 层:把每个词映射成向量。可以用预训练的词向量(如 Word2Vec、GloVe),也可以用 BERT 这样的预训练模型。
  2. BiLSTM 层:双向 LSTM 捕捉上下文信息。它从左到右、从右到左各跑一遍,然后把两个方向的隐状态拼接起来。这样每个位置的表示都包含了完整的上下文。
  3. CRF 层:还是用 CRF 来做标签解码。深度学习负责提取特征,CRF 负责约束标签序列的合法性。

说白了,深度学习负责“理解语义”,CRF 负责“保证语法”。两者结合,效果杠杠的。

# BiLSTM-CRF 伪代码
class BiLSTM_CRF(nn.Module):
    def __init__(self, vocab_size, tag_size, embedding_dim, hidden_dim):
        self.embedding = nn.Embedding(vocab_size, embedding_dim)
        self.lstm = nn.LSTM(embedding_dim, hidden_dim // 2, bidirectional=True)
        self.fc = nn.Linear(hidden_dim, tag_size)
        self.crf = CRF(tag_size)
    
    def forward(self, x):
        emb = self.embedding(x)
        lstm_out, _ = self.lstm(emb)
        logits = self.fc(lstm_out)
        return self.crf.decode(logits)

这段代码看着简单,但实际训练时有很多细节。比如学习率怎么调?LSTM 的层数设多少?要不要用 dropout?这些都得根据具体数据来试。

我记得有一次做中文 NER,用 BiLSTM-CRF 训练了 50 个 epoch,F1 值卡在 85% 上不去。后来我把词向量换成了 BERT 的预训练向量,F1 直接飙到了 92%。嗯,预训练模型的力量,不服不行。

当前主流:BERT + CRF

现在做 NER,大家基本都直接用 BERT 了。BERT 本身就是一个强大的上下文编码器,它输出的每个 token 的表示已经包含了丰富的语义信息。在 BERT 后面接一个 CRF 层,就是目前最主流的方案。

为什么还要加 CRF?因为 BERT 虽然强,但它对标签之间的转移关系没有显式建模。比如“B-Org”后面不能直接跟“I-Per”,这种约束 CRF 能天然处理。所以 BERT + CRF 是黄金搭档。

方法 优点 缺点 适用场景
CRF 可解释性强,小数据效果好 需要人工特征,泛化能力弱 数据量小(<1万条)
BiLSTM-CRF 自动提取特征,效果稳定 训练慢,对长文本不友好 数据量中等(1万-10万条)
BERT-CRF 效果最好,泛化能力强 计算资源消耗大,推理慢 数据量大(>10万条),对精度要求高

这张表是我在实际项目中总结出来的。选哪种方法,取决于你的数据量和资源。别一上来就上 BERT,如果你的数据只有几千条,CRF 可能更合适。

一个小技巧: 如果你用 BERT-CRF,记得把 BERT 的最后一层输出做一下线性变换,再输入到 CRF。直接拿 BERT 的输出给 CRF,效果反而不好。我试过,大概差 2-3 个点。

好了,这一章的内容就到这里。词性标注和 NER 是搜索引擎理解语言的基石。没有它们,搜索引擎就像个文盲,只知道词,不知道意。下一章我们会聊到句法分析,看看机器是怎么理解句子结构的。