一种快速在向量空间中寻找k紧邻的算法——annoy index

Annoy是一个用于查找空间中接近查询点的点的C++库,支持欧氏距离、曼哈顿距离和余弦距离。文章介绍了Annoy的内部结构、建树过程、搜索算法以及如何处理距离度量。Annoy通过构建多棵树和使用静态文件作为索引来优化内存使用和提高搜索效率,特别适用于推荐系统中的相似项查找。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

几个需要关注的点:

1.这是一个精确度换速度的算法,找到的k紧邻不能保证是全局的k紧邻(例如在分割平面附近的点),所以如果要找exact的k紧邻的话并不合适,还是得做全局的搜索
2.可以通过设置tree的数量来balance精度和速度
3.每次对同一份数据建立索引是不同的,所以两次计算结果可能也会不同
4.github:https://github.com/spotify/annoy

最近工作中使用了一下annoy,于是抽时间看了下代码,记录下。。

annoy支持三种距离度量方式,cos距离,欧式距离和曼哈顿距离。下面主要通过最简单的欧氏距离来看。

首先看下节点node的结构

n_descendants记录了该节点下子节点的个数,children[2]记录了左右子树,v和a之后会详细说,先知道v[1]代表该节点对应的向量,a代表偏移就好。

然后看下AnnoyIndex类

_n_items记录了我们一共有多少个向量需要构建索引,_n_nodes记录了一共有多少个节点,_s是node占有的空间大小,_f是向量的维度,_nodes所有节点,_roots是所有树的根节点。

annoy建树的时候当该区域内的节点数小于k的时候就不会再继续递归建树,之前疑惑怎么调整k这个参数,看完代码才发现没法调整,_K是一个定值,如果一个区域内的节点数小于_K的时候,这个节点就不再记录向量v,v的空间也用来记录节点的id。

另外还有一个比较奇怪的事情就是annoy为node开辟空间的方式。。比如我有三个item,建索引的时候id分别为3,6,10,那么annoy会开辟11个node空间,从0-10。。看下面这段代码就能明白

再接下来就是到了建树。annoy建树如下图,每次选择空间中的两个质心作为分割点,相当于kmeans过程,以使得两棵子树分割的尽量均匀以保证logn的检索复杂度。以垂直于过两点的直线的超平面来分割整个空间,然后在两个子空间内递归分割直到子空间最多只有k个点。如下图

然后看下创建分割面的过程,入参为当前空间的所有点nodes,维度f,随机函数random,分割节点n

best_iv和best_jv就是选出来的那两个点,n->v存储的就是这两个点连线对应的向量,即分隔面的法向量,计算方式就是两点对应向量相减。n->a存储的就是分割超平面对应的偏移,以三维空间举例,三维空间中的平面表示方法为Ax + By + Cz + D = 0,n->a存储的就是这个D,计算方法如下,因为平面的法向量已经确定,又因为该平面过best_iv和best_jv连线中点,将中点坐标代入,连线中心点定义为m=((best_iv[0] + best_jv[0])/2, (best_iv[1] + best_jv[1])/2, (best_iv[2] +best_jv[2])/2),则A * m[0] + B * m[1] + C * m[2] + D =0  => D= -(A * m[0] +B * m[1] + C * m[2])。

接下来看一下是如何选择两个点的,即two_means

为了保证nlogn的检索复杂度,需要使得每次分割得到的两棵子树尽量平衡,所以要找空间中的两个质心,过程很像kmeans,初始随机选取两个点,每次迭代过程中随机选择一个点计算该点属于哪个子树,并更新对应的质心坐标。

建树完成之后就是检索,对于给定的点去树中找topk近邻,最基本的想法就是从根开始,根据该点的向量信息和每个树节点的分割超平面比较决定去哪个子树遍历。如图所示

但是这样还是存在一些问题,就是最近邻不一定会和查询点在同一个叶结点上

解决方法是这样的,一是建立多棵树,二是在查询点遍历树的时候不一定只选择一条路径,这两个方法对应两个参数treenum和searchnum,如图所示

### 更快的 K-NN 实现方法 为了提升 K-NN 的性能,可以采用多种策略来减少计算复杂度并加速最近邻查找过程。以下是几种常见的优化方式: #### 使用索引结构降低搜索时间 传统的暴力搜索会逐一比较测试样本与数据集中的每一个点的距离,这可能导致较高的时间开销 \(O(n)\),其中 \(n\) 是训练集中样本的数量。通过引入空间划分技术(如 KD-Trees 或 Ball Trees),可以在高维空间快速定位最接近的数据点[^3]。 ```python from sklearn.neighbors import KDTree # 构建KD树 tree = KDTree(X_train, leaf_size=40) # 查询距离目标点最近的k个邻居 distances, indices = tree.query(X_test, k=5) ``` 上述代码片段展示了如何利用 `scikit-learn` 库构建 KD 树以加快查询速度。注意,在维度较高 (\(d>20\)) 时,这些基于树的方法可能退化为线性扫描效果[^1]。 #### 并行处理 现代硬件支持多核 CPU 和 GPU 加速运算。对于大规模数据集来说,并行执行多个子任务能够显著缩短整体运行时间。例如,可以通过分布式框架或者专门库 (比如 cuML) 来完成这一操作: ```python import cudf from cuml.neighbors import NearestNeighbors as cuNearestNeighbors # 转换pandas DataFrame至cudf格式 gdf_data = cudf.DataFrame(data=X_train) model = cuNearestNeighbors() model.fit(gdf_data) neighbors = model.kneighbors(cudf.DataFrame(data=X_test), n_neighbors=5)[1].get() ``` 这里采用了 NVIDIA 提供的 RAPIDS 工具包来进行 GPU 上的高效 knn 计算. #### 近似最近邻(Approximate Nearest Neighbor Search) 当精确找到最近邻不是严格必要条件的时候,ANN 可作为替代方案之一。它允许一定程度上的误差换取更高的效率。常用工具包括 Annoy、Faiss 等[^2]: ```python import faiss # make faiss available index = faiss.IndexFlatL2(d) # build the index index.add(xb) # add vectors to the index D, I = index.search(xq, k) # actual search print(I[:5]) # neighbors of the 5 first queries print(D[:5]) # their corresponding distances ``` 以上实例说明了 Facebook 开发的 Faiss 如何被用来做高效的相似性检索。 #### 数据预处理 除了改变核心算法外,还可以通过对原始输入进行变换从而间接改善表现。例如降维PCA/ICA能有效缓解“维度灾难”,使得后续步骤更加顺畅;而标准化则有助于消除不同特征间尺度差异带来的负面影响。 ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值