首页 / 资讯中心 / 文章详情

NLP语义学实战:词义消歧、语义角色标注与相似度计算

NLP语义学实战:词义消歧、语义角色标注与相似度计算 ★ FEATURED ARTICLE
最近在梳理NLP知识体系的时候正好推进到第5个专题语义学Semantics。如果说分词、词性、句法分析解决的是“句子长什么样”那么语义学解决的就是“这句话到底什么意思”。这个专题内容不算多但却是从“能跑通流程”迈向“能做对任务”的分水岭。无论你是刚开始接触NLP的初学者还是正在做检索、问答、对话系统这类落地项目的开发者把这一节吃透后续看预训练模型、大语言模型的很多行为都会通透很多。这个单元很适合边学边动手不需要特别大的算力主要是吃透三类问题词义消歧、语义相似度、语义角色标注。下面我按自己实际学习和做项目时的思路来拆把核心原理讲清楚也把我在实验里踩过的坑和调试经验一并写出来。1. 语义模块的整体设计与思路拆解1.1 为什么说语义是NLP的分水岭我们先看一个老生常谈但特别直观的例子“苹果熟了”。如果没有语境这句话至少有三种理解水果成熟了、某科技公司的产品迭代成熟了、电视剧里的人物剧情到位了。分词和句法分析根本分不出这三种差异它们只保证句子结构合法不会告诉你要选哪个意思。语义学的任务就是在这些候选意义之间做选择并把“谁对谁做了什么”这种关系抽出来。从实际产品角度看语义能力几乎决定了所有下游任务的上限。搜索引擎要理解“怎么炖排骨”和“排骨炖多久”是不是同一个问题客服机器人要区分“我要退款”和“退款多久到账”的意图差异内容审核要判断“打死我也不说”和“打死了人也不说”的严重程度完全不一样。如果只停留在词匹配层面这些场景全都会翻车。所以很多团队会把“第四层语义能力”当成一个独立模块来建设而不是让每个业务各自去凑。1.2 三种主流的语义研究视角做语义时会有三种完全不同的切入角度它们对后续技术选型影响很大。第一种是真值条件视角简单说就是“句子在什么条件下为真”。比如“小明在图书馆”和“图书馆里有小明”逻辑上表达的是同一件事的两个侧面。研究这类问题通常需要谓词逻辑、量词、事件结构非常严谨但落到工程里往往太沉重因为真实用户说话充满了省略和歧义。第二种是认知视角探索人脑怎么表征“苹果”这个概念。这里涉及概念网络、原型理论、常识推理。工程上很难直接落地但很多词汇知识资源比如wordnet这类词网的构建思路都受了它的影响。第三种是分布视角也是现在统治工业界的一种观点一个词的意义由它经常共现的语境决定。你不需要给“苹果”写定义只需要统计它在大量文本里和哪些词一起出现。这一条路线直接催生了词向量、语言模型、预训练模型这一整个技术家族。我在单元设计里会把三种视角都点到但工程实操会主要围绕认知视角的词汇资源以及分布视角的向量化方法展开。1.3 本单元的学习路线设计如果把语义学拆成一门可操作的技术课我建议按下述三段走每段都能独立形成能力。第一段是词义级别的功课拿到一个多义词根据上下文选出正确义项这叫词义消歧。第二段是关系级别的功课识别句子里的“谁对谁做了什么”这叫语义角色标注。第三段是度量级别的功课给两个词或两个句子算一个分数表示它们含义有多接近这叫语义相似度。这三段之间有明确的递进关系词义消歧是基础角色标注依赖词义的准确选择相似度计算又可以反过来验证前两者的效果。我在给团队做内部分享时会把这三个能力画成一条流水线有点像先看懂单词再看懂动作最后给整句打分。2. 核心细节解析与实操要点2.1 词义消歧一词多义是第一道坎词义消歧WSD的原理并不复杂一个词有多个义项我们要根据上下文中其他词的提示选出最合适的一个。早期经典算法是Lesk算法核心思想很直白把目标词的上下文词和每个义项的释义词放在一起数重叠重叠越多的义项越可能是正确含义。比如“苹果”有两个义项一个和水果相关释义里有“果实、果树、甜、脆”这类词一个和科技产品相关释义里有“手机、电脑、操作系统”这类词。如果输入句子是“苹果在秋季大量上市”上下文里出现“秋季、上市”这些词显然更贴近第一个义项的释义环境于是消歧结果就是水果义项。这个算法放在今天看并不高级但它的可解释性极强出问题了能直接看出是哪个词撞了。实际工程里现在更多用统计和神经网络方法比如基于词向量聚类的消歧先给多义词的每个义项准备若干例句用句向量做聚类中心判断新例句和哪个中心更近。但无论用什么方法数据都绕不开义项划分这件事。中文尤其麻烦“苹果”这种名词还算好办像“打”这种动词手里能列出二三十个义项标注一套完整的义项语料本身就是工作量巨大的活。我给一个实操建议不要一上来就想Cover全部义项先按业务场景把高频义项标注到覆盖度80%以上其他低频义项直接走默认第一义项兜底。这样投入产出比最高。后面再遇到错误样本补充标注就行。2.2 语义角色标注把句子转成动作片脚本语义角色标注解决的是“谁、对谁、做了什么事、什么时候、在哪、用什么方式”的问题。以句子“他昨天用锤子敲碎了窗户”为例标注结果是这样一组角色施事者A0他动作V敲碎受事者A1窗户时间AM-TMP昨天工具AM-MNR锤子为什么这个结构这么重要因为一旦抽出来你就可以直接回答“谁敲碎了窗户”“他敲碎了什么”“什么时候发生的”。问答系统、信息抽取、事件抽取全都建立在这样的结构之上。很多知识图谱构建项目卡点往往不在实体识别而在“实体之间的关系”抽不准。而语义角色标注恰恰训练的就是这种“动宾关系、施受关系”的感知能力。中文做SRL时有个额外难点动词经常不显式出现比如说“他苹果”意思是“他买了苹果”还是“他吃了苹果”完全靠语境补。这种省略动词的情况规则系统几乎无解统计模型能学个大概但漏标率仍然偏高。我的经验是做SRL之前先做好论元边界切分宁可先把“谁、什么”切成块再去判断块之间的关系也不要从头到尾走端到端黑盒。分步走错误好定位。2.3 语义相似度从词到句的度量问题相似度计算可以分三级来看。词级相似度是最好做的常见思路是路径相似度和信息相似度。路径相似度利用词汇概念网络中的父子关系层次两个词在概念树上离得越近越相似“轿车”和“汽车”关系近所以相似度高“轿车”和“香蕉”关系远相似度自然低。信息相似度则会考虑一个词在语料里的罕见程度越罕见的词越能区分概念权重也越高这也符合直观感受都比较常见的高频词关系未必紧密。句子级相似度就难一些。简单做法是取句子中所有词的向量做平均得到一个句向量然后算余弦相似度。但这种方式对词序不敏感“猫追狗”和“狗追猫”会得到几乎一样的向量。更进阶一点的做法是给词按TF-IDF加权再平均能缓解一些高频虚词的干扰但仍解决不了词序问题。再往后才轮到各类句编码模型可解释性就差很多了。这里要给个很重要的提醒语义相似度没有“唯一的标准答案”。同一对句子在搜索场景里“相关”的判断标准是主题一致在问答场景里“相似”的标准是语义蕴含在去重场景里“相似”的标准是句子结构几乎相同。所以做相似度前一定要先定义清楚业务上说的“相似”具体是什么否则模型很难取舍。2.4 数据与工具的准备要点在做这套语义分析之前要先把数据和工具备齐。基础的NLP工具链至少要覆盖中文分词、词性标注、依存句法分析这几项。语义层的实验通常建议准备三类资源词汇语义资源包含词语之间上下位关系、同义关系、反义关系的词典类资源用途是词义消歧和路径相似度。语义角色标注语料带有人工标注的“动词-论元”关系数据规模不需要太大几千句就可以开始实验。预训练词向量或语言模型用于计算分布相似度如果资源紧张用公开的中文词向量也能完成大部分实验。工具链的选择我的偏好是快速原型阶段用轻量的开源NLP工具包跑通流程后再考虑性能优化和模型替换。很多人一上来就调大模型API最后发现连基础的切分都出了问题反而不容易定位。3. 实操过程与核心环节实现3.1 用简化版Lesk算法实现词义消歧我们先做一个迷你版词义消歧用来理解Lesk算法的核心逻辑。为方便演示我用一个简化的词义表示结构每个义项只有ID、名称和释义关键词。实际使用中你可以从更成熟的“词网”资源里读取这些数据。def lesk_simplified(context_words, senses, window2): def score(sense, ctx): overlap 0 for i, token in enumerate(ctx): if token in sense[gloss]: # 加一点位置权重离目标词越近的词权重越高 dist_factor max(0, 1 - abs(i - window) / (window 1)) overlap 1 * dist_factor return overlap best_score -1 best_sense None for sense in senses: s score(sense, context_words) if s best_score: best_score s best_sense sense return best_sense这里的context_words是目标词前后若干个词组成的列表window是我们设定的窗口半径。位置权重的设计是我自己加的经验项离目标词越近的上下文词对义项选择的贡献越明显。比如句子“我在菜市场看到苹果在卖”上下文里“菜市场、卖”都靠近目标词而“看到”这类词因为常见即使和某个义项有重叠也会因为距离较远而压低贡献。用上面这个函数实测几个句子你会发现只要窗口大小在3以内效果都还行一旦窗口拉到整句低频噪音词就会把分数带偏。所以这个算法最核心的调参点不是算法本身而是“给多大窗口”和“过滤多少停用词”。词义消歧在学术上很难但在工程上做成“业务够用”相对容易。关键是不要贪心一次解决所有词先把多义词的清单收敛到业务高频的几十个然后逐个优化。3.2 计算语义相似度手动算一个例子看一个具体的相似度计算过程同时对比两种方法。先看最简单直接的路径相似度在一个常识概念树里节点代表一个概念概念边代表上下位关系。“汽车”的父节点是“机动车”“轿车”的父节点也是“机动车”“香蕉”的父节点是“水果”。那么从“轿车”到“汽车”的距离是2步相似度可以设定为距离越短得分越高。一个常用的公式是[ sim \frac{1}{1 dist} ]所以“轿车”和“汽车”的sim是0.33而“轿车”和“香蕉”的距离可能超过10sim接近0.09。这个分数表面上看起来很小但如果把它映射到排序场景里它的作用主要是区分相对远近而不是给出一个绝对的“多相似”。原理本身不复杂真正麻烦的是维护一棵与时俱进的概念树新词“固态电池”“钠离子电池”这类名词需要持续插入否则相似度计算就会漏。另一种更贴合工业界的方法是向量余弦相似度。假设我们用某个公开词向量库取“轿车”向量v1和“汽车”向量v2那相似度就是import numpy as np def cosine_sim(vec_a, vec_b): dot np.dot(vec_a, vec_b) norm_a np.linalg.norm(vec_a) norm_b np.linalg.norm(vec_b) return dot / (norm_a * norm_b 1e-8) v_car np.array([0.2, 0.3, -0.1]) v_sedan np.array([0.21, 0.28, -0.08]) v_banana np.array([-0.2, 0.1, 0.4]) print(cosine_sim(v_sedan, v_car)) # 接近0.95 print(cosine_sim(v_sedan, v_banana)) # 可能是负数或者0.1以下这里用三个手工假设的向量演示计算过程实际训练好的词向量维度通常在100到300维但余弦计算就是这样一个公式没有任何黑魔法。一个实用的经验是计算相似度之前先对向量做中心化处理即将每个维度的均值减去。中心化能消除不同向量之间的系统性偏移实验结果通常比直接算余弦更稳定因为纯余弦只关心方向不关心模长而模长有时也包含重要信息。句子级相似度我建议从简单到复杂试起。先取每个词的向量算平均用这个平均向量代表句子如果效果不满意再加TF-IDF权重还不行再考虑用真正的句编码模型。每一次升级都要用同一组测试集做对比才能确认升级到底有没有用。切忌一上来就追求最复杂的模型否则最后连什么模块产生了什么效果都说不清。3.3 语义角色标注把规则跑起来语义角色标注最轻量的落地方式是规则加词典。我们先用一个小系统理解标签体系的含义再看哪些环节可以用统计模型替换。def simple_srl(sentence, verb): # 这是一个极度简化的模板只用来理解语义角色抽取的基本思路 tokens sentence.split() agent_idx None patient_idx None for i, tok in enumerate(tokens): if tok in {他, 她, 某人, 公司, 团队}: if i tokens.index(verb): agent_idx i if i tokens.index(verb) and tok not in {把, 被, 了}: if patient_idx is None: patient_idx i result {} if agent_idx is not None: result[A0] tokens[agent_idx] result[V] verb if patient_idx is not None: result[A1] tokens[patient_idx] return result print(simple_srl(他敲碎了窗户, 敲碎))上面的代码非常简单很多句子会标错但它的价值在于给你建立清晰的标签直觉A0一般在动词前A1一般在动词后被字句和把字句需要特殊处理。“把”字句里“他把窗户敲碎了”“窗户”虽然在动词前但它是动作的承受者不是施事者所以A0应该是“他”A1是“窗户”。这个规则如果不单独写初学者很容易被词序干扰。真实项目里用规则做SRL只能覆盖一小部分常见句式更常见的做法是收集一批已标注的SRL语料训练一个序列标注模型。序列标注会把每个词贴上BIO标签比如B-A0代表某个论元开始I-A1代表论元继续。训练这种模型和命名实体识别非常像输入是词向量加词性特征输出是每个词的标签序列。这个过程中需要人工调整的地方主要是标签体系是只有A0/A1还是需要细分成工具、地点、时间等附加角色。附加角色一多样本量需求就成倍上升。我个人的建议是如果业务只需要做知识抽取A0、V、A1三个角色基本够用如果要做深层的问答类似“他是用什么敲碎的”才需要考虑AM-MNR、AM-TMP这类附加角色。先定义最小可用的角色集合别为了学术上的完整而把问题无谓放大这是我踩过坑后总结出来的。3.4 评估语义模块做得好不好语义模块不像分词有明确的准确率标准因为它涉及“语义对不对”的主观判断。所以工程上一定要建立一套自己的验收指标我常用三个一个是消歧准确率。方法很简单准备几百条人工标注好的多义词句子跑一遍消歧流程算出正确比例。注意采样要按业务场景分布来不要均匀抽样否则高频词占大头评估结果虚高。一个是相似度与人工打分的相关性。准备若干对句子让两个人分别打0到5的相似度分和模型输出的分数做排序相关性计算。相关度高说明模型产出的排序基本符合人的直觉这是检索场景里最看重的指标。还有一个是端到端任务提升。比如你做一个FAQ问答系统把相似度模块从基础的词重叠换成语义度量后看最终问题召回率提高多少。这是最实用的验收方式因为最终的指标才是业务方关心的。这三个指标前两个在开发期用最后一个在灰度阶段看。如果端到端指标没提升哪怕前两个指标很好看也要回头检查是不是相似度模块的使用方式出了问题。我遇到过一种情况模型分数排序是对的但产品侧只拿了一个绝对的相似度阈值去过滤由于分数整体偏高导致误召回了一堆无关内容。这种事情不对比端到端指标根本发现不了。4. 常见问题与排查技巧实录4.1 词典资源里查不到词的义项怎么办这是最常遇到的情况。词网这类人工构建的资源更新滞后网络新词、专业术语覆盖不全消歧时经常找不到对应义项。我的处理思路是分层兜底先查基础词网资源命中不到再看领域词表领域词表也没有就返回高频义项。这样虽然对单条样本可能判断不够精准但系统不会整体崩掉。实际操作时可以把“找不到义项”这件事单独打日志每天统计频率。如果某个词反复查不到就启动人工补充流程把词条加进领域词典并标注好义项提示词。这种滚动更新的节奏比一次性追求完整覆盖稳健得多。4.2 词向量在不同批次训练后不稳定用相同语料训练两次词向量得到的向量空间可能差异很大原因是随机初始化和迭代顺序的影响。这会给相似度模块带来一个隐患模型参数更新后向量空间发生变化导致线上相似度分数波动。解决办法是固定随机种子以及固定训练数据的迭代顺序。如果这些做了还是不稳定另一个方案是引入一个线性对齐以旧版向量为基准对新版向量做正交变换最小化两个集合中重叠词的向量差异。对齐之后的向量带上线分数漂移会显著下降。这套方法在向量模型升级时非常实用。4.3 余弦相似度给反义词打了高分“好”和“坏”在词向量空间里往往很近因为它们的语法功能和语境高度相似。这在语义相似度场景里会带来一种尴尬你问“这个产品好不好”系统把“这个产品很坏”也当成高度相关。这是分布语义的固有偏差解决起来不能完全靠向量本身。我的经验是叠加一个情感极性维度。具体做法是给每个词打一个情感分在计算相似度时如果两词的极性方向不同就在相似度分数上乘以一个惩罚系数。这样“好”和“坏”的向量距离近但因为极性相反最终排序时不至于被误当成同义。这是规则和模型结合的典型案例。4.4 标注语料太少模型训练不动语义角色标注这种任务没有几千条高质量标注句子模型很难收敛。如果预算和人力有限我建议用远程监督的思路先从已有的结构化知识库或业务数据库里拿到“主语-谓语-宾语”三元组再回填到文本中找出对应的句子片段自动产生一批弱标注数据。这批数据噪音大但作为预训练集非常合适。拿到弱标注模型后再让人工只修正模型置信度低的样本形成快速迭代闭环。整个过程中保证“修正后的数据被重新加入训练集”这一条很重要否则标注资源等于只修了一半。实际操作里弱标注加上少量人工修正通常能把F1从40多分提到75分以上在这个基础上再想往上走就得仔细分析错误类型而不是无脑加数据。4.5 生产环境里向量检索性能差语义相似度检索要遍历几十万个向量做点积当请求量上来后纯计算耗时和延迟会让人头疼。常规优化手段是建向量索引先做粗筛再做精排。粗筛用近似最近邻算法牺牲少量精度换取高召回候选集精排阶段再用准确的余弦相似度在候选集内排序。这样可以把全量检索的计算量降下一两个数量级。还有更简单的缓存方案相同或高度相似的查询在短期内重复率高尤其在企业客服这类场景中。对输入句子先算一个短指纹如果相似度超过某个阈值直接返回缓存结果。我见过一个问答系统引入缓存后后端压力降低了40%。所以性能优化不要只看索引也得看业务流量特征。5. 把语义能力落到项目里的工具选型建议5.1 数据量决定方法选择我经常被问到一个问题做语义相似度到底用规则、经典模型还是预训练大模型这个问题不能脱离数据量单独回答。这里给一个参考阈值当你的标注样本只有几十到几百条时用规则配合词表比训练任何模型都强。因为模型在样本不够时会严重过拟合上线后碰到稍有变化的句式就会崩。当样本量达到几千条时可以考虑训练轻量的分类或匹配模型此时词向量特征加上简单的神经网络往往效果稳定。当样本量达到几万条以上且你有多卡训练能力时再考虑微调预训练语言模型这样才能物尽其用。这个经验很适合中小团队。很多人一上来就微调大模型结果算力消耗大、迭代速度慢最后效果和简单的词向量模型不相上下就是因为数据量不够支撑复杂模型的学习。模型复杂度不是越高越好要和手里的弹药匹配。5.2 语义服务和业务解耦的经验项目进入后期语义能力最好沉淀成独立服务而不是每个业务各写一套。独立服务的好处是所有场景共用同一套词义资源、向量库和标注体系出了问题只需在一处修复所有调用方同时受益。设计接口时建议把输入定义为纯文本输出定义为结构化结果比如“消歧结果列表”“语义角色集合”“相似度分数”。让上游业务只关心自己需要的字段不要暴露过多的内部细节。语义服务内部怎么升级、换模型只要输出schema不变业务方就感知不到。这个解耦的收益需要等系统做过一次大升级后才能真正体会到。5.3 预训练语言模型时代还需要学经典语义吗经常有人问现在大模型这么强大还需要理解词义消歧、语义角色标注这些基础概念吗我的回答始终是需要而且理解了这些概念会帮助你更好地用大模型。大模型本质上是分布语义的一个超级大的实现它的很多“聪明”之处来自深度学习语言模型但它的缺陷也继承了分布语义的先天不足。比如它对反义词的区分、对罕见义项的识别、对复杂逻辑关系的推理仍然是吃力不讨好的。当你清楚这些经典模块擅长什么、不擅长什么你就能在大模型输出里敏锐识别出错误信号知道该去追问还是该换一种方式提问。反过来如果你完全不懂底下这些原理你看到大模型一本正经地胡说八道时根本不知道应该怎么定位问题。我在每次做项目总结时都会把经典语义模块比作一套“基本装备”词义消歧是盾过滤歧义语义角色标注是矛刺穿表层句子结构相似度计算是尺度量远近。这三样装备不一定每个项目都同时用上但你手里得常备着因为随时会用到。最后再分享一个小技巧做语义类项目时千万别只盯着单个指标。我习惯每周固定抽几十条线上真实请求把模型的中间输出打印出来逐条看尤其关注那些“模型打了高分但用户明显不满意”的样本。这些样本往往能暴露出评测集里覆盖不到的问题比如标的义项不符合业务习惯或者角色标注把领域术语拆碎了。先修这类问题模型效果才会有质的提升。
阅读完成 · 觉得有帮助?
咨询建站