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:
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.
* 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.