文本相似度的计算广泛的运用在信息检索,搜索引擎, 文档复制等处:
因此在各种不同的情况与任务中,有不同的文本相似度计算。
近期在处理搜索引擎的相关项目
下面介绍一下我们主要使用的相似度计算方式及其实现 Github
余弦相似度是纯数学中的概念,首先,将进行计算的两个str中的word抽取出来,用作非重复词库。
遍历词库,将两个句子的表示向量化: 每个向量长度为 词库大小
import numpy as np
def cosine_similarity(sentence1: str, sentence2: str) -> float:
"""
compute normalized COSINE similarity.
:param sentence1: English sentence.
:param sentence2: English sentence.
:return: normalized similarity of two input sentences.
"""
seg1 = sentence1.strip(" ").split(" ")
seg2 = sentence2.strip(" ").split(" ")
word_list = list(set([word for word in seg1 + seg2]))
word_count_vec_1 = []
word_count_vec_2 = []
for word in word_list:
word_count_vec_1.append(seg1.count(word))
word_count_vec_2.append(seg2.count(word))
vec_1 = np.array(word_count_vec_1)
vec_2 = np.array(word_count_vec_2)
num = vec_1.dot(vec_2.T)
denom = np.linalg.norm(vec_1) * np.linalg.norm(vec_2)
cos = num / denom
sim = 0.5 + 0.5 * cos
return sim
编辑距离是指文本A变为文本B的处理次数
处理包含:
- 删除一个字符
- 增加一个字符
- 修改一个字符
例如:
A: requiremant
B: requirements
step1: requiremant -> requirement
step2: requirement -> requirements
所以编辑距离为2
# Using difflib is python 3.6 default package
import difflib
def compute_levenshtein_distance(sentence1: str, sentence2: str) -> int:
"""
compute levenshtein distance.
"""
leven_cost = 0
s = difflib.SequenceMatcher(None, sentence1, sentence2)
for tag, i1, i2, j1, j2 in s.get_opcodes():
if tag == 'replace':
leven_cost += max(i2 - i1, j2 - j1)
elif tag == 'insert':
leven_cost += (j2 - j1)
elif tag == 'delete':
leven_cost += (i2 - i1)
return leven_cost
利文斯顿距离即为编辑距离
而做相似度度量时,所需要的范围为0-1之间浮点数
因此需要将上述的编辑距离整数值转换为normalize的分数
def compute_levenshtein_similarity(sentence1: str, sentence2: str) -> float:
"""Compute the hamming similarity."""
leven_cost = compute_levenshtein_distance(sentence1, sentence2)
return 1 - (leven_cost / len(sentence2))
计算字符串之间的相似度,步骤如下:
- 分词 :将字符串之间分词(中文需要分词,英文本身具有空格)
- HASH :将每个单词进行hash,生成长度相同的01序列
- 加权 :计算每个单词本身的权重,例如 -4, 5 等等不同的权重,与0/1字符串相乘,1为正, 0为负, 例如:
- W(清华) = 100101*4 = 4 -4 -4 4 -4 4
- W(大学)=101011*5 = 5 -5 5 -5 5 5
- 合并 : 将上述各个词语,与权重相乘之后的hash数值相加
- W(清华大学) = (4+5) (-4-5) (-4+5) (4-5) (-4+5) (4+5) = 9 -9 1 -1 1 9
- 降维: 将上述数值进行降维, ≥ 1 \ge 1 ≥1的转换为1, ≤ 0 \le 0 ≤0的转换为0
- W(清华大学) = 1 0 1 0 1 1
则可以计算出 句子的代表数值,同样长度的句子0/1数值对应位置进行与运算,true值数量与句子总长度比例,则可以得出hamming distance.
此处代码使用了 python simHash package.
安装与使用tutorial
import re
from simhash import Simhash
def compute_simhash_hamming_similarity(sentence1: str, sentence2: str) -> float:
"""need to normalize after compute!"""
def get_features(s):
width = 3
s = s.lower()
s = re.sub(r'[^\w]+', '', s)
return [s[i:i + width] for i in range(max(len(s) - width + 1, 1))]
hash_value1 = Simhash(get_features(sentence1)).value
hash_value2 = Simhash(get_features(sentence2)).value
return compute_levenshtein_similarity(str(hash_value1), str(hash_value2))
S a S_a Sa 是sentence A
S b S_b Sb 是sentence B
其单词交集与单词并集的比例即为杰卡德相似系数
(若遇见相同单词,则需要看数量重复是否占比很高,具体情况而定。两项处理均可。)
S a ∩ S b S a ∪ S b \frac{S_a \cap S_b}{S_a \cup S_b} Sa∪SbSa∩Sb
def compute_jaccard_similarity(sentence1: str, sentence2: str) -> float:
word_set1 = set(sentence1.strip(" ").split(" "))
word_set2 = set(sentence2.strip(" ").split(" "))
return len(word_set1 & word_set2) / len(word_set1 | word_set2)
TF其实由两项组成:
- TF: Term Frequency
- IDF: Inverse Document Frequency
TF 代表了词语在文档中出现的频率,当进行索引的时候,词语出现频率较高的文本,匹配度也会较高,但是某些停止词,例如 to 在文本中会出现相当多的次数,但这对匹配并没有起到很好的索引作用,因此需要引入另一个度量值 IDF(逆文本频率)
I D F = l o g N N ( x ) IDF = log\frac{N}{N(x)} IDF=logN(x)N
其中 N N N为语料库中文本的总数, N ( x ) N(x) N(x)为文本中出现单词 x x x的文本数量。
次可以度量该单词的重要程度。
某些特殊情况下, x x x并未出现在语料库中(所有文本),则需要考虑将公式平滑为:
I D F = l o g N + 1 N ( x ) + 1 + 1 IDF = log\frac{N+1}{N(x)+1}+1 IDF=logN(x)+1N+1+1
最终的TF-IDF值为:
T F − I D F ( x ) = T F ( x ) ∗ I D F ( x ) TF-IDF(x) = TF(x) * IDF(x) TF−IDF(x)=TF(x)∗IDF(x)
此处我们调用了nltk工具库来实现TF-IDF计算:
由于query可能有2-3个单词同时建立索引,此处采用了平均值。
from nltk.text import TextCollection
from nltk.tokenize import word_tokenize
def compute_tf_idf_similarity(query: str, content: str, compute_type: str) -> float:
"""
Compute the mean tf-idf or tf
similarity for one sentence with multi query words.
:param query: a string contain all key word split by one space
:param content: string list with every content relevent to this query.
:return: average tf-idf or tf similarity.
"""
sents = [word_tokenize(content), word_tokenize("")] # add one empty file to smooth.
corpus = TextCollection(sents) # 构建语料库
result_list = []
for key_word in query.strip(" ").split(" "):
if compute_type == "tf_idf":
result_list.append(corpus.tf_idf(key_word, corpus))
elif compute_type == "tf":
result_list.append(corpus.tf(key_word, corpus))
else:
raise KeyError
return sum(result_list) / len(result_list)
注意此处使用pip install nltk是仍然可能出现问题的,nltk是一个很大的自然语言处理库,里面仍然有许多资源没有被安装,可能在使用过程中仍然会出现问题,提示需要下载punkt 资源
只要在任何地方加入
import nltk
nltk.download("punkt")
下载完毕之后即可删除这两行,因为资源已经在python的package中了。代码便不会有其他的问题。