Faiss從入門到實戰精通

1.Faiss是什麼

Faiss是Facebook Ai Research開發的一款稠密向量檢索工具。引用Faiss Wiki上面的一段簡介

Faiss is a library for efficient similarity search and clustering of dense vectors.
It contains algorithms that search in sets of vectors of any size, up to ones that possibly do not fit in RAM.
It also contains supporting code for evaluation and parameter tuning. 
Faiss is written in C++ with complete wrappers for Python (versions 2 and 3).
Some of the most useful algorithms are implemented on the GPU. 
It is developed by Facebook AI Research.

上面的簡介包含了如下信息:
1.Faiss是針對稠密向量進行相似性搜索和聚類的一個高效類庫。
2. 它包含可搜索任意大小的向量集的算法,這些向量集的大小甚至都不適合RAM。
3. 它還包含用於評估和參數調整的支持代碼。
4. Faiss用C ++編寫,並且有python2與python3的封裝代碼。
5. 一些最有用的算法在GPU上有實現。
6. Faiss是由Facebook AI Research開發的。

2.faiss安裝

No module named swigfaiss
中有簡單介紹,可以用如下方式安裝

#cpu 版本
conda install faiss-cpu -c pytorch
# GPU 版本
conda install faiss-gpu cudatoolkit=8.0 -c pytorch # For CUDA8
conda install faiss-gpu cudatoolkit=9.0 -c pytorch # For CUDA9
conda install faiss-gpu cudatoolkit=10.0 -c pytorch # For CUDA10

3.faiss的使用方法簡介

整體來說,faiss的使用方式可以分爲三個步驟:
1.構建訓練數據以矩陣的形式表示,比如我們現在經常使用的embedding,embedding出來的向量就是矩陣的一行。
2.爲數據集選擇合適的index,index是整個faiss的核心部分,將第一步得到的訓練數據add到index當中。
3.search,或者說query,搜索到最終結果。

4.faiss原理與核心算法

faiss的主要功能是對向量進行相似搜索。具體就是給定一個向量,在所有已知的向量庫中找出與其相似度最高的一些向量,本質是一個KNN(K近鄰)問題,比如google的以圖找圖功能。隨着目前embedding的流行,word2vec,doc2vec,img2vec,item2vec,video2vec,everything2vec,所以faiss也越來越受到大家的歡迎。
根據上面的描述不難看出,faiss本質是一個向量(矢量)數據庫,這個數據庫在進行向量查詢的時候有其獨到之處,因此速度比較快,同時佔用的空間也比較小。

faiss中最重要的是索引Index,具體的索引類型見參考文獻2.

Method Class name index_factory Main parameters Bytes/vector Exhaustive Comments
Exact Search for L2 IndexFlatL2 "Flat" d 4*d yes brute-force
Exact Search for Inner Product IndexFlatIP "Flat" d 4*d yes also for cosine (normalize vectors beforehand)
Hierarchical Navigable Small World graph exploration IndexHNSWFlat 'HNSWx,Flat` d, M 4*d + 8 * M no
Inverted file with exact post-verification IndexIVFFlat "IVFx,Flat" quantizer, d, nlists, metric 4*d no Take another index to assign vectors to inverted lists
Locality-Sensitive Hashing (binary flat index) IndexLSH - d, nbits nbits/8 yes optimized by using random rotation instead of random projections
Scalar quantizer (SQ) in flat mode IndexScalarQuantizer "SQ8" d d yes 4 bit per component is also implemented, but the impact on accuracy may be inacceptable
Product quantizer (PQ) in flat mode IndexPQ "PQx" d, M, nbits M (if nbits=8) yes
IVF and scalar quantizer IndexIVFScalarQuantizer "IVFx,SQ4" "IVFx,SQ8" quantizer, d, nlists, qtype SQfp16: 2 * d, SQ8: d or SQ4: d/2 no there are 2 encodings: 4 bit per dimension and 8 bit per dimension
IVFADC (coarse quantizer+PQ on residuals) IndexIVFPQ "IVFx,PQy" quantizer, d, nlists, M, nbits M+4 or M+8 no the memory cost depends on the data type used to represent ids (int or long), currently supports only nbits <= 8
IVFADC+R (same as IVFADC with re-ranking based on codes) IndexIVFPQR "IVFx,PQy+z" quantizer, d, nlists, M, nbits, M_refine, nbits_refine M+M_refine+4 or M+M_refine+8 no

上面的索引中,三個最重要的索引爲IndexFlatL2,IndexIVFFlat,IndexIVFPQ。下面針對這三種索引來進行分析與說明。

5.IndexFlatL2

