概率主题模型

本教程实现了 Akash Srivastava 和 Charles Sutton 在 Autoencoding Variational Inference For Topic Models 中提出的 ProdLDA 主题模型。该模型比普通的 LDA 模型能持续生成更好的主题,并且训练速度快得多。此外,它不需要依赖复杂的数学推导的自定义推断算法。本教程也是使用 Pyro 进行概率建模的入门,并深受 David Blei 的 Probabilistic topic models 的启发。

简介

主题模型是一套无监督学习算法,旨在发现大型文档集合中的主题信息并进行标注。概率主题模型使用统计方法分析文本中的词语,以发现共同主题、主题之间的联系以及主题随时间的变化。它们使我们能够以仅靠人工标注无法实现的规模来组织和总结电子文档集。最受欢迎的主题模型称为隐狄利克雷分配(latent Dirichlet allocation),简称 LDA。

隐狄利克雷分配 (LDA):直观理解

LDA 是一种文档集合的统计模型,它体现了文档包含多个主题的直观思想。最容易通过其生成过程来描述它,生成过程是模型假设文档产生的理想化随机过程。下图说明了其直观思想:

image1

图 1:隐狄利克雷分配的直观思想 (Blei 2012)。

我们假设存在给定数量的“主题”,每个主题都是词汇表中词语的概率分布(最左边)。每个文档都假设按以下方式生成:i) 首先,随机选择一个关于主题的分布(右侧的直方图);ii) 然后,对于每个词语,随机选择一个主题分配(彩色的硬币),并从对应的主题中随机选择词语。有关深入的直观描述,请查阅 David Blei 的优秀文章。

主题建模的目标是从文档集合中自动发现主题。文档本身是可观测的,而主题结构——主题、每个文档的主题分布以及每个文档中每个词语的主题分配——是隐藏的。主题建模的核心计算问题是利用观测到的文档来推断隐藏的主题结构

现在我们将看到在 Pyro 中实现这个模型是多么容易。

Pyro 中的概率建模和狄利克雷分布

LDA 是概率建模这个更大领域的一部分。如果您已经熟悉概率建模和 Pyro,请随意跳到下一节:LDA 伪代码、数学形式和图模型;如果不熟悉,请继续阅读!

在生成概率建模中,我们将数据视为来自包含隐藏变量的生成过程。这个生成过程定义了观测随机变量和隐藏随机变量的联合概率分布。我们通过使用该联合分布计算在观测变量给定情况下隐藏变量的条件分布来执行数据分析。这种条件分布也称为后验分布。

为了理解概率建模和 Pyro 的工作原理,让我们设想一个非常简单的例子。假设我们有一个骰子,并且想确定它是被做过手脚的还是公平的。我们无法直接观测骰子的“公平性”;我们只能通过投掷骰子并观测结果来推断它。因此,我们投掷 30 次并观测到以下结果:

\[\mathcal{D} = \{5, 4, 2, 5, 6, 5, 3, 3, 1, 5, 5, 3, 5, 3, 5, 3, 5, 5, 3, 5, 5, 3, 1, 5, 3, 3, 6, 5, 5, 6\}\]

计算每个结果的出现次数

数字

1

2

3

4

5

6

计数

2

1

9

1

14

3

对于一个公平的骰子,我们应该期望在 30 次投掷中,6 个数字中的每一个都出现大约 5 次。然而,这与我们观测到的不同:数字 3 和 5 的频率高得多,而 2 和 4 的频率尤其低。这并不奇怪,因为我们使用了具有以下概率的随机数生成器来生成数据集 \(\mathcal{D}\)

\[P = \{0.12, 0.06, 0.26, 0.12, 0.32, 0.12\}\]

然而,在一般情况下,生成数据集的概率对我们是隐藏的:它们是隐随机变量。我们无法直接访问它们,只能访问观测值 \(\mathcal {D}\)。概率建模的目的是仅从观测值中学习隐藏变量。

