Faiss中文教程

文章目录

  • 前言
  • 一、简介
  • 二、向量检索(Similarity Search)
    • 1.索引
    • 2.最近邻算法(KNN,K-NearestNeighbor)
    • 3.近似最近邻算法(ANN,Approximate Nearest Neighbor)
  • 三、Faiss中文教程
    • * * Tutorial
    • (一)Getting started
    • (二)Faster search
    • (三)Lower memory footprint
    • (四)Running on GPUs
    • * * Basics
    • (一)MetricType and distances
    • (二)Faiss building blocks: clustering, PCA, quantization
    • (三)Guidelines to choose an index
  • 总结


前言

本文是根据Faiss发布的官方wiki,在学习过程中写的中文版记录。有表意不清晰的地方可查看英文原版,如有侵权,请联系作者删除。

英文教程:Faiss-Facebook


一、简介

  Faiss是一个用于对向量进行高效相似向量搜索(Similarity Search)和聚类的库。它同时支持验证和调参。它是C++写的,同时支持Python调用,一些有用的算法也实现了GPU版本。

二、向量检索(Similarity Search)

1.索引

  给定一个d维的向量集合x_i,Faiss在内存上建立一个数据结构。当这个结构被建立之后,给定一个新的d维向量x时,这个结构可以高效地返回与x最相近的Topk个向量。这个结构就叫做索引(index)。这个对象有一个add方法来添加x_i向量,计算topk个最相近向量的操作就叫做搜索(search)

2.最近邻算法(KNN,K-NearestNeighbor)

  1中提到的问题,其实就是为1个或者多个向量找它的k个最近邻的向量,可以认为是一个最近邻问题。第一反应的解决办法可能是计算x和向量集合中所有向量的欧式距离(或者cos距离,点积等),然后从小到大排序,这看上去似乎并不需要很复杂的算法,但在现实场景中往往面临海量的数据,当数据量在上亿级别的时候,这种暴力搜索的方法就并不可取了。所以在进行大规模向量检索时,通常采用的是近似最近邻算法(Approximate Nearest Neighbor,ANN)

3.近似最近邻算法(ANN,Approximate Nearest Neighbor)

  和暴力解法不同的是,ANN算法不需要和集合中的所有向量计算距离,而是有选择地和部分向量计算距离,这样得到的答案往往不是最精确的,但是在效率上却大大提高了。所以向量检索其实是一个精度和效率的权衡问题。
  ANN算法主要分为基于空间划分的方法基于图的方法。
  基于划分的方法的主要思路是将向量划分到不同的空间中(可以通过聚类等方法),在检索时先确定最相近的一个或多个空间,然后只在这些空间中搜索最近的向量。主要有以下几种:

  • 基于树的方法
    -比较典型的有KDTree、BallTree、VPTree。KDTree会选取向量中某个方差最大的维度取中值作为判定标准,也就是以超平面去划分空间。基于树的方法还有很多其他类型,但万变不离其宗,无非就是按照某个判定标准,对向量空间进行划分,但不管怎么划分,由于需要回溯的,都决定了基于树的方法在性能上要稍逊一筹。
  • 局部敏感哈希(Locality Sensitive Hashing,LSH)
    -高维空间的两点若距离很近,那么设计一种哈希函数对这两点进行哈希值计算,使得他们哈希值有很大的概率是一样的,若两点之间的距离较远,他们哈希值相同的概率会很小。不同距离度量的哈希函数不同,不是所有距离度量(如内积)都能找到对应局部敏感哈希。
  • 基于倒排的方法
    -向量的倒排索引不同于传统的倒排索引(根据文档中包含某个词,就把这篇文档放到这个词的倒排索引结构中),向量的倒排索引是通过聚类把向量空间划分为K个子空间,每个向量将归入距离自己最近的聚类中心的倒排结构中。

  基于图的方法的主要思路是邻居的邻居也可能是邻居,这样把最近邻的查找转化为图的遍历,由于其连通性,可以针对性的考察部分向量而不是按区域来考察,因此可以大幅降低向量的考察范围。典型的如HNSW方法。

  • 其他加速技术:
      上述方法缩小了向量检索的候选空间,但是对于高维向量来说,两两向量之间的计算量还是很大,向量量化就是从这个角度来减少计算量,比较典型的是乘积量化方法。通过把高维向量切分成不同的子段,原始向量就可以表示成由各个子段索引值构成的压缩向量。计算压缩向量之间的距离可以大大减小计算量。
      (以上只是粗略地描述了一下几类方法(参考【一文纵览KNN(ANN)向量检索】),其中细节还需要寻找对应的资料进一步理解,后续有时间会补充上详细的算法描述)
      以上的算法在后续的Faiss使用介绍中可能会陆续用到。

三、Faiss中文教程

  上面简单介绍了向量检索的背景,后面会基本按照Tutorial的顺序来介绍Faiss的使用。

* * Tutorial

(一)Getting started

  假设Faiss已经安装好了,且后面的代码演示主要以Python为主,C++版本可以去官方github上查看。

1.构造数据
  
  Faiss处理固定维度d维的向量集合,通常从10维到100维均可。这些集合可以被存储在矩阵中。我们假设按行存储,向量i的第j维元素被存储在矩阵的第i行第j列。Faiss使用的是32位的浮点数矩阵。
  我们需要两个矩阵:
  xb用于构建数据库,包含我们要被构建索引的,即将被检索的所有向量。它的维度是nb * d
  xq是我们需要寻找其最近邻的查询向量,它的维度是nq * d。如果只有单一query,nq=1。
  在下面的例子中,我们将要使用的向量是64维的服从均匀分布的向量,仅仅是为了实验,我们给第一维做了一些变换。

In Python

import numpy as np
d = 64                           # 定义向量维度
nb = 100000                      # 数据库大小
nq = 10000                       # query数量
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.

  在python中,矩阵通常被表示成numpy数组的形式(array)。数据类型dtype一定要等于float32

2. 构建索引并向其中添加向量

  Faiss通常构建一个index对象,它封装了数据库的向量集合,并且有选择地对它们进行一些预处理,使得检索更加高效。Faiss有很多不同类型的索引(index),我们首先使用最简单的版本,暴力搜索——欧式距离(L2 distance)的方法:IndexFlatL2
  所有的index在它们构建的时候都需要知道它们要操作的向量维度,在我们的例子里就是d。然后,许多index需要一个训练(training)阶段,来分析向量的分布。对于IndexFlatL2索引来说,我们可以跳过这个步骤。
  当索引被构建和训练之后,就可以在index上执行两个操作:addsearch
  向index中添加元素,我们称作add。我们也可以查看index的两个状态变量:is_trained,一个boolean型的变量指示是否需要训练;ntotal,表示被构建索引的向量的个数。
  一些index可以给每个向量存储它们对应的int型ID(但不是IndexFlatL2)。如果没有提供ID,add只是用向量原始的作为id,比如:第1个向量的id是0,第2个向量的id是1,等等。

In Python

import faiss                   # 使Faiss可调用
index = faiss.IndexFlatL2(d)   # 构建索引
print(index.is_trained)        # 打印index是否训练了
index.add(xb)                  # 向index中添加向量
print(index.ntotal)            # 打印被构建索引的向量数量

Results

True  # index已经是训练好的了
100000 # 被存储在index中的向量

3. 检索(Search)

  在索引上可执行的基础search操作是k近邻搜索,也就是对于每一个query向量,在数据库中寻找它最相近的 k 个邻居。
  这个操作的结果被方便地存储在一个nq * k大小的整型矩阵中,其中第 i 行包含第 i 个query向量的邻居的IDs(根据距离递增排序好的)。除了这个矩阵,search操作还返回了一个nq * k大小的浮点型矩阵,对应着平方距离。
  为了合理性检查,我们可以search一些数据库中的向量,来确保它们的最近邻就是它们自己本身。

In Python

k = 4                          # 我们想查看4个最近的邻居
D, I = index.search(xb[:5], k) # 合理性检查
print(I)
print(D)
D, I = index.search(xq, k)     # 真实的search
print(I[:5])                   # 打印前5个query的邻居
print(I[-5:])                  # 打印后5个query的邻居

Results

  上述合理性检查的代码结果如下:

[[  0 393 363  78]
 [  1 555 277 364]
 [  2 304 101  13]
 [  3 173  18 182]
 [  4 288 370 531]]

[[ 0.          7.17517328  7.2076292   7.25116253]
 [ 0.          6.32356453  6.6845808   6.79994535]
 [ 0.          5.79640865  6.39173603  7.28151226]
 [ 0.          7.27790546  7.52798653  7.66284657]
 [ 0.          6.76380348  7.29512024  7.36881447]]

  即,每个query最近的邻居就是它们本身的向量id,对应的距离是0。并且在一行中,距离是增加的。
  真实的search结果如下:

[[ 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维被添加了随机值,所以数据集在d维空间的第1维被扰乱。因此前面一些向量的邻居集中在数据集的开头,在~10000周围的向量的邻居在集中在数据集的10000左右的位置。
  执行这次search操作在一台2016年的机器上大约耗时3.3秒。

(二)Faster search

(三)Lower memory footprint

(四)Running on GPUs

* * Basics

(一)MetricType and distances

(二)Faiss building blocks: clustering, PCA, quantization

(三)Guidelines to choose an index

(未完待续…)


总结

你可能感兴趣的:(python,机器学习,数据挖掘)