Tokenizador no Hugging Face: Princípios e como usar em Java

1. Visão geral

Ao trabalhar com modelos NLP no Hugging Face, o primeiro passo é sempre a tokenização — transformar texto bruto em uma sequência de números que o modelo entende. Este artigo explica como o tokenizador funciona, as estratégias de divisão de tokens comuns (BPE, WordPiece, SentencePiece), e como chamar HuggingFaceTokenizer diretamente do Java via biblioteca DJL sem precisar executar Python. Ao terminar, você saberá construir um TokenizerService em Spring Boot para codificar/decodificar texto, controlar padding/truncation, e escrever testes para isso.

2. O que é um tokenizador e por que é importante

Um tokenizador faz duas coisas: divide uma sentença em tokens (que podem ser palavras, subwords ou caracteres), depois mapeia cada token para um ID numérico inteiro no vocabulário do modelo. O modelo nunca “vê” letras — ele só vê essa sequência de IDs.

Por exemplo, a sentença "Pedido de serviço processa pagamento" com o tokenizador do bert-base-uncased pode gerar:

tokens: ["pedido", "de", "serviço", "pro", "##cessa", "pagamento"]
ids:    [2344, 2326, 1045, 2135, 7899, 8821]

A notação ##cessa significa que este token está ligado ao token anterior (subword). É por isso que o número de tokens é geralmente maior que o número de palavras — isso é importante quando você calcula o custo de chamar uma API ou define o max_length do modelo.

Nota: Cada modelo pré-treinado vem com seu próprio tokenizador, treinado junto com o modelo no mesmo vocabulário. Usar o tokenizador errado (por exemplo, tokenizador BERT para modelo GPT) produzirá IDs sem sentido e o modelo fará previsões completamente erradas sem relatar nenhum erro.

3. Estratégias de tokenização comuns

3.1. BPE (Byte-Pair Encoding)

Usado em GPT-2, RoBERTa. Começa com caracteres e repetidamente mescla os pares de caracteres que aparecem com mais frequência em um novo token, até atingir o tamanho do vocabulário desejado.

3.2. WordPiece

Usado em BERT. Similar ao BPE, mas seleciona pares para mesclar com base em probabilidade estatística em vez de frequência bruta. Tokens sub-palavras são marcados com ## para indicar que estão ligados à palavra anterior.

3.3. SentencePiece

Usado em T5, XLNet, ALBERT. Trata espaços em branco como um caractere normal (representado como ▁), por isso funciona bem com idiomas sem limites de palavra claros, como japonês e chinês.

Você não precisa implementar esses algoritmos — o Hugging Face já os empacotou no arquivo tokenizer.json que vem com cada modelo. Seu trabalho é apenas carregar e chamar.

4. Adicionar dependência

Para usar o tokenizador diretamente em Java (sem passar por um servidor Python), adicione a biblioteca DJL Hugging Face Tokenizers:

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

Esta biblioteca lê diretamente o arquivo tokenizer.json padrão do Hugging Face, sem a necessidade de executar JVM via JNI complexo.

5. Codificar texto básico

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("Pedido de serviço processa pagamento");

        System.out.println(Arrays.toString(encoding.getIds()));
        System.out.println(Arrays.toString(encoding.getTokens()));
    }
}
  • HuggingFaceTokenizer.newInstance(...): carrega o tokenizador do Hugging Face Hub (ou cache local) pelo nome do modelo.
  • encode(...): retorna um objeto Encoding contendo ids, tokens, attentionMask.
  • Na primeira execução, o arquivo tokenizer.json será baixado para ~/.djl.ai/cache; nas execuções seguintes, o cache é usado e não é necessário acesso à rede.

Saída de referência:

[101, 2344, 2326, 16024, 7909, 102]
[[CLS], pedido, de, serviço, processa, pagamento, [SEP]]

Note os dois tokens especiais [CLS] (início de sentença) e [SEP] (fim de sentença) que são inseridos automaticamente — este é um padrão BERT, não um erro.

6. Decodificar de volta

long[] ids = encoding.getIds();
String decoded = tokenizer.decode(ids);
System.out.println(decoded);
[CLS] pedido de serviço processa pagamento [SEP]

decode é útil para debug: visualizar exatamente o que o modelo “vê” após tokenização, ou ao processar saída gerada por modelos generativos.

7. Padding, truncation e lote

Quando você envia múltiplas sentenças para o modelo simultaneamente, elas devem ter o mesmo comprimento (padding) e não exceder o limite do 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[] {
        "Pedido colocado com sucesso",
        "Pagamento falhou devido ao saldo insuficiente"
});

for (Encoding e : batch) {
    System.out.println(Arrays.toString(e.getIds()));
    System.out.println(Arrays.toString(e.getAttentionMask()));
}
  • padding=true: sentenças curtas recebem tokens [PAD] (ID 0) para igualar o comprimento das sentenças mais longas no lote.
  • truncation=true + maxLength=16: sentenças mais longas que 16 tokens são truncadas.
  • attentionMask: sequência de 1/0 marcando qual token é real (1) ou padding (0) — o modelo usa essa máscara para ignorar partes de padding ao calcular atenção.
[101, 2344, 2872, 4652, 102, 0, 0, 0]
[1, 1, 1, 1, 1, 0, 0, 0]

Nota: Se esquecer de definir attentionMask ao passar para o modelo, a parte de padding pode ser incluída no cálculo de atenção, causando saídas incorretas sem relatar nenhum erro em tempo de execução.

8. Integração com Spring Boot

Encapsule o tokenizador em um bean compartilhado para evitar recarregar a cada requisição (carregar o tokenizador do Hub/cache leva 100–300ms, não deve ser feito na thread de requisição).

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;
    }
}
  • O construtor inicializa o tokenizador apenas uma vez quando o bean é criado, e Spring gerencia o ciclo de vida como singleton.
  • countTokens: útil para verificar limites de comprimento de entrada antes de chamar o modelo (por exemplo, descrição de pedido não deve exceder 512 tokens).

Uso em outro serviço:

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

Chamar GET /tokens/count?text=Pedido de serviço processa pagamento retornará o número de tokens que o tokenizador BERT cria para essa sentença.

9. Escrever testes

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 = "pagamento falhou";
        long[] ids = service.encode(original);
        String decoded = service.decode(ids);

        assertThat(ids).isNotEmpty();
        assertThat(decoded).contains("pagamento").contains("falhou");
    }

    @Test
    void shouldCountMoreTokensThanWordsForSubwordSplit() throws Exception {
        TokenizerService service = new TokenizerService();

        int tokenCount = service.countTokens("circunstâncias inacreditáveis");

        // "inacreditáveis" geralmente é dividida em múltiplos subwords
        assertThat(tokenCount).isGreaterThan(2);
    }
}
  • Teste 1 verifica se a codificação → decodificação round-trip preserva o conteúdo original (pode perder maiúsculas/acentos devido ao tokenizador uncased).
  • Teste 2 confirma o comportamento de subword: palavras longas e incomuns são divididas em mais tokens do que o número contado por espaços em branco.

10. Erros comuns e quando não usar

  • Misturar tokenizadores entre diferentes modelos: o tokenizador de bert-base-uncased não é compatível com gpt2. Sempre carregue o tokenizador com o mesmo nome do modelo que você usa para inferência.
  • Não calcular attention mask com padding: resulta em saída incorreta do modelo, mas sem exceção — muito difícil de debugar. Sempre passe attentionMask junto com inputIds para o modelo.
  • Carregar tokenizador a cada requisição: desperdiça tempo e I/O desnecessários. Sempre declare um bean singleton como TokenizerService acima.
  • Sem rede no ambiente de produção: a primeira vez que carrega o tokenizador requer internet para baixar arquivo do Hugging Face Hub. Em ambientes offline, você precisa baixar tokenizer.json antecipadamente e carregá-lo de um caminho local usando HuggingFaceTokenizer.newInstance(Path.of(...)).
  • Contar tokens para modelo que não é Hugging Face (por exemplo, GPT-4 via OpenAI API): o tokenizador BPE do GPT é diferente do tokenizador Hugging Face padrão. Neste caso, use a biblioteca de contagem de tokens específica do provedor, não HuggingFaceTokenizer.
  • Texto em vietnamita com marcas diacríticas: alguns tokenizadores treinados principalmente em inglês podem dividir texto vietnamita de forma não ideal (muito subword estranho). Se processar vietnamita é prioritário, escolha um modelo/tokenizador fine-tuned para vietnamita (por exemplo, vinai/phobert-base) em vez de BERT original.

11. Conclusão

O tokenizador é um passo fundamental que determina como o modelo “vê” o texto — compreender os mecanismos de BPE/WordPiece/SentencePiece ajuda a evitar erros difíceis de debugar como desajustes de vocabulário ou máscara de atenção incorreta. Com a biblioteca DJL, você pode codificar/decodificar diretamente em Java sem precisar configurar um servidor Python separado, integrando-se perfeitamente em Spring Boot como um bean comum. Lembre-se: sempre use o tokenizador correto do modelo, cache o tokenizador como singleton, e verifique cuidadosamente padding/truncation antes de passar dados para o modelo.

huggingfacenlptokenizerjavaspring-boot