Tokenizer en Hugging Face: Principios y uso en Java

1. Descripción general

Cuando trabajas con modelos NLP en Hugging Face, el primer paso es siempre la tokenización — convertir texto crudo en una secuencia de números que el modelo entienda. Este artículo explica cómo funciona el tokenizer, las estrategias comunes de tokenización (BPE, WordPiece, SentencePiece), y cómo llamar a HuggingFaceTokenizer directamente desde Java a través de la librería DJL sin necesidad de ejecutar Python. Al terminar, construirás un TokenizerService en Spring Boot para codificar/decodificar texto, controlar padding/truncation, y escribir tests para él.

2. ¿Qué es un tokenizer y por qué es importante

El tokenizer hace dos cosas: divide una oración en tokens (que pueden ser palabras, subpalabras o caracteres), y luego mapea cada token a un ID entero en el vocabulario del modelo. El modelo nunca “ve” letras — solo ve esta secuencia de IDs.

Por ejemplo, la oración "Order service xử lý thanh toán" con el tokenizer de bert-base-uncased podría producir:

tokens: ["order", "service", "xu", "##ly", "thanh", "toan"]
ids:    [2344, 2326, 1045, 2135, 7899, 8821]

La notación ##ly significa que este token se adhiere directamente al token anterior (subpalabra). Esta es la razón por la cual el número de tokens es a menudo mayor que el número de palabras — algo importante cuando calculas el costo de llamadas a API o estableces el max_length del modelo.

Nota: Cada modelo preentrenado viene con su propio tokenizer, entrenado simultáneamente con el modelo en el mismo vocabulario. Usar un tokenizer incorrecto (por ejemplo, el tokenizer de BERT para un modelo GPT) generará IDs sin sentido, el modelo hará predicciones incorrectas completamente sin mostrar ningún error en tiempo de ejecución.

3. Estrategias comunes de tokenización

3.1. BPE (Byte-Pair Encoding)

Se usa en GPT-2, RoBERTa. Comienza con caracteres y repite el proceso de fusionar los pares de caracteres más frecuentes en un nuevo token, hasta alcanzar el tamaño de vocabulario deseado.

3.2. WordPiece

Se usa en BERT. Similar a BPE pero elige qué pares fusionar basándose en la probabilidad estadística en lugar de la frecuencia bruta. Los tokens sub-palabras se marcan con ## para indicar que se adhieren a la palabra anterior.

3.3. SentencePiece

Se usa en T5, XLNet, ALBERT. Trata los espacios en blanco como un carácter normal (representado como ▁), por lo que funciona mejor para idiomas sin separación de palabras clara como el japonés o chino.

No necesitas implementar estos algoritmos por ti mismo — Hugging Face ya los empaqueta en el archivo tokenizer.json que viene con cada modelo; tu trabajo es simplemente cargar e invocarlo.

4. Agregar dependencia

Para usar el tokenizer directamente en Java (sin pasar por un servidor Python), añade la librería DJL Hugging Face Tokenizers:

<dependency>
    <groupId>ai.djl.huggingface</groupId>
    <artifactId>tokenizers</artifactId>
    <version>0.28.0</version>
</dependency>

Esta librería lee directamente el archivo tokenizer.json estándar de Hugging Face sin necesidad de complicadas llamadas JNI desde la JVM.

5. Codificar texto básicamente

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(...): carga el tokenizer de Hugging Face Hub (o caché local) por nombre del modelo.
  • encode(...): devuelve un objeto Encoding que contiene ids, tokens, attentionMask.
  • La primera ejecución descargará el archivo tokenizer.json a ~/.djl.ai/cache, las ejecuciones posteriores usarán la caché sin necesidad de internet.

Salida de referencia:

[101, 2344, 2326, 16024, 7909, 102]
[[CLS], order, service, handles, payment, [SEP]]

Ten en cuenta los dos tokens especiales [CLS] (inicio de oración) y [SEP] (fin de oración) que se insertan automáticamente — esta es una convención de BERT, no un error.

6. Decodificar de vuelta

long[] ids = encoding.getIds();
String decoded = tokenizer.decode(ids);
System.out.println(decoded);
[CLS] order service handles payment [SEP]

decode es útil cuando necesitas depurar: ver exactamente qué “ve” el modelo después de tokenizar, o al procesar salida de modelos generativos.

7. Padding, truncation y batch

Cuando envías múltiples oraciones al modelo simultáneamente, deben tener la misma longitud (padding) y no exceder el límite del modelo (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: las oraciones cortas se rellenan con tokens [PAD] (ID 0) hasta igualarse con la oración más larga del batch.
  • truncation=true + maxLength=16: las oraciones más largas de 16 tokens se truncan.
  • attentionMask: una secuencia de 1/0 que marca qué tokens son reales (1) o padding (0) — el modelo usa esta máscara para ignorar la parte de padding al calcular la atención.
[101, 2344, 2872, 4652, 102, 0, 0, 0]
[1, 1, 1, 1, 1, 0, 0, 0]

Nota: Si olvidas establecer attentionMask al pasar datos al modelo, la parte de padding puede incluirse en el cálculo de atención, haciendo que la salida sea incorrecta sin mostrar ningún error en tiempo de ejecución.

8. Integración en Spring Boot

Empaqueta el tokenizer como un bean compartido, evitando recargarlo en cada request (cargar el tokenizer desde Hub/caché toma 100-300ms, no debería hacerse dentro del hilo de request).

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;
    }
}
  • El constructor inicializa el tokenizer una única vez cuando se crea el bean, Spring gestiona su ciclo de vida como singleton.
  • countTokens: útil cuando necesitas verificar los límites de longitud del input antes de llamar al modelo (por ejemplo, Order description no debe exceder 512 tokens).

Uso en otro servicio:

@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);
    }
}

Llamar a GET /tokens/count?text=Order service xử lý thanh toán devolverá el número de tokens que el tokenizer de BERT crea para esa oración.

9. Escribir 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" típicamente se divide en múltiples subpalabras
        assertThat(tokenCount).isGreaterThan(2);
    }
}
  • Test 1 verifica que el viaje de ida y vuelta encode → decode conserve el contenido original (puede perder mayúsculas/puntuación debido al tokenizer uncased).
  • Test 2 confirma el comportamiento de subpalabra: palabras largas e infrecuentes se dividen en más tokens que el conteo de palabras por espacios en blanco.

10. Errores comunes y cuándo no usar esta aproximación

  • Confundir tokenizers entre diferentes modelos: el tokenizer de bert-base-uncased no es compatible con gpt2. Siempre carga el tokenizer con el mismo nombre que el modelo que usas para inferencia.
  • No calcular attention mask cuando usas padding: resulta en salida de modelo incorrecta pero sin excepciones, muy difícil de depurar. Siempre pasa attentionMask junto con inputIds al modelo.
  • Cargar tokenizer en cada request: desperdicia tiempo e I/O innecesarios. Siempre declara un bean singleton como TokenizerService arriba.
  • Sin red en ambiente de producción: la primera carga del tokenizer necesita internet para descargar el archivo de Hugging Face Hub. En ambientes offline, necesitas descargar tokenizer.json de antemano y cargar desde una ruta local usando HuggingFaceTokenizer.newInstance(Path.of(...)).
  • Contar tokens para modelos que no son de Hugging Face (ej. GPT-4 vía OpenAI API): el tokenizer BPE de GPT es diferente del tokenizer estándar de Hugging Face. En este caso, deberías usar la librería de conteo de tokens propia del proveedor, no HuggingFaceTokenizer.
  • Texto en vietnamita con diacríticos: algunos tokenizers entrenados principalmente en inglés pueden dividir mal el vietnamita (demasiadas subpalabras extrañas). Si procesas vietnamita es fundamental, elige un modelo/tokenizer fine-tuned para vietnamita (ej. vinai/phobert-base) en lugar de BERT original.

11. Conclusión

El tokenizer es un paso fundamental que determina cómo el modelo “ve” el texto — entender bien los mecanismos de BPE/WordPiece/SentencePiece te ayuda a evitar errores difíciles de depurar como vocabulario que no coincide o attention mask incorrecta. Con la librería DJL, puedes codificar/decodificar directamente en Java sin necesidad de montar un servidor Python separado, integrado limpiamente en Spring Boot como un bean normal. Recuerda: siempre usa el tokenizer correcto del modelo, cachea el tokenizer como singleton, y verifica cuidadosamente el padding/truncation antes de pasar datos al modelo.

huggingfacenlptokenizerjavaspring-boot