Problème:
Étant donné une grande liste (~ 100 millions) d'entiers 32 bits non signés, une valeur d'entrée entière 32 bits non signée et un maximum distance de Hamming , renvoyer tous les membres de la liste qui se trouvent dans la distance de Hamming spécifiée de la valeur d'entrée.
La structure de données réelle pour contenir la liste est ouverte, les exigences de performances dictent une solution en mémoire, le coût de construction de la structure de données est secondaire, le faible coût pour interroger la structure de données est critique.
Exemple:
For a maximum Hamming Distance of 1 (values typically will be quite small)
And input:
00001000100000000000000001111101
The values:
01001000100000000000000001111101
00001000100000000010000001111101
should match because there is only 1 position in which the bits are different.
11001000100000000010000001111101
should not match because 3 bit positions are different.
Mes pensées jusqu'à présent:
Pour le cas dégénéré d'une distance de Hamming de 0, utilisez simplement une liste triée et effectuez une recherche binaire pour la valeur d'entrée spécifique.
Si la distance de Hamming ne devait jamais être que de 1, je pourrais retourner chaque bit dans l'entrée d'origine et répéter les 32 fois ci-dessus.
Comment puis-je découvrir efficacement (sans scanner toute la liste) les membres de la liste avec une distance de Hamming> 1.
Question: Que savons-nous de la distance de Hamming d (x, y)?
Réponse:
Question: Pourquoi nous en soucions-nous?
Réponse: Parce que cela signifie que la distance de Hamming est un métrique pour un espace métrique. Il existe des algorithmes d'indexation des espaces métriques.
Vous pouvez également rechercher des algorithmes pour "l'indexation spatiale" en général, armés de la connaissance que votre espace n'est pas euclidien mais qu'il est un espace métrique. De nombreux livres sur ce sujet couvrent l'indexation des chaînes en utilisant une métrique telle que la distance de Hamming.
Note de bas de page: Si vous comparez la distance de Hamming des chaînes de largeur fixe, vous pourrez peut-être obtenir une amélioration significative des performances en utilisant l'assemblage ou les intrinsèques du processeur. Par exemple, avec GCC ( manual ) vous faites ceci:
static inline int distance(unsigned x, unsigned y)
{
return __builtin_popcount(x^y);
}
Si vous informez ensuite GCC que vous compilez pour un ordinateur avec SSE4a, je pense que cela devrait se réduire à quelques opcodes.
Edit: Selon un certain nombre de sources, c'est parfois/souvent plus lent que le code masque/shift/add habituel. L'analyse comparative montre que sur mon système, une version C surclasse le GCC de __builtin_popcount
d'environ 160%.
Addendum: J'étais moi-même curieux de connaître le problème, j'ai donc profilé trois implémentations: recherche linéaire, arbre BK et arbre VP. Notez que les arbres VP et BK sont très similaires. Les enfants d'un nœud dans un arbre BK sont des "coquilles" d'arbres contenant des points qui sont chacun à une distance fixe du centre de l'arbre. Un nœud dans une arborescence VP a deux enfants, l'un contenant tous les points d'une sphère centrée sur le centre du nœud et l'autre enfant contenant tous les points à l'extérieur. Vous pouvez donc considérer un nœud VP comme un nœud BK avec deux "coques" très épaisses au lieu de plusieurs plus fines.
Les résultats ont été capturés sur mon PC à 3,2 GHz et les algorithmes n'essaient pas d'utiliser plusieurs cœurs (ce qui devrait être facile). J'ai choisi une base de données de 100 millions d'entiers pseudo-aléatoires. Les résultats sont la moyenne de 1000 requêtes pour la distance 1..5, et 100 requêtes pour 6..10 et la recherche linéaire.
- Arbre BK - - Arbre VP - - Linéaire - Résultats Dist Vitesse Cov Vitesse Cov Vitesse Cov 1 0,90 3800 0,048% 4200 0,048% 2 11 300 0,68% 330 0,65% 3 130 56 3,8% 63 3,4% 4 970 18 12% 22 10% 5 5700 8,5 26% 10 22% 6 2,6e4 5,2 42% 6,0 37% 7 1,1e5 3,7 60% 4,1 54% 8 3,5e5 3,0 74% 3,2 70% 9 1,0e6 2,6 85% 2,7 82% 10 2,5e6 2,3 91% 2,4 90% Tout 2,2 100%
Dans votre commentaire, vous avez mentionné:
Je pense que les arbres BK pourraient être améliorés en générant un tas d'arbres BK avec différents nœuds racine et en les répartissant.
Je pense que c'est exactement la raison pour laquelle l'arbre VP fonctionne (légèrement) mieux que l'arbre BK. Étant "plus profond" plutôt que "moins profond", il compare avec plus de points plutôt que d'utiliser des comparaisons plus fines avec moins de points. Je soupçonne que les différences sont plus extrêmes dans les espaces de dimension supérieure.
Un dernier conseil: les nœuds foliaires dans l'arbre ne devraient être que des tableaux plats d'entiers pour un balayage linéaire. Pour les petits ensembles (peut-être 1000 points ou moins), ce sera plus rapide et plus efficace en mémoire.
Une approche courante (du moins commune à moi) consiste à diviser votre chaîne de bits en plusieurs morceaux et à rechercher sur ces morceaux une correspondance exacte en tant qu'étape de préfiltrage. Si vous travaillez avec des fichiers, vous créez autant de fichiers que vous avez de morceaux (par exemple 4 ici) avec chaque morceau permuté devant, puis triez les fichiers. Vous pouvez utiliser une recherche binaire et vous pouvez même étendre votre recherche au-dessus et au-dessous d'un morceau correspondant pour le bonus.
Vous pouvez ensuite effectuer un calcul de distance de hamming au niveau du bit sur les résultats renvoyés qui ne devraient être qu'un sous-ensemble plus petit de votre ensemble de données global. Cela peut être fait en utilisant des fichiers de données ou des tables SQL.
Donc, pour récapituler: Supposons que vous ayez un tas de chaînes de 32 bits dans une base de données ou des fichiers et que vous souhaitiez trouver chaque hachage à une distance de 3 bits ou moins de votre chaîne de bits de "requête":
créer une table avec quatre colonnes: chacune contiendra une tranche de 8 bits (sous forme de chaîne ou int) des hachages 32 bits, islice 1 à 4. Ou si vous utilisez des fichiers, créez quatre fichiers, chacun étant une permutation des tranches ayant une "islice" à l'avant de chaque "rangée"
découpez votre chaîne de bits de requête de la même manière dans les tranches 1 à 4.
interroger cette table de telle sorte que l'un des qslice1=islice1 or qslice2=islice2 or qslice3=islice3 or qslice4=islice4
. Cela vous donne chaque chaîne de 7 bits (8 - 1
) de la chaîne de requête. Si vous utilisez un fichier, effectuez une recherche binaire dans chacun des quatre fichiers permutés pour les mêmes résultats.
pour chaque chaîne de bits renvoyée, calculez la distance de hamming exacte par paire avec votre chaîne de bits d'interrogation (reconstruction des chaînes de bits côté index à partir des quatre tranches à partir de la base de données ou d'un fichier permuté)
Le nombre d'opérations à l'étape 4 doit être bien inférieur à un calcul de hamming complet par paire de votre table entière et est très efficace dans la pratique. De plus, il est facile de diviser les fichiers en fichiers plus petits en raison du besoin de plus de vitesse en utilisant le parallélisme.
Maintenant, bien sûr, dans votre cas, vous recherchez une auto-jointure en quelque sorte, c'est-à-dire toutes les valeurs qui sont à une certaine distance les unes des autres. La même approche fonctionne toujours à mon humble avis, bien que vous deviez développer de haut en bas à partir d'un point de départ pour les permutations (en utilisant des fichiers ou des listes) qui partagent le bloc de départ et calculent la distance de brouillage pour le cluster résultant.
Si vous exécutez en mémoire au lieu de fichiers, votre ensemble de données de chaînes de 100 bits 32 bits serait de l'ordre de 4 Go. Par conséquent, les quatre listes permutées peuvent nécessiter environ 16 Go + de RAM. Bien que j'obtienne à la place d'excellents résultats avec des fichiers mappés en mémoire et que je dois moins RAM pour des ensembles de données de taille similaire.
Des implémentations open source sont disponibles. Le meilleur de l'espace est à mon humble avis celui fait pour Simhash par Moz , C++ mais conçu pour les chaînes de 64 bits et non 32 bits.
Cette approche de distance de dépassement limitée a été décrite pour la première fois par l'AFAIK par Moses Charikar dans son séminal "simhash" papier et le Google correspondant brevet :
- RECHERCHE APPROXIMATIVE LA PLUS PROCHE DANS L'ESPACE DE MARTEAU
[...]
Étant donné des vecteurs de bits constitués de d bits chacun, nous choisissons N = O (n 1/(1+)) permutations aléatoires des bits. Pour chaque permutation aléatoire σ, nous maintenons un ordre trié O σ des vecteurs bits, dans l'ordre lexicographique des bits permutés par σ. Étant donné un vecteur de bit de requête q, nous trouvons le plus proche voisin approximatif en procédant comme suit:
Pour chaque permutation σ, nous effectuons une recherche binaire sur O σ pour localiser les deux vecteurs bits les plus proches de q (dans l'ordre lexicographique obtenu par les bits permutés par σ). Nous recherchons maintenant dans chacun des ordres triés O σ examinant les éléments au-dessus et au-dessous de la position retournée par la recherche binaire dans l'ordre de la longueur du préfixe le plus long qui correspond à q.
Monika Henziger développé à ce sujet dans son article "Trouver des pages Web presque en double: une évaluation à grande échelle des algorithmes" :
3.3 Les résultats pour l'algorithme C
Nous avons partitionné la chaîne de bits de chaque page en 12 morceaux de 4 octets qui ne se chevauchent pas, créant 20B morceaux, et avons calculé la similitude C de toutes les pages qui avaient au moins un morceau en commun. Cette approche est garantie de trouver toutes les paires de pages avec une différence allant jusqu'à 11, c'est-à-dire la similitude C 373, mais peut en manquer certaines pour des différences plus importantes.
Ceci est également expliqué dans l'article Detecting Near-Duplicates for Web Crawling par Gurmeet Singh Manku, Arvind Jain et Anish Das Sarma:
- LE PROBLÈME DE LA DISTANCE DE MARTEAU
Définition: Étant donné une collection d'empreintes digitales à f bits et une empreinte digitale de requête F, identifier si une empreinte digitale existante diffère de F sur au plus k bits. (Dans la version en mode batch du problème ci-dessus, nous avons un ensemble d'empreintes digitales de requête au lieu d'une seule empreinte digitale de requête)
[...]
Intuition: considérons un tableau trié d'empreintes digitales vraiment aléatoires à 2 d f bits. Concentrez-vous uniquement sur les bits d les plus significatifs du tableau. Une liste de ces nombres de bits d équivaut à "presque un compteur" dans le sens où (a) il existe un certain nombre de combinaisons de bits 2 d, et (b) très peu de combinaisons de bits d sont dupliquées. En revanche, les bits f - d les moins significatifs sont "presque aléatoires".
Choisissez maintenant d tel que | d - d | est un petit entier. Puisque le tableau est trié, une seule sonde suffit pour identifier toutes les empreintes digitales qui correspondent à F dans d positions binaires les plus significatives. Depuis | d - d | est faible, le nombre de ces correspondances devrait également être faible. Pour chaque empreinte digitale correspondante, nous pouvons facilement déterminer si elle diffère de F dans au plus k positions binaires ou non (ces différences seraient naturellement limitées aux f - d positions binaires les moins significatives).
La procédure décrite ci-dessus nous aide à localiser une empreinte digitale existante qui diffère de F en k positions binaires, qui sont toutes limitées à être parmi les bits f - d les moins significatifs de F. Cela prend en charge un bon nombre de cas. Pour couvrir tous les cas, il suffit de construire un petit nombre de tableaux triés supplémentaires, comme indiqué officiellement dans la section suivante.
Remarque: j'ai posté une réponse similaire à une question liée à la base de données uniquement
Vous pouvez pré-calculer chaque variation possible de votre liste d'origine dans la distance de hamming spécifiée et la stocker dans un filtre de floraison. Cela vous donne un "NON" rapide mais pas nécessairement une réponse claire à propos de "OUI".
Pour OUI, stockez une liste de toutes les valeurs d'origine associées à chaque position dans le filtre de floraison et parcourez-les une par une. Optimisez la taille de votre filtre de floraison pour des compromis vitesse/mémoire.
Je ne sais pas si tout fonctionne correctement, mais cela semble être une bonne approche si vous avez un runtime RAM à graver et que vous êtes prêt à passer très longtemps en pré-calcul.
Une approche possible pour résoudre ce problème consiste à utiliser une structure de données à ensemble disjoint . L'idée est de fusionner les membres de la liste avec une distance de Hamming <= k dans le même ensemble. Voici le contour de l'algorithme:
Pour chaque membre de la liste calculez toutes les valeurs possibles avec une distance de Hamming <= k . Pour k = 1, il existe 32 valeurs (pour les valeurs 32 bits). Pour k = 2, 32 + 32 * 31/2 valeurs.
Pour chaque valeur calculée , testez si elle se trouve dans l'entrée d'origine. Vous pouvez utiliser un tableau de taille 2 ^ 32 ou une carte de hachage pour effectuer cette vérification.
Si la valeur se trouve dans l'entrée d'origine, effectuez une opération "union" avec le membre de liste .
Vous démarrez l'algorithme avec N ensembles disjoints (où N est le nombre d'éléments dans l'entrée). Chaque fois que vous exécutez une opération d'union, vous diminuez de 1 le nombre d'ensembles disjoints. Lorsque l'algorithme se termine, la structure de données d'ensemble disjoint aura toutes les valeurs avec une distance de Hamming <= k regroupées en ensembles disjoints. Cette structure de données à ensembles disjoints peut être calculée en presque temps linéaire .
Que diriez-vous de trier la liste puis de faire une recherche binaire dans cette liste triée sur les différentes valeurs possibles dans votre Hamming Distance?