NLP(十六):Faiss应用

Faiss库是由 Facebook 开发的适用于稠密向量匹配的开源库,支持 c++ 与 python 调用。

通过实验证实,128维的125W向量,在 CPU 下检索耗时约70ms,经过 GPU 加速后检索耗时仅5ms。

一、安装

Faiss 支持直接通过 conda 安装 python 接口,以及通过源码编译方式安装 c++ 和 python 接口,以下我会分别进行说明。

Conda 安装

Conda 下可以分别安装 cpu 与 gpu 两种版本

# CPU version only
 conda install faiss-cpu -c pytorch

# GPU version
 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

​二、源码编译

Faiss 在编译前需要预先完成依赖项的配置,这里的依赖项仅包括 openblas 与 lapack ,关于这两个依赖项的安装说明本文就不叙述了。

在安装完上述两个数学库之后,需执行 BLAS 的测试用例,若无报错即代表数学库安装成功。

# 进入Faiss源码目录
cd faiss# 根据系统配置选取makefile配置文件
cp example_makefiles/makefile.inc.Linux ./makefile.inc

# 检查环境是否符合编译条件
./configure

# 编译测试用例,运行无报错则表明数学库安装成功
make misc/test_blas
./misc/test_blas

在测试完成后即可执行 make 进行算法库的生成。需要说明的是,如果通过源码编译的方式安装 python 接口,需预先安装 swig,并修改 makefile.inc 中相关设置。

# 修改makefile.inc
SWIG = /usr/local/bin/swig
prefix ?= /usr/local/share/swig/3.0.12
PYTHONCFLAGS = -I/usr/include/python2.7 \
	-I/usr/local/lib/python2.7/dist-packages/numpy/core/include

三、使用

Faiss 支持多种向量检索方式,包括内积、欧氏距离等,同时支持精确检索与模糊搜索,篇幅有限嘛,我就先简单介绍精确检索相关内容。

一般来说,Faiss 的使用涉及两个概念:data — 包含了被检索的所有向量,即数据库;query — 索引值,Faiss 据此查找到对应向量在数据库 data 中的所在位置。

精确检索

精确检索不需要对数据进行训练操作,通过提供的索引方式来遍历数据库,精确计算查询向量与被查询向量之间距离,这里的距离可以是欧氏距离 (IndexFlatL2) 或内积 (IndexFlatIP) 等。

Faiss 的使用是围绕着 index 这一对象进行的,index 中包含了被索引的数据库向量以及对应的索引值。在构建 index 时,需预先提供数据库中每个向量的维度 d,随后通过 add() 的方式将被检索向量存入 index 中,最终通过 search() 接口获取与检索向量最邻近 topk 的距离及索引。

Python接口

# 创建index对象
index = faiss.IndexFlatIP(feature_dim)
# 将数据集加载入index中
index.add(np.ascontiguousarray(Datasets['feature']))
# 获取index中向量的个数
print(index.ntotal)  
# 获取与检索向量最邻近的topk的距离distance与索引值match_idx
distance, match_idx = index.search(feature.reshape(1,-1), topk)

c++接口与 python 接口基本一致,大家看着来就行。

GPU加速

Faiss 可以通过 GPU 进行硬件加速,极大的提升检索速度。

Python 接口

# 采用单卡GPU
res = faiss.StandardGpuResources()
# 创建index
index_flat = faiss.IndexFlatL2(d)
# 将index置入GPU下
gpu_index_flat = faiss.index_cpu_to_gpu(res, 0, index_flat)


四、Faiss结合Sentence_Bert

双语原文链接:Billion-scale semantic similarity search with FAISS+SBERT

 

介绍

语义搜索是一种关注句子意义而不是传统的关键词匹配的信息检索系统。尽管有许多文本嵌入可用于此目的,但将其扩展到构建低延迟api以从大量数据集合中获取数据是很少讨论的。在本文中,我将讨论如何使用SOTA语句嵌入(语句转换器)和FAISS来实现最小语义搜索引擎。

句子Transformers

它是一个框架或一组模型,给出句子或段落的密集向量表示。这些模型是transformer网络(BERT、RoBERTa等),它们专门针对语义文本相似性的任务进行了微调,因为BERT在这些任务中执行得不是很好。下面给出了不同模型在STS基准测试中的性能。

 

图片来源:句子 transformers

我们可以看到句子transformer模型比其他模型有很大的优势。

但是如果你用代码GLUE来看看排行榜,你会看到很多的模型超过90。为什么我们需要句子transformers?

