Sous le capot des bases de données vectorielles

Cet article est agrémenté d’un tutoriel pour implémenter une base de données vectorielles from scratch en Python.

Introduction

En 2023, les bases de données vectorielles suscitent un vif intérêt, en témoignent les statistiques de recherche de Google Trends visibles sur la figure 1. Ce type de base de données a un lien direct avec les Large Language Models (LLM), tels que ChatGPT, en permettant la "Retrieval Augmented Generation" (RAG) par exemple. Cette approche offre la possibilité d'exploiter la puissance d'un agent conversationnel en utilisant nos propres données, ouvrant ainsi à de nouveaux cas d’usages que vous avez déjà vus dans l’article "Construire son RAG".

Statistiques de recherche de Google Trends montrant l'engouement autour des base de données vectorielles.Figure 1 : Illustration Google Trends.

Mais alors, comment est implémentée une base de données vectorielles ? Dans cet article, nous allons examiner ce qui se cache sous le capot des bases de données vectorielles. Cet article est agrémenté d’un tutoriel pour implémenter une base de données vectorielles en Python débutée de zéro. Vous pourrez la reproduire chez vous !

Nous commencerons par présenter ce qu’est une base de données vectorielles et quelle est leur utilité. Nous présenterons ensuite une méthode permettant d’évaluer la qualité des résultats et les performances. Enfin, nous parlerons des contraintes de mémoire et comment il est possible de compresser les vecteurs stockés dans la base.

1 - Qu’est-ce qu’une base de données vectorielles ?

Les bases de données vectorielles existaient bien avant l’avènement des LLMs et du RAG les accompagnant. Elles sont notamment utilisées dans des tâches comme la recommandation de contenus, les moteurs de recherche d’image, de texte ou de musique. Ce format de données est rarement utilisé tel quel , il est souvent nécessaire de passer par une phase de vectorisation. Les vecteurs sont une structure de données qui permettent de représenter des objets dans l’espace et ainsi d’effectuer des calculs de distance, opération utile dans la recherche ou recommandation de contenu. Dans le cas d'une image, par exemple, la vectorisation peut être brute en convertissant l'objet en un vecteur représentant l'état des pixels, ou elle peut être plus complexe, généralement en utilisant un modèle de machine learning pour extraire un vecteur d'embedding (i.e., un résumé de l'information contenue dans l'objet dans un espace de dimension réduite). La figure 2 illustre la notion d’embedding. Les termes proches sémantiquement apparaissent groupés. De plus, le déplacement pour passer de “man” à “woman” est similaire à celui pour passer de “king” à “queen”, ce qui montre que la différence entre chaque terme est similaire.

Illustration de vecteurs représentant des mots : la distance entre "man" et "woman" est similaire à celle entre "king" et "queen"Figure 2 : Illustration de vecteurs représentant des mots.

Quelle que soit la méthode utilisée pour la vectorisation, ces vecteurs auront besoin d'une structure permettant leur stockage, leur indexation et de méthodes de requêtage pour pouvoir y accéder. Idéalement le plus précisément et le plus rapidement possible. Voici en figure 3 un exemple d’utilisation d’une base de données vectorielles : l’utilisation d’un smartphone pour identifier une musique qui vous plaît et dont vous souhaitez connaître le titre et l’artiste.

L'acquisition de l'audio est effectuée typiquement sur un smartphone, puis une application client vectorise l'audio, puis ce vecteur est envoyé vers un serveur où l'on recherche des vecteurs de chansons similaires dans une base de données. Enfin, la chanson identifiée est renvoyée vers l'application du smartphone.Figure 3 : Illustration d’un cas d’utilisation d’une base de données vectorielles.

Vous enregistrez avec votre smartphone un extrait d’une chanson qui est ensuite convertie en vecteur. L’application va ensuite utiliser ce vecteur comme requête dans une base de données, contenant presque toutes les chansons existantes. Ces dernières auront subi la même vectorisation avant d’être stockées dans la base. La base de données va chercher le “vecteur chanson” le plus similaire du vecteur de la chanson que vous avez enregistrée. Il s’agit ici d’effectuer une recherche du plus proche voisin (Wang, 2003). Le terme fréquemment utilisé est “KNN” pour K nearest neighbors. Ici K = 1. Dans le paragraphe précédent, la phrase “Le plus rapidement et le plus précisément possible” est peut-être ce qui caractérise le mieux une base de données vectorielles: un compromis entre la rapidité et la précision des réponses. On parlera de méthodes d’Approximate Nearest Neighbors (ANN) (Frederickson, 2017), par opposition à la recherche exacte K Nearest Neighbors (KNN). La suite de l’article vous permettra de comprendre pourquoi, et surtout comment mesurer cette approximation.

