Kapitel 4. Tokenisierung

Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com

Dies ist unser erstes Kapitel im Abschnitt NLP von Grund auf. In den ersten drei Kapiteln haben wir dich durch die wichtigsten Komponenten einer NLP-Pipeline geführt. Von hier an bis Kapitel 9 werden wir uns mit vielen Details beschäftigen, um wirklich zu verstehen, wie moderne NLP-Systeme funktionieren. Die wichtigsten Komponenten sind:

  • Tokenisierung

  • Einbettungen

  • Architekturen

Bisher wurden all diese Schritte in den von uns verwendeten Bibliotheken (spaCy, transformers und fastai) abstrahiert. Aber jetzt werden wir versuchen zu verstehen, wie diese Bibliotheken tatsächlich funktionieren und wie du deinen Code auf einer niedrigen Ebene verändern kannst, um erstaunliche NLP-Anwendungen zu erstellen, die über die einfachen Beispiele hinausgehen, die wir in diesem Buch vorgestellt haben.

Eines sei angemerkt: "Low-Level" ist ein subjektiver Begriff. Während die einen PyTorch als Low-Level Deep-Learning-Bibliothek bezeichnen, können andere diesen Begriff nur für die Erstellung eines benutzerdefinierten Speicherzuweisers in x86-Assembler verwenden. Das ist eine Frage des Blickwinkels. Was wir hier mit Low-Level meinen, ist, dass du nach dem Erlernen dieser Dinge genug Verständnis hast, um nützliche Anwendungen mit NLP in der realen Welt zu erstellen, und dass du auch in der Lage bist, die neueste Forschung auf diesem Gebiet zu verstehen und zu verfolgen. Wir werden keine Themen behandeln, die zu weit über den Rahmen von NLP hinausgehen. Zum Beispiel ist es sicherlich interessant und nützlich, mehr über zu erfahren, wie CUDA funktioniert, und wir werden das in Anhang B ein wenig tun. Aber CUDA selbst ist als Werkzeug für viele Dinge außerhalb des NLP nützlich, so dass wir das als über den Rahmen dieses Buches hinausgehend betrachten würden. Wir werden versuchen, uns so weit wie möglich auf Dinge zu konzentrieren, die die Leistung deiner Modelle in der Produktion tatsächlich verbessern.

Jedes der Elemente in der Liste, die wir gerade gesehen haben (d.h. Tokenizer, Einbettungen und Modelle), kann als unabhängige Funktion betrachtet werden. Sie nehmen eine Eingabe entgegen und erzeugen eine Ausgabe. Jede dieser Funktionen gibt dann ihre Ausgabe an die nächste Stufe der Pipeline weiter. Genauer gesagt, geben wir den tokenisierten Text an die Einbettungsschicht und die Einbettungen an das Modell weiter. Wenn du möchtest, kannst du diese Funktionen als Blackboxen betrachten und dich jeweils nur auf eine konzentrieren. Wir werden uns jede Funktion einzeln ansehen, zuerst die Tokenizer.

Ein minimaler Tokenizer

Wenn wir über die Low-Level-Teile des Deep-Learning-Stacks nachdenken, ist es sinnvoll, die Komponenten im Hinblick auf ihre Eingaben und Ausgaben zu verstehen.

Was sind hier die Eingaben und Ausgaben? Die Eingabe ist Text. Normalerweise wird dieser als .txt Datei oder etwas anderes bereitgestellt, das in ein Python-Objekt eingelesen wird. Die Ausgabe ist eine Folge von Token. Eines der Hauptthemen dieses Kapitels wird die Diskussion darüber sein, was genau ein "Token" ist und was es tun soll.

Wie immer ist eine der besten Möglichkeiten, etwas zu verstehen, sich den Code anzuschauen. Hier ist also, was ein Tokenizer ist:

text = open('example.txt', 'r').read()
words = text.split(" ")
tokens = {v: k for k, v in enumerate(words)}
tokens
{'The': 0,
 'quick': 1,
 'brown': 2,
 'fox': 3,
 'jumps': 4,
 'over': 5,
 'the': 6,
 'lazy': 7,
 'dog.': 8}

