前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >练习题 - 基于快速文本标题匹配的知识问答实现(一,基础篇)

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

作者头像
悟乙己
发布2019-05-27 14:58:39
8150
发布2019-05-27 14:58:39
举报
文章被收录于专栏:素质云笔记素质云笔记

版权声明:博主原创文章,微信公众号:素质云笔记,转载请注明来源“素质云博客”,谢谢合作!! 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 处理速度不错,有一些参数可以自行看着调整:

可能项目底层技术本身在热火朝天的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进去。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2018年08月30日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • —– 目 录 —–
  • 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过程结合
              相关产品与服务
              NLP 服务
              NLP 服务(Natural Language Process,NLP)深度整合了腾讯内部的 NLP 技术,提供多项智能文本处理和文本生成能力,包括词法分析、相似词召回、词相似度、句子相似度、文本润色、句子纠错、文本补全、句子生成等。满足各行业的文本智能需求。
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档