Tokenisierung im Natural Language Processing (NLP) und effiziente GPU-Beschleunigung
Im Bereich des Natural Language Processing (NLP) sind Machine-Learning-Modelle nicht in der Lage, rohe menschliche Sprache unmittelbar zu verstehen. Stattdessen müssen Texte zunächst in eine strukturierte Form überführt werden, die von Algorithmen verarbeitet werden kann. Dieser Umwandlungsprozess wird als Tokenisierung bezeichnet.
Die Tokenisierung bildet die grundlegende Stufe jeder NLP-Pipeline. Dabei wird Klartext in kleinere Einheiten – sogenannte Tokens – zerlegt, die anschließend von Transformer-Modellen wie BERT oder GPT verarbeitet werden können. Je nach gewählter Methode können Tokens aus vollständigen Wörtern, Subwörtern, einzelnen Zeichen oder Satzzeichen bestehen.
Beispiel einer tokenisierten Eingabe
Beispieltext = „Hello! I’m learning how to build a tokenizer in Python.“
Tokenisierte Ausgabe [‘hello’, ‘i’, ‘m’, ‘learning’, ‘how’, ‘to’, ‘build’, ‘a’, ‘tokenizer’, ‘in’, ‘python’]
Klassische Tokenisierungsverfahren, die auf der CPU ausgeführt werden, können insbesondere bei großen Datenmengen oder Echtzeit-Inferenz zu einem Leistungsengpass werden. GPUs sind primär für vektorisierte Berechnungen und Matrixoperationen optimiert, wodurch String-Manipulationen, reguläre Ausdrücke und Dictionary-Lookups weniger effizient sind. Hugging Face stellt jedoch leistungsfähige, in Rust implementierte Tokenizer bereit, die sich effizient in GPU-Workflows integrieren lassen. In diesem Beitrag werden verschiedene Tokenizer-Typen erläutert und gezeigt, wie sich Tokenisierung mithilfe von GPUs beschleunigen lässt.
Was ist ein Tokenizer?
Ein Tokenizer zerlegt Rohtext in kleinere Bestandteile – meist Subwörter oder Tokens – und wandelt diese in numerische IDs um. Diese numerischen Repräsentationen dienen als Eingabe für Transformer-Architekturen wie BERT, GPT oder RoBERTa.
Arten von Tokenizern
1. Wortbasierte Tokenizer
Wort-Tokenizer trennen Text in der Regel anhand von Leerzeichen und Satzzeichen. Sie sind einfach nachvollziehbar, stoßen jedoch bei unbekannten oder seltenen Begriffen (Out-of-Vocabulary, OOV) an ihre Grenzen.
Beispiel (mit NLTK):
import nltk
nltk.download('punkt')
from nltk.tokenize import word_tokenize
text = "Tokenization is essential for NLP models!"
tokens = word_tokenize(text)
print(tokens)
['Tokenization', 'is', 'essential', 'for', 'NLP', 'models', '!']
2. Subword-Tokenizer
Subword-Tokenizer zerlegen Wörter in kleinere bedeutungstragende Einheiten. Dieser Ansatz eignet sich besonders gut zur Verarbeitung seltener oder zusammengesetzter Wörter.
a. Byte-Pair Encoding (BPE)
Byte-Pair Encoding führt schrittweise Zusammenführungen der häufigsten benachbarten Zeichen- oder Subwortpaare innerhalb eines Textkorpus durch.
Beispiel (mit der Hugging Face Tokenizers-Bibliothek):
from tokenizers import Tokenizer
from tokenizers.models import BPE
from tokenizers.trainers import BpeTrainer
from tokenizers.pre_tokenizers import Whitespace
# Initialize tokenizer
tokenizer = Tokenizer(BPE())
tokenizer.pre_tokenizer = Whitespace()
# Trainer and training corpus
trainer = BpeTrainer(special_tokens=["[UNK]", "[CLS]", "[SEP]", "[PAD]", "[MASK]"])
files = ["your_corpus.txt"] # Replace with a path to your text file
tokenizer.train(files, trainer)
# Encode text
output = tokenizer.encode("Tokenization is essential for NLP models!")
print(output.tokens)
b. WordPiece (verwendet in BERT)
WordPiece funktioniert ähnlich wie BPE, nutzt jedoch einen wahrscheinlichkeitsbasierten, gierigen Optimierungsansatz.
Beispiel (mit Hugging Face Transformers)
from transformers import BertTokenizer
tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
tokens = tokenizer.tokenize("Tokenization is essential for NLP models!")
print(tokens)
Output: [‘token’, ‘##ization’, ‘is’, ‘essential’, ‘for’, ‘nl’, ‘##p’, ‘models’, ‘!’]
c. SentencePiece
SentencePiece behandelt Eingaben als rohe Byte-Sequenzen und eignet sich besonders gut für mehrsprachige Anwendungen.
import sentencepiece as spm
# Train a SentencePiece model (one-time)
# spm.SentencePieceTrainer.train(input='your_corpus.txt', model_prefix='m', vocab_size=5000)
# Load and tokenize
sp = spm.SentencePieceProcessor(model_file='m.model')
tokens = sp.encode("Tokenization is essential for NLP models!", out_type=str)
print(tokens)
3. Zeichenbasierte Tokenizer
Bei zeichenbasierten Tokenizern wird jedes einzelne Zeichen als eigenständiges Token betrachtet.
text = "Token"
tokens = list(text)
print(tokens)
['T', 'o', 'k', 'e', 'n']
Werkzeuge für GPU-gestützte Tokenisierung
1. Hugging Face Tokenizers (Fast Tokenizers)
Hugging Face stellt mit PreTrainedTokenizerFast eine Variante bereit, die auf einer in Rust entwickelten Backend-Bibliothek basiert und für parallele Verarbeitung optimiert ist. Als „slow“ werden die in Python implementierten Tokenizer innerhalb der Transformers-Bibliothek bezeichnet, während „fast“ die Rust-basierte Implementierung nutzt.
Der Geschwindigkeitsvorteil zeigt sich insbesondere bei der Verarbeitung großer Textmengen. Bei einzelnen Sätzen kann der Unterschied gering sein oder sogar leicht zugunsten der langsameren Variante ausfallen. Ein wesentlicher Vorteil der schnellen Tokenizer ist das sogenannte Offset Mapping, das exakt angibt, welcher Abschnitt des Originaltexts welchem Token entspricht.
Obwohl die Tokenisierung selbst auf der CPU erfolgt, können die erzeugten Tensoren direkt auf die GPU übertragen werden.
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased", use_fast=True)
inputs = tokenizer(["Tokenize this on GPU"], return_tensors="pt", padding=True)
inputs = {k: v.to("cuda") for k, v in inputs.items()}
2. Byte-Pair Encoding (BPE) im Detail
Byte-Pair Encoding ist ein Subword-Segmentierungsverfahren, das wiederholt die häufigsten benachbarten Tokenpaare zusammenführt, bis eine definierte Vokabulargröße erreicht ist.
Angenommen, ein Korpus enthält die Wörter: „cat“, „cap“, „can“, „bat“ und „bats“. Das anfängliche Vokabular besteht aus einzelnen Zeichen: [„a“, „b“, „c“, „n“, „p“, „s“, „t“]. Das Verfahren identifiziert die häufigsten benachbarten Symbole – beispielsweise („a“, „t“) – und kombiniert sie zu „at“. Dieser Prozess setzt sich fort, sodass größere Subwörter wie „cat“ oder „bat“ entstehen, während seltene Begriffe weiterhin aus kleineren Einheiten zusammengesetzt werden können.
Die Tokenisierung endet, sobald die gewünschte Vokabulargröße oder Anzahl an Zusammenführungen erreicht ist.
Python-Implementierung von BPE:
from collections import defaultdict, Counter
# Sample corpus with word frequencies
corpus = {
"cat": 5,
"cap": 3,
"can": 2,
"bat": 4,
"bats": 2
}
# Step 1: Represent each word as a list of characters + word boundary token
def get_tokenized_corpus(corpus):
return {
tuple(word): freq for word, freq in corpus.items()
}
# Step 2: Count frequency of all adjacent symbol pairs
def get_pair_freqs(tokenized_corpus):
pairs = defaultdict(int)
for word, freq in tokenized_corpus.items():
for i in range(len(word) - 1):
pair = (word[i], word[i + 1])
pairs[pair] += freq
return pairs
# Step 3: Merge the most frequent pair
def merge_pair(pair, tokenized_corpus):
new_corpus = {}
bigram = ' '.join(pair)
replacement = ''.join(pair)
for word, freq in tokenized_corpus.items():
new_word = []
i = 0
while i < len(word):
if i < len(word) - 1 and word[i] == pair[0] and word[i + 1] == pair[1]:
new_word.append(replacement)
i += 2
else:
new_word.append(word[i])
i += 1
new_corpus[tuple(new_word)] = freq
return new_corpus
# Step 4: Apply BPE for a few merges
tokenized_corpus = get_tokenized_corpus(corpus)
vocab = set(char for word in tokenized_corpus for char in word)
print("Initial vocabulary:", sorted(vocab))
print("Initial corpus:", tokenized_corpus)
num_merges = 5
for i in range(num_merges):
pair_freqs = get_pair_freqs(tokenized_corpus)
if not pair_freqs:
break
most_frequent = max(pair_freqs, key=pair_freqs.get)
print(f"\nMerge {i+1}: Merging {most_frequent} → {''.join(most_frequent)}")
tokenized_corpus = merge_pair(most_frequent, tokenized_corpus)
vocab.add(''.join(most_frequent))
print("Updated corpus:", tokenized_corpus)
print("\nFinal vocabulary:", sorted(vocab))
Initial vocabulary: ['a', 'b', 'c', 'n', 'p', 's', 't']
Initial corpus: {('c', 'a', 't'): 5, ('c', 'a', 'p'): 3, ('c', 'a', 'n'): 2, ('b', 'a', 't'): 4, ('b', 'a', 't', 's'): 2}
Merge 1: Merging ('a', 't') → at
Updated corpus: {('c', 'at'): 5, ('c', 'a', 'p'): 3, ('c', 'a', 'n'): 2, ('b', 'at'): 4, ('b', 'at', 's'): 2}
Merge 2: Merging ('b', 'at') → bat
...
3. NVIDIA RAPIDS cuDF GPU-Subword-Tokenizer
Die RAPIDS cuDF-Bibliothek von NVIDIA ermöglicht GPU-beschleunigte Subword-Tokenisierung. CPU-basierte Tokenizer verursachen häufig Verzögerungen durch wiederholte Datenübertragungen zwischen CPU und GPU. Die Methode cudf.str.subword_tokenize führt die Tokenisierung vollständig auf der GPU aus, wodurch unnötige Speichertransfers vermieden und der Durchsatz deutlich erhöht wird. Zentrale Vorteile:
- Bis zu 483-fach schneller als herkömmliche CPU-Tokenizer
- Alle Zwischenergebnisse verbleiben im GPU-Speicher
- Keine kostspieligen CPU-GPU-Datenkopien
- Nahtlose Integration in RAPIDS-DataFrame-Workflows
import cudf
from cudf.utils.hash_vocab_utils import hash_vocab
from cudf.core.subword_tokenizer import SubwordTokenizer
# Step 1: Hash the BERT vocabulary (only needs to be done once)
hash_vocab('bert-base-cased-vocab.txt', 'voc_hash.txt')
# Step 2: Initialize the tokenizer with the hashed vocab
cudf_tokenizer = SubwordTokenizer('voc_hash.txt', do_lower_case=True)
# Step 3: Create a cuDF Series with input text
str_series = cudf.Series(['This is the', 'best book'])
# Step 4: Tokenize using GPU
tokenizer_output = cudf_tokenizer(
str_series,
max_length=8,
max_num_rows=len(str_series),
padding='max_length',
return_tensors='pt', # Return PyTorch tensors
truncation=True
)
# Step 5: Access tokenized output (all in GPU memory)
print("Input IDs:\n", tokenizer_output['input_ids'])
print("Attention Mask:\n", tokenizer_output['attention_mask'])
print("Metadata:\n", tokenizer_output['metadata'])
Output:
Input IDs:
tensor([[ 101, 1142, 1110, 1103, 102, 0, 0, 0],
[ 101, 1436, 1520, 102, 0, 0, 0, 0]],
device='cuda:0', dtype=torch.int32)
Attention Mask:
tensor([[1, 1, 1, 1, 1, 0, 0, 0],
[1, 1, 1, 1, 0, 0, 0, 0]],
device='cuda:0', dtype=torch.int32)
Metadata:
tensor([[0, 1, 3],
[1, 1, 2]], device='cuda:0', dtype=torch.int32)
cudf.str.subword_tokenize ist besonders nützlich, um Millionen von Textdatensätzen zu verarbeiten oder große NLP-Workloads in Echtzeit zu unterstützen. Außerdem kann es helfen, Tokenizer-Engpässe in Produktivumgebungen zu beseitigen, da es eine leistungsstarke Alternative zu langsameren Tokenisierungspipelines wie spaCy oder Hugging Face bietet.
Best Practices für GPU-Tokenisierung
- PreTrainedTokenizerFast verwenden, um eine höhere Performance bei großen Datensätzen zu erzielen.
- Tensors nach CUDA verschieben mittels
.to("cuda"), um unnötige Datenübertragungen zu vermeiden. - Zu kleine Batch-Größen vermeiden, da sie die GPU nicht effizient auslasten.
- Datensätze vorab tokenisieren und cachen, insbesondere im Trainingskontext.
- Batch-Größen benchmarken, um optimale Geschwindigkeit und Speicherauslastung zu bestimmen.
Durch die Beachtung dieser Maßnahmen wird sichergestellt, dass die Tokenisierung nicht zum Flaschenhals der NLP-Pipeline wird und die GPU-Ressourcen optimal genutzt werden – insbesondere bei großskaligem Modelltraining oder latenzkritischen Anwendungen wie Chatbots.
Häufige Fragen
Kann Tokenisierung auf der GPU ausgeführt werden?
Ja. Frameworks wie RAPIDS ermöglichen eine vollständig GPU-beschleunigte Tokenisierung. Zudem erzeugen Hugging Face Tokenizer Ausgaben, die direkt auf GPUs übertragen werden können.
Welche Tokenizer unterstützen GPU?
RAPIDS und FasterTransformer bieten native GPU-Unterstützung. Hugging Face stellt hochoptimierte CPU-Tokenizer mit GPU-kompatiblen Tensor-Ausgaben bereit.
Ist GPU-Tokenisierung schneller?
Bei großen Datensätzen und Batch-Verarbeitung ist GPU-Tokenisierung in der Regel schneller, da parallele Berechnungen genutzt werden. Bei kleinen Eingaben kann der Vorteil durch Datenübertragungs-Overhead geringer ausfallen.
Müssen Tokenizer trainiert werden?
Viele Tokenizer sind bereits vortrainiert, beispielsweise der BERT-Tokenizer. Eigene Tokenizer lassen sich mit der Hugging Face Tokenizers-Bibliothek trainieren.
Wie lade ich einen Tokenizer?
from transformers import AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")
Fazit
Obwohl Tokenisierung wie ein kleiner Vorverarbeitungsschritt wirkt, kann sie in großen NLP-Systemen zu einem erheblichen Performance-Engpass werden – insbesondere bei ausschließlicher CPU-Ausführung. GPU-basierte Tokenisierung beschleunigt diesen Prozess deutlich und steigert die Effizienz von Machine-Learning- und Inferenz-Workflows.
Bibliotheken wie Hugging Face PreTrainedTokenizerFast und RAPIDS SubwordTokenizer erleichtern die skalierbare Verarbeitung großer Textmengen. Ob beim Training umfangreicher Modelle, beim Einsatz von Chatbots oder bei der Analyse riesiger Textkorpora – GPU-gestützte Tokenisierung bietet eine leistungsfähige und skalierbare Lösung.


