Press "Enter" to skip to content

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

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

 

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

 

基于ES的简单召回

 

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

 

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

 

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

 

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

通过在查询添加 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,
            "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" : [...]
    },
]

 

$$

 

score = boost * tf * idf \

 

tf = \frac{freq}{freq + k_{1} * (1 – b + b * \frac{d}{d_{avg}} )} \

 

idf = log_{2} \frac{N + 1}{n+0.5}

 

$$

 

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 ,是一个pair wise的模型架构,并且针对负样本的构建做了深入的实验。

 

模型结构

 

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

 

 

这里的loss function:$L = max(0,D(q,d+)-D(q,d-)+m)$, 其中 $D(q,d) = 1-cos(q,d)$

 

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

 

模型构建

 

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

 

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

 

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

 

 

 

{
    "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: https://www.elastic.co/cn/blog/text-similarity-search-with-vectors-in-elasticsearch

 

双路召回

 

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

 

 

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

 

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

Be First to Comment

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注