Atrae el flujo de trabajo de modelado de su tema con Bertópica

El modelado de temas sigue siendo una herramienta crítica en la caja de herramientas AI y NLP. Mientras que los modelos de idiomas grandes (LLM) manejan el texto excepcionalmente bien, extraer temas de alto nivel de conjuntos de datos masivos todavía requiere técnicas dedicadas de modelado de temas. Un flujo de trabajo típico incluye cuatro pasos principales: incrustación, reducción de dimensionalidad, agrupación y representación de temas.

los marcos de hoy es Bertópicoque simplifica cada etapa con componentes modulares y una API intuitiva. En esta publicación, recorreré ajustes prácticos que pueda hacer para mejorar los resultados de la agrupación y aumentar la interpretabilidad basada en experimentos prácticos utilizando el conjunto de datos de grupos de noticias de código abiertoque se distribuye bajo la licencia Internacional Creative Commons Attribution 4.0.

Descripción general del proyecto

Comenzaremos con la configuración predeterminada recomendada en la documentación de Bertópica y actualizaremos progresivamente configuraciones específicas para resaltar sus efectos. En el camino, explicaré el propósito de cada módulo y cómo tomar decisiones informadas al personalizarlas.

Preparación de datos

Cargamos una muestra de 500 documentos de noticias.

import random
from datasets import load_dataset
dataset = load_dataset("SetFit/20_newsgroups")
random.seed(42)
text_label = list(zip(dataset["train"]["text"], dataset["train"]["label_text"]))
text_label_500 = random.sample(text_label, 500)

Dado que los datos se originan en discusiones casuales de Usenet, aplicamos pasos de limpieza para quitar los encabezados, eliminar el desorden y preservar solo oraciones informativas.

Este preprocesamiento garantiza incrustaciones de mayor calidad y un proceso de agrupación más suave.

import re

def clean_for_embedding(text, max_sentences=5):
    lines = text.split("\n")
    lines = [line for line in lines if not line.strip().startswith(">")]
    lines = [line for line in lines if not re.match\
            (r"^\s*(from|subject|organization|lines|writes|article)\s*:", line, re.IGNORECASE)]
    text = " ".join(lines)
    text = re.sub(r"\s+", " ", text).strip()
    text = re.sub(r"[!?]{3,}", "", text)
    sentence_split = re.split(r'(?<=[.!?]) +', text)
    sentence_split = [
        s for s in sentence_split
        if len(s.strip()) > 15 and not s.strip().isupper()
    ]
    return " ".join(sentence_split[:max_sentences])
texts_clean = [clean_for_embedding(text) for text,_ in text_label_500]
labels = [label for _, label in text_label_500]

Tubería inicial de Bertópica

Utilizando el diseño modular de Bertópica, configuramos cada componente: Sentencetransformer para incrustaciones, UMAP para la reducción de dimensionalidad, HDBSCAN para la agrupación y CountVectorizer + Keybert para la representación del tema. Esta configuración produce solo unos pocos temas amplios con representaciones ruidosas, destacando la necesidad de ajustar para lograr resultados más coherentes.

from bertopic import BERTopic
from umap import UMAP
from hdbscan import HDBSCAN
from sentence_transformers import SentenceTransformer

from sklearn.feature_extraction.text import CountVectorizer
from bertopic.vectorizers import ClassTfidfTransformer
from bertopic.representation import KeyBERTInspired

# Step 1 - Extract embeddings
embedding_model = SentenceTransformer("all-MiniLM-L6-v2")

# Step 2 - Reduce dimensionality
umap_model = UMAP(n_neighbors=10, n_components=5, min_dist=0.0, metric='cosine', random_state=42)

# Step 3 - Cluster reduced embeddings
hdbscan_model = HDBSCAN(min_cluster_size=15, metric='euclidean', cluster_selection_method='eom', prediction_data=True)

# Step 4 - Tokenize topics
vectorizer_model = CountVectorizer(stop_words="english")

# Step 5 - Create topic representation
ctfidf_model = ClassTfidfTransformer()

# Step 6 - (Optional) Fine-tune topic representations with
# a `bertopic.representation` model
representation_model = KeyBERTInspired()

# All steps together
topic_model = BERTopic(
  embedding_model=embedding_model,          # Step 1 - Extract embeddings
  umap_model=umap_model,                    # Step 2 - Reduce dimensionality
  hdbscan_model=hdbscan_model,              # Step 3 - Cluster reduced embeddings
  vectorizer_model=vectorizer_model,        # Step 4 - Tokenize topics
  ctfidf_model=ctfidf_model,                # Step 5 - Extract topic words
  representation_model=representation_model # Step 6 - (Optional) Fine-tune topic representations
)
topics, probs = topic_model.fit_transform(texts_clean)

Esta configuración produce solo unos pocos temas amplios con representaciones ruidosas. Este resultado resalta la necesidad de finisuring para lograr resultados más coherentes.

Temas descubiertos originales (imagen generada por el autor)