假设我们观测到 N 次投掷,\(\mathcal{D} = \{x_1, ..., x_n\}\),其中 \(x_i \in \{1, ..., 6\}\)。如果我们假设数据是独立同分布的 (iid),则观测到这个特定数据集的似然具有以下形式:

\begin{equation*} p(\mathcal{D} | \theta) = \prod_{i}^{6} \theta_{k}^{N_k} \label{eq:multinomial} \tag{1} \end{equation*}

其中 \(N_k\) 是我们观测到每个数字 \(k\) 的次数。多项分布的似然具有相同的形式:这是建模这个简单例子的最佳分布。但如何建模隐随机变量 \(\theta\) 呢?

\(\theta\) 是一个维度为6(对应每个数字)的向量,它位于6维概率单纯形中,即所有概率都是正实数且加起来等于1。对于这类概率向量存在一种自然分布:**Dirichlet 分布**。事实上,Dirichlet 分布通常用作贝叶斯统计中的先验分布,因为它与多项分布(和分类分布)是“共轭的”。

狄利克雷分布由向量 \(\alpha\) 参数化,该向量的元素数量与我们的多项参数 \(\theta\) 相同(在我们的例子中为 6)。虽然在这个简单的情况下,我们可以通过解析方式计算隐藏变量 \(\theta\)\(\alpha\) 的后验,但我们不妨看看如何使用 Pyro 和马尔可夫链蒙特卡洛 (MCMC) 进行推断:

[1]:
import os
import pyro
import pyro.distributions as dist
from pyro.infer import MCMC, NUTS
import torch

assert pyro.__version__.startswith('1.9.1')
# Enable smoke test - run the notebook cells on CI.
smoke_test = 'CI' in os.environ
[2]:
def model(counts):
    theta = pyro.sample('theta', dist.Dirichlet(torch.ones(6)))
    total_count = int(counts.sum())
    pyro.sample('counts', dist.Multinomial(total_count, theta), obs=counts)

data = torch.tensor([5, 4, 2, 5, 6, 5, 3, 3, 1, 5, 5, 3, 5, 3, 5, \
                     3, 5, 5, 3, 5, 5, 3, 1, 5, 3, 3, 6, 5, 5, 6])
counts = torch.unique(data, return_counts=True)[1].float()

nuts_kernel = NUTS(model)
num_samples, warmup_steps = (1000, 200) if not smoke_test else (10, 10)
mcmc = MCMC(nuts_kernel, num_samples=1000, warmup_steps=200)
mcmc.run(counts)
hmc_samples = {k: v.detach().cpu().numpy()
               for k, v in mcmc.get_samples().items()}
Sample: 100%|██████████| 1200/1200 [00:09, 127.59it/s, step size=7.89e-01, acc. prob=0.900]
[3]:
means = hmc_samples['theta'].mean(axis=0)
stds = hmc_samples['theta'].std(axis=0)
print('Inferred dice probabilities from the data (68% confidence intervals):')
for i in range(6):
    print('%d: %.2f ± %.2f' % (i + 1, means[i], stds[i]))
Inferred dice probabilities from the data (68% confidence intervals):
1: 0.08 ± 0.05
2: 0.05 ± 0.04
3: 0.29 ± 0.07
4: 0.05 ± 0.04
5: 0.41 ± 0.08
6: 0.11 ± 0.05

就是这样!通过根据观测值对生成模型进行条件化,我们能够使用 MCMC 推断隐藏随机变量。我们看到 3 和 5 具有更高的推断概率,正如我们在数据集中观测到的那样。推断结果与生成数据的真实概率吻合良好。

在继续之前,最后提一点。我们没有直接使用 data,而是使用了 counts,它总结了数据集中每个数字出现的次数。这是因为多项分布对一个 \(k\) 面骰子投掷 \(n\) 次时每一面出现次数的概率进行建模。或者,我们可以直接使用 data,而无需进行汇总。为此,我们只需将多项分布替换为分类分布,它是多项分布在 \(n = 1\) 时的特例。

