# 文档相似性
文档相似性或者文档间距离是信息提取任务的中心主题。
人类是如何定义相似文本的?一般文本语义相近或者描述相似概念时被认为是相似的。另一方面,相似性还被用来进行重复性检验。我们将会回顾几个常见的例子。
text2vec 包提供了两套函数用来评价多种距离和相似性。所有函数都十分关注计算性能和内存的效率:
sim2(x, y, method)
- 计算两个矩阵,x和y,每一行的使用指定方法的相似性psim2(x, y, method)
- 计算两个矩阵,x和y,每一行的并行相似性。dist2(x, y, method)
- 计算两个矩阵,x和y,每一行的使用指定方法的距离dist2(x, y, method)
- 计算两个矩阵,x和y,每一行的使用指定方法的并行距离各个方法含有后缀 2
,因为与 dist()
函数不同,这些函数作用在两个矩阵上,而不是一个矩阵上。
下面的方法已经实现了:
使用 text2vec::moview_review
,并对数据进行预处理:
library(stringr)
library(text2vec)
data("movie_review")
# select 500 rows for faster running times
movie_review = movie_review[1:500, ]
prep_fun = function(x) {
x %>%
# make text lower case
str_to_lower %>%
# remove non-alphanumeric symbols
str_replace_all("[^[:alnum:]]", " ") %>%
# collapse multiple spaces
str_replace_all("\\s+", " ")
}
movie_review$review_clean = prep_fun(movie_review$review)
定义两个文档,并用来计算距离:
doc_set_1 = movie_review[1:300, ]
it1 = itoken(doc_set_1$review_clean, progressbar = FALSE)
# specially take different number of docs in second set
doc_set_2 = movie_review[301:500, ]
it2 = itoken(doc_set_2$review_clean, progressbar = FALSE)
我们会在一个向量空间里比较文档,因此我们需要定义一个通用空间,并把文档映射过去。我们将会使用基于词汇表的向量化来获得更好的解释性:
it = itoken(movie_review$review_clean, progressbar = FALSE)
v = create_vocabulary(it) %>% prune_vocabulary(doc_proportion_max = 0.1, term_count_min = 5)
vectorizer = vocab_vectorizer(v)
Jaccard similarity 是一个简单但是易懂的衡量相似性的方法。
\[J(doc_1, doc_2) = \frac{doc_1 \cap doc_2}{doc_1 \cup doc_2}\]
对于每个文档,我们使用共有词汇占独立词汇的比例作为文档的衡量单位。
在 NLP 领域中,jaccard similarity 对于重复性检验十分有用。 text2vec 实现了一个通用高效的方法,可以运用在其他应用程序中。
为了计算两个文档的 jaccard similarity ,用户需要提供每个项目的 DTM,DTM 应该在同一个向量空间中。
# they will be in the same space because we use same vectorizer
# hash_vectorizer will also work fine
dtm1 = create_dtm(it1, vectorizer)
dim(dtm1)
## [1] 300 2339
dtm2 = create_dtm(it2, vectorizer)
dim(dtm2)
## [1] 200 2339
一旦我们拥有文档在向量空间中的表示,我们只需要运行 sim2()
:
d1_d2_jac_sim = sim2(dtm1, dtm2, method = "jaccard", norm = "none")
检查结果:
dim(d1_d2_jac_sim)
## [1] 300 200
d1_d2_jac_sim[1:2, 1:5]
## 2 x 5 sparse Matrix of class "dgCMatrix"
## 1 2 3 4 5
## 1 0.02142857 . 0.02362205 0.007575758 0.02597403
## 2 0.01204819 . 0.02898551 0.013698630 0.02061856
我们也可以计算每一行的并行相似性:
dtm1_2 = dtm1[1:200, ]
dtm2_2 = dtm2[1:200, ]
d1_d2_jac_psim = psim2(dtm1_2, dtm2_2, method = "jaccard", norm = "none")
str(d1_d2_jac_psim)
## Named num [1:200] 0.02143 0 0.00735 0 0.03311 ...
## - attr(*, "names")= chr [1:200] "1" "2" "3" "4" ...
sim2()
and psim2()
还有类似的函数 dist2()
, pdist2()
用来计算不相似性。
注意,绝大多数的例子的相似性为 0,这会导致结果较为稀疏,sim2
的输出矩阵是一个稀疏矩阵。
对于大稀疏矩阵使用 dist2()
时需要格外小心。
传统的计算语义相似性的方法是使用内容的重合程度。为了达到这个目的,我们将文档以 bag-of-words 的形式表示,这样文档会成为一个稀疏矩阵,使用夹角来作为距离的量度。
\[similarity(doc_1, doc_2) = cos(\theta) = \frac{doc_1 doc_2}{\lvert doc_1\rvert \lvert doc_2\rvert}\]
余弦距离定义为:
\[distance(doc_1, doc_2) = 1 - similarity(doc_1, doc_2)\]
注意,这不是一个严格的距离定义,因为它不满足三角不等式等条件。
计算余弦相似性和 jaccard similarity 类似。
d1_d2_cos_sim = sim2(dtm1, dtm2, method = "cosine", norm = "l2")
检查结果:
dim(d1_d2_cos_sim)
## [1] 300 200
d1_d2_cos_sim[1:2, 1:5]
## 2 x 5 sparse Matrix of class "dgCMatrix"
## 1 2 3 4 5
## 1 0.02703999 . 0.05063299 0.009500143 0.02753954
## 2 0.02440658 . 0.06528840 0.034299717 0.03977196
我们也可以使用 Tf-Idf 来计算相似性:
dtm = create_dtm(it, vectorizer)
tfidf = TfIdf$new()
dtm_tfidf = fit_transform(dtm, tfidf)
计算 dtm_tfidf
矩阵的相似性:
d1_d2_tfidf_cos_sim = sim2(x = dtm_tfidf, method = "cosine", norm = "l2")
d1_d2_tfidf_cos_sim[1:2, 1:5]
## 2 x 5 sparse Matrix of class "dgCMatrix"
## 1 2 3 4 5
## 1 1.000000000 0.007850872 0.02380155 0.02864296 0.01510648
## 2 0.007850872 1.000000000 0.01115547 . .
tf-idf/bag-of-words 模型有很多噪声,使用 LSA 模型可以帮助解决这个问题,你可以实现更高质量的相似性:
lsa = LSA$new(n_topics = 100)
dtm_tfidf_lsa = fit_transform(dtm_tfidf, lsa)
计算 dtm_tfidf_lsa
矩阵的相似性:
d1_d2_tfidf_cos_sim = sim2(x = dtm_tfidf_lsa, method = "cosine", norm = "l2")
d1_d2_tfidf_cos_sim[1:2, 1:5]
## 1 2 3 4 5
## 1 1.0000000 0.1699811 0.3688836 0.32099516 0.40136353
## 2 0.1699811 1.0000000 0.2255552 0.03386353 0.05705533
并行相似性:
x = dtm_tfidf_lsa[1:250, ]
y = dtm_tfidf_lsa[251:500, ]
head(psim2(x = x, y = y, method = "cosine", norm = "l2"))
## 1 2 3 4 5 6
## 0.21685168 0.19825486 0.30505822 0.10039961 0.29248642 0.03675954
欧式距离在自然语言处理中不如 Jaccard 或者余弦相似性常用。但是我们仍然值得尝试不同的方法。text2vec 支持稠密矩阵的输入。
x = dtm_tfidf_lsa[1:300, ]
y = dtm_tfidf_lsa[1:200, ]
m1 = dist2(x, y, method = "euclidean")
我们还可以使用 L2 正则化:
m2 = dist2(x, y, method = "euclidean", norm = "l1")
m3 = dist2(x, y, method = "euclidean", norm = "none")