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.
A aula 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, com as armadilhas. A etapa 1 fica nesta página e cada uma das outras etapas tem a sua página. Cada etapa leva à sua seção ou página; 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; as armadilhas
Slides
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
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.
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 é | O que a página faz |
|---|---|---|
| U5.1 LDA | uma distribuição sobre palavras | a história generativa à mão, a priori de Dirichlet, o ajuste num corpus sintético, depois as frases escritas à mão e os posts |
| U5.2 NMF | uma linha de pesos não negativos sobre as palavras | \(X \approx WH\), por que não negativa (uma comparação com a SVD), os posts; quantos tópicos, perplexidade e coerência |
| U5.3 BERTopic | um grupo de documentos | por que documentos curtos pedem embeddings; os quatro passos um de cada vez, depois em uma chamada; o ajuste das palavras dos tópicos |
| U5.4 Comparando os Modelos | LDA, NMF e BERTopic medidos lado a lado; as armadilhas |
O lab abaixo repete a aula inteira em cinco outros fóruns; o quiz fecha a aula.
Lab: tópicos em cinco fóruns, de três jeitos
O lab repete a aula num corpus novo (outros cinco fóruns do 20 Newsgroups) e compara os três modelos com os fóruns de onde os posts vieram. Os passos 2 e 3 ajustam a LDA e a NMF; os passos 4 a 7 constroem o BERTopic passo a passo e depois em uma chamada; os passos 8 a 10 inspecionam os tópicos, comparam-nos com os fóruns e tratam dos outliers.
Rode os blocos em ordem, numa mesma sessão. Toda saída abaixo é o que o código imprime; em outra máquina, os embeddings (e, portanto, os números do BERTopic) podem variar um pouco. Para trabalhar direto no notebook, baixe o notebook do lab e abra no Jupyter ou no Colab (File → Upload notebook). No Colab, rode antes !pip install bertopic numa célula. O lab inteiro leva de dois a três minutos numa CPU, quase tudo no passo 4.
Passo 1 — carregar cinco fóruns
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
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
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.
Passo 4 — passo 1 do BERTopic: embeddings dos posts
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. É 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)
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
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 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.
Passo 7 — passo 4 do BERTopic e o modelo em uma chamada
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.
Passo 8 — inspecionar tópicos
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.
Passo 9 — comparar com os fóruns
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.
Passo 10 — os outliers
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).
Sua vez
- 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
O lab acima como 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