[4]:
def model(data):
    theta = pyro.sample('theta', dist.Dirichlet(torch.ones(6)))
    with pyro.plate('data', len(data)):
        pyro.sample('obs', dist.Categorical(theta), obs=data)

nuts_kernel = NUTS(model)
num_samples, warmup_steps = (1000, 200) if not smoke_test else (10, 10)
mcmc = MCMC(nuts_kernel, num_samples=num_samples, warmup_steps=warmup_steps)
mcmc.run(data - 1)  # -1: we need to work with indices [0, 5] instead of [1, 6]
hmc_samples = {k: v.detach().cpu().numpy()
               for k, v in mcmc.get_samples().items()}
Sample: 100%|██████████| 1200/1200 [00:10, 112.07it/s, step size=6.78e-01, acc. prob=0.922]
[5]:
means = hmc_samples['theta'].mean(axis=0)
stds = hmc_samples['theta'].std(axis=0)
print('Inferred dice probabilities from the data (68% confidence intervals):')
for i in range(6):
    print('%d: %.2f ± %.2f' % (i + 1, means[i], stds[i]))
Inferred dice probabilities from the data (68% confidence intervals):
1: 0.08 ± 0.05
2: 0.06 ± 0.04
3: 0.28 ± 0.08
4: 0.06 ± 0.04
5: 0.42 ± 0.08
6: 0.11 ± 0.05

正如预期的那样,这产生了与之前计算出的相同的推断结果。现在我们已经对 Pyro 中的概率编程进行了简要介绍,并回顾了狄利克雷分布和多项/分类分布的一个简单应用,我们已经准备好继续我们的 LDA 教程了。

LDA 伪代码、数学形式和图模型

我们可以通过 3 种不同的方式更正式地定义 LDA:通过伪代码、通过联合分布的数学形式以及通过概率图模型。让我们从伪代码开始。

文档集合中的每个文档都表示为主题的混合,其中每个主题 \(\beta_k\) 是词汇表上的概率分布(参见图 1 中左侧的词语分布)。我们也用 \(\beta\) 表示矩阵 \(\beta = (\beta_1, ..., \beta_k)\)。然后,生成过程如下述算法所述:

image2

图 2:生成过程的伪代码。

\(d\) 个文档的主题比例表示为 \(\theta_d\),其中 \(\theta_{d, k}\) 是文档 \(d\) 中主题 \(k\) 的主题比例(参见图 1 中的卡通直方图)。第 \(d\) 个文档的主题分配表示为 \(z_d\),其中 \(z_{d, n}\) 是文档 \(d\) 中第 \(n\) 个词语的主题分配(参见图 1 中的彩色硬币)。最后,文档 \(d\) 的观测词语表示为 \(w_d\),其中 \(w_{d, n}\) 是文档 \(d\) 中第 \(n\) 个词语,它是固定词汇表中的一个元素。

现在,让我们看看联合分布的数学形式。给定参数 \(\alpha\)\(\beta\),主题混合 \(\theta\)、一组 \(N\) 个主题 \(\bf z\) 和一组 \(N\) 个词语 \(\bf w\) 的联合分布由下式给出:

\begin{equation*} p(\theta, \mathbf{z}, \mathbf{w} | \alpha, \beta) = p(\theta | \alpha) \prod_{n=1}^{N} p(z_n | \theta) p(w_n | z_n, \beta) , \label{eq:joint1} \tag{2} \end{equation*}

其中 \(p(z_n | \theta)\) 对于唯一的 \(i\) 使得 \(z_n^i = 1\) 来说就是 \(\theta_i\)。对 \(\theta\) 进行积分并对 \(z\) 求和,我们得到文档的边际分布:

\begin{equation*} p(\mathbf{w} | \alpha, \beta) = \int_{\theta} \Bigg( \prod_{n=1}^{N} \sum_{z_n=1}^{k} p(w_n | z_n, \beta) p(z_n | \theta) \Bigg) p(\theta | \alpha) d\theta , \label{eq:joint2} \tag{3} \end{equation*}

