版权声明:博主原创文章,微信公众号:素质云笔记,转载请注明来源“素质云博客”,谢谢合作!! 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()
实现增量训练。
当然,还可以两个模型合并:
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
,两个函数观察到模型里面的参数:
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:
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_vector
、 doc_vector
- 然后求相似:expected_score = np.sum(query_vector * doc_vector) / doc_length
# 测试基于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
进去。