Como usar embeddings para deduplicação semântica de conteúdo

1. Fundamentos da Deduplicação Semântica com Embeddings

A deduplicação semântica é o processo de identificar e remover conteúdo que transmite o mesmo significado, mesmo quando expresso com palavras diferentes. Métodos tradicionais baseados em hash (como SHA-256) ou similaridade exata de strings falham porque consideram "gato comeu o rato" e "o felino devorou o roedor" como textos completamente distintos, quando semanticamente são equivalentes.

Embeddings vetoriais resolvem esse problema ao mapear textos para vetores numéricos em um espaço multidimensional onde a proximidade geométrica reflete similaridade semântica. Diferentemente de abordagens léxicas como TF-IDF ou similaridade de Jaccard (que contam palavras em comum), embeddings capturam relações contextuais, sinônimos e paráfrases.

A métrica mais comum para comparar embeddings é a similaridade por cosseno, que mede o ângulo entre dois vetores. Valores próximos de 1 indicam alta similaridade semântica, enquanto valores próximos de 0 ou negativos indicam baixa ou nenhuma relação.

2. Escolhendo o Modelo de Embedding Ideal para o Domínio

Para o contexto de "Temas — Lista Final (1200 temas)", onde trabalhamos com categorias hierárquicas de conteúdo, a escolha do modelo de embedding é crítica. Modelos da família sentence-transformers oferecem um excelente equilíbrio entre desempenho e custo computacional.

Modelos recomendados para deduplicação semântica:
- all-MiniLM-L6-v2: 384 dimensões, rápido, ideal para inglês
- paraphrase-multilingual-MiniLM-L12-v2: suporte a 50+ idiomas
- OpenAI text-embedding-3-small: 1536 dimensões, alta acurácia
- Cohere embed-multilingual-v3: otimizado para recuperação semântica

O trade-off principal envolve dimensionalidade versus velocidade. Modelos com 384 dimensões processam 10x mais rápido que modelos com 1536 dimensões, mas podem perder nuances em domínios especializados. Para "Temas — Lista Final (1200 temas)", recomenda-se começar com all-MiniLM-L6-v2 e validar com amostras do seu corpus.

O pré-processamento deve incluir:
- Tokenização preservando pontuação relevante (vírgulas, ponto final)
- Normalização de maiúsculas/minúsculas
- Remoção de HTML tags e caracteres especiais sem perder contexto semântico

3. Pipeline de Geração de Embeddings para Conteúdo Textual

O pipeline começa com a extração e chunking inteligente do conteúdo. Para artigos longos, divida em parágrafos ou seções de 256-512 tokens, mantendo contexto suficiente para capturar o tema central.

Exemplo de estrutura de dados para conteúdo com embedding:

{
  "id": "tema_0452",
  "titulo": "Inteligência Artificial na Educação",
  "conteudo": "A IA está transformando o aprendizado personalizado...",
  "embedding": [0.0234, -0.1567, ..., 0.0891],  # vetor 384-dim
  "metadata": {
    "fonte": "artigo_academico",
    "data": "2024-03-15",
    "categoria": "tecnologia_educacional"
  }
}

Para processamento em lote, utilize batch processing:

def gerar_embeddings_lote(textos, modelo, batch_size=32):
    embeddings = []
    for i in range(0, len(textos), batch_size):
        batch = textos[i:i+batch_size]
        batch_emb = modelo.encode(batch, show_progress_bar=True)
        embeddings.extend(batch_emb)
    return np.array(embeddings)

O armazenamento pode ser feito em bancos vetoriais como FAISS (para busca em memória) ou Chroma (para persistência em disco). Para datasets menores (<100k itens), arrays NumPy em memória são suficientes.

4. Métricas de Similaridade e Thresholds de Deduplicação

A similaridade por cosseno é a métrica padrão para embeddings normalizados, pois é invariante à escala dos vetores. A distância euclidiana pode ser usada quando a magnitude do embedding carrega informação semântica relevante.

def similaridade_cosseno(vetor_a, vetor_b):
    produto_interno = np.dot(vetor_a, vetor_b)
    norma_a = np.linalg.norm(vetor_a)
    norma_b = np.linalg.norm(vetor_b)
    return produto_interno / (norma_a * norma_b)

Para definir thresholds dinâmicos, analise a distribuição das similaridades no dataset:

# Calcula matriz de similaridade e encontra threshold ótimo
similaridades = cosine_similarity(matriz_embeddings)
threshold = np.percentile(similaridades[similaridades < 1], 95)
# threshold típico: 0.85-0.92 para deduplicação semântica

