Faiss 基础构建块
Faiss 的核心是几个高效实现的基础算法,包括:k-means 聚类(K均值)、PCA(主成分分析)、PQ(乘积量化)编码与解码。
聚类(Clustering)
Faiss 提供了高效的 K-means 聚类实现。要对存储在二维张量 x 中的一组向量进行聚类,可以这样操作:
ncentroids = 1024 # 聚类中心数量
niter = 20 # 迭代次数
verbose = True # 是否打印详细日志
d = x.shape[1] # 向量维度
kmeans = faiss.Kmeans(d, ncentroids, niter=niter, verbose=verbose)
kmeans.train(x)
聚类训练后得到的**质心(centroid)**存储在 kmeans.centroids 中。
聚类目标函数的值(对于 K-means 是总的平方误差)在每次迭代的结果会保存在 kmeans.obj,更详细的统计信息放在 kmeans.iteration_stats。
若希望在 GPU 上运行,只需在 Kmeans 构造函数中加入 gpu=True,会自动使用所有可用的 GPU。
x 应为浮点型二维数组(如 numpy 的 float32 类型),每行是一个待聚类的样本向量。
其它配置选项
Kmeans 对象主要是 C++ Clustering 对象的一个封装。你可以通过构造函数设置以下参数:
nredo:聚类训练多次,保留表现最好的质心(按目标函数挑选)verbose:是否显示更详细的日志spherical:是否执行球形 K-means(每次迭代都将质心做 L2 归一化)int_centroids:将质心参数四舍五入为整数update_index:每次迭代后都重新训练索引吗?min_points_per_centroid/max_points_per_centroid:如果低于这个数会有警告;高于则对训练集进行子采样seed:设置随机数生成器的种子
向量归属(Assignment)
聚类完成后,想知道输入向量 x 各自最接近的质心,可以这样操作:
D, I = kmeans.index.search(x, 1)
这里,I 返回每个输入向量最近的质心编号;D 则是对应的 L2 距离的平方。
找到离质心最近的若干原始点
如果你想查找给定质心在原始数据中最近的 15 个点:
index = faiss.IndexFlatL2(d)
index.add(x)
D, I = index.search(kmeans.centroids, 15)
此时,I 的形状为 (质心数, 15),每行是距离对应质心最近的 15 个原始样本的下标。
在 GPU 上进行聚类
你可以通过 gpu=True(全部 GPU)或如 gpu=3(3 块 GPU)等方式,在多 GPU 上进行 K-means 聚类。只需在 Kmeans 构造函数中配置即可。
GPU 聚类适用于数据量较大或对性能有特别需求的场景。
计算主成分分析(PCA)
假设我们想把 40 维的向量降到 10 维,可以这样操作:
# 随机训练样本
mt = np.random.rand(1000, 40).astype('float32')
mat = faiss.PCAMatrix(40, 10)
mat.train(mt)
assert mat.is_trained
tr = mat.apply(mt)
# 可以打印 tr 的每一列平方和,观察主成分的能量逐步减少
print((tr ** 2).sum(0))
PCA 可以显著降低高维特征的存储和计算成本,常用于数据预处理阶段。
量化器(Quantizers)
量化器对象继承自 Quantizer,通常有以下常用方法(详见 impl/Quantizer.h):
train:在训练样本(二位矩阵)上训练量化器compute_codes和decode:分别是编码器和解码器。编码通常是有损的,会为每个输入向量生成一个uint8(无符号8位) 类型的代码矩阵。get_DistanceComputer:返回一个DistanceComputer对象(见下文)。
量化器对象的状态即为训练产生的码本(codebook)或归一化系数。其 code_size 字段表示每个向量编码后占用的字节数。
量化器的类型
Faiss 支持的量化器主要有:
ScalarQuantizer:对每个向量分量(维度)进行独立线性范围的量化ProductQuantizer:对子向量做矢量量化(将原始向量分为子块分开量化)AdditiveQuantizer:将向量编码为一组码本条目的和。详见Additive Quantizers。ResidualQuantizer、LocalSearchQuantizer、ProductAdditiveQuantizer等属于加法量化器的子类,不同训练方式。
每种量化器都有其对应的索引类型,用于存储量化后向量的集合。
| 量化器类 | 平坦索引类 | IVF 索引类 |
|---|---|---|
| ScalarQuantizer | IndexScalarQuantizer | IndexIVFScalarQuantizer |
| ProductQuantizer | IndexPQ | IndexIVFPQ |
| AdditiveQuantizer | IndexAdditiveQuantizer | IndexIVFAdditiveQuantizer |
| ResidualQuantizer | IndexResidualQuantizer | IndexIVFResidualQuantizer |
| LocalSearchQuantizer | IndexLocalSearchQuantizer | IndexIVFLocalSearchQuantizer |
除 ScalarQuantizer 外,所有量化索引还存在 FastScan 版本(快速扫描),详情参考Fast accumulation of PQ and AQ codes。
IVF(倒排文件索引,Inverted File Index)有助于处理大规模数据的检索任务。FastScan 版本在编码密集型应用中可进一步加速搜索。
距离计算器(Distance computers)
部分量化器提供了 DistanceComputer 对象。它主要用于在已编码的情况下,高效计算给定向量与大量压缩后代码之间的距离。这样通常可以通过预计算查找表大幅提升距离计算速度。
DistanceComputer 提供:
set_query:设置当前要比对的原始(未编码)向量distance_to_code:给定编码计算实际距离
示例:PQ 编码 / 解码
ProductQuantizer(乘积量化器)可以对向量进行编码和解码:
d = 32 # 向量维度
cs = 4 # 每个向量编码长度(单位:字节)
# 训练集
nt = 10000
xt = np.random.rand(nt, d).astype('float32')
# 待编码数据(也可以与训练集相同)
n = 20000
x = np.random.rand(n, d).astype('float32')
pq = faiss.ProductQuantizer(d, cs, 8)
pq.train(xt)
# 编码
codes = pq.compute_codes(x)
# 解码为原始数据的近似还原
x2 = pq.decode(codes)
# 计算重构误差
avg_relative_error = ((x - x2)**2).sum() / (x ** 2).sum()
ProductQuantizer 适用于大规模向量检索等需要压缩和加速场景。训练样本应能代表实际数据分布。
标量量化(Scalar Quantizer)用法类似:
d = 32 # 向量维度
# 训练集
nt = 10000
xt = np.random.rand(nt, d).astype('float32')
# 待编码数据(可以和训练集相同)
n = 20000
x = np.random.rand(n, d).astype('float32')
# QT_8bit 为每一维分配8比特;也可用 QT_4bit
sq = faiss.ScalarQuantizer(d, faiss.ScalarQuantizer.QT_8bit)
sq.train(xt)
# 编码
codes = sq.compute_codes(x)
# 解码
x2 = sq.decode(codes)
# 计算重构误差
avg_relative_error = ((x - x2)**2).sum() / (x ** 2).sum()
量化后的数据虽然大幅压缩了存储,但会有一定信息损失。实际应用中需关注重构误差及查询精度的权衡。
如需了解更多每个方法的参数和详细说明,请查阅 Faiss 官方文档和示例链接。