Ein Tokenizer liest Text ein und gibt eine Zuordnung zwischen Wörtern und Indizes zurück. Im Wesentlichen erstellt er ein Wörterbuch (sowohl im übertragenen als auch im wörtlichen Sinne, da das vorangegangene Beispiel ein Python-Wörterbuch erstellt), das Wörter auf Zahlen abbildet. Das ist äußerst nützlich, denn jetzt haben wir eine Darstellung des Ausgangstextes, die in ein NLP-Modell eingespeist werden kann:

token_map = map(lambda t: tokens[t], words)
list(token_map)
[0, 1, 2, 3, 4, 5, 6, 7, 8]

Das war natürlich ein drastisch vereinfachtes Beispiel. In der Praxis würdest du die Tokenisierung niemals auf diese Weise durchführen wollen: Sie ist zum einen langsam und berücksichtigt zum anderen nicht viele Feinheiten der verschiedenen Sprachen. Außerdem berücksichtigt dieser einfache Tokenisierer weder Interpunktion noch Grammatik oder zusammengesetzte Wortstrukturen (z. B. die Tatsache, dass Wörter, die auf "-ing", "-ify" usw. enden, miteinander verwandt sind) auf sinnvolle Weise. Nichtsdestotrotz ist es ein Anfang.

Hier ist eine präzisere Art, zu sagen, was ein Tokenizer sein sollte: Ein Tokenizer ist ein Programm, das eine Folge von Zeichen in eine Folge von Token umwandelt. Tokenizer sind als allgemeines Werkzeug auch außerhalb von NLP sehr nützlich. Überall, wo Text geparst werden muss, gibt es wahrscheinlich irgendeine Form von Tokenizer. Nehmen wir ein Beispiel aus der Welt der Compiler, denn es zeigt sich, dass die Tokenisierung eine sehr alte, grundlegende und nützliche Funktion ist.

Tipp

So nützlich, dass es in den 80er Jahren populäre Tools wie lex und flex gab, die den C-Code für einen schnellen Tokenizer generierten, wenn man das zu analysierende Token einfach beschrieb!

Bei der Erstellung eines Compilers für eine Programmiersprache müssen zunächst Schlüsselwörter wie if und for identifiziert und markiert werden, um sie an die nächste Stufe weiterzugeben. Hier liest der Tokenizer eine Datei ein und erstellt eine neue Darstellung des Quellcodes, in der die rohen ASCII/Unicode-Zeichen durch Token ersetzt werden, die diese Schlüsselwörter repräsentieren, die dann zum Aufbau einer Datenstruktur, dem sogenannten Parse-Baum, verwendet werden können.

Wir bauen hier keinen Compiler, also ist der Parse-Baum nicht ganz so wichtig, und in der Praxis werden wir eher Bibliotheken als komplexe Codegenerierungsverfahren verwenden. Aber wir wollten ein Beispiel dafür geben, dass ein Tokenizer ein sehr nützliches und robustes Programm ist, das auch außerhalb des NLP-Bereichs eingesetzt werden kann.

Die Tokenizer, an denen wir als Deep-Learning-Experten interessiert sind, liefern uns normalerweise keine Parse-Trees. Was wir wollen, ist ein Tokenizer, der den Text liest und eine Folge von One-Hot-Vektoren erzeugt.

Das ist das Wichtigste, was aus unserer Top-Down-Perspektive über Tokenizer wissen muss. Die Eingabe ist roher Text und die Ausgabe ist eine Folge von Vektoren. Genauer gesagt, sind die Vektoren in unserem Fall einfach PyTorch-Tensoren, die wir an einenn.Embedding Schicht weitergeben. Sobald wir so weit sind, dass wir etwas an eine Einbettungsschicht weitergeben können (die wir im nächsten Kapitel besprechen werden), sind wir mit der Tokenisierung fertig.

Nachdem wir nun die Ein- und Ausgänge verstanden haben, können wir uns direkt an die Umsetzung machen. Danach werden wir uns einige der neuen Ideen in diesem Bereich ansehen und sie im Detail untersuchen.

Unserer Meinung nach gibt es zwei Tools zur Tokenisierung, die den meisten anderen überlegen sind - der Tokenizer von SpaCy und die Bibliothek Hugging Face tokenizers.

Der Tokenizer von spaCy wird häufiger verwendet, ist älter und etwas zuverlässiger. Er hat einen eigenen Algorithmus zur Tokenisierung, der für gängige NLP-Aufgaben gut geeignet ist. Die tokenizers Bibliothek ist ein etwas moderneres Paket, das sich auf die Implementierung der neuesten Algorithmen aus der neuesten Forschung konzentriert.

Warnung

Einige Modelle wie BERT erwarten bestimmte Token, so dass du nicht jeden beliebigen Tokenizer für diese Modelle verwenden kannst. Um dies zu umgehen, enthalten neuere Versionen von spaCy Wrapper für die Hugging Face transformers Bibliothek, mit der du den Rest deines spaCy-Workflows mit Transformatoren kombinieren kannst. Hinter den Kulissen wird aber immer noch der BERT-Tokenizer und nicht der spaCy-Tokenizer verwendet.

Wir haben spaCy bereits in den Kapiteln 1 und 3 verwendet und werden es in Teil III wieder aufgreifen, wenn wir das Modell einsetzen. In diesem Kapitel werden wir uns also auf die Bibliothek Hugging Face tokenizers konzentrieren.

Gesicht umarmen Tokenizer

tokenizers ist Hugging Face's offizielles Tokenisierungswerkzeug, das in der Programmiersprache Rust geschrieben wurde (die zum Zeitpunkt des Schreibens zufällig Ajays Lieblings-Programmiersprache ist), mit Bindungen zu Python und JavaScript. Auch wenn tokenizers als universeller Tokenizer verwendet werden kann, wurde es von Hugging Face speziell für Deep Learning und NLP entwickelt.

Im Gegensatz zu anderen Teilen der Deep-Learning-Pipeline wird die Tokenisierung normalerweise von der CPU ausgeführt. Das bedeutet aber nicht, dass sie langsam sein muss! Die Bibliothek von Hugging Face nutzt die vielen Kerne deines Rechners gut aus und kann große Datensätze im Gigabyte-Bereich (was für nicht-akademisches NLP ziemlich viel ist) in weniger als einer Minute tokenisieren.

Die Bibliothek tokenizers unterteilt die Aufgabe der Tokenisierung in kleinere, überschaubare Schritte. Hier ist die Beschreibung der Komponenten des Tokenisierungsprozesses in der Bibliothek von Hugging Face:

Normalisierer

Führt alle anfänglichen Transformationen über den ursprünglichen Eingabe-String aus. Wenn du z.B. einen Text klein schreiben, ihn entfernen oder einen der üblichen Unicode-Normalisierungsprozesse anwenden willst, fügst du einen Normalizer hinzu.

PreTokenizer

Verantwortlich für die Aufteilung der ursprünglichen Eingabezeichenkette. Das ist die Komponente, die entscheidet, wo und wie die Zeichenkette origin vorsegmentiert wird. Das einfachste Beispiel wäre, wie wir bereits gesehen haben, die Aufteilung nach Leerzeichen.

Modell

Kümmert sich um die Erkennung und Erstellung der Untertoken . Dieser Teil ist trainierbar und hängt von deinen Eingabedaten ab.

Post-Prozessor

Bietet erweiterte Konstruktionsfunktionen, um mit einigen der Transformer-basierten SOTA-Modelle kompatibel zu sein. Für BERT würde es zum Beispiel den tokenisierten Satz um [CLS]- und [SEP]-Tokens wickeln.

Dekodiere

Der Decoder ist dafür zuständig, eine tokenisierte Eingabe auf die ursprüngliche Zeichenkette abzubilden. Der Decoder wird in der Regel nach dem PreTokenizer ausgewählt, das wir zuvor verwendet haben.

Trainer

Bietet für jedes Modell die Möglichkeit zur Schulung .

Für jedes dieser logischen Module gibt es mehrere Optionen/Implementierungen in der Bibliothek:

Normalisierer

Kleinbuchstaben, Unicode (NFD, NFKD, NFC, NFKC), Bert, Strip...

PreTokenizer

ByteLevel, WhitespaceSplit, CharDelimiterSplit, Metaspace, ...

Modell

WordLevel, BPE, WordPiece, ...

Post-Prozessor

BertProcessor, ...

Decoder

WordLevel, BPE, WordPiece, ...

Bei der Auswahl der Komponenten hast du einen gewissen Spielraum, aber in den meisten Fällen bist du auf die Komponenten beschränkt, die von dem von dir verwendeten Modell unterstützt werden. In der Praxis wirst du das verwenden wollen, was in der Dokumentation für dein Modell vorgeschlagen wird, also empfehlen wir dir, sie durchzugehen, wenn du auf Fehler stößt.

Die Installation der Bibliothek ist so einfach wie das Ausführen des folgenden Befehls:

pip install tokenizers

Aber natürlich haben wir dies bereits in unseren Dateien requirements.txtund environment.yml im GitHub Repo vermerkt:

import tokenizers

Tokenisierung von Unterwörtern

Wenn du dich weiter auf durch die Dokumentation von tokenizers wühlst, wirst du feststellen, dass es viele verschiedene Algorithmen gibt, die in der Bibliothek implementiert sind. Aber die Tokenisierung scheint doch eine ziemlich einfache Aufgabe zu sein, oder? Woran liegt das?

Nun, es stellt sich heraus, dass es viele Möglichkeiten gibt, ein "Token" aus einer Textkette zu bilden.

