练习题 - 基于快速文本标题匹配的知识问答实现(一,基础篇)

2019-05-27 14:58:39 浏览数 (1)

版权声明:博主原创文章,微信公众号:素质云笔记,转载请注明来源“素质云博客”,谢谢合作!! https://blog.csdn.net/sinat_26917383/article/details/82224413

该练习题来的很蹊跷,笔者在看entity embeddings的东西,于是看到了16年的这篇文章:Learning Query and Document Relevance from a Web-scale Click Graph,想试试效果,就搜到了qdr这个项目,然后试了试,虽然entity embeddings做的不好,但是好像可以依据里面的文本匹配搞搞问答,于是花了一点时间,因为是cython,速度还不错,可以做个简单的demo,于是有了该篇练习。

该项目qdr:Query-Document Relevance ranking functions,包含了以下几类文本权值表示方式:

  • TF-IDF
  • Okapi BM25
  • Language Model

内嵌Cython 处理速度不错,有一些参数可以自行看着调整:

  • For TF-IDF see Salton and Buckley, “Term-weighting approaches in automatic text retrieval” (“best fully weighted system tfc * nfx” (Table 2, first line))
  • For Okapi BM25, see “An Introduction to Information Retrieval” by Manning et al. (Section 11.4.3 (page 233), eqn 11.32).
  • For the Language Model approach, see Zhai and Lafferty “A Study of Smoothing Methods for Language Models Applied to Ad Hoc Information Retrieval”

可能项目底层技术本身在热火朝天的QA中,各种高大上的embedding然后进行DSSM匹配比起来,很low,但很高效/简单,而且项目虽小,五脏俱全,入门极佳~


—– 目 录 —–

      • —– 目 录 —–
    • 1 安装与使用
      • 1.1 安装
      • 1.2 使用
    • 2 代码实现
      • 2.1 数据集准备
      • 2.2 数据训练
        • 2.2.1 常规训练与增量训练
        • 2.2.2 模型属性
        • 2.2.2 模型保存
        • 2.2.3 词条剪枝
      • 2.3 模型Scoring环节
        • 2.3.1 文本比对
      • 2.3.2 复现计算tfidf、bm25、三款lm模型
      • 2.4 模型保存与加载
      • 2.5 trianing scoring过程结合

1 安装与使用

1.1 安装

https://github.com/seomoz/qdr

代码语言:javascript复制
sudo pip install -r requirements.txt
sudo make install

1.2 使用

环境是:py2.7 主要是两个步骤:training 以及 scoring。training 步骤可以理解为计算IDF等信息环节;scoring为匹配 排序环节。 一些辅助小功能:

  • 计算IDF并输出
  • 基于出现次数进行剪枝
  • 两句内容求相似
  • 三种适配模型:tfidf/bm25/Language Model

其中Language Model包括了三种平滑方法:Jelinek-Mercer Dirichlet 以及Absolute discount:语言模型和概率模型不太相同,根据实证研究语言模型比较靠谱,其中平滑方法更加是语言模型的核心。


2 代码实现

使用的是py2,所以中文编码问题很多。

2.1 数据集准备

代码语言:javascript复制
corpus = ["he went down to the store".split(),
        "he needed a shovel from the store to shovel the snow".split()]
corpus_update = ["the snow was five feet deep".split()]

corpus_unigrams = {
 'a': [1, 1],
 'deep': [1, 1],
 'down': [1, 1],
 'feet': [1, 1],
 'five': [1, 1],
 'from': [1, 1],
 'he': [2, 2],
 'needed': [1, 1],
 'shovel': [2, 1],
 'snow': [2, 2],
 'store': [2, 2],
 'the': [4, 3],
 'to': [2, 2],
 'was': [1, 1],
 'went': [1, 1],
}
# 其中,[单词数量 word_count, 文档数量min_doc_count]
corpus_ndocs = 3

query = ["buy", "snow", "shovel", "shovel"]
document = ["the", "store", "sells", "snow", "shovel", "snow"]