同时对单个文档的边际概率进行乘积,我们得到语料库的概率:

\begin{equation*} p(\mathcal{D} | \alpha, \beta) = \prod_{d=1}^{M} \int_{\theta} \Bigg( \prod_{n=1}^{N_d} \sum_{z_{d,n}=1}^{k} p(w_{d,n} | z_{d,n}, \beta) p(z_{d,n} | \theta_d) \Bigg) p(\theta_d | \alpha) d\theta_d . \label{eq:joint3} \tag{4} \end{equation*}

请注意,这个分布指定了许多(统计)依赖关系。例如,主题分配 \(z_{d,n}\) 取决于每个文档的主题比例 \(\theta_d\)。另一个例子是,观测到的词语 \(w_{d,n}\) 取决于主题分配 \(z_{d,n}\) 和所有主题 \(\beta\)。这些依赖关系定义了 LDA 模型。

最后,让我们看第三种表示:概率图模型。概率图模型提供了一种图形语言,用于描述概率分布族。LDA 的图模型如下:

image2

图 3:LDA 的图模型表示。

每个节点是一个随机变量,并根据其在生成过程中的作用进行标记。隐藏节点——主题比例、分配和主题——未着色。观测节点——文档中的词语——已着色。矩形表示“板”(plate)记号,用于编码变量的复制。\(N\) 板表示文档内的词语集合;\(M\) 板表示文档集合内的文档集合。

现在我们转向计算问题:给定观测文档,计算主题结构的后验分布。

隐狄利克雷分配中的自编码变分贝叶斯

由于多项分布假设下 \(\theta\)\(\beta\) 之间的耦合(参见公式 \(\eqref{eq:joint1}\)),对隐藏变量 \(\theta\)\(z\) 进行后验推断是棘手的。这实际上是应用主题模型和开发新模型的一个主要挑战:计算后验分布的计算成本。

为了解决这个挑战,有两种常见的方法:- 近似推断方法,最流行的是变分方法,尤其是均值场方法;- (渐近精确) 基于采样的方法,最流行的是马尔可夫链蒙特卡洛,特别是基于折叠 Gibbs 采样的方法。

均值场方法和折叠 Gibbs 方法都有一个缺点,就是将它们应用于新的主题模型时,即使建模假设只有很小的变化,也需要重新推导推断方法,这在数学上可能非常繁琐且耗时,限制了实践者自由探索不同建模假设空间的能力。

自编码变分贝叶斯 (AEVB) 是主题模型的一个特别自然的选择,因为它训练了一个编码器网络,这是一个能直接将文档映射到近似后验分布的神经网络,而无需运行进一步的变分更新。然而,尽管对于隐高斯模型取得了一些显著成功,但将黑盒推断方法应用于主题模型却更具挑战性。两个主要挑战是:首先,狄利克雷先验不是位置-尺度族,这阻碍了重参数化;其次,众所周知的成分坍缩问题,即编码器网络陷入所有主题都相同的糟糕局部最优。 (然而请注意,PyTorch/Pyro 自 2018 年以来已经包含了对狄利克雷分布的可重参数化梯度的支持)。

在 2017 年的论文 Autoencoding Variational Inference For Topic Models 中,Akash Srivastava 和 Charles Sutton 解决了这两个挑战。他们提出了第一个有效的主题模型 AEVB 推断方法,并通过引入一种名为 ProdLDA 的新主题模型进行了说明,该模型比标准 LDA 生成更好的主题,速度快且计算效率高,并且无需复杂的数学推导来适应模型的改变。现在让我们了解这个特定模型,并看看如何使用 Pyro 来实现它。

数据预处理和文档向量化

