Problem:
Bei einer großen (~ 100 Millionen) Liste von vorzeichenlosen 32-Bit-Ganzzahlen, einem vorzeichenlosen 32-Bit-Ganzzahl-Eingabewert und einer maximalen Hamming-Entfernung werden alle Listenelemente zurückgegeben, die innerhalb der angegebenen Hamming-Entfernung des Eingabewerts liegen.
Die tatsächliche Datenstruktur zum Speichern der Liste ist offen, die Leistungsanforderungen schreiben eine In-Memory-Lösung vor, die Kosten für den Aufbau der Datenstruktur sind zweitrangig, die geringen Kosten für die Abfrage der Datenstruktur sind kritisch.
Beispiel:
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.
Meine bisherigen Gedanken:
Verwenden Sie für den entarteten Fall einer Hamming-Distanz von 0 einfach eine sortierte Liste und führen Sie eine binäre Suche nach dem spezifischen Eingabewert durch.
Wenn der Hamming-Abstand immer nur 1 wäre, könnte ich jedes Bit in der ursprünglichen Eingabe umdrehen und die obigen 32 Mal wiederholen.
Wie kann ich effizient (ohne die gesamte Liste zu scannen) Listenmitglieder mit einem Hamming-Abstand> 1 ermitteln?
Antworten:
Frage: Was wissen wir über den Hamming-Abstand d (x, y)?
Antworten:
Frage: Warum interessiert es uns?
Antwort: Weil dies bedeutet, dass der Hamming-Abstand eine Metrik für einen metrischen Raum ist . Es gibt Algorithmen zum Indizieren von Metrikräumen.
Sie können auch Algorithmen für „räumliche Indizierung“ im Allgemeinen sehen, bewaffnet mit dem Wissen , dass Ihr Raum nicht euklidischen ist , aber es ist ein metrischer Raum. Viele Bücher zu diesem Thema behandeln die Indizierung von Zeichenfolgen mithilfe einer Metrik wie der Hamming-Entfernung.
Fußnote: Wenn Sie den Hamming-Abstand von Strings mit fester Breite vergleichen, können Sie möglicherweise eine signifikante Leistungsverbesserung erzielen, indem Sie Assembly- oder Prozessor-Intrinsics verwenden. Mit GCC ( manuell ) tun Sie beispielsweise Folgendes :
Wenn Sie dann GCC darüber informieren, dass Sie für einen Computer mit SSE4a kompilieren, sollte sich dies meiner Meinung nach auf nur ein paar Opcodes reduzieren.
Bearbeiten: Laut einer Reihe von Quellen ist dies manchmal / oft langsamer als der übliche Mask / Shift / Add-Code. Das Benchmarking zeigt, dass auf meinem System eine C-Version die GCCs
__builtin_popcount
um etwa 160% übertrifft .Nachtrag: Ich war selbst neugierig auf das Problem und habe drei Implementierungen profiliert: lineare Suche, BK-Baum und VP-Baum. Beachten Sie, dass VP- und BK-Bäume sehr ähnlich sind. Die untergeordneten Elemente eines Knotens in einem BK-Baum sind "Schalen" von Bäumen, die Punkte enthalten, die jeweils einen festen Abstand vom Baumzentrum haben. Ein Knoten in einem VP-Baum hat zwei untergeordnete Elemente, von denen eines alle Punkte innerhalb einer Kugel enthält, die auf der Mitte des Knotens zentriert ist, und das andere untergeordnete Element alle Punkte außerhalb. Sie können sich also einen VP-Knoten als einen BK-Knoten mit zwei sehr dicken "Schalen" anstelle vieler feinerer vorstellen.
Die Ergebnisse wurden auf meinem 3,2-GHz-PC erfasst, und die Algorithmen versuchen nicht, mehrere Kerne zu verwenden (was einfach sein sollte). Ich habe eine Datenbankgröße von 100M Pseudozufallszahlen gewählt. Die Ergebnisse sind der Durchschnitt von 1000 Abfragen für die Entfernung 1..5 und 100 Abfragen für die Entfernung 6..10 und die lineare Suche.
In Ihrem Kommentar haben Sie erwähnt:
Ich denke, dies ist genau der Grund, warum der VP-Baum (etwas) besser abschneidet als der BK-Baum. Da es eher "tiefer" als "flacher" ist, vergleicht es mit mehr Punkten, anstatt feinkörnigere Vergleiche mit weniger Punkten zu verwenden. Ich vermute, dass die Unterschiede in höherdimensionalen Räumen extremer sind.
Ein letzter Tipp: Blattknoten im Baum sollten für einen linearen Scan nur flache Anordnungen von Ganzzahlen sein. Bei kleinen Sätzen (möglicherweise 1000 Punkte oder weniger) ist dies schneller und speichereffizienter.
quelle
Ich habe eine Lösung geschrieben, bei der ich die Eingangsnummern in einem Bitsatz von 2 bis 32 Bit darstelle, damit ich in O (1) überprüfen kann, ob eine bestimmte Zahl in der Eingabe enthalten ist. Dann generiere ich für eine abgefragte Zahl und eine maximale Entfernung rekursiv alle Zahlen innerhalb dieser Entfernung und vergleiche sie mit dem Bitset.
Für den maximalen Abstand 5 sind dies beispielsweise 242825 Zahlen ( Summe d = 0 bis 5 {32 wähle d} ). Zum Vergleich: Dietrich Epps VP-Tree-Lösung durchläuft beispielsweise 22% der 100 Millionen Zahlen, dh 22 Millionen Zahlen.
Ich habe Dietrichs Code / Lösungen als Grundlage verwendet, um meine Lösung hinzuzufügen und mit seiner zu vergleichen. Hier sind die Geschwindigkeiten in Abfragen pro Sekunde für maximale Entfernungen von bis zu 10:
Für kleine Entfernungen ist die Bitset-Lösung bei weitem die schnellste der vier. Der Autor der Frage, Eric, kommentierte unten, dass die größte Entfernung von Interesse wahrscheinlich 4-5 sein würde. Natürlich wird meine Bitset-Lösung für größere Entfernungen langsamer, sogar langsamer als die lineare Suche (für die Entfernung 32 würde sie 2 32 Zahlen durchlaufen ). Aber für Distanz 9 führt es immer noch leicht.
Ich habe auch Dietrichs Tests modifiziert. Jedes der obigen Ergebnisse dient dazu, den Algorithmus mindestens drei Abfragen und so viele Abfragen wie möglich in etwa 15 Sekunden lösen zu lassen (ich mache Runden mit 1, 2, 4, 8, 16 usw. Abfragen, bis mindestens 10 Sekunden vergangen sind insgesamt bestanden). Das ist ziemlich stabil, ich bekomme sogar ähnliche Zahlen für nur 1 Sekunde.
Meine CPU ist ein i7-6700. Mein Code (basierend auf Dietrichs) ist hier (ignoriere die Dokumentation dort zumindest vorerst, weiß nicht, was ich dagegen tun soll, aber er
tree.c
enthält den gesamten Code und meinetest.bat
Shows, wie ich kompiliert und ausgeführt habe (ich habe die Flags von Dietrichs verwendetMakefile
)). . Verknüpfung zu meiner Lösung .Eine Einschränkung: Meine Abfrageergebnisse enthalten nur einmal Zahlen. Wenn die Eingabeliste also doppelte Zahlen enthält, kann dies erwünscht sein oder nicht. In dem Fall des fraglichen Autors Eric gab es keine Duplikate (siehe Kommentar unten). In jedem Fall kann diese Lösung für Personen geeignet sein, die entweder keine Duplikate in der Eingabe haben oder keine Duplikate in den Abfrageergebnissen möchten oder benötigen (ich denke, es ist wahrscheinlich, dass die reinen Abfrageergebnisse nur ein Mittel zum Zweck sind und dann Ein anderer Code verwandelt die Zahlen in etwas anderes, z. B. eine Karte, die eine Zahl einer Liste von Dateien zuordnet, deren Hash diese Zahl ist.
quelle
Ein gängiger Ansatz (zumindest für mich üblich) besteht darin, Ihre Bitfolge in mehrere Blöcke zu unterteilen und diese Blöcke nach einer genauen Übereinstimmung als Vorfilterschritt abzufragen. Wenn Sie mit Dateien arbeiten, erstellen Sie so viele Dateien, wie Sie Blöcke haben (z. B. 4 hier), wobei jeder Block vor Ihnen permutiert wird, und sortieren dann die Dateien. Sie können eine binäre Suche verwenden und Ihre Suche sogar über und unter einem passenden Teil für den Bonus erweitern.
Sie können dann eine bitweise Hamming-Distanzberechnung für die zurückgegebenen Ergebnisse durchführen, die nur eine kleinere Teilmenge Ihres gesamten Datensatzes sein sollte. Dies kann mithilfe von Datendateien oder SQL-Tabellen erfolgen.
Um es noch einmal zusammenzufassen: Angenommen, Sie haben eine Reihe von 32-Bit-Zeichenfolgen in einer Datenbank oder in Dateien und möchten jeden Hash finden, der sich innerhalb eines 3-Bit-Hamming-Abstands oder weniger Ihrer "Abfrage" -Bitzeichenfolge befindet:
Erstellen Sie eine Tabelle mit vier Spalten: Jede enthält ein 8-Bit-Slice (als Zeichenfolge oder Int) der 32-Bit-Hashes, Islice 1 bis 4. Wenn Sie Dateien verwenden, erstellen Sie vier Dateien, von denen jede eine Permutation der Slices ist eine "Insel" vor jeder "Reihe"
Schneiden Sie Ihre Abfragebitzeichenfolge auf die gleiche Weise in qslice 1 bis 4.
Fragen Sie diese Tabelle so ab, dass eine von
qslice1=islice1 or qslice2=islice2 or qslice3=islice3 or qslice4=islice4
. Dies gibt Ihnen jede Zeichenfolge, die innerhalb von 7 Bits (8 - 1
) von der Abfragezeichenfolge liegt. Wenn Sie eine Datei verwenden, führen Sie in jeder der vier permutierten Dateien eine binäre Suche durch, um dieselben Ergebnisse zu erzielen.Berechnen Sie für jede zurückgegebene Bitfolge paarweise die genaue Hamming-Distanz mit Ihrer Abfrage-Bitfolge (Rekonstruktion der indexseitigen Bitfolgen aus den vier Slices entweder aus der DB oder aus einer permutierten Datei).
Die Anzahl der Operationen in Schritt 4 sollte viel geringer sein als eine vollständige paarweise Hamming-Berechnung Ihrer gesamten Tabelle und ist in der Praxis sehr effizient. Darüber hinaus ist es einfach, die Dateien in kleinere Dateien zu zerlegen, wenn eine höhere Geschwindigkeit durch Parallelität erforderlich ist.
In Ihrem Fall suchen Sie natürlich nach einer Art Selbstverknüpfung, dh nach allen Werten, die sich in einiger Entfernung voneinander befinden. Der gleiche Ansatz funktioniert IMHO immer noch, obwohl Sie von einem Startpunkt aus für Permutationen (unter Verwendung von Dateien oder Listen), die den Startblock gemeinsam nutzen, nach oben und unten expandieren und die Hamming-Distanz für den resultierenden Cluster berechnen müssen.
Wenn der Speicher anstelle von Dateien im Speicher ausgeführt wird, liegt Ihr 100-MB-32-Bit-String-Datensatz im Bereich von 4 GB. Daher benötigen die vier permutierten Listen möglicherweise mehr als 16 GB RAM. Ich erhalte jedoch hervorragende Ergebnisse mit Dateien mit Speicherzuordnung und muss weniger RAM für Datensätze ähnlicher Größe benötigen.
Es sind Open Source-Implementierungen verfügbar. Das Beste im Raum ist IMHO das für Mozh von Moz , C ++, aber für 64-Bit-Strings und nicht für 32-Bit-Zeichenfolgen.
Diese beschränkte Happing Abstand Ansatz wurde zuerst AFAIK durch beschrieben Moses Charikar in seiner „simhash“ Samen Papier und dem entsprechenden Google - Patent :
Monika Henziger hat dies in ihrem Artikel "Suche nach nahezu doppelten Webseiten: eine umfassende Bewertung von Algorithmen" erweitert :
Dies wird auch in der Arbeit von Gurmeet Singh Manku, Arvind Jain und Anish Das Sarma in der Veröffentlichung von Fast -Duplikaten für das Web- Crawlen erläutert :
Hinweis: Ich habe eine ähnliche Antwort auf eine verwandte Nur-DB-Frage gepostet
quelle
Sie können jede mögliche Variation Ihrer ursprünglichen Liste innerhalb des angegebenen Hamming-Abstands vorberechnen und in einem Bloom-Filter speichern. Dies gibt Ihnen ein schnelles "NEIN", aber nicht unbedingt eine klare Antwort auf "JA".
Speichern Sie für JA eine Liste aller Originalwerte, die jeder Position im Bloom-Filter zugeordnet sind, und gehen Sie sie einzeln durch. Optimieren Sie die Größe Ihres Bloom-Filters, um Kompromisse zwischen Geschwindigkeit und Speicher einzugehen.
Ich bin mir nicht sicher, ob alles genau funktioniert, aber es scheint ein guter Ansatz zu sein, wenn Sie Laufzeit-RAM zum Brennen haben und bereit sind, sehr viel Zeit für die Vorberechnung aufzuwenden.
quelle
Wie wäre es, wenn Sie die Liste sortieren und dann in dieser sortierten Liste eine binäre Suche nach den verschiedenen möglichen Werten in Ihrer Hamming-Entfernung durchführen?
quelle
Ein möglicher Ansatz zur Lösung dieses Problems ist die Verwendung einer Disjoint-Set-Datenstruktur . Die Idee ist, Listenmitglieder mit Hamming-Abstand <= k in derselben Menge zusammenzuführen. Hier ist der Umriss des Algorithmus:
Für jedes Mitglied der Liste alle möglichen berechnen Wert mit Hamming - Distanz <= k. Für k = 1 gibt es 32 Werte (für 32-Bit-Werte). Für k = 2 sind 32 + 32 * 31/2 Werte.
Testen Sie für jeden berechneten Wert , ob er in der ursprünglichen Eingabe enthalten ist. Sie können ein Array mit der Größe 2 ^ 32 oder eine Hash-Map verwenden, um diese Prüfung durchzuführen.
Wenn sich der Wert in der ursprünglichen Eingabe befindet, führen Sie eine "Vereinigungs" -Operation mit dem Listenmitglied durch .
Sie starten den Algorithmus mit N disjunkten Mengen (wobei N die Anzahl der Elemente in der Eingabe ist). Jedes Mal, wenn Sie eine Vereinigungsoperation ausführen, verringern Sie die Anzahl der disjunkten Sätze um 1. Wenn der Algorithmus beendet wird, werden in der Datenstruktur des disjunkten Satzes alle Werte mit dem Hamming-Abstand <= k in disjunkten Sätzen gruppiert. Diese disjunkte Datenstruktur kann in nahezu linearer Zeit berechnet werden .
quelle
Hier ist eine einfache Idee: Führen Sie eine byteweise Radix-Sortierung der 100-m-Eingabe-Ganzzahlen durch, wobei das höchstwertige Byte zuerst angezeigt wird, und verfolgen Sie die Bucket-Grenzen auf den ersten drei Ebenen in einer externen Struktur.
Beginnen Sie zum Abfragen mit einem Entfernungsbudget von
d
und Ihrem Eingabewortw
.b
Berechnen Sie für jeden Bucket in der obersten Ebene mit Bytewert den Hamming-Abstandd_0
zwischenb
und das High-Byte vonw
. Durchsuchen Sie diesen Bucket rekursiv mit einem Budget vond - d_0
: Das heißt, für jeden Bytewertb'
seid_1
der Hamming-Abstand zwischenb'
und das zweite Byte vonw
. Suchen Sie rekursiv in der dritten Ebene mit einem Budget vond - d_0 - d_1
usw.Beachten Sie, dass die Eimer einen Baum bilden. Wenn Ihr Budget negativ wird, hören Sie auf, diesen Teilbaum zu durchsuchen. Wenn Sie rekursiv in ein Blatt absteigen, ohne Ihr Entfernungsbudget zu sprengen, sollte dieser Blattwert Teil der Ausgabe sein.
Hier ist eine Möglichkeit, die externe Bucket-Grenzstruktur darzustellen: Haben Sie ein Array mit der Länge 16_777_216 (
= (2**8)**3 = 2**24
), wobei das Element am Indexi
der Startindex des Buckets ist, der Werte im Bereich [256 * i, 256 * i + 255] enthält. Um den Index eins jenseits des Endes dieses Buckets zu finden, schauen Sie nach Index i + 1 (oder verwenden Sie das Ende des Arrays für i + 1 = 2 ** 24).Das Speicherbudget beträgt 100 m * 4 Bytes pro Wort = 400 MB für die Eingänge und 2 ** 24 * 4 Bytes pro Adresse = 64 MiB für die Indexierungsstruktur oder insgesamt nur knapp einen halben Gig. Die Indexierungsstruktur ist ein Overhead von 6,25% für die Rohdaten. Sobald Sie die Indexierungsstruktur erstellt haben, müssen Sie natürlich nur das niedrigste Byte jedes Eingabeworts speichern, da die anderen drei im Index implizit in der Indexierungsstruktur enthalten sind, und zwar für insgesamt ~ (64 + 50) MB.
Wenn Ihre Eingabe nicht gleichmäßig verteilt ist, können Sie die Bits Ihrer Eingabewörter mit einer (einzelnen, universell geteilten) Permutation permutieren, die die gesamte Entropie zum oberen Rand des Baums bringt. Auf diese Weise werden durch die erste Bereinigungsstufe größere Teile des Suchraums entfernt.
Ich habe einige Experimente ausprobiert, und dies funktioniert ungefähr so gut wie die lineare Suche, manchmal sogar noch schlimmer. Soviel zu dieser ausgefallenen Idee. Na ja, zumindest ist es speichereffizient.
quelle