Ajuste de parámetros para temas granulares

N_Neighbors del módulo UMAP

UMAP es el módulo de reducción de dimensionalidad para reducir la incrustación de origen a un vector denso de menor dimensión. Al ajustar los N_Neighbors de UMAP, controlamos cómo se interpretan los datos locales o globales durante la reducción de la dimensionalidad. La reducción de este valor descubre clústeres de grano más fino y mejora el carácter distintivo del tema.

umap_model_new = UMAP(n_neighbors=5, n_components=5, min_dist=0.0, metric='cosine', random_state=42)
topic_model.umap_model = umap_model_new
topics, probs = topic_model.fit_transform(texts_clean)
topic_model.get_topic_info()
Imagen generada por el autor
Temas descubiertos después de establecer el parámetro N_NeighBors de UMAP (imagen generada por el autor)

min_cluster_size y cluster_selection_method desde el módulo HDBSCAN

Hdbscan es el módulo de agrupación establecido de forma predeterminada para BerTopic. Modificando el min_cluster_size de HDBSCAN y cambiando el cluster_selection_method de “EOM” a “hoja” afecta aún más la resolución del tema. Estas configuraciones ayudan a descubrir temas más pequeños y más enfocados y equilibrar la distribución entre grupos.

hdbscan_model_leaf = HDBSCAN(min_cluster_size=5, metric='euclidean', cluster_selection_method='leaf', prediction_data=True)
topic_model.hdbscan_model = hdbscan_model_leaf
topics, _ = topic_model.fit_transform(texts_clean)
topic_model.get_topic_info()

El número de grupos aumenta a 30 configurando cluster_selection_method en Leaf y min_cluster_size a 5.

Imagen generada por el autor
Temas descubiertos después de configurar los parámetros relacionados con HDBSCAN (imagen generada por el autor)

Controlar la aleatoriedad para la reproducibilidad

UMAP es inherentemente no determinista, lo que significa que puede producir diferentes resultados en cada ejecución a menos que establezca explícitamente un random_state fijo. Este detalle a menudo se omite en el código de ejemplo, así que asegúrese de incluirlo para garantizar la reproducibilidad.

Del mismo modo, si está utilizando una API de incrustación de terceros (como OpenAI), tenga cuidado. Algunas API introducen ligeras variaciones en las llamadas repetidas. Para las salidas reproducibles, la memoria caché las incrustaciones y alimentarlas directamente en Bertópica.

from bertopic.backend import BaseEmbedder
import numpy as np
class CustomEmbedder(BaseEmbedder):
    """Light-weight wrapper to call NVIDIA's embedding endpoint via OpenAI SDK."""

    def __init__(self, embedding_model, client):
        super().__init__()
        self.embedding_model = embedding_model
        self.client = client

    def encode(self, documents):  # type: ignore[override]
        response = self.client.embeddings.create(
            input=documents,
            model=self.embedding_model,
            encoding_format="float",
            extra_body={"input_type": "passage", "truncate": "NONE"},
        )
        embeddings = np.array([embed.embedding for embed in response.data])
        return embeddings
topic_model.embedding_model = CustomEmbedder()
topics, probs = topic_model.fit_transform(texts_clean, embeddings=embeddings)

Cada dominio del conjunto de datos puede requerir diferentes configuraciones de agrupación para obtener resultados óptimos. Para optimizar la experimentación, considere definir los criterios de evaluación y automatizar el proceso de ajuste. Para este tutorial, utilizaremos la configuración de clúster que establece N_Neighbors en 5, min_cluster_size a 5 y cluster_selection_method a “EOM”. Esta es una combinación que logra un equilibrio entre granularidad y coherencia.

Mejora de las representaciones de temas

La representación juega un papel crucial para hacer que los grupos sean interpretables. Por defecto, Bertopic genera representaciones basadas en unigram, que a menudo carecen de contexto suficiente. En la siguiente sección, exploraremos varias técnicas para enriquecer estas representaciones y mejorar la interpretabilidad del tema.

Ngram

gama N-gram

En Bertópica, CountVectorizer es la herramienta predeterminada para convertir los datos de texto en representaciones de la bolsa de palabras. En lugar de confiar en unigrams genéricos, cambie a bigrams o trigramas Uso de NGRAM_RANGE en CountVectorizer. Este simple cambio agrega un contexto muy necesario.

Dado que solo estamos actualizando la representación, BerTopic ofrece la función Update_Topics para evitar rehacer el modelado nuevamente.

topic_model.update_topics(texts_clean, vectorizer_model=CountVectorizer(stop_words="english", ngram_range=(2,3)))
topic_model.get_topic_info()
Imagen generada por el autor
Representaciones de temas utilizando bigrams (imagen generada por el autor)

Tokenizador personalizado

Algunos bigrams siguen siendo difíciles de interpretar, por ejemplo, 486DX 50, AC UK, DXF DOC, … para un mayor control, implementar un tokenizador personalizado que filtra N-Grams basados en patrones de parte de voz. Esto elimina las combinaciones sin sentido y eleva la calidad de las palabras clave de su tema.

