# 文档相似性

文档相似性或者文档间距离是信息提取任务的中心主题。

人类是如何定义相似文本的?一般文本语义相近或者描述相似概念时被认为是相似的。另一方面,相似性还被用来进行重复性检验。我们将会回顾几个常见的例子。

API

text2vec 包提供了两套函数用来评价多种距离和相似性。所有函数都十分关注计算性能和内存的效率:

  1. sim2(x, y, method) - 计算两个矩阵,x和y,每一行的使用指定方法的相似性
  2. psim2(x, y, method) - 计算两个矩阵,x和y,每一行的并行相似性。
  3. dist2(x, y, method) - 计算两个矩阵,x和y,每一行的使用指定方法的距离
  4. dist2(x, y, method) - 计算两个矩阵,x和y,每一行的使用指定方法的并行距离

各个方法含有后缀 2,因为与 dist() 函数不同,这些函数作用在两个矩阵上,而不是一个矩阵上。

下面的方法已经实现了:

  1. Jaccard 距离
  2. 余弦距离
  3. 欧式距离
  4. Relaxed Word Mover’s Distance

实际例子

使用 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

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() 时需要格外小心。

Cosine similarity

传统的计算语义相似性的方法是使用内容的重合程度。为了达到这个目的,我们将文档以 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

Cosine similarity with Tf-Idf

我们也可以使用 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 .          .

Cosine similarity with LSA

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")
text2vecDmitry Selivanov 和其他开发者一起开发。 © 2016.
如果您发现了 bugs 等问题,请到GitHub 报告。