FAISS 用法

坑挖太多了,已经没时间填。那就再挖一个吧

faiss是为稠密向量提供高效相似度搜索和聚类的框架。由Facebook AI Research研发。 具有以下特性。

  • 1、提供多种检索方法
  • 2、速度快
  • 3、可存在内存和磁盘中
  • 4、C++实现,提供Python封装调用。
  • 5、大部分算法支持GPU实现


faiss简介及示例 - CSDN博客blog.csdn.net


#简单的cosine_similarity的计算
import faiss
from faiss import normalize_L2
import numpy as np
def cosine_similar():
    '''
    cosine_similarity
    use

    :return:
    '''
    d = 64                           # dimension
    nb = 105                    # database size
    #主要是为了测试不是归一化的vector
    training_vectors= np.random.random((nb, d)).astype('float32')*10
    print('just  compare with skearn')
    from sklearn.metrics.pairwise import cosine_similarity
    #主要是为了与sklearn 比较结果
    ag=cosine_similarity(training_vectors)
    fe=np.sort(ag,axis=1)
    print('normalize_L2')
    normalize_L2(training_vectors)
    print('IndexFlatIP')
    index=faiss.IndexFlatIP(d)
    index.train(training_vectors)
    print(index)
    print('train')
    print(index.is_trained)
    print('add')
    print(index)
    index.add(training_vectors)
    print('search')
    D, I =index.search(training_vectors[:100], 5)
    print(I[:5])                   # 表示最相近的前5个的index
    print(D[:5])  # 表示最相近的前5个的相似度的值

cosine_similar()

结论:sklearn 和 FAISS的IndexFlatIP模式的计算结果是一模一样的,IndexFlatIP这个模式就是精确的暴力的计算模式。

效率上,实际应用,embedding后向量计算相似度,在200w条的数据中,计算每一条前100最相似的邻居,全量计算肯定是不合适的,如果全量计算要等10h+才能出最后的结果,所以是分批计算,每次只计算若干条,经试验:计算1W条160s左右,计算2W条329s,计算1k条24s左右,经过测算1w条算一次是比较合适的,具体时间如下图:


上面暴力算是精度高了,但是search time 也边长了,因此引入倒排版的相似度计算:

创建IndexIVFFlat时需要指定一个其他的索引作为量化器(quantizer)来计算距离或相似度。

这里同使用IndexFlatL2对比,在add方法之前需要先训练。

下面简述示例中的几个参数。

faiss.METRIC_L2: faiss定义了两种衡量相似度的方法(metrics),分别为faiss.METRIC_L2faiss.METRIC_INNER_PRODUCT。一个是欧式距离,一个是向量内积(余弦相似度)。

nlist:聚类中心的个数

k:查找最相似的k个向量

index.nprobe:查找聚类中心的个数,默认为1个

#加速版的cosine_similarity的计算
def IndexIVFFlat():
    d = 64                           # dimension
    nb = 100005                    # database size
    np.random.seed(1234)             # make reproducible
    training_vectors= np.random.random((nb, d)).astype('float32')*10

    normalize_L2(training_vectors)

    nlist = 1000  # 聚类中心的个数
    k = 50 #邻居个数
    quantizer = faiss.IndexFlatIP(d)  # the other index,需要以其他index作为基础

    index = faiss.IndexIVFFlat(quantizer, d, nlist, faiss.METRIC_INNER_PRODUCT)
    # by default it performs inner-product search
    assert not index.is_trained
    index.train(training_vectors)
    assert index.is_trained
    index.nprobe = 300  # default nprobe is 1, try a few more
    index.add(training_vectors)  # add may be a bit slower as well
    t1=time.time()
    D, I = index.search(training_vectors[:100], k)  # actual search
    t2 = time.time()
    print('faiss kmeans result times {}'.format(t2-t1))
    # print(D[:5])  # neighbors of the 5 first queries
    print(I[:5])

IndexIVFFlat()

