基于FAQ的智能问答(二): 召回篇

背景

基于FAQ的智能问答本质是一个信息检索的问题,所以可以简单划分成:召回+精排 两个步骤。召回的目标是从知识库中快速的召回一小批与query相关的候选集。所以召回模型的评价方法,主要侧重于 响应时间top@n的召回率 两个方面。

本文将分享我们召回模型的逐步迭代过程,从最基础的“ES字面召回”到 “ES字面召回和向量召回”的双路召回模式。

基于ES的简单召回

在第一篇分享"基于FAQ的智能问答(一): Elasticsearch的调教" 中已经介绍了信息检索中的神器Elasticsearch!所以可以基于ES快速搭建一个召回的baseline。

具体而言,构建如下的ES查询语句,按照评分从高到低返回top50的结果:

{
    "query":{
      "match":{
        "question":"公务员考试"
      }
    },
    "explain": true
}

而基于ES的召回实质是基于BM25的召回[1]:

基于FAQ的智能问答(二): 召回篇_第1张图片
ES官网关于相似度计算的说明

通过在查询添加`explain=true`可以获取计算的细节:

...
"hits" : [
{
    "_shard" : "[lime-ai-faq][0]",
    "_node" : "X0rpCxNLQcuOW56gzFjPAA",
    "_index" : "lime-ai-faq",
    "_type" : "_doc",
    "_id" : "360",
    "_score" : 17.105383,
    "_source" : {
        "question" : "公务员考试省考和国考的题型区别大吗?",
},
"_explanation" : {
    "value" : 17.105383,
    "description" : "sum of:",
    "details" : [
    {
        "value" : 4.8125763,
        {
            "value" : 4.8125763,
            "description" : "score(freq=1.0), computed as boost * idf * tf from:",
            "details" : [
            {
                "value" : 2.2,
                "description" : "boost",
                "details" : [ ]
            },
            {
                "value" : 5.628467,
                "description" : "idf, computed as log(1 + (N - n + 0.5) / (n + 0.5)) from:",
                "details" : [
                {
                    "value" : 8,
                    "description" : "n, number of documents containing term",
                    "details" : [ ]
                },
                {
                    "value" : 2364,
                    "description" : "N, total number of documents with field",
                    "details" : [ ]
                }
                ]
            },
            {
                "value" : 0.38865548,
                "description" : "tf, computed as freq / (freq + k1 * (1 - b + b * dl / avgdl)) from:",
                "details" : [
                {
                    "value" : 1.0,
                    "description" : "freq, occurrences of term within document",
                    "details" : [ ]
                },
                {
                    "value" : 1.2,
                    "description" : "k1, term saturation parameter",
                    "details" : [ ]
                },
                {
                    "value" : 0.75,
                    "description" : "b, length normalization parameter",
                    "details" : [ ]
                },
                {
                    "value" : 11.0,
                    "description" : "dl, length of field",
                    "details" : [ ]
                },
                {
                    "value" : 7.777073,
                    "description" : "avgdl, average length of field",
                    "details" : [ ]
                }
                ]
            }
            ]
        }
        ]
    }
},
{
  "value" : 4.8125763,
  "description" : "weight(question:公务 in 965) [PerFieldSimilarity], result of:",
  "details" : [...]
},
{
  "value" : 3.8403268,
  "description" : "weight(question:员 in 965) [PerFieldSimilarity], result of:",
  "details" : [...]
},
{
  "value" : 3.639904,
  "description" : "weight(question:考试 in 965) [PerFieldSimilarity], result of:",
  "details" : [...]
},
]

boost:用来控制检索字段的权重。默认值为2.2;

k1:控制词频结果在词频饱和度中的上升速度默认值为1.2。值越小饱和度变化越快,值越大饱和度变化越慢;

b:控制字段长归一值所起的作用0.0会禁用归一化,1.0会启用完全归一化。默认值为0.75;

-----------

但是基于ES的召回,也就是BM25的召回方案,还是基于字面的关键词进行召回,无法进行语义的召回。考虑下面的场景

知识库内的问题是:“可以免运费吗?”

用户的query如果是:“你们还包邮?”

可以看到用户的query和知识库中的问题没有一个关键词相同,但是其实是语义一致的。这就需要基于语义的召回来补充。

基于语义的召回

基于语义的召回通常是基于embedding的召回。具体而言,首先训练sentence embedding模型,然后将知识库中的问题都预先计算出embedding向量。在线上预测阶段,对于每个query同样先计算出embedding,再到知识库中检索出相近的embedding所属的问题。

这里采用的方案参考的是facebook最新的论文: Embedding-based Retrieval in Facebook Search[2],是一个pair wise的模型架构,并且针对负样本的构建做了深入的实验。

模型结构

我们采用的基本结构是albert获取embedding,然后通过pair-loss function进行fine-tuning。

基于FAQ的智能问答(二): 召回篇_第2张图片
基于pair-wise的模型结构

这里的loss function:

其中

这里m为超参数,对于“简单”的样本,正样本和负样本之间的距离大于m,L即为0。所以m的存在,让模型更加关注比较“难”的样本。

数据构建

我们尝试构建一个通用领域的召回数据集,格式为(q,d+,d-)的三元组。

这里我们借鉴了论文Embedding-based Retrieval in Facebook Search中的思路,d-负样本包括easy和hard两类。

具体而言,首先收集一个通用领域的query集合,再带入百度知道中检索,提取1-20页的结果。

基于FAQ的智能问答(二): 召回篇_第3张图片
第一页的结果,作为正样本

基于FAQ的智能问答(二): 召回篇_第4张图片
第二十页的结果
{
    "q":"北京有哪些旅游景点"
    "page_1": [
        "北京有哪些旅游景点啊?"
        "北京有哪些旅游景点好玩"
        "北京有哪些旅游景点最出名?可以推荐一些吗?"
        ...
    ],
    ....
    "page_20": [
        "北京旅游景点线路的服务标准都有哪些?"
        "北京周围方圆三百里有什么旅游景点。。农业观光区。。",
        "北京到包头沿途有什么旅游景点?"
        ...
    ]
}

抓取的数据集如上所示,每个query包括都包括1-20页的问题。

对于每个query,随机从page1-3页中提取5个问题作为正样本,从page20页中随机抽取3个问题,并且与query之间的rouge值<0.15的作为 “hard”负样本,从其他的query的问题中随机抽取12个问题作为 “easy 负样本

这样每个query,包括5个正样本和15个负样本。两两组合就可以构建出 75个 (q,d+,d-)三元组。

ES dense_vector

基于embedding的检索,已经有很多成熟的方案,包括:Annoy、Faiss、Elasticsearch (dense_vector)等。最终考虑不引入新的框架(挖坑),我们还是选择继续在ES中来实现,主要就是基于ES的dense vector。

在MySQL同步到ES的阶段,为每个问题计算出句子级别的embedding向量,并存储到`question_vector`字段中。在检索阶段,先计算出query的embedding向量,再基于以下的检索语句:

{
  "script_score": {
    "query": {"match_all": {}},
    "script": {
      "source": "cosineSimilarity(params.query_vector, 'question_vector') + 1.0",
      "params": {"query_vector": query_vector}
    }
  }
}

具体可以参考ES的官网blog:

elastic.co/cn/blog/text

双路召回

最终的召回框架如下图所示,是 “字面召回 + 语义召回” 的双路召回结构。在收到query的请求后会同步发起两路召回,最后进行结果的合并。

基于FAQ的智能问答(二): 召回篇_第5张图片

其中Embedding Server是一个基于Tensorflow-Serving的独立服务。

全部的服务(Flask,ES,TF-Serving)均部署在CPU的k8s集群上,在知识库问题数量<10000的场景下,整个召回阶段的响应时间<50ms,满足线上的实际需要。 同时top@50的召回率也能接近100%

你可能感兴趣的:(基于FAQ的智能问答(二): 召回篇)