Foto von real-napster auf Pixabay
In einem meiner letzten Projekte musste ich ein semantisches Suchsystem aufbauen, das mit hoher Leistung skalierbar ist und Echtzeit-Antworten für Berichtssuchen liefert. Um dies zu erreichen, haben wir PostgreSQL mit pgvector auf AWS RDS in Kombination mit AWS Lambda verwendet. Die Herausforderung bestand darin, den Benutzern die Suche mit Abfragen in natürlicher Sprache zu ermöglichen, anstatt sich auf starre Schlüsselwörter zu verlassen, und gleichzeitig sicherzustellen, dass die Antworten weniger als 1–2 Sekunden oder sogar weniger dauerten und nur CPU-Ressourcen nutzen konnten.
In diesem Beitrag werde ich die Schritte durchgehen, die ich zum Aufbau dieses Suchsystems unternommen habe, vom Abrufen bis zum Neuranking, und die Optimierungen, die mit OpenVINO und intelligentem Batching für die Tokenisierung vorgenommen wurden.
Moderne, hochmoderne Suchsysteme bestehen in der Regel aus zwei Hauptschritten: Retrieval und Reranking.
1) Abruf: Der erste Schritt besteht darin, eine Teilmenge relevanter Dokumente basierend auf der Benutzerabfrage abzurufen. Dies kann mithilfe vorab trainierter Einbettungsmodelle erfolgen, z. B. der kleinen und großen Einbettungen von OpenAI, der Einbettungsmodelle von Cohere oder der mxbai-Einbettungen von Mixbread. Der Abruf konzentriert sich auf die Eingrenzung des Dokumentenpools durch Messung ihrer Ähnlichkeit mit der Abfrage.
Hier ist ein vereinfachtes Beispiel, bei dem die Satztransformatoren-Bibliothek von Huggingface zum Abrufen verwendet wird, eine meiner Lieblingsbibliotheken hierfür:
from sentence_transformers import SentenceTransformer import numpy as np # Load a pre-trained sentence transformer model model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2") # Sample query and documents (vectorize the query and the documents) query = "How do I fix a broken landing gear?" documents = ["Report 1 on landing gear failure", "Report 2 on engine problems"] # Get embeddings for query and documents query_embedding = model.encode(query) document_embeddings = model.encode(documents) # Calculate cosine similarity between query and documents similarities = np.dot(document_embeddings, query_embedding) # Retrieve top-k most relevant documents top_k = np.argsort(similarities)[-5:] print("Top 5 documents:", [documents[i] for i in top_k])
2) Neues Ranking: Sobald die relevantesten Dokumente abgerufen wurden, verbessern wir das Ranking dieser Dokumente mithilfe eines Cross-Encoder-Modells weiter. In diesem Schritt wird jedes Dokument in Bezug auf die Abfrage noch genauer bewertet, wobei der Schwerpunkt auf einem tieferen Kontextverständnis liegt.
Eine Neubewertung ist von Vorteil, da sie eine zusätzliche Verfeinerungsebene hinzufügt, indem die Relevanz jedes Dokuments genauer bewertet wird.
Hier ist ein Codebeispiel für das Reranking mit Cross-Encoder/ms-marco-TinyBERT-L-2-v2, einem leichten Cross-Encoder:
from sentence_transformers import CrossEncoder # Load the cross-encoder model cross_encoder = CrossEncoder("cross-encoder/ms-marco-TinyBERT-L-2-v2") # Use the cross-encoder to rerank top-k retrieved documents query_document_pairs = [(query, doc) for doc in documents] scores = cross_encoder.predict(query_document_pairs) # Rank documents based on the new scores top_k_reranked = np.argsort(scores)[-5:] print("Top 5 reranked documents:", [documents[i] for i in top_k_reranked])
Während der Entwicklung habe ich festgestellt, dass die Tokenisierungs- und Vorhersagephasen ziemlich lange dauerten, wenn 1.000 Berichte mit Standardeinstellungen für Satztransformatoren verarbeitet wurden. Dies führte zu einem Leistungsengpass, insbesondere da wir Antworten in Echtzeit anstrebten.
Unten habe ich meinen Code mit SnakeViz profiliert, um die Leistungen zu visualisieren:
Wie Sie sehen, sind die Tokenisierungs- und Vorhersageschritte unverhältnismäßig langsam, was zu erheblichen Verzögerungen bei der Bereitstellung von Suchergebnissen führt. Insgesamt dauerte es durchschnittlich 4-5 Sekunden. Dies liegt daran, dass es zwischen den Tokenisierungs- und Vorhersageschritten Blockierungsoperationen gibt. Wenn wir auch andere Vorgänge wie Datenbankaufrufe, Filter usw. zusammenzählen, kamen wir locker auf insgesamt 8-9 Sekunden.
Die Frage, mit der ich konfrontiert wurde, war: Können wir es schneller machen? Die Antwort lautet: Ja, indem wir OpenVINO nutzen, ein optimiertes Backend für CPU-Inferenz. OpenVINO trägt dazu bei, die Deep-Learning-Modellinferenz auf Intel-Hardware zu beschleunigen, die wir auf AWS Lambda verwenden.
Codebeispiel für OpenVINO-Optimierung
So habe ich OpenVINO in das Suchsystem integriert, um die Inferenz zu beschleunigen:
from sentence_transformers import SentenceTransformer import numpy as np # Load a pre-trained sentence transformer model model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2") # Sample query and documents (vectorize the query and the documents) query = "How do I fix a broken landing gear?" documents = ["Report 1 on landing gear failure", "Report 2 on engine problems"] # Get embeddings for query and documents query_embedding = model.encode(query) document_embeddings = model.encode(documents) # Calculate cosine similarity between query and documents similarities = np.dot(document_embeddings, query_embedding) # Retrieve top-k most relevant documents top_k = np.argsort(similarities)[-5:] print("Top 5 documents:", [documents[i] for i in top_k])
Mit diesem Ansatz könnten wir eine 2-3-fache Beschleunigung erreichen und die ursprünglichen 4-5 Sekunden auf 1-2 Sekunden reduzieren. Der vollständige Arbeitscode ist auf Github.
Ein weiterer entscheidender Faktor zur Verbesserung der Leistung war die Optimierung des Tokenisierungsvorgangs und die Anpassung der Stapelgröße und der Token-Länge. Durch Erhöhen der Batch-Größe (batch_size=16) und Reduzieren der Token-Länge (max_length=512) konnten wir die Tokenisierung parallelisieren und den Overhead sich wiederholender Vorgänge reduzieren. In unseren Experimenten haben wir herausgefunden, dass eine Batchgröße zwischen 16 und 64 gut funktioniert, wobei alles, was größer ist, die Leistung beeinträchtigt. Ebenso haben wir uns für eine maximale Länge von 128 entschieden, was sinnvoll ist, wenn die durchschnittliche Länge Ihrer Berichte relativ kurz ist. Mit diesen Änderungen haben wir insgesamt eine 8-fache Beschleunigung erreicht und die Reranking-Zeit auf unter 1 Sekunde reduziert, selbst auf der CPU.
In der Praxis bedeutete dies, mit verschiedenen Stapelgrößen und Token-Längen zu experimentieren, um das richtige Gleichgewicht zwischen Geschwindigkeit und Genauigkeit für Ihre Daten zu finden. Dadurch konnten wir deutliche Verbesserungen der Reaktionszeiten feststellen, sodass das Suchsystem auch bei 1.000 Berichten skalierbar ist.
Durch die Verwendung von OpenVINO und die Optimierung der Tokenisierung und Stapelverarbeitung konnten wir ein leistungsstarkes semantisches Suchsystem aufbauen, das Echtzeitanforderungen in einem reinen CPU-Setup erfüllt. Tatsächlich haben wir insgesamt eine 8-fache Beschleunigung erlebt. Die Kombination aus Retrieval mithilfe von Satztransformatoren und Reranking mit einem Cross-Encoder-Modell schafft ein leistungsstarkes, benutzerfreundliches Sucherlebnis.
Wenn Sie ähnliche Systeme mit Einschränkungen hinsichtlich Reaktionszeit und Rechenressourcen erstellen, empfehle ich Ihnen dringend, OpenVINO und intelligentes Batching auszuprobieren, um eine bessere Leistung zu erzielen.
Hoffentlich hat Ihnen dieser Artikel gefallen. Wenn Sie diesen Artikel nützlich fanden, geben Sie mir ein „Gefällt mir“, damit auch andere ihn finden und ihn mit Ihren Freunden teilen können. Folgen Sie mir auf Linkedin, um über meine Arbeit auf dem Laufenden zu bleiben. Danke fürs Lesen!
Das obige ist der detaillierte Inhalt vonAufbau eines schnellen und effizienten semantischen Suchsystems mit OpenVINO und Postgres. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!