1. 程式人生 > >超平面多維近似向量查詢工具annoy使用總結

超平面多維近似向量查詢工具annoy使用總結

需求:我有800萬的中文詞向量,我想要查詢其中任意一個向量對應的最鄰近的向量是哪些。通常情況下如果向量集比較小的話,幾十萬個向量(幾個G這種),我們都可以用gensim的word2vec來查詢,但是880萬有16個G,加到記憶體中就爆炸了,而且gensim中的查詢屬於暴力搜尋,即全都遍歷比較餘弦相似度來進行查詢,因此幾百萬的詞向量查詢起來就很慢了。這裡我需要用更快速的工具來進行查詢,找到了兩個工具,一個是facebook的faiss包另一個是annoy包。Faiss只能部署在linux,而且看著好複雜,各種索引型別啥的,估計夠我研究一陣,索性使用了annoy包;

關於annoy包的使用方法參考這兩個網址足夠:

 

一個notebook實踐案例,基於gensim的,我就是被這個版本給坑了。。。這裡面有很多說道,首先我先說一下程式碼邏輯,其實很簡單,首先是讀取你的帶 word和vec的txt向量檔案作為model:

1 model = gensim.models.KeyedVectors.load_word2vec_format('
D:\\describe\\dic\\synonyms_vector.txt',binary=False,unicode_errors='ignore') 2 annoy_index = AnnoyIndexer(model, 100)      # 生成索引 3 fname = 'synonyms_txt_index' 4 annoy_index.save(fname)               # 將索引檔案儲存到硬碟

程式碼說明

這四行程式碼目的是

1、載入model

2、對model進行聚類計算

3、建立一個二叉樹集合的索引(樹的數量為100),

4、將索引儲存到硬碟

接下來我們就可以根據建立的這個索引來查詢近似向量了:

1 annoy_index2 = AnnoyIndexer()    # 初始化索引
2 annoy_index2.load(fname)
3 annoy_index2.model = model

這三行就是來載入索引,值得注意的是這裡的model就是之前最開始載入的txt檔案對應的model

接下來問題來了,執行

1 word = ‘人民’
2 vector1 = model[word]
3 approximate_neighbors = model.most_similar([vector1], topn=30, indexer=annoy_index2)

這裡想要查詢“人民”對應的前30個相近詞,通過載入索引來查詢,可是最終的查詢速度跟我沒建立索引之前的暴力搜尋(即word2vec自帶搜尋)是一樣的,但是如果我在這個載入索引之前先進行一個詞的暴力搜尋,然後再對其他的詞進行載入索引搜尋,速度就會快出很多倍,這讓我百思不得其解,最後沒辦法我就先用暴力搜尋先搜尋一個詞,然後剩下的詞都用annoy搜尋,這樣速度還是很快的;

但是我還是想弄明白到底怎麼回事,於是我去官網問作者,作者就說了一句,你需要進行整數對映,(而且應該是非負整數),靠!!!其實官網寫的明明白白:

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.

也就是說我的txt檔案格式需要如下這種格式:

1 vecor
2 vecor

而不是開頭是漢語單詞以及對應的vector,最後再做一個integer到word的對映字典即可;

接下來我對作者給出的github上的版本進行了驗證,程式碼如下:

from annoy import AnnoyIndex
import random
f = 100
t = AnnoyIndex(f)
dict = {}
with open('C:\\Users\Administrator\Desktop\synonyms\\synonyms_vector.txt','r',encoding='utf-8') as f:
    count = 0
    for line in f:
        result = line.split()
        if len(result)>10:
            count+=1
            word = result[0]
            dict[count] = word
            vector = list(map(eval, result[1:]))  # 需要將txt中的str格式vec轉化為float格式
            t.add_item(count, vector)
t.build(10)
t.save('C:\\Users\Administrator\Desktop\synonyms\\test.ann')
u = AnnoyIndex(100)
u.load('C:\\Users\Administrator\Desktop\synonyms\\test.ann')
simi_id = u.get_nns_by_item(880, 20,include_distances=True)
id = simi_id[0]
score = simi_id[1]
# print(simi_id)
# for i,j in zip(id,score):
    # print(dict[i])
    # print(0.5*(abs(1-j))+0.5)
result =[(dict[i],0.5*(abs(1-j))+0.5) for i,j in zip(id,score)]
輸出結果(result):
[('投資', 1.0), ('融資', 0.6934992074966431), ('投資者', 0.6180032193660736), ('投資額', 0.6166684031486511), ('房地產', 0.6127455532550812), ('外資', 0.6104367673397064)]

這裡面我需要指出幾點需要注意的地方:

1、需要將txt中的str格式vec轉化為float格式,否則會報錯;

2、我建立了一個字典對映,這樣能夠最後從查詢到的近似向量對應的id值查詢到對映的詞;

3、最後那個0.5*(abs(1-j))+0.5是餘弦相似度歸一化的計算公式,作者程式中建立的距離索引以及最後查詢返回值都是預設angular模式(即j的值),也即是餘弦相似度,即angular=1-cosin,且其值域為[0,2](因為cosin值域為[-1,1])。但是我們通常只需要求取cosin的絕對值,即其值域應該位於[0,1]。所以我先用1-angular 獲取cosin 然後再求取絕對值,最後再進行歸一化((1+餘弦相似度)/2)即可。

4、對於上面程式碼求取結果我和word2vec的most_similar對比了一下,近似度基本一致,前三位精度完全一致,說明最後的餘弦相似度求取向量相似度就是按照我上面說的方法來進行的;

最後附上餘弦相似度計算方法參考網址:餘弦相似度python實現

PS:關於annoy載入索引還需要注意一點,索引檔案路徑必須是英文路徑,否則程式就會提示查詢不到檔案,望注意!!!