在这些模型中,语义文本相似度被视为一个回归任务。这意味着,每当我们需要计算两个句子之间的相似度得分时,我们需要将它们一起传递到模型中,然后模型输出它们之间的数值分数。虽然这对于基准测试很有效,但是对于实际的用例来说,它的伸缩性很差,原因如下。

1.当你需要搜索大约10k个文档时,你需要进行10k个独立的推理计算,不可能单独计算嵌入量而只计算余弦相似度。见作者的解释
2.最大序列长度(模型一次可以接受的单词/标记的总数)在两个文档之间共享,这会导致的表示的含义由于分块而被稀释

FAISS

Faiss是一个基于C++的库,由FacebookAI构建,在Python中有完整的包装器,用于索引矢量化数据并对其进行有效的搜索。Faiss基于以下因素提供了不同的索引。

  • 搜索时间
  • 搜索质量
  • 每个索引向量使用的内存
  • 训练时间
  • 无监训练需要外部数据

因此,选择合适的指数将是这些因素之间的权衡。

加载模型并对数据集执行推理

首先,让我们安装并加载所需的库

!pip install faiss-cpu
!pip install -U sentence-transformersimport numpy as npimport torchimport osimport pandas as pdimport faissimport timefrom sentence_transformers import SentenceTransformer

加载一个包含一百万个数据点的数据集

我使用了一个来自Kaggle的数据集,其中包含了17年来出版的新闻标题。

df=pd.read_csv("abcnews-date-text.csv")
data=df.headline_text.to_list()

加载预训练模型并且进行推断

model = SentenceTransformer('distilbert-base-nli-mean-tokens')encoded_data = model.encode(data)

为数据集编制索引

我们可以根据我们的用例通过参考指南来选择不同的索引选项。

让我们定义索引并向其添加数据

index = faiss.IndexIDMap(faiss.IndexFlatIP(768))index.add_with_ids(encoded_data, np.array(range(0, len(data))))

序列化索引

faiss.write_index(index, 'abc_news')

将序列化的索引导出到托管搜索引擎的任何计算机中

反序列化索引

index = faiss.read_index('abc_news')

执行语义相似性搜索

让我们首先为搜索构建一个包装函数

def search(query):
t=time.time() query_vector = model.encode([query]) k = 5 top_k = index.search(query_vector, k) print('totaltime: {}'.format(time.time()-t)) return [data[_id] for _id in top_k[1].tolist()[0]]

执行搜索

