Einführung
Die Bedeutung der natürlichen Sprachverarbeitung (NLP) hat in den letzten Jahren, insbesondere durch moderne große Sprachmodelle, erheblich zugenommen. NLP-Techniken und -Technologien sind heute in zahlreichen Anwendungen zu finden, darunter Suchmaschinen, Chatbots und automatisierte Kundenservice-Routing-Systeme. Im Bereich der produktionsreifen NLP-Anwendungen in Python gilt spaCy als der unbestrittene Branchenstandard. spaCy wurde speziell für den produktiven Einsatz entwickelt und bietet eine industrielle Geschwindigkeit, vortrainierte statistische und transformerbasierte Modelle sowie eine benutzerfreundliche API.
Leider behandeln viele Entwickler spaCy als eine einfache Black-Box-Lösung. Sie laden ein Modell, wenden es auf Text an und akzeptieren die standardmäßigen Verarbeitungsgeschwindigkeiten und Extraktionsgrenzen. Wenn es darum geht, von einem lokalen Prototyp auf die Verarbeitung von Millionen von Dokumenten zu skalieren, können diese Standardkonfigurationen zu Rechenengpässen führen, die zu Verzögerungen, erhöhtem Speicherbedarf und verpassten domänenspezifischen Entitäten führen. Um leistungsstarke Textverarbeitungspipelines zu erstellen, ist es notwendig, die interne Ausführungslogik von spaCy zu optimieren.
In diesem Artikel werden drei wesentliche Tipps für spaCy vorgestellt, die jeder Entwickler in seinem Werkzeugkasten haben sollte, um die Verarbeitungsgeschwindigkeit zu maximieren und die Entitätserkennung anzupassen: selektives Laden von Pipelines, parallele Batch-Verarbeitung und hybride regelbasierte statistische Entitätserkennung.
1. Selektives Laden von Pipelines und Deaktivierung von Komponenten
Standardmäßig initialisiert spaCy beim Laden eines vortrainierten Modells (wie en_core_web_sm) eine vollständige NLP-Pipeline. Diese Pipeline umfasst typischerweise:
- einen Tokenizer
- einen Part-of-Speech-Tagger
- einen Abhängigkeitsparser
- einen Lemmatisierer
- einen Attributregler
- einen Named Entity Recognizer
Obwohl dieses vollständige Standardangebot an Funktionen ausgezeichnet ist, bringt es erhebliche Rechenkosten mit sich. Wenn Ihre Anwendung nur die benannte Entitätserkennung (NER) benötigt, ist das Ausführen des Abhängigkeitsparsers und des Lemmatisierers eine Verschwendung von CPU-Zyklen und Speicher. Umgekehrt ist es ineffizient, das tiefgreifende statistische NER-Modell zu verwenden, wenn Sie lediglich Text bereinigen und Lemmas extrahieren möchten. Sie können dies optimieren, indem Sie Komponenten beim Laden selektiv ausschließen oder sie während der Ausführung mithilfe eines Kontextmanagers vorübergehend deaktivieren.
Ein naiver Ansatz lädt und führt jede Standardkomponente auf dem Text aus, unabhängig davon, ob die Ausgaben der Komponenten tatsächlich verwendet werden:
import spacy\nimport time\n# Lade das kleine englische Modell\nnlp = spacy.load("en_core_web_sm")\ntexts = ["Apple is looking at buying U.K. startup for $1 billion"] * 1000\n# Naive Ausführung: führt Tagger, Parser, Lemmatisierer und NER auf jedem Dokument aus\n# Angenommen, wir interessieren uns hier nur für benannte Entitäten\nstart_time = time.time()\nfor text in texts:\n doc = nlp(text)\n entities = [(ent.text, ent.label_) for ent in doc.ents]\nduration_full = time.time() - start_time\nprint(f"Vollständige Pipeline hat 1.000 Dokumente in: {duration_full:.4f} Sekunden verarbeitet")
Die Ausgabe zeigt:
Vollständige Pipeline hat 1.000 Dokumente in: 2.8540 Sekunden verarbeitet
Nun optimieren wir die Ausführung auf zwei spezifische Arten. Zunächst werden wir schwere, ungenutzte Komponenten wie den Abhängigkeitsparser beim Laden ausschließen. Zweitens verwenden wir nlp.select_pipes(), um Komponenten bei der Verarbeitung spezifischer Arbeitslasten vorübergehend zu deaktivieren.
import spacy\nimport time\n# Ladezeitoptimierung: Schließe den schweren Parser und Tagger von Anfang an aus\n# Dies reduziert die Initialisierungszeit und den Speicherbedarf\nnlp_optimized = spacy.load("en_core_web_sm", exclude=["parser", "tagger"])\ntexts = ["Apple is looking at buying U.K. startup for $1 billion"] * 1000\n# Kontextmanager-Optimierung, Komponenten vorübergehend deaktivieren\n# Wir haben den Parser und Tagger vollständig ausgeschlossen, hier deaktivieren wir den Attributregler und Lemmatisierer\nstart_time = time.time()\nwith nlp_optimized.select_pipes(disable=["attribute_ruler", "lemmatizer"]):\n for text in texts:\n doc = nlp_optimized(text)\n entities = [(ent.text, ent.label_) for ent in doc.ents]\nduration_opt = time.time() - start_time\nprint(f"Optimierte Pipeline hat 1.000 Dokumente in: {duration_opt:.4f} Sekunden verarbeitet")\nprint(f"Geschwindigkeit: {duration_full / duration_opt:.2f}x schneller!")
Vergleichen wir die Laufzeiten:
Vollständige Pipeline hat 1.000 Dokumente in: 2.8739 Sekunden verarbeitet\nOptimierte Pipeline hat 1.000 Dokumente in: 1.7859 Sekunden verarbeitet\nGeschwindigkeit: 1.61x schneller!
Im optimierten Beispiel verhindert das Übergeben von exclude=[„parser“, „tagger“] an spacy.load(), dass diese Komponenten in den Speicher geladen werden. In einer alternativen Methode, um im Wesentlichen dasselbe Ergebnis zu erzielen, haben wir disable=[„attribute_ruler“, „lemmatizer“] übergeben, um deren Verarbeitung vorübergehend zu deaktivieren. Der Effekt ist, dass spaCy bei der Verarbeitung des Textes die Analyse der Tokenabhängigkeiten und die Part-of-Speech-Tag-Beschriftung überspringt, was mathematisch aufwendig ist, und direkt zur Entitätserkennung übergeht. Dies führt zu einer spürbaren Geschwindigkeitssteigerung ohne Auswirkungen auf die NER-Genauigkeit, mit noch deutlicheren Vorteilen bei größerem Umfang.
2. Hochdurchsatz-Batch-Verarbeitung mit nlp.pipe und Metadatenweitergabe
Wenn Sie über einen großen Korpus iterieren (z. B. pandas DataFrames, Datenbankzeilen oder Rohtextdateien), ist es ein Anti-Muster, das nlp-Objekt in einer Schleife auf einzelnen Zeichenfolgen aufzurufen (z. B. [nlp(text) for text in texts]).
Die sequenzielle Verarbeitung hindert spaCy daran, Speicherpuffer zu optimieren, Operationen zu gruppieren und die Parallelisierung über mehrere Kerne zu nutzen. Zudem müssen Sie beim Verarbeiten von Texten für die Datenbankspeicherung oder ETL-Pipelines häufig Metadaten (wie eine Datensatz-ID, einen Zeitstempel oder eine Kategorie) durch den NLP-Prozess mitführen, um die resultierenden Entitäten den richtigen Datenbankzeilen zuordnen zu können.
Die Lösung besteht darin, nlp.pipe() zu verwenden. Diese Methode verarbeitet Dokumente als Stream, puffert sie intern und unterstützt die Mehrprozessverarbeitung. Durch das Setzen von as_tuples=True können Sie spaCy Tupel von (Text, Kontext) übergeben. Es wird (doc, Kontext)-Paare zurückgegeben, sodass Sie Metadaten direkt durch die Pipeline weitergeben können.
Ein naiver Ansatz führt die Verarbeitung sequenziell aus und verwendet manuelles Index-Tracking, um die resultierenden Dokumente mit ihren Datenbank-IDs abzugleichen, was anfällig für Fehler und langsam ist:
import spacy\nimport time\nnlp = spacy.load("en_core_web_sm", exclude=["parser", "tagger"])\n# Rohdatenbankeinträge mit eindeutigen IDs\nrecords = [\n {"id": f"DB-REC-{i}", "text": "Google wurde im September 1998 von Larry Page und Sergey Brin gegründet."}\n for i in range(1000)\n]\n# Sequenzielle Schleife: langsam und manuell verwaltete Metadaten\nstart_time = time.time()\nextracted_data = []\nfor i, record in enumerate(records):\n doc = nlp(record["text"])\n entities = [(ent.text, ent.label_) for ent in doc.ents]\n extracted_data.append({\n "id": record["id"],\n "entities": entities\n })\nduration_seq = time.time() - start_time\nprint(f"Sequenzielle Schleife hat 1.000 Dokumente in: {duration_seq:.4f} Sekunden verarbeitet")
Die Ausgabe zeigt:
Sequenzielle Schleife hat 1.000 Dokumente in: 2.7375 Sekunden verarbeitet
Hier streamen wir die Daten mit nlp.pipe, nutzen die Batch-Verarbeitung und die Parallelisierung über mehrere Kerne (n_process), während wir die Datenbank-ID als Kontextvariable mitführen:
import spacy\nimport time\n# Halten Sie Ihre Importe und Definitionen global, damit Kindprozesse sie sehen können\nnlp = spacy.load("en_core_web_sm", exclude=["parser", "tagger"])\n# Wickeln Sie den tatsächlichen Ausführungscode in den Hauptblock ein\nif __name__ == '__main__':\n records = [\n {"id": f"DB-REC-{i}", "text": "Google wurde im September 1998 von Larry Page und Sergey Brin gegründet."}\n for i in range(1000)\n ]\n start_time = time.time()\n # Formatieren Sie die Eingabe als Liste von (Text, Kontext)-Tupeln\nstream_input = [(rec["text"], rec["id"]) for rec in records]\n # Streamen Sie Batches und verwenden Sie alle verfügbaren CPU-Kerne mit n_process=-1\nextracted_data_pipe = []\ndocs_stream = nlp.pipe(stream_input, as_tuples=True, batch_size=256, n_process=-1)\nfor doc, rec_id in docs_stream:\n entities = [(ent.text, ent.label_) for ent in doc.ents]\n extracted_data_pipe.append({\n "id": rec_id,\n "entities": entities\n })\nduration_pipe = time.time() - start_time\nprint(f"nlp.pipe hat 1.000 Dokumente in: {duration_pipe:.4f} Sekunden verarbeitet")\nprint(f"Geschwindigkeit: {duration_seq / duration_pipe:.2f}x schneller!")
Die Ausgabe zeigt:
nlp.pipe hat 1.000 Dokumente in: 7.1310 Sekunden verarbeitet
Im optimierten Code-Snippet strukturieren wir den Eingabedatensatz in eine Sequenz von Tupeln um: (text_string, metadata_context). Bei der Verwendung von nlp.pipe(stream_input, as_tuples=True, batch_size=256, n_process=-1):
- batch_size=256 weist spaCy an, Texte in Gruppen von 256 zu puffern und zu verarbeiten, wodurch der interne Python-Schleifenüberhead minimiert wird.
- n_process=-1 weist spaCy an, die Anzahl der CPU-Kerne automatisch zu erkennen und die Tokenisierung sowie die Komponentenextraktion über alle verfügbaren Kerne zu parallelisieren.
- as_tuples=True weist spaCy an, Paare von (doc, Kontext) zurückzugeben, sodass die Metadaten (die Datensatz-ID) perfekt mit dem verarbeiteten Dokument übereinstimmen, ohne dass manuelle Index-Arrays oder Listenabgleichscode erforderlich sind.
Der aufmerksame Leser wird bemerken, dass die Verarbeitungszeit für den parallelen Batch-Verarbeitungs-Code tatsächlich länger ist als die seines Vorgängers. Dies liegt jedoch an den Kosten, die mit der Einrichtung des parallelen Jobs verbunden sind, und die Einsparungen werden deutlich, wenn die Anzahl der zu verarbeitenden Dokumente steigt.
Wenn wir die gleichen Codeausschnitte jedoch mit 10.000 Datensätzen anstelle von 1.000 erneut ausführen, ergeben sich folgende Ergebnisse:
Sequenzielle Schleife hat 10.000 Dokumente in: 276.7330 Sekunden verarbeitet\nnlp.pipe hat 10.000 Dokumente in: 115.4440 Sekunden verarbeitet
Sie können sehen, wie sich die Einsparungen weiter summieren würden.
3. Hybride benannte Entitätserkennung mit EntityRuler
Vortrainierte statistische und transformerbasierte NER-Modelle sind äußerst leistungsfähig, wenn es darum geht, allgemeine Entitätstypen wie ORG, PERSON oder DATE basierend auf dem Kontext zu erkennen. Allerdings können Modelle häufig versagen, domänenspezifische Begriffe (wie benutzerdefinierte Produkt-SKUs, Legacy-Code-IDs oder hochspezialisierte medizinische Begriffe) zu erkennen, da sie während des Trainings nicht damit konfrontiert wurden.
Das Feintuning eines tiefen Lernmodells auf benutzerdefinierte Entitäten ist eine Lösung, erfordert jedoch das Labeln von Tausenden von Sätzen und birgt das Risiko des „katastrophalen Vergessens“, bei dem das Modell vergisst, wie man Standardentitäten erkennt.
Eine sauberere, hocheffiziente Lösung ist ein hybrider NER-Ansatz unter Verwendung des EntityRuler von spaCy. Der EntityRuler ermöglicht es Ihnen, Muster (mithilfe von regulären Ausdrücken oder tokenbasierten Wörterbüchern) zu definieren und sie direkt in Ihre Pipeline einzufügen. Sie können ihn vor dem statistischen NER hinzufügen, um deterministische Entitäten vorzutaggen und dem Modell bei der Kontextentscheidung zu helfen, oder danach, um als Fallback oder Überschreibung zu fungieren.
Entwickler versuchen oft, Lücken in der statistischen NER zu schließen, indem sie Regex auf den Text anwenden, nachdem sie die spaCy-Pipeline durchlaufen haben, was komplexe Koordinatenoffset-Mathematik und getrennte Datenstrukturen erfordert:
import spacy\nimport re\nnlp = spacy.load("en_core_web_sm")\ntext = "Bitte überprüfen Sie das Systemticket-ID: TKT-98421 auf unserem Unternehmensportal."\ndoc = nlp(text)\n# Standardstatistische NER übersieht benutzerdefinierte Ticket-IDs\nentities = [(ent.text, ent.label_) for ent in doc.ents]\nprint("Vor der Nachbearbeitung:", entities)\n# Nachbearbeitungsregex-Patch\nticket_pattern = r"TKT-\\d+"\nmatches = re.finditer(ticket_pattern, text)\ncustom_ents = []\nfor match in matches:\n # Erfordert komplexe Zeichen-zu-Token-Offset-Konvertierung, um Spannen zu erstellen\n custom_ents.append((match.group(), "TICKET_ID"))\n# Wir haben jetzt zwei getrennte Listen von Entitäten, die manuell zusammengeführt werden müssen\nprint("Regex-Entitäten:", custom_ents)
Die Ausgabe zeigt:
Vor der Nachbearbeitung: []\nRegex-Entitäten: [('TKT-98421', 'TICKET_ID')]
Durch das Hinzufügen einer EntityRuler-Komponente direkt in die Pipeline kombinieren wir regelbasierte Regex-Muster und statistische Analysen in einer einzigen, einheitlichen doc.ents-Ausgabe:
import spacy\nnlp = spacy.load("en_core_web_sm")\n# Fügen Sie die entity_ruler-Komponente der Pipeline vor ner hinzu, damit sie Entitäten vorab taggt, aber auch danach funktioniert\nruler = nlp.add_pipe("entity_ruler", before="ner")\n# Definieren Sie tokenbasierte Muster, einschließlich regulärer Ausdrücke\npatterns = [\n # Übereinstimmung von Zeichenfolgen, die mit "TKT-" beginnen, gefolgt von Ziffern\n {"label": "TICKET_ID", "pattern": [{"TEXT": {"REGEX": "^TKT-\\d+$"}}]},\n # Übereinstimmung spezifischer Domainphrasen genau\n {"label": "ORG", "pattern": "corporate portal"}\n]\nruler.add_patterns(patterns)\ntext = "Bitte überprüfen Sie das Systemticket-ID: TKT-98421 auf unserem Unternehmensportal."\ndoc = nlp(text)\n# Sowohl statistische als auch regelbasierte Entitäten werden in doc.ents konsolidiert\nfor ent in doc.ents:\n print(f"Entität: {ent.text} | Label: {ent.label_}")
Die Ausgabe zeigt:
Entität: TKT-98421 | Label: TICKET_ID\nEntität: corporate portal | Label: ORG
In dieser hybriden Implementierung rufen wir nlp.add_pipe(„entity_ruler“, before=“ner“) auf. Der EntityRuler fungiert als native Pipeline-Komponente. Wenn der Text verarbeitet wird:
- Der Tokenizer zerlegt den Satz in Tokens.
- Der EntityRuler läuft zuerst und identifiziert Tokens, die unserem Ticket-Regex-Muster oder genauen Wörterbuchstrings entsprechen, und taggt sie als TICKET_ID oder ORG.
- Die statistische NER-Komponente läuft anschließend. Da sie sieht, dass diese Tokens bereits als Entitäten getaggt sind, respektiert sie die Tags (oder passt ihre Vorhersagen entsprechend an, um Konflikte zu vermeiden).
Dies stellt
Quellen: kdnuggets
Bildquelle: KI generiert