mirror of
https://github.com/facebookresearch/faiss.git
synced 2026-10-12 02:13:36 +00:00
Synchronization with FB version 2017-06-21
* moved most FAISS_ASSERT calls to C++ exceptions, and adjusted memory allocation to avoid mem leaks * added an IndexIVFScalarQuantizer type that offers an intermediate compression between IVFFlat and IVFPQ * support removal of indices in IndexIDMap / IndexFlat combination * various fixes in GPU code
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the CC-by-NC license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
// Copyright 2004-present Facebook. All Rights Reserved
|
||||
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
||||
#include <sys/time.h>
|
||||
|
||||
|
||||
#include <faiss/IndexPQ.h>
|
||||
#include <faiss/IndexIVFPQ.h>
|
||||
#include <faiss/IndexFlat.h>
|
||||
#include <faiss/index_io.h>
|
||||
|
||||
double elapsed ()
|
||||
{
|
||||
struct timeval tv;
|
||||
gettimeofday (&tv, nullptr);
|
||||
return tv.tv_sec + tv.tv_usec * 1e-6;
|
||||
}
|
||||
|
||||
|
||||
int main ()
|
||||
{
|
||||
double t0 = elapsed();
|
||||
|
||||
// dimension of the vectors to index
|
||||
int d = 64;
|
||||
|
||||
// size of the database we plan to index
|
||||
size_t nb = 1000 * 1000;
|
||||
size_t add_bs = 10000; // # size of the blocks to add
|
||||
|
||||
// make a set of nt training vectors in the unit cube
|
||||
// (could be the database)
|
||||
size_t nt = 100 * 1000;
|
||||
|
||||
//---------------------------------------------------------------
|
||||
// Define the core quantizer
|
||||
// We choose a multiple inverted index for faster training with less data
|
||||
// and because it usually offers best accuracy/speed trade-offs
|
||||
//
|
||||
// We here assume that its lifespan of this coarse quantizer will cover the
|
||||
// lifespan of the inverted-file quantizer IndexIVFFlat below
|
||||
// With dynamic allocation, one may give the responsability to free the
|
||||
// quantizer to the inverted-file index (with attribute do_delete_quantizer)
|
||||
//
|
||||
// Note: a regular clustering algorithm would be defined as:
|
||||
// faiss::IndexFlatL2 coarse_quantizer (d);
|
||||
//
|
||||
// Use nhash=2 subquantizers used to define the product coarse quantizer
|
||||
// Number of bits: we will have 2^nbits_coarse centroids per subquantizer
|
||||
// meaning (2^12)^nhash distinct inverted lists
|
||||
//
|
||||
// The parameter bytes_per_code is determined by the memory
|
||||
// constraint, the dataset will use nb * (bytes_per_code + 8)
|
||||
// bytes.
|
||||
//
|
||||
// The parameter nbits_subq is determined by the size of the dataset to index.
|
||||
//
|
||||
size_t nhash = 2;
|
||||
size_t nbits_subq = 9;
|
||||
size_t ncentroids = 1 << (nhash * nbits_subq); // total # of centroids
|
||||
int bytes_per_code = 16;
|
||||
|
||||
faiss::MultiIndexQuantizer coarse_quantizer (d, nhash, nbits_subq);
|
||||
|
||||
printf ("IMI (%ld,%ld): %ld virtual centroids (target: %ld base vectors)",
|
||||
nhash, nbits_subq, ncentroids, nb);
|
||||
|
||||
// the coarse quantizer should not be dealloced before the index
|
||||
// 4 = nb of bytes per code (d must be a multiple of this)
|
||||
// 8 = nb of bits per sub-code (almost always 8)
|
||||
faiss::MetricType metric = faiss::METRIC_L2; // can be METRIC_INNER_PRODUCT
|
||||
faiss::IndexIVFPQ index (&coarse_quantizer, d, ncentroids, bytes_per_code, 8);
|
||||
index.quantizer_trains_alone = true;
|
||||
|
||||
// define the number of probes. 2048 is for high-dim, overkill in practice
|
||||
// Use 4-1024 depending on the trade-off speed accuracy that you want
|
||||
index.nprobe = 2048;
|
||||
|
||||
|
||||
{ // training.
|
||||
|
||||
// The distribution of the training vectors should be the same
|
||||
// as the database vectors. It could be a sub-sample of the
|
||||
// database vectors, if sampling is not biased. Here we just
|
||||
// randomly generate the vectors.
|
||||
|
||||
printf ("[%.3f s] Generating %ld vectors in %dD for training\n",
|
||||
elapsed() - t0, nt, d);
|
||||
|
||||
std::vector <float> trainvecs (nt * d);
|
||||
for (size_t i = 0; i < nt; i++) {
|
||||
for (size_t j = 0; j < d; j++) {
|
||||
trainvecs[i * d + j] = drand48();
|
||||
}
|
||||
}
|
||||
|
||||
printf ("[%.3f s] Training the index\n", elapsed() - t0);
|
||||
index.verbose = true;
|
||||
index.train (nt, trainvecs.data());
|
||||
}
|
||||
|
||||
// the index can be re-loaded later with
|
||||
// faiss::Index * idx = faiss::read_index("/tmp/trained_index.faissindex");
|
||||
faiss::write_index(&index, "/tmp/trained_index.faissindex");
|
||||
|
||||
size_t nq;
|
||||
std::vector<float> queries;
|
||||
|
||||
{ // populating the database
|
||||
printf ("[%.3f s] Building a dataset of %ld vectors to index\n",
|
||||
elapsed() - t0, nb);
|
||||
|
||||
std::vector <float> database (nb * d);
|
||||
std::vector <long> ids (nb);
|
||||
for (size_t i = 0; i < nb; i++) {
|
||||
for (size_t j = 0; j < d; j++) {
|
||||
database[i * d + j] = drand48();
|
||||
}
|
||||
ids[i] = 8760000000L + i;
|
||||
}
|
||||
|
||||
printf ("[%.3f s] Adding the vectors to the index\n", elapsed() - t0);
|
||||
|
||||
for (size_t begin = 0; begin < nb; begin += add_bs) {
|
||||
size_t end = std::min (begin + add_bs, nb);
|
||||
index.add_with_ids (end - begin,
|
||||
database.data() + d * begin,
|
||||
ids.data() + begin);
|
||||
}
|
||||
|
||||
// remember a few elements from the database as queries
|
||||
int i0 = 1234;
|
||||
int i1 = 1244;
|
||||
|
||||
nq = i1 - i0;
|
||||
queries.resize (nq * d);
|
||||
for (int i = i0; i < i1; i++) {
|
||||
for (int j = 0; j < d; j++) {
|
||||
queries [(i - i0) * d + j] = database [i * d + j];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A few notes on the internal format of the index:
|
||||
//
|
||||
// - the positing lists for PQ codes are index.codes, which is a
|
||||
// std::vector < std::vector<uint8_t> >
|
||||
// if n is the length of posting list #i, codes[i] has length bytes_per_code * n
|
||||
//
|
||||
// - the corresponding ids are stored in index.ids
|
||||
//
|
||||
// - given a vector float *x, finding which k centroids are
|
||||
// closest to it (ie to find the nearest neighbors) can be done with
|
||||
//
|
||||
// long *centroid_ids = new long[k];
|
||||
// float *distances = new float[k];
|
||||
// index.quantizer->search (1, x, k, dis, centroids_ids);
|
||||
//
|
||||
|
||||
faiss::write_index(&index, "/tmp/populated_index.faissindex");
|
||||
|
||||
{ // searching the database
|
||||
int k = 5;
|
||||
printf ("[%.3f s] Searching the %d nearest neighbors "
|
||||
"of %ld vectors in the index\n",
|
||||
elapsed() - t0, k, nq);
|
||||
|
||||
std::vector<faiss::Index::idx_t> nns (k * nq);
|
||||
std::vector<float> dis (k * nq);
|
||||
|
||||
index.search (nq, queries.data(), k, dis.data(), nns.data());
|
||||
|
||||
printf ("[%.3f s] Query results (vector ids, then distances):\n",
|
||||
elapsed() - t0);
|
||||
|
||||
for (int i = 0; i < nq; i++) {
|
||||
printf ("query %2d: ", i);
|
||||
for (int j = 0; j < k; j++) {
|
||||
printf ("%7ld ", nns[j + i * k]);
|
||||
}
|
||||
printf ("\n dis: ");
|
||||
for (int j = 0; j < k; j++) {
|
||||
printf ("%7g ", dis[j + i * k]);
|
||||
}
|
||||
printf ("\n");
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
# Copyright (c) 2015-present, Facebook, Inc.
|
||||
# All rights reserved.
|
||||
#
|
||||
# This source code is licensed under the CC-by-NC license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
#! /usr/bin/env python2
|
||||
|
||||
import numpy as np
|
||||
|
||||
import faiss
|
||||
import unittest
|
||||
|
||||
class TestClustering(unittest.TestCase):
|
||||
|
||||
def test_clustering(self):
|
||||
d = 64
|
||||
n = 1000
|
||||
np.random.seed(123)
|
||||
x = np.random.random(size=(n, d)).astype('float32')
|
||||
|
||||
km = faiss.Kmeans(d, 32, niter=10)
|
||||
err32 = km.train(x)
|
||||
|
||||
# check that objective is decreasing
|
||||
prev = 1e50
|
||||
for o in km.obj:
|
||||
self.assertGreater(prev, o)
|
||||
prev = o
|
||||
|
||||
km = faiss.Kmeans(d, 64, niter=10)
|
||||
err64 = km.train(x)
|
||||
|
||||
# check that 64 centroids give a lower quantization error than 32
|
||||
self.assertGreater(err32, err64)
|
||||
|
||||
|
||||
class TestPCA(unittest.TestCase):
|
||||
|
||||
def test_pca(self):
|
||||
d = 64
|
||||
n = 1000
|
||||
np.random.seed(123)
|
||||
x = np.random.random(size=(n, d)).astype('float32')
|
||||
|
||||
pca = faiss.PCAMatrix(d, 10)
|
||||
pca.train(x)
|
||||
y = pca.apply_py(x)
|
||||
|
||||
# check that energy per component is decreasing
|
||||
column_norm2 = (y**2).sum(0)
|
||||
|
||||
prev = 1e50
|
||||
for o in column_norm2:
|
||||
self.assertGreater(prev, o)
|
||||
prev = o
|
||||
|
||||
|
||||
class TestProductQuantizer(unittest.TestCase):
|
||||
|
||||
def test_pq(self):
|
||||
d = 64
|
||||
n = 1000
|
||||
cs = 4
|
||||
np.random.seed(123)
|
||||
x = np.random.random(size=(n, d)).astype('float32')
|
||||
pq = faiss.ProductQuantizer(d, cs, 8)
|
||||
pq.train(x)
|
||||
codes = pq.compute_codes(x)
|
||||
x2 = pq.decode(codes)
|
||||
diff = ((x - x2)**2).sum()
|
||||
|
||||
# print "diff=", diff
|
||||
# diff= 1807.98
|
||||
self.assertGreater(2500, diff)
|
||||
|
||||
|
||||
|
||||
class TestRevSwigPtr(unittest.TestCase):
|
||||
|
||||
def test_rev_swig_ptr(self):
|
||||
|
||||
index = faiss.IndexFlatL2(4)
|
||||
xb0 = np.vstack([
|
||||
i * 10 + np.array([1, 2, 3, 4], dtype='float32')
|
||||
for i in range(5)])
|
||||
index.add(xb0)
|
||||
xb = faiss.rev_swig_ptr(index.xb.data(), 4 * 5).reshape(5, 4)
|
||||
self.assertEqual(np.abs(xb0 - xb).sum(), 0)
|
||||
|
||||
|
||||
class TestException(unittest.TestCase):
|
||||
|
||||
def test_exception(self):
|
||||
|
||||
index = faiss.IndexFlatL2(10)
|
||||
|
||||
a = np.zeros((5, 10), dtype='float32')
|
||||
b = np.zeros(5, dtype='int64')
|
||||
|
||||
try:
|
||||
# an unsupported operation for IndexFlat
|
||||
index.add_with_ids(a, b)
|
||||
except RuntimeError, e:
|
||||
assert 'add_with_ids not implemented' in str(e)
|
||||
else:
|
||||
assert False, 'exception did not fire???'
|
||||
|
||||
def test_exception_2(self):
|
||||
|
||||
try:
|
||||
faiss.index_factory(12, 'IVF256,Flat,PQ8')
|
||||
except RuntimeError, e:
|
||||
assert 'could not parse' in str(e)
|
||||
else:
|
||||
assert False, 'exception did not fire???'
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,146 @@
|
||||
# Copyright (c) 2015-present, Facebook, Inc.
|
||||
# All rights reserved.
|
||||
#
|
||||
# This source code is licensed under the CC-by-NC license found in the
|
||||
# LICENSE file in the root directory of this source tree.
|
||||
|
||||
#! /usr/bin/env python2
|
||||
|
||||
"""this is a basic test script that works with fbmake to check if
|
||||
some simple indices work"""
|
||||
|
||||
import sys
|
||||
import numpy as np
|
||||
import unittest
|
||||
import faiss
|
||||
|
||||
|
||||
class EvalIVFPQAccuracy(unittest.TestCase):
|
||||
|
||||
def get_dataset(self):
|
||||
d = 64
|
||||
nb = 1000
|
||||
nt = 1500
|
||||
nq = 200
|
||||
np.random.seed(123)
|
||||
xb = np.random.random(size=(nb, d)).astype('float32')
|
||||
xt = np.random.random(size=(nt, d)).astype('float32')
|
||||
xq = np.random.random(size=(nq, d)).astype('float32')
|
||||
|
||||
return (xt, xb, xq)
|
||||
|
||||
def test_IndexIVFPQ(self):
|
||||
(xt, xb, xq) = self.get_dataset()
|
||||
d = xt.shape[1]
|
||||
|
||||
gt_index = faiss.IndexFlatL2(d)
|
||||
gt_index.add(xb)
|
||||
D, gt_nns = gt_index.search(xq, 1)
|
||||
|
||||
coarse_quantizer = faiss.IndexFlatL2(d)
|
||||
index = faiss.IndexIVFPQ(coarse_quantizer, d, 25, 16, 8)
|
||||
index.train(xt)
|
||||
index.add(xb)
|
||||
index.nprobe = 5
|
||||
D, nns = index.search(xq, 10)
|
||||
n_ok = (nns == gt_nns).sum()
|
||||
nq = xq.shape[0]
|
||||
|
||||
self.assertGreater(n_ok, nq * 0.4)
|
||||
|
||||
|
||||
class TestMultiIndexQuantizer(unittest.TestCase):
|
||||
|
||||
def test_search_k1(self):
|
||||
|
||||
# verify codepath for k = 1 and k > 1
|
||||
|
||||
d = 64
|
||||
nt = 1500
|
||||
nq = 200
|
||||
np.random.seed(123)
|
||||
|
||||
xt = np.random.random(size=(nt, d)).astype('float32')
|
||||
xq = np.random.random(size=(nq, d)).astype('float32')
|
||||
|
||||
miq = faiss.MultiIndexQuantizer(d, 2, 6)
|
||||
|
||||
miq.train(xt)
|
||||
|
||||
D1, I1 = miq.search(xq, 1)
|
||||
|
||||
D5, I5 = miq.search(xq, 5)
|
||||
|
||||
self.assertEqual(np.abs(I1[:, :1] - I5[:, :1]).max(), 0)
|
||||
self.assertEqual(np.abs(D1[:, :1] - D5[:, :1]).max(), 0)
|
||||
|
||||
|
||||
class TestIVFScalarQuantizer(unittest.TestCase):
|
||||
|
||||
def test_4variants(self):
|
||||
d = 32
|
||||
nt = 1500
|
||||
nq = 200
|
||||
nb = 10000
|
||||
|
||||
np.random.seed(123)
|
||||
|
||||
xt = np.random.random(size=(nt, d)).astype('float32')
|
||||
xq = np.random.random(size=(nq, d)).astype('float32')
|
||||
xb = np.random.random(size=(nb, d)).astype('float32')
|
||||
|
||||
# common quantizer
|
||||
quantizer = faiss.IndexFlatL2(d)
|
||||
|
||||
ncent = 128
|
||||
|
||||
index_gt = faiss.IndexFlatL2(d)
|
||||
index_gt.add(xb)
|
||||
D, I_ref = index_gt.search(xq, 10)
|
||||
|
||||
nok = {}
|
||||
|
||||
index = faiss.IndexIVFFlat(quantizer, d, ncent,
|
||||
faiss.METRIC_L2)
|
||||
index.nprobe = 4
|
||||
index.train(xt)
|
||||
index.add(xb)
|
||||
D, I = index.search(xq, 10)
|
||||
nok['flat'] = (I[:, 0] == I_ref[:, 0]).sum()
|
||||
|
||||
for qname in "QT_4bit QT_4bit_uniform QT_8bit QT_8bit_uniform".split():
|
||||
qtype = getattr(faiss.ScalarQuantizer, qname)
|
||||
index = faiss.IndexIVFScalarQuantizer(quantizer, d, ncent,
|
||||
qtype, faiss.METRIC_L2)
|
||||
|
||||
index.nprobe = 4
|
||||
index.train(xt)
|
||||
index.add(xb)
|
||||
D, I = index.search(xq, 10)
|
||||
|
||||
nok[qname] = (I[:, 0] == I_ref[:, 0]).sum()
|
||||
|
||||
print(nok)
|
||||
|
||||
self.assertGreaterEqual(nok['flat'], nok['QT_8bit'])
|
||||
self.assertGreaterEqual(nok['QT_8bit'], nok['QT_4bit'])
|
||||
self.assertGreaterEqual(nok['QT_8bit'], nok['QT_8bit_uniform'])
|
||||
self.assertGreaterEqual(nok['QT_4bit'], nok['QT_4bit_uniform'])
|
||||
|
||||
|
||||
class TestRemove(unittest.TestCase):
|
||||
|
||||
def test_remove(self):
|
||||
# only tests the python interface
|
||||
|
||||
index = faiss.IndexFlat(5)
|
||||
xb = np.zeros((10, 5), dtype='float32')
|
||||
xb[:, 0] = np.arange(10) + 1000
|
||||
index.add(xb)
|
||||
index.remove_ids(np.arange(5) * 2)
|
||||
xb2 = faiss.vector_float_to_array(index.xb).reshape(5, 5)
|
||||
assert np.all(xb2[:, 0] == xb[np.arange(5) * 2 + 1, 0])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
@@ -0,0 +1,64 @@
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the CC-by-NC license found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <faiss/IndexIVFPQ.h>
|
||||
#include <faiss/IndexFlat.h>
|
||||
#include <faiss/utils.h>
|
||||
|
||||
|
||||
// dimension of the vectors to index
|
||||
int d = 64;
|
||||
|
||||
// size of the database we plan to index
|
||||
size_t nb = 8000;
|
||||
|
||||
|
||||
double eval_codec_error (long ncentroids, long m, const std::vector<float> &v)
|
||||
{
|
||||
faiss::IndexFlatL2 coarse_quantizer (d);
|
||||
faiss::IndexIVFPQ index (&coarse_quantizer, d,
|
||||
ncentroids, m, 8);
|
||||
index.pq.cp.niter = 10; // speed up train
|
||||
index.train (nb, v.data());
|
||||
|
||||
// encode and decode to compute reconstruction error
|
||||
|
||||
std::vector<long> keys (nb);
|
||||
std::vector<uint8_t> codes (nb * m);
|
||||
index.encode_multiple (nb, keys.data(), v.data(), codes.data(), true);
|
||||
|
||||
std::vector<float> v2 (nb * d);
|
||||
index.decode_multiple (nb, keys.data(), codes.data(), v2.data());
|
||||
|
||||
return faiss::fvec_L2sqr (v.data(), v2.data(), nb * d);
|
||||
}
|
||||
|
||||
|
||||
|
||||
TEST(IVFPQ, codec) {
|
||||
|
||||
std::vector <float> database (nb * d);
|
||||
for (size_t i = 0; i < nb * d; i++) {
|
||||
database[i] = drand48();
|
||||
}
|
||||
|
||||
double err0 = eval_codec_error(16, 8, database);
|
||||
|
||||
// should be more accurate as there are more coarse centroids
|
||||
double err1 = eval_codec_error(128, 8, database);
|
||||
EXPECT_GT(err0, err1);
|
||||
|
||||
// should be more accurate as there are more PQ codes
|
||||
double err2 = eval_codec_error(16, 16, database);
|
||||
EXPECT_GT(err0, err2);
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
/**
|
||||
* Copyright (c) 2015-present, Facebook, Inc.
|
||||
* All rights reserved.
|
||||
|
||||
Reference in New Issue
Block a user