Annoy解析


Annoy是高維空間求近似最近鄰的一個開源庫。

Annoy構建一棵二叉樹,查詢時間為O(logn)。

Annoy通過隨機挑選兩個點,並使用垂直於這個點的等距離超平面將集合划分為兩部分。

如圖所示,圖中灰色線是連接兩個點,超平面是加粗的黑線。按照這個方法在每個子集上迭代進行划分。

依此類推,直到每個集合最多剩余k個點,下圖是一個k = 10 的情況。

相應的完整二叉樹結構:

隨機投影森林。

一個思想依據是:在原空間中相鄰的點,在樹結構上也表現出相互靠近的特點,也就是說,如果兩個點在空間上相互靠近,那么他們很可能被樹結構划分到一起。

如果要在空間中查找臨近點,我們可以在這個二叉樹中搜索。上圖中每個節點用超平面來定義,所以我們可以計算出該節點往哪個方向遍歷,搜索時間 log n

如上圖,我們找到了七個最近鄰,但是假如我們想找到更多的最近鄰怎么辦?有些最近鄰是在我們遍歷的葉子節點的外邊的。

技巧1:使用優先隊列

如果一個划分的兩邊“靠得足夠近”(量化方式在后面介紹),我們就兩邊都遍歷。這樣就不只是遍歷一個節點的一邊,我們將遍歷更多的點

我們可以設置一個閾值,用來表示是否願意搜索划分“錯”的一遍。如果設置為0,我們將總是遍歷“對”的一片。但是如果設置成0.5,就按照上面的搜索路徑。

這個技巧實際上是利用優先級隊列,依據兩邊的最大距離。好處是我們能夠設置比0大的閾值,逐漸增加搜索范圍。

技巧2:構建一個森林

我們能夠用一個優先級隊列,同時搜索所有的樹。這樣有另外一個好處,搜索會聚焦到那些與已知點靠得最近的那些樹——能夠把距離最遠的空間划分出去

每棵樹都包含所有的點,所以當我們搜索多棵樹的時候,將找到多棵樹上的多個點。如果我們把所有的搜索結果的葉子節點都合在一起,那么得到的最近鄰就非常符合要求。

 

依照上述方法,我們找到一個近鄰的集合,接下來就是計算所有的距離和對這些點進行排序,找到最近的k個點。

很明顯,我們會丟掉一些最近的點,這也是為什么叫近似最近鄰的原因。

Annoy在實際使用的時候,提供了一種機制可以調整(搜索k),你能夠根據它來權衡性能(時間)和准確度(質量)。

tips:

1.距離計算,采用歸一化的歐氏距離:vectors = sqrt(2-2*cos(u, v)) 

2.向量維度較小(<100),即使維度到達1000變現也不錯

3.內存占用小

4.索引創建與查找分離(特別是一旦樹已經創建,就不能添加更多項)

5.有兩個參數可以用來調節Annoy 樹的數量n_trees和搜索期間檢查的節點數量search_k

  n_trees在構建時提供,並影響構建時間和索引大小。 較大的值將給出更准確的結果,但更大的索引。

  search_k在運行時提供,並影響搜索性能。 較大的值將給出更准確的結果,但將需要更長的時間返回。

如果不提供search_k,它將默認為n * n_trees,其中n是近似最近鄰的數目。 否則,search_k和n_tree大致是獨立的,即如果search_k保持不變,n_tree的值不會影響搜索時間,反之亦然。 基本上,建議在可用負載量的情況下盡可能大地設置n_trees,並且考慮到查詢的時間限制,建議將search_k設置為盡可能大。

 

 Python demo:

from annoy import AnnoyIndex
import random

f = 40 #維度
t = AnnoyIndex(f)  # Length of item vector that will be indexed
for i in xrange(1000):
    v = [random.gauss(0, 1) for z in xrange(f)]
    t.add_item(i, v) #添加向量

t.build(10) # 10 trees
t.save('test.ann') 

# ...

u = AnnoyIndex(f)
u.load('test.ann') # super fast, will just mmap the file
print(u.get_nns_by_item(0, 1000)) # will find the 1000 nearest         neighbors of the first(0) vec

python API:

AnnoyIndex(f, metric='angular') returns a new index that's read-write and stores vector of f dimensions. Metric can be either "angular" or "euclidean".
a.add_item(i, v) adds item i (any nonnegative integer) with vector v. Note that it will allocate memory for max(i)+1 items.
a.build(n_trees) builds a forest of n_trees trees. More trees gives higher precision when querying. After calling build, no more items can be added.
a.save(fn) saves the index to disk.
a.load(fn) loads (mmaps) an index from disk.
a.unload() unloads.
a.get_nns_by_item(i, n, search_k=-1, include_distances=False) returns the n closest items. During the query it will inspect up to search_k nodes which defaults to n_trees * n if not provided. search_k gives you a run-time tradeoff between better accuracy and speed. If you set include_distances to True, it will return a 2 element tuple with two lists in it: the second one containing all corresponding distances.
a.get_nns_by_vector(v, n, search_k=-1, include_distances=False) same but query by vector v.
a.get_item_vector(i) returns the vector for item i that was previously added.
a.get_distance(i, j) returns the distance between items i and j. NOTE: this used to returned the squared distance, but has been changed as of Aug 2016.
a.get_n_items() returns the number of items in the index.

 類似可以做這個工作的RPForest和sklearn.neighbors中的LSHForest,但Annoy的效果要比他們好很多。

查詢樹上所有節點都共用這個數據結構:

  1. n_descendants為該節點及其子孫節點包含的向量個數。
  2. children為左右子樹指針。因為搜索樹在物理上其實是這個結構體的數組,按照16字節對齊。搜索時把索引及數據文件mmap到內存,然后通過數組下標進行隨機定位。所以指針就是數組下標

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM