搜索引擎优化 知乎 一只小狐狸带你解锁炼丹术ampamp;NLP秘籍
优采云 发布时间: 2021-07-06 05:38搜索引擎优化 知乎
一只小狐狸带你解锁炼丹术ampamp;NLP秘籍
小狐狸带你解锁炼金术&NLP秘籍
前言
随着用户规模和产品的发展,知乎search 面临着越来越多的查询长尾挑战。查询理解是提高搜索召回质量的关键。本次分享将介绍知乎search 在查询词加权、同义词扩展、查询重写、语义召回等方面的实用方法和实现。
知乎搜索历史
先回顾一下知乎search的历史。现有的知乎search 16年开始内部研发,17年完全取代外部支持的系统。算法团队成立于 18 年初,规模约 5 人,随后开始了快速迭代过程。
主要进展有:Term Weight于2018年4月上线;自研的Rust引擎于6月上线,并已开源;改写了今天分享的纠错、语义相关性、语义等一系列算法模型的索引和查询;今年下半年的工作主要集中在排序方面,主要是用DNN替代树排序模型,并在此基础上推出了几项收益不错的优化。由于今天分享的内容主要是在NLP的范围内,排序端的内容以后有机会可以分享。
知乎是一个问答社区平台,其内容主要基于用户生成的问答。问答网站的属性导致知乎搜索查询词的长尾。事实上,在大搜索领域,问答查询也是一种满意度比较差的类型。目前,各类垂直搜索已经分流了原有大搜索的流量。其中知乎search 承担了部分比较难的查询。
从我们内部的实际数据来看,知乎search的长尾趋势在过去一年越来越明显。如上图右上部分,显示了头、腰、尾的搜索比例。可以看出,长尾查询有明显的增长趋势。但是,在我们搜索团队的努力下,用户的搜索满意度并没有随着查询难度的增加而下降。
长尾查询的特点
长尾查询的多样性对搜索系统来说是一个巨大的挑战,原因如下:
❶输入错误。比如上图中的错误查询“塞尔维亚”(Serbia),我们希望系统能自动纠正错误;
❷表达上有冗余。例如,输入“孙子兵法智慧的现代意义”。在这种情况下,“智慧”是一个无关紧要的词。如果强制匹配“智慧”,则不会匹配到真正想要的结果;
❸存在语义鸿沟。例如“高跟鞋沉默”,其中“沉默”一词的表达很少,因此同时收录“高跟鞋”和“沉默”的文献较少。 “如何消除高跟鞋的声音”、“消音高跟鞋的声音”等类似的表达方式可能更多。用户输入的查询与用户生成的内容之间存在语义鸿沟。其他类型的困难包括表达不完整、意图不明确等。
我们先通过图右侧的查询来介绍如何解决上述问题:“iPhone手机价格是多少”:
❶对于输入错误,比如用户输入的query是iPhone,但是单词输入错误,会通过纠错模块纠正为正确的query;
❷对于表达式冗余,计算词条权重,即每个词的重要性,确定参与倒排索引交集操作的词。首先将查询细分为 iPhone、手机、价格和多少,然后判断每个单词对于表达意图更重要。重要的词在检索时会参与倒排索引的交集操作。不严格要求不太重要的词。必须出现在文档中;
❸解决语义鸿沟问题。原创查询需要通过同义词进行扩展。例如,“iPhone”和“Apple”是同义词,“价格”和“售价”是同义词。
所以传统搜索领域的查询模块往往收录这些子任务:纠错、分词、接近度、同义词、词权重,以及其他诸如实体词识别、意图识别等。这些查询推导会生成结构化的数据,检索模块可以通过结构化查询字符串调用相关文档。
知乎搜索召回和排序
知乎的召回系统主要分为两部分:
❶传统的基于词的倒排索引召回;
❷基于向量索引的召回机制。
如图所示,实际recall有3个队列,基于倒排索引的2个队列分别是原创Query队列和重写后的Query队列。重写队列就是利用翻译模型生成一个更适合检索的查询,然后做查询理解和索引召回。第三个队列基于嵌入的索引。首先,将所有doc和原创query转化为向量,然后通过空间最近邻的KNN-Search找到最相似的文档进行查询。
这三个队列最后参与排序,合并细化。细化排序后,将进行上下文排序。下面介绍一下各个模块的具体实现方法。
查询词权重
对于term weight,最简单的实现方式就是使用IDF逆文档频率来计算,即看词在语料库中出现的次数。如果次数少,则认为它收录的信息相对较多。
这种方法的局限性在于它不能动态适应查询上下文,因为它的权重在不同的上下文中是一致的。因此,我们会通过统计的点击数据进行调整。如图,如果用户查询收录a和b两个词,点击Doc1和Doc4,Doc1收录a,b,Doc4只收录a,即a出现两次,b出现一次。一个简单的想法就是a的权重为1,b为0.5,这样就可以从历史日志中统计出每个查询中每一个term的权重。
对于以前从未出现过的查询,无法根据历史数据计算此方法。所以我们从查询粒度泛化到gram粒度,通过ngram聚合。比如a和b是bigrams,类似于mapreduce的map过程。首先,查询中的ngrams会先被映射出来,然后发送到reduce进行统计。这样就可以计算出每个ngram下更可信的权重。
对于ngram,在实际操作中需要注意以下几点:
❶词汇量很大,往往在十亿以上;
❷字典更新慢;
❸无法处理长距离依赖。基本上最多3grams,多个gram词不仅会膨胀词汇量,而且词之间的依赖关系会更难捕捉;
❹无法解决太稀疏的词,太长的词会退化为idf。
我们的解决方案是将ngram的基于字典的泛化方法改为基于嵌入的模型泛化方法。目标是获得动态适应查询上下文的术语权重。如果可以得到与查询上下文相关的动态词向量,那么就可以在词向量的基础上通过连接MLP来预测词权重。目前的方法是将词条本身的词向量减去所有查询词池化后得到词向量。未来我们会尝试用ELMo或者Transformer的结构来代替,以获得更好的效果。
同义词扩展
对于同义词扩展策略,您首先需要一个同义词词汇表。现在有可以直接应用的公共数据集。但是由于分词粒度不一致,数据集中的词和知乎搜索中的词可能不匹配,不能保证同义词更新。所以我们主要使用内部语料进行数据挖掘,主要方向是:
❶用户日志:会话
用户搜索日志,使用用户搜索查询后重写的新查询,将两者视为相关,使用对齐工具找出两者的同义词;
❷查询日志:点击
同理,使用两个查询点击的同一个doc构建查询关系;
❸ 文档:预训练嵌入
Doc 本身的语料库。主要利用知乎内部数据训练词嵌入模型,利用嵌入值计算相关性。
Word2vec的embedding计算方法,核心假设是上下文相似的词是相似的。需要注意的是,该方法无法区分同义词和反义词/同位词,例如max/min、apple/banana。这里我们使用监督学习的信号对训练后的词向量空间进行微调。
❹外部数据
使用百科中的“XXX aka XXX”等规则进行挖矿。
查询重写
接下来要分享的是查询的重写部分。为什么需要重写查询?
在传统搜索中,一个查询需要针对多个子任务进行处理。如果每个任务只能达到90%,那么累积的损失会非常大。我们需要一种同时完成多个任务的方式来减少损失,比如直接将“苹果手机价格”改写为“苹果手机价格”。
这个需求可以用翻译方法来实现,即将重写问题看成是src和目标语言是同一种语言的翻译问题,翻译模型使用了谷歌的NMT结构。
对于这个任务,挖掘训练语料比修改模型结构更重要。我们使用用户行为日志来挖掘训练语料。比如从query到title,点击同一个doc的不同query就可以形成平行语料。
最后,我们用来训练模型的语料库是Query->Query的平行语料库,因为两者在同一个语义空间,长度基本相似。如果将查询翻译成标题,由于标题的长度通常太长,训练复杂度会增加。具体方法是:
❶语言模型过滤
目的是用常用表达替换稀有表达。使用ngram模型计算一个分数,把比较少的放在前面;
❷执行相关性过滤
考虑到点击日志比较嘈杂,通过相关性过滤掉噪声;
❸确定分词的粒度
使用了BPE的Subword方法。之所以不使用知乎内部切词,是因为未注册词无法预测,词汇量大,训练速度慢。
查询重写强化学习
具体来说,重写模型使用共现数据作为训练语料,但不能保证重写的结果会召回有用的文档。我们参考了Google 2018 ICLR的工作。为了将用户输入的查询重写为更容易检索答案的查询,我们将 QA 系统视为一个环境,将重写后的模型视为一个代理。 CNN 是选择所有召回结果的模块。
在我们的场景中,图中灰色框内的QA对应一组倒排索引,CNN对应在线排序服务模块。
首先将原查询的recall结果和改写query的recall结果发送给排序服务模块,然后得到topK排名;那么,topK结果中有多少是重写查询召回队列的结果,作为奖励。
接下来,我们使用强化学习策略梯度方法对模型进行微调和重写。使用强化学习的优势之一是不再需要对齐文本语料库,只需随机查询即可。
使用强化学习训练时遇到的问题之一是稀疏奖励问题。我们将很多 NMT 模型的结果发送到在线系统,但是返回的 topK 列表并没有包括 rewrite 队列的召回。这导致训练过程非常缓慢,也给在线系统带来了很大的开销。
为了缓解稀疏奖励问题,参考alphago中的模型,由策略网络、价值网络和蒙特卡罗树搜索三个组件完成。我们可以把NMT看成一个战略网络,然后这里主要是加入价值网络。价值网络用于评估在重写完成后它实际上可以从系统中获得多少奖励。他的输入是拼接在一起的两个查询,输出是一个浮点值。利用强化学习的经验来学习这个价值网络,然后在后续的训练过程中加速训练。
最终的优化效果非常明显。加入价值网络后,奖励比例提升50%以上。
同时,这个价值网络也可以用于在线改写触发rerank。如下图所示,线上覆盖率明显提升。 Top20 收录 24% 的具有重写率的查询,以及 4% 的查询文档。
在重写中,我们还训练了一个从标题到查询的模型,解决了检索中重要字段点击查询的覆盖范围有限和显示偏差两个问题。 Clicked query 字段记录了点击这个文档的查询,也就是说虽然这个文档不收录查询中的词,但是如果其他用户搜索了类似查询点击的文档,它也会被召回。
记录点击查询会遇到点击噪音和显示偏差问题。因为被点击的文档比未被点击的文档多了一个字段,所以在召回上有一定的优势,形成一个正循环,导致显示偏差。
通过从标题到查询训练模型,点击查询被替换。这样,每个文档都有这个字段。虽然该模型的效果不如query-to-query模型,但由于该模型内置于索引中,容错率会更高。