那么,让我们开始吧。我们需要做的第一件事是准备数据。我们将使用 20 newsgroups 文本数据集,这是作者使用的数据集之一。20 newsgroups 数据集包含大约 18000 条关于 20 个主题的新闻组帖子。让我们获取数据:

[6]:
import pandas as pd
import numpy as np
from sklearn.datasets import fetch_20newsgroups
from sklearn.feature_extraction.text import CountVectorizer

现在,让我们对语料库进行向量化。这意味着:

  1. 创建一个字典,其中每个词语对应一个(整数)索引。

  2. 删除罕见词语(出现在少于 20 个文档中的词语)和常用词语(出现在超过 50% 文档中的词语)。

  3. 计算每个词语在每个文档中出现的次数。

最终的数据 docs 是一个 M x N 数组,其中 M 是文档数量,N 是词汇表中词语数量,数据是词语的总计数。我们的词汇表存储在 vocab 数据框中。

[7]:
news = fetch_20newsgroups(subset='all')
vectorizer = CountVectorizer(max_df=0.5, min_df=20, stop_words='english')
docs = torch.from_numpy(vectorizer.fit_transform(news['data']).toarray())

vocab = pd.DataFrame(columns=['word', 'index'])
vocab['word'] = vectorizer.get_feature_names_out()
vocab['index'] = vocab.index
[8]:
print('Dictionary size: %d' % len(vocab))
print('Corpus size: {}'.format(docs.shape))
Dictionary size: 12722
Corpus size: torch.Size([18846, 12722])

就是这样!我们得到了一个包含 12,722 个唯一词语及其索引的字典!我们的语料库包含近 19,000 个文档,其中每一行代表一个文档,每一列代表词汇表中的一个词语。数据记录了每个词语在该特定文档中出现的次数。现在我们准备进入模型部分。

ProdLDA:带有专家积的隐狄利克雷分配

为了成功地将 AEVB 应用于 LDA,论文作者是这样解决前面提到的两个挑战的。

挑战 #1:狄利克雷先验不是位置-尺度族。 为了解决这个问题,他们使用了一个编码器网络,该网络用 logistic-normal 分布(更准确地说,是 softmax-normal 分布)来近似狄利克雷先验 \(\mathcal{D}(\theta | \alpha)\)。换句话说,他们使用:

\[p(\theta | \alpha) \approx p(\theta | \mu, \Sigma) = \mathcal{LN}(\theta | \mu, \Sigma)\]

其中 \(\mu\)\(\Sigma\) 是编码器网络的输出。

挑战 #2:成分坍缩(即编码器网络陷入糟糕的局部最优)。 为了解决这个问题,他们在编码器网络中使用了 Adam 优化器、批量归一化和 Dropout 单元。

ProdLDA vs LDA。 最后,ProdLDA 与普通 LDA 之间仅有的区别在于:i) \(\beta\) 未归一化;ii) \(w_n\) 的条件分布定义为 \(w_n | \beta, \theta \sim \text{Categorical}(\sigma(\beta \theta))\)

就是这样。有关这些特定建模和推断选择的更多详细信息,请参阅论文。现在,让我们在 Pyro 中实现这个模型:

[9]:
import math
import torch.nn as nn
import torch.nn.functional as F
from pyro.infer import SVI, TraceMeanField_ELBO
from tqdm import trange
[10]:
class Encoder(nn.Module):
    # Base class for the encoder net, used in the guide
    def __init__(self, vocab_size, num_topics, hidden, dropout):
        super().__init__()
        self.drop = nn.Dropout(dropout)  # to avoid component collapse
        self.fc1 = nn.Linear(vocab_size, hidden)
        self.fc2 = nn.Linear(hidden, hidden)
        self.fcmu = nn.Linear(hidden, num_topics)
        self.fclv = nn.Linear(hidden, num_topics)
        # NB: here we set `affine=False` to reduce the number of learning parameters
        # See https://pytorch.ac.cn/docs/stable/generated/torch.nn.BatchNorm1d.html
        # for the effect of this flag in BatchNorm1d
        self.bnmu = nn.BatchNorm1d(num_topics, affine=False)  # to avoid component collapse
        self.bnlv = nn.BatchNorm1d(num_topics, affine=False)  # to avoid component collapse

    def forward(self, inputs):
        h = F.softplus(self.fc1(inputs))
        h = F.softplus(self.fc2(h))
        h = self.drop(h)
        # μ and Σ are the outputs
        logtheta_loc = self.bnmu(self.fcmu(h))
        logtheta_logvar = self.bnlv(self.fclv(h))
        logtheta_scale = (0.5 * logtheta_logvar).exp()  # Enforces positivity
        return logtheta_loc, logtheta_scale


