变分推断:贝叶斯神经网络#
机器学习中的当前趋势#
概率编程、深度学习和“大数据”是机器学习中最大的主题之一。在概率编程(PP)中,许多创新集中在使用变分推断来扩展规模。在这个例子中,我将展示如何在PyMC中使用变分推断来拟合一个简单的贝叶斯神经网络。我还将讨论将概率编程与深度学习结合如何为未来的研究开辟非常有趣的探索途径。
大规模概率编程#
概率编程允许非常灵活地创建自定义概率模型,主要关注推理和从数据中学习。该方法本质上是贝叶斯的,因此我们可以指定先验来通知和约束我们的模型,并以后验分布的形式获得不确定性估计。使用MCMC采样算法,我们可以从这个后验中抽取样本,非常灵活地估计这些模型。PyMC、NumPyro和Stan是目前用于构建和估计这些模型的最先进的工具。然而,采样的一个主要缺点是它通常很慢,特别是对于高维模型和大数据集。这就是为什么最近开发了变分推理算法,这些算法几乎与MCMC一样灵活,但速度快得多。这些算法不是从后验中抽取样本,而是将一个分布(例如正态分布)拟合到后验,将采样问题转化为优化问题。自动微分变分推理[Kucukelbir 等,2015]在包括PyMC、NumPyro和Stan在内的几个概率编程包中实现。
不幸的是,当涉及到传统的机器学习问题,如分类或(非线性)回归时,概率编程在(准确性和可扩展性方面)往往次于像集成学习(例如随机森林或梯度提升回归树这样的算法方法。
深度学习#
现在正处于第三次复兴中,神经网络通过主导几乎所有的物体识别基准、在Atari游戏中表现出色[Mnih 等人,2013],并在围棋中击败了世界冠军李世石[D. Silver, 2016]。从统计学的角度来看,神经网络是非常优秀的非线性函数逼近器和表示学习器。虽然主要以分类而闻名,但它们已经扩展到无监督学习,如自动编码器[Kingma 和 Welling, 2014],并以各种其他有趣的方式(例如循环网络,或MDNs来估计多模态分布)。为什么它们工作得如此出色?没有人真正知道,因为其统计特性仍未完全理解。
深度学习中的许多创新在于能够训练这些极其复杂的模型。这依赖于几个支柱:
速度:启用GPU使得处理速度大大加快。
软件:像PyTorch和TensorFlow这样的框架允许灵活地创建抽象模型,然后可以对其进行优化并编译到CPU或GPU。
学习算法:在数据子集上进行训练——随机梯度下降——使我们能够在大量数据上训练这些模型。像drop-out这样的技术可以避免过拟合。
架构上的:许多创新来自于改变输入层,比如卷积神经网络,或者输出层,比如MDNs。
桥接深度学习和概率编程#
一方面,我们拥有概率编程,它允许我们以非常原则性和易于理解的方式构建相当小且集中的模型,以深入了解我们的数据;另一方面,我们拥有深度学习,它使用许多启发式方法来训练庞大且高度复杂的模型,这些模型在预测方面非常出色。最近在变分推断方面的创新使得概率编程能够扩展模型复杂性和数据规模。因此,我们正处于能够结合这两种方法的边缘,希望能够解锁机器学习领域的新创新。更多动机,请参阅Dustin Tran的博客文章。
虽然这使得概率编程可以应用于更广泛的有趣问题,但我认为这种结合也为深度学习的创新带来了巨大的潜力。一些想法包括:
预测中的不确定性:正如我们将在下面看到的,贝叶斯神经网络告诉我们其预测中的不确定性。我认为不确定性是机器学习中一个被低估的概念,因为它在现实世界应用中显然非常重要。但它也可能在训练中发挥作用。例如,我们可以专门针对模型最不确定的样本进行训练。
表示中的不确定性:我们还可以获得权重的不确定性估计,这些估计可以告诉我们网络学习到的表示的稳定性。
使用先验进行正则化:权重通常通过L2正则化来避免过拟合,这自然地成为权重系数的先验高斯分布。然而,我们可以想象各种其他先验,例如用于强制稀疏性的spike-and-slab(这更像使用L1范数)。
使用带先验知识的迁移学习:如果我们想在一个新的物体识别数据集上训练网络,我们可以通过放置以从其他预训练网络(如GoogLeNet)中检索到的权重为中心的先验知识来引导学习,[Szegedy 等, 2014]。
分层神经网络:在概率编程中,一种非常强大的方法是分层建模,它允许将子组中学到的内容汇集到整个总体中(参见PyMC3中的分层线性回归)。应用于神经网络时,在分层数据集中,我们可以训练个体神经网络来专门处理子组,同时仍然了解整个总体的表示。例如,想象一个用于从汽车图片中分类汽车型号的网络。我们可以训练一个分层神经网络,其中子神经网络被训练来区分仅来自单一制造商的型号。直觉上,来自某个制造商的所有汽车共享某些相似之处,因此训练专门针对品牌的个体网络是有意义的。然而,由于个体网络在更高层连接,它们仍然会与其他专门子网络共享对所有品牌有用的特征信息。有趣的是,网络的不同层可以由层次结构的不同级别提供信息——例如,提取视觉线条的早期层在所有子网络中可能是相同的,而高阶表示则不同。分层模型将从数据中学习所有这些。
其他混合架构:我们可以更自由地构建各种神经网络。例如,贝叶斯非参数方法可以用于灵活调整隐藏层的大小和形状,以在训练期间将网络架构最佳地扩展到当前问题。目前,这需要昂贵的超参数优化和大量的经验知识。
PyMC中的贝叶斯神经网络#
生成数据#
首先,让我们生成一些示例数据——一个不是线性可分的简单二元分类问题。
import arviz as az
import matplotlib.pyplot as plt
import numpy as np
import pymc as pm
import pytensor
import seaborn as sns
from sklearn.datasets import make_moons
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import scale
%config InlineBackend.figure_format = 'retina'
floatX = pytensor.config.floatX
RANDOM_SEED = 9927
rng = np.random.default_rng(RANDOM_SEED)
az.style.use("arviz-darkgrid")
X, Y = make_moons(noise=0.2, random_state=0, n_samples=1000)
X = scale(X)
X = X.astype(floatX)
Y = Y.astype(floatX)
X_train, X_test, Y_train, Y_test = train_test_split(X, Y, test_size=0.5)
fig, ax = plt.subplots()
ax.scatter(X[Y == 0, 0], X[Y == 0, 1], color="C0", label="Class 0")
ax.scatter(X[Y == 1, 0], X[Y == 1, 1], color="C1", label="Class 1")
sns.despine()
ax.legend()
ax.set(xlabel="X1", ylabel="X2", title="Toy binary classification data set");

模型规范#
神经网络其实相当简单。基本单元是一个感知器,它只不过是逻辑回归。我们并行使用许多这样的感知器,然后将它们堆叠起来以获得隐藏层。这里我们将使用2个隐藏层,每层5个神经元,这对于这样一个简单的问题已经足够了。
def construct_nn(ann_input, ann_output):
n_hidden = 5
# Initialize random weights between each layer
init_1 = rng.standard_normal(size=(X_train.shape[1], n_hidden)).astype(floatX)
init_2 = rng.standard_normal(size=(n_hidden, n_hidden)).astype(floatX)
init_out = rng.standard_normal(size=n_hidden).astype(floatX)
coords = {
"hidden_layer_1": np.arange(n_hidden),
"hidden_layer_2": np.arange(n_hidden),
"train_cols": np.arange(X_train.shape[1]),
"obs_id": np.arange(X_train.shape[0]),
}
with pm.Model(coords=coords) as neural_network:
ann_input = pm.Data("ann_input", X_train, dims=("obs_id", "train_cols"))
ann_output = pm.Data("ann_output", Y_train, dims="obs_id")
# Weights from input to hidden layer
weights_in_1 = pm.Normal(
"w_in_1", 0, sigma=1, initval=init_1, dims=("train_cols", "hidden_layer_1")
)
# Weights from 1st to 2nd layer
weights_1_2 = pm.Normal(
"w_1_2", 0, sigma=1, initval=init_2, dims=("hidden_layer_1", "hidden_layer_2")
)
# Weights from hidden layer to output
weights_2_out = pm.Normal("w_2_out", 0, sigma=1, initval=init_out, dims="hidden_layer_2")
# Build neural-network using tanh activation function
act_1 = pm.math.tanh(pm.math.dot(ann_input, weights_in_1))
act_2 = pm.math.tanh(pm.math.dot(act_1, weights_1_2))
act_out = pm.math.sigmoid(pm.math.dot(act_2, weights_2_out))
# Binary classification -> Bernoulli likelihood
out = pm.Bernoulli(
"out",
act_out,
observed=ann_output,
total_size=Y_train.shape[0], # IMPORTANT for minibatches
dims="obs_id",
)
return neural_network
neural_network = construct_nn(X_train, Y_train)
这并不算太糟。Normal
先验有助于正则化权重。通常我们会向输入中添加一个常数 b
,但为了保持代码更简洁,我在这里省略了它。
变分推断:扩展模型复杂度#
我们现在可以运行一个MCMC采样器,比如pymc.NUTS
,在这种情况下效果很好,但正如已经提到的,当我们把模型扩展到更深的架构和更多层时,这会变得非常慢。
相反,我们将使用pymc.ADVI
变分推断算法。这会快得多,并且扩展性更好。请注意,这是一个均值场近似,因此我们忽略了后验中的相关性。
%%time
with neural_network:
approx = pm.fit(n=30_000)
Finished [100%]: Average Loss = 138.57
CPU times: user 8.64 s, sys: 229 ms, total: 8.87 s
Wall time: 9.41 s
绘制目标函数(ELBO),我们可以看到优化过程逐步改进了拟合效果。
plt.plot(approx.hist, alpha=0.3)
plt.ylabel("ELBO")
plt.xlabel("iteration");

trace = approx.sample(draws=5000)
现在我们已经训练了我们的模型,让我们使用后验预测检查(PPC)在保留集上进行预测。我们可以使用sample_posterior_predictive()
从后验(从变分估计中采样)生成新数据(在这种情况下是类别预测)。
with neural_network:
pm.set_data(new_data={"ann_input": X_test})
ppc = pm.sample_posterior_predictive(trace)
trace.extend(ppc)
Sampling: [out]
我们可以对每个观测值的预测结果进行平均,以估计类别1的潜在概率。
pred = ppc.posterior_predictive["out"].mean(("chain", "draw")) > 0.5
fig, ax = plt.subplots()
ax.scatter(X_test[pred == 0, 0], X_test[pred == 0, 1], color="C0", label="Predicted 0")
ax.scatter(X_test[pred == 1, 0], X_test[pred == 1, 1], color="C1", label="Predicted 1")
sns.despine()
ax.legend()
ax.set(title="Predicted labels in testing set", xlabel="X1", ylabel="X2");

print(f"Accuracy = {(Y_test == pred.values).mean() * 100:.2f}%")
Accuracy = 96.20%
嘿,我们的神经网络做得不错!
让我们看看分类器学到了什么#
为此,我们在整个输入空间上对网格上的类概率预测进行评估。
coords_eval = {
"train_cols": np.arange(grid_2d.shape[1]),
"obs_id": np.arange(grid_2d.shape[0]),
}
with neural_network:
pm.set_data(new_data={"ann_input": grid_2d, "ann_output": dummy_out}, coords=coords_eval)
ppc = pm.sample_posterior_predictive(trace)
Sampling: [out]
y_pred = ppc.posterior_predictive["out"]
概率表面#
cmap = sns.diverging_palette(250, 12, s=85, l=25, as_cmap=True)
fig, ax = plt.subplots(figsize=(16, 9))
contour = ax.contourf(
grid[0], grid[1], y_pred.mean(("chain", "draw")).values.reshape(100, 100), cmap=cmap
)
ax.scatter(X_test[pred == 0, 0], X_test[pred == 0, 1], color="C0")
ax.scatter(X_test[pred == 1, 0], X_test[pred == 1, 1], color="C1")
cbar = plt.colorbar(contour, ax=ax)
_ = ax.set(xlim=(-3, 3), ylim=(-3, 3), xlabel="X1", ylabel="X2")
cbar.ax.set_ylabel("Posterior predictive mean probability of class label = 0");

预测值的不确定性#
请注意,我们可以使用非贝叶斯神经网络完成上述所有操作。每个类别标签的后验预测均值应与最大似然预测值相同。然而,我们还可以查看后验预测的标准差,以了解我们预测中的不确定性。以下是这种情况的示意图:
cmap = sns.cubehelix_palette(light=1, as_cmap=True)
fig, ax = plt.subplots(figsize=(16, 9))
contour = ax.contourf(
grid[0], grid[1], y_pred.squeeze().values.std(axis=0).reshape(100, 100), cmap=cmap
)
ax.scatter(X_test[pred == 0, 0], X_test[pred == 0, 1], color="C0")
ax.scatter(X_test[pred == 1, 0], X_test[pred == 1, 1], color="C1")
cbar = plt.colorbar(contour, ax=ax)
_ = ax.set(xlim=(-3, 3), ylim=(-3, 3), xlabel="X1", ylabel="X2")
cbar.ax.set_ylabel("Uncertainty (posterior predictive standard deviation)");

我们可以看到,在决策边界附近,我们对预测哪个标签的不确定性最高。你可以想象,将预测与不确定性相关联是许多应用(如医疗保健)的关键属性。为了进一步提高准确性,我们可能希望主要在来自高不确定性区域的样本上训练模型。
小批量ADVI#
到目前为止,我们已经一次性地在所有数据上训练了我们的模型。显然,这种方法无法扩展到像ImageNet这样的大规模数据集。此外,在数据的小批量上进行训练(随机梯度下降)可以避免局部最小值,并且可以更快地收敛。
幸运的是,ADVI也可以在小型批次上运行。这只需要一些设置:
minibatch_x, minibatch_y = pm.Minibatch(X_train, Y_train, batch_size=50)
neural_network_minibatch = construct_nn(minibatch_x, minibatch_y)
with neural_network_minibatch:
approx = pm.fit(40000, method=pm.ADVI())
Finished [100%]: Average Loss = 134.51
plt.plot(approx.hist)
plt.ylabel("ELBO")
plt.xlabel("iteration");

如您所见,小批量ADVI的运行时间要低得多。它似乎也收敛得更快。
为了好玩,我们也可以看看跟踪。关键是,我们还可以得到神经网络权重的置信度。
az.plot_trace(trace);

你可能会认为上述网络并不真正深,但请注意,我们可以很容易地扩展它以拥有更多层,包括卷积层,以训练更具挑战性的数据集。
致谢#
吉冈拓也在PyMC3中对ADVI做了很多工作,包括小批量实现的以及从变分后验采样的部分。我还想感谢Stan团队(特别是Alp Kucukelbir和Daniel Lee)推导了ADVI并为我们讲解了它。同时感谢Chris Fonnesbeck、Andrew Campbell、吉冈拓也和Peadar Coyle对早期草稿的有益评论。
参考资料#
Alp Kucukelbir, Rajesh Ranganath, Andrew Gelman, 和 David M. Blei. 在Stan中的自动变分推断。2015年。arXiv:1506.03431。
Volodymyr Mnih, Koray Kavukcuoglu, David Silver, Alex Graves, Ioannis Antonoglou, Daan Wierstra, 和 Martin Riedmiller. 使用深度强化学习玩雅达利游戏。2013年。arXiv:1312.5602。
C. Maddison 等人。D. Silver, A. Huang. 使用深度神经网络和树搜索掌握围棋游戏。自然,529:484–489, 2016. URL: https://doi.org/10.1038/nature16961.
Diederik P Kingma 和 Max Welling。自动编码变分贝叶斯。2014年。arXiv:1312.6114。
Christian Szegedy, Wei Liu, Yangqing Jia, Pierre Sermanet, Scott Reed, Dragomir Anguelov, Dumitru Erhan, Vincent Vanhoucke, 和 Andrew Rabinovich. 深入研究卷积。2014年。arXiv:1409.4842。
水印#
%load_ext watermark
%watermark -n -u -v -iv -w -p xarray
Last updated: Sat Jun 15 2024
Python implementation: CPython
Python version : 3.11.9
IPython version : 8.25.0
xarray: 2024.6.0
matplotlib: 3.8.4
seaborn : 0.13.2
pymc : 5.15.1
pytensor : 2.22.1
numpy : 1.26.4
arviz : 0.18.0
Watermark: 2.4.3