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

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):

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 — 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:

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

@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.

Downloads last month
-
Inference Providers NEW
This model isn't deployed by any Inference Provider. 🙋 Ask for provider support

Model tree for histlearn/community-notes-reranker-ptbr

Adapter
(2)
this model