query=str(input())
results=search(query)print('results :')for result in results: print('\t'

CPU中的结果

现在让我们看看搜索结果和响应时间

 

只需1.5秒,就可以在仅使用CPU后端的百万文本文档的数据集上执行基于意义的智能搜索。

GPU中的结果

首先让我们关闭CPU版本的Faiss并重启GPU版本

!pip uninstall faiss-cpu
!pip install faiss-gpu

之后执行相同步骤,但是最后将索引移到GPU上。

res = faiss.StandardGpuResources()
gpu_index = faiss.index_cpu_to_gpu(res, 0, index)

现在让我们转移这个搜索方法并用GPU执行这个搜索

很好,你可以在0.02秒内得到结果,使用GPU(在这个实验中使用了Tesla T4),它比CPU后端快75倍

但是为什么我不能仅仅序列化编码数据的NumPy数组而不是索引它们呢?如果我能等几秒钟的话,使用余弦相似性呢?

因为NumPy没有序列化函数,因此唯一的方法是将其转换为JSON,然后保存JSON对象,但是大小将增加五倍。例如,在768维向量空间中编码的一百万个数据点具有正常的索引,大约为3GB,将其转换为JSON将使其成为15GB,而普通机器无法保存它的RAM。因此,每次执行搜索时,我们都要运行一百万次计算推理,这是不实际的。

最后的想法

这是一个基本的实现,在语言模型部分和索引部分仍然需要做很多工作。有不同的索引选项,应该根据用例、数据大小和可用的计算能力选择正确的索引选项。另外,这里使用的句子嵌入只是对一些公共数据集进行了微调,在特定领域的数据集上对它们进行微调可以改进,从而提高搜索结果。

五、参考文献

[1] Nils Reimers and Iryna Gurevych. “Making Monolingual Sentence Embeddings Multilingual using Knowledge Distillation.” arXiv (2020): 2004.09813.

[2]Johnson, Jeff and Douze, Matthijs and J{\’e}gou, Herv{\’e}. “Billion-scale similarity search with GPUs” arXiv preprint arXiv:1702.08734.

六、实战

import numpy as np
import torch
import os
import pandas as pd
import faiss
import time
from sentence_transformers import SentenceTransformer

class SemanticMatch(object):
    def __init__(self):
        parent_path = os.path.split(os.path.realpath(__file__))[0]
        self.root = parent_path[:parent_path.find("models")] #E:\personas\semantics\
        data_path = os.path.join(self.root, "datas", "finally_data", "train_data.csv")
        df = pd.read_csv(data_path, sep="\t")
        self.data = df["sentence"].to_list()
        self.y = df["y"].to_list()
        self.bert_model_path = os.path.join(self.root, "checkpoints", "two_albert_similarity_model")
        self.bert_model = SentenceTransformer(self.bert_model_path)
        self.match_path = os.path.join(self.root,"datas", "match_data", "match.csv")
        self.match_data = pd.read_csv(self.match_path, sep="\t")
        self.key_yuyin = ["calling", "SUBSCRIBER", "SORRY", "小飞", "小智",
                          "呼叫", "智能", "小爱"]
        self.key_xiexie = ["谢谢", "好的", "知道", "收到"]
        self.key_zhuanda = ["转达", "我爸爸", "帮你带着话", "我是他女儿", "帮你转"]
        self.name_list = ["yuyin", "xiexie", "zhuanda"]
        self.dict_key = {
            "yuyin": self.key_yuyin,
            "xiexie": self.key_xiexie,
            "zhuanda": self.key_zhuanda
        }

    def bert(self, query):
        encoded_data = self.bert_model.encode(self.data)
        encoded_data = np.array(encoded_data)
        print(encoded_data.shape)

        print("开始创建索引")
        index = faiss.IndexFlatL2(512)
        index.add(encoded_data)
        #序列化持久存储
        faiss.write_index(index, 'abc_news')
        #反序列化index = faiss.read_index('abc_news')

        t = time.time()
        print("开始查询")
        query_vector = self.bert_model.encode([query])
        k = 10
        top_k = index.search(query_vector, k)
        print('totaltime: {}'.format(time.time() - t))
        print(top_k)

        return [self.data[_id] for _id in top_k[1].tolist()[0]]


    def main(self):
        print("开始执行")
        results = self.bert("你好")
        print('results :')
        for result in results:
            print('\t', result)

    def get_index(self, query):
        print("选择了所有faiss")
        index = faiss.read_index('abc_news')
        t = time.time()
        print("开始查询")
        query_vector = self.bert_model.encode([query])
        k = 20
        top_k = index.search(query_vector, k)
        print('查询耗费了多少秒: {}'.format(time.time() - t))
        print(top_k)
        result = [self.data[_id] for _id in top_k[1].tolist()[0]]
        y = [self.y[_id] for _id in top_k[1].tolist()[0]]
        return result, y

    def get_index_from_sub(self, query, faiss_n):
        data_path = os.path.join(self.root, "datas", "match_data", faiss_n + ".csv")
        df = pd.read_csv(data_path, sep="\t")
        data = df["sentence"].to_list()
        y = df["y"].to_list()
        index = faiss.read_index(faiss_n + ".faiss")
        t = time.time()
        print("开始查询")
        query_vector = self.bert_model.encode([query])
        k = 20
        top_k = index.search(query_vector, k)
        print('查询耗费了多少秒: {}'.format(time.time() - t))
        print(top_k)
        result = [data[_id] for _id in top_k[1].tolist()[0]]
        y = [y[_id] for _id in top_k[1].tolist()[0]]
        return result, y




    def bert_match(self):

        s = self.match_data["sentence"]
        y = self.match_data["y"]
        i = 0
        j = 0
        for s1, y1 in zip(s, y):
            i = i + 1
            print("=============================开始新一轮查询===============================")
            result, y = self.search_key(s1)
            print("查询值:", s1)
            print("相似度最高的20的类别:", y)
            print("标注的类别:", y1)
            print("相似度最高的语句:", result)
            maxlabel = max(y, key=y.count)
            if maxlabel == y1:
                j = j+ 1
        print(j/i)

    def write_faiss(self, _data, faiss_name):

        encoded_data = self.bert_model.encode(_data)
        encoded_data = np.array(encoded_data)
        print("开始创建索引")
        index = faiss.IndexFlatL2(512)
        index.add(encoded_data)
        #序列化持久存储
        faiss.write_index(index, faiss_name + ".faiss")

    def creat_faiss_file(self):
        for name in self.name_list:
            file_path = os.path.join(self.root, "datas", "match_data", name + ".csv")
            df = pd.read_csv(file_path, sep="\t")
            data_list = df["sentence"].to_list()
            faiss_name = name
            self.write_faiss(data_list, faiss_name)

    def search_key(self, query):
        faiss_name = []
        for k,v in self.dict_key.items():
            for key in v:
                if key in query:
                    faiss_name.append(k)
        faiss_name = set(faiss_name)
        print("选择faiss:", faiss_name)
        if len(faiss_name) > 0:

            r, label = [], []
            for faiss_n in faiss_name:
                result, y = self.get_index_from_sub(query, faiss_n)
                r.extend(result)
                label.extend(y)
            return r, label
        else:
            r, label = self.get_index(query)
            return r, label

    def main(self):
        s = self.match_data["sentence"]
        y = self.match_data["y"]
        i = 0
        j = 0
        for s1, y1 in zip(s, y):
            i = i + 1
            print("=============================开始新一轮查询===============================")
            result, y = self.search_key(s1)
            print("查询值:", s1)
            print("相似度最高的20的类别:", y)
            print("标注的类别:", y1)
            print("相似度最高的语句:", result)
            maxlabel = max(y, key=y.count)
            if maxlabel == y1:
                j = j+ 1
        print(j/i)

if __name__ == '__main__':
    SemanticMatch().main()

 

七、原理分析

https://www.cnblogs.com/yhzhou/p/10568728.html

 1、Faiss简介

  Faiss是Facebook AI团队开源的针对聚类和相似性搜索库,为稠密向量提供高效相似度搜索和聚类,支持十亿级别向量的搜索,是目前最为成熟的近似近邻搜索库。它包含多种搜索任意大小向量集(备注:向量集大小由RAM内存决定)的算法,以及用于算法评估和参数调整的支持代码。Faiss用C++编写,并提供与Numpy完美衔接的Python接口。除此以外,对一些核心算法提供了GPU实现。相关介绍参考《Faiss:Facebook 开源的相似性搜索类库

 2、Faiss安装

  参考《faiss_note/1.Install faiss安装.ipynb》,此文是对英文版本的翻译,便于查看。

      基于本机环境,采用了anaconda进行安装,这也是faiss推荐的方式,facebook研发团队也会及时推出faiss的新版本conda安装包,在conda安装时会自行安装所需的libgcc, mkl, numpy模块。

      针对mac os系统,可以先安装Homebrew(mac下的缺失包管理,比较方便使用)。

  安装anaconda的命令如下所示:

复制代码
#安装anaconda包
brew cask install anaconda
#conda加入环境变量
export PATH=/usr/local/anaconda3/bin:"$PATH"
#更新conda
conda update conda
#先安装mkl
conda install mkl
#安装faiss-cpu
conda install faiss-cpu -c pytorch
#测试安装是否成功
python -c "import faiss”
复制代码

  备注:mkl全称Intel Math Kernel Library,提供经过高度优化和大量线程化处理的数学例程,面向性能要求极高的科学、工程及金融等领域的应用。MKL是一款商用函数库(考虑版权问题,后续可以替换为OpenBLAS),在Intel CPU上,MKL的性能要远高于Eigen, OpenBLAS和其性能差距不是太大,但OpenBLAS提供的函数相对较少,另外OpenBLAS的编译依赖系统环境。

 3、Faiss原理及示例分析

  3.1 Faiss核心算法实现

  Faiss对一些基础的算法提供了非常高效的失效

  • 聚类Faiss提供了一个高效的k-means实现
  • PCA降维算法
  • PQ(ProductQuantizer)编码/解码

  3.2 Faiss功能流程说明

       通过Faiss文档介绍可以了解faiss的主要功能就是相似度搜索。如下图所示,以图片搜索为例,所谓相似度搜索,便是在给定的一堆图片中,寻找出我指定的目标最像的K张图片,也简称为KNN(K近邻)问题。

  为了解决KNN问题,在工程上需要实现对已有图库的存储,当用户指定检索图片后,需要知道如何从存储的图片库中找到最相似的K张图片。基于此,我们推测Faiss在应用场景中具备添加功能和搜索功能,有了添加相应的修改和删除功能也会接踵而来,从上述分析看,Faiss本质上是一个向量(矢量)数据库。

      对于数据库来说,时空优化是两个永恒的主题,即在存储上如何以更少的空间来存储更多的信息,在搜索上如何以更快的速度来搜索出更准确的信息。如何减少搜索所需的时间?在数据库中很最常见的操作便是加各种索引,把各种加速搜索算法的功能或空间换时间的策略都封装成各种各样的索引,以满足各种不同的引用场景。

 3.3 组件分析

      Faiss中最常用的是索引Index,而后是PCA降维、PQ乘积量化,这里针对Index和PQ进行说明,PCA降维从流程上都可以理解。

  3.3.1索引Index

      Faiss中有两个基础索引类Index、IndexBinary,下面我们先从类图进行分析。

  下面给出Index和IndexBinary的类图如下所示:

  Faiss提供了针对不同场景下应用对Index的封装类,这里我们针对Index基类进行说明。

  基础索引的说明参考:Faiss indexes涉及方法解释、参数说明以及推荐试用的工厂方法创建时的标识等。

      索引的创建提供了工厂方法,可以通过字符串灵活的创建不同的索引。

index = faiss.index_factory(d,"PCA32,IVF100,PQ8 ")

  该字符串的含义为:使用PCA算法将向量降维到32维, 划分成100个nprobe (搜索空间), 通过PQ算法将每个向量压缩成8bit。

  其他的字符串可以参考上文给出的Faiss indexes链接中给出的标识。

 3.3.1.1索引说明

  此部分对索引id进行说明,此部分的理解是基于PQ量化及Faiss创建不同的索引时选择的量化器而来,可能会稍有偏差,不影响对Faiss的使用操作。

      默认情况,Faiss会为每个输入的向量记录一个次序id,也可以为向量指定任意我们需要的id。部分索引类(IndexIVFFlat/IndexPQ/IndexIVFPQ等)有add_with_ids方法,可以为每个向量对应一个64-bit的id,搜索的时候返回此id。此段中说明的id从我的角度理解就是索引。(备注:id是long型数据,所有的索引id类型在Index基类中已经定义,参考类图中标注,typedef long idx_t;    ///< all indices are this type)

      示例:

复制代码
import numpy as np
import faiss                   # make faiss available

# 构造数据
import time
d = 64                           # dimension
nb = 1000000                      # database size
nq = 1000000                       # 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.

# 为向量集构建IndexFlatL2索引,它是最简单的索引类型,只执行强力L2距离搜索
index = faiss.IndexFlatL2(d)   # build the index
# #此处索引是按照默认方式,即faiss给的次序id为主
# #可以添加我们需要的索引方式,因IndexFlatL2不支持add_with_ids方法,需要借助IndexIDMap进行映射,代码如下
# ids = np.arange(100000, 200000)  #id设定为6位数整数,默认id从0开始,这里我们将其设置从100000开始
# index2 = faiss.IndexIDMap(index)
# index2.add_with_ids(xb, ids)
#
# print(index2.is_trained)
# # index.add(xb)                  # add vectors to the index
# print(index2.ntotal)
# k = 4   # we want to see 4 nearest neighbors
# D, I = index2.search(xb[:5], k) # sanity check
# print(I)     # 向量索引位置
# print(D)     # 相似度矩阵

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(xb[:5])
# print(I)     # 向量索引位置
# print(D)     # 相似度矩阵

D, I = index.search(xq, 10)     # actual search
# xq is the query data
# k is the num of neigbors you want to search
# D is the distance matrix between xq and k neigbors
# I is the index matrix of k neigbors
print(I[:5])                   # neighbors of the 5 first queries
print(I[-5:]) # neighbors of the 5 last queries

#从index中恢复数据,indexFlatL2索引就是将向量进行排序
# print(xb[381])
# print(index.reconstruct(381))
复制代码

 3.3.1.2索引选择

      此部分没做实践验证,对Faiss给的部分说明进行翻译过来作为后续我们使用的一个参考。

      如果关心返回精度,可以使用IndexFlatL2,该索引能确保返回精确结果。一般将其作为baseline与其他索引方式对比,以便在精度和时间开销之间做权衡。不支持add_with_ids,如果需要,可以用“IDMap”给予任意定义id。

      如果关注内存开销,可以使用“..., Flat“的索引,"..."是聚类操作,聚类之后将每个向量映射到相应的bucket。该索引类型并不会保存压缩之后的数据,而是保存原始数据,所以内存开销与原始数据一致。通过nprobe参数控制速度/精度。

      对内存开销比较关心的话,可以在聚类的基础上使用PQ成绩量化进行处理。

  3.3.1.3检索数据恢复

      Faiss检索返回的是数据的索引及数据的计算距离,在检索获得的索引后需要根据索引将原始数据取出。

      Faiss提供了两种方式,一种是一条一条的进行恢复,一种是批量恢复。

  给定id,可以使用reconstruct进行单条取出数据;可以使用reconstruct_n方法从index中回批量复出原始向量(备注:该方法从给的示例看是恢复连续的数据(0,10),如果索引是离散的话恢复数据暂时还没做实践)。

  上述方法支持IndexFlat, IndexIVFFlat (需要与make_direct_map结合), IndexIVFPQ(需要与make_direct_map结合)等几类索引类型。

  3.3.2PCA降维

      具体的算法流程没有进行深入的了解,可以参考看:《PCA 降维算法详解 以及代码示例》,待后续算法学习中在进行深入了解。

      基于3.2节中对Faiss流程的说明,简要说下对Faiss中PCA的理解。

      PCA通过数据压缩减少内存或者硬盘的使用以及数据降维加快机器学习的速度。从数据存储的角度,图片处理中通过PCA可以将图片从高维空间(p维)转换到低维空间(q维, 其中p > q ),其具体操作便是是将高维空间中的图片向量(n*p)乘以一个转换矩阵(p*q),得到一个低维空间中的向量(n*q)。

  为了使得在整个降维的过程中信息丢失最少,我们需要对待转换图片进行分析计算得到相应的转换矩阵(p*q)。也就是说这个降维中乘以的转换矩阵是与待转换图片息息相关的。回到我们的Faiss中来,假设我期望使用PCA预处理来减少Index中的存储空间,那在整个处理流程中,除了输入搜索图库外,我必须多输入一个转换矩阵,但是这个转换矩阵是与图库息息相关的,是可以由图库数据计算出来的。如果把这个转换矩阵看成一个参数的话,我们可以发现,在Faiss的一些预处理中,我们会引入一些参数,这些参数又无法一开始由人工来指定,只能通过喂样本来训练出来,所以Index中需要有这样的一个train() 函数来为这种参数的训练提供输入训练样本的接口。

  3.3.3Product quantization(乘积量化PQ)

      Faiss中使用的乘积量化是Faiss的作者在2011年发表的论文,参考:《Product Quantization for Nearest Neighbor Search

      PQ算法可以理解为首先把原始的向量空间分解为m个低维向量空间的笛卡尔积,并对分解得到的低维向量空间分别做量化。即是把原始D维向量(比如D=128)分成m组(比如m=4),每组就是D∗=D/m维的子向量(比如D∗=D/m=128/4=32),各自用kmeans算法学习到一个码本,然后这些码本的笛卡尔积就是原始D维向量对应的码本。用qj表示第j组子向量,用Cj表示其对应学习到的码本,那么原始D维向量对应的码本就是C=C1×C2×…×Cm。用k∗表示子向量的聚类中心点数或者说码本大小,那么原始D维向量对应的聚类中心点数或者说码本大小就是k=(k∗)m。

      示例参考《实例理解product quantization算法》。

 上文针对Faiss安装和一些原理做了简单说明,本文针对标题所列三种索引方式进行编码验证。

  首先生成数据集,这里采用100万条数据,每条50维,生成数据做本地化保存,代码如下:

复制代码
import numpy as np

# 构造数据
import time
d = 50                           # dimension
nb = 1000000                     # database size
# nq = 1000000                       # 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.

print(xb[:1])

# 写入文件中
# file = open('data.txt', 'w')
np.savetxt('data.txt', xb)
复制代码

  在上述训练集的基础上,做自身查询,即本身即是Faiss的训练集也是查寻集,三个索引的查询方式在一个文件内,如下示例代码:

复制代码
import numpy as np
import faiss

# 读取文件形成numpy矩阵
data = []
with open('data.txt', 'rb') as f:
    for line in f:
        temp = line.split()
        data.append(temp)
print(data[0])
# 训练与需要计算的数据
dataArray = np.array(data).astype('float32')

# print(dataArray[0])
# print(dataArray.shape[1])
# 获取数据的维度
d = dataArray.shape[1]

# IndexFlatL2索引方式
# # 为向量集构建IndexFlatL2索引,它是最简单的索引类型,只执行强力L2距离搜索
# index = faiss.IndexFlatL2(d)   # build the index
# index.add(dataArray)                  # add vectors to the index
#
# # we want to see 4 nearest neighbors
# k = 11
# # search
# D, I = index.search(dataArray, k)
#
# # neighbors of the 5 first queries
# print(I[:5])

# IndexIVFFlat索引方式
# nlist = 100 # 单元格数
# k = 11
# quantizer = faiss.IndexFlatL2(d)  # the other index  d是向量维度
# 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(dataArray)
# assert index.is_trained
# index.add(dataArray)                  # add may be a bit slower as well
# index.nprobe = 10        # 执行搜索访问的单元格数(nlist以外)      # default nprobe is 1, try a few more
# D, I = index.search(dataArray, k)     # actual search
#
# print(I[:5]) # neighbors of the 5 last queries

# IndexIVFPQ索引方式
nlist = 100
m = 5
k = 11
quantizer = faiss.IndexFlatL2(d)  # this remains the same
# 为了扩展到非常大的数据集,Faiss提供了基于产品量化器的有损压缩来压缩存储的向量的变体。压缩的方法基于乘积量化。
# 损失了一定精度为代价, 自身距离也不为0, 这是由于有损压缩。
index = faiss.IndexIVFPQ(quantizer, d, nlist, m, 8)
# 8 specifies that each sub-vector is encoded as 8 bits
index.train(dataArray)
index.add(dataArray)
# D, I = index.search(xb[:5], k) # sanity check
# print(I)
# print(D)
index.nprobe = 10              # make comparable with experiment above
D, I = index.search(dataArray, k)     # search
print(I[:5])
复制代码

  三种索引的结果和运行时长统计如下图所示:

  从上述结果可以看出,加聚类后运行速度比暴力搜索提升很多,结果准确度也基本一致,加聚类加量化运行速度更快,结果相比暴力搜索差距较大,在数据量不是很大、维度不高的情况下,建议选择加聚类的索引方式即可。

八、多种索引

https://zhuanlan.zhihu.com/p/133210698

Faiss的全称是Facebook AI Similarity Search。
这是一个开源库,针对高维空间中的海量数据,提供了高效且可靠的检索方法。
暴力检索耗时巨大,对于一个要求实时人脸识别的应用来说是不可取的。
而Faiss则为这种场景提供了一套解决方案。
Faiss从两个方面改善了暴力搜索算法存在的问题:降低空间占用加快检索速度首先,
Faiss中提供了若干种方法实现数据压缩,包括PCA、Product-Quantization等。

(1)对于一个检索任务,我们的操作流程一定分为三步:训练、构建数据库、查询。因此下面将分别对这三个步骤详细介绍。

faiss的核心就是索引(index)概念,它封装了一组向量,并且可以选择是否进行预处理,帮忙高效的检索向量。faiss中由多种类型的索引,我们可以是呀最简单的索引类型:indexFlatL2,这就是暴力检索L2距离(欧式距离)。不管建立什么类型的索引,我们都必须先知道向量的维度。另外,对于大部分索引类型而言,在建立的时候都包含了训练阶段,但是L2这个索引可以跳过。当索引被建立 和训练之后,我能就可以调用add,search着两种方法。

(2)精确搜索:faiss.indexFlatL2(欧式距离) faiss.indexFlatIP(内积)

在精确搜索的时候,选择上述两种索引类型,遍历计算索引向量,不需要做训练操作。下面的例子中,给出了上面提到的两种索引实际应用。

import sys
import faiss
import numpy as np 
d = 64  
nb = 100
nq = 10
np.random.seed(1234)
xb = np.random.random((nb,d)).astype('float32')
print xb[:2]
xb[:, 0] += np.arange(nb).astype('float32') / 1000
#sys.exit()
print xb[:2]
xq = np.random.random((nq, d)).astype('float32')
xq[:, 0] += np.arange(nq).astype('float32') / 1000
index = faiss.IndexFlatL2(d) # buid the index
print (index.is_trained),"@@"
index.add(xb)
print index.ntotal  # 加入了多少行数据

k = 4
D,I = index.search(xb[:5],k)
print "IIIIIIIIIIII" 
print I
print "ddddddddd"
print D

print "#########"
index = faiss.IndexFlatIP(d)
index.add(xb)
k = 4
D,I = index.search(xb[:5],k)
print I
print "ddddddddd"
print D

(3)如果存在的向量太多,通过暴力搜索索引indexFlatL2搜索时间会边长,这里介绍一种加速搜索的方法indexIVFFlat(倒排文件)。起始就是使用k-means建立聚类中心,然后通过查询最近的聚类中心,然后比较聚类中所有向量得到相似的向量。

创建IndexIVFFlat的时候需要指定一个其他的索引作为量化器(quantizer)来计算距离或者相似度。faiss提供了两种衡量相似度的方法:1)faiss.METRIC_L2、
2)faiss.METRIC_INNER_PRODUCT。一个是欧式距离,一个是向量内积。

