概述: 通常大家说的文本相似度多指文本语义的相似度,但是对于部分场景却会非常关注文本字面上的相似度,比如对于内容投放平台需要去判重,这就涉及看文本是否字面大量重复,需要去比较文本的字面相似度。
梳理了下常用的字面相似度算法:
文本字面相似度包括:
- Simhash
- 序列相似度(Sequence Similarity)
- Jaccard相似度
- 余弦相似度(Cosine Similarity)
- Levenshtein距离
- N-gram相似度
各种算法的概述:
1、Simhash相似度
-
主要思想: Simhash是一种局部敏感哈希算法,旨在快速检测近似重复的文档。它将长文本或文档转换为固定长度的指纹(通常是64位整数),使得相似的文档会产生相似的指纹。SimHash算法是Google在2007年发表的论文《Detecting Near-Duplicates for Web Crawling》中提到的一种指纹生成算法,被应用在Google搜索引擎网页去重的工作之中。
实现步骤:
- 将文档分割成特征(如词或n-gram)
- 对每个特征计算哈希值
- 对每个哈希值的每一位进行加权
- 合并所有特征的加权结果
- 对合并结果进行阈值化,得到最终的指纹
应用场景:
- 网页去重
- 文档相似度检测
- 抄袭检测
对于文本去重这个问题,常见的解决办法有余弦算法、欧式距离、Jaccard相似度、最长公共子串等方法。但是这些方法并不能对海量数据高效的处理。
比如说,在搜索引擎中,会有很多相似的关键词,用户所需要获取的内容是相似的,但是搜索的关键词却是不同的,如“北京好吃的火锅“和”哪家北京的火锅好吃“,是两个可以等价的关键词,然而通过普通的hash计算,会产生两个相差甚远的hash串。而通过SimHash计算得到的Hash串会非常的相近,从而可以判断两个文本的相似程度。
通过对不同文本的SimHash值进而比较海明距离,从而判断两个文本的相似度。海明距离越小,相似度越低(根据 Detecting Near-Duplicates for Web Crawling 论文中所说),一般海明距离为3就代表两篇文章相同。
什么是海明距离呢?
简单的说,海明距离(Hamming distance)可以理解为,两个二进制串之间相同位置不同的个数。
举个例子,[1,1,1,0,0,0]和[1,1,1,1,1,1]的海明距离就是3。
在处理大规模数据的时候,我们一般使用64位的SimHash,正好可以被一个long型存储。这种时候,海明距离在3以内就可以认为两个文本是相似的。
实现代码:
from simhash import Simhash
def simhash_similarity(text1, text2):
"""计算两个文本的Simhash相似度"""
hash1 = Simhash(get_features(text1))
hash2 = Simhash(get_features(text2))
distance = hash1.distance(hash2)
return 1 - (distance / 64) # 64
2、序列相似度(基于最长公共子序列LCS)
主要思想: 通过找到两个序列中最长的公共子序列来衡量它们的相似度。子序列不需要连续,但必须保持原始顺序。
实现步骤:
- 构建一个矩阵来存储两个序列的所有子问题的解
- 填充矩阵,每个单元格表示到当前位置的LCS长度
- 回溯矩阵以构建最长公共子序列
- 计算相似度:LCS长度 / max(序列1长度, 序列2长度)
应用场景:
- 文本比较和差异检测
- 生物信息学中的DNA序列比对
- 版本控制系统中的文件比较
实现代码:
from difflib import SequenceMatcher
def sequence_similarity(text1, text2):
"""计算两个文本的序列相似度"""
matcher = SequenceMatcher(None, text1, text2)
return matcher.ratio()
3、Jaccard相似度
主要思想: 衡量两个集合的相似度,通过计算它们交集的大小除以并集的大小。
实现步骤:
- 将文本转换为集合(如单词集或字符集)
- 计算两个集合的交集
- 计算两个集合的并集
- 相似度 = 交集大小 / 并集大小
应用场景:
- 文档相似度计算
- 推荐系统
- 聚类分析
实现代码
def jaccard_similarity(text1, text2):
"""计算两个文本的Jaccard相似度"""
set1 = set(text1)
set2 = set(text2)
intersection = set1.intersection(set2)
union = set1.union(set2)
return len(intersection) / len(union)
4、余弦相似度
主要思想: 将文本表示为向量空间中的点,然后计算这些向量之间的夹角余弦值来衡量相似度。
实现步骤:
- 将文本转换为词频向量
- 计算两个向量的点积
- 计算每个向量的模长
- 相似度 = 点积 / (向量1模长 * 向量2模长)
应用场景:
- 信息检索
- 文本分类
- 推荐系统
代码:
from collections import Counter
def cosine_similarity(text1, text2):
"""计算两个文本的余弦相似度"""
vec1 = Counter(text1)
vec2 = Counter(text2)
intersection = set(vec1.keys()) & set(vec2.keys())
numerator = sum([vec1[x] * vec2[x] for x in intersection])
sum1 = sum([vec1[x]**2 for x in vec1.keys()])
sum2 = sum([vec2[x]**2 for x in vec2.keys()])
denominator = np.sqrt(sum1) * np.sqrt(sum2)
if not denominator:
return 0.0
return numerator / denominator
5、Levenshtein距离
主要思想: 计算将一个字符串转换为另一个字符串所需的最小编辑操作(插入、删除、替换)次数。
实现步骤:
- 创建一个矩阵来存储子问题的解
- 初始化第一行和第一列
- 填充矩阵,每个单元格表示到当前位置的最小编辑距离
- 矩阵右下角的值即为Levenshtein距离
应用场景:
- 拼写检查
- DNA序列比对
- 模糊字符串匹配
代码实现:
def levenshtein_distance(s1, s2):
"""计算两个字符串的Levenshtein距离"""
if len(s1) < len(s2):
return levenshtein_distance(s2, s1)
if len(s2) == 0:
return len(s1)
previous_row = range(len(s2) + 1)
for i, c1 in enumerate(s1):
current_row = [i + 1]
for j, c2 in enumerate(s2):
insertions = previous_row[j + 1] + 1
deletions = current_row[j] + 1
substitutions = previous_row[j] + (c1 != c2)
current_row.append(min(insertions, deletions, substitutions))
previous_row = current_row
return previous_row[-1]
def levenshtein_similarity(text1, text2):
"""基于Levenshtein距离计算相似度"""
distance = levenshtein_distance(text1, text2)
max_length = max(len(text1), len(text2))
return 1 - (distance / max_length)
def sliding_window_similarity(text1, text2):
"""使用滑动窗口计算子集相似度"""
try:
# 动态选择window_size为较短文本的长度
window_size = min(len(text1), len(text2))
if len(text1) < len(text2):
text1, text2 = text2, text1
best_similarity = 0
for i in range(len(text1) - window_size + 1):
window = text1[i:i+window_size]
similarity = 1 - levenshtein_distance(window, text2) / max(len(window), len(text2))
best_similarity = max(best_similarity, similarity)
return best_similarity
except Exception as e:
logger.error(f"Error calculating sliding_window_similarity Levenshtein similarity: {str(e)}")
raise
6、N-gram相似度
主要思想: 将文本分割成连续的n个字符(或词)的序列,然后比较这些序列的重合程度。
实现步骤:
- 将文本分割成n-gram
- 计算两个文本的n-gram集合的交集
- 计算两个文本的n-gram集合的并集
- 相似度 = 交集大小 / 并集大小
应用场景:
- 语言识别
- 拼写检查
- 文本分类
代码实现:
def ngram_similarity(text1, text2, n=3):
"""计算n-gram相似度"""
def get_ngrams(text, n):
return [text[i:i+n] for i in range(len(text)-n+1)]
ngrams1 = get_ngrams(text1, n)
ngrams2 = get_ngrams(text2, n)
common = set(ngrams1) & set(ngrams2)
unique = set(ngrams1) | set(ngrams2)
return len(common) / len(unique)