Faiss(12):python接口faiss.py文件分析

1. 前言

本篇笔记主要分析faiss code下的python接口文件——faiss.py的工作流程以及内容。

2. faiss.py分析

2.1 导入文件

在faiss code 编译完成后,在python目录下执行make/make install命令后,会在该文件下产生faiss.py, faiss/, faiss.egg-info/等文件和目录,其中faiss.py和faiss/目录下的__init__.py内容一样,但是在应用程序中import faiss时,导入的是__init__.py文件。

2.2 执行流程

faiss.py在导入时,会执行以下工作:

  1. 导入依赖模块
import numpy as np
import sys
import inspect
import pdb
import platform
import subprocess
import logging
  1. 创建一个log文件,这里__name__是"faiss"
logger = logging.getLogger(__name__)
  1. 根据指令集加载swigfaiss包
try:
    instr_set = instruction_set()
    if instr_set == "AVX2":
        logger.info("Loading faiss with AVX2 support.")
        from .swigfaiss_avx2 import *
    else:
        logger.info("Loading faiss.")
        from .swigfaiss import *

except ImportError:
    # we import * so that the symbol X can be accessed as faiss.X
    logger.info("Loading faiss.")
    from .swigfaiss import *

在instruction_set方法中会调用platform.system()和subprocess.check_output来获取当前平台的系统信息和CPU的指令集信息。我使用的平台返回的是Linux和"AVX2"。

  1. 记录版本信息
__version__ = "%d.%d.%d" % (FAISS_VERSION_MAJOR,
                            FAISS_VERSION_MINOR,
                            FAISS_VERSION_PATCH)

上述信息定义在Index.h文件中,我使用的版本是1.6.1

  1. 替换clustering的train方法
handle_Clustering()

def handle_Clustering():
    def replacement_train(self, x, index):
        assert x.flags.contiguous
        n, d = x.shape
        assert d == self.d
        self.train_c(n, swig_ptr(x), index)
    replace_method(Clustering, 'train', replacement_train)


def replace_method(the_class, name, replacement, ignore_missing=False):
    try:
        orig_method = getattr(the_class, name)
    except AttributeError:
        if ignore_missing:
            return
        raise
    if orig_method.__name__ == 'replacement_' + name:
        # replacement was done in parent class
        return
    setattr(the_class, name + '_c', orig_method)
    setattr(the_class, name, replacement)

将clustering class中的train方法名称替换为replacement的名称。
replace_method方法中:

  • the_class:类名,文件中直接使用的类定义在swigfaiss.py中,即这里能调用的类都必须是在该文件中有定义的。
  • name: 函数名,即要重新替换的index的方法
  • replacement: 替换后的方法,这里传入方法的地址,在faiss.py中针对index的部分函数如train, add等定义了一套python下的方法,主要是添加了"replacement_"前缀。所以,在应用程序中调用上述方法时会运行到替换后的方法中来。如index.add --> replacement_add
  1. 替换 ProductQuantizer 和 ScalarQuantizer 的方法
handle_Quantizer(ProductQuantizer)
handle_Quantizer(ScalarQuantizer)

def handle_Quantizer(the_class):

    def replacement_train(self, x):
        n, d = x.shape
        assert d == self.d
        self.train_c(n, swig_ptr(x))

    def replacement_compute_codes(self, x):
        n, d = x.shape
        assert d == self.d
        codes = np.empty((n, self.code_size), dtype='uint8')
        self.compute_codes_c(swig_ptr(x), swig_ptr(codes), n)
        return codes

    def replacement_decode(self, codes):
        n, cs = codes.shape
        assert cs == self.code_size
        x = np.empty((n, self.d), dtype='float32')
        self.decode_c(swig_ptr(codes), swig_ptr(x), n)
        return x

    replace_method(the_class, 'train', replacement_train)
    replace_method(the_class, 'compute_codes', replacement_compute_codes)
    replace_method(the_class, 'decode', replacement_decode)

这两个类是量化器相关的,这里会替换三个函数: train, compute_codes, decode。

  1. 替换MatrixStats的初始化方法
def handle_MatrixStats(the_class):
    original_init = the_class.__init__

    def replacement_init(self, m):
        assert len(m.shape) == 2
        original_init(self, m.shape[0], m.shape[1], swig_ptr(m))

    the_class.__init__ = replacement_init

handle_MatrixStats(MatrixStats)

MatrixStats用于报告数据集相关的一些统计信息并对其进行注释。

  1. 替换已经导入的index的部分方法
this_module = sys.modules[__name__]

for symbol in dir(this_module):
    obj = getattr(this_module, symbol)
    # print symbol, isinstance(obj, (type, types.ClassType))
    if inspect.isclass(obj):
        the_class = obj
        if issubclass(the_class, Index):
            handle_Index(the_class)

        if issubclass(the_class, IndexBinary):
            handle_IndexBinary(the_class)

        if issubclass(the_class, VectorTransform):
            handle_VectorTransform(the_class)

        if issubclass(the_class, AutoTuneCriterion):
            handle_AutoTuneCriterion(the_class)

        if issubclass(the_class, ParameterSpace):
            handle_ParameterSpace(the_class)

首先sys.modules返回所有已经导入的faiss相关的module,然后根据symbol名称判断是否为class,如果是,则根据该class的父类类型依次替换部分方法,具体的替换过程在handle_xxx中实现。

  1. 对类或方法添加参数引用
add_ref_in_constructor(IndexIVFFlat, 0)
add_ref_in_constructor(IndexIVFFlatDedup, 0)
add_ref_in_constructor(IndexPreTransform, {2: [0, 1], 1: [0]})
add_ref_in_method(IndexPreTransform, 'prepend_transform', 0)
add_ref_in_constructor(IndexIVFPQ, 0)
add_ref_in_constructor(IndexIVFPQR, 0)
add_ref_in_constructor(Index2Layer, 0)
add_ref_in_constructor(Level1Quantizer, 0)
add_ref_in_constructor(IndexIVFScalarQuantizer, 0)
add_ref_in_constructor(IndexIDMap, 0)
add_ref_in_constructor(IndexIDMap2, 0)
add_ref_in_constructor(IndexHNSW, 0)
add_ref_in_method(IndexShards, 'add_shard', 0)
add_ref_in_method(IndexBinaryShards, 'add_shard', 0)
add_ref_in_constructor(IndexRefineFlat, 0)
add_ref_in_constructor(IndexBinaryIVF, 0)
add_ref_in_constructor(IndexBinaryFromFloat, 0)
add_ref_in_constructor(IndexBinaryIDMap, 0)
add_ref_in_constructor(IndexBinaryIDMap2, 0)

add_ref_in_method(IndexReplicas, 'addIndex', 0)
add_ref_in_method(IndexBinaryReplicas, 'addIndex', 0)

# seems really marginal...
# remove_ref_from_method(IndexReplicas, 'removeIndex', 0)

if hasattr(this_module, 'GpuIndexFlat'):
    # handle all the GPUResources refs
    add_ref_in_function('index_cpu_to_gpu', 0)
    add_ref_in_constructor(GpuIndexFlat, 0)
    add_ref_in_constructor(GpuIndexFlatIP, 0)
    add_ref_in_constructor(GpuIndexFlatL2, 0)
    add_ref_in_constructor(GpuIndexIVFFlat, 0)
    add_ref_in_constructor(GpuIndexIVFScalarQuantizer, 0)
    add_ref_in_constructor(GpuIndexIVFPQ, 0)
    add_ref_in_constructor(GpuIndexBinaryFlat, 0)
  • add_ref_in_constructor():对arg1的类添加arg2的参数引用
  • add_ref_in_method():对arg1类中arg2的方法添加arg3的参数引用
  • add_ref_in_function():对arg1的函数添加arg2的参数引用,其中arg1的函数是独立于类的函数。
  1. 替换MapLong2Long的方法
replace_method(MapLong2Long, 'add', replacement_map_add)
replace_method(MapLong2Long, 'search_multiple', replacement_map_search_multiple)

2.3 定义的方法接口

faiss.py除了在import 时会自动执行上述操作,在文件中还定义了一些独立的函数供用户应用程序调用。

1. GPU相关操作

index_cpu_to_gpu_multiple_py()
此方法的作用是将index从CPU中拷贝到所有的GPU中,为GPU索引和资源构建c++向量。

方法调用了swigfaiss.py的index_cpu_to_gpu_multiple(),该函数拷贝操作的具体实现,初始定义在gpu/GpuCloner.cpp中。

def index_cpu_to_gpu_multiple_py(resources, index, co=None):
    """builds the C++ vectors for the GPU indices and the
    resources. Handles the common case where the resources are assigned to
    the first len(resources) GPUs"""
    vres = GpuResourcesVector()
    vdev = IntVector()
    for i, res in enumerate(resources):
        vdev.push_back(i)
        vres.push_back(res)
    index = index_cpu_to_gpu_multiple(vres, vdev, index, co)
    index.referenced_objects = resources
    return index
  • resources:资源列表
  • index: CPU中索引实例
  • return index: GPU中的索引实例

index_cpu_to_all_gpus()
此方法的作用与同样是拷贝index,但是接口更简单,更易于应用程序调用,其内部通过调用index_cpu_to_gpu_multiple_py实现。

def index_cpu_to_all_gpus(index, co=None, ngpu=-1):
    if ngpu == -1:
        ngpu = get_num_gpus()
    res = [StandardGpuResources() for i in range(ngpu)]
    index2 = index_cpu_to_gpu_multiple_py(res, index, co)
    return index2
  • index: 要拷贝的CPU 索引实例
  • co: 拷贝的参数,默认为0
  • ngpu: gpu个数,默认为-1,此时由系统判断

  • index2: 拷贝后GPU中的索引实例

2. numpy数组和std::vector转换操作

vector_to_array()
将c++的std::vector容器转换成np的数组

