序
- 自己在看这个开源代码中看到了这个相似性算法和一些工程中的技巧,感觉很不错,算是多了点儿见识,以前还从没有用过稀疏矩阵这个存储结构,这里就写一个文档简单记录一下
python小知识
- Python中关于eval函数与ast.literal_eval使用的区别介绍(图文)
- https://www.php.cn/python-tutorials-376459.html
- numpy的广播机制(具体没有用,算是回顾一下):
- https://www.cnblogs.com/jiaxin359/p/9021726.html
BM25
- 首先,我们抛却一切,需要知道这个算法是什么,推导过程以及背景什么的就不叙述了,可以参考但不限于 这篇博客
- 归根结底,我们的算法其实就是一个计算公式,如下:
普通代码
代码工程优化
- 这个开源库一共从两个方面优化了计算方式,具体可以测试跑跑代码,个人测试数据就是CDQA这个开源库的数据:
-
- 稀疏存储: 节约空间。
-
- 向量化矩阵计算:加速以及简洁。
-
- 在上述向量化的基础上,作者只针对非零元素进行计算bm分数,进一步加速计算速度。
代码
- 原始库里的代码逻辑如图:
- 自己把这块代码单独从库里提出来实验了一下,自己注释的有点乱
依赖和超参
import numpy as np
import scipy.sparse as sp
from sklearn.utils.validation import check_is_fitted, check_array, FLOAT_DTYPES
from sklearn.feature_extraction.text import _document_frequency
from sklearn.feature_extraction.text import CountVectorizer
import pandas as pd
from ast import literal_eval
use_idf = True
floor = None
k1 = 2.0
b = 0.75
norm = None
主体函数
def fit(X):
"""
Parameters
----------
X : sparse matrix, [n_samples, n_features]
document-term matrix
"""
X = check_array(X, accept_sparse=("csr", "csc"))
# x是否为sparse类型
# 压缩稀疏列矩阵
if not sp.issparse(X):
X = sp.csc_matrix(X)
# 如果用idf
if use_idf:
n_samples, n_features = X.shape
# (57164,),所有文档对应单词的频率
df = _document_frequency(X)
# (57164,)
idf = np.log((n_samples - df + 0.5) / (df + 0.5))
if floor is not None:
idf = idf * (idf > floor) + floor * (idf < floor)
# 创建对角稀疏矩阵,其实就是一维变成2维
_idf_diag = sp.spdiags(idf, diags=0, m=n_features, n=n_features)
print(_idf_diag.shape)
# Create BM25 features
# Document length (number of terms) in each row
# Shape is (n_samples, 1)
dl = X.sum(axis=1)
# Number of non-zero elements in each row
# Shape is (n_samples, )
sz = X.indptr[1:] - X.indptr[0:-1]
# In each row, repeat `dl` for `sz` times
# Shape is (sum(sz), )
# Example
# -------
# dl = [4, 5, 6]
# sz = [1, 2, 3]
# rep = [4, 5, 5, 6, 6, 6]
# 每一个单词对应的文档长度
rep = np.repeat(np.asarray(dl), sz)
print(rep.shape)
# Average document length
# Scalar value
avgdl = np.average(dl)
# Compute BM25 score only for non-zero elements
# 实验一下整个计算
# X: 每个文档中每一个单词的个数
# print(X.shape)
# print(X.data.shape)
# print(rep.shape)
# print(avgdl.shape)
# (非零元素_num,)
# X.data表示的是其中非零元素,算是加速计算
data = (
X.data
* (k1 + 1)
/ (X.data + k1 * (1 - b + b * rep / avgdl))
)
# 恢复结构
X = sp.csr_matrix((data, X.indices, X.indptr), shape=X.shape)
if norm:
X = normalize(X, norm=norm, copy=False)
_doc_matrix = X
return _doc_matrix
函数调用
# 出于安全考虑,对字符串进行类型转换的时候,最好使用ast.literal_eval()函数!
rdf = pd.read_csv('/home/lixiang/桌面/cdqa/cdQA-master/path-to-directory/bnpp_newsroom-v1.1.csv', converters={'paragraphs': literal_eval})
raw_documents = rdf["paragraphs"].apply(lambda x: " ".join(x))
wow = CountVectorizer( input="content",
encoding="utf-8",
decode_error="strict",
strip_accents=None,
lowercase=True,
preprocessor=None,
tokenizer=None,
analyzer="word",
stop_words=None,
token_pattern=r"(?u)\b\w\w+\b",
ngram_range=(1, 2),
max_df=1.0,
min_df=1,
max_features=None,
vocabulary=None,
binary=False,
dtype=np.float64)
X = wow.fit_transform(raw_documents=raw_documents)
# 传入的是:每一个样本,对应单词的个数
a = fit(X)
END
- 本文没有写过多细节的介绍,工程这个东西还是需要读源码自己体会一下。