Ein Tokenizer ist eine Komponente von Large Language Models (LLMs) die dazu dient, einen Text in Token zu zerlegen (encoding) und die Token wieder in einen lesbaren Text umzuwandeln (decoding). Ohne einen Tokenizer ist die Verarbeitung von natürlicher Sprache in den aktuell verfügbaren LLMs nicht möglich, da sie nur Token verarbeiten können.
Das grundlegende Konzept des Tokenizers besteht darin, einen Text in kleinere Einheiten zu zerlegen und in Zahlen umzuwandeln. Anstatt den gesamten Text als eine zusammenhängende Zeichenkette zu betrachten, wird er in kleine Einheiten wie Wörter, Subwörter oder sogar einzelne Zeichen unterteilt. Dieser Prozess ermöglicht es dem LLM, die verschiedenen Bestandteile eines Textes besser zu verstehen und in Beziehung zueinander zu setzen.
Statt Wörter als Einheiten zu betrachten, werden sie in kleinere, sinnvolle Teile (Token) zerlegt. Dieser Ansatz ermöglicht es dem Modell, seltene Wörter oder komplexe Zusammensetzungen zu behandeln. Beispielsweise könnte das Wort „unwahrscheinlich“ in die Subwörter „un„, „wahr“ und „scheinlich“ zerlegt werden. Dadurch kann das Modell das Wort besser versteht und Beziehungen zu ähnlichen Wörtern herstellen.
Der CodeGenTokenizerFast für SourceCode
Der CodeGenTokenizerFast ist ein “schneller” CodeGen-Tokenizer, der in der „tokenizers“-Bibliothek von HuggingFace bereitgestellt wird. Dieser Tokenizer wurde so realisiert, dass er sich besonders gut für SourceCode eignet. So behandelt er Leerzeichen z.B. wie Teile der Tokens (Ähnlich wie z.B. auch Google SentencePiece), sodass ein Wort je nachdem, ob es am Anfang des Satzes (ohne Leerzeichen) oder nicht anders kodiert wird.
Die 50295 verwendeten Tokens findet man in der tokenizer.json Datei. Diese Datei enthält die Konfiguration und das Vokabular eines Tokenizers. Sie wird verwendet, um die Informationen des Tokenizers zu speichern und zu laden, so dass der Tokenizer problemlos geteilt werden kann. Die Datei liegt im JSON-Format vor und ist Teil des Salesforce/CodeT5 Projektes. Sie enthält unter anderem das Vokabular und die Verschmelzungsregeln(Merges) .
Die „Merges“ in einer tokenizer.json-Datei beziehen sich auf die Merge-Regeln, die von einem Byte Pair Encoding (BPE) Tokenizer verwendet werden. BPE ist ein Algorithmus zur Tokenisierung von Teilwörtern, der mit einem anfänglichen Vokabular von Einzelzeichen beginnt und iterativ die häufigsten Zeichenpaare oder Zeichenfolgen zusammenführt, um neue Token zu bilden. Die Verschmelzungsregeln geben an, welche Zeichenpaare oder Zeichenfolgen zu neuen Token verschmolzen werden sollen. Diese Regeln werden aus den Trainingsdaten gelernt und in der Datei tokenizer.json gespeichert, so dass der Tokenizer bei der Tokenisierung von neuem Text die gleichen Zusammenführungsregeln anwenden kann.
Verschmelzungsregeln(Merges)
Die Implementierung des CodeGenTokenizerFast kann man unter https://github.com/huggingface/transformers/blob/main/src/transformers/models/codegen/tokenization_codegen_fast.py finden.
Den CodeGenTokenizerFast nutzen
SourceCode wird bei den meisten Programmiersprachen als Zeichenkette repräsentiert. Bevor dieser von dem CodeT5 Modell verarbeitet werden kann, müssen die Zeichenkennte vom Tokenizer in eine Zahlenrepräsentation umgewandelt werden. Diese Tokens dienen dann als Grundbausteine für das CodeT5 Modell.
Beispiel:
from transformers import AutoTokenizer
checkpoint = "Salesforce/codet5p-2b"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)
data = “Dies ist ein Test!“
encoded = tokenizer.encode(data)
print (encoded)
# [35, 444, 318, 83, 304, 259, 6208, 0]
data = tokenizer.decode(encoded)
print (data)
# Dies ist ein Test!
CodeGenTokenizerFast mit Salesforce/codet5p-2b Playground
Unter https://tokenizer.jentsch.io/ kann man den CodeGenTokenizerFast mit dem Checkpoint „Salesforce/codet5p-2b“ auch online ausprobieren. Dazu habe ich ein kleines Tool online gestellt, das dazu dient, sich mal etwas mit dem Thema zu beschäftigen.
Hier mal am Beispiel der folgenden onHandleIntent Methode.
@Override
protected void onHandleIntent(Intent intent) {
if (intent != null) {
final String action = intent.getAction();
if (ACTION_FOO.equals(action)) {
final String param1 = intent.getStringExtra(EXTRA_PARAM1);
final String param2 = intent.getStringExtra(EXTRA_PARAM2);
handleActionFoo(param1, param2);
} else if (ACTION_BAZ.equals(action)) {
final String param1 = intent.getStringExtra(EXTRA_PARAM1);
final String param2 = intent.getStringExtra(EXTRA_PARAM2);
handleActionBaz(param1, param2);
}
}
}
Wenn man diese Methode dem CodeGenTokenizerFast mit dem Salesforce/codet5p-2b Checkpoint übergibt, wird der SourceCode wie folgt aufgeteilt.
Jedes Rechteck entspricht einem Token, dem eine Zahl zwischen 1 und 50295 zugeordnet wird.
Hinweis: Bewegt man die Maus auf der Webseite über ein Token, werden alle Repräsentationen des gleichen Tokens farblich hervorgehoben.
Folgendermaßen sehen die eigentlichen Rohdaten des Encoders aus. Einfach nur ein Array von Ziffern. Wie oben schon beschrieben werden Textelemente unterschiedliche behandelt, je nachdem ob ein Leerzeichen davorsteht oder nicht. Z.B. entspricht die Zahl 357 der öffnenden Klammer “(“ mit einem vorgestellten Leerzeichen und die Zahl 7 der öffnenden Klammer “(“ ohne ein Leerzeichen. Aus Sicht des Java-Compilers ist diese Unterscheidung überflüssig, aber der Tokenizer unterscheidet hier trotzdem, so dass es dem LLM später möglich ist diese Feinheiten zu beachten (Es könnte sich ja auch um eine andere Programmiersprache handeln – da unterscheidet der Tokenizer nicht). Ob diese Unterscheidung von dem Modell berücksichtigt, werden entscheidet sich während der Trainings-Phase des LLM.
[50284, 31, 37961, 198, 50284, 24326, 7951, 319, 37508, 5317, 298, 7, 5317, 298, 6824, 8, 1391, 198, 50280, 361, 357, 48536, 14512, 9242, 8, 1391, 198, 50276, 20311, 10903, 2223, 796, 6824, 13, 1136, 12502, 9783, 198, 50276, 361, 357, 44710, 62, 6080, 46, 13, 4853, 874, 7, 2673, 4008, 1391, 198, 50272, 20311, 10903, 5772, 16, 796, 6824, 13, 1136, 10100, 27726, 7, 13918, 3861, 62, 27082, 2390, 16, 1776, 198, 50272, 20311, 10903, 5772, 17, 796, 6824, 13, 1136, 10100, 27726, 7, 13918, 3861, 62, 27082, 2390, 17, 1776, 198, 50272, 28144, 12502, 37, 2238, 7, 17143, 16, 11, 5772, 17, 1776, 198, 50276, 92, 2073, 611, 357, 44710, 62, 4339, 57, 13, 4853, 874, 7, 2673, 4008, 1391, 198, 50272, 20311, 10903, 5772, 16, 796, 6824, 13, 1136, 10100, 27726, 7, 13918, 3861, 62, 27082, 2390, 16, 1776, 198, 50272, 20311, 10903, 5772, 17, 796, 6824, 13, 1136, 10100, 27726, 7, 13918, 3861, 62, 27082, 2390, 17, 1776, 198, 50272, 28144, 12502, 33, 1031, 7, 17143, 16, 11, 5772, 17, 1776, 198, 50276, 92, 198, 50280, 92, 198, 50284, 92]
Einfach nur ein Array von Zahlen, mit dem z.B. dann das LLM gefüttert wird. Auf der anderen Seite des LLM kommt aus dem Model auch wieder ein Token-Array heraus, das dann mit Hilfe des Tokenizers (decode) wieder in den ursprünglichen Text (SourceCode) umgewandelt werden kann.
Ein Modell, das auf genau diesen Token trainiert wurde, kommt es in der Regel auch damit klar, wenn z.B. Methoden wie „getStringExtra“ in mehrere Teile geteilt wird. Auffällig ist, dass z.B. in dem obigen Beispiel der Variablenname intent als ein Token erkannt wird, aber die Klasse Intent auf die beiden Token “Int“ und “ent“ aufgeteilt wird. Das ist insofern schade, da kontextuell ähnliche Token idealerweise auch ähnliche Zahlenrepräsentationen haben. So können semantisch ähnliche Bedeutungen besser verarbeitet werden. Das bedeutet, dass Wörter oder Ausdrücke, die inhaltlich ähnlich sind, in der numerischen Repräsentation nahe beieinander liegen sollten. Dies ermöglicht es dem Modell, semantische Beziehungen zwischen Wörtern einfacher zu erfassen und zu verstehen, wie sie in Bezug zueinanderstehen.
Es gibt auch Fälle, in denen es sinnvoll sein kann, unterschiedliche Zahlenrepräsentationen für ähnliche Tokens zu haben, z. B. bei der Behandlung von Wörtern mit mehreren Bedeutungen. Es ist eine Abwägung zwischen der Notwendigkeit, ähnliche Tokens ähnliche Repräsentationen zu geben, und der Fähigkeit des Modells, feine semantische Unterschiede zu erfassen. In solchen Fällen sind kontextabhängige Embeddings sinnvoll, um die Bedeutungsunterschiede innerhalb ähnlicher Tokens zu erfassen.
Zahlenrepräsentationen
Embeddings
Embeddings sind Vektor-Repräsentationen eines oder mehrerer Tokens. Mit Hilfe dieser Embeddings kann man z.B. die Ähnlichkeit von mehreren Tokens/Token-Blöcken miteinander vergleichen. Die Idee dahinter ist, dass 2 Multidimensionale-Embeddings-Vektoren einen geringen Abstand voneinander haben, je ähnlicher sie sich sind. So kann man Embeddings nach ihrer Ähnlichkeit sortieren bzw. miteinander vergleichen.
Die Embeddings gehören eigentlich nicht direkt zum Thema Tokenizer, sind aber an der Schnittstelle zum Transformer ein entscheidender Schritt, denn die Tokens werden vom im Eingang des Modells in Embeddings umgewandelt. Dieser Prozess ist ein wesentlicher Teil des Transformers.
Die Huggingface transformer Bbieteibliothek bietet mit der „get_input_embeddings()“ Funktion eine einfache Möglichkeit, diese Embeddings zu erzeugen. So kann man mit dem folgenden Code-Snippet den Input-String in Token umwandeln und aus denen dann die Embeddings des entsprechenden Modells (in diesem Fall google/flan-t5-base) erzeugen.
from transformers import AutoTokenizer, AutoModelForSeq2SeqLM, GenerationConfig
line = 'What color is the undoubtedly beautiful sky?'
model_name = 'google/flan-t5-base'
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForSeq2SeqLM.from_pretrained(model_name)
config = GenerationConfig(max_new_tokens=200)
tokens = tokenizer(line, return_tensors="pt")
input_embeddings = model.get_input_embeddings()
token_ids = tokens['input_ids'][0]
line_embeddings = input_embeddings(token_ids)
print (token_ids)
print (line_embeddings.shape)
Das obige Beispiel basiert auf dem https://github.com/Me163/youtube/blob/main/Transformers/repl.py Python Skript von Me163.
tensor([ 363, 945, 19, 8, 73, 16501, 786, 5796, 58, 1])
torch. Size([10, 768])
Der Text ‚What color is the undoubtedly beautiful sky?‘ wird hier in die Token „363, 945, 19, 8, 73, 16501, 786, 5796, 58, 1“ umgewandelt und dann mit Hilfer der Funktion „input_embeddings(token_ids)“ die entsprechenden Embeddings erzeugt.
Das erzeugte Embeddings-Array (10×768) enthält pro Token einen 768-dimensionalen Vektor, der dann von dem Transformer verarbeitet wird.
Ein nettes Tool zur Visualisierung von Embeddings kann man unter https://projector.tensorflow.org/ finden.
Das Prinzip wird von Geoffrey Hinton (neben einer Reihe anderer sehr interessanter Themen) in folgendem Video sehr gut erklärt.
Fazit
Der CodeGenTokenizerFast ermöglicht eine schnelle Tokenisierung von Code, so dass es leicht möglich ist, Programmcode in seine grundlegenden Token aufzuteilen. Dabei ist der CodeGenTokenizerFast sehr schnell, sodass es große Mengen an Code in kurzer Zeit verarbeiten kann. Der CodeGenTokenizerFast ist speziell auf die Bedürfnisse einer Programmiersprache zugeschnitten und erkennt die verschiedenen Bestandteile des Codes präzise. Er kann komplexe Sprachstrukturen, wie z.B. verschachtelte Klammern oder Kommentare besser handhaben als Text-Tokenizer, die für die Verarbeitung von natürlicher Sprache optimiert sind. Dabei unterstützt der CodeGenTokenizerFast eine Vielzahl von Programmiersprachen, wie z.B. C/C++, Java, Python, JavaScript, Ruby, PHP, Swift, Go, Rust, C#, Objective-C, Kotlin, TypeScript und mehr.
CodeGenTokenizerFast Playground: https://tokenizer.jentsch.io/