Annoy算法
与Faiss相比,Annoy搜索,速度更快一点,主要目的是建立一个数据结构快速找到任何查询点的最近点。通过牺牲查询准确率来换取查询速度,这个速度比faiss速度还要快。
是什么
Annoy:最近邻向量搜索,
原理/过程
算法原理:先构建索引,对于每个二叉树都建立索引,在这里二叉树是随机构造的
第一步:先随机找两个点,根据这两个点进行连线,找到垂直平分线,称为超平面。
第二步:在切分后的子空间,继续按照上面的方法,找一个点,然后,进行连线,找垂直平分超平面。
不断继续分割,直到子空间中数据量不超过k,在这里k是可以提前定义的,如k=10.
其实,从另一个角度出发,在不断分割的过程中,类似于二叉树,从根节点到子节点,然后不断的切割
通过二叉树来表示空间节点的分布,节点表示子空间,在点的分布空间中,接近的子空间在二叉树中表现为位置靠近的节点。
对应着一个假设条件,对于空间内距离非常近的两个点,任何切分方法都不能使之分开。
要想查找某个点,只需要从根节点出发,一路往下走,最终到达子节点,自然就找到匹配的数据了。
但是有时候可能找到的数据量比较少,
1、优先队列
把多棵树放到优先队列,挨个进行处理,然后设定阈值,如果查询的点,与二叉树中某个节点距离比较近,我们可以在根节点出发的时候,同样找旁边的相关节点,走两个分支。
或者还可以建立多个二叉树,
多个二叉树进行交叉,构成森林,基本上可以覆盖到查找匹配点所在的区域,然后通过计算所有点的距离,并进行排序,取TopK
如何查询:
把每个二叉树的根节点都放进优先队列
对每一个二叉树都进行搜索,每一个二叉树都可以得到TopK个候选集
删除重复候选集
计算候选集与查询点的距离,并进行排序
返回TopK
优缺点
优点:查询速度快,时间复杂度O(log(n))
缺点:对于存在边界附近的点,可能还是会无法查找出来,导致损失一部分查询精确率。
如何优化
应用场景
如:在搜索业务中,数据候选集dataset,需要对新来的一个或多个数据进行查询,返回数据集中与该查询最相似的TopK数据。
最笨的方法,每条数据都计算一次,当然,如果候选集比较小,也无所谓,对于海量数据呢,总不能依次计算吧。通过annoy算法可以大大降低查询时间,在牺牲少量查询精确率的情况下。
具体应用
安装包:pip install annoy
参数介绍:
重要设置:n_trees:树的个数,直接影响构建索引的时间,值越大表示最终的精度越高,但是会有更多的索引,主要影响索引时间
Search_k:衡量查询精度和速度,值越大表示搜索耗时越长,搜索的精度越高;如果不进行设定的情况下,默认为n_trees*n
from annoy import AnnoyIndex
import random
# f 表示向量的维度
f = 40
# 'angular' 是 annoy 支持的一种度量;
t = AnnoyIndex(f, 'angular') # Length of item vector that will be indexed
# 插入数据
for i in range(1000):
v = [random.gauss(0, 1) for z in range(f)]
# i 是一个非负数,v 表示向量
t.add_item(i, v)
# 树的数量
t.build(10) # 10 trees
# 存储索引成为文件
t.save('test.ann')
# 读取存储好的索引文件
u = AnnoyIndex(f, 'angular')
u.load('test.ann') # super fast, will just mmap the file
# 返回与第 0 个向量最相似的 Top 100 向量;
print(u.get_nns_by_item(0, 1000)) # will find the 1000 nearest neighbors
# 返回与该向量最相似的 Top 100 向量;
print(u.get_nns_by_vector([random.gauss(0, 1) for z in range(f)], 1000))
# 返回第 i 个向量与第 j 个向量的距离;
# 第 0 个向量与第 0 个向量的距离
print(u.get_distance(0, 0))
# 第 0 个向量与第 1 个向量的距离
print(u.get_distance(0, 1))
# 返回索引中的向量个数;
print(u.get_n_items())
# 返回索引中的树的棵数;
print(u.get_n_trees())
# 不再加载索引
print(u.unload())
不同距离应用:基于hamming距离
from annoy import AnnoyIndex
# Mentioned on the annoy-user list
bitstrings = [
'0000000000011000001110000011111000101110111110000100000100000000',
'0000000000011000001110000011111000101110111110000100000100000001',
'0000000000011000001110000011111000101110111110000100000100000010',
'0010010100011001001000010001100101011110000000110000011110001100',
'1001011010000110100101101001111010001110100001101000111000001110',
'0111100101111001011110010010001100010111000111100001101100011111',
'0011000010011101000011010010111000101110100101111000011101001011',
'0011000010011100000011010010111000101110100101111000011101001011',
'1001100000111010001010000010110000111100100101001001010000000111',
'0000000000111101010100010001000101101001000000011000001101000000',
'1000101001010001011100010111001100110011001100110011001111001100',
'1110011001001111100110010001100100001011000011010010111100100111',
]
# 将其转换成二维数组
vectors = [[int(bit) for bit in bitstring] for bitstring in bitstrings]
# 64 维度
f = 64
idx = AnnoyIndex(f, 'hamming')
for i, v in enumerate(vectors):
idx.add_item(i, v)
# 构建索引
idx.build(10)
idx.save('idx.ann')
idx = AnnoyIndex(f, 'hamming')
idx.load('idx.ann')
js, ds = idx.get_nns_by_item(0, 5, include_distances=True)
# 输出索引和 hamming 距离
print(js, ds)
基于欧几里得距离的 annoy 使用案例:
from annoy import AnnoyIndex
import random
# f 表示向量的维度
f = 2
# 'euclidean' 是 annoy 支持的一种度量;
t = AnnoyIndex(f, 'euclidean') # Length of item vector that will be indexed
# 插入数据
t.add_item(0, [0, 0])
t.add_item(1, [1, 0])
t.add_item(2, [1, 1])
t.add_item(3, [0, 1])
# 树的数量
t.build(n_trees=10) # 10 trees
t.save('test2.ann')
u = AnnoyIndex(f, 'euclidean')
u.load('test2.ann') # super fast, will just mmap the file
print(u.get_nns_by_item(1, 3)) # will find the 1000 nearest neighbors
print(u.get_nns_by_vector([0.1, 0], 3))