2021年秋招,我在准备NLP算法岗的面试题时,几乎每轮都会碰到同一个话题:NER。命名实体识别作为NLP基础任务模型里的典型代表,它考察的其实不只是背几个概念,而是一个人能否把数据、标签、模型、评估和部署串成一条完整链路。这篇文章把我当时整理的NER知识体系、手写思路和踩坑记录完整放出来,适合正在备战秋招、或者刚接触序列标注任务的读者。哪怕你只是临时补课,这里也有能直接上手的框架。
1. 秋招面试里的NER:为什么它总被拿来当考题
1.1 实体识别是信息抽取的地基
先对齐概念。NER要从一段文本中抽取出人名、地名、机构名、时间、金额等实体,同时判断类别。比如,文本“王小甲在云澜市的蓝山科技担任算法工程师”里,“王小甲”是人名,“云澜市”是地名,“蓝山科技”是机构名。听起来简单,但落地应用非常广,搜索、问答、知识图谱、舆情分析、文本结构化,第一层几乎都要靠NER先找到关键对象。实体抽错了,后面所有信息抽取、关系分类都是空中楼阁。
这也就是为什么在NLP基础任务模型里,NER和分词、词性标注一样,是最能看出基本功的题。秋招面试官不会指望你上来就做出多贵的模型,但如果你连实体边界、标签体系都讲不清楚,后面基本没法聊。尤其是很多业务场景里,NER不是最后一环,只是前置引擎,它做得稳不稳直接决定下游效果。
1.2 面试现场的高频问法
我自己感受,秋招里NER题常以三种面目出现。
第一种是概念题:“说说NER的难点。”如果只回“边界模糊、嵌套实体、样本不均衡”,面试官通常不会太满意。我会往下拆:中文没有天然空格,分词结果可能把实体切碎,所以很多方案选择基于字标注;实体边界本身也有歧义,比如“云澜市第一中学”里,到底把“第一中学”标成机构,还是把整个“云澜市第一中学”标成机构,这需要标注规范定义;实体还有嵌套,例如“某大学电子系”里既有大学又有院系,一套扁平BIO标签表达不了。把这些串成自己的理解,比背十条目录强很多。
第二种是方案设计题:“给你一批数据,怎么做实体识别系统?”这种题没有标准答案,但你要把数据清洗、标注、模型选型、后处理、评估全串起来。对方想看的是你如何权衡成本和收益,而不是无脑上BERT。比如说,数据只有几百条,那第一反应不应该是重训大模型,而是先看看规则和词典能不能解决一部分;如果数据有几万条,再考虑BiLSTM-CRF或BERT-CRF。
第三种是代码题,比如“写一个函数把BIO标签解码成实体列表”“手写维特比”。这种题考察的是你能不能把概念落到代码。后面我会给一个可以直接用的版本。
1.3 一次模拟面试暴露的问题
我有一个很深的记忆。当时模拟面,对方抛出“实体类别有十种,其中一类占样本80%,另一类只占1%,怎么做?”我的第一反应是“给少样本类别加权重”,对方接着问“那你会不会让模型过度拟合少数类?”那一瞬间我意识到,面试官要的不是一个操作,而是一套诊断流程:先看类别分布,看错误类型,看是召回不够还是精确率不够,然后才决定要用重采样、加权损失还是阈值调整。
这个复盘帮我养成了习惯:任何模型改动前,先明确“当前指标差在哪一类实体、哪一种边界情况”,而不是直接换一个新的网络结构。后来在实际项目里,这个习惯帮我省了很多无用功。
2. 建模前先想清楚:从跑通到能用,方案怎么选
2.1 把NER问题定义为序列标注
在写模型之前,要先决定输出表示。最主流的方式是为每个token打一个标签。中文场景更常用“字”而不是“词”,因为分词工具本身可能把实体边界切破。假设预分词结果是“蓝山/科技”,而真实实体是“蓝山科技”,那实体就被切开了,后面再还原特别费劲。基于字标注可以绕开这个问题。
标签体系我常聊两个:
- BIO:B表示实体第一个字,I表示实体内部其他字,O表示非实体。类别跟在B/I后面。
- BIOES:在BIO基础上增加E和S,分别表示实体结尾和单字实体。
举例,文本“王小甲在云澜市”,两种标注分别如下:
BIO:王/B-PER,小/I-PER,甲/I-PER,在/O,云/B-LOC,澜/I-LOC,市/I-LOC
BIOES:王/B-PER,小/I-PER,甲/E-PER,在/O,云/B-LOC,澜/I-LOC,市/E-LOC
为什么要跟面试官强调BIOES?因为E和S把边界信息显式建模,解码时实体收尾更明确,后处理可以少做边界尝试;代价是标签数变多,类别不均衡可能更明显。到底选哪种,没有绝对答案,要看数据和工程约束。
2.2 四类方案:规则、词典、统计、深度学习
这个问题我几乎每次都会被追问。可以先用一张表说清区别:
| 方案类型 | 核心原理 | 优点 | 缺点 | 适合场景 |
|---|---|---|---|---|
| 规则模板 | 正则、词典、句法模式 | 可解释、上线快、无需训练数据 | 维护成本高、跨领域难迁移 | 数字、日期、固定番号等强规则实体 |
| 统计模型 | CRF、HMM | 能建模标签依赖,结果稳定 | 依赖特征工程,泛化能力一般 | 中小规模数据、资源受限场景 |
| 深度学习 | BiLSTM-CRF、BERT-CRF | 自动学习上下文特征,效果好 | 需要数据和算力,调试成本高 | 大规模通用领域、复杂语义实体 |
| 混合方案 | 规则优先层 + 模型 | 兼顾精度与召回,容错性好 | 两套系统维护成本 | 大多数真实业务系统 |
在真实业务里,很多团队为求稳妥,会在模型前面挂一层“规则优先层”,把电话号码、身份证号、日期这种几乎没有歧义的实体先抽走,剩余交给模型。面试时如果能说出这种混合思路,会显得务实,而不是唯模型论。
2.3 为什么标准答案还是序列标注
2021年前后,很多前沿试验把NER建模成阅读理解,比如“找出句子中的人名”并输出一段span;生成式大模型出现后,也有人直接用生成方式做实体抽取。但秋招面试里的标准答案仍然是序列标注。原因是稳定、标签可控、部署成本低,而且训练数据在千条级别也能跑出可用模型。
阅读理解式更灵活,支持嵌套实体和复杂指令,但需要更大的模型和更强的推理,工程复杂度也更高。面试官如果问“这两种你怎么选”,你可以答:数据量、速度、可解释性决定。小数据、低延时场景用序列标注更划算,复杂语义场景可以探索抽取式或生成式方案。
3. BiLSTM-CRF:秋招必答的范式模型
3.1 两个模块各管什么
在经典方案里,BiLSTM负责看上下文,计算每个字到每个标签的发射分数。CRF层负责相邻标签的依赖关系,把发射分数加起来,再考虑转移矩阵。
举个例子,如果模型输出序列“B-PER I-LOC I-LOC”,CRF会压制这种不合法转移。转移矩阵是可学习的,形状是(标签数, 标签数),训练后可以学会“B-PER后面不能直接跟I-LOC”这类约束。所以即便BiLSTM在某几个位置给了I-LOC很高分,维特比搜索也会因为转移分数太低而绕开这条路径。
这里有一个很多人理解不了的细节:CRF不是简单地在输出层加规则,而是把“转移关系”也当成参数参与训练。所以模型学到的不是“谁不能跟谁”这种硬规则,而是“在数据中哪种转移更合理”的软约束。
3.2 训练目标:让真实标签路径得分尽可能高
给定句子x和标签序列y,模型的打分可以写成:
score(y|x) = 所有位置的发射分数之和 + 所有相邻位置的转移分数之和
训练目标是要让真实标签路径的条件概率P(y|x)尽可能大。由于所有可能标签序列的数量是指数级,计算分母时要通过动态规划把所有路径得分做log-sum-exp,也就是“配分函数”。预测时则用维特比算法找到得分最高的路径,等价于全局最优解。把这个逻辑讲清楚,面试官就知道你不只是在CNN上套softmax。
我可以拿生活场景打比方:发射分数是每门课的单科成绩,转移矩阵是选课顺序要求,CRF决定的是整条路径是否合法,而不是只看某一门课高不高。真正算绩点时要看完整课程计划,不能只看单科成绩。
3.3 从BiLSTM到BERT:编码器变了,约束没变
到了后期项目里,更常见的是BERT-CRF:把BERT输出的每个token向量接一层线性分类,再进CRF。预训练模型效果更好,是因为文本表示是上下文相关的,对OOV和罕见写法也更鲁棒。CRF层保持不变。
面试中问到“为什么BERT比BiLSTM强”,不要只说“用了Transformer”,最好提到大规模预训练语料带来的泛化能力,以及动态上下文编码。如果是垂直领域,BERT可能需要继续预训练,也就是做领域迁移训练。这个点能体现你真正调过模型。
3.4 面试官追问:CRF的反向传播难在哪
这个问题相对少,但一旦被问就是拉分项。难点在于配分函数对转移矩阵求梯度时,不能只对真实路径求梯度,还要对所有路径的期望做计算。不过实际工程中你不需要手推梯度,更关键的是理解CRF把标签间的条件依赖建模成了概率图结构。如果你能从“观察序列到隐状态序列”的角度解释,就已经合格了。
4. 秋招必须会写的代码与高频追问
4.1 从原始标签到实体列表的解码函数
面试手撕题里,最常见的是把BIO标签解码成实体列表。下面是一个极简实现:
def decode_bio(tokens, tags): entities = [] cur = None for token, tag in zip(tokens, tags): if tag.startswith("B-"): if cur: entities.append(cur) cur = {"type": tag[2:], "tokens": [token]} elif tag.startswith("I-"): if cur and cur["type"] == tag[2:]: cur["tokens"].append(token) else: if cur: entities.append(cur) cur = None else: # O 或非法标签 if cur: entities.append(cur) cur = None if cur: entities.append(cur) return entities逻辑其实很简单:遇到B就开启新实体,遇到I且类型和当前实体一致就继续,遇到O或类型不一致就结束当前实体。生产环境里还要加一步合法性校验,比如不允许O后面直接跟I,不应该让后处理把不合法输出强行补成实体。面试时能写出这个函数,基本就能看出你了解序列标注任务的实际输出。
4.2 评估时的匹配口径
很多新手拿token-level准确率说“模型有98%”,但样本里绝大多数token是O,实体级F1很可能低到没法看。正确做法是把预测的标签序列转回实体列表,再做entity-level匹配。常见口径有这几种:
| 匹配方式 | 判定标准 | 什么时候用 |
|---|---|---|
| strict | 类型和边界完全一致才算对 | 大多数学术基准 |
| type only | 只看类型,边界不同可以容忍 | 关注某类实体是否被找到 |
| boundary only | 只看边界,类型不同也算对 | 关注定位是否有偏移 |
| token-level | 逐token计算指标 | 快速看趋势,不适合最终评价 |
这里要特别注意:不同口径算出来的F1差别很大。写项目报告或博客时,一定要写明用的是什么匹配方式,不然别人很难复现你的结论。
4.3 高频追问与答题思路
第一问:实体嵌套怎么处理?如果业务不需要嵌套,就把嵌套实体拆成单层,只保留最外层或最核心的实体;如果一定要支持嵌套,可以用分层标注,或者用抽取式方案,而不是硬套BIOES。
第二问:OOV和罕见实体怎么识别?我的思路是先保证基于字建模,再用上下文编码,这样未登录词也不会直接变成“UNK”。预训练模型对OOV更友好,但仍需注意测试集里可能出现训练时完全没见过的领域词,所以要留一部分数据专门看泛化。
第三问:类别严重不均衡怎么办?不要一上来就调loss。先分析是哪些实体漏召回,是数据量少,还是边界标注混乱。常见手段有重采样、类别权重、难例挖掘、数据增强。我通常会先给少样本实体加一些权重,然后观察验证集,避免过拟合少数类。
第四问:线上推理太慢怎么办?先看瓶颈在编码器还是CRF解码。BERT编码器很重,可以蒸馏成小模型,或者换成轻量编码器;CRF解码本身不慢,但如果标签多,可以限制候选标签,或者批量处理。另外,长文本要做合理的截断策略,不能简单一刀切。接口层可以加同文本缓存。
第五问:标注样本有噪声怎么办?一定要先做数据清洗。统计训练集里是否存在非法转移,比如O后面直接跟I,大概率是标注工具导出bug或人工误标。如果只是个别噪声,可以在损失里给这部分样本降权,或者直接删除。
5. 实操里的坑与排查清单
5.1 标注一致性是最容易被低估的坑
很多项目效果差,根本不是模型问题,而是标注标准不一致。同一个“云澜市第一中学”,有人把“云澜市第一中学”整体标成机构,有人只标“第一中学”,还有人把“云澜市”标成地名后就不再标机构。模型看到同一个句子有好几种标签,自然学不稳。
实操中我会先抽一批标注样本,人工检查标签长度分布和转移矩阵。如果出现大量“O到I”这种非法转移,说明标注工具或数据导出有bug。标签一致性甚至比模型选择更重要。这里有一个经验:新项目启动前,先让不同标注者对同一条数据各标一遍,算一下一致率,再开始大规模标注。
5.2 训练与评估指标里的隐性坑
除了匹配口径,还有一个容易被忽略的问题:很多数据集在评测时会忽略重复实体,或者把同一实体在句子中出现多次算一个。比如一个句子里两次出现“蓝山科技”,严格实体列表可能只算一个。如果评估脚本没做去重,F1就会偏低。
另一个坑是提前用测试集调参。反复跑测试集选模型,最后得到的分数虚高,上线后很容易翻车。秋招项目复盘时,宁可少拿一个点,也要诚实地把验证集和测试集分开。我自己的做法是只在最终阶段碰一次测试集,平时都用验证集做早停和选模型。
5.3 部署阶段的性能优化思路
实际落地场景经常是CPU推理,BERT-CRF在高并发下会比较吃力。我的做法是:第一,优先上小模型,字向量加BiLSTM-CRF往往已经能解决90%的问题;第二,如果必须用BERT,考虑蒸馏成6层,或者用动态长度截断减少计算量;第三,长文本要有截断策略,比如保留首尾,因为很多关键实体出现在开头和结尾;第四,接口层加缓存,同一段文本不重复推理,这个提升往往很直接。
在一次压力测试里,同样一批文本,加了缓存之后吞吐量提升了大约三成。数字其实不重要,重要的是你有没有这种工程意识。秋招面试官很吃这一套。
5.4 排查清单:当F1上不去时
项目调优时,我会按这个顺序排查:
- 看坏案例分布:是哪一类实体差,是边界错还是类型错。
- 看预测结果有没有非法转移:如果后处理能修复,先修后处理再考虑换模型。
- 看训练集和验证集的标签分布是否差距很大:分布不一致要先做数据修正。
- 看预测实体的长度分布:如果模型只会抽单字,可能是边界标签学得不好,BIOES会有帮助。
- 试一次最简单的规则基底:规则把强规则实体抽走后,模型接受的任务变简单,指标往往能涨一点。
这个清单现在还在用。大多数情况下,动手改模型之前先过滤一轮数据问题,能省下很多调参时间。
我个人到现在都记得,秋招那阵子在纸上画了无数遍BIOES和转移矩阵之后,又花两周把NER从标注到部署完整做了一遍。那两周最大的体会是:模型结构只是NER最表面的部分,数据、标签、评估、后处理才是真正花时间的地方。面试也一样,你不需要把Transformer复现得毫厘不差,但需要能清楚说出“我要建模什么、标签怎么设计、算对还是算错”。如果正在准备秋招,我的建议是别只看论文,拿一份真实语料,用BIOES标两百条,跑一个最基础的模型,把实体级F1算明白,遇到难题时你自然会有底气。