想象你是一座城市的管理员:街道上挤满了建筑,有人住的宅子(人名)、地标广场(地名)、政府大院(机构名)。城市要对外发布地图,你必须先给这些“重要建筑”贴上分类门牌——这一步,在自然语言处理里就叫命名实体识别(NER)。 最早,你派出一队拿着手电筒的巡逻员,从左到右再从右到左扫视街道,照着“贴门牌手册”逐栋标注,这就是 BiLSTM-CRF 的朴素智慧。后来,档案馆请来一位阅遍全城史料的老先生,他只需看一眼街景就能判断建筑类别,这是 BERT 预训练带来的质变。最后,无人机升空全局俯瞰,无论大院套着小院,一次扫描全部框出,这是 GlobalPointer 对嵌套实体的终极解法。 这篇文章会沿着这条“门牌进化史”展开:从标注规则讲起,一路拆解三种方法的模型结构、关键公式与工程落地教训,帮你建立 NER 的完整认知地图。
掌握 NER 三大方法,等于拿到信息抽取与知识图谱构建的入场券——这是搜索、推荐、问答系统的地基能力,也是 NLP 算法工程师面试和实战的必考点。
可视化演示
本节配有交互动效(进入视口后自动加载,约 3KB)。
BiLSTM-CRF架构 BiLSTM提取双向上下文特征 CRF建模标签转移约束 P = BiLSTM(x) + CRF transitions 经典NER架构
任务概述
城市管理局要绘制一张全市功能地图,第一步不是画路,而是给“值得标注的建筑”贴上门牌:住人的宅子、对外开放的广场、挂牌办公的大院。在自然语言处理里,这个“贴门牌”的动作就是命名实体识别(Named Entity Recognition,NER)——从一段自然语言中定位出具有特定意义的实体片段,并判断它的类别。
这套动作的价值贯穿整个 AI 应用链:知识图谱要靠 NER 从百科和公告中抽出头实体,搜索引擎要靠它高亮“人名/地名”提高点击率,客服系统要靠它识别用户话里的“产品名/订单号”才能做下一步意图处理。可以说,凡是需要从文本里“捞关键信息”的系统,第一站几乎都是 NER。
从技术脉络看,NER 的建模目标可以写成:在给定输入句子 X 的条件下,找到最合理的标签序列 Y。
所以“贴门牌”这件事要回答三个问题:哪些建筑值得贴(实体定义)、门牌怎么编码(标注体系)、用什么工具去贴(三大方法)。下面我们就从“门牌编码规则”说起。
- 输入句子 X把一句话切成一串词,X 就是这串词的集合。
- 标签序列 Y每个词对应一个门牌标签,Y 就是这些标签组成的有序序列。
- 取最大概率者\arg\max 表示在所有候选 Y 中选出条件概率最高的那个,作为最终标注结果。
标注体系
门牌不能乱编,全市得有一套统一编码。最早的规则叫 BIO:B(Begin)代表一栋建筑的第一块门牌,I(Inside)代表建筑内部的延续门牌,O(Outside)代表路边普通路灯,不值得贴。比如“张三去北京大学读书”,切分成字后,标签可以是:张/B、三/I、去/O、北/B、京/I、大/I、学/I、读/O、书/O。
后来大家觉得还差点意思:单字实体(比如“京”单独指北京时)用 B 开头再直接接 O,边界感不够强。于是有了 BIOES 扩展:E(End)表示建筑最后一块门牌,S(Single)表示一整栋只有一块门牌的小屋。BIOES 对实体边界更敏感,在不少数据集上能小幅提升 F1。
有了编码规则,还得有“尺子”衡量贴得好不好。常用的尺子是精确率 P、召回率 R 和 F1。
规则和尺子都备齐了,接下来要看工具。第一代工具,是拿着手电筒的巡逻队——BiLSTM-CRF。
- 精确率 P在所有贴了门牌的建筑中,真正贴对的比例。
- 召回率 R在真正值得贴的建筑中,被成功贴到的比例。
- F1 调和平均当 P 和 R 都高时 F1 才高,避免模型“只贴少数稳的”或“乱贴一堆”。
BiLSTM编码
第一代巡逻队员的装备是一支“双向手电筒”。他从街头走到巷尾,记下每栋建筑左边的邻居;再从巷尾走回街头,记下右边的邻居。两条路线的记忆汇合在一起,就构成了这栋建筑最完整的“邻里画像”——这就是双向长短期记忆网络(BiLSTM)的直觉。
LSTM 之所以能“记住”,靠的是三扇门:遗忘门决定丢掉哪些旧记忆,输入门决定吸纳哪些新信息,输出门决定释放哪些记忆给下一个时刻。但单向 LSTM 有个天然缺陷:它只能看到左边的历史。于是人们把正向 LSTM 和反向 LSTM 的输出拼接起来,让每一个词的位置同时拥有左右两侧的上下文视野。
巡逻队记住了左右街景,但他贴门牌时有个毛病:可能前一栋贴“人名”,下一栋直接贴“地名内部”,闹出笑话。要让门牌贴得合规,巡逻队需要一本城管手册——CRF 层。
- 正向记忆从句子开头扫到第 t 个词时,LSTM 积累的“左侧上下文”。
- 反向记忆从句子末尾扫到第 t 个词时,LSTM 积累的“右侧上下文”。
- 向量拼接分号表示把两个向量首尾相接,得到同时携带左右信息的上下文向量 h_t。
CRF解码
城管手册上写着一条铁律:门牌标签不能乱跳。“人名建筑”可以连排几间(B-PER 后跟 I-PER),但 B-PER 后面突然冒出 I-ORG(地名内部门牌)就是违规。这种“前一个标签到后一个标签是否合法”的规矩,就是条件随机场(CRF)的转移矩阵。
光有 BiLSTM 时,每个词的门牌是独立判断的——它会给“北”打一个高分给 B-ORG,给“京”打一个高分给 I-ORG,但完全不管两个标签连起来是否合理。CRF 出场后,把“发射分数”(BiLSTM 说该贴什么)和“转移分数”(城管手册说能不能这么接)加起来,构成整条路径的总分:
巡逻队加手册的组合,在中小规模任务上稳扎稳打。但手电筒的视野终究有限——它只能沿着街道一维地走,看不到建筑的“全貌气质”。于是城市请来了一位读过全城史料的老先生:BERT。
- 发射分数E 是 BiLSTM 输出矩阵中的元素,表示第 t 个词贴上标签 y_t 的“底分”。
- 转移分数T 是 CRF 学到的转移矩阵,表示从标签 y_t 跳到 y_{t+1} 的合法程度,非法跳转会被训练成很大的负数。
- 累加与寻优把发射和转移逐项相加得到路径总分,再用维特比算法一次性动态规划出全局最优路径。
BERT预训练
老先生年轻时通读过全城所有街区的档案:青砖的烧制年代、牌坊的雕刻风格、门墩的形状……他只需要扫一眼街景,就能判断这栋建筑是什么类别。这种“见识”,来自日复一日的海量阅读——对应到技术里,就是预训练。
BERT 的做法分两步。第一步是预训练:在 GB 级的无标注文本上,随机遮住 15% 的词,让模型根据上下文猜被遮住的词是什么。这个任务叫掩码语言模型(Masked Language Model,MLM)。第二步是微调:把预训练好的参数作为起点,在 NER 的标注数据上再训几个 epoch,输出层换成标签分类头。于是模型从“通用语言理解专家”变成了“贴门牌专家”。
老先生见多识广,但遇到“大院套小屋”的嵌套建筑(“北京大学”里藏着“北京”),老先生也得顺着门牌一行行找,费时费力。于是新一代工具登场:无人机全局扫描——GlobalPointer。
- 被遮住的词w_i 是被随机掩码的目标词,模型看不到它。
- 上下文条件除了 w_i 之外的整句其余词,都作为预测的依据。
- 条件概率模型输出每个候选词的概率,训练目标就是让正确词的概率最高。
GlobalPointer
无人机升到高空,俯瞰整片街区。它不再沿着街道一栋栋走,而是直接问:任意两个点 i 和 j 围出来的那片区域,会不会是一座院子?如果是,就整体框出来。这种“任意区间直接配对”的思路,就是全局指针(GlobalPointer)的核心。
传统序列标注给每个词分配一个标签,天然只能表达“连续的一段”,遇到嵌套实体就手足无措:同一个“北”字,既属于地名“北京”,又属于机构名“北京大学”,一个标签根本装不下。GlobalPointer 换了个建模空间:对每一类实体,构造一个 n×n 的打分矩阵,矩阵中 (i, j) 位置的值,代表“从第 i 个词到第 j 个词构成该类实体的置信度”。
无人机能同时圈出大院子和小院子,嵌套实体不再是难题。但它的代价也很明显:要比较 n² 个区间,计算和显存压力都不小。于是我们来到最后一关——怎么给真实业务选工具。
- 编码向量h_i 和 h_j 是经过编码器(如 BERT)得到的第 i 词、第 j 词表示。
- 投影变换W_q 把起点向量转成“查询”,W_k 把终点向量转成“键”,学习该从什么角度匹配头尾。
- 点积打分内积越大,说明这两个位置越像一对合法的“头尾”;对每个实体类别各做一套打分。
- 缩放稳定除以 \sqrt{d} 防止维度高导致点积过大,让打分分布更平稳。
工程选型
不是每个城市都值得出动无人机。小镇街区派巡逻队加手册就够用,大城市才请老先生和无人机。选型不是“越新越好”,而是数据量、算力、时延、嵌套需求四者的权衡。
工程经验大致可以这样划:标注数据只有几千句、用 CPU 部署、要求毫秒级响应,BiLSTM-CRF 是最务实的选择——它轻量、稳定、易调试;如果数据量充足、有 GPU、追求榜单效果,BERT 或 BERT+CRF 是主流;如果业务文本里嵌套实体密集(地址解析、生物医学命名、法律条款),GlobalPointer 的优势就体现出来了。三种方法的计算复杂度对比很能说明问题:
回到我们的城市:巡逻队稳、老先生博学、无人机全知,但没有谁绝对最优。先看清自家街区的规模、预算和“建筑分布”,再决定出动哪支队伍。这,就是 NER 三大方法演进留给我们的最朴素心法。
- 句子长度 n输入序列包含的词数,是复杂度分析的规模变量。
- 线性复杂度BiLSTM-CRF 对每个词只做常数次计算,整体随 n 线性增长。
- 平方复杂度GlobalPointer 要打分所有 (i, j) 区间对,共约 n²/2 个候选,随 n 平方增长。
AI 应用场景
知识图谱构建中的实体抽取
阶段:数据准备与信息抽取
解决的问题:知识图谱的上游需要从海量百科、研报、公告中抽出“实体-关系-属性”,人工标注成本极高。传统规则模板只能覆盖固定措辞,换个说法就失效。用 BERT+CRF 微调后,模型能理解上下文语义,在长句和复杂表达中准确抽取出人名、机构名、地名,把非结构化文本转成结构化三元组的原料。
具体案例:某金融知识图谱项目,从 10 万份上市公司公告中抽取“公司名、股东名、高管名”。初期用规则命中率仅 65%,改用 BERT+CRF 微调后 F1 提升到 88%,新公告直接进入推理管线自动入库。
地址解析与嵌套实体识别
阶段:模型选型与推理上线
解决的问题:中文地址天然嵌套严重:“北京市海淀区中关村大街”中,“北京市”“海淀区”“中关村大街”层层嵌套。用传统序列标注只能抽出一段最外层,“海淀区”会丢失。GlobalPointer 用全局区间打分,一次性框出每层地址要素,再配合层级规则组装成标准行政区划。
具体案例:某电商物流系统每天处理 50 万条用户填写的不规范地址,使用 GlobalPointer 后嵌套地址要素召回率从 71% 提升至 93%,“省市区街”四级拆分准确率显著改善,送错率下降。
轻量级客服系统的实体识别
阶段:端侧推理与在线服务
解决的问题:在线客服机器人需要实时从用户消息中识别“产品名、订单号、业务类型”,但并发高、响应时间苛刻,且 GPU 资源有限。BiLSTM-CRF 在 CPU 上单条推理只需几毫秒,参数规模小,适合嵌入实时流水线。
具体案例:某银行智能客服将 BiLSTM-CRF 部署在 CPU 集群上,识别“转账/挂失/查余额”等意图下的关键实体,平均耗时 3ms,支撑日均百万次请求,成为高并发场景的轻量基座。
Python 演示
# -*- coding: utf-8 -*-"""从零演示 NER 三大方法核心: BIO 标注, CRF 维特比解码, GlobalPointer 区间打分""" SENTENCE = "张三去北京大学读书"WORDS = list(SENTENCE) # 简化: 按字切分TAGS = ["B", "I", "O"] # BIO 标注体系 def build_entities(tags): """把 BIO 标签序列还原成实体片段""" entities = [] i = 0 while i < len(tags): if tags[i] == "B": j = i while j + 1 < len(tags) and tags[j + 1] == "I": j += 1 entities.append((i, j, "".join(WORDS[i:j + 1]))) i = j + 1 else: i += 1 return entities # 模拟 BiLSTM 输出的发射分数: 每行是 [B, I, O] 的得分EMIT = [ [0.9, 0.6, 0.1], # 张 [0.7, 0.9, 0.2], # 三 [0.1, 0.2, 0.9], # 去 [0.9, 0.5, 0.1], # 北 [0.5, 0.9, 0.2], # 京 [0.2, 0.9, 0.3], # 大 [0.1, 0.8, 0.3], # 学 [0.1, 0.2, 0.9], # 读 [0.2, 0.1, 0.9], # 书] # CRF 转移矩阵: TRANS[prev][cur], 用大负数惩罚非法转移TRANS = [ # prev = B, cur: B / I / O [-10.0, 1.0, 0.5], # prev = I, cur: B / I / O [-10.0, 1.0, 0.5], # prev = O, cur: B / I / O [1.0, -10.0, 0.5],] def viterbi(emit, trans): """维特比解码: 动态规划寻找全局最优标签路径""" n = len(emit) m = len(emit[0]) dp = [[-1e9] * m for _ in range(n)] back = [[-1] * m for _ in range(n)] for j in range(m): dp[0][j] = emit[0][j] for i in range(1, n): for j in range(m): for k in range(m): score = dp[i - 1][k] + trans[k][j] + emit[i][j] if score > dp[i][j]: dp[i][j] = score back[i][j] = k tags = [0] * n tags[-1] = max(range(m), key=lambda j: dp[-1][j]) for i in range(n - 1, 0, -1): tags[i - 1] = back[i][tags[i]] return tags def global_pointer(emit, threshold=1.3): """GlobalPointer 简化演示: 任意 (i, j) 区间打分, 支持嵌套""" n = len(emit) candidates = {} for i in range(n): for j in range(i, n): score = emit[i][0] + emit[j][1] # 头部取 B 分, 尾部取 I 分 if score > threshold: candidates[(i, j)] = score return candidates if __name__ == "__main__": tags = viterbi(EMIT, TRANS) tag_names = [TAGS[t] for t in tags] print("维特比标签:", tag_names) print("抽取实体:", build_entities(tag_names)) print("\nGlobalPointer 候选区间:") cands = global_pointer(EMIT) for (i, j), s in sorted(cands.items(), key=lambda x: -x[1]): print(" ", (i, j), "".join(WORDS[i:j + 1]), "得分", round(s, 2))
WORDS = list(SENTENCE)简化任务,按字切分句子,字符序列就是模型要标注的最小单元。build_entities(tags)把 B/I/O 标签流还原成连续的实体片段,对应“贴门牌后汇总建筑清单”。EMIT手工构造的发射分数矩阵,模拟 BiLSTM 对每个字的标签打分。TRANS转移矩阵用-10.0惩罚非法转移,比如 B 后面不能再跟 B。dp[i - 1][k] + trans[k][j] + emit[i][j]维特比的核心递推式:上一位置最优分 + 转移分 + 当下发射分。tags[-1] = max(range(m), key=lambda j: dp[-1][j])从最后一个位置选总分配最大的标签,作为回溯起点。for (i, j), s in sorted(cands.items(), key=lambda x: -x[1])按区间得分从高到低输出 GlobalPointer 捕捉到的所有候选实体,包括嵌套的“北京”与“北京大学”。
常见误区
误区刚接触 BiLSTM-CRF 时,以为 CRF 是某个深度网络,甚至觉得 NER 的语义理解全靠 CRF 完成,BiLSTM 只是配角。
正解CRF 是概率图模型,本质是一张“标签转移表”。它负责的是标签之间的合法性约束,不负责理解句子。真正的语义特征提取是 BiLSTM 的功劳:双向手电筒把上下文记忆扫出来,CRF 只是最后把关“门牌不能乱接”的城管手册。
为何常见:模型名字里 CRF 写在最后,作为输出层,新手容易把“最终输出者”误当成“主要贡献者”。
误区觉得预训练模型无所不能,BERT 微调后每个词独立过 Softmax 就能拿到完美标签,不需要 CRF 或任何规则。
正解BERT 输出层的 softmax 是逐词独立的,依然可能产出 B-PER 后面直接跟 I-ORG 这种非法标签。工程上常见的做法是 BERT 后接 CRF,或至少做解码约束。预训练提升的是“语义理解”,不是“标签合规”。
为何常见:把预训练的强大语义能力误当成端到端的全部,忽略了序列标注固有的结构性问题。
误区遇到“北京大学”和“北京”同时出现,第一反应是先抽外层机构,再在内部抽地名,觉得嵌套只能靠两遍模型。
正解GlobalPointer 把建模空间从“词到标签”换成“任意区间到实体类别”,每个 (i, j) 组合独立打分,大区间和小区间可以同时命中,天然支持嵌套,不需要分层。传统序列标注才是受限于一维结构的那个。
为何常见:因为大多数人接触 NER 都是从 BIO 序列标注入手的,思维被“一个词一个标签”的框架锁住了。
误区看到 GlobalPointer 能解嵌套、BERT 效果好,就认为 BiLSTM-CRF 是过时产物,任何项目都该上最新模型。
正解工程选型看的是约束条件:标注数据只有几千条、CPU 部署、延迟敏感时,BiLSTM-CRF 以极小代价稳定运行;大模型和全局区间打分带来的显存与耗时开销可能远超收益。没有普适最优,只有场景匹配。
为何常见:技术社区的“新模型屠榜”叙事容易让人忽略资源成本和业务约束。
误区以为预训练模型自带 NER 能力,给十几条例子微调一下就能上线,甚至尝试零样本直接抽实体。
正解预训练学的是通用语言知识,但“哪些词是实体、实体边界在哪、类别怎么划分”是任务特有的。领域差距越大(比如法律、生物、方言),需要的标注数据越多。微调的本质是把通用知识“校准”到贴门牌任务上,校准总得有样本。
为何常见:“预训练”这个词太有迷惑性,让人误以为模型已经做过了 NER 训练。
面试题
Q1. NER 的任务本质是什么?
- A.对文本做情感倾向打分
- B.识别实体并对其分类标注← 正确答案
- C.自动生成文章内容摘要
- D.把一种语言翻译成另一种
答案:B。NER 的目标是从文本中定位实体片段并判断类别,对应“给重要建筑贴分类门牌”。A 是情感分析,C 是文本摘要,D 是机器翻译,三者都是 NLP 的不同任务。
Q2. BIO 标注中 I 标签的含义是什么?
- A.实体外部区域的标记
- B.实体内部延续的标记← 正确答案
- C.实体开始位置的标记
- D.标点符号类别的标记
答案:B。I 是 Inside,表示实体内部的延续门牌,比如“张三”里的“三”贴 I-PER。A 描述的是 O,C 描述的是 B,D 与实体标注无关。
Q3. BiLSTM 为什么能捕捉“上下文信息”?
- A.它把句子随机打乱顺序输入
- B.它同时从正反两个方向扫描句子← 正确答案
- C.它只根据当前词自身判断标签
- D.它把整个句子压缩成一个向量
答案:B。BiLSTM 由正向和反向两个 LSTM 组成,分别从左到右、从右到左扫描,再把两个方向的记忆拼接起来。因此每个词同时携带“左边邻居”和“右边邻居”的信息,这就是双向上下文。
Q4. 在 BiLSTM-CRF 中,CRF 层的主要作用是什么?
- A.提取句子的深层语义特征
- B.约束标签间转移的合法性← 正确答案
- C.减少模型总体的参数量
- D.加快模型收敛的迭代速度
答案:B。CRF 通过转移矩阵约束相邻标签的组合,比如 B-PER 后不能直接接 I-ORG。深层语义特征由 BiLSTM 负责;CRF 既不减少参数量,也不加速收敛,它管的是“门牌接得合不合规”。
Q5. BERT 做 NER 的核心优势是什么?
- A.模型整体的参数量最少
- B.预训练带来双向上下文语义← 正确答案
- C.任务不需要任何标注数据
- D.推理速度比 LSTM 快很多
答案:B。BERT 在海量文本上做掩码语言模型预训练,学到深厚的双向上下文理解,微调后对实体边界的判断更准。它参数量并不少,依然需要标注数据微调,推理也远慢于 LSTM。
Q6. GlobalPointer 建模的核心空间是什么?
- A.词到标签的单步映射空间
- B.所有头尾位置的配对空间← 正确答案
- C.相邻标签的转移概率空间
- D.句法树结构的递归嵌套空间
答案:B。GlobalPointer 对任意起点 i 和终点 j 组合打分,把候选实体表示为“区间”,生活在 (i, j) 的二维配对空间里。A 是序列标注,C 是 CRF 的转移矩阵,D 是句法分析,均不符合。
Q7. 在 BiLSTM-CRF 的维特比解码中,back 数组记录的是什么?
- A.每个词的所有候选标签得分
- B.每个位置最优路径的前驱标签← 正确答案
- C.BiLSTM 的隐藏状态向量序列
- D.转移矩阵的合法转移集合
答案:B。维特比算法在动态规划时,每到一个位置都要记下“当前最优分是从上一个位置的哪个标签转移来的”,这个回溯指针就是 back 数组。解码最后一步从末尾往前倒推,靠它拼出整条全局最优标签路径。
Q8. 关于 BIO 标注,下列说法正确的是?
- A.B 表示一个实体的开始位置← 正确答案
- B.I 只能出现在 B 或 I 之后← 正确答案
- C.O 表示实体内部的一个词
- D.B 后面可以直接接 O 收尾← 正确答案
答案:ABD。B 标记实体起点,I 表示实体延续,因此 I 前面只可能是 B 或 I;单字实体用 B 开头后直接接 O 收尾也是合法的。O 表示实体外部,不是实体内部,所以 C 错误。
Q9. BERT 微调做 NER 时,哪些做法是合理的?
- A.全量参数参与下游任务微调← 正确答案
- B.输出层外接 CRF 加强约束← 正确答案
- C.冻结全部参数不进行任何更新
- D.用掩码语言模型替代标注输出
答案:AB。全量微调让预训练参数适配 NER 任务,是主流做法;外接 CRF 能约束标签转移,在工程中很常见。完全冻结参数则无法有效适配任务;掩码语言模型是预训练任务,不是 NER 的输出形式,不能直接替代标注。
Q10. GlobalPointer 相比传统 BiLSTM-CRF 的特点有哪些?
- A.一次建模所有实体区间← 正确答案
- B.天然支持嵌套结构实体← 正确答案
- C.区间计算复杂度为线性级别
- D.不需要使用预训练编码器
答案:AB。GlobalPointer 对任意 (i, j) 区间打分,所有候选一次建模,因此大区间套小区间的嵌套实体能同时命中。但它要对约 n² 个区间打分,复杂度是平方级而非线性;它同样依赖 BERT 等编码器提供词向量,所以 C、D 错误。