文本相似度计算

句子相似度计算的方法

在收集一些文本资料时遇到一个问题:怎么快速去掉重复的内容?记录一下解决办法。

一、文本向量化

在文本处理中,经常需要判断两篇文档是否相似,或者把类似的句子归到一起。在对语料进行处理时,可以根据句子间的相似度来判断。文本相似度是一种非常有用的工具,可以帮助我们解决此类很多问题。计算文本相似度第一步是要把文本向量化,转换成可以计算的形式。

二、词袋模型

文本向量化的基本的方法之一是利用词袋模型。

用词袋模型将下面3句话转化成文本向量。

It was the best of times
it was the worst of times
it was the age of wisdom

先统计出语料中出现过的单词,所有出现过的单词组成一个”词汇表“。

“it”
“was
“the”
“best”
“of”
“times”
“worst”
“age”
“wisdom”

词典中总共有9个单词,使用一个长度为9的向量来表示文档。其中,向量中每个位置的值为该位置对应单词的评分。其中一种最简单的评分方式是:将该位置的单词的评分作为布尔值,1表示该位置对应的单词在文档中出现了;0则表示该位置对应的单词没有出现。这种方式也叫one-hot编码。

以第1个文档“It was the best of times”为例,这篇文档的评分如下:

“it” = 1
“was = 1
“the” = 1
“best” = 1
“of” = 1
“times” = 1
“worst” = 0
“age” = 0
“wisdom” = 0

因此,“It was the best of times”生成的文本向量为 [1, 1, 1, 1, 1, 1, 0, 0, 0,]

同理:
‘it was the worst of times’ = [1, 1, 1, 0, 1, 1, 1, 0, 0]
‘it was the age of wisdom’ = [1, 1, 1, 0, 1, 0, 0, 1, 1]

三、词汇表的处理

随着词汇表增大,文档向量的维度同样也会增大,对于一个非常大的语料库词汇表的大小或者向量的维度或许将达到上百万。此外,每一篇文档中可能只有极少数单词出现在词汇表中。那么这就会导致向量中会有很多0,称为稀疏向量或稀疏表示。稀疏向量在计算时需要大量的内存和计算资源。因此,在使用词袋模型时,需要减小词汇表的大小。一种方式就是对原始文本进行清理,有一些简单的文本清理方法,例如:

  1. 忽略大小写
  2. 忽略标点符号
  3. 忽略那些出现频率高但没有包含很多信息的单词,如“a”,“of”等,这类词称为停用词(stop words)
  4. 恢复拼写错误的单词
  5. 使用词干提取算法将单词简化为词干(例如从“playing”变成“play”)

另一种更复杂的方法是创建一个由单词组构成的词汇表,这既改变了词汇表的范围,又允许词汇表从文档中获取更多的含义,这种方法称为n-gram。

在这个方法中,每个单词(符号)被称为“元”,那么创建由两个单词组成的对构成的词汇表称为二元模型(bigram model)。只有出现在语料库中的二元组被建模,而不是所有可能的二元组。

比如前面的文档“It was the best of times”的二元组如下:

“it was”
“was the”
“the best”
“best of”
“of times”

同样经常使用的一种n-gram模型是三元模型(trigram model),它是由连续的三个单词构成的。以上面的例子为例,它的三元组表示:

“it was the”
“was the best”
“the best of”
“best of times”

四、单词的评分

确定好了词表,就需要开始对文档中的词汇进行评分。除了前面的one-hot向量的形式。还有一些其他的评分方式,比如:

  1. 计数(counts):统计一篇文档中每个单词出现的次数
  2. 频率(frequencies):计算文档中每个单词在所有单词中出现的频率
  3. TF-IDF: $TFIDF=TF\times IDF$,其中,$TF=\frac{m}{n}$,$m$表示词语在整个文档中出现的次数,$n$表示该文档中词语总数;IDF为逆文本频率,假设整个语料有n篇文档,而一个词语在k篇文档中出现,则某个单词w的$IDF(w)=log(\frac{n}{k+1})$

五、文本相似度计算

文本向量化后,相似度的计算可以转化成向量间距离的计算。

在计算机自然语言处理中,距离(distance)、相似度(similarity)是经常出现的基本概念,这些概念大多源于数学领域的度量(metric)、测度(measure)等概念。

常用于文本相似度判断的:

  1. 欧式距离(Euclidean Distance):
    $d=\sqrt{\sum(x_i-y_i)^2}$,$similarity = \frac{1}{d+1}$
    分母d加1用来保证相似度最高为1,也可以根据具体问题重新定义。
  2. 余弦相似度。简单来说就是计算两个向量间的夹角的余弦值作为相似度,适用于文本较短时。
    $$
    S={\frac{A \cdot B}{|A| |B|} =\frac{\sum A \times B}{\sqrt{\sum A^2}\times\sqrt{\sum B^2}} }
    $$
  3. Jacard相似度。杰卡德相似度一般被用来度量两个集合之间的差异大小。两个集合共有的元素越多,二者越相似。为了控制距离的取值范围,可以增加一个分母,也就是两个集合拥有的所有元素。
    假设有两个集合A和B,那么二者的杰卡德相似度为:
    $$
    J(A,B)={|A \bigcap B|\over |A \bigcup B|} =\frac{len(A\ and\ B)}{len(A\ or\ B)}
    $$
  4. 海明距离(Hamming Distance)。逐个字符(或逐位)对比,统计不一样的位数的个数总和。
    $$d=\sum_{i=1}^Nr_i$$
    其中
    $$
    r_i=
    \begin{cases}
    1 &if\ a_i \neq\ b_i\\
    0, &if\ a_i = b_i\
    \end{cases}
    $$

六、R语言实现

library(jiebaR) # 分词包

# step1:分词
chr <- readLines("xxx.txt",encoding = "UTF-8") # 每行一句文本
names(chr) <- 1:length(chr)
engine <- worker(stop_word = "stopword.txt") # 停用词   可加dictionary指定专有词
chr_seg <- sapply(chr, function (x) segment(x,engine))

# step2:词汇表                  
vocab <- Reduce(union,chr_seg)

# step3:单词的评分 
# 利用词频作为评分
chr_tb <- sapply(chr_seg, table)
i = 1
vocab <- matrix(NA,nrow = length(vocab),ncol = length(chr),
                    dimnames = list(vocab,1:length(chr)))
for (x in chr_tb) {
  vocab[names(x),i] <-  as.vector(x)
  i = i+1
}
vocab[is.na(vocab)] <- 0

# 词汇表过滤
tmp <- apply(vocab != 0, 1, sum)
vacab_mat_filter <- vocab[as.vector(tmp>= 10),] 

# step4:相似度计算
# 余弦相似度 (1代表完全相同)
chr_cos_sim <- function(a,b){
  return(sum(a*b)/(sqrt(sum(a^2))*sqrt(sum(b^2))))
}

text_sim_mat <- matrix(nrow = length(chr),ncol = length(chr))
vocab_mat <- t(vacab_mat_filter)
for (i in 1:length(chr)) {
  for (k in i:length(chr)) {
    text_sim_mat[i,k] <- chr_cos_sim(vocab_mat[i,],vocab_mat[k,])   
  }
}

# save(text_sim_mat,file = "text_sim_mat.Rdata")
# load("text_sim_mat.Rdata")
text_sim_mat <- round(text_sim_mat,digits = 3)
# 余弦相似度大于0.9的文本
indx <- which(text_sim_mat > 0.90 & text_sim_mat < 1.0,arr.ind = T) 

留下评论