IndexIVFFlat这个模式就是Inverted file with exact post-verification,翻译过来叫倒排文件,其实是使用K-means建立聚类中心,然后通过查询最近的聚类中心,然后比较聚类中的所有向量得到相似的向量。

这个模式的Python的 写法不对的如下:

def make_index(): 
  index_sub = faiss.IndexFlatL2(10)
  index = faiss.IndexIDMap(index_sub)
  return index    # CRASH!

正确的写法:

def make_index(): 
  index_sub = faiss.IndexFlatL2(10)
  index = faiss.IndexIDMap(index_sub)
  index_sub.this.disown()
  index.own_fields = True
  return index

具体原因facebookresearch/faiss

如下2个图是IndexFlatIP模式的计算结果search time 0.9045s

暴力算的,结果是准确可靠的

如下2个图是IndexIVFFlat模式的计算结果,search time 0.0818s

暴力模式下的第4,7...个邻居(第一个邻居是自己本身)在倒排模式没了,速度是快了,但是找最相邻的邻居有丢三落四的情况。看相似度结果,能找到的邻居,相似度都与暴力模式是相同,所以相似度结果是准确的。

对比与结论:1.找最相邻的邻居有丢三落四的情况,2.计算的相似度结果是准确的(不会出现说原来排名靠后的邻居,现在变成排名靠前)。

效率上,

一些参数的敏感性:

nprobe越大,召回效果越好。

C=10 to give an idea ncentroids = C * sqrt (n)

index.nprobe 改成3,search time 0.0404949s,结果有点惨不忍睹了,第二居的相似度到了0.8851595,0.9以上的直接丢了。

暴力模式下的,和前面的图是一模一样的,只是复制了一份下来
index.nprobe =3的对比暴力模式的,直接丢弃很多

nlist 改成 2500 ,index.nprobe 还是300,search 时间0.0766s

这个是nlist 改成 2500

第3个邻居丢了,往后也丢了好长一段邻居,难道这nlist ,index.nprobe这两个有个合适的搭配?

改成nlist500 ,index.nprobe 还是300,search 时间0.107s,结果比最开始的nlist=1000 ,index.nprobe =300还要好,但是到了第二行还是可以看到丢了几个

结果好很多

nlist改成2500 ,index.nprobe改成1500,search 时间0.106s,这种改法比前面的结果还要好,全对。

数据总结:

base line :nlist=1000 ,index.nprobe=300,search =0.0818s,

nlist=500 ,index.nprobe=300,search =0.107s,召回效果比baseline好,同时search time 增加。

nlist=1000 ,index.nprobe=3,search time 0.0404949s,结果有点惨不忍睹了,同时search time 下降。

nlist=2500 ,index.nprobe=300,search =0.0766s,召回效果比baseline差好多,同时search time下降。

nlist=2500 ,index.nprobe=1500,search =0.106s,召回效果比baseline好非常多,同时search time上升,也比nlist=500 ,index.nprobe=300召回效果好,时间短。+

结论:

1.index.nprobe 越大,search time 越长,召回效果越好。

2.nlist=2500,不见得越大越好,需要与nprobe 配合,这两个参数同时大才有可能做到好效果。

3.不管哪种倒排的时间,在search 阶段都是比暴力求解快很多,0.9s与0.1s级别的差距。

以上的时间都没有包括train的时间。也暂时没有做内存使用的比较。


IndexIVFPQ这个是少内存条件下使用的。




归一化后计算的欧式距离是关于余弦相似的单调函数,可以认为归一化后,余弦相似与欧式距离效果是一致的(欧式距离越小等价于余弦相似度越大),具体证明如下(一定是L2归一化后)。

因此可以将 求余弦相似转为求欧式距离 ,余弦相似的计算复杂度过高,转为求欧式距离后,可以借助KDTree(KNN算法用到)或者BallTree(对高维向量友好)来降低复杂度。

编辑于 2018-08-16