2 - Mesure de qualité d’un ANN

2 - a - Cadre de la mesure de qualité d’un ANN

Les méthodologies d’indexation et de requêtage qui seront évoquées dans cet article permettent d’obtenir une approximation des N vecteurs les plus proches de celui donné en requête. Le niveau d’approximation dépend des différents paramètres de chacune des méthodes (qui impactent aussi la vitesse d'exécution des requêtes). Ce niveau d'approximation est mesurable en considérant que le résultat d’un KNN exhaustif représente la ground truth (la vraie cible), soit le résultat attendu si l’on effectuait la mesure de similarité entre le vecteur en requête, et tous les autres vecteurs de la base de données (Aumüller, 2020).

Attention, cette méthode n’évalue pas la pertinence de la représentation vectorielle pour obtenir des images similaires sémantiquement (aucun embedding n’est extrait de l’image). L’objectif ici est de mesurer à quel degré un ANN se rapproche d’un KNN exhaustif (ou brute force).

Pour mesurer le niveau précision de notre base de données vectorielles utilisant une des méthodes ANN, il faut :

  1. Fixer un set de valeurs d’hyper-paramètres,
  2. Fixer une mesure de similarité,
  3. Calculer un rappel entre le résultat de l’ANN et le résultat du KNN.

Dans notre cas, le rappel sera la proportion des K plus proches voisins issus du KNN présents dans les résultats de l’ANN pour une même valeur de K.

2 - b - Evaluation sur Fashion MNIST

Nous avons fait l’exercice sur notre base de données recodée en python en utilisant le dataset Fashion MNIST de Zalando. Cette base de données est constituée de 70k images de prêt-à-porter en nuances de gris.

La première étape, détaillée sur la figure 4, aura pour but de calculer la ground truth de notre base d’évaluation. Pour chaque image de notre base d’évaluation, il est nécessaire de stocker la liste de ses K plus proches voisins, tels qu’obtenus en utilisant la méthode des KNNs. Enfin dans la seconde étape, la base d’image est chargée dans la base de données vectorielles et indexée selon la méthode que l’on souhaite évaluer.

Etape 1 : Les KNN sont calculés pour chaque image du dataset et constituent la vérité terrain.
Etape 2 : Le dataset complet est inséré dans la base de données vectorielles. Une image = un vecteur.Figure 4 : Indexation du dataset d’évaluation et calcul des KNN (ground truth).

La figure 5 présente la suite de l'évaluation. La première étape consiste en la récupération des K plus proches voisins de l’image en requête, en utilisant la méthode ANN de la base de données. La seconde étape permet d’obtenir la vérité terrain de cette même requête et de calculer le rappel entre les deux résultats. Sur la figure 5, nous observons que pour K=10, la base de données vectorielles, dans sa configuration actuelle, obtient un rappel de 60 %, car 6 des résultats qu’elle renvoie sont contenus dans les 10 plus proches voisins renvoyés par le KNN.

Etape 1 : on requête la base de données vectorielles avec un vecteur de notre dataset d'évaluation. Etape 2 : Récupération des vrais k plus proches voisins (KNN) calculés précédemment.  On compare ensuite les deux listes. Dans cet exemple, sur 10 voisins, 6 font partie des vrais plus proches voisins.  Le rappel@10 est de 6/10 : 60%Figure 5 : Calcul du rappel pour une image de la base d’évaluation.

En répétant l’opération pour tous les vecteurs du dataset, et pour un set d’hyper paramètres, on obtient un point de la courbe de la figure 6 ci-dessous (la moyenne des rappels@k sur tous les vecteurs du dataset). Ceci nous permet d’observer l’évolution des performances en termes de rapidité, en fonction des performances en termes de qualité, càd à quel point nous arrivons à approcher le vrai KNN en utilisant des méthodes d’approximation.

Lecture d’une courbe d’évaluation des performances d’un ANN.

Figure 6 : Lecture d’une courbe d’évaluation des performances d’un ANN.

3 - L’indexation

L'indexation est une composante essentielle pour accélérer la recherche. Elle consiste à créer des métadonnées qui permettent un accès rapide aux données, évitant ainsi une exploration complète des données pour trouver des correspondances. Une base de données relationnelle utilise généralement une clé primaire pour cela. Une analogie pour comprendre le principe peut être faite avec l’index d’un livre. Dans un livre, l'index est généralement situé à la fin. Il répertorie les termes importants, les noms propres, les concepts clés, et les pages auxquelles ils se réfèrent. Plutôt que de feuilleter tout le livre à la recherche d'une information spécifique, vous pouvez consulter l'index, trouver le terme recherché, et obtenir une référence directe à la page où cette information est présente ! Ainsi, à partir de nos vecteurs, différentes stratégies d'indexation sont mises en place pour optimiser la recherche des vecteurs, chacune présentant des compromis particuliers.

3 - a - Index plat ou recherche exhaustive

Il s’agit de la façon la plus simple, mais également la plus naïve, de créer un index. En effet, on calcule la distance entre un vecteur requête et absolument tous les vecteurs de la base, avec une distance L2 par exemple. L’avantage de cet index est qu’il est parfait, exhaustif. Le gros désavantage, évidemment, c’est que cette méthode n’est absolument pas scalable car elle croît linéairement avec le nombre d’éléments, et nécessite aussi une quantité de mémoire considérable.

3 - b - Indexation par clustering, ou inverted file index (IVF)

L'indexation par clustering est déjà beaucoup plus maline. Elle consiste à regrouper les vecteurs similaires en clusters homogènes et à attribuer à chaque vecteur un cluster auquel il appartient. Chaque cluster est représenté par un vecteur moyen, appelé centroïde. Étape par étape, cela donne :

1 - Regrouper les vecteurs dans un nombre de k clusters à définir. Chaque vecteur a donc un numéro de cluster et c’est de là que vient le terme “inverted file index” : nous construisons une table dont l’index est le numéro de cluster et dont les valeurs sont les vecteurs de ce cluster. L’algorithme utilisé est souvent K-means.

2 - Représenter chaque vecteur par le vecteur moyen de son cluster (appelé centroïde dans l’algorithme K-means). Dans le schéma clustering 1, on voit que chaque vecteur blanc (représentés par des croix blanches) est dans un cluster, représenté par son vecteur bleu (représentés par des croix bleues), le centroïde.

3 - Comparer le vecteur requête aux centroïdes uniquement. Dans la figure 7, le vecteur rouge est comparé aux 3 vecteurs bleus.

On voit que chaque vecteur blanc (représentés par des croix blanches) est dans un cluster, représenté par son vecteur bleu (représentés par des croix bleues), le centroïde.Figure 7 : Schéma clustering 1.

4 - Comparer le vecteur requête aux vecteurs situés dans les clusters restants, dont les centroïdes sont les plus proches. Ce nombre de clusters est le paramètre nprobes dans les implémentations de bases de données vectorielles, qui correspond au nombre de cellules de Voronoï définies par les K-Means. Dans la figure 8, nprobes=2.

On compare le vecteur requête aux vecteurs situés dans les clusters restants, dont les centroïdes sont les plus proches. Ce nombre de clusters est le paramètre nprobes dans les implémentations de bases de données vectorielles, qui correspond au nombre de cellules de Voronoï définies par les K-Means. Dans le schéma clustering 2, nprobes=2.Figure 8 : Schéma clustering 2.

5 - Récupérer les x vecteurs les plus proches dans ces clusters. Dans la figure 9, x=3.

On récupère les x vecteurs les plus proches dans ces clusters. Dans le schéma clustering 3, x=3.Figure 9 : Schéma clustering 3.

Voici un exemple réaliste de clustering sur des vecteurs (dataset d’images MNIST représenté en 2D grâce à une réduction de dimensions T-SNE), sur la figure 10 : Exemple de clustering sur le dataset MNIST (une cellule = un cluster, avec le centroïde en orange)Figure 10 : Exemple de clustering sur le dataset MNIST (une cellule = un cluster, avec le centroïde en orange).

Nous avons ainsi limité la recherche en choisissant d’emblée les clusters dont les centroïdes sont les plus proches, puis cherché à l’intérieur de ces clusters les vecteurs les plus proches. Cette méthode est finalement plus rapide qu’une recherche exhaustive, mais la qualité de la recherche est variable, car elle dépend de comment les clusters ont été créés. Par exemple, on peut très bien tomber sur un cas où le vecteur le plus proche de la requête n’est pas dans la cellule “filtrée” (quand le vecteur en question est proche du bord d’une cellule, par exemple). En outre, il n’est pas aisé de choisir les bons paramètres de cette méthode :

  • L’augmentation de K (nombre de clusters/cellules) peut augmenter la précision de la recherche, mais peut également la réduire si les données sont sur-segmentées et que nprobes est trop élevé.
  • Un nprobes élevé améliore la précision, mais ralentit la recherche
AvantagesInconvénients
- Plus rapide qu’une recherche exhaustive- Nécessite moins de mémoire pour la recherche- L’ajout ou la suppression de vecteurs peut nécessiter la réorganisation des clusters- Perte de précision - Paramètres difficiles à choisir

3 - c - Mesure de performances

La méthode vue dans la section “Mesure de qualité d’un ANN” va permettre d’effectuer des mesures de performance sur la méthode d’indexation par clustering que nous avons implémentée en Python.

Exemple de mesure de qualité et des performances de l’indexation par clusteringFigure 11 - Mesure de qualité et des performances de l’indexation par clustering.

Le graphique de gauche sur la figure 11 permet d’observer l’impact des paramètres “nombre de clusters” et “nombre de sondes (probes)” sur le rappel (la qualité de l’ANN). Plus le nombre de sondes est élevé, plus le rappel est bon. Il est aussi intéressant d’observer que le gain en rappel diminue entre chaque incrément du nombre de sondes. Le graphique à droite de cette même figure permet d’observer les performances de cet index en termes de nombre de requêtes et de rappel. Nous constatons ici le dilemme majeur des bases de données vectorielles : qualité vs performance. Ici le nombre de sondes le plus faible donne en moyenne les vitesses de requêtage les plus élevées. Inversement, plus le nombre de sondes est élevé, plus cette vitesse sera faible (mais avec un meilleur rappel). Enfin, le dernier graphique, sur le bas de la figure 11, permet d’observer l’effet du nombre de clusters sur ces mêmes métriques. Sur chaque nuage lié à une valeur du nombre de sondes, nous observons que le nombre de clusters impacte la vitesse et la qualité de manière non linéaire. Sur cet échantillon, le nombre optimal de clusters semble être entre 27 et 52 clusters (en bleu sur le graphique), pour un nombre de sondes fixé.

3 - d - Les autres méthodes d’indexation

Il existe beaucoup d’autres méthodes d’indexation, comme HNSW qui est une méthode d’indexation par graphe, l’indexation à base d’arbres (utilisée par ANNOY) ou les méthodes d’indexation par hachage comme LSH. Ce sont les plus complexes, mais aussi celles qui permettent d’obtenir les meilleures performances, en termes de rapidité et de précision.

4 - Product quantization

L’objectif de cette méthode va être de réduire l’empreinte mémoire de notre ensemble de vecteurs et d’augmenter la vitesse de requêtage. Au niveau matériel, cela implique de charger un grand nombre voir l’ensemble des vecteurs dans la RAM. Dans certains cas d’usage, ayant des contraintes de rapidité, on pourra même utiliser la VRAM des GPU. Ce type de ressources est coûteux et disponible en quantité limitée. Par exemple : une image peut être représentée par un vecteur d’embedding de 4 096 composantes. Une composante étant un nombre flottant stocké sur 32 bits, soit 4 octets. Un vecteur de ce type a une empreinte mémoire de 4 096 × 4 octets, soit 16k kilooctets. Et voici quelques ordres de grandeur d’empreinte mémoire pour ce type de vecteurs :

  • 1 million de vecteurs d’images : 16 gigaoctets
  • 10 millions de vecteurs d’images : 160 gigaoctets
  • 100 millions de vecteurs d’images : 1.6 téraoctet
  • etc.

Si l’on prend l’exemple de Spotify, leur catalogue contient environ 100 millions de chansons. On comprend alors aisément l’importance de la compression des vecteurs.

4 - a - Compression des vecteurs

Le principe de la product quantization est une extension de l’indexation par clustering, ou inverted file index (IVF) vue plus haut.

Le schéma de la figure 12 détaille le processus de quantization pour un vecteur de dimension d=4, d’un nombre de sections m=2, et d’un nombre de clusters (par sous espace) = 4. On obtient bien un nouveau vecteur de dimension m=2, ou chaque composante est maintenant l’identifiant du cluster de chaque sous-espace. Nous sommes passés d’un vecteur représenté par 4 nombres flottants à un vecteur représenté par 2 entiers (entiers très petits en mémoire car compris entre 1 et 4).

Illustrant montrant l'encodage des sections.Figure 12 - Encodage des sections.

Voici les étapes pour quantizer un vecteur :

  1. Prendre un vecteur "v" de dimension "d". Diviser ce vecteur en "m" sections de taille égale, où "m" est généralement un nombre choisi à l'avance (plus m est grand et plus l’on compresse). Chaque section devient un sous-espace vectoriel de dimension "d/m". Par exemple, si "d" est égal à 32 et "m" est égal à 4, chaque sous-espace aura une dimension "d/m" de 8.
  2. Au sein de chaque sous-espace, effectuer une segmentation k-means en "k" clusters. Par exemple, si "k" est égal à 8 et "m" est égal à 4, on obtient un total de "k * m" clusters, soit 32 clusters au total, avec 8 clusters par sous-espace. Chaque cluster aura un centroïde comme dans la méthode précédente.
  3. Créer le codebook, qui sera essentiel pour décompresser les vecteurs ultérieurement. Le codebook contient les vecteurs représentant les centroïdes de chaque cluster. Il y aura "k*m”, soit dans ce cas 32 vecteurs dans le codebook, un pour chaque cluster de chaque section du vecteur d’origine.
  4. Pour compresser le vecteur d'origine, attribuer le centroïde le plus proche dans chaque sous-espace de chaque section du vecteur d'origine. Ainsi, le vecteur "v" d'origine, qui avait "d" dimensions, est maintenant compressé en "m" composantes, chacune des dimensions prenant la valeur de l'ID unique du cluster (de 0 à "(k*m)-1").

4 - b - Décompression du vecteur

Pour rappel, nous avons compressé notre vecteur v constitué de d composantes continues en m composantes discrètes (les ids des clusters). Pour décompresser le vecteur, il suffit, pour chaque composante, de récupérer dans le codebook le vecteur représentant le centroïde et de concaténer le tout-en-un seul vecteur. On obtient donc bien un vecteur de d= m* sd= 4x8 = 32, soit la dimension d’origine du vecteur. Sur la figure 13, on peut observer le processus permettant de passer du vecteur compressé de la figure 12. On retrouve bien un codebook composé de m*k = 8 clusters au total.

Illustration montrant le décodage à partir du codebook.Figure 13 : Décodage à partir du codebook.

Une chose importante à retenir est que l’information de chaque section du vecteur d’origine est maintenant résumée aux valeurs du vecteur représentant le centroïde le plus proche dans le sous-espace de chaque section. Ainsi, si un cluster contient 1000 vecteurs différents, il seront tous représentés par ce centroïde. Toute autre information de leur position dans ce sous-espace est perdue.

La figure 14, illustre bien cette perte d’information où deux vecteurs éloignés dans l’espace, mais appartenant au même cluster, se retrouvent présentement encodés par les mêmes valeurs, issues du vecteur représentant le centroïde du cluster. Ce schéma illustre également qu’en augmentant le nombre de clusters, nous augmentons le maillage de l’espace, réduisant ainsi la perte d’information. En revanche, ceci se fait au détriment d’un temps de calcul du clustering plus long, et d’une augmentation de la taille des entiers à stocker pour représenter le vecteur compressé (par exemple, à partir de 127 clusters, nous devons passer d’un entier de 8 bits vers 16 bits).

Illustration montrant la perte d’information et finesse de maillageFigure 14 : Perte d’information et finesse de maillage.

4 - c - Qualité de compression

Deux paramètres vont agir sur la qualité de compression : le nombre de sections (m) et le nombre de clusters (k) :

  • Plus le nombre de sections est élevé, plus la compression va être précise car la résolution est plus fine. En revanche, la dimension du vecteur compressé m est donc plus élevée et consomme plus de mémoire.
  • Plus le nombre de clusters est élevé, plus la compression va être précise. Le maillage de chaque sous-espace est plus précis et on généralise moins chaque sous-espace à un cluster occupant plus de place dans chaque sous-espace. Cela aura un impact sur le temps de calcul de la compression et sur le type d’entier utilisé pour stocker l’id du cluster.

Nous pouvons mesurer la qualité de la compression en calculant une MSE (Mean Squared Error) entre le vecteur original et le vecteur décompressé. Il s’agit de la moyenne des écarts au carré sur chacune des d composantes du vecteur d’origine et décompressé. Plus il est faible, mieux c’est.

La figure 15 illustre le principe du calcul du MSE sur l’exemple du vecteur compressé et décompressé dans les figures 13 et 14.

Illustration montrant le calcul du MSE de compression.Figure 15 : calcul du MSE de compression.

Sur les graphiques de la figure 16, nous pouvons observer les effets du nombre de sections “m” ainsi que du nombre de clusters par section, sur la qualité de la compression (à gauche) et la consommation de mémoire (à droite).

Image illustrant le compromis entre nombre de centroides par section et qualité de compression / mémoireFigure 16 : Qualité de compression et empreinte mémoire.

Tout d’abord, nous observons clairement que plus nous avons de sections et de clusters, plus la perte de compression est faible (MSE tendant vers 0): le découpage en section du vecteur étant plus fin ainsi que le maillage de chaque sous-espace par les clusters. Au niveau de la consommation de mémoire, on peut voir que plus, il y a de sections et de clusters, plus la consommation mémoire est élevée. Le phénomène de plateaux s’explique par les seuils des différents types de stockage d’entier de numpy. Pour rappel, le vecteur compressé est représenté par les identifiants des clusters. Ainsi, si nous avons moins de 128 clusters, chaque identifiant tient sur un entier de 8 bits (la valeur max d’un entier numpy int8 étant 127, 255 pour un entier non signé uint8)

4 - d - Recherche de similarité sur vecteurs quantizés

Nous avons vu comment compresser nos vecteurs en m entiers représentant les identifiants des centroïdes les plus proches de chaque section de nos vecteurs. Nos vecteurs sont stockés de cette façon en base de données et nous avons aussi stocké le codebook. Il n’est pas possible de calculer de similarité ou de distance directement sur les identifiants des centroïdes. Pour contourner cette contrainte, nous allons utiliser la méthode dites des distances asymétriques ou quasi-distances, dont voici les grandes étapes:

1 - Pour chaque section du vecteur en entrée (la requête), calculer les distances avec les centroïdes du sous-espace de cette section. Pour rappel, les coordonnées des centroïdes sont stockées dans le codebook. On obtient donc une table de distance de m colonnes (le nombre de sections) et de k lignes (le nombre de centroïdes) par section. (voir figure 17).

2 - Cette table nous donne pour une cellule en ligne i et colonne j, la distance entre le centroïde d’id i et la j-ième section du vecteur. Illustration du calcul de la matrice de distance asymétriqueFigure 17 : Calcul de la matrice de distance asymétrique.

3 - Pour tous les vecteurs de la base de données (on est sur une méthode brute force), compressés et donc représentés par des ids, aller chercher dans cette table la valeur de la distance. Par exemple, si la première section du premier vecteur de la base de données est codée 5, alors on va récupérer la distance présente dans la cellule à la 5ème ligne et première colonne. On fait cela pour chaque section et somme les distances. (voir figure 18).

Illustration du calcul des distances avec les vecteurs de la base de donnéesFigure 18 : Calcul des distances avec les vecteurs de la base de données.

4 - Nous avons donc pour chaque vecteur de la base de données, une distance asymétrique avec le vecteur en entrée. Il ne reste plus qu’à trier par ordre croissant (pour une distance) et prendre les K plus proches voisins.

On comprend vite que les paramètres de la compression vont avoir un impact sur la vitesse de calcul des similarités et sur la qualité des plus proches voisins remontés :

  • Plus il y a de sections et de clusters par section, plus nous augmentons le nombre d’opérations à faire pour chaque calcul de distance. En revanche, la qualité de compression est meilleure, ainsi que la qualité des plus proches voisins.
  • À l’inverse, si l’on réduit les valeurs des deux paramètres, la recherche est plus rapide, mais de moins bonne qualité

4 - e - Mesure de performances

Nous avons appliqué la méthode quantization sur la base de données Fashion MNIST afin de mesurer la qualité des résultats en fonction des paramètres utilisés. Le graphique de gauche de la figure 19 montre que le nombre de sections (couleur des points) et le nombre de centroïdes utilisés pour encoder chaque section impacte fortement la mesure de rappel : dans ce cas, plus les deux paramètres ont une valeur élevée, plus le rappel sera élevé.

Le choix du nombre de sections (m) est une histoire de compromis : globalement plus m est petit et plus l’on compresse les vecteurs, mais l’on perd en rappel, et inversement. Mais certaines données ont des structures intrinsèques qui peuvent être mieux capturées avec un certain nombre de sous-vecteurs, si les données ont une structure hiérarchique par exemple. Le choix peut donc être empirique, en testant plusieurs valeurs du nombre de sections et comparant les performances.

En parallèle, sur le graphique de droite, le nombre de requêtes par seconde apparaît comme très sensible aux valeurs des paramètres choisis. La version la plus rapide étant celle avec 2 sections par vecteur et celui avec le moins de centroïdes. Il s’agit de la version avec la moins bonne qualité de compression et donc le plus faible rappel. Ces deux graphiques illustrent bien le dilemme Mémoire/Qualité/performance des algorithmes des bases de données vectorielles.

Illustration de la mesure de qualité et de performance sur vecteurs quantizésFigure 19 : Mesure de qualité et de performance sur vecteurs quantizés.

Pour avoir un aperçu des performances des bases de données vectorielles disponibles sur le marché, vous pouvez visiter le repository github de Erik Bernhardsson (créateur de la BDD vectorielles Annoy de Spotify) qui propose un framework de benchmark.

Conclusion

Les bases de données vectorielles ont différentes astuces comme des méthodes d’indexation ou de compression qui permettent d’effectuer des recherches sur des vecteurs, avec différents compromis de rapidité et de précision. Notre tutoriel pour réimplémenter une base de données vectorielles from scratch permet de comprendre sous le capot ce qui s’y cache, et, évidemment cette implémentation n’est pas à déployer en production : il faut intégrer des implémentations plus sérieuses dans la vie réelle, comme Milvus ou Faiss.

Nous sommes persuadés que la connaissance du fonctionnement “sous le capot” de ces bases de données vous permettra de prendre des décisions éclairées concernant les différents hyperparamètres, en tenant compte des compromis entre les performances et la précision des différentes solutions disponibles sur le marché.

Ces bases de données, en tous les cas, ne sont a priori pas si différentes que cela des bases de données classiques ; ce n’est donc pas un hasard si des extensions comme pgvector pour PostgreSQL voient le jour pour intégrer ces caractéristiques propres aux vecteurs, sur des bases de données déjà standards dans nos architectures.

Références

  1. Aumüller, M., Bernhardsson, E., & Faithfull, A. (2020). ANN-Benchmarks : a benchmarking tool for approximate nearest neighbor algorithms. Information Systems, 87, 101374. https://doi.org/10.1016/j.is.2019.02.006
  2. Bastin, F., & Cavallo, N. (2023). Construire son RAG (Retrieval Augmented Generation) grâce à langchain: L’exemple de l’Helpdesk d’OCTO. OCTO Talks!. https://blog.octo.com/le-chatbot-docto-langchain-rag-et-code-associe
  3. Frederickson, B. (2017). Approximate nearest neighbours for recommender systems. https://www.benfrederickson.com/approximate-nearest-neighbours-for-recommender-systems/
  4. Jegou, H., Douze, M., & Johnson, J. (2018, 28 juin). FAISS : A library for efficient similarity search. Engineering at Meta. https://engineering.fb.com/2017/03/29/data-infrastructure/faiss-a-library-for-efficient-similarity-search/
  5. Jeǵou, H., Douze, M., & Schmid, C. (2011). Product quantization for nearest neighbor search. IEEE Transactions on Pattern Analysis and Machine Intelligence, 33(1), 117‑128. https://doi.org/10.1109/tpami.2010.57
  6. About Spotify. (2023). Spotify. https://newsroom.spotify.com/company-info/#:~:text=We%20are%20the%20world's%20most,in%20more%20than%20180%20markets.
  7. Wang, A. (2003). An industrial strength audio search algorithm. International Symposium/Conference on Music Information Retrieval. http://dblp.uni-trier.de/db/conf/ismir/ismir2003.html#Wang03