import spacy
from typing import List

class ImprovedTokenizer:
    def __init__(self):
        self.nlp = spacy.load("en_core_web_sm", disable=["parser", "ner"])
        self.MEANINGFUL_BIGRAMS = {
            ("ADJ", "NOUN"),
            ("NOUN", "NOUN"),
            ("VERB", "NOUN"),
        }
    # Keep only the most meaningful syntactic bigram patterns
    def __call__(self, text: str, max_tokens=200) -> List[str]:
        doc = self.nlp(text[:3000])  # truncate long docs for speed
        tokens = [(t.text, t.lemma_.lower(), t.pos_) for t in doc if t.is_alpha]
       
        bigrams = []
        for i in range(len(tokens) - 1):
            word1, lemma1, pos1 = tokens[i]
            word2, lemma2, pos2 = tokens[i + 1]
            if (pos1, pos2) in self.MEANINGFUL_BIGRAMS:
                # Optionally lowercase both words to normalize
                bigrams.append(f"{lemma1} {lemma2}")
       
        return bigrams
topic_model.update_topics(docs=texts_clean,vectorizer_model=CountVectorizer(tokenizer=ImprovedTokenizer()))
topic_model.get_topic_info()
Imagen generada por el autor
Representaciones de temas que eliminan los bigrams desordenados (imagen generada por el autor)

LLM

Finalmente puedes Integrar LLMS generar títulos o resúmenes coherentes para cada tema. Bertopic admite la integración de Openai directamente o mediante la solicitud personalizada. Estos resúmenes basados en LLM mejoran drásticamente la explicabilidad.

import openai
from bertopic.representation import OpenAI

client = openai.OpenAI(api_key=os.environ["OPENAI_API_KEY"])
topic_model.update_topics(texts_clean, representation_model=OpenAI(client, model="gpt-4o-mini", delay_in_seconds=5))
topic_model.get_topic_info()

Las representaciones son ahora todas las oraciones significativas.

Imagen generada por el autor
Representaciones de temas que son oraciones generadas por LLM (imagen generada por el autor)

También puede escribir su propia función para obtener el título generado por LLM y actualizarlo al objeto del modelo de tema utilizando la función Update_Topic_Labels. Consulte el fragmento de código de ejemplo a continuación.

import openai
from typing import List
def generate_topic_titles_with_llm(
    topic_model,
    docs: List[str],
    api_key: str,
    model: str = "gpt-4o"
) -> Dict[int, Tuple[str, str]]:
    client = openai.OpenAI(api_key=api_key)
    topic_info = topic_model.get_topic_info()
    topic_repr = {}
    topics = topic_info[topic_info.Topic != -1].Topic.tolist()

    for topic in tqdm(topics, desc="Generating titles"):
        indices = [i for i, t in enumerate(topic_model.topics_) if t == topic]
        if not indices:
            continue
        top_doc = docs[indices[0]]

        prompt = f"""You are a helpful summarizer for topic clustering.
        Given the following text that represents a topic, generate:
        1. A short **title** for the topic (2–6 words)
        2. A one or two sentence **summary** of the topic.
        Text:
        \"\"\"
        {top_doc}
        \"\"\"
        """

        try:
            response = client.chat.completions.create(
                model=model,
                messages=[
                    {"role": "system", "content": "You are a helpful assistant for summarizing topics."},
                    {"role": "user", "content": prompt}
                ],
                temperature=0.5
            )
            output = response.choices[0].message.content.strip()
            lines = output.split('\n')
            title = lines[0].replace("Title:", "").strip()
            summary = lines[1].replace("Summary:", "").strip() if len(lines) > 1 else ""
            topic_repr[topic] = (title, summary)
        except Exception as e:
            print(f"Error with topic {topic}: {e}")
            topic_repr[topic] = ("[Error]", str(e))

    return topic_repr

topic_repr = generate_topic_titles_with_llm( topic_model, texts_clean, os.environ["OPENAI_API_KEY"])
topic_repr_dict = {
    topic: topic_repr.get(topic, "Topic")
    for topic in topic.get_topic_info()["Topic"]
 }
topic_model.set_topic_labels(topic_repr_dict)

Conclusión

Esta guía describió las estrategias procesables para aumentar los resultados de modelado de temas utilizando Bertópica. Al comprender el papel de cada módulo y parámetros de ajuste para su dominio específico, puede lograr temas más enfocados, estables e interpretables.

La representación importa tanto como la agrupación. Ya sea a través de N-Grams, filtrado sintáctico o LLMS, invertir en mejores representaciones hace que sus temas sean más fáciles de entender y más útiles en la práctica.

Bertópica también ofrece técnicas de modelado avanzadas más allá de los conceptos básicos cubiertos aquí. En una publicación futura, exploraremos esas capacidades en profundidad. ¡Manténganse al tanto!