局部敏感哈希(LSH)与余弦相似度:快速找到相似的“你”
在海量数据中,如何快速找到和你相似的“另一个你”?比如,在百万首歌曲中找到与你喜欢的歌曲风格最接近的那些,或者在亿万条微博中找到与你观点最相似的那些。传统的相似度计算方法,如计算余弦相似度,虽然准确,但在大数据面前却显得力不从心,因为它们需要两两比较,计算量巨大。
这时,局部敏感哈希(Locality-Sensitive Hashing,简称LSH)就派上用场了。LSH 是一种快速找到相似项的方法,它的核心思想是:将原始数据映射到新的空间,使得相似的数据在新的空间中仍然大概率相邻,而不相似的数据则大概率不相邻。这样,我们只需要比较相邻的数据,就能找到大部分相似项,大大减少了计算量。
而余弦相似度,则是衡量两个向量方向相似性的指标。它通过计算两个向量夹角的余弦值来判断它们的相似程度,值越接近1,表示两个向量越相似;值越接近0,表示两个向量越不相似。
本文将结合LSH和余弦相似度,用Python代码实现一个简单的LSH算法,并通过具体例子演示如何快速找到相似项,还会进行性能测试和比较。
1. 原理:LSH如何“缩小包围圈”?
想象一下,你是一个侦探,要在一座城市里找到和你拥有相同特征的人。如果挨家挨户地毯式搜索,那效率太低了。更好的办法是,先缩小范围,比如根据一些线索(如年龄、职业、爱好等)将城市划分为几个区域,然后重点搜索你认为最有可能的区域。
LSH的原理与此类似。它通过一系列哈希函数,将原始数据“分桶”,相似的数据更有可能被分到同一个桶里,而不相似的数据则更有可能被分到不同的桶里。这样,我们只需要在同一个桶里寻找相似项,就能大大减少搜索范围。
1.1 余弦相似度LSH的哈希函数
对于余弦相似度,LSH使用的哈希函数通常是随机超平面。具体来说,就是随机生成一个向量(超平面法向量),然后计算原始数据与这个向量的点积。点积大于0的数据被分到一类,点积小于等于0的数据被分到另一类。这样,我们就完成了一次“分桶”。
为什么随机超平面可以实现“分桶”呢?
- 相似向量,点积大概率同号:想象两个方向相似的向量,它们与同一个随机向量的点积,大概率会同号(同为正或同为负)。
- 不相似向量,点积大概率异号:想象两个方向不相似的向量,它们与同一个随机向量的点积,大概率会异号(一个为正,一个为负)。
通过多次“分桶”(使用多个随机超平面),我们就能更精确地找到相似项。
2. Python代码实现:动手搭建你的LSH
接下来,我们将用Python代码实现一个基于余弦相似度的LSH算法。为了方便演示,我们使用NumPy库来处理向量和矩阵。
import numpy as np
class CosineLSH:
def __init__(self, num_planes, dimensions):
"""
初始化CosineLSH。
Args:
num_planes: 哈希函数的数量(随机超平面的数量)。
dimensions: 数据的维度。
"""
self.num_planes = num_planes
self.dimensions = dimensions
self.planes = np.random.randn(num_planes, dimensions)
def hash(self, vector):
"""
计算向量的哈希值。
Args:
vector: 输入向量。
Returns:
一个字符串,表示向量的哈希值。
"""
hashes = (np.dot(self.planes, vector) > 0).astype(int)
return ''.join(hashes.astype(str))
def index(self, data):
"""
构建索引。
Args:
data: 一个二维数组,每一行表示一个向量。
Returns:
一个字典,键是哈希值,值是对应的数据索引列表。
"""
index = {}
for i, vector in enumerate(data):
h = self.hash(vector)
if h in index:
index[h].append(i)
else:
index[h] = [i]
return index
def query(self, vector, index, k=10):
"""
查询与输入向量相似的数据。
Args:
vector: 输入向量。
index: 索引。
k: 返回最相似的数据的数量。
Returns:
一个列表,包含最相似的数据的索引。
"""
h = self.hash(vector)
candidates = index.get(h, [])
#如果桶内数据少于K,则返回桶内所有的.
if len(candidates) <= k:
return candidates
# 计算候选数据与查询向量的余弦相似度
similarities = []
for i in candidates:
sim = np.dot(vector, data[i]) / (np.linalg.norm(vector) * np.linalg.norm(data[i]))
similarities.append((sim, i))
# 按照相似度排序,返回前k个
similarities.sort(reverse=True)
return [i for sim, i in similarities[:k]]
代码解读:
__init__
:初始化LSH对象,生成指定数量的随机超平面。hash
:计算输入向量的哈希值,将向量映射成一个由0和1组成的字符串。index
:构建索引,将数据按照哈希值分组,方便后续查询。query
:查询与输入向量相似的数据。先找到与输入向量哈希值相同的候选数据,然后计算它们与输入向量的余弦相似度,最后返回最相似的k个数据。
3. 演示:LSH如何找到相似电影?
假设我们有一个电影数据集,每部电影都用一个向量表示,向量的每个维度代表一个特征(如类型、演员、导演等)。现在,我们要找到与某部电影最相似的几部电影。
# 假设电影数据集如下:
data = np.array([
[0.8, 0.2, 0.5, 0.1],
[0.7, 0.3, 0.4, 0.2],
[0.1, 0.9, 0.2, 0.8],
[0.2, 0.8, 0.3, 0.7],
[0.9, 0.1, 0.6, 0.3],
])
# 创建LSH对象
lsh = CosineLSH(num_planes=5, dimensions=4)
# 构建索引
index = lsh.index(data)
# 查询与第一部电影相似的电影
query_vector = data[0]
similar_movies = lsh.query(query_vector, index, k=2)
print(f"与第一部电影相似的电影索引:{similar_movies}")
运行结果可能如下:
与第一部电影相似的电影索引:[4, 1]
这表示,与第一部电影最相似的两部电影是第五部和第二部。当然,由于LSH的随机性,每次运行的结果可能会略有不同。
4. 性能测试:LSH vs 暴力搜索
为了验证LSH的性能优势,我们将其与暴力搜索(brute-force search)进行比较。暴力搜索就是计算查询向量与数据集中所有向量的余弦相似度,然后排序返回最相似的k个。
import time
# 生成更大的数据集
data = np.random.rand(1000, 100)
# 创建LSH对象
lsh = CosineLSH(num_planes=20, dimensions=100)
# 构建索引
index = lsh.index(data)
# 查询向量
query_vector = data[0]
# LSH查询
start_time = time.time()
lsh_results = lsh.query(query_vector, index, k=10)
lsh_time = time.time() - start_time
# 暴力搜索
start_time = time.time()
similarities = []
for i, vector in enumerate(data):
sim = np.dot(query_vector, vector) / (np.linalg.norm(query_vector) * np.linalg.norm(vector))
similarities.append((sim, i))
similarities.sort(reverse=True)
brute_force_results = [i for sim, i in similarities[:10]]
bf_time = time.time() - start_time
print(f"LSH查询时间:{lsh_time:.4f}秒")
print(f"暴力搜索时间:{bf_time:.4f}秒")
# 比较结果
common_results = set(lsh_results) & set(brute_force_results)
print(f"LSH和暴力搜索找到的相同结果数量:{len(common_results)}")
运行结果可能如下:
LSH查询时间:0.0013秒
暴力搜索时间:0.0055秒
LSH和暴力搜索找到的相同结果数量:8
可以看到,LSH的查询时间明显快于暴力搜索,而且找到的大部分结果是相同的。随着数据集的增大,LSH的性能优势会更加明显。
5. 总结与进阶
本文介绍了局部敏感哈希(LSH)和余弦相似度的基本原理,并通过Python代码实现了一个简单的LSH算法。我们演示了如何使用LSH找到相似项,并将其与暴力搜索进行了性能比较。
LSH是一种非常有用的技术,它可以应用于各种需要快速找到相似项的场景,如推荐系统、搜索引擎、图像检索等。
当然,LSH还有很多可以改进和扩展的地方,比如:
- 调整哈希函数的数量:哈希函数的数量越多,LSH的精度越高,但计算量也越大。需要根据实际情况进行权衡。
- 使用更复杂的哈希函数:除了随机超平面,还可以使用其他类型的哈希函数,如MinHash、SimHash等。
- 结合其他技术:LSH可以与其他技术结合使用,如倒排索引、多级索引等,进一步提高搜索效率。
希望本文能帮助你入门LSH,并在实际应用中发挥它的作用。