此为项目中使用的数据样例。

  • 其中corpus 是一个字符list;
  • corpus_update是为了增量学习;
  • corpus_unigrams 其实是corpus corpus_update,训练出来之后,模型里面保存的内容,意思为:[单词数量 word_count, 文档数量min_doc_count]
  • corpus_ndocs为一共的样本量;
  • query是查询语句;
  • document为比对语句。

2.2 数据训练

训练有常规训练 增量训练 模型保存 词条剪枝。训练的意思其实是统计词条频次 / 单词存在的文档数量两个数据。

2.2.1 常规训练与增量训练
代码语言:javascript复制
import os
import unittest
from tempfile import mkstemp 
from qdr import trainer

def _get_qd():
    qd = trainer.Trainer()
    qd.train(corpus)
    qd.train(corpus_update)
    return qd

这边初始化了训练器qd,给入训练的是字符列表corpus,如果有新的字符,可以继续qd.train()实现增量训练。 当然,还可以两个模型合并:

代码语言:javascript复制
qd = trainer.Trainer()
qd.train(corpus)
qd2 = trainer.Trainer()
qd2.train(corpus_update)
qd.update_counts_from_trained(qd2)   # 合并两个容器的训练集

譬如一个语料序列训练了qd,另一个训练了qd2,两个合并可以使用:update_counts_from_trained进行模型融合。

2.2.2 模型属性

此时,训练好的模型,可以通过qd._total_docs,qd._counts,两个函数观察到模型里面的参数:

代码语言:javascript复制
qd2._counts,qd2._total_docs
>>> ({'a': [1, 1],
  'deep': [1, 1],
  'down': [1, 1],
  'feet': [1, 1],
  'five': [1, 1]},
 3)

qd2._counts,得到的是模型保存的每个词条的属性:[单词出现次数 word_count,单词出现的文档数量min_doc_count] qd._total_docs,总文档数量。

2.2.2 模型保存
代码语言:javascript复制
qd = _get_qd()
t = mkstemp()

# save
qd.serialize_to_file(t[1])
# load
qd2 = trainer.Trainer.load_from_file(t[1])

os.unlink(t[1])# 文件夹删除

mkstemp()生成一个临时文件夹,serialize_to_file把模型保存在文件夹之中;load_from_file可以把保存的模型加载进去。

2.2.3 词条剪枝

词条剪枝就是一般的,删除掉词频比较低的词条。

代码语言:javascript复制
# 剪枝测试
qd = _get_qd()
print('-------------------------- origin')
print(qd._counts)
qd.prune(2, 0)
print('-------------------------- prune 1 ')
print(qd._counts)
print('-------------------------- prune 2 ')
qd.prune(2, 3)
print(qd._counts)

其中prune(2, 3),代表单词出现次数<2单词出现文档数量<3的一起进行删除。


2.3 模型Scoring环节

在training的基础上,统计词条频次 / 单词存在的文档数量两个数据,计算idf以及各个指标:tfidf 、bm25 、lm三款平滑方法。

代码语言:javascript复制
import unittest
import numpy as np
from qdr import ranker

corpus_unigrams = {
 'a': [1, 1],
 'deep': [1, 1],
 'down': [1, 1],
 'feet': [1, 1],
 'five': [1, 1],
 'from': [1, 1],
 'he': [2, 2],
 'needed': [1, 1],
 'shovel': [2, 1],
 'snow': [2, 2],
 'store': [2, 2],
 'the': [4, 3],
 'to': [2, 2],
 'was': [1, 1],
 'went': [1, 1],
}
# 其中,[单词数量 word_count, 文档数量min_doc_count]
corpus_ndocs = 3

def _get_qd():
    return ranker.QueryDocumentRelevance(corpus_unigrams, corpus_ndocs)

QueryDocumentRelevance计算了每个词条的idf:

代码语言:javascript复制
qd.get_idf('deep')   # 计算公式:np.log(corpus_ndocs / 1.0)
qd.get_idf('the')   # np.log(corpus_ndocs / 3.0)
qd.get_idf('not_in_corpus') # np.log(corpus_ndocs / 1.0)

其中,如何出现没有出现的词条,那么计数为1,计算公式为: np.log(corpus_ndocs / 1.0),其中corpus_ndocs 样本总条数。

2.3.1 文本比对

文本比对,单词比对两个功能,对于未知的词,idf中tf都记为1。

代码语言:javascript复制
# 未知/ 空缺的queries or documents,返回报错
qd = _get_qd()
query = ["buy", "snow", "shovel", "shovel"]
document = ["the", "store", "sells", "snow", "shovel", "snow"]

print(qd.score(document,query))  # 填入【document list , query list】
>>> {'tfidf': 0.8080392903006515, 'bm25': 3.0736956444773362, 'lm_jm': -10.839020864087779, 'lm_dirichlet': -11.344517596971485, 'lm_ad': -10.254189725660689}

一共会计算五种相似情况,其中,query和document都是分好词的,而且一定要有字符,不然会报错,如下:

代码语言:javascript复制
qd.score([],query) 
>>> ValueError: Document and query both need to be non-empty
qd.score([],[query]) 
>>> TypeError: expected string or Unicode object, list found

还有批量文本匹配的方法:

代码语言:javascript复制
# 批量预测
qd.score_batch(document, [query])  # ValueError: Document and query both need to be non-empty
qd.score_batch(document, [])  # []

document批量与其他队列进行匹配,document,与query的几个句子同时进行文本匹配,计算。

2.3.2 复现计算tfidf、bm25、三款lm模型

tfidf复现过程基本为: - 计算query_vectordoc_vector - 然后求相似:expected_score = np.sum(query_vector * doc_vector) / doc_length

代码语言:javascript复制
# 测试基于tfidf的相似度
qd = _get_qd()
query = ["buy", "snow", "shovel", "shovel"]
document = ["the", "store", "sells", "snow", "shovel", "snow"]

computed_score = qd.score(document, query)['tfidf']
print(u'计算结果',computed_score)

# 复现计算过程
max_query_tf = 2.0
query_vector = np.array([
             (0.5   0.5 / max_query_tf) * qd.get_idf("buy"),
             (0.5   0.5 / max_query_tf) * qd.get_idf("snow"),
             (0.5   0.5 * 2.0 / max_query_tf) * qd.get_idf("shovel")])

doc_vector = np.array([0.0,
              2.0 * qd.get_idf("snow"),
              1.0 * qd.get_idf("shovel")])

print(' doc_vector : ',doc_vector)
print(' query_vector : ',query_vector)

doc_length = np.sqrt(np.sum(np.array([
                1.0 * qd.get_idf("the"),
                1.0 * qd.get_idf("store"),
                1.0 * qd.get_idf("sells"),
                2.0 * qd.get_idf("snow"),
                1.0 * qd.get_idf("shovel")]) ** 2))

expected_score = np.sum(query_vector * doc_vector) / doc_length

print(u'复现结果',expected_score)

第二个算法bm25:

代码语言:javascript复制
# 测试基于bm25的相似度
qd = _get_qd()
query = ["buy", "snow", "shovel", "shovel"]
document = ["the", "store", "sells", "snow", "shovel", "snow"]

computed_score = qd.score(document, query)['bm25']
print(u'计算结果',computed_score)

# SUM_{t in query} log(N / df[t]) * (k1   1) * tf[td] /
# (k1 * ((1 - b)   b * (Ld / Lave))   tf[td])

k1 = 1.6
b = 0.75
Lave = sum([len(ele) for ele in corpus]
           [len(ele) for ele in corpus_update]) / float(corpus_ndocs)

score_buy = 0.0 # not in document
score_snow = np.log(float(corpus_ndocs) / corpus_unigrams['snow'][1]) 
    * (k1   1.0) * 2.0 / (k1 * ((1.0 - b)   b * (6.0 / Lave))   2.0)
score_shovel = 
    np.log(float(corpus_ndocs) / corpus_unigrams['shovel'][1]) 
    * (k1   1.0) * 1.0 / (k1 * ((1.0 - b)   b * (6.0 / Lave))   1.0)