还有其他几个参数:nlist:聚类中心的个数;k:查找最相似的k个向量;index.nprobe:查找聚类中心的个数,默认为1个。

nlist = 50  #  聚类中心个数
k = 10      # 查找最相似的k个向量
quantizer = faiss.IndexFlatL2(d)  # 量化器
index = faiss.IndexIVFFlat(quantizer, d, nlist, faiss.METRIC_L2)
       # METRIC_L2计算L2距离, 或faiss.METRIC_INNER_PRODUCT计算内积
assert not index.is_trained   #倒排表索引类型需要训练
index.train(data)  # 训练数据集应该与数据库数据集同分布
assert index.is_trained
# index.nprobe :查找聚类中心的个数 默认为1 
#index.nprobe = 300 # default nprobe is 1, try a few more
index.add(data)
index.nprobe = 50  # 选择n个维诺空间进行索引,
dis, ind = index.search(query, k)

1.index.nprobe 越大,search time 越长,召回效果越好。
2.nlist=2500,不见得越大越好,需要与nprobe 配合,这两个参数同时大才有可能做到好效果。
3.不管哪种倒排的时间,在search 阶段都是比暴力求解快很多,0.9s与0.1s级别的差距。
以上的时间都没有包括train的时间。也暂时没有做内存使用的比较。