看到這個名字大家應該就能猜個八九不離十。沒錯,這種索引的方式是計算L2距離,爲一種暴力的(brute-force))精確搜索的方式,計算方式自然就是計算各向量的歐式距離(L2距離)。

看一下官方給的一個例子

import numpy as np
d = 64                           # dimension
nb = 100000                      # database size
nq = 10000                       # nb of queries
np.random.seed(1234)             # make reproducible
xb = np.random.random((nb, d)).astype('float32')
xb[:, 0] += np.arange(nb) / 1000. # # 每一項增加了一個等差數列的對應項數
xq = np.random.random((nq, d)).astype('float32')
xq[:, 0] += np.arange(nq) / 1000.


import faiss                   # make faiss available
index = faiss.IndexFlatL2(d)   # build the index
print(index.is_trained)        # 表示索引是否需要訓練的布爾值
index.add(xb)                  # add vectors to the index
print(index.ntotal)            # 索引中向量的數量。


k = 4                          # we want to see 4 nearest neighbors
D, I = index.search(xb[:5], k) # sanity check
print(I)
print(D)

D, I = index.search(xq, k)     # actual search
print(I[:5])                   # neighbors of the 5 first queries
print(I[-5:])                  # neighbors of the 5 last qu

輸出結果爲

True
100000
[[  0 393 363  78]
 [  1 555 277 364]
 [  2 304 101  13]
 [  3 173  18 182]
 [  4 288 370 531]]
[[0.        7.1751733 7.207629  7.2511625]
 [0.        6.3235645 6.684581  6.7999454]
 [0.        5.7964087 6.391736  7.2815123]
 [0.        7.2779055 7.5279865 7.6628466]
 [0.        6.7638035 7.2951202 7.3688145]]
[[ 381  207  210  477]
 [ 526  911  142   72]
 [ 838  527 1290  425]
 [ 196  184  164  359]
 [ 526  377  120  425]]
[[ 9900 10500  9309  9831]
 [11055 10895 10812 11321]
 [11353 11103 10164  9787]
 [10571 10664 10632  9638]
 [ 9628  9554 10036  9582]]

具體的步驟爲:
一、構建數據集
1.xb相當於數據庫中待搜索的向量,這些向量都會建立索引並且我們會進行搜索,xb的大小爲nb * d。
2.xq爲查詢向量,我們期望找到xb中xq的K近鄰向量。xq的大小爲xq * d。如果是查詢單個向量,nq = 1。
二、構建索引
三、搜索

IndexFlatL2的結果是精確,可以用來作爲其他索引測試中準確性程度的參考。當數據集比較大的時候,暴力搜索的時間複雜度很高,因此我們一般會使用其他方式的索引。

6.IndexIVFFlat

上面的IndexFlatL2爲暴力搜索,速度慢,實際中我們需要更快的方式,於是就有了IndexIVFFlat。
爲了加快搜索的速度,我們可以將數據集分割爲幾部分,將其定義爲Voronoi Cells,每個數據向量只能落在一個cell中。查詢時只需要查詢query向量落在cell中的數據了,降低了距離計算次數。
IndexIVFFlat需要一個訓練的階段,其與另外一個索引quantizer有關,通過quantizer來判斷屬於哪個cell。

IndexIVFFlat在搜索的時候,引入了nlist(cell的數量)與nprob(執行搜索的cell樹)參數。通過調整這些參數可以在速度與精度之間平衡。

import numpy as np
d = 64                              # 向量維度
nb = 100000                         # 向量集大小
nq = 10000                          # 查詢次數
np.random.seed(1234)                # 隨機種子,使結果可復現
xb = np.random.random((nb, d)).astype('float32')
xb[:, 0] += np.arange(nb) / 1000.
xq = np.random.random((nq, d)).astype('float32')
xq[:, 0] += np.arange(nq) / 1000.

import faiss

nlist = 100
k = 4
quantizer = faiss.IndexFlatL2(d)  # the other index
index = faiss.IndexIVFFlat(quantizer, d, nlist, faiss.METRIC_L2)
# here we specify METRIC_L2, by default it performs inner-product search

assert not index.is_trained
index.train(xb)
assert index.is_trained

index.add(xb)                  # 添加索引可能會有一點慢
D, I = index.search(xq, k)     # 搜索
print(I[-5:])                  # 最初五次查詢的結果
index.nprobe = 10              # 默認 nprobe 是1 ,可以設置的大一些試試
D, I = index.search(xq, k)
print(I[-5:])                  # 最後五次查詢的結果

最後的結果爲

[[ 9900  9309  9810 10048]
 [11055 10895 10812 11321]
 [11353 10164  9787 10719]
 [10571 10664 10632 10203]
 [ 9628  9554  9582 10304]]