actual_score = score_buy   score_snow   2.0 * score_shovel
actual_score

第三套语言模型LM:

代码语言:javascript复制
# Language Model
qd = _get_qd()
query = ["buy", "snow", "shovel", "shovel"]
document = ["the", "store", "sells", "snow", "shovel", "snow"]

computed_score1 = qd.score(document, query)['lm_jm']
computed_score2 = qd.score(document, query)['lm_dirichlet']
computed_score3 = qd.score(document, query)['lm_ad']
print(u'lm_jm computed_score',computed_score1)
print(u'lm_dirichlet computed_score',computed_score2)
print(u'lm_ad computed_score',computed_score3)

# lm模型有三款:

lam = 0.1
mu = 2000.0
delta = 0.7

jm = 0.0
dirichlet = 0.0
ad = 0.0
sum_w_cwd_doc = float(len(document))
nwords_corpus = sum(v[0] for v in corpus_unigrams.itervalues())
n2p1 = len(corpus_unigrams)   nwords_corpus   1
for word in query:
    try:
        word_count_corpus = corpus_unigrams[word][0]
    except KeyError:
        word_count_corpus = 0
    corpus_prob = (word_count_corpus   1.0) / n2p1

    cwd = 0
    for doc_word in document:
        if doc_word == word:
            cwd  = 1

    if cwd == 0:
        # not in document
        jm  = np.log(lam * corpus_prob)
        dirichlet  = np.log(mu / (sum_w_cwd_doc   mu) * corpus_prob)
        ad  = np.log(
            delta * len(set(document)) / sum_w_cwd_doc * corpus_prob)
    else:
        jm  = np.log(
                (1.0 - lam) * cwd / sum_w_cwd_doc   lam * corpus_prob)
        dirichlet  = np.log(
                (cwd   mu * corpus_prob) / (sum_w_cwd_doc   mu))
        ad  = np.log(
           max(cwd - delta, 0.0) / sum_w_cwd_doc  
           delta * len(set(document)) / sum_w_cwd_doc * corpus_prob)

jm,dirichlet,ad

2.4 模型保存与加载

代码语言:javascript复制
# 外部加载数据
import os
from tempfile import mkstemp
from qdr.trainer import write_model

corpus_unigrams = {
 'a': [1, 1],
 'deep': [1, 1],
 'down': [1, 1],
 'feet': [1, 1],
 'five': [1, 1],
 'from': [1, 1],
 'he': [2, 2],
 'needed': [1, 1],
 'shovel': [2, 1],
 'snow': [2, 2],
 'store': [2, 2],
 'the': [4, 3],
 'to': [2, 2],
 'was': [1, 1],
 'went': [1, 1],
}
# 其中,[corpus corpus_update数据集的单词个数,corpus数据集中的单词个数]
corpus_ndocs = 3


t = mkstemp()
write_model(corpus_ndocs, corpus_unigrams, t[1])  #  把corpus_ndocs,corpus_unigrams导出到临时文件夹之中
qd = ranker.QueryDocumentRelevance.load_from_file(t[1]) # 重新load进来

通过write_model把信息保存,然后通过load_from_file加载进来。


2.5 trianing scoring过程结合

第一种结合的方式:train()

代码语言:javascript复制
from qdr import Trainer
qd= Trainer()
qd.train(corpus)

# 第一种方式
t = mkstemp()
qd.serialize_to_file(t[1])
scorer = QueryDocumentRelevance.load_from_file(t[1])
relevance_scores = scorer.score(query[0], document[0])
relevance_scores

# 第二种方式
scorer = QueryDocumentRelevance(qd._counts,qd._total_docs)
relevance_scores = scorer.score(query[0], document[0])
relevance_scores

qd.train(corpus)的过程其实就是统计词条频次 / 单词存在的文档数量两个数据,接下来导给QueryDocumentRelevance,可以直接qd._counts,qd._total_docs,也通过serialize_to_file可以先保存,然后load_from_file进去。

0 人点赞