# -*- coding:utf-8 -*-
#coding:utf-8
import sys
import faiss
import numpy as np 
import os
import sys
reload(sys)
sys.setdefaultencoding('utf-8') 
d = 64  
nb = 100
nq = 10
np.random.seed(1234)
xb = np.random.random((nb,d)).astype('float32')
xb[:, 0] += np.arange(nb).astype('float32') / 1000
xq = np.random.random((nq, d)).astype('float32')
xq[:, 0] += np.arange(nq).astype('float32') / 1000

nlist = 50 # 聚类中心个数
k = 4   # 查询相似的k个向量
quantizer = faiss.IndexFlatL2(d)
index = faiss.IndexIVFFlat(quantizer,d,nlist,faiss.METRIC_L2)

print (index.is_trained),"@@"
index.train(xb)
index.add(xb)
index.nprobe = 3 # 搜索的聚类 个数
print index.ntotal  # 

D,I = index.search(xb[:5],k)
print "IIIIIIIIIIII" 
print I
print "ddddddddd"
print D

(4)上面我们在建立 IndexFlatL2 和IndexIVFFlat都会全量存储所有向量在内存中,为了满足大的数据需求,faiss提供了一种基于 Product Quantizer(乘积量化)的压缩算法,编码向量大小到指定的字节数。此时,存储的向量是压缩过的,查询的距离也是近似的。