class Decoder(nn.Module):
    # Base class for the decoder net, used in the model
    def __init__(self, vocab_size, num_topics, dropout):
        super().__init__()
        self.beta = nn.Linear(num_topics, vocab_size, bias=False)
        self.bn = nn.BatchNorm1d(vocab_size, affine=False)
        self.drop = nn.Dropout(dropout)

    def forward(self, inputs):
        inputs = self.drop(inputs)
        # the output is σ(βθ)
        return F.softmax(self.bn(self.beta(inputs)), dim=1)


class ProdLDA(nn.Module):
    def __init__(self, vocab_size, num_topics, hidden, dropout):
        super().__init__()
        self.vocab_size = vocab_size
        self.num_topics = num_topics
        self.encoder = Encoder(vocab_size, num_topics, hidden, dropout)
        self.decoder = Decoder(vocab_size, num_topics, dropout)

    def model(self, docs):
        pyro.module("decoder", self.decoder)
        with pyro.plate("documents", docs.shape[0]):
            # Dirichlet prior 𝑝(𝜃|𝛼) is replaced by a logistic-normal distribution
            logtheta_loc = docs.new_zeros((docs.shape[0], self.num_topics))
            logtheta_scale = docs.new_ones((docs.shape[0], self.num_topics))
            logtheta = pyro.sample(
                "logtheta", dist.Normal(logtheta_loc, logtheta_scale).to_event(1))
            theta = F.softmax(logtheta, -1)

            # conditional distribution of 𝑤𝑛 is defined as
            # 𝑤𝑛|𝛽,𝜃 ~ Categorical(𝜎(𝛽𝜃))
            count_param = self.decoder(theta)
            # Currently, PyTorch Multinomial requires `total_count` to be homogeneous.
            # Because the numbers of words across documents can vary,
            # we will use the maximum count accross documents here.
            # This does not affect the result because Multinomial.log_prob does
            # not require `total_count` to evaluate the log probability.
            total_count = int(docs.sum(-1).max())
            pyro.sample(
                'obs',
                dist.Multinomial(total_count, count_param),
                obs=docs
            )

    def guide(self, docs):
        pyro.module("encoder", self.encoder)
        with pyro.plate("documents", docs.shape[0]):
            # Dirichlet prior 𝑝(𝜃|𝛼) is replaced by a logistic-normal distribution,
            # where μ and Σ are the encoder network outputs
            logtheta_loc, logtheta_scale = self.encoder(docs)
            logtheta = pyro.sample(
                "logtheta", dist.Normal(logtheta_loc, logtheta_scale).to_event(1))

    def beta(self):
        # beta matrix elements are the weights of the FC layer on the decoder
        return self.decoder.beta.weight.cpu().detach().T

现在我们已经定义了模型,接下来训练它。我们将使用以下超参数:

  • 20 个主题

  • 批大小为 32

  • 学习率为 1e-3

  • 训练 50 个 epoch

重要提示:使用上述超参数在 GPU 系统上训练大约需要 5 分钟,但在 CPU 系统上可能需要几个小时或更长时间。

[11]:
# setting global variables
seed = 0
torch.manual_seed(seed)
pyro.set_rng_seed(seed)
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