Betrachten wir zum Beispiel die Zeichenketten "cat" und "cats". Eine gültige Untertokenisierung von "cats" wäre [cat, ##s], wobei das Doppel-Hashtag ein Präfix-Subtoken der ursprünglichen Eingabe darstellt. Der Vorteil dieses Ansatzes ist, dass du die semantischen Informationen bekommst, die wortbasierte Tokenizer liefern, ohne dass du die Kosten für ein sehr großes Vokabular tragen musst. Diese Trainingsalgorithmen könnten Subtoken wie "##ing" und "##ed" über einen englischen Korpus extrahieren.

Dieser Ansatz hat Vor- und Nachteile in Bezug auf die Rechenkosten. Einerseits hast du dann weniger Wörter in deinem Vokabular, was eine kleinere Einbettungsmatrix bedeutet (siehe Kapitel 5). Andererseits hat ein Wort jetzt mehrere Token, sodass du weniger Wörter in ein Modell einbauen kannst, das eine feste Anzahl von Token akzeptiert.

Wie in Abbildung 4-1 dargestellt, produzieren die einfachsten zeichenbasierten Tokenizer in der Regel keine unbekannten Token, aber sie zerlegen ein Wort auch in viele kleine Teile, was zu einem gewissen Informationsverlust führen kann. Andererseits kannst du Wörter mit der Tokenisierung auf Wortebene vollständig und genau darstellen, aber dann brauchst du ein sehr großes Vokabular oder du riskierst, dass du viele unbekannte Token hast.

subtokenization
Abbildung 4-1. Untertokenisierung

Das Ziel ist also ein zweifaches:

  • Erhöhe die Menge der Informationen pro Token.

  • Verringere die Gesamtzahl der Token (Wortschatzgröße).

Unterwort-Tokenizer erreichen dies effektiv, indem sie eine gute Balance zwischen Zeichen, Unterwörtern und Wörtern finden.

Hinweis

Unterwort-Tokenisierung Algorithmen (zumindest die neueren) sind nicht in Stein gemeißelt. Es gibt eine "Trainingsphase", bevor wir den Text tatsächlich tokenisieren können. Dabei handelt es sich nicht um das Training des Sprachmodells selbst, sondern um einen Prozess, mit dem wir die optimale Balance zwischen der Tokenisierung auf Zeichen- und Wortebene finden.

Die Idee, Präfixe und Suffixe zu verwenden, ist einfach genug, und du könntest vielleicht einen einigermaßen effektiven Teilwort-Tokenizer entwerfen, indem du Regeln für häufige Teilwörter wie "##ing" und "##ed" kodierst. In der Praxis gibt es jedoch eine Reihe von Schwierigkeiten mit diesem Ansatz:

  • Es gibt viele verschiedene Sprachen, von denen jede ihre eigenen Regeln hat. Um einen guten Subword-Tokenizer zu entwickeln, müsste man dann für jede Sprache einen neuen Satz von Regeln verstehen und implementieren.

  • Es gibt keine Garantie dafür, dass die Regeln, die du aufstellst, auch wirklich gut sind. Ein extremes Beispiel: Du könntest dich entscheiden, ein Unterwort-Token für"super##" zu erstellen, aber das taucht vielleicht nie im Text auf. Du hast also einen Platz im Wörterbuch vergeudet. Du könntest die Anzahl der übereinstimmenden Token auswerten und deine Regeln erneut anpassen, aber an diesem Punkt könntest du genauso gut einen Trainingsalgorithmus verwenden.

  • Du als Mensch, der den Text liest, bist vielleicht nicht in der Lage, die Feinheiten der sich wiederholenden Sprachmuster zu erfassen. Es ist viel einfacher, einen Computer 40+ GB Text lesen zu lassen und die sich wiederholenden Token herauszufinden, als selbst 40+ GB Text zu lesen!

Das Ziel des Trainingsverfahrens ist es also, wiederkehrende Texte in einem Korpus zu identifizieren und sie in ein Token umzuwandeln. Wenn sich ein bestimmtes Muster nicht oft wiederholt, wird es nicht als Token aufgenommen.

Wenn dein Textkorpus zum Beispiel ein ausgeglichenes Verhältnis der Zeichenketten"car" und "cat" (zusammen mit vielen anderen Wörtern) aufweist, dann wären die Token, die du erhalten könntest, ["ca##", "r", "t", ...]. Wenn dein Korpus jedoch viel mehr Vorkommen von "cat" als von anderen Wörtern enthält, könnte es von Vorteil sein, dies zu einem einzigen Token zusammenzufassen und die Token["cat", "ca##", "r" ...] zu erhalten. Idealerweise sollten wir es vermeiden, ganze Wörter in einem einzigen Token zusammenzufassen, da dies das Vokabular vergrößert (siehe Abbildung 4-1). Aber wenn etwas oft wiederholt wird, wie das Wort the, ist es effizienter, diese Information in einem einzigen Token zusammenzufassen.

Die Tokenisierung von Teilwörtern verringert auch die Auswirkungen des Problems, dass das Modell auf ein neues Wort stößt, das es noch nie gesehen hat. Wenn dein Trainingskorpus die Zeichenfolgen swim, play und playing enthält, würde ein Tokenizer auf Wortebene die Zeichenfolge swimming als unbekanntes Wort identifizieren. "swimming" ist jedoch einfach ein neues Wort, das aus primitiven Teilwörtern gebildet wird, die das Modell bereits gesehen hat. Ein Teilwort-Tokenisierer könnte es daher als ["swim", "##m##", "##ing"] identifizieren und dem Modell relevantere Informationen übermitteln.

Schauen wir uns an, wie diese Ideen in der tokenizers Bibliothek umgesetzt werden.

Baue deinen eigenen Tokenizer

Die gebrauchsfertigen Tokenizer für Unterwörter sind großartig, aber manchmal brauchst du wirklich einen Tokenizer, der spezielle Nuancen deines Textbereichs herausfiltert. Die klassischen Beispiele sind juristische und medizinische Texte. In diesen Bereichen gibt es in der Regel eine Reihe von häufig verwendeten Begriffen, die so wichtig sind, dass sie ein eigenes Token verdienen (z. B. Namen von Molekülen oder bestimmte Abschnitte von Rechtsdokumenten).

Hinweis

Ja, wir haben "trainieren" gesagt, denn Subword-Tokenizer brauchen einige Kriterien, um zu entscheiden, wie sie Wörter aufteilen sollen, und Lernen ist oft die besteLösung.

Wenn du deinen eigenen Tokenizer trainieren willst, gibt es ein paar beliebte Optionen. Hier sind einige Verweise auf den aktuellen Stand der Forschung zu Tokenizern:

Byte-Paar-Kodierung (BPE)

Siehe R. Sennrich et al., "Neural Machine Translation of Rare Words with Subword Units", arXiv, 2015, https://oreil.ly/dlFNw.

WordPiece

Siehe M. Schuster und K. Nakajima, "Japanese and Korean Voice Search," International Conference on Acoustics, Speech and Signal Processing, IEEE (2012), https://oreil.ly/fvGTh.

SentencePiece

Siehe T. Kudo und J. Richardson, "SentencePiece: A Simple and Language Independent Subword Tokenizer and Detokenizer for Neural Text Processing," arXiv, 2018, https://oreil.ly/YNFhP.

Reale medizinische Daten zu erhalten, ist aufgrund von Vorschriften und einem Mangel an datenschutzfreundlichen maschinellen Lernverfahren ziemlich schwierig. Deshalb verwenden wir jetzt den WikiText-103-Datensatz, also den Satz von Wikipedia-Artikeln, den wir in Kapitel 2 verwendet haben. Wenn deine Textdaten den typischen literarischen Mustern im Internet entsprechen, musst du deine eigenen Tokenizer in den meisten Fällen nicht von Grund auf trainieren.

Zuerst müssen wir den Datensatz besorgen (falls du ihn noch nicht heruntergeladen hast):

wget https://s3.amazonaws.com/research.metamind.io/wikitext/
  wikitext-103-raw-v1.zip
unzip wikitext-103-raw-v1.zip

Die Verwendung eines etablierten Tokenizers ist mit der Bibliothek von Hugging Face tokenizers ganz einfach. Hier richten wir zunächst in einer einzigen Codezeile einen Tokenizer für die Byte-Paar-Kodierung (eine Form der Subword-Tokenisierung) ein:

from tokenizers import Tokenizer
from tokenizers.models import BPE

tokenizer = Tokenizer(BPE(unk_token="[UNK]"))

Als nächstes initialisieren wir ein spezielles BpreTrainer Objekt. Dies ist nur erforderlich, wenn du einen neuen Tokenizer von Grund auf trainierst:

from tokenizers.trainers import BpeTrainer

trainer = BpeTrainer(
    special_tokens=["[UNK]", "[CLS]", "[SEP]", "[PAD]", "[MASK]"])

Schließlich geben wir die Dateien an und trainieren unseren BPE Tokenizer:

files = [
    f"data/wikitext-103-raw/wiki.{split}.raw" for split in
    ["test", "train", "valid"]]

tokenizer.train(files, trainer)

Fazit

In diesem Kapitel haben wir uns die erste Stufe der NLP-Pipeline angesehen - die Tokenizer. Tokenizer sind nicht die Stufe des Stacks, die die meisten Leute optimieren sollten, da verschiedene Tokenizer in der realen Welt keinen großen Einfluss auf die Leistung deiner Anwendung haben, aber sie sind dennoch eine wichtige Komponente. In der Praxis solltest du spacyoder tokenizers verwenden, da dort die neuesten Versionen der neuesten Tokenizer aus der Forschung implementiert sind. Wenn du einen benutzerdefinierten Datensatz mit viel domänenspezifischem Vokabular hast (wie bei juristischen oder medizinischen Anwendungen), ist es sinnvoll, einen etablierten Tokenizer-Algorithmus wie WordPiece oder SentencePiece zu trainieren.

Wir haben auch einige der Feinheiten bei der Entwicklung von schnellen Tokenizern erforscht. Insbesondere haben wir untersucht, wie sich die Wahl der Programmiersprache auf die Leistung deines Tokenizers auswirken kann.

Jetzt haben wir ein grundlegendes Verständnis von Tokenizern, und wenn du willst, kannst du deine eigenen Tokenizer von Grund auf bauen (in der Praxis ist das natürlich nicht so nützlich). So können wir aus großen Textdateien Token erzeugen, die unser Modell zur Lösung komplexer NLP-Probleme verwenden kann.

Aber wir können dem Modell keine rohen Token übergeben. Token sind im Wesentlichen immer noch Indizes in Wörterbüchern, was für ein Deep Learning-Modell semantisch nicht sinnvoll ist. Stattdessen übergeben wir so genannte "Einbettungen" der Token, um die es im nächsten Kapitel geht.

Get Angewandte natürliche Sprachverarbeitung im Unternehmen now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.