def vector_to_array(v):
    """ convert a C++ vector to a numpy array """
    classname = v.__class__.__name__
    assert classname.endswith('Vector')
    dtype = np.dtype(vector_name_map[classname[:-6]])
    a = np.empty(v.size(), dtype=dtype)
    if v.size() > 0:
        memcpy(swig_ptr(a), v.data(), a.nbytes)
    return a

# 其中vector_name_map表示了std::vector和python中数据类型的映射:
vector_name_map = {
    'Float': 'float32',
    'Byte': 'uint8',
    'Char': 'int8',
    'Uint64': 'uint64',
    'Long': 'int64',
    'Int': 'int32',
    'Double': 'float64'
    }
  • v: 要转换的c++类型的数组
  • a: 转换后的numpy风格的列表

vector_float_to_array()
同vector_to_array()

def vector_float_to_array(v):
    return vector_to_array(v)

copy_array_to_vector()
将numpy的列表拷贝到c++的容器中

def copy_array_to_vector(a, v):
    """ copy a numpy array to a vector """
    n, = a.shape
    classname = v.__class__.__name__
    assert classname.endswith('Vector')
    dtype = np.dtype(vector_name_map[classname[:-6]])
    assert dtype == a.dtype, (
        'cannot copy a %s array to a %s (should be %s)' % (
            a.dtype, classname, dtype))
    v.resize(n)
    if n > 0:
        memcpy(v.data(), swig_ptr(a), a.nbytes)

3. 数据操作相关的接口

kmin(array, k)
返回数组array中每一行的k个最小值

  • array: 要搜索的二维数组
  • k: 要查找的最小值的个数

返回值:

  • D : 返回<m, k>的二维数组,类型为int
  • I : 返回<m, k>的二维数组,类型为float32

kmax(array, k)
返回数组array中每一行的k个最大值,其他同kmin()

pairwise_distances(xq, xb, mt=METRIC_L2, metric_arg=0)
计算两组向量之间的整个成对距离矩阵

  • xq: 第一个二维向量
  • xb: 第二个二维向量
  • mt: 计算的距离类型,默认为L2
  • metric_arg: 距离类型参数

返回值,dis: 一个<m1, m2>的二维向量,其中m1为xq的行数,m2为xb的行数

rand/randint/randn

  • rand: 产生浮点型的随机数
  • randint: 产生int64型随机数
  • randn: 从正态分布中产生folat32型随机数

eval_intersection(I1, I2)
两个结果表的每行之间相交的大小


normalize_L2(x)

def normalize_L2(x):
    fvec_renorm_L2(x.shape[1], x.shape[0], swig_ptr(x))

4. kmeans类

class Kmeans:
    """shallow wrapper around the Clustering object. The important method is train()."""

    def __init__(self, d, k, **kwargs):
        """d: input dimension, k: nb of centroids. Additional
         parameters are passed on the ClusteringParameters object,
         including niter=25, verbose=False, spherical = False
        """
        self.d = d
        self.k = k
        self.gpu = False
        self.cp = ClusteringParameters()
        for k, v in kwargs.items():
            if k == 'gpu':
                self.gpu = v
            else:
                # if this raises an exception, it means that it is a non-existent field
                getattr(self.cp, k)
                setattr(self.cp, k, v)
        self.centroids = None

    def train(self, x):
        n, d = x.shape
        assert d == self.d
        clus = Clustering(d, self.k, self.cp)
        if self.cp.spherical:
            self.index = IndexFlatIP(d)
        else:
            self.index = IndexFlatL2(d)
        if self.gpu:
            if self.gpu == True:
                ngpu = -1
            else:
                ngpu = self.gpu
            self.index = index_cpu_to_all_gpus(self.index, ngpu=ngpu)
        clus.train(x, self.index)
        centroids = vector_float_to_array(clus.centroids)
        self.centroids = centroids.reshape(self.k, d)
        self.obj = vector_float_to_array(clus.obj)
        return self.obj[-1] if self.obj.size > 0 else 0.0

    def assign(self, x):
        assert self.centroids is not None, "should train before assigning"
        self.index.reset()
        self.index.add(self.centroids)
        D, I = self.index.search(x, 1)
        return D.ravel(), I.ravel()

5. 索引与数组的转换

serialize_index()
将索引转换成uint8型数组

def serialize_index(index):
    """ convert an index to a numpy uint8 array  """
    writer = VectorIOWriter()
    write_index(index, writer)
    return vector_to_array(writer.data)

deserialize_index()
使用数组生成索引

def deserialize_index(data):
    reader = VectorIOReader()
    copy_array_to_vector(data, reader.data)
    return read_index(reader)

3. 总结

  1. 导入faiss.py后可以直接生成各类Index的实例,其中参数与c++中初始定义的相同,但是并不是所有的函数都可以直接调用,取决于该文件中是否替换该类型的index。
  2. faiss.py本身与c++ core不关联,它通过swigfaiss而形成完整的index的操作;
  3. swigfaiss.py是由SWIG将C++编写的软件嵌入联接成的python语言的接口模块。要继续分析修改c++代码(添加函数和功能)如何反应在python应用程序中,还需要研究SWIG软件的编译规则,并修改之。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

翔底

您的鼓励将是我创作最大的动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值