7.4K 66K 36

Detecção de Idiomas com XLM-RoBERTa Base e LoRA

Como desenvolvi um classificador de 20 idiomas com XLM-RoBERTa Base e um adaptador LoRA, com 99,43% de acurácia publicada no teste balanceado.

Como desenvolvi a detecção de idiomas com XLM-RoBERTa Base e LoRA

Identificar o idioma de um texto parece simples quando os exemplos são longos, monolíngues e escritos em alfabetos distintos. O problema fica mais interessante com frases curtas, idiomas aparentados, transliteração, ruído de redes sociais ou mistura de idiomas. Neste projeto, transformei o xlm-roberta-base em um classificador fechado de 20 classes por meio de Low-Rank Adaptation (LoRA): o encoder pré-treinado permanece congelado, pequenas matrizes treináveis são inseridas nas projeções de atenção e a cabeça de classificação é aprendida para a tarefa [1–5].

O resultado autorreportado no Hugging Face é 99,43% de acurácia e F1 no teste balanceado do conjunto papluca/language-identification [2, 6]. O número deve ser lido dentro desse protocolo: são 10.000 exemplos, 500 por idioma, provenientes das mesmas famílias de fontes usadas no treinamento. Não é uma promessa de 99,43% para qualquer texto da internet. O artefato também não é um checkpoint autônomo: é um adaptador PEFT de cerca de 3,6 MB que precisa do modelo-base e da configuração de classificação [2].

Escopo e artefatos

Publiquei o projeto em três superfícies complementares:

Os 20 rótulos ISO 639-1 são: árabe (ar), búlgaro (bg), alemão (de), grego moderno (el), inglês (en), espanhol (es), francês (fr), hindi (hi), italiano (it), japonês (ja), neerlandês (nl), polonês (pl), português (pt), russo (ru), suaíli (sw), tailandês (th), turco (tr), urdu (ur), vietnamita (vi) e chinês (zh). Qualquer entrada sempre será projetada sobre uma dessas classes: não existem rótulos unknown, mixed ou para outros idiomas.

Renderizando diagrama...

Dados: origem, equilíbrio e divisões

Treinei com 90.000 passagens rotuladas por idioma, combinadas a partir de Multilingual Amazon Reviews, XNLI e STSb Multi MT [6]. Agrupei os idiomas conforme a origem: Amazon fornece en, de, fr, es, ja e zh; XNLI fornece ar, el, hi, ru, th, tr, vi, bg, sw e ur; STSb Multi MT fornece it, nl, pl e pt [1]. Essa composição importa porque idioma e domínio podem ficar correlacionados: o modelo pode aprender, além de traços linguísticos, padrões de avaliações, inferência textual ou similaridade de sentenças.

As divisões oficiais são:

DivisãoTotalPor idiomaUso no projeto
train70.0003.500ajuste dos parâmetros LoRA e do classificador
validation / valid10.000500avaliação a cada época e seleção do melhor checkpoint
test10.000500avaliação final e comparação com langid.py

O equilíbrio perfeito torna a acurácia fácil de interpretar e dá o mesmo peso a cada idioma no F1 macro. Contudo, ele não representa necessariamente tráfego real, no qual inglês ou português podem dominar. Um teste de produção deve manter uma amostra temporal, incluir a prevalência real e impedir duplicatas ou documentos quase idênticos entre treino e avaliação. A ficha informa que não há dados pessoais dos autores ou anotadores, mas herda os possíveis vieses das três fontes e não documenta uma licença para o dataset [6].

XLM-RoBERTa como encoder multilíngue

XLM-RoBERTa (XLM-R) é um Transformer encoder pré-treinado por masked language modeling em 2,5 TB de CommonCrawl filtrado, cobrindo 100 idiomas [3, 4]. Ao contrário de modelos que exigem um identificador de idioma na entrada, XLM-R compartilha representações e vocabulário entre línguas. O artigo original também destaca que não usar embeddings explícitos de idioma favorece o tratamento de code-switching no encoder; isso não significa, porém, que esta cabeça supervisionada consiga devolver mais de um idioma [3].

Usei a variante Base, com 12 camadas, estado oculto de 768 dimensões, 12 cabeças de atenção por camada, bloco feed-forward de 3.072 dimensões e vocabulário SentencePiece de 250.002 itens [2–4]. Limitei cada exemplo a 128 tokens, bem abaixo das 514 posições disponíveis. O token inicial <s> resume a sequência para a cabeça de classificação, que produz 20 logits; uma softmax os converte em uma distribuição fechada sobre os rótulos.

Usei a cabeça de XLMRobertaForSequenceClassification: dropout, uma projeção densa de 768 para 768, tanh, novo dropout e a projeção final de 768 para 20 classes [11]. No model card, resumi essa estrutura como uma camada linear sobre a saída agrupada. A implementação completa explica por que modules_to_save=["classifier"] adiciona mais parâmetros treináveis do que apenas uma matriz 768 × 20.

LoRA: atualização de baixo posto

No ajuste completo, uma matriz pré-treinada W0 de dimensões d × k receberia uma atualização densa Delta W, com d × k parâmetros. LoRA congela W0 e representa a atualização pelo produto de duas matrizes de posto r, com r muito menor que d e k [5]:

W=W0+ΔW,ΔW=αrBA,h=W0x+αrBAx,BRd×r,ARr×k.\begin{aligned} W &= W_0+\Delta W, & \Delta W &= \frac{\alpha}{r}BA, \\ h &= W_0x+\frac{\alpha}{r}BAx, & B &\in\mathbb{R}^{d\times r},\quad A\in\mathbb{R}^{r\times k}. \end{aligned}

O custo treinável por matriz cai de d × k para r(d + k). A redução teórica é:

reducao=1r(d+k)dk.\text{reducao}=1-\frac{r(d+k)}{dk}.

Neste projeto, d = k = 768 e r = 8. Uma projeção densa tem 589.824 pesos; seu par LoRA tem 12.288, uma redução de 97,92%, ou 48 vezes menos parâmetros para aquela atualização. Como query e value são adaptadas nas 12 camadas, as matrizes LoRA somam 294.912 parâmetros. A cabeça salva separadamente soma 605.972; por isso o notebook registra 900.884 treináveis entre 278.353.940 parâmetros totais, isto é, 0,32365% treinável ou aproximadamente 309 vezes menos que ajustar o conjunto inteiro [1].

Configurei o LoRA assim:

from peft import LoraConfig
 
lora_config = LoraConfig(
    task_type="SEQ_CLS",
    inference_mode=False,
    r=8,
    lora_alpha=16,
    target_modules=["query", "value"],
    lora_dropout=0.1,
    bias="none",
    modules_to_save=["classifier"],
)

O fator aplicado à atualização é alpha/r = 16/8 = 2. bias="none" mantém vieses fora do ajuste LoRA, enquanto modules_to_save treina e persiste a cabeça inteira. A inicialização padrão torna a contribuição LoRA inicialmente nula; durante a inferência, PEFT combina a base congelada, as atualizações de baixo posto e o classificador salvo [5, 7].

Treinamento e reprodutibilidade

No notebook, carreguei o dataset, converti os rótulos em IDs ordenados alfabeticamente, tokenizei em lotes com truncamento em 128 tokens e apliquei padding dinâmico no collate_fn. Treinei com a API Trainer, avaliação e salvamento ao fim de cada época, seleção do melhor checkpoint pela acurácia e precisão mista FP16 [1]. Usei estes hiperparâmetros:

HiperparâmetroValor
épocas2
learning rate2e-5
batch por dispositivo, treino64
batch por dispositivo, avaliação128
otimizadoradamw_torch
precisão mistaFP16
comprimento máximo128 tokens

A execução preservada usou uma NVIDIA A100-SXM4 de 40 GB e completou 2.188 passos em 2min59s. As versões registradas foram PyTorch 1.13.1+cu116, Datasets 2.10.1, scikit-learn 1.2.1, Transformers 4.27.0.dev0, langid 1.1.6 e PEFT 0.3.0.dev0 [1]. Esses dados são históricos, não requisitos rígidos. Em uma reprodução moderna, fixe versões compatíveis do modelo, do dataset e das bibliotecas, além das sementes; então compare os logits em uma amostra antes de atualizar dependências.

Na validação, a primeira época registrou loss 0,059124, acurácia 0,9952 e F1 ponderado 0,995203. A segunda registrou loss 0,027223, acurácia 0,9955 e F1 0,995502. A avaliação Trainer no teste do notebook retornou loss 0,029861, acurácia 0,9946 e F1 ponderado 0,994605 [1]. Já no model card e em seu model-index, publiquei 99,43% para acurácia e F1 [2]. Como não registrei a causa dos três acertos de diferença, trato 99,43% como resultado canônico da publicação e 99,46% apenas como saída daquela execução do notebook.

Métricas e comparação concreta

Para N exemplos e C = 20 classes, as métricas são:

Accuracy=1Ni=1N1[y^i=yi],Precisionc=TPcTPc+FPc,Recallc=TPcTPc+FNc,F1c=2PrecisioncRecallcPrecisionc+Recallc,Macro-F1=1Cc=1CF1c.\begin{aligned} \mathrm{Accuracy}&=\frac{1}{N}\sum_{i=1}^{N}\mathbf{1}[\hat y_i=y_i],\\ \mathrm{Precision}_c&=\frac{TP_c}{TP_c+FP_c},\\ \mathrm{Recall}_c&=\frac{TP_c}{TP_c+FN_c},\\ F1_c&=\frac{2\,\mathrm{Precision}_c\,\mathrm{Recall}_c}{\mathrm{Precision}_c+\mathrm{Recall}_c},\\ \mathrm{Macro\text{-}F1}&=\frac{1}{C}\sum_{c=1}^{C}F1_c. \end{aligned}

Como cada classe tem 500 exemplos, F1 macro e ponderado recebem pesos iguais; no card ambos arredondam para o mesmo 99,43% da acurácia. Isso é uma propriedade observada neste resultado, não uma identidade geral entre acurácia e F1.

SistemaAdaptaçãoIdiomas candidatosAcurácia no teste
langid.pymodelo Naive Bayes pré-treinado, restringido com set_languages20 de 9798,5%
XLM-RoBERTa Base + LoRA0,32365% dos parâmetros treináveis2099,43%
papluca/xlm-roberta-base-language-detectionajuste completo2099,6%

No mesmo teste, o adaptador supera o baseline langid.py em 0,93 ponto percentual e fica 0,17 ponto abaixo do XLM-R ajustado integralmente descrito por Papariello [2, 6, 8]. A comparação evidencia o compromisso: LoRA retém quase todo o resultado do ajuste completo com uma fração pequena de parâmetros específicos da tarefa. langid.py, por sua vez, é muito menor e foi projetado para velocidade, robustez entre domínios e implantação com poucas dependências; seu modelo usa seleção de n-gramas de bytes e Naive Bayes, treinado em cinco fontes e 97 idiomas [9, 10].

Resultados por idioma

O relatório publicado por classe mostra F1 mais baixo em hindi (0,972), urdu (0,982), português (0,987), polonês (0,990), inglês (0,992) e italiano (0,992) [2]. Para hindi, recall 1,000 e precisão 0,945 indicam que os exemplos hindi foram recuperados, mas textos de outras classes também receberam hi. Para urdu ocorre o inverso: precisão 1,000 e recall 0,964 indicam perdas de exemplos urdu para outras classes. Português combina precisão 0,988 e recall 0,986. Alemão, grego, japonês, tailandês, turco, vietnamita e chinês aparecem com F1 1,000 nesse teste.

Esses números não revelam quais pares foram confundidos. Publiquei classification_report, mas não a matriz de confusão nem os textos errados; por isso, não afirmo, por exemplo, que português foi confundido com espanhol. Na próxima análise, pretendo preservar y_true, y_pred, fonte, tamanho e texto, gerar a matriz sem normalização e outra normalizada por linha e revisar os maiores blocos fora da diagonal. Também pretendo estratificar os resultados por fonte, comprimento, alfabeto e presença de URLs, porque uma média global pode esconder um subconjunto frágil.

from sklearn.metrics import classification_report, confusion_matrix
 
labels = ["ar", "bg", "de", "el", "en", "es", "fr", "hi", "it", "ja",
          "nl", "pl", "pt", "ru", "sw", "th", "tr", "ur", "vi", "zh"]
 
print(classification_report(y_true, y_pred, labels=labels, digits=4))
counts = confusion_matrix(y_true, y_pred, labels=labels)
by_true_language = confusion_matrix(
    y_true, y_pred, labels=labels, normalize="true"
)

Carregamento e inferência fundamentada

Instale versões compatíveis de PyTorch, Transformers e PEFT. O carregamento explícito abaixo é deliberado: adapter_config.json aponta para xlm-roberta-base, enquanto o config.json do repositório do adaptador contém a cabeça de 20 classes e os mapas id2label/label2id. Carregar apenas a base sem essa configuração pode criar uma cabeça com dimensões erradas.

import torch
from peft import PeftConfig, PeftModel
from transformers import AutoConfig, AutoModelForSequenceClassification, AutoTokenizer
 
ADAPTER_ID = "dominguesm/xlm-roberta-base-lora-language-detection"
 
peft_config = PeftConfig.from_pretrained(ADAPTER_ID)
task_config = AutoConfig.from_pretrained(ADAPTER_ID)
base = AutoModelForSequenceClassification.from_pretrained(
    peft_config.base_model_name_or_path,
    config=task_config,
)
tokenizer = AutoTokenizer.from_pretrained(peft_config.base_model_name_or_path)
model = PeftModel.from_pretrained(base, ADAPTER_ID).eval()
 
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)
 
texts = [
    "Cada qual sabe amar a seu modo.",
    "Een man zingt en speelt gitaar.",
]
batch = tokenizer(
    texts,
    padding=True,
    truncation=True,
    max_length=128,
    return_tensors="pt",
).to(device)
 
with torch.inference_mode():
    probabilities = model(**batch).logits.softmax(dim=-1)
 
top_probability, top_id = probabilities.max(dim=-1)
predictions = [
    {"label": model.config.id2label[i.item()], "score": p.item()}
    for i, p in zip(top_id, top_probability)
]
print(predictions)

O score é uma probabilidade softmax relativa às 20 alternativas, não uma confiança calibrada nem evidência de que o idioma está no conjunto. Não copie o valor numérico de uma execução antiga como expectativa contratual: hardware, versões e texto exato podem alterar os últimos dígitos.

Renderizando diagrama...

Escopo atual: OOD, textos curtos e code-switching

Fora de distribuição (OOD). Um texto em catalão, coreano ou indonésio receberá um dos 20 rótulos mesmo que o encoder conheça esses idiomas no pré-treinamento. Emojis, hashtags, código-fonte, números, URLs e strings vazias também não têm classe própria. Crie um conjunto OOD separado com idiomas não suportados e entradas não linguísticas; meça a taxa de falsa aceitação e calibre uma regra de abstenção em validação. Entropia, maior probabilidade e margem entre top-1 e top-2 são sinais úteis, mas nenhum limiar é publicado pelo projeto e, portanto, nenhum valor universal deve ser inventado.

Textos curtos. Uma palavra como “no”, nomes próprios ou marcas pode ser válida em vários idiomas. Avalie curvas por número de caracteres e tokens. Em aplicações conversacionais, acumular mensagens até obter contexto costuma ser mais defensável do que classificar cada token isoladamente.

Code-switching. O treinamento atribui exatamente um rótulo a cada passagem e não mede segmentos mistos. Para documentos longos, classifique janelas ou sentenças e retenha a distribuição dos rótulos; discordância persistente pode sinalizar mistura. Esse procedimento precisa de um corpus anotado de code-switching para validação. Ele não transforma automaticamente o classificador em detector por token, e a média das probabilidades pode esconder um idioma minoritário.

Mudança de domínio. O teste reutiliza as três famílias de fonte do dataset. Antes da implantação, avalie chats, OCR, consultas de busca, documentos jurídicos ou conteúdo social conforme o domínio real. Compare também variantes regionais, romanização de hindi/urdu, ausência de diacríticos e ataques com caracteres visualmente semelhantes.

Implantação

Para throughput, agrupe requisições com padding dinâmico, mantenha max_length=128 para reproduzir o treino e monitore truncamento. Textos longos devem ser segmentados; classificar apenas os primeiros 128 tokens pode ignorar uma mudança de idioma no restante. Separe a validação de entrada, inferência e política de abstenção para que o modelo não determine sozinho decisões críticas.

Meu adaptador reduz armazenamento por tarefa, mas não elimina o custo da base: o processo ainda carrega XLM-RoBERTa Base. PEFT permite manter uma base residente e trocar adaptadores ou executar merge_and_unload() para incorporar Delta W ao modelo e exportar um checkpoint autônomo [7]. A fusão simplifica o runtime e remove a sobrecarga do wrapper, porém perde a vantagem de distribuir somente 3,62 MB e exige nova validação numérica. Desativei o widget hospedado com inference: false; isso não impede inferência local.

Medi 4,67 s para langid.py e 150,91 s para o pipeline neural em 10.000 textos [1]. Não apresento isso como benchmark de latência entre arquiteturas: não equalizei caminhos de execução, batching e hardware, e usei o pipeline sequencialmente na GPU. Em uma nova medição, pretendo fazer aquecimento, fixar batch, concorrência, dispositivo e comprimentos e reportar p50/p95/p99, memória e itens por segundo. Para rotas sensíveis a custo, langid.py pode ser um primeiro estágio; o Transformer pode processar casos ambíguos, desde que a cascata seja calibrada no domínio.

Monitore distribuição de idiomas, taxa de abstenção, margens, comprimento, truncamento e deriva, sem registrar texto sensível desnecessariamente. Uma amostra revisada por humanos deve alimentar avaliações periódicas, não treinamento automático sem controle, pois feedback enviesado pode reforçar erros.

Oportunidades de evolução, licenças e uso responsável

  • O sistema é um classificador fechado de 20 idiomas, não um detector universal.
  • Avaliei passagens monolíngues e balanceadas; ainda não publiquei métricas para OOD, code-switching, texto muito curto, dialetos, transliteração, robustez adversarial ou calibração.
  • Não publiquei a matriz de confusão nem os exemplos classificados incorretamente; mantenho a análise por pares como uma próxima etapa.
  • O model card informa CC BY 4.0 para o adaptador, enquanto o repositório de código/notebook usa CC BY-SA 4.0. XLM-RoBERTa Base é publicado sob MIT, e a ficha do dataset deixa a licença em branco [1, 2, 4, 6, 12]. Verifique separadamente as obrigações de cada componente antes de redistribuir dados, código, adaptador ou modelo fundido.
  • Os valores do card (99,43%) e da execução do notebook (99,46%) não coincidem exatamente. Preserve versão, configuração e relatório ao reproduzir.
  • Uma pontuação softmax alta pode estar errada. Em moderação, roteamento regulatório ou acesso a serviços, ofereça abstenção, revisão e possibilidade de correção.

Em síntese, demonstrei uma adaptação eficiente e tecnicamente bem delimitada: XLM-R fornece representações multilíngues, LoRA altera somente query e value, a cabeça aprende as 20 classes e o teste balanceado chega a 99,43%. Pondero o ganho de 0,93 ponto sobre langid.py contra uma implantação neural mais pesada. Na próxima versão, pretendo priorizar matriz de confusão, avaliação por domínio, OOD, code-switching e calibração com política explícita de abstenção, em vez de reivindicar mais casas decimais.

Referências

  1. Domingues, M. language-detection-lora: README, notebook e licença. GitHub, 2023.
  2. Domingues, M. xlm-roberta-base-lora-language-detection: model card, adapter config e arquivos. Hugging Face, 2023.
  3. Conneau, A. et al. Unsupervised Cross-lingual Representation Learning at Scale. arXiv:1911.02116, 2019.
  4. Hugging Face. XLM-RoBERTa Base model card.
  5. Hu, E. J. et al. LoRA: Low-Rank Adaptation of Large Language Models. ICLR, 2022.
  6. Papariello, L. Language Identification dataset card. Hugging Face.
  7. Hugging Face. PEFT: documentação de LoRA e carregamento de PeftModel.
  8. Papariello, L. xlm-roberta-base-language-detection model card. Hugging Face.
  9. Lui, M.; Baldwin, T. langid.py: An Off-the-shelf Language Identification Tool. ACL System Demonstrations, p. 25–30, 2012.
  10. Lui, M.; Baldwin, T. langid.py repository and documentation. GitHub.
  11. Hugging Face. Implementação de XLM-RoBERTa no Transformers 4.27.0.
  12. Creative Commons. CC BY 4.0 e CC BY-SA 4.0.

BibTeX

@article{conneau2019xlmr,
  title   = {Unsupervised Cross-lingual Representation Learning at Scale},
  author  = {Conneau, Alexis and Khandelwal, Kartikay and Goyal, Naman and Chaudhary, Vishrav and Wenzek, Guillaume and Guzm\'{a}n, Francisco and Grave, Edouard and Ott, Myle and Zettlemoyer, Luke and Stoyanov, Veselin},
  journal = {arXiv preprint arXiv:1911.02116},
  year    = {2019},
  url     = {https://arxiv.org/abs/1911.02116}
}
 
@inproceedings{hu2022lora,
  title     = {{LoRA}: Low-Rank Adaptation of Large Language Models},
  author    = {Hu, Edward J. and Shen, Yelong and Wallis, Phillip and Allen-Zhu, Zeyuan and Li, Yuanzhi and Wang, Shean and Wang, Lu and Chen, Weizhu},
  booktitle = {International Conference on Learning Representations},
  year      = {2022},
  url       = {https://openreview.net/forum?id=nZeVKeeFYf9}
}
 
@inproceedings{lui-baldwin-2012-langid,
  title     = {langid.py: An Off-the-shelf Language Identification Tool},
  author    = {Lui, Marco and Baldwin, Timothy},
  booktitle = {Proceedings of the ACL 2012 System Demonstrations},
  pages     = {25--30},
  publisher = {Association for Computational Linguistics},
  year      = {2012},
  url       = {https://aclanthology.org/P12-3005/}
}
 
@misc{domingues2023language,
  title  = {XLM-RoBERTa Base LoRA Language Detection},
  author = {Domingues, Maicon},
  year   = {2023},
  url    = {https://huggingface.co/dominguesm/xlm-roberta-base-lora-language-detection},
  note   = {Adapter PEFT-LoRA e notebook de treinamento}
}
 
@misc{papariello_language_identification,
  title  = {Language Identification Dataset},
  author = {Papariello, Luca},
  url    = {https://huggingface.co/datasets/papluca/language-identification},
  note   = {Dataset card no Hugging Face Hub}
}