Annoy算法

与Faiss相比,Annoy搜索,速度更快一点,主要目的是建立一个数据结构快速找到任何查询点的最近点。通过牺牲查询准确率来换取查询速度,这个速度比faiss速度还要快。

是什么

Annoy:最近邻向量搜索,

原理/过程

算法原理:先构建索引,对于每个二叉树都建立索引,在这里二叉树是随机构造的

第一步:先随机找两个点,根据这两个点进行连线,找到垂直平分线,称为超平面。

es检索和向量检索_java

 

第二步:在切分后的子空间,继续按照上面的方法,找一个点,然后,进行连线,找垂直平分超平面。

es检索和向量检索_python_02

 

不断继续分割,直到子空间中数据量不超过k,在这里k是可以提前定义的,如k=10.

es检索和向量检索_java_03

 

其实,从另一个角度出发,在不断分割的过程中,类似于二叉树,从根节点到子节点,然后不断的切割

es检索和向量检索_es检索和向量检索_04

 

通过二叉树来表示空间节点的分布,节点表示子空间,在点的分布空间中,接近的子空间在二叉树中表现为位置靠近的节点。

对应着一个假设条件,对于空间内距离非常近的两个点,任何切分方法都不能使之分开。

要想查找某个点,只需要从根节点出发,一路往下走,最终到达子节点,自然就找到匹配的数据了。

但是有时候可能找到的数据量比较少,

1、优先队列

把多棵树放到优先队列,挨个进行处理,然后设定阈值,如果查询的点,与二叉树中某个节点距离比较近,我们可以在根节点出发的时候,同样找旁边的相关节点,走两个分支。

es检索和向量检索_二叉树_05

es检索和向量检索_算法_06

 

 

或者还可以建立多个二叉树,

多个二叉树进行交叉,构成森林,基本上可以覆盖到查找匹配点所在的区域,然后通过计算所有点的距离,并进行排序,取TopK

es检索和向量检索_python_07

 

es检索和向量检索_es检索和向量检索_08

 

如何查询:

把每个二叉树的根节点都放进优先队列

对每一个二叉树都进行搜索,每一个二叉树都可以得到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))