Modelagem de Tópicos & BERTopic
A modelagem de tópicos responde a uma pergunta que parece simples: dados milhares de documentos, do que eles tratam? Ninguém rotula os documentos e ninguém lista os temas de antemão, então é aprendizado não supervisionado, o irmão textual do agrupamento. Usos típicos: ler um ano de avaliações de clientes ou de chamados de suporte sem lê-los, organizar um arquivo de notícias, acompanhar o que se discute nas redes sociais, mapear uma literatura científica.
Esta aula constrói três modelos de tópicos, do clássico ao moderno. A LDA conta uma história probabilística de como as palavras são escritas. A NMF fatora a matriz TF-IDF das aulas de representação de texto. O BERTopic é feito quase inteiramente de peças que você já conhece: sentence embeddings, UMAP, HDBSCAN e uma variante do TF-IDF. Dois corpora acompanham a aula inteira: onze frases escritas à mão (pequenas o bastante para acompanhar com uma calculadora) e 2.155 posts reais de quatro fóruns da internet, cujo fórum conhecemos mas escondemos dos modelos, para podermos conferir o que eles encontram. Todo número abaixo vem do código mostrado ao lado; cada figura é desenhada por um script curto que fica junto da sua página.
Como esta aula está organizada
A aula tem cinco páginas, esta visão geral e as páginas E3.1 a E3.4, e cada página tem duas abordagens, cada uma com os seus slides e o seu texto. Leia primeiro a abordagem fundamental das cinco páginas, em ordem, e depois faça o lab fundamental, no fim da abordagem fundamental desta página; a abordagem avançada de cada página parte da fundamental e o lab avançado fecha a aula. O quiz do fim cobre a abordagem fundamental.
| Abordagem fundamental | Abordagem avançada | |
|---|---|---|
| pergunta | Do que tratam milhares de documentos sem rótulo? Como três modelos diferentes respondem? | Dá para confiar nos tópicos: eles são recuperados, quantos deveriam ser, como recebem nome, quão estáveis são? |
| conteúdo | duas respostas para "o que é um tópico?"; a história generativa da LDA e a sua priori de Dirichlet; a NMF como \(X \approx WH\) e por que nada é negativo; por que documentos curtos pedem embeddings e os quatro passos do BERTopic; os três modelos medidos lado a lado | a LDA ajustada num corpus de tópicos conhecidos e em palavras de menos; perplexidade, coerência pela NPMI e a escolha do \(k\); o c-TF-IDF dos posts à mão e palavras dos tópicos ajustadas depois do ajuste; as armadilhas: stop words, outliers, granularidade, reprodutibilidade, língua |
| lab em aula | cinco outros fóruns, três modelos: passos 1 a 7 | usar, comparar e limpar os tópicos: passos 8 a 10 |
| ao final, você sabe | calcular \(p(w \mid d)\) para uma mistura de tópicos; ler os tópicos e os pesos da LDA e da NMF; rodar os quatro passos do BERTopic e dizer de que aula vem cada um; comparar modelos de tópicos pelo ARI com rótulos que os modelos nunca viram | conferir um modelo de tópicos em dados de tópicos conhecidos; calcular uma NPMI; escolher o \(k\) sem confiar numa pontuação só; renomear tópicos sem reagrupar; perceber um degrau ou uma dependência da semente antes de relatar tópicos |
Os dois labs usam o mesmo notebook do lab: a parte fundamental vai do passo 1 ao 7; a avançada, do 8 ao 10. Ele precisa do bertopic, que uma célula do Colab instala em um minuto.
As páginas desta aula
A aula continua em quatro páginas, feitas para serem lidas em ordem. O código de cada página continua o da anterior; cada página também começa com um bloco de preparação que reconstrói o que ela usa, para que possa ser rodada sozinha.
| Página | Um tópico é | Abordagem fundamental | Abordagem avançada |
|---|---|---|---|
| E3.1 LDA | uma distribuição sobre palavras | a história generativa à mão, a priori de Dirichlet, a LDA nos posts | o ajuste num corpus sintético de tópicos conhecidos; palavras de menos; as prioris no scikit-learn |
| E3.2 NMF | uma linha de pesos não negativos sobre as palavras | \(X \approx WH\) nas frases escritas à mão, por que não negativa (uma comparação com a SVD), a NMF nos posts | quantos tópicos: perplexidade, coerência (a NPMI à mão), diversidade, a escolha do \(k\) |
| E3.3 BERTopic | um grupo de documentos | por que documentos curtos pedem embeddings; os quatro passos um de cada vez, depois em uma chamada | o c-TF-IDF dos posts à mão; o ajuste das palavras dos tópicos; um modelo ajustado como ferramenta |
| E3.4 Comparando os Modelos | LDA, NMF e BERTopic medidos lado a lado; a comparação geral | as armadilhas: stop words, outliers, granularidade, reprodutibilidade, língua |
Abordagem fundamental
Uma pergunta guia esta parte: dados milhares de documentos que ninguém rotulou, do que eles tratam? Há duas respostas para "o que é um tópico?" e os três modelos da aula adotam uma ou outra. Esta parte constrói cada modelo, primeiro em onze frases escritas à mão e depois em 2.155 posts de fóruns, e mede o que cada um encontra contra os fóruns que os modelos nunca viram.
Esta parte segue um caminho em cinco etapas, desenhado abaixo: 1. o que é um tópico?, as duas respostas e um corpus escrito à mão; 2. LDA, um tópico como distribuição sobre palavras, contado como uma história probabilística; 3. NMF, a mesma resposta com álgebra linear; 4. BERTopic, um tópico como grupo de documentos; 5. comparar e escolher, os três modelos medidos lado a lado. A etapa 1 fica nesta página e cada uma das outras é a abordagem fundamental da sua página. Cada etapa leva à sua seção ou página e, nos slides, uma faixa embaixo mostra em que etapa você está.
- O que é um tópico?palavras que andam juntas ou documentos que andam juntos
- LDAalocação latente de Dirichlet: uma história de como as palavras são escritas
- NMFfatoração de matrizes não negativas: um documento como soma de partes
- BERTopicembeddings, redução, agrupamento, descrição
- Comparar e escolheros três lado a lado
Slides da abordagem fundamental
Clique nos slides e use as setas ← → para navegar; F abre em tela cheia e Esc mostra todos os slides. Abrir os slides numa aba nova
1. O que é um tópico?
Etapa 1. Há duas respostas; cada modelo desta aula adota uma delas:
- um tópico é uma distribuição sobre palavras. "Esportes" dá probabilidade alta a team, game, goal e quase nenhuma a oven. Um documento é uma mistura de tópicos: 80% esportes e 20% espaço, digamos. Assim funcionam a LDA e também a NMF (esta com pesos não negativos no lugar de probabilidades);
- um tópico é um grupo de documentos. Cada documento pertence a um tópico (ou a nenhum); as palavras do tópico são calculadas depois, para descrever o grupo. O BERTopic funciona assim.
Os dois partem de documentos transformados em números. Eis o corpus feito à mão (em inglês, como o corpus real que vem depois), como as contagens bag-of-words das aulas de representação de texto:
import numpy as np
import pandas as pd
from sklearn.feature_extraction.text import CountVectorizer
docs_toy = [
"the team won the game with a late goal", # esportes
"fans cheered when the team scored a goal",
"the coach said the game was a fair win for the team",
"the rocket launch put a satellite in orbit", # espaço
"nasa delayed the rocket launch to the moon",
"the satellite reached orbit around the moon",
"bake the bread in a hot oven", # culinária
"knead the dough and bake the bread slowly",
"fresh bread from the oven smells great",
"the team watched the rocket launch before the game", # esportes + espaço
"astronauts ate fresh bread on the moon", # espaço + culinária
]
cv_toy = CountVectorizer(stop_words='english')
X_toy = cv_toy.fit_transform(docs_toy)
words_toy = cv_toy.get_feature_names_out()
print(X_toy.shape, '| word tokens kept:', X_toy.sum())
counts = pd.DataFrame(X_toy.toarray(), columns=words_toy)
print(counts[['team', 'game', 'goal', 'rocket', 'launch', 'moon', 'bread', 'bake']])
(11, 33) | word tokens kept: 53
team game goal rocket launch moon bread bake
0 1 1 1 0 0 0 0 0
1 1 0 1 0 0 0 0 0
2 1 1 0 0 0 0 0 0
3 0 0 0 1 1 0 0 0
4 0 0 0 1 1 1 0 0
5 0 0 0 0 0 1 0 0
6 0 0 0 0 0 0 1 1
7 0 0 0 0 0 0 1 1
8 0 0 0 0 0 0 1 0
9 1 1 0 1 1 0 0 0
10 0 0 0 0 0 1 1 0
Depois de retiradas as stop words do inglês (the, a, with, ...), as 11 frases ficam com 53 palavras tiradas de um vocabulário de 33. Três temas saltam aos olhos (esportes, espaço, culinária); as duas últimas frases os misturam. Um modelo de tópicos recebe só esta matriz (sem os comentários) e precisa encontrar os temas.
O caminho continua na abordagem fundamental da E3.1 LDA e depois na E3.2, na E3.3 e na E3.4, em ordem. Volte aqui para o resumo e o lab.
2. Resumo: a modelagem de tópicos em cinco linhas
Leia depois da abordagem fundamental das quatro páginas.
- Um tópico é uma distribuição sobre palavras, com cada documento uma mistura de tópicos (LDA, NMF), ou um grupo de documentos que ganha nome depois pelas suas palavras (BERTopic).
- A LDA conta uma história generativa: cada posição de palavra sorteia um tópico da mistura \(\theta_d\) do documento e depois uma palavra desse tópico, então \(p(w \mid d) = \sum_k \theta_{d,k}\, \varphi_{k,w}\). A sua priori de Dirichlet \(\alpha\) diz quão misturado é um documento típico.
- A NMF escreve \(X \approx WH\) sem nada negativo: cada tópico é um tema inteiro, cada documento uma soma de partes, e os pesos se leem diretamente.
- O BERTopic encadeia quatro passos: embeddings, redução com UMAP, agrupamento com HDBSCAN, um nome para cada grupo pelo c-TF-IDF. O número de tópicos sai dos dados e um documento pode ficar sem tópico (−1).
- Nenhum modelo vence sempre. Meça os tópicos contra algo que os modelos nunca viram e leia-os.
| o que você vê | o que está acontecendo | o que fazer |
|---|---|---|
| LDA e NMF perto do acaso em textos curtos | dois documentos curtos raramente compartilham uma palavra | gerar embeddings dos documentos (BERTopic) |
| um tópico feito da assinatura de uma pessoa | texto repetido palavra por palavra é fácil de reconstruir | tirar assinaturas e rodapés ou pôr as suas palavras nas stop words |
| tópicos nomeados por the, of, to | as palavras mais frequentes de qualquer grupo grande são palavras funcionais | uma lista de stop words no vetorizador |
| muitos documentos no tópico −1 | eles ficam em regiões esparsas do espaço de embeddings | ler alguns antes de decidir o que fazer |
Lab fundamental — tópicos em cinco fóruns, de três jeitos
O problema. Um moderador herda um arquivo de 2.613 posts de cinco fóruns da coleção 20 Newsgroups, todos misturados, e quer saber do que as pessoas falam sem ler os posts. O fórum de cada post é conhecido, então o moderador pode conferir a resposta, mas os modelos nunca o veem. Esta parte ajusta os três modelos da aula: a LDA e a NMF nos passos 2 e 3; depois o BERTopic, um passo por vez e em uma chamada, nos passos 4 a 7. Usar, comparar e limpar os tópicos fica para o lab avançado.
Como fazer este lab em aula (cerca de 40 min, em duplas)
- Baixe o notebook do lab e abra no Jupyter ou no Colab (File → Upload notebook). No Colab, rode antes
!pip install bertopicnuma célula: ele trazsentence-transformers,umap-learnehdbscan. Rode as células em ordem, numa mesma sessão; o lab inteiro leva de dois a três minutos de cálculo numa CPU, quase tudo no passo 4. - Este lab corresponde aos passos 1 a 7 do notebook. O lab avançado continua no mesmo notebook, a partir do passo 8.
- Cada passo começa com um Preveja: responda com a dupla antes de rodar a célula. Errar a previsão é o que torna o resultado memorável.
- Depois de rodar, compare com a saída e a leitura desta página. Toda saída aqui é o que o código imprime; em outra máquina, os embeddings (e, portanto, os números do BERTopic) podem variar um pouco.
- As Simulações pedem que vocês escrevam um pequeno experimento em Python: mudar uma entrada e observar o que acontece com a saída. Nada de conta à mão. A solução fica recolhida logo abaixo: tente antes de abrir.
- Ritmo sugerido: passos 1 a 3 e a primeira simulação em 15 min; passos 4 e 5 em 10 min; passos 6 e 7 e a segunda simulação em 15 min.
Passo 1 — carregar cinco fóruns
Preveja. Cinco fóruns de uns 500 posts cada: software de janelas, carros, beisebol, medicina e política do Oriente Médio. Qual deles pode ter, na verdade, mais de um assunto? Quais dois um modelo de contagem de palavras pode confundir?
import numpy as np
import pandas as pd
from sklearn.datasets import fetch_20newsgroups
forums = ['comp.windows.x', 'rec.autos', 'rec.sport.baseball', 'sci.med', 'talk.politics.mideast']
ng = fetch_20newsgroups(subset='train', categories=forums, random_state=0,
remove=('headers', 'footers', 'quotes'))
keep = np.array([len(text.split()) >= 20 for text in ng.data])
docs = [text for text, k in zip(ng.data, keep) if k]
forum = np.array(ng.target_names)[ng.target[keep]] # escondido dos modelos; serve para conferi-los
print(len(docs), 'posts')
print(pd.Series(forum).value_counts())
2613 posts
sci.med 551
comp.windows.x 549
rec.autos 510
talk.politics.mideast 508
rec.sport.baseball 495
Name: count, dtype: int64
2.613 posts, entre 495 e 551 por fórum. Os nomes dos fóruns ficam em forum para conferir os resultados; nenhum dos modelos os vê. talk.politics.mideast discute vários conflitos diferentes, o que vai importar a partir do passo 6.
Passo 2 — LDA
Preveja. Cinco tópicos para cinco fóruns. Cada tópico da LDA vai ser um fórum? Qual fórum tem mais chance de se esconder atrás de palavras genéricas?
from sklearn.decomposition import LatentDirichletAllocation
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.metrics import adjusted_rand_score
settings = dict(stop_words='english', min_df=5, max_df=0.5, token_pattern=r'(?u)\b[a-zA-Z]{3,}\b')
cv = CountVectorizer(**settings)
X = cv.fit_transform(docs)
vocab = cv.get_feature_names_out()
def top_words(components, n=8):
return [' '.join(vocab[row.argsort()[::-1][:n]]) for row in components]
lda = LatentDirichletAllocation(n_components=5, random_state=0).fit(X)
lda_topic = lda.transform(X).argmax(axis=1)
print(*top_words(lda.components_), sep='\n')
print(f"ARI with the forums: {adjusted_rand_score(forum, lda_topic):.3f}")
car entry like don file just output use
year good think don game just team like
people armenian said armenians israel turkish jews know
window edu com server use available motif file
edu health medical use university research patients disease
ARI with the forums: 0.603
Cinco tópicos para cinco fóruns. O X Window System (window, server, motif), o Oriente Médio e a medicina são reconhecíveis; o beisebol se esconde atrás de palavras genéricas (year good think don game); o primeiro tópico mistura carros com palavras de software (entry, output, file). Mandar cada post para o seu maior tópico dá um ARI de 0,603 com os fóruns.
Passo 3 — NMF
Preveja. Nos quatro fóruns da aula a NMF venceu a LDA com folga (0,767 contra 0,456). Ela vence de novo aqui, com as mesmas configurações?
from sklearn.decomposition import NMF
from sklearn.feature_extraction.text import TfidfVectorizer
T = TfidfVectorizer(**settings).fit_transform(docs) # mesmo vocabulário de X
nmf = NMF(n_components=5, init='nndsvda', max_iter=500, random_state=0).fit(T)
nmf_topic = nmf.transform(T).argmax(axis=1)
print(*top_words(nmf.components_), sep='\n')
print(f"ARI with the forums: {adjusted_rand_score(forum, nmf_topic):.3f}")
print(pd.crosstab(nmf_topic, forum, rownames=['NMF topic'], colnames=['forum']).to_string())
car don just year good think like know
geb pitt cadre dsl chastity skepticism shameful intellect
window server motif use thanks application widget program
armenian armenians turkish people armenia turks turkey genocide
israel israeli jews arab arabs jewish peace lebanese
ARI with the forums: 0.333
forum comp.windows.x rec.autos rec.sport.baseball sci.med talk.politics.mideast
NMF topic
0 12 460 464 394 66
1 0 2 0 79 0
2 537 41 22 70 14
3 0 1 0 2 142
4 0 6 9 6 286
Aqui a NMF se sai pior (ARI de 0,333); a tabela cruzada mostra por quê. O tópico 1 é geb pitt cadre dsl chastity skepticism shameful intellect: a assinatura de um participante muito ativo do sci.med, "Skepticism is the chastity of the intellect, and it is shameful to surrender it too soon", com o endereço de e-mail dele em cadre.dsl.pitt.edu. Ela se repete palavra por palavra no fim de dezenas de posts dele, então a NMF gasta um tópico inteiro para reconstruí-la (79 posts de medicina vão parar ali). O Oriente Médio fica com dois tópicos (Armênia e Turquia; Israel), enquanto carros, beisebol e medicina acabam dividindo o tópico 0 (460 + 464 + 394 posts). Com \(k\) fixo em 5, cada tópico gasto com uma assinatura ou um subtema é um tópico a menos para o resto. No corpus da aula a NMF venceu a LDA; aqui é o contrário: nenhum dos dois vence sempre.
Simulação: mais tópicos para a NMF. A leitura acima culpou o orçamento de cinco tópicos pelo ARI baixo da NMF, com um deles gasto numa assinatura. Dê à NMF um orçamento maior: reajuste-a com \(k = 6\) e \(k = 8\) e imprima as palavras do topo e o ARI de cada uma. O tópico da assinatura some? Carros, beisebol e medicina ganham tópicos próprios?
Solução
for k in [6, 8]:
nmf_k = NMF(n_components=k, init='nndsvda', max_iter=500, random_state=0).fit(T)
label_k = nmf_k.transform(T).argmax(axis=1)
print(f"k = {k}: ARI with the forums {adjusted_rand_score(forum, label_k):.3f}")
print(*top_words(nmf_k.components_, 6), sep='\n')
k = 6: ARI with the forums 0.474
year team game games runs pitching
geb pitt cadre dsl chastity skepticism
window server motif use application widget
armenian armenians turkish people armenia turks
israel israeli jews arab arabs jewish
car just don like know good
k = 8: ARI with the forums 0.511
year team game games runs pitching
geb dsl cadre pitt chastity skepticism
server motif thanks widget use edu
armenian armenians turkish armenia people turks
israel israeli jews arab arabs jewish
car cars engine dealer new like
msg don people know just food
window manager application windows display expose
Com 6 tópicos o beisebol ganha o seu (year team game games runs pitching), mas a assinatura continua lá e os carros dividem um tópico de palavras genéricas; o ARI sobe para 0,474. Com 8, os carros também ganham um tópico (car cars engine dealer), aparece um tópico sobre msg e food no meio de palavras genéricas e o fórum do X Window fica com dois (servidores e widgets; gerenciadores de janelas); o ARI é 0,511, ainda abaixo do 0,603 da LDA com cinco tópicos. A assinatura sobrevive a todo \(k\): texto repetido palavra por palavra é a coisa mais fácil de uma fatoração reconstruir. Mais tópicos recompram os fóruns um a um; não tiram o lixo. Tirá-lo é o item 2 de Sua vez.
Passo 4 — passo 1 do BERTopic: embeddings dos posts
Preveja. Um post de 300 palavras e um de 30: os embeddings deles têm a mesma quantidade de números? O que isso diz sobre como um post longo é resumido?
from sentence_transformers import SentenceTransformer
encoder = SentenceTransformer('all-MiniLM-L6-v2') # baixado uma vez, cerca de 90 MB
E = encoder.encode(docs, show_progress_bar=True) # o passo lento: cerca de 1 minuto em CPU
print(E.shape)
Um vetor de 384 dimensões por post, qualquer que seja o tamanho dele. É o único passo que precisa de uma rede neural; os seguintes levam segundos. Se rodar o notebook de novo, guarde E e pule esta célula.
Passo 5 — passo 2 do BERTopic: reduzir com UMAP (e olhar)
Preveja. O código reduz os embeddings duas vezes: para 5 dimensões e para 2. Por que não usar a versão de 2 dimensões para tudo? Na figura, que fórum você espera ver em mais de um pedaço?
import matplotlib.pyplot as plt
from umap import UMAP
umap_model = UMAP(n_neighbors=15, n_components=5, min_dist=0.0, metric='cosine', random_state=42)
Z = umap_model.fit_transform(E) # 5 dimensões, para o agrupamento
print(Z.shape)
Z2 = UMAP(n_neighbors=15, n_components=2, min_dist=0.0, metric='cosine',
random_state=42).fit_transform(E) # 2 dimensões, só para olhar
for name, color in zip(forums, ['#58a6ff', '#f0883e', '#3fb950', '#ff7b72', 'gray']):
m = forum == name
plt.scatter(Z2[m, 0], Z2[m, 1], s=4, color=color, label=name)
plt.legend(markerscale=4, fontsize=8)
plt.title('posts após o UMAP, coloridos pelo fórum que os modelos nunca viram')
plt.show()
O Z de 5 dimensões é o que o HDBSCAN vai agrupar. A versão em 2 dimensões é só para os olhos (como na aula de redução de dimensionalidade, não leia distâncias entre ilhas afastadas). Você deve ver uma ilha por fórum, bem separada das outras, com o Oriente Médio em mais de um pedaço.
Passo 6 — passo 3 do BERTopic: agrupar com HDBSCAN
Preveja. Ninguém diz ao HDBSCAN quantos grupos achar. Ele vai achar exatamente cinco? Que fóruns vão lhe dar mais outliers (−1)?
from hdbscan import HDBSCAN
hdbscan_model = HDBSCAN(min_cluster_size=20, metric='euclidean', # o padrão do BERTopic é 10
cluster_selection_method='eom', prediction_data=True)
labels = hdbscan_model.fit_predict(Z)
print(labels.max() + 1, 'clusters |', (labels == -1).sum(), 'outliers')
print(pd.crosstab(labels, forum, rownames=['cluster'], colnames=['forum']).to_string())
7 clusters | 59 outliers
forum comp.windows.x rec.autos rec.sport.baseball sci.med talk.politics.mideast
cluster
-1 3 2 8 23 23
0 0 3 470 1 0
1 531 3 0 9 1
2 1 481 4 6 0
3 0 0 1 0 152
4 1 4 2 498 4
5 3 7 7 9 326
6 10 10 3 5 2
7 grupos e 59 outliers (2% dos posts, contra 20% no corpus da aula: estes fóruns são mais bem separados). Aqui min_cluster_size é 20, não o padrão 10 do BERTopic: neste corpus, 10 fica num dos degraus da seção de armadilhas, onde diferenças na sexta casa decimal dos embeddings mudam o número de tópicos (o item 1 de Sua vez (avançado) permite conferir). Quatro grupos são de um só fórum: 470 posts de beisebol no grupo 0, 531 de X Window no grupo 1, 481 de carros no grupo 2, 498 de medicina no grupo 4. O Oriente Médio se divide em dois grupos (326 posts no grupo 5, 152 no grupo 3); o grupo 6 é uma pequena mistura de tudo (30 posts). Os outliers vêm sobretudo de medicina e do Oriente Médio, 23 de cada.
Simulação: os embeddings ou o HDBSCAN? Esta tabela cruzada é muito mais limpa que as dos passos 2 e 3. Quem merece o crédito, os embeddings ou o HDBSCAN? Rode um k-means simples com 5 grupos em três representações dos mesmos posts (a matriz TF-IDF T, os embeddings E e o Z de 5 dimensões) e compare o ARI de cada uma com os fóruns.
Solução
from sklearn.cluster import KMeans
for name, V in [('TF-IDF T', T), ('embeddings E', E), ('UMAP Z', Z)]:
km = KMeans(n_clusters=5, n_init=10, random_state=0).fit_predict(V)
print(f"k-means with 5 clusters on {name:12s} ARI {adjusted_rand_score(forum, km):.3f}")
k-means with 5 clusters on TF-IDF T ARI 0.374
k-means with 5 clusters on embeddings E ARI 0.863
k-means with 5 clusters on UMAP Z ARI 0.892
Na TF-IDF, o k-means chega a 0,374, na faixa da LDA (0,603) e da NMF (0,333). Nos embeddings o mesmo k-means salta para 0,863 e, nas coordenadas do UMAP, para 0,892. A maior parte do crédito é da representação: quando significado é distância, até o agrupamento mais simples acha os fóruns. O HDBSCAN contribui com outra coisa, que o k-means não sabe fazer: escolhe sozinho o número de grupos e deixa de fora os 59 posts que não cabem em lugar nenhum.
Passo 7 — passo 4 do BERTopic e o modelo em uma chamada
Preveja. O BERTopic recebe os mesmos objetos UMAP e HDBSCAN e os embeddings do passo 4. A partição dele vai ser a do passo 6? O que vai dar nome aos tópicos?
from bertopic import BERTopic
vectorizer = CountVectorizer(stop_words='english', token_pattern=r'(?u)\b[a-zA-Z]{3,}\b')
topic_model = BERTopic(embedding_model=encoder, umap_model=umap_model,
hdbscan_model=hdbscan_model, vectorizer_model=vectorizer)
topics = np.array(topic_model.fit_transform(docs, embeddings=E)[0])
print("same partition as step 6 (ARI):", round(adjusted_rand_score(labels, topics), 3))
print(topic_model.get_topic_info()[['Topic', 'Count', 'Name']].to_string(index=False))
same partition as step 6 (ARI): 1.0
Topic Count Name
-1 59 -1_photography_kirlian_anania_music
0 544 0_file_window_program_use
1 509 1_medical_edu_patients_msg
2 492 2_car_cars_engine_like
3 474 3_year_game_team_games
4 352 4_israel_israeli_jews_arab
5 153 5_armenian_armenians_turkish_people
6 30 6_com_health_tobacco_edu
A mesma partição do passo 6, renumerada por tamanho, agora com nomes. O Oriente Médio acaba sendo dois conflitos diferentes: Israel e o mundo árabe (tópico 4, 352 posts) e Armênia e Turquia (tópico 5, 153). O tópico 6 é o misturado, 30 posts dos cinco fóruns, nomeado por com, health, tobacco, edu: pedidos, avisos de listas de discussão e boletins, agrupados mais pela forma (endereços, instruções de inscrição) que por um assunto. O tópico dos outliers leva o nome de uma pequena conversa lateral de medicina sobre fotografia Kirlian, com posts insuficientes para formar um grupo de 20. Nada disso foi pedido: o número de tópicos veio de min_cluster_size e da densidade dos embeddings.
Para fechar a parte fundamental. Em uma frase cada: (1) por que a NMF gastou um tópico inteiro com a assinatura de uma pessoa? (2) Segundo a segunda simulação, que parte do BERTopic merece a maior parte do crédito pelos grupos limpos? (3) Quais são os quatro passos do BERTopic e de que aula vem cada um?
Sua vez (fundamental)
Para casa:
- A priori da LDA. Refaça o passo 2 com
doc_topic_prior=0.05e comdoc_topic_prior=1.0. As palavras do topo mudam? E o ARI com os fóruns? - Tirar a assinatura. Ponha as oito palavras do tópico 1 da NMF (geb, pitt, cadre, dsl, chastity, skepticism, shameful, intellect) nas stop words, com
stop_words=list(ENGLISH_STOP_WORDS) + [...](ENGLISH_STOP_WORDSvem desklearn.feature_extraction.text), e refaça o passo 3 com 5 tópicos. A medicina ganha um tópico próprio? O que acontece com o ARI? - Colorir por tópico. Redesenhe o mapa de 2 dimensões do passo 5 colorido pelos tópicos do passo 7 em vez dos fóruns. Que ilhas o BERTopic dividiu e onde estão os outliers?
Abordagem avançada
A abordagem fundamental construiu três modelos de tópicos e leu os seus tópicos. Esta parte pergunta se dá para confiar nesses tópicos. Em cada página ela desce um nível: o ajuste da LDA recupera tópicos conhecidos? O que acontece quando as palavras são poucas? Quantos tópicos deveria haver? Alguma pontuação é um bom juiz? Os nomes do BERTopic podem ser reproduzidos à mão e melhorados sem reagrupar? Que armadilhas pegam a maioria dos projetos de modelagem de tópicos? O lab avançado fecha a aula usando o modelo ajustado como ferramenta e comparando os três modelos de forma justa.
Esta parte segue um caminho em cinco etapas, desenhado abaixo: 1. por dentro da LDA; 2. quantos tópicos; 3. dar nome e renomear; 4. as armadilhas; 5. usar e conferir. As etapas 1 a 4 são as abordagens avançadas das quatro páginas, em ordem; a etapa 5 é o lab avançado desta página. Cada etapa leva à sua página ou seção e, nos slides, uma faixa embaixo mostra em que etapa você está.
- Por dentro da LDAtópicos conhecidos recuperados; palavras de menos
- Quantos tópicosperplexidade, coerência pela NPMI, a escolha do k
- Dar nome e renomearo c-TF-IDF dos posts; palavras ajustadas
- As armadilhasstop words, outliers, degraus, sementes, língua
- Usar e conferirbusca, posts novos, uma comparação justa, outliers
Slides da abordagem avançada
Clique nos slides e use as setas ← → para navegar; F abre em tela cheia e Esc mostra todos os slides. Abrir os slides numa aba nova
Lab avançado — usar, comparar e limpar os tópicos
Etapa 5. O lab fundamental terminou com um modelo BERTopic de 7 tópicos, uma LDA e uma NMF de 5. Esta parte usa o modelo ajustado como ferramenta (busca, posts novos), compara os três modelos com os fóruns, primeiro como eles são e depois em condições iguais, e decide o que fazer com os outliers. Continue no mesmo notebook, na mesma sessão: os passos abaixo usam docs, forum, E, lda_topic, nmf_topic, topics, topic_model e vectorizer dos passos 1 a 7.
Como fazer este lab em aula (cerca de 30 min, em duplas)
- Continue no notebook do lab, a partir do passo 8. Se a sessão caiu, rode antes as células dos passos 1 a 7 (o passo 4 leva cerca de um minuto).
- Como no lab fundamental: Preveja antes de rodar, compare com esta página e tente as Simulações antes de abrir a solução.
- Ritmo sugerido: passo 8 e a primeira simulação em 12 min; passo 9 e a segunda simulação em 10 min; passo 10 em 8 min.
Passo 8 — inspecionar tópicos
Preveja. find_topics('car engine') busca os tópicos pelo significado. Ele acharia o tópico de carros mesmo se a palavra engine não estivesse entre as do topo? E duas frases novas, nunca vistas no treino, vão cair em tópicos sensatos?
found, sim = topic_model.find_topics('car engine', top_n=1) # busca tópicos pelo significado
t = found[0]
print(t, [w for w, _ in topic_model.get_topic(t)][:6], round(float(sim[0]), 3))
for doc in topic_model.get_representative_docs(t)[:2]:
print(' -', ' '.join(doc.split())[:90])
new = ["The doctor prescribed antibiotics for my sinus infection",
"Which V8 engine should I put in my old pickup truck?"]
print([int(t) for t in topic_model.transform(new)[0]])
2 ['car', 'cars', 'engine', 'like', 'just', 'good'] 0.466
- While I don't read normally read this group, I was looking for Valentine radar information
- *nnnnnnnng* Thank you for playing, I cannot agree with this. I believed this and to put it
[1, 2]
O find_topics gera o embedding da consulta e devolve os tópicos mais parecidos: car engine casa com o tópico 2, com cosseno de 0,466. O get_representative_docs dá até três posts típicos de um tópico (o primeiro é sobre um detector de radar, assunto de donos de carro). O transform gera os embeddings de documentos novos e os passa pelo UMAP e pelo HDBSCAN ajustados: a frase dos antibióticos vai para o tópico 1 (medicina) e a pergunta sobre o V8 para o tópico 2 (carros). É assim que um modelo de tópicos ajustado rotula os chamados da semana que vem sem ser reajustado.
Simulação: os posts da semana que vem. O transform rotulou duas frases. Rotule um lote novo inteiro: a parte de teste dos mesmos cinco fóruns (fetch_20newsgroups(subset='test', ...), com os mesmos filtros do passo 1), que o modelo nunca viu. Quantos posts caem no tópico −1 e quão bem os tópicos concordam com os fóruns?
Solução
test = fetch_20newsgroups(subset='test', categories=forums, random_state=0,
remove=('headers', 'footers', 'quotes'))
keep_test = np.array([len(text.split()) >= 20 for text in test.data])
docs_test = [text for text, k in zip(test.data, keep_test) if k]
forum_test = np.array(test.target_names)[test.target[keep_test]]
topics_test = np.array(topic_model.transform(docs_test)[0]) # embeddings, redução, grupos: sem reajuste
print(len(docs_test), 'new posts |', (topics_test == -1).sum(), 'outliers')
print(f"ARI with their forums: {adjusted_rand_score(forum_test, topics_test):.3f}")
print(pd.crosstab(topics_test, forum_test, rownames=['topic'], colnames=['forum']).to_string())
1729 new posts | 95 outliers
ARI with their forums: 0.829
forum comp.windows.x rec.autos rec.sport.baseball sci.med talk.politics.mideast
topic
-1 3 3 6 21 62
0 347 0 1 4 2
1 0 0 4 311 3
2 4 328 1 5 1
3 0 0 320 3 3
4 0 0 6 4 189
5 0 1 0 2 84
6 2 3 1 5 0
São 1.729 posts novos, 95 deles outliers (5%, contra 2% no treino), e um ARI de 0,829 com os seus fóruns, perto do 0,848 que o passo 9 mede nos posts de treino: os tópicos generalizam. A tabela cruzada mostra onde estão as dúvidas: 62 dos 95 outliers são posts do Oriente Médio, que de novo se divide entre os tópicos 4 e 5 (189 e 84 posts). Um modelo de tópicos ajustado rotula documentos novos de graça: sem reajuste e sem rótulos, só com transform.
Passo 9 — comparar com os fóruns
Preveja. O BERTopic tem 7 tópicos e 59 outliers; a LDA e a NMF têm 5 tópicos e põem todo post em algum. Qual modelo vai concordar melhor com os fóruns? A disputa é justa?
from sklearn.metrics import homogeneity_score
def scores(name, lab):
inside = lab >= 0
return dict(model=name, topics=len(set(lab[inside])), outliers=int((~inside).sum()),
ARI=round(adjusted_rand_score(forum, lab), 3),
homogeneity=round(homogeneity_score(forum[inside], lab[inside]), 3))
rows = [scores('LDA', lda_topic), scores('NMF', nmf_topic), scores('BERTopic', topics)]
topic_model.reduce_topics(docs, nr_topics=6) # funde até 5 tópicos + o tópico dos outliers
merged = np.array(topic_model.topics_)
rows.append(scores('BERTopic, merged', merged))
print(pd.DataFrame(rows).to_string(index=False))
print(pd.crosstab(merged, forum, rownames=['topic'], colnames=['forum']).to_string())
model topics outliers ARI homogeneity
LDA 5 0 0.603 0.611
NMF 5 0 0.333 0.420
BERTopic 7 59 0.848 0.902
BERTopic, merged 5 59 0.891 0.888
forum comp.windows.x rec.autos rec.sport.baseball sci.med talk.politics.mideast
topic
-1 3 2 8 23 23
0 531 3 0 9 1
1 11 491 7 11 2
2 1 4 2 498 4
3 3 7 8 9 478
4 0 3 470 1 0
Com os próprios 7 tópicos, o BERTopic concorda com os fóruns muito mais que a LDA e a NMF (ARI de 0,848 contra 0,603 e 0,333), mesmo com o ARI penalizando-o por dividir o Oriente Médio em dois. O reduce_topics funde os tópicos mais parecidos (pelo cosseno dos seus vetores c-TF-IDF) até restarem 5, mais o −1. Os tópicos fundidos correspondem um a um aos fóruns: 531 dos 544 posts do tópico 0 são de X Window, 491 de carros no tópico 1, 498 de medicina no tópico 2, 478 do Oriente Médio no tópico 3, 470 de beisebol no tópico 4; o ARI sobe para 0,891. A comparação não é totalmente justa: as pontuações do BERTopic deixam de fora os 59 outliers, enquanto a LDA e a NMF precisam colocar todo post em algum tópico. O passo 10 tira essa vantagem.
Simulação: uma comparação justa. Tire a vantagem do BERTopic pelo outro lado: pontue a LDA e a NMF só nos 2.554 posts que o BERTopic pôs em algum tópico, e o BERTopic nos mesmos posts. A LDA e a NMF chegam perto do BERTopic quando os posts difíceis saem?
Solução
kept = merged >= 0 # os posts que o BERTopic pôs num tópico
for name, lab in [('LDA', lda_topic), ('NMF', nmf_topic), ('BERTopic, merged', merged)]:
print(f"{name:17s} ARI on the {kept.sum()} posts BERTopic kept: "
f"{adjusted_rand_score(forum[kept], lab[kept]):.3f}")
LDA ARI on the 2554 posts BERTopic kept: 0.616
NMF ARI on the 2554 posts BERTopic kept: 0.346
BERTopic, merged ARI on the 2554 posts BERTopic kept: 0.917
Não. Nos mesmos 2.554 posts a LDA marca 0,616 e a NMF 0,346, mal acima dos 0,603 e 0,333 em todos os posts, enquanto o BERTopic fundido chega a 0,917. Os 59 posts que o BERTopic deixou de fora não eram o que segurava a LDA e a NMF; os tópicos delas é que são. A ordem sobrevive à comparação justa; o passo 10 confere a outra direção, com o BERTopic obrigado a pôr todo post num tópico.
Passo 10 — os outliers
Preveja. Dar a cada outlier o tópico do centroide mais próximo acaba com o tópico −1. A concordância com os fóruns sobe ou desce? E a pureza dos tópicos?
out = merged == -1
print(pd.Series(forum[out]).value_counts().to_string())
for doc in np.array(docs)[out][:3]:
print(' -', ' '.join(doc.split())[:90])
new_topics = np.array(topic_model.reduce_outliers(docs, merged, strategy='embeddings', embeddings=E))
topic_model.update_topics(docs, topics=new_topics.tolist(), vectorizer_model=vectorizer)
print(scores('BERTopic, merged, no outliers', new_topics))
sci.med 23
talk.politics.mideast 23
rec.sport.baseball 8
comp.windows.x 3
rec.autos 2
- In-reply-to: todamhyp@charles.unlv.edu's message of Mon, 19 Apr 93 20:56:15 GMT Newsgroups
- Yes, I know this is not Rec.music, (as someone has already pointed out, thanks I know that
- What _is_ your problem? Hite's post wasn't a flame. It was a correction of *your* error. Y
{'model': 'BERTopic, merged, no outliers', 'topics': 5, 'outliers': 0, 'ARI': 0.899, 'homogeneity': 0.868}
Os outliers vêm sobretudo de medicina e do Oriente Médio (23 de cada); ler alguns deles mostra por quê: uma conversa publicada em vários grupos, respostas discutindo outras respostas, posts com pouco assunto próprio. O reduce_outliers com strategy='embeddings' dá a cada um o tópico cujo embedding central é o mais próximo; o update_topics então recalcula as palavras dos tópicos com os novos membros (o BERTopic avisa que reduzir tópicos depois disso seria arriscado; é o último passo). Em todos os 2.613 posts, o ARI é 0,899 e a homogeneidade 0,868, ainda muito à frente da LDA (0,603) e da NMF (0,333).
Para fechar a aula. Em uma frase cada: (1) por que a vantagem do BERTopic sobre a LDA e a NMF não é um efeito dos posts que ele deixa de fora? (2) O que o transform permite fazer com os posts da semana que vem? Do que ele não precisa? (3) Por que a homogeneidade cai quando os outliers são forçados para dentro dos tópicos, enquanto o ARI sobe?
Sua vez (avançado)
Para casa, com mais tempo:
- Reprodutibilidade. Reajuste os passos 5 a 7 com
random_state0, 1 e 2 no UMAP de 5 dimensões, mantendoE. Para cada semente, anote o número de tópicos, o número de outliers e o ARI com os fóruns; depois calcule o ARI entre as três rodadas. Rode uma semente duas vezes e confira que o resultado é idêntico. Depois voltemin_cluster_sizepara 10 e repita, somando também aEum ruído de1e-6como na seção de armadilhas. Qual configuração se mexe mais? O que você responderia a quem perguntasse "quantos tópicos existem"? - Granularidade. Reajuste com
min_cluster_size5, 20 e 50 (mesmoZ). Compare com manter 10 e chamarreduce_topics(docs, nr_topics=...). Qual caminho dá tópicos mais legíveis com uns 8 tópicos? - Representação. Reajuste com um
CountVectorizer()sem stop words e depois chameupdate_topicscomrepresentation_model=KeyBERTInspired()(debertopic.representation). Quais tópicos mais mudam de palavras? Confira que as atribuições não mudam. - Textos curtos. Corte cada post nas primeiras 15 palavras e repita os passos 2, 3 e 7 (gerando os embeddings de novo). Qual modelo sobrevive melhor? Depois experimente um corpus em português de sua escolha (manchetes de notícias, avaliações), com
SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')e uma lista de stop words em português.
Material de aula
Notebook do lab
Os dois labs acima como um notebook, sem saídas: lab-topic-modeling.ipynb. Abra no Colab (File → Upload notebook) e rode antes !pip install bertopic.
Referências
- Blei, D. M.; Ng, A. Y.; Jordan, M. I. "Latent Dirichlet Allocation." JMLR 3 (2003): 993–1022 — a história generativa e a sua inferência variacional. texto completo
- Lee, D. D.; Seung, H. S. "Learning the Parts of Objects by Non-negative Matrix Factorization." Nature 401 (1999): 788–791 — a NMF e as representações por partes. DOI
- Grootendorst, M. "BERTopic: Neural Topic Modeling with a Class-based TF-IDF Procedure." arXiv (2022). arXiv:2203.05794
- Reimers, N.; Gurevych, I. "Sentence-BERT: Sentence Embeddings using Siamese BERT-Networks." EMNLP-IJCNLP (2019) — os sentence-transformers por trás do passo 1. arXiv:1908.10084
- McInnes, L.; Healy, J.; Melville, J. "UMAP: Uniform Manifold Approximation and Projection for Dimension Reduction." arXiv (2018). arXiv:1802.03426
- Campello, R. J. G. B.; Moulavi, D.; Sander, J. "Density-Based Clustering Based on Hierarchical Density Estimates." PAKDD (2013), LNCS 7819: 160–172 — o HDBSCAN. DOI
- Röder, M.; Both, A.; Hinneburg, A. "Exploring the Space of Topic Coherence Measures." WSDM (2015): 399–408 — medidas de coerência comparadas com julgamentos humanos. DOI
- Chang, J.; Boyd-Graber, J.; Gerrish, S.; Wang, C.; Blei, D. M. "Reading Tea Leaves: How Humans Interpret Topic Models." NeurIPS 22 (2009) — perplexidade melhor não quer dizer tópicos mais interpretáveis.
- Dieng, A. B.; Ruiz, F. J. R.; Blei, D. M. "Topic Modeling in Embedding Spaces." TACL 8 (2020): 439–453 — de onde vem a pontuação de diversidade de tópicos. DOI
Quiz
O quiz cobre a abordagem fundamental das cinco páginas.