Indexation de milliards de vecteurs de texte


Lors de l'extraction d'informations, la tĂąche se pose souvent de trouver de tels fragments de texte. Dans le cadre d'une recherche, une requĂȘte peut ĂȘtre gĂ©nĂ©rĂ©e par l'utilisateur (par exemple, le texte que l'utilisateur saisit dans le moteur de recherche) ou par le systĂšme lui-mĂȘme. Souvent, nous devons faire correspondre une requĂȘte entrante avec des requĂȘtes dĂ©jĂ  indexĂ©es. Dans cet article, nous verrons comment vous pouvez crĂ©er un systĂšme qui rĂ©sout ce problĂšme par rapport Ă  des milliards de demandes sans dĂ©penser une fortune sur l'infrastructure du serveur.

Tout d'abord, nous définissons formellement le problÚme:

Étant donnĂ© un ensemble fixe de requĂȘtes Q demande entrante q et entier k . Besoin de trouver un tel sous-ensemble de requĂȘtes R = \ left \ {q0, q1, ..., qk \ right \} \ sous-ensemble Q Ă  chaque demande qi enR Ă©tait plus comme q que toute autre demande Q∖R .

Par exemple, avec cet ensemble de requĂȘtes Q :

{tesla cybertruck, beginner bicycle gear, eggplant dishes, tesla new car, how expensive is cybertruck, vegetarian food, shimano 105 vs ultegra, building a carbon bike, zucchini recipes} 

et k=3 Vous pouvez vous attendre à ce résultat:

Demande d'entrée q
RequĂȘtes similaires R
ramassage tesla
{tesla cybertruck, tesla new car, combien coûte le cybertruck}
meilleur vélo 2019
{shimano 105 vs ultegra, les vélos en carbone sont-ils meilleurs, l'équipement de vélo}
cuisiner avec des légumes
{plats d'aubergines, recettes de courgettes, plats végétariens}

Veuillez noter que nous n'avons pas encore dĂ©fini de critĂšre de similitude . Dans ce contexte, cela peut signifier presque n'importe quoi, mais cela se rĂ©sume gĂ©nĂ©ralement Ă  une certaine forme de similitude basĂ©e sur des mots clĂ©s ou des vecteurs. En utilisant la similitude basĂ©e sur les mots clĂ©s, nous pouvons trouver deux requĂȘtes similaires si elles contiennent suffisamment de mots communs. Par exemple, les requĂȘtes «ouverture d'un restaurant Ă  munich» et «meilleur restaurant de munich» sont similaires car elles contiennent les mots «restaurant» et «munich». Et les requĂȘtes «meilleur restaurant de munich» et «oĂč manger Ă  munich» sont dĂ©jĂ  moins similaires, car elles n'ont qu'un seul mot commun. Cependant, quelqu'un qui cherche un restaurant Ă  Munich serait mieux si la deuxiĂšme paire de demandes s'avĂ©rait similaire. Et en cela, nous allons aider la comparaison basĂ©e sur des vecteurs.

Représentation vectorielle des mots


La reprĂ©sentation vectorielle des mots est une technique d'apprentissage automatique utilisĂ©e dans le traitement du langage naturel pour convertir du texte ou des mots en vecteurs. DĂ©placer la tĂąche dans l'espace vectoriel, nous pouvons utiliser des opĂ©rations mathĂ©matiques avec des vecteurs - additionner et calculer les distances. Pour Ă©tablir des liens entre des mots similaires, vous pouvez utiliser des mĂ©thodes traditionnelles de regroupement vectoriel. La signification de ces opĂ©rations dans l'espace de mots d'origine n'est peut-ĂȘtre pas Ă©vidente, mais l'avantage est que nous avons maintenant accĂšs Ă  un large Ă©ventail d'outils mathĂ©matiques. Si vous ĂȘtes intĂ©ressĂ© par des dĂ©tails sur les vecteurs de mots et leur application, lisez Ă  propos de word2vec et GloVe .

Nous avons un moyen de générer des vecteurs à partir de mots, nous allons maintenant les collecter en vecteurs de texte (vecteurs de documents ou d'expressions). La façon la plus simple de le faire est d'ajouter (ou de faire la moyenne) les vecteurs de tous les mots du texte.


Figure 1: vecteurs de requĂȘte.

Vous pouvez maintenant dĂ©terminer la similitude de deux morceaux de texte (ou requĂȘtes) en les reprĂ©sentant dans l'espace vectoriel et en calculant la distance entre les vecteurs. En rĂšgle gĂ©nĂ©rale, une distance angulaire est utilisĂ©e pour cela.

Par consĂ©quent, la reprĂ©sentation vectorielle des mots permet une correspondance textuelle d'un type diffĂ©rent, qui complĂšte la correspondance basĂ©e sur des mots clĂ©s. Vous pouvez explorer la similitude sĂ©mantique des demandes (par exemple, "meilleur restaurant de munich" et "oĂč manger Ă  munich"), comme nous ne pouvions pas le faire auparavant.

Recherche approximative du voisin le plus proche


Maintenant, nous pouvons affiner notre problĂšme de correspondance de requĂȘte d'origine:

Étant donnĂ© un ensemble fixe de vecteurs de requĂȘte Q vecteur entrant q et entier k . Vous devez trouver un tel sous-ensemble de vecteurs R = \ left \ {q0, q1, ..., qk \ right \} \ sous-ensemble Q de sorte que la distance angulaire de q Ă  chaque vecteur qi enR Ă©tait plus courte que la distance Ă  tout autre vecteur Q∖R .

C'est ce qu'on appelle la tùche de trouver le plus proche voisin. Il existe un certain nombre d'algorithmes pour sa solution rapide dans les espaces de faible dimension. Mais lorsque nous travaillons avec des représentations vectorielles de mots, nous opérons généralement avec des vecteurs de haute dimension (100-1000 dimensions). Et ici, les méthodes mentionnées ne fonctionnent plus.

Il n'existe aucun moyen approprié de déterminer rapidement les voisins les plus proches dans des espaces de grande dimension. Par conséquent, nous simplifions le problÚme en permettant l'utilisation de résultats approximatifs: au lieu de toujours renvoyer les vecteurs les plus proches, nous nous contenterons seulement de certains des voisins les plus proches ou dans une certaine mesure proches. C'est ce qu'on appelle la recherche approximative de la tùche des voisins les plus proches et c'est un domaine de recherche active.

Petit monde hiérarchique


Le graphique hiĂ©rarchique du petit monde navigable ( HNSW ) est l'un des algorithmes les plus rapides pour la recherche approximative des voisins les plus proches. L'index de recherche dans HNSW est une structure Ă  plusieurs niveaux dans laquelle chaque niveau est un graphe de proximitĂ©. Chaque nƓud de graphe correspond Ă  l'un des vecteurs de requĂȘte.


Figure 2: Graphique de proximité à plusieurs niveaux.
La recherche de voisins les plus proches dans HNSW utilise la mĂ©thode de zoom avant. Il commence dans le nƓud d'entrĂ©e du plus haut niveau et effectue rĂ©cursivement une traversĂ©e de graphe gourmande Ă  chaque niveau jusqu'Ă  ce qu'il atteigne un minimum local en bas.

Les dĂ©tails sur l'algorithme et la technique de recherche sont bien dĂ©crits dans les travaux scientifiques. Il est important de se rappeler que chaque cycle de recherche des voisins les plus proches consiste Ă  parcourir les nƓuds des graphes avec calcul des distances entre les vecteurs. Nous examinerons ces Ă©tapes ci-dessous pour utiliser cette mĂ©thode pour crĂ©er un index Ă  grande Ă©chelle.

La difficultĂ© d'indexer des milliards de requĂȘtes


Supposons que nous devons indexer 4 milliards de vecteurs de requĂȘte Ă  200 dimensions, chaque dimension Ă©tant reprĂ©sentĂ©e par un nombre Ă  virgule flottante de quatre octets (4 milliards suffisent pour rendre la tĂąche intĂ©ressante, mais vous pouvez toujours stocker les ID de nƓud dans des nombres normaux Ă  quatre octets) . Un calcul approximatif nous indique que la taille des vecteurs seuls sera d'environ 3 To. Comme la plupart des bibliothĂšques existantes utilisent une capacitĂ© RAM pour une recherche approximative des voisins les plus proches, nous aurons besoin d'un trĂšs grand serveur pour pousser au moins des vecteurs dans la RAM. Veuillez noter que cela ne prend pas en compte l'index de recherche supplĂ©mentaire, qui est nĂ©cessaire pour la plupart des mĂ©thodes.

Dans toute l'histoire du développement de notre moteur de recherche, nous avons utilisé plusieurs approches différentes pour résoudre ce problÚme. Examinons certains d'entre eux.

Sous-ensemble de données


La premiĂšre et la plus simple approche, qui ne nous a pas permis de rĂ©soudre complĂštement le problĂšme, a Ă©tĂ© de limiter le nombre de vecteurs dans l'indice. En prenant un dixiĂšme des donnĂ©es, nous avons crĂ©Ă© un index qui nĂ©cessite - surprise - 10% de mĂ©moire. Cependant, la qualitĂ© de la recherche s'est dĂ©tĂ©riorĂ©e, car nous avons maintenant opĂ©rĂ© avec moins de requĂȘtes.

Quantification


Ici, nous avons utilisĂ© toutes les donnĂ©es, mais nous les avons rĂ©duites en utilisant la quantification (nous avons utilisĂ© diffĂ©rentes techniques de quantification, par exemple, la quantification des produits, mais nous n'avons pas pu atteindre la qualitĂ© de travail souhaitĂ©e avec cette quantitĂ© de donnĂ©es). En arrondissant certaines erreurs, nous avons pu remplacer tous les nombres Ă  quatre octets dans les vecteurs d'origine par des versions quantifiĂ©es Ă  un octet. La quantitĂ© de RAM pour les vecteurs a diminuĂ© de 75%. Cependant, nous avions encore besoin de 750 Go de mĂ©moire (sans compter la taille de l'index lui-mĂȘme), et c'est toujours un trĂšs grand serveur.

Résolution des problÚmes de mémoire avec Granne


Les approches décrites avaient leurs avantages, mais elles nécessitaient beaucoup de ressources et donnaient une mauvaise qualité de recherche. Bien qu'il existe des bibliothÚques qui répondent en moins de 1 ms, nous pourrions sacrifier la vitesse en échange d'exigences matérielles plus faibles.

Granne (graphique-based voisins les plus proches) est une bibliothĂšque HNSW dĂ©veloppĂ©e et utilisĂ©e par Cliqz pour rechercher de telles requĂȘtes. Il a du code open source, mais la bibliothĂšque est toujours en dĂ©veloppement actif. Une version amĂ©liorĂ©e sera publiĂ©e sur crates.io en 2020. Il est Ă©crit en rouille avec des inserts en Python, conçu pour gĂ©rer des milliards de vecteurs en utilisant la compĂ©titivitĂ©. Du point de vue des vecteurs de requĂȘte, il est intĂ©ressant de noter que Granne dispose d'un mode spĂ©cial qui nĂ©cessite beaucoup moins de mĂ©moire par rapport aux autres bibliothĂšques.

ReprĂ©sentation compacte des vecteurs de requĂȘte


RĂ©duire la taille des vecteurs de requĂȘte nous apportera de nombreux avantages. Pour ce faire, revenons en arriĂšre et envisageons de crĂ©er des vecteurs. Étant donnĂ© que les requĂȘtes sont composĂ©es de mots et que les vecteurs de requĂȘte sont des sommes de vecteurs de mots, nous pouvons explicitement refuser de stocker des vecteurs de requĂȘte et les calculer si nĂ©cessaire.

Vous pouvez stocker des requĂȘtes sous forme d'ensembles de mots et utiliser la table de recherche pour trouver les vecteurs correspondants. Cependant, nous Ă©vitons la redirection en stockant chaque requĂȘte sous la forme d'une liste d'ID entiers correspondant aux vecteurs de mots dans la requĂȘte. Par exemple, enregistrez la requĂȘte «meilleur restaurant de munich» sous [ibest,irestaurant,iof,imunich] oĂč ibest - c'est l'ID de vecteur du mot «meilleur», etc. Supposons que nous ayons moins de 16 millions de vecteurs de mots (plus que ce montant coĂ»tera 1 octet par mot), alors vous pouvez utiliser 3 octets pour reprĂ©senter tous les ID de mot. Autrement dit, au lieu de stocker 800 octets (ou 200 octets dans le cas de vecteurs quantifiĂ©s), nous ne stockerons que 12 octets pour cette demande (ce n'est pas tout Ă  fait vrai. Comme les demandes se composent d'un nombre diffĂ©rent de mots, nous devons Ă©galement stocker l'offset de la liste dans l'index des mots. Pour cela nĂ©cessitera 5 octets par demande).

Quant aux vecteurs de mots, nous en avons tous besoin. Cependant, le nombre de mots est beaucoup plus petit que le nombre de requĂȘtes pouvant ĂȘtre crĂ©Ă©es en combinant ces mots. Et cela signifie que la taille des mots n'est pas si importante. Si vous stockez des vecteurs de mots sous forme de versions Ă  virgule flottante de quatre octets dans un tableau simple v , nous avons besoin de moins de 1 Go pour chaque million de mots. Ce volume peut facilement tenir dans la RAM. Maintenant, le vecteur de requĂȘte ressemble Ă  ceci: {v _ {{i_ {best}}}} + {v _ {{i_ {restaurant}}} + {v _ {{i_ {of}}} + {v _ {{i_ {munich}}}}} .

La taille finale de la soumission de la requĂȘte dĂ©pend du nombre total de mots dans la requĂȘte. Pour 4 milliards de requĂȘtes, cela reprĂ©sente environ 80 Go (y compris les vecteurs de mots). En d'autres termes, par rapport aux vecteurs de mots d'origine, la taille a diminuĂ© de 97% et par rapport aux vecteurs quantifiĂ©s - de 90%.

Et encore une chose. Pour une recherche, nous devons visiter environ 200-300 nƓuds du graphique. Chaque nƓud a 20 Ă  30 voisins. Donc, nous devons calculer la distance entre le vecteur de requĂȘte d'entrĂ©e et les vecteurs 4000-9000 dans l'index. Et de plus, nous devons gĂ©nĂ©rer des vecteurs. Combien de temps faut-il pour crĂ©er des vecteurs de requĂȘte Ă  la volĂ©e?

Il s'avĂšre qu'avec un processeur assez rĂ©cent, ce problĂšme peut ĂȘtre rĂ©solu en quelques millisecondes. Une requĂȘte qui s'exĂ©cutait en 1 ms s'exĂ©cute maintenant en 5 ms environ. Mais ensuite, nous avons rĂ©duit la quantitĂ© de mĂ©moire pour les vecteurs de 90%. Nous avons acceptĂ© avec plaisir un tel compromis.

Affichage en mémoire des vecteurs et index


Ci-dessus, nous avons rĂ©solu le problĂšme de la rĂ©duction de la quantitĂ© de mĂ©moire pour les vecteurs. Mais aprĂšs avoir rĂ©solu ce problĂšme, la structure de l'indice elle-mĂȘme est devenue un facteur limitant. Vous devez maintenant rĂ©duire sa taille.

À Granne, la structure du graphe est stockĂ©e de maniĂšre compacte sous la forme d'une liste d'adjacence avec un nombre variable de voisins pour chaque nƓud. Autrement dit, la mĂ©moire n'est guĂšre gaspillĂ©e en mĂ©tadonnĂ©es. La taille de la structure d'index dĂ©pend en grande partie des paramĂštres de conception et des propriĂ©tĂ©s du graphique. NĂ©anmoins, pour avoir une idĂ©e de la taille de l'index, il suffit de dire que nous pouvons construire un index pour 4 milliards de vecteurs avec une taille totale d'environ 240 Go. Cela peut ĂȘtre acceptable pour une utilisation en mĂ©moire sur un grand serveur, mais cela peut ĂȘtre mieux fait.


Figure 3: Deux dispositions différentes en RAM et SSD.

Une propriĂ©tĂ© importante de Granne est la possibilitĂ© d' afficher les vecteurs d'index et de requĂȘte en mĂ©moire . Cela nous permet de charger l'index paresseusement et de partager la mĂ©moire avec plusieurs processus. Les fichiers d'index et de requĂȘte sont divisĂ©s en fichiers d'affichage distincts en mĂ©moire et peuvent ĂȘtre utilisĂ©s dans diffĂ©rentes dispositions en RAM et sur SSD. Si les exigences pour le dĂ©lai sont lĂ©gĂšrement plus faibles, alors en plaçant l'index sur le SSD, les requĂȘtes en RAM, nous maintenons une vitesse acceptable sans consommation excessive de mĂ©moire. À la fin de l'article, nous verrons Ă  quoi ressemble ce compromis.

Amélioration de la localisation des données


Dans notre configuration actuelle, lorsque l'index est sur un SSD, chaque demande nĂ©cessite jusqu'Ă  200-300 lectures Ă  partir du disque. Vous pouvez essayer d'augmenter la localitĂ© des donnĂ©es en disposant les Ă©lĂ©ments dont les vecteurs sont si proches que leurs nƓuds HNSW sont situĂ©s dans l'index Ă©galement non loin les uns des autres. La localisation des donnĂ©es amĂ©liore les performances, car une seule opĂ©ration de lecture (gĂ©nĂ©ralement extraite de 4 Ko) est plus susceptible de contenir d'autres nƓuds nĂ©cessaires pour parcourir le graphique. Et cela, Ă  son tour, rĂ©duit le nombre de rĂ©cupĂ©rations de donnĂ©es par recherche.


Figure 4: La localisation des données réduit la récupération d'informations.

Il convient de noter que la rĂ©organisation des Ă©lĂ©ments n'affecte pas les rĂ©sultats de la recherche, c'est une façon de l'accĂ©lĂ©rer. Autrement dit, l'ordre peut ĂȘtre quelconque, mais toutes les options n'accĂ©lĂšrent pas la recherche. Il est trĂšs difficile de trouver l'ordre optimal. Cependant, l'heuristique que nous avons utilisĂ©e avec succĂšs est de trier les requĂȘtes par le mot le plus important dans chaque requĂȘte.

Conclusion


Nous utilisons Granne pour crĂ©er et maintenir des index de plusieurs milliards de dollars avec des vecteurs de requĂȘte afin de rechercher des requĂȘtes similaires avec une consommation de mĂ©moire relativement faible. Le tableau ci-dessous montre les exigences pour diffĂ©rentes mĂ©thodes. Soyez sceptique quant aux valeurs absolues des retards pendant la recherche, car ils dĂ©pendent fortement de ce qui est considĂ©rĂ© comme une rĂ©ponse acceptable. Cependant, ces informations dĂ©crivent les performances relatives des mĂ©thodes.

Valeur initiale
Quantification
Granne (RAM uniquement)
Granne (RAM + SSD)
La mémoire
3000 + 240 Go
750 + 240 Go
80 + 240 Go
80-150 Go *
SSD
---240 Go
Retard
1 ms
1 ms
5 ms
10-50 ms

* L'allocation d'un index mĂ©moire supĂ©rieur Ă  la quantitĂ© requise a conduit Ă  la mise en cache de certains nƓuds (frĂ©quemment visitĂ©s), ce qui a rĂ©duit le retard dans la recherche. Aucun cache interne n'a Ă©tĂ© utilisĂ© pour cela, uniquement des outils de systĂšme d'exploitation internes (noyau Linux).

Il convient de noter que certaines des optimisations mentionnĂ©es dans l'article ne sont pas applicables pour rĂ©soudre le problĂšme gĂ©nĂ©ral de trouver des voisins les plus proches avec des vecteurs indĂ©composables. Cependant, ils sont applicables dans toutes les situations oĂč des Ă©lĂ©ments peuvent ĂȘtre gĂ©nĂ©rĂ©s Ă  partir de moins de piĂšces (comme c'est le cas avec les mots et les requĂȘtes). Sinon, vous pouvez toujours utiliser Granne avec les vecteurs sources, cela prend juste plus de mĂ©moire, comme avec les autres bibliothĂšques.

Source: https://habr.com/ru/post/fr479692/


All Articles