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

 
 
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM