Text Generation
PEFT
Safetensors
Portuguese
text-classification
community-notes
portuguese
reranker
lora
misinformation
Instructions to use histlearn/community-notes-reranker-ptbr with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PEFT
How to use histlearn/community-notes-reranker-ptbr with PEFT:
Task type is invalid.
- Notebooks
- Google Colab
- Kaggle
| license: apache-2.0 | |
| base_model: Qwen/Qwen3-Reranker-0.6B | |
| language: | |
| - pt | |
| tags: | |
| - text-classification | |
| - community-notes | |
| - portuguese | |
| - reranker | |
| - lora | |
| - peft | |
| - misinformation | |
| pipeline_tag: text-generation | |
| # Community Notes Reranker (PT-BR) | |
| Cross-encoder fine-tunado para prever se uma nota da comunidade do X (antigo Twitter) será avaliada como útil em português brasileiro. Adapters LoRA sobre `Qwen/Qwen3-Reranker-0.6B`. Dado um par `(tweet, nota)`, devolve uma probabilidade no intervalo `[0, 1]`. | |
| ## Por que Qwen3-Reranker | |
| A pergunta central do projeto é **"esta nota responde adequadamente a este tweet?"**. Isso exige um modelo que *compare termo a termo* o que o tweet afirma e o que a nota desmente ou contextualiza — não basta saber se os dois textos falam do mesmo assunto. | |
| A família Qwen3 dedicada a recuperação tem dois ramos com arquiteturas diferentes: | |
| - **Qwen3-Embedding** é um *bi-encoder*: codifica tweet e nota em vetores separados e mede similaridade vetorial. Captura "falam do mesmo assunto", mas perde nuance lógica — não percebe quando uma frase específica da nota desmente uma afirmação específica do tweet. | |
| - **Qwen3-Reranker** é um *cross-encoder*: lê o par `(tweet, nota)` concatenado num único contexto, com atenção cruzada em todas as camadas. Cada token da nota pode atender a cada token do tweet ao longo de toda a rede. | |
| | Aspecto | Bi-Encoder (Embedding) | Cross-Encoder (Reranker) | | |
| |---|---|---| | |
| | Entrada | tweet e nota processados **separadamente** | par `(tweet, nota)` **concatenado** | | |
| | Encoder | um encoder por texto | um encoder único que vê os dois | | |
| | Onde os textos se comparam | **depois** da codificação (similaridade vetorial) | **durante** a codificação (atenção cruzada em todas as camadas) | | |
| | Saída | dois vetores + similaridade | logit conjunto | | |
| | Captura bem | "falam do mesmo assunto" | "esta frase da nota desmente esta afirmação do tweet" | | |
| O Qwen3-Reranker é treinado nativamente como julgador de pares: emite `yes` ou `no` em resposta a uma instrução. Aqui mantemos esse protocolo literal e re-orientamos o `yes` para significar *nota útil*, com loss binária `BCEWithLogitsLoss` sobre `logit("yes") − logit("no")`. O tamanho `0.6B` viabiliza fine-tune fold-a-fold sob `StratifiedGroupKFold(5)` no mesmo regime dos baselines lexicais e bi-encoder, mantendo a comparação justa. | |
| ## Como usar | |
| ```python | |
| import json, torch | |
| from transformers import AutoTokenizer, AutoModelForCausalLM | |
| from peft import PeftModel | |
| from huggingface_hub import snapshot_download | |
| REPO = "histlearn/community-notes-reranker-ptbr" | |
| path = snapshot_download(REPO, allow_patterns=["manifesto.json", "adapter_fold_1/*"]) | |
| m = json.load(open(f"{path}/manifesto.json")) | |
| tok = AutoTokenizer.from_pretrained(m["base_model"], padding_side="left") | |
| base = AutoModelForCausalLM.from_pretrained( | |
| m["base_model"], torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32) | |
| model = PeftModel.from_pretrained(base, f"{path}/adapter_fold_1") | |
| if torch.cuda.is_available(): model.cuda() | |
| model.eval() | |
| def util_prob(tweet: str, nota: str) -> float: | |
| text = (m["prompt_prefixo"] + "<Instruct>: " + m["instrucao"] + | |
| "\n<Query>: " + tweet + "\n<Document>: " + nota + m["prompt_sufixo"]) | |
| enc = tok(text, return_tensors="pt", truncation=True, max_length=m["max_length"]).to(model.device) | |
| with torch.no_grad(): | |
| l = model(**enc).logits[:, -1, :] | |
| return float(torch.sigmoid(l[:, m["id_yes"]] - l[:, m["id_no"]]).item()) | |
| ``` | |
| O ponto operacional ótimo medido sob CV (Platt scaling) é `prob >= 0.38` — o reranker emite logits não calibrados que tendem a ficar abaixo de 0.50. Para reproduzir o número reportado abaixo, use o ensemble dos 5 folds (`examples/inference_ensemble.py`); a média das 5 probabilidades dá o resultado de referência. | |
| ## Resultados | |
| Avaliação *out-of-fold* sob `StratifiedGroupKFold(5)` agrupado por `tweetId`, em 13.525 notas em PT-BR (universo `CRH ∪ CRNH`, 71,7% positivos). | |
| | Configuração | macro-F1 | ROC-AUC | MCC | PR-AUC (minoritária) | | |
| |---|---|---|---|---| | |
| | **Ensemble de 5 folds (média de probabilidades)** | **0.7920** | **0.8932** | **0.5905** | **0.8293** | | |
| Comparado às demais configurações testadas no projeto sob o mesmo protocolo (ver notebook `02_pipeline_experimento.ipynb` no [Space](https://huggingface.co/spaces/histlearn/communitynotesbr)): | |
| | Configuração de referência | macro-F1 | ROC-AUC | Usa tweet? | | |
| |---|---|---|---| | |
| | Baseline trivial (classe majoritária) | 0.4175 | 0.5000 | — | | |
| | TF-IDF da nota + Logistic Regression | 0.7725 | 0.8622 | não | | |
| | Embedding Qwen *frozen* da nota + LR | 0.7489 | 0.8488 | não | | |
| | Embedding Qwen *frozen* nota+tweet + LR | 0.7193 | 0.8057 | sim (sem aprendizado) | | |
| | **Qwen3-Reranker-0.6B + LoRA (este modelo)** | **0.7920** | **0.8932** | **sim (aprendido)** | | |
| | Stacking dos baselines acima + este modelo | 0.8282 | 0.9081 | sim | | |
| O ganho do cross-encoder fine-tunado sobre o melhor baseline lexical é de ~2 pp macro-F1 e ~3 pp ROC-AUC. Sob bootstrap de 300 reamostras, IC95 do stacking é [0.821, 0.835] contra [0.785, 0.800] deste modelo — intervalos não se sobrepõem, confirmando significância. Com representações *frozen*, somar o tweet degrada o desempenho (linha 4 da tabela); o ganho aparece quando o modelo aprende a interação entre os dois textos. | |
| ## Dados de treino | |
| - **Notas:** [`histlearn/notas-comunidade-ptbr`](https://huggingface.co/datasets/histlearn/notas-comunidade-ptbr) — 142.448 notas em PT-BR (CC0). Universo estrito (`consenso ∈ {CRH, CRNH}`) após filtro: ~20k notas; após hidratação dos tweets: **13.525 notas usadas**. | |
| - **Tweets:** hidratados via *X syndication*. Não redistribuídos (restrição da X). | |
| ## Detalhes de treinamento | |
| | | | | |
| |---|---| | |
| | Base | `Qwen/Qwen3-Reranker-0.6B` | | |
| | Método | LoRA (`r=16`, `α=32`, `dropout=0.1`) | | |
| | Alvos LoRA | `q_proj`, `k_proj`, `v_proj`, `o_proj` | | |
| | Loss | `BCEWithLogitsLoss` sobre `logit(yes) − logit(no)`, com `pos_weight = n_neg / n_pos` | | |
| | Otimizador | AdamW, `lr = 1e-4` | | |
| | Batch | 8 | | |
| | Épocas | 2 por fold | | |
| | `max_length` | 512 | | |
| | Precisão | fp16 | | |
| | Protocolo | `StratifiedGroupKFold(5)` agrupado por `tweetId` | | |
| | Seed | 42 | | |
| ### Template do prompt | |
| ``` | |
| <|im_start|>system | |
| Judge whether the Document meets the requirements based on the Query and the Instruct provided. Note that the answer can only be "yes" or "no".<|im_end|> | |
| <|im_start|>user | |
| <Instruct>: A nota e uma Community Note util e bem avaliada para o tweet? | |
| <Query>: <texto do tweet> | |
| <Document>: <texto da nota><|im_end|> | |
| <|im_start|>assistant | |
| <think> | |
| </think> | |
| ``` | |
| Score = `sigmoid(logits["yes"] − logits["no"])` na última posição. | |
| ## Escopo | |
| O modelo prevê se a comunidade marcaria a nota como útil, não se o conteúdo é factualmente correto — útil ≠ verdadeiro. Os números refletem o universo `CRH ∪ CRNH` (notas que já receberam consenso) e o subconjunto cujos tweets foram hidratados via *X syndication* (~70% das notas elegíveis). Domínio: PT-BR, contexto político-social brasileiro 2024–2025. Sob deslocamento temporal (treino no passado, teste no futuro), o ensemble degrada cerca de 1 pp macro-F1; baselines lexicais sozinhos degradam cerca de 3 pp. | |
| ## Reprodutibilidade | |
| O projeto está hospedado inteiramente no Hugging Face: | |
| - **Demo e contexto:** [`histlearn/communitynotesbr`](https://huggingface.co/spaces/histlearn/communitynotesbr) — Space com a página *Anatomia do classificador*. | |
| - **Dataset principal:** [`histlearn/notas-comunidade-ptbr`](https://huggingface.co/datasets/histlearn/notas-comunidade-ptbr). | |
| - **Dump bruto da X:** [`histlearn/community-notes-br`](https://huggingface.co/datasets/histlearn/community-notes-br). | |
| Os 5 adapters correspondem exatamente às dobras do `StratifiedGroupKFold(5)` agrupado por `tweetId` rodado no notebook 2 do projeto. O pacote completo de artefatos (~190 MB, com embeddings pré-computados, scores de reranker zero-shot, índices das dobras e OOFs de todos os modelos comparados) é distribuído junto com os notebooks da entrega. | |
| ## Citação | |
| ```bibtex | |
| @misc{communitynotes_reranker_ptbr_2026, | |
| author = {Rocha, Davi Machado}, | |
| title = {Community Notes Reranker (PT-BR) — Qwen3-Reranker-0.6B fine-tunado com LoRA}, | |
| year = {2026}, | |
| publisher = {Hugging Face}, | |
| url = {https://huggingface.co/histlearn/community-notes-reranker-ptbr}, | |
| note = {Disciplina Introdução à IA, prof. Ricardo M. Marcacini, ICMC/USP} | |
| } | |
| ``` | |
| ## Licença | |
| Apache 2.0. | |