5. Algoritmos de Agrupamento para Deduplicação em Escala

Para grandes volumes, uma abordagem two-pass é eficiente:

  1. Passo rápido: LSH (Locality-Sensitive Hashing) reduz o espaço de busca, agrupando embeddings em buckets hash
  2. Passo fino: Similaridade por cosseno dentro de cada bucket para identificar duplicatas exatas
from sklearn.cluster import HDBSCAN

def deduplicar_hdbscan(embeddings, min_cluster_size=2):
    clusterer = HDBSCAN(min_cluster_size=min_cluster_size, metric='euclidean')
    labels = clusterer.fit_predict(embeddings)
    return labels  # -1 indica outlier (conteúdo único)

HDBSCAN é particularmente útil porque não requer especificar o número de clusters e identifica automaticamente outliers, evitando falsos positivos em conteúdos similares mas distintos.

6. Implementação Prática com Exemplos de Código

Abaixo, um pipeline completo de deduplicação semântica:

import numpy as np
from sentence_transformers import SentenceTransformer
from sklearn.metrics.pairwise import cosine_similarity

# Carrega modelo
modelo = SentenceTransformer('all-MiniLM-L6-v2')

# Conteúdos de exemplo (1200 temas)
conteudos = [
    "Inteligência Artificial na medicina diagnóstica",
    "IA aplicada ao diagnóstico médico",
    "Machine learning para diagnósticos clínicos",
    "Energia solar fotovoltaica residencial",
    "Painéis solares para casas"
]

# Gera embeddings
embeddings = modelo.encode(conteudos)

# Matriz de similaridade
matriz_sim = cosine_similarity(embeddings)

# Encontra pares duplicados (threshold 0.85)
threshold = 0.85
duplicatas = []
for i in range(len(conteudos)):
    for j in range(i+1, len(conteudos)):
        if matriz_sim[i][j] > threshold:
            duplicatas.append((i, j, matriz_sim[i][j]))

# Remove duplicatas preservando o melhor representante
indices_manter = set(range(len(conteudos)))
for i, j, sim in duplicatas:
    # Mantém o conteúdo mais longo (assumindo mais informativo)
    if len(conteudos[i]) >= len(conteudos[j]):
        indices_manter.discard(j)
    else:
        indices_manter.discard(i)

conteudos_deduplicados = [conteudos[i] for i in sorted(indices_manter)]
print(f"Removidas {len(conteudos) - len(conteudos_deduplicados)} duplicatas")

7. Métricas de Avaliação e Otimização do Sistema

Para validar o sistema, crie um dataset anotado com pares de duplicatas semânticas e não-duplicatas:

# Exemplo de métricas
from sklearn.metrics import precision_score, recall_score, f1_score

y_true = [1, 1, 0, 0, 1]  # 1 = duplicata, 0 = não duplicata
y_pred = [1, 0, 0, 1, 1]  # predições do modelo

precision = precision_score(y_true, y_pred)
recall = recall_score(y_true, y_pred)
f1 = f1_score(y_true, y_pred)

print(f"Precisão: {precision:.2f}, Recall: {recall:.2f}, F1: {f1:.2f}")

Ajuste fino do threshold pode ser feito usando curva ROC para maximizar o F1-score. Monitore também o drift semântico: se a distribuição dos embeddings mudar significativamente ao longo do tempo, pode ser necessário re-treinar ou trocar o modelo.

8. Considerações de Performance e Casos Reais

Para grandes volumes (1M+ itens), utilize indexação vetorial com HNSW (Hierarchical Navigable Small World) e quantização escalar para reduzir memória:

import faiss

dim = 384  # dimensionalidade do embedding
index = faiss.IndexHNSWFlat(dim, 32)  # 32 conexões por nó
index.hnsw.efConstruction = 200
index.add(embeddings_np)

# Busca os 10 vizinhos mais próximos
distancias, indices = index.search(query_embedding, k=10)

Para conteúdo multilingue, modelos como paraphrase-multilingual-MiniLM-L12-v2 alinham embeddings de diferentes idiomas no mesmo espaço vetorial. Após a deduplicação, implemente estratégias de merge que preservem o histórico de versões, mantendo metadados como data de criação, fonte e autor do conteúdo original.

A deduplicação semântica com embeddings não é apenas uma técnica de limpeza de dados — é uma estratégia fundamental para manter a qualidade e relevância em bases de conhecimento como "Temas — Lista Final (1200 temas)", garantindo que cada tema único seja representado sem redundância semântica.

Referências