num_topics = 20 if not smoke_test else 3
docs = docs.float().to(device)
batch_size = 32
learning_rate = 1e-3
num_epochs = 50 if not smoke_test else 1
[12]:
# training
pyro.clear_param_store()

prodLDA = ProdLDA(
    vocab_size=docs.shape[1],
    num_topics=num_topics,
    hidden=100 if not smoke_test else 10,
    dropout=0.2
)
prodLDA.to(device)

optimizer = pyro.optim.Adam({"lr": learning_rate})
svi = SVI(prodLDA.model, prodLDA.guide, optimizer, loss=TraceMeanField_ELBO())
num_batches = int(math.ceil(docs.shape[0] / batch_size)) if not smoke_test else 1

bar = trange(num_epochs)
for epoch in bar:
    running_loss = 0.0
    for i in range(num_batches):
        batch_docs = docs[i * batch_size:(i + 1) * batch_size, :]
        loss = svi.step(batch_docs)
        running_loss += loss / batch_docs.size(0)

    bar.set_postfix(epoch_loss='{:.2e}'.format(running_loss))
100%|██████████| 50/50 [04:37<00:00,  5.55s/it, epoch_loss=3.72e+05]

就是这样!现在,让我们可视化结果。

词云

让我们检查一下 20 个主题各自的词云。我们将使用一个名为 wordcloud 的 Python 包。我们将可视化每个主题的前 100 个词语,其中每个词语的字体大小与其 beta 值成比例(即,词语越大,对相应主题的重要性越高)。

[13]:
def plot_word_cloud(b, ax, v, n):
    sorted_, indices = torch.sort(b, descending=True)
    df = pd.DataFrame(indices[:100].numpy(), columns=['index'])
    words = pd.merge(df, vocab[['index', 'word']],
                     how='left', on='index')['word'].values.tolist()
    sizes = (sorted_[:100] * 1000).int().numpy().tolist()
    freqs = {words[i]: sizes[i] for i in range(len(words))}
    wc = WordCloud(background_color="white", width=800, height=500)
    wc = wc.generate_from_frequencies(freqs)
    ax.set_title('Topic %d' % (n + 1))
    ax.imshow(wc, interpolation='bilinear')
    ax.axis("off")

if not smoke_test:
    import matplotlib.pyplot as plt
    from wordcloud import WordCloud

    beta = prodLDA.beta()
    fig, axs = plt.subplots(7, 3, figsize=(14, 24))
    for n in range(beta.shape[0]):
        i, j = divmod(n, 3)
        plot_word_cloud(beta[n], axs[i, j], vocab, n)
    axs[-1, -1].axis('off');

    plt.show()
_images/prodlda_21_0.png

从上面的 20 个词云可以看出,模型成功地找到了几个连贯的主题。亮点包括:

  • 主题 1 领域:计算机图形学

  • 主题 3 领域:健康

  • 主题 5 领域:运动

  • 主题 6 领域:交通

  • 主题 7 领域:销售

  • 主题 8 领域:宗教

  • 主题 10 领域:硬件

  • 主题 11 领域:数字

  • 主题 12 领域:中东

  • 主题 13 领域:电子通信

  • 主题 15 领域:太空

  • 主题 18 领域:医学

  • 主题 20 领域:无神论

结论

在本教程中,我们介绍了概率主题建模、隐狄利克雷分配,并在 Pyro 中实现了 ProdLDA:这是一种在 2017 年引入的新主题模型,它有效地将 AEVB 推断算法应用于隐狄利克雷分配。我们希望您在探索无监督机器学习在管理大型文档集合方面的强大能力时获得乐趣!

参考文献

  1. Akash Srivastava, & Charles Sutton. (2017). Autoencoding Variational Inference For Topic Models.

  2. Blei, D. (2012). Probabilistic Topic Models. Commun. ACM, 55(4), 77–84.

  3. Blei, D. M., Ng, A. Y., & Jordan, M. I. (2003). Latent dirichlet allocation. Journal of machine Learning research, 3(Jan), 993-1022.