Trabalho da disciplina de Processamento de Linguagem Natural, 7º semestre de Ciência de Dados, UniCEUB.
Grupo: Carlos Mateo Wall Bruno, Lucas Wall e Jamille Ghazaleh
A POC compara três abordagens sobre o mesmo corpus e o mesmo split:
- Pipeline clássico (
notebooks/poc_fake_news.ipynb): limpeza, lematização, BoW/TF-IDF e modelos lineares. - Classificador semântico (
notebooks/bertimbau_finetune.ipynb): fine-tune do BERTimbau com a bibliotecatransformers. Feito para rodar no Google Colab com GPU. - LLM generativo (
scripts/avaliar_llm_ollama.py): um LLM classificando zero-shot com saída estruturada, avaliado em amostra do teste. Pensei a etapa para não depender de API paga: roda de graça com um modelo aberto local via Ollama (o que uso por padrão) ou, como alternativa, com a API gratuita do Gemini. Onotebooks/llm_generativo.ipynbtem o protótipo da ideia.
E uma interface (app/app.py, Gradio) em que o usuário cola uma notícia e vê o veredito das três abordagens lado a lado.
Escolhi esse tema porque venho acompanhando o impacto da desinformação no debate público brasileiro, principalmente em período eleitoral, e queria entender na prática até onde um modelo simples de classificação consegue ir antes de precisar partir pra coisas mais pesadas como BERT. A intuição é que vocabulário e estilo de escrita de notícia falsa diferem o suficiente da imprensa tradicional pra que um classificador linear treinado em bag of words ou TF-IDF já entregue um resultado decente.
A POC tem três objetivos:
- Aplicar o pipeline clássico de PLN (limpeza, tokenização, lematização, vetorização).
- Comparar três modelos lineares em duas representações vetoriais.
- Discutir as limitações honestas de um modelo treinado em corpus de 2017 quando aplicado a notícias atuais.
Fake.br Corpus, de Monteiro et al. (2018). É um corpus público com 7.200 notícias em português brasileiro, balanceado em 3.600 notícias verdadeiras e 3.600 fake news, coletadas entre 2016 e 2018. As notícias verdadeiras vêm de portais consolidados (Folha, Estadão, G1, entre outros) e as falsas de sites identificados como propagadores de desinformação.
Repositório oficial: https://github.com/roneysco/Fake.br-Corpus
Não commitei o dataset no repo. As instruções de download estão em data/README.md.
texto bruto
|
limpeza (lower, regex remove pontuação, url e número)
|
tokenização (nltk)
|
remoção de stopwords (nltk pt)
|
lematização (spaCy pt_core_news_sm)
|
vetorização (BoW ou TF-IDF, ngram 1-2, max 5000 features)
|
modelo (Naive Bayes, LogReg ou SVM Linear)
|
avaliação (accuracy, F1, matriz de confusão, ROC AUC)
| Abordagem | Avaliado em | Accuracy | F1 | Custo |
|---|---|---|---|---|
| Clássico: Regressão Logística + TF-IDF | teste completo (1.440) | 0,9535 | 0,9539 | segundos de CPU |
| Semântico: BERTimbau fine-tunado | teste completo (1.440) | 0,9944 | 0,9944 | 1min47s em RTX 5070 Ti |
| Generativo: LLM zero-shot (llama 3.1 8B) | amostra (200) | 0,77 | 0,71 | grátis (Ollama local) |
As métricas do LLM saem ao rodar scripts/avaliar_llm_ollama.py (ou _gemini.py) e ficam em models/llm_metricas.json, que não vai pro git por ser artefato gerado.
O BERTimbau errou só 8 das 1.440 notícias do teste (ROC AUC 0,9997). Numa verificação manual com 6 notícias reais, o clássico acertou 5/6 e o BERTimbau 6/6. Métricas completas em models/bertimbau_metricas.json após rodar scripts/treinar_bertimbau.py.
O LLM zero-shot fica bem abaixo dos dois modelos treinados, e o interessante é como ele erra: é conservador, com precisão alta para fake (0,98) mas recall baixo (0,55). Na matriz de confusão (models/llm_metricas.json) ele deixa passar 45 das 100 fakes, classificando-as como verdadeiras. Olhando as justificativas em models/llm_resultados.csv, o motivo aparece: ele se deixa convencer quando a fake cita fontes de aparência crível ("fontes verificáveis (Polícia Federal)", "(Jornal Nacional)") e imita o estilo editorial. Sem ter visto o corpus, ele julga pela plausibilidade de superfície, o oposto do clássico e do BERTimbau, que aprenderam o padrão do Fake.br.
Treinei 6 combinações (3 modelos x 2 vetorizações) com split 80/20 estratificado, random_state=42:
| Modelo | Vetorização | Accuracy | F1 | ROC AUC |
|---|---|---|---|---|
| Regressão Logística | TF-IDF | 0.96 | 0.96 | 0.99 |
| SVM Linear | TF-IDF | 0.96 | 0.96 | 0.99 |
| Regressão Logística | BoW | 0.95 | 0.95 | 0.99 |
| Naive Bayes | BoW | 0.94 | 0.94 | 0.98 |
| Naive Bayes | TF-IDF | 0.93 | 0.93 | 0.98 |
| SVM Linear | BoW | 0.92 | 0.92 | 0.97 |
(números aproximados, os valores exatos saem ao rodar o notebook)
Melhor combinação: Regressão Logística com TF-IDF e bigramas. Empate técnico com o SVM Linear, mas a Logística tem coeficientes interpretáveis e isso ajudou bastante na análise das features que mais pesam pra cada classe. No fim acabei priorizando ela.
Vale comentar que o SVM Linear com Bag of Words veio bem abaixo do que eu esperava. Acho que a matriz esparsa de 5000 features sem o peso TF-IDF acaba prejudicando o hiperplano. Não cheguei a investigar a fundo, mas anotei como ponto pra olhar depois (provavelmente escalonando as features com MaxAbsScaler).
# clone
git clone https://github.com/carlosmateowall/poc-fake-news-pln.git
cd poc-fake-news-pln
# ambiente
python -m venv .venv
.venv\Scripts\activate # Windows
# source .venv/bin/activate # Linux ou Mac
# dependências
pip install -r requirements.txt
python -m spacy download pt_core_news_sm
# baixar dataset (instruções em data/README.md)
# rodar notebook
jupyter notebook notebooks/poc_fake_news.ipynbA célula de pré-processamento demora de 5 a 10 minutos dependendo da máquina, é o gargalo do pipeline (spaCy roda o lematizador token por token).
BERTimbau (Colab): abra notebooks/bertimbau_finetune.ipynb no Google Colab, selecione GPU (T4), descomente as células de pip install e git clone e rode tudo. Ao final, baixe o zip do modelo e descompacte em models/bertimbau/ no repositório local. Treino: 20 a 40 min.
LLM generativo (de graça): a avaliação roda por dois caminhos, ambos sem custo.
Local, com Ollama (recomendado, sem limite e offline):
# instala o Ollama (https://ollama.com/download) e baixa um modelo aberto
ollama pull llama3.1:8b
python scripts/avaliar_llm_ollama.pyNuvem, com a API gratuita do Gemini:
pip install google-genai
setx GEMINI_API_KEY "sua_chave" # chave grátis em https://aistudio.google.com/apikey
python scripts/avaliar_llm_gemini.pyOs dois usam o mesmo split 80/20 (random_state 42) e uma amostra estratificada de 200 notícias do teste, e salvam models/llm_metricas.json e models/llm_resultados.csv (com as justificativas). O tier gratuito do Gemini limita as chamadas por dia, então o script salva o progresso e continua de onde parou; para a amostra completa de uma vez, o caminho local é mais tranquilo.
Interface (local):
pip install -r requirements.txt
python app/treinar_baseline.py # uma vez, salva models/baseline.joblib (~10 min)
python app/app.py # abre em http://127.0.0.1:7860O BERTimbau na interface é opcional (precisa de models/bertimbau/); o LLM também. Por padrão a interface usa o Gemini (defina GEMINI_API_KEY); dá pra trocar para um modelo local com LLM_PROVIDER=ollama ou para a Anthropic com LLM_PROVIDER=anthropic.
Algumas coisas que vale deixar claras antes de qualquer leitura otimista dos resultados:
- O corpus é de 2016 a 2018. Vocabulário, temas e estilo das fake news mudaram bastante desde então. Um modelo treinado nele provavelmente generaliza mal pra desinformação atual, especialmente nos temas de saúde, ciência e tecnologia.
- As notícias verdadeiras vêm todas de portais consolidados, com estilo editorial padronizado. O modelo pode estar capturando "estilo de jornal grande" em vez de "veracidade". Uma notícia verdadeira escrita por um blog independente possivelmente seria classificada como fake.
- Bag of Words e TF-IDF ignoram ordem e contexto. Negação ("não é verdade que") fica perdida, sarcasmo idem.
- Não fiz validação cruzada, só um holdout 80/20. Pra um trabalho mais sério valeria k-fold.
- Não testei contra notícias atuais fora do corpus de forma sistemática, só fiz um teste pontual com 4 manchetes na seção 9 do notebook.
Próximo passo natural seria fine-tunar um BERTimbau pra capturar contexto, mas isso já foge do escopo dessa POC e da máquina que tenho disponível.
poc-fake-news-pln/
├── app/
│ ├── app.py # interface Gradio (3 classificadores lado a lado)
│ ├── treinar_baseline.py # treina e salva o baseline pra interface
│ └── preprocessamento.py # pipeline de limpeza compartilhado
├── data/
│ └── README.md # instruções pra baixar o Fake.br Corpus
├── models/ # artefatos gerados (fora do git)
├── notebooks/
│ ├── poc_fake_news.ipynb # etapa 1: pipeline clássico
│ ├── bertimbau_finetune.ipynb # etapa 2: fine-tune BERTimbau (Colab + GPU)
│ └── llm_generativo.ipynb # etapa 3: LLM zero-shot (protótipo)
├── scripts/
│ ├── treinar_bertimbau.py # fine-tune do BERTimbau local (sem notebook)
│ ├── avaliar_llm_ollama.py # avaliação do LLM local via Ollama (grátis)
│ └── avaliar_llm_gemini.py # avaliação do LLM via API gratuita do Gemini
├── .gitignore
├── README.md
└── requirements.txt
MONTEIRO, R. A.; SANTOS, R. L. S.; PARDO, T. A. S.; ALMEIDA, T. A.; RUIZ, E. E. S.; VALE, O. A. Contributions to the Study of Fake News in Portuguese: New Corpus and Automatic Detection Results. In: Computational Processing of the Portuguese Language (PROPOR 2018), Lecture Notes in Computer Science, vol. 11122. Springer, 2018.
JURAFSKY, D.; MARTIN, J. H. Speech and Language Processing. 3rd ed. draft, 2024. Disponível em: https://web.stanford.edu/~jurafsky/slp3/
Documentação consultada:
- scikit-learn, "Working With Text Data", https://scikit-learn.org
- spaCy, "Linguistic Features", https://spacy.io/usage/linguistic-features
- NLTK Book, capítulos 3 e 6, https://www.nltk.org/book/