##注意这个时候 没有相似度 度量参数

### 乘积量化  
d = 64  
nb = 10000
nq = 10
np.random.seed(1234)
xb = np.random.random((nb,d)).astype('float32')
xb[:, 0] += np.arange(nb).astype('float32') / 1000
xq = np.random.random((nq, d)).astype('float32')
xq[:, 0] += np.arange(nq).astype('float32') / 1000

nlist = 50 # 聚类中心个数
k = 4   # 查询相似的k个向量
m = 8  # number of bytes per vector    每个向量都被编码为8个字节大小
quantizer = faiss.IndexFlatL2(d)
index = faiss.IndexIVFPQ(quantizer,d,nlist,m,8)   ##注意这个时候 没有相似度 度量参数

print (index.is_trained),"@@"
index.train(xb)
index.add(xb)
index.nprobe = 3 # 搜索的聚类 个数
print index.ntotal  # 

D,I = index.search(xq[:5],k)
print "IIIIIIIIIIII" 
print I
print "ddddddddd"
print D

 

######什么是乘积量化 ########

(5)乘积量化

 

(6)faiss 安装方法(python版本)

faiss 安装步骤: python 版本安装

(1) 下载最新版的 anaconda 之前遇到过 用老版本的 anconda 装不上 faiss 但是换成新版本的就可以了 最新版本的 已上传 网盘

(2) 安装 anaconda sh Anaconda2-2019.03-Linux-x86_64 .sh 参考 

有两点需要注意 一是 可以修改 安装路径

输入一个有权限的 路径 并追加一个文件夹 eg: anaconda2

二是,在最后 要把 环境变量加入

conda install faiss-cpu -c pytorch

 
 
 
posted @ 2021-05-21 11:39  jasonzhangxianrong  阅读(2993)  评论(0编辑  收藏  举报