Tokenizer in Hugging Face: Prinzipien und Verwendung in Java
1. Überblick
Wenn Sie mit NLP-Modellen auf Hugging Face arbeiten, ist der erste Schritt immer Tokenisierung — das Konvertieren von Rohtext in eine Zahlenfolge, die das Modell verstehen kann. Dieser Artikel erläutert, wie Tokenizer funktionieren, die gängigsten Token-Splitting-Strategien (BPE, WordPiece, SentencePiece) und wie Sie HuggingFaceTokenizer direkt aus Java über die DJL-Bibliothek aufrufen, ohne Python ausführen zu müssen. Nach dem Lesen können Sie einen TokenizerService in Spring Boot erstellen, um Text zu encodieren/decodieren, Padding/Truncation zu kontrollieren und Tests dafür zu schreiben.
2. Was ist ein Tokenizer und warum ist er wichtig?
Ein Tokenizer führt zwei Dinge durch: Er teilt Sätze in Token auf (die Wörter, Subwords oder Zeichen sein können) und ordnet dann jedem Token eine ID als ganze Zahl aus dem Vokabular des Modells zu. Das Modell “sieht” niemals Text — es sieht nur diese ID-Sequenz.
Beispiel: Der Satz "Order service xử lý thanh toán" könnte mit dem bert-base-uncased-Tokenizer folgende Ausgabe erzeugen:
tokens: ["order", "service", "xu", "##ly", "thanh", "toan"]
ids: [2344, 2326, 1045, 2135, 7899, 8821]
Die Notation ##ly bedeutet, dass dieses Token direkt an das vorherige Token angehängt wird (Subword). Das ist der Grund, warum die Anzahl der Token normalerweise größer ist als die Anzahl der Wörter — was wichtig ist, wenn Sie API-Kosten berechnen oder die max_length des Modells begrenzen.
Hinweis: Jedes vortrainierte Modell wird mit einem eigenen Tokenizer geliefert, der gleichzeitig mit dem Modell auf demselben Vokabular trainiert wurde. Die Verwendung des falschen Tokenizers (z. B. BERT-Tokenizer für GPT-Modelle) führt zu bedeutungslosen IDs, und das Modell macht vollständig falsche Vorhersagen, ohne einen Fehler zu melden.
3. Häufige Tokenisierungsstrategien
3.1. BPE (Byte-Pair Encoding)
Wird in GPT-2, RoBERTa verwendet. Es beginnt mit Zeichen und wiederholt den Prozess, die am häufigsten vorkommenden Zeichenpaare in ein neues Token zu kombinieren, bis die gewünschte Vokabulargröße erreicht ist.
3.2. WordPiece
Wird in BERT verwendet. Ähnlich wie BPE, aber die zu kombinierenden Paare werden auf der Grundlage der statistischen Wahrscheinlichkeit (Likelihood) ausgewählt, nicht der rohen Häufigkeit. Subtoken werden mit ## gekennzeichnet, um anzuzeigen, dass sie an das vorherige Wort angehängt sind.
3.3. SentencePiece
Wird in T5, XLNet, ALBERT verwendet. Es behandelt Leerzeichen als normales Zeichen (gekennzeichnet durch ▁), daher funktioniert es gut bei Sprachen ohne explizite Wortbegrenzungen wie Japanisch oder Chinesisch.
Sie müssen diese Algorithmen nicht selbst implementieren — Hugging Face hat sie bereits in der Datei tokenizer.json verpackt, die bei jedem Modell mitgeliefert wird. Sie müssen diese Datei nur laden und aufrufen.
4. Abhängigkeit hinzufügen
Um den Tokenizer direkt in Java zu verwenden (ohne einen Python-Server), fügen Sie die Bibliothek DJL Hugging Face Tokenizers hinzu:
<dependency>
<groupId>ai.djl.huggingface</groupId>
<artifactId>tokenizers</artifactId>
<version>0.28.0</version>
</dependency>
Diese Bibliothek liest direkt die standardmäßige tokenizer.json-Datei von Hugging Face, ohne komplexe JNI-Aufrufe zu erfordern.
5. Basis-Text-Kodierung
import ai.djl.huggingface.tokenizers.HuggingFaceTokenizer;
import ai.djl.huggingface.tokenizers.Encoding;
import java.util.Arrays;
public class TokenizerDemo {
public static void main(String[] args) throws Exception {
HuggingFaceTokenizer tokenizer = HuggingFaceTokenizer.newInstance("bert-base-uncased");
Encoding encoding = tokenizer.encode("Order service handles payment");
System.out.println(Arrays.toString(encoding.getIds()));
System.out.println(Arrays.toString(encoding.getTokens()));
}
}
HuggingFaceTokenizer.newInstance(...): lädt den Tokenizer vom Hugging Face Hub (oder aus dem lokalen Cache) nach Modellname.encode(...): gibt einEncoding-Objekt zurück, dasids,tokensundattentionMaskenthält.- Beim ersten Durchlauf wird die
tokenizer.json-Datei in~/.djl.ai/cacheheruntergeladen. Bei späteren Aufrufen wird der Cache verwendet, kein Netzwerk erforderlich.
Beispielausgabe:
[101, 2344, 2326, 16024, 7909, 102]
[[CLS], order, service, handles, payment, [SEP]]
Beachten Sie die zwei speziellen Token [CLS] (Satzanfang) und [SEP] (Satzende), die automatisch eingefügt werden — das ist eine BERT-Konvention, kein Fehler.
6. Rückgängigmachen durch Dekodierung
long[] ids = encoding.getIds();
String decoded = tokenizer.decode(ids);
System.out.println(decoded);
[CLS] order service handles payment [SEP]
decode ist nützlich zum Debuggen: Sie können genau sehen, was das Modell nach der Tokenisierung „sieht”, oder beim Verarbeiten von Text-Generierungs-Ausgaben von generativen Modellen.
7. Padding, Truncation und Batch
Wenn Sie mehrere Sätze gleichzeitig an das Modell übergeben, müssen sie die gleiche Länge haben (Padding) und dürfen das Modell-Limit nicht überschreiten (Truncation).
import java.util.Map;
HuggingFaceTokenizer tokenizer = HuggingFaceTokenizer.newInstance(
"bert-base-uncased",
Map.of(
"padding", "true",
"truncation", "true",
"maxLength", "16"
));
Encoding[] batch = tokenizer.batchEncode(new String[] {
"Order placed successfully",
"Payment failed due to insufficient balance"
});
for (Encoding e : batch) {
System.out.println(Arrays.toString(e.getIds()));
System.out.println(Arrays.toString(e.getAttentionMask()));
}
padding=true: kürzere Sätze erhalten Token[PAD](ID 0) hinzugefügt, um die Länge des längsten Satzes im Batch auszugleichen.truncation=true+maxLength=16: Sätze mit mehr als 16 Token werden gekürzt.attentionMask: eine Reihe von 1/0, die markiert, welche Token echt (1) oder Padding (0) sind — das Modell verwendet diese Maske, um die Padding-Teile bei der Attention-Berechnung zu ignorieren.
[101, 2344, 2872, 4652, 102, 0, 0, 0]
[1, 1, 1, 1, 1, 0, 0, 0]
Hinweis: Wenn Sie vergessen,
attentionMaskbeim Übergeben an das Modell zu setzen, kann die Padding-Partie in die Attention-Berechnung eingehen, was zu fehlerhaften Ausgaben führt, ohne dass ein Laufzeitfehler gemeldet wird.
8. Integration in Spring Boot
Kapseln Sie den Tokenizer als gemeinsam genutzten Bean, um zu vermeiden, dass er bei jedem Request neu geladen wird (das Laden des Tokenizers von Hub/Cache dauert 100–300ms und sollte nicht im Request-Thread geschehen).
import ai.djl.huggingface.tokenizers.HuggingFaceTokenizer;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.Map;
@Service
public class TokenizerService {
private final HuggingFaceTokenizer tokenizer;
public TokenizerService() throws IOException {
this.tokenizer = HuggingFaceTokenizer.newInstance(
"bert-base-uncased",
Map.of("padding", "true", "truncation", "true", "maxLength", "128"));
}
public long[] encode(String text) {
return tokenizer.encode(text).getIds();
}
public String decode(long[] ids) {
return tokenizer.decode(ids);
}
public int countTokens(String text) {
return tokenizer.encode(text).getTokens().length;
}
}
- Der Konstruktor initialisiert den Tokenizer genau einmal, wenn der Bean erstellt wird. Spring verwaltet den Lebenszyklus des Singletons.
countTokens: nützlich, wenn Sie das Input-Längenlimit prüfen müssen, bevor Sie das Modell aufrufen (z. B. Bestellbeschreibung darf nicht mehr als 512 Token haben).
Verwendung in einem anderen Service:
@RestController
@RequestMapping("/tokens")
public class TokenController {
private final TokenizerService tokenizerService;
public TokenController(TokenizerService tokenizerService) {
this.tokenizerService = tokenizerService;
}
@GetMapping("/count")
public int count(@RequestParam String text) {
return tokenizerService.countTokens(text);
}
}
Der Aufruf GET /tokens/count?text=Order service xử lý thanh toán gibt die Anzahl der Token zurück, die der BERT-Tokenizer für diesen Satz erzeugt.
9. Tests schreiben
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
class TokenizerServiceTest {
@Test
void shouldEncodeAndDecodeRoundTrip() throws Exception {
TokenizerService service = new TokenizerService();
String original = "payment failed";
long[] ids = service.encode(original);
String decoded = service.decode(ids);
assertThat(ids).isNotEmpty();
assertThat(decoded).contains("payment").contains("failed");
}
@Test
void shouldCountMoreTokensThanWordsForSubwordSplit() throws Exception {
TokenizerService service = new TokenizerService();
int tokenCount = service.countTokens("unbelievable circumstances");
// "unbelievable" wird normalerweise in mehrere Subword aufgeteilt
assertThat(tokenCount).isGreaterThan(2);
}
}
- Test 1 prüft die Round-Trip-Kodierung → Dekodierung bewahrt den ursprünglichen Inhalt (Groß-/Kleinschreibung und Interpunktion können vom
uncased-Tokenizer verloren gehen). - Test 2 bestätigt das Subword-Verhalten: lange, seltene Wörter werden in mehr Token aufgeteilt als Wörter, die durch Leerzeichen getrennt sind.
10. Häufige Fehler und wann man es nicht verwenden sollte
- Verwechslung von Tokenizern zwischen verschiedenen Modell-Familien: Der
bert-base-uncased-Tokenizer ist nicht mitgpt2kompatibel. Laden Sie immer einen Tokenizer mit dem gleichen Namen wie das Modell, das Sie für Inferenzen verwenden. - Berechnung von Attention Mask beim Padding vergessen: führt zu fehlerhaften Modellergebnissen, aber keine Exception, sehr schwer zu debuggen. Geben Sie
attentionMaskzusammen mitinputIdsan das Modell weiter. - Tokenizer bei jedem Request laden: verschwendet Zeit und unnötige I/O. Deklarieren Sie immer einen Singleton-Bean wie
TokenizerServiceoben. - Keine Internetverbindung in der Produktionsumgebung: Das erste Laden des Tokenizers benötigt Internetzugriff, um die Datei vom Hugging Face Hub zu ziehen. In einer Offline-Umgebung müssen Sie
tokenizer.jsonvorher herunterladen und über einen lokalen Pfad mitHuggingFaceTokenizer.newInstance(Path.of(...))laden. - Token für Nicht-Hugging-Face-Modelle zählen (z. B. GPT-4 über OpenAI API): Der BPE-Tokenizer von GPT unterscheidet sich vom Standard-Hugging-Face-Tokenizer. In diesem Fall sollten Sie die Token-Zähl-Bibliothek des Anbieters verwenden, nicht
HuggingFaceTokenizer. - Vietnamesischer Text mit diakritischen Zeichen: Einige Tokenizer, die hauptsächlich auf Englisch trainiert wurden, können vietnamesische Wörter nicht optimal aufteilen (zu viele unbekannte Subwords). Wenn die Verarbeitung von Vietnamesisch ein Schwerpunkt ist, wählen Sie ein Modell/einen Tokenizer, der für Vietnamesisch feinabgestimmt wurde (z. B.
vinai/phobert-base), statt das ursprüngliche BERT zu verwenden.
11. Fazit
Der Tokenizer ist ein grundlegender Schritt, der bestimmt, wie das Modell Text „sieht” — das richtige Verständnis der BPE/WordPiece/SentencePiece-Mechanismen hilft Ihnen, schwer zu debuggende Fehler wie Vokabular-Mismatches oder falsche Attention Masks zu vermeiden. Mit der DJL-Bibliothek können Sie direkt in Java kodieren/dekodieren, ohne einen separaten Python-Server aufzubauen. Sie können sie sauber als normalen Bean in Spring Boot integrieren. Denken Sie daran: Verwenden Sie immer den richtigen Tokenizer für Ihr Modell, cachen Sie den Tokenizer als Singleton, und überprüfen Sie gründlich Padding/Truncation, bevor Sie Daten an das Modell übergeben.