Tokenizer dans Hugging Face : Principes et utilisation en Java
1. Aperçu
Lorsque vous travaillez avec des modèles NLP sur Hugging Face, la première étape est toujours la tokenization — transformer le texte brut en une séquence de nombres que le modèle comprend. Cet article explique comment fonctionne le tokenizer, les stratégies de tokenization courantes (BPE, WordPiece, SentencePiece), et comment appeler HuggingFaceTokenizer directement depuis Java via la bibliothèque DJL sans avoir besoin d’exécuter Python. Une fois terminé, vous pourrez construire un TokenizerService dans Spring Boot pour encoder/décoder du texte, contrôler le padding/truncation, et écrire des tests pour lui.
2. Qu’est-ce qu’un Tokenizer et pourquoi c’est important
Le tokenizer fait deux choses : diviser la phrase en tokens (qui peuvent être des mots, des sous-mots, ou des caractères), puis mapper chaque token à un ID entier dans le vocabulaire du modèle. Le modèle ne « voit » jamais le texte — il ne voit que cette séquence d’ID.
Par exemple, la phrase "Order service xử lý thanh toán" avec le tokenizer de bert-base-uncased pourrait donner :
tokens: ["order", "service", "xu", "##ly", "thanh", "toan"]
ids: [2344, 2326, 1045, 2135, 7899, 8821]
La notation ##ly signifie que ce token est attaché directement au token précédent (sous-mot). C’est la raison pour laquelle le nombre de tokens est généralement supérieur au nombre de mots — c’est important à prendre en compte quand vous calculez le coût d’appels API ou que vous définissez une limite max_length pour votre modèle.
Remarque : Chaque modèle pré-entraîné est fourni avec son propre tokenizer, entraîné en même temps que le modèle sur le même vocabulaire. Utiliser un tokenizer différent (par exemple, le tokenizer de BERT pour un modèle GPT) produira des ID sans sens, et le modèle fera des prédictions complètement erronées sans vous signaler d’erreur.
3. Stratégies de tokenization courantes
3.1. BPE (Byte-Pair Encoding)
Utilisé dans GPT-2 et RoBERTa. Commence par les caractères, puis répète l’opération de fusion des paires de caractères les plus fréquentes en un nouveau token, jusqu’à atteindre la taille de vocabulaire souhaitée.
3.2. WordPiece
Utilisé dans BERT. Similaire à BPE mais choisit les paires à fusionner en fonction d’une mesure statistique de probabilité plutôt que de la fréquence brute. Les tokens de sous-mots sont marqués avec ## pour indiquer qu’ils s’attachent au mot précédent.
3.3. SentencePiece
Utilisé dans T5, XLNet et ALBERT. Traite l’espace blanc comme un caractère normal (noté ▁), ce qui fonctionne bien pour les langues sans séparation de mots claire comme le japonais et le chinois.
Vous n’avez pas besoin de réimplémenter ces algorithmes — Hugging Face les a déjà empaquetés dans le fichier tokenizer.json qui accompagne chaque modèle. Vous devez juste le charger et l’appeler.
4. Ajouter la dépendance
Pour utiliser le tokenizer directement en Java (sans passer par un serveur Python), ajoutez la bibliothèque DJL Hugging Face Tokenizers :
<dependency>
<groupId>ai.djl.huggingface</groupId>
<artifactId>tokenizers</artifactId>
<version>0.28.0</version>
</dependency>
Cette bibliothèque lit directement le fichier tokenizer.json standard de Hugging Face, sans avoir besoin de passer la JVM par JNI de façon complexe.
5. Encoder du texte de base
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(...): charge le tokenizer depuis Hugging Face Hub (ou cache local) par nom de modèle.encode(...): retourne un objetEncodingcontenant lesids,tokens, etattentionMask.- À la première exécution, le fichier
tokenizer.jsonest téléchargé dans~/.djl.ai/cache, les exécutions suivantes utilisent le cache, aucun accès réseau n’est nécessaire.
Exemple de sortie :
[101, 2344, 2326, 16024, 7909, 102]
[[CLS], order, service, handles, payment, [SEP]]
Notez les deux tokens spéciaux [CLS] (début de phrase) et [SEP] (fin de phrase) qui sont automatiquement ajoutés — c’est la convention de BERT, pas une erreur.
6. Décoder en arrière
long[] ids = encoding.getIds();
String decoded = tokenizer.decode(ids);
System.out.println(decoded);
[CLS] order service handles payment [SEP]
decode est utile pour déboguer : regarder exactement ce que le modèle « voit » après tokenization, ou quand traiter la sortie d’un modèle génératif.
7. Padding, truncation et batch
Quand vous envoyez plusieurs phrases au modèle en même temps, elles doivent avoir la même longueur (padding) et ne pas dépasser la limite du modèle (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: les phrases courtes reçoivent des tokens[PAD](ID 0) pour égaler la longueur de la phrase la plus longue du batch.truncation=true+maxLength=16: les phrases de plus de 16 tokens sont tronquées.attentionMask: une séquence de 1/0 indiquant quels tokens sont réels (1) ou du padding (0) — le modèle utilise ce masque pour ignorer le padding lors du calcul de l’attention.
[101, 2344, 2872, 4652, 102, 0, 0, 0]
[1, 1, 1, 1, 1, 0, 0, 0]
Remarque : Si vous oubliez de définir
attentionMaskquand vous le passez au modèle, la partie padding peut être incluse dans le calcul de l’attention, rendant la sortie incorrecte sans aucune erreur runtime.
8. Intégration dans Spring Boot
Encapsulez le tokenizer dans un bean partagé, en évitant de le recharger à chaque requête (charger le tokenizer depuis Hub/cache prend 100–300ms, c’est à éviter dans le thread de requête).
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;
}
}
- Le constructeur initialise le tokenizer une seule fois quand le bean est créé, Spring gère le cycle de vie singleton.
countTokens: utile quand vous avez besoin de vérifier la limite de longueur avant d’appeler le modèle (par exemple, une description de commande ne dépasse pas 512 tokens).
Utilisation dans un autre 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);
}
}
Appeler GET /tokens/count?text=Order service xử lý thanh toán retournera le nombre de tokens que le tokenizer BERT crée pour cette phrase.
9. Écrire des tests
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" is often split into multiple subwords
assertThat(tokenCount).isGreaterThan(2);
}
}
- Le test 1 vérifie que l’encode → decode conserve le contenu d’origine (peut perdre la casse/ponctuation à cause du tokenizer
uncased). - Le test 2 confirme le comportement des sous-mots : les mots longs et rares seront divisés en plus de tokens que le nombre de mots séparés par des espaces.
10. Erreurs courantes et quand ne pas utiliser
- Confondre les tokenizers entre différents modèles: le tokenizer de
bert-base-uncasedn’est pas compatible avecgpt2. Chargez toujours le tokenizer du même nom que le modèle que vous utilisez pour l’inférence. - Ne pas calculer l’attention mask lors du padding: conduit à des résultats de modèle incorrects sans exception, très difficile à déboguer. Passez toujours
attentionMaskavecinputIdsau modèle. - Charger le tokenizer à chaque requête: gaspille du temps et des I/O inutiles. Déclarez toujours un bean singleton comme
TokenizerServiceci-dessus. - Pas d’accès réseau en environnement production: la première fois, charger le tokenizer a besoin d’internet pour récupérer le fichier depuis Hugging Face Hub. En environnement offline, vous devez télécharger
tokenizer.jsonà l’avance et le charger depuis un chemin local avecHuggingFaceTokenizer.newInstance(Path.of(...)). - Compter les tokens pour un modèle non-Hugging Face (par exemple GPT-4 via OpenAI API): le tokenizer BPE de GPT est différent du tokenizer Hugging Face standard. Dans ce cas, vous devriez utiliser la bibliothèque de comptage de tokens du fournisseur, pas
HuggingFaceTokenizer. - Texte en vietnamien avec diacritiques: certains tokenizers entraînés principalement en anglais peuvent diviser le vietnamien de manière non optimale (trop de sous-mots inconnus). Si le vietnamien est votre cas d’usage principal, choisissez un modèle/tokenizer fine-tuné pour le vietnamien (par exemple
vinai/phobert-base) plutôt que BERT original.
11. Conclusion
Le tokenizer est une étape fondamentale qui détermine comment le modèle « voit » le texte — comprendre les mécanismes de BPE/WordPiece/SentencePiece vous aide à éviter les erreurs difficiles à déboguer comme l’incompatibilité de vocabulaire ou le masque d’attention incorrect. Avec la bibliothèque DJL, vous pouvez encoder/décoder directement en Java sans avoir besoin de mettre en place un serveur Python séparé, en l’intégrant dans Spring Boot comme un bean ordinaire. N’oubliez pas : utilisez toujours le bon tokenizer pour votre modèle, mettez le tokenizer en cache en tant que singleton, et vérifiez attentivement le padding/truncation avant de passer les données au modèle.