[[ 9900 10500  9309  9831]
 [11055 10895 10812 11321]
 [11353 11103 10164  9787]
 [10571 10664 10632  9638]
 [ 9628  9554 10036  9582]]

由上面的實驗可以看出,結果並不是完全一致的,增大nprobe可以得到與brute-force更爲接近的結果,nprobe就是速度與精度的調節器。

7.IndexIVFPQ

IndexFlatL2 和 IndexIVFFlat都要存儲所有的向量數據。對於超大規模數據集來說,可能會不大顯示。因此IndexIVFPQ索引可以用來壓縮向量,具體的壓縮算法爲PQ(乘積量化, Product Quantizer)。

import numpy as np

d = 64                              # 向量維度
nb = 100000                         # 向量集大小
nq = 10000                          # 查詢次數
np.random.seed(1234)                # 隨機種子,使結果可復現
xb = np.random.random((nb, d)).astype('float32')
xb[:, 0] += np.arange(nb) / 1000.
xq = np.random.random((nq, d)).astype('float32')
xq[:, 0] += np.arange(nq) / 1000.

import faiss

nlist = 100
m = 8
k = 4
quantizer = faiss.IndexFlatL2(d)    # 內部的索引方式依然不變
index = faiss.IndexIVFPQ(quantizer, d, nlist, m, 8)
                                    # 每個向量都被編碼爲8個字節大小
index.train(xb)
index.add(xb)
D, I = index.search(xb[:5], k)      # 測試
print(I)
print(D)
index.nprobe = 10                   # 與以前的方法相比
D, I = index.search(xq, k)          # 檢索
print(I[-5:])

結果爲

[[   0   78  608  159]
 [   1 1063  555  380]
 [   2  304  134   46]
 [   3   64  773  265]
 [   4  288  827  531]]
[[1.6157436 6.1152253 6.4348025 6.564184 ]
 [1.389575  5.6771317 5.9956017 6.486294 ]
 [1.7025063 6.121688  6.189084  6.489888 ]
 [1.8057687 6.5440307 6.6684756 6.859398 ]
 [1.4920276 5.79976   6.190908  6.3791513]]
[[ 9900  8746  9853 10437]
 [10494 10507 11373  9014]
 [10719 11291 10424 10138]
 [10122  9638 11113 10630]
 [ 9229 10304  9644 10370]]

IndexIVFPQ能正確找到距離最小的向量(他本身),但是距離不爲0,這是因爲向量數據存儲時候有壓縮,會損失一部分精度。

另外搜索真實查詢時,雖然結果大多是錯誤的(與剛纔的IVFFlat進行比較),但是它們在正確的空間區域,而對於真實數據,情況更好,因爲:
1.統一數據很難進行索引,因爲沒有規律性可以被利用來聚集或降低維度
2.對於自然數據,語義最近鄰居往往比不相關的結果更接近。
(參考文獻4)

8.索引選擇

如果需要精確的搜索結果,不要降維、不要量化,使用 Flat,同時,使用Flat 意味着數據不會被壓縮,將佔用同等大小的內存;
如果內存很緊張,可以使用 PCA 降維、PQ 量化編碼,來減少內存佔用,最終佔用的內存大小約等於 <降維後的向量維度> * <量化後的每個向量的字節數> * <向量個數>; 如果量化編碼後的字節數大於64,推薦使用SQx 替換PQx,準確度相同但速度會更快;爲了便於量化編碼,可以使用 OPQx_y 先對向量做線性變換,y 必須是編碼後字節數x的倍數,但最好小於維度dim和4x;
如果總向量個數 N 小於 1百萬,推薦使用 IVFx ,x 的選值介於 4sqrt(N) 和 16*sqrt(N) 之間,訓練數據的大小至少要是x的30倍;如果總向量個數 N 大於 1百萬、小於 1千萬,推薦使用 IMI2x10,實際內部聚類個數是 2 ^ (2 * 10),將需要64 * 2 ^ 10 個向量參與訓練;如果總向量個數 N 大於 1千萬、小於 1億,推薦使用 IMI2x12;如果總向量個數 N 大於 1億、小於 10億,推薦使用 IMI2x14;IMI方法不支持GPU;
IndexIVF 天生支持 add_with_ids 方法,對於不支持 add_with_ids方法的類型,可以使用IndexIDMap 輔助。
(參考文獻5)

參考文獻

1.faiss的wiki地址:https://github.com/facebookresearch/faiss/wiki
2.faiss的index: https://github.com/facebookresearch/faiss/wiki/Faiss-indexes
3.PQ算法: https://hal.inria.fr/file/index/docid/514462/filename/paper_hal.pdf
4.https://www.jianshu.com/p/43db601b8af1
5.https://github.com/vieyahn2017/iBlog/issues/339

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章