作者: Khalid Salama
创建日期: 2021/01/15
最后修改: 2021/01/15
描述: 使用TensorFlow Probability构建概率贝叶斯神经网络模型。
采用概率方法进行深度学习可以考虑不确定性,使得模型能够对错误预测分配较低的置信度等级。不确定性的来源可以在数据中找到,因测量误差或标签中的噪声,或在模型中,由于数据可用性不足,使得模型无法有效学习。
此示例演示如何构建基本的概率贝叶斯神经网络,以应对这两种类型的不确定性。我们使用与Keras API兼容的TensorFlow Probability库。
此示例需要TensorFlow 2.3或更高版本。您可以使用以下命令安装TensorFlow Probability:
pip install tensorflow-probability
我们使用Wine Quality数据集,该数据集可在TensorFlow Datasets中获得。我们使用红酒子集,其中包含4,898个示例。该数据集具有11个酒的数值物理化学特征,任务是预测酒的质量,分数在0到10之间。在此示例中,我们将此视为回归任务。
您可以使用以下命令安装TensorFlow Datasets:
pip install tensorflow-datasets
import numpy as np
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
import tensorflow_datasets as tfds
import tensorflow_probability as tfp
在这里,我们使用tfds.load()
加载wine_quality
数据集,并将目标特征转换为浮点类型。然后,我们打乱数据集并将其拆分为训练集和测试集。我们将前train_size
个示例作为训练拆分,其余作为测试拆分。
def get_train_and_test_splits(train_size, batch_size=1):
# 我们使用与数据集大小相同的缓冲区进行预取,因为数据集
# 非常小并可以放入内存中。
dataset = (
tfds.load(name="wine_quality", as_supervised=True, split="train")
.map(lambda x, y: (x, tf.cast(y, tf.float32)))
.prefetch(buffer_size=dataset_size)
.cache()
)
# 我们使用与数据集大小相同的缓冲区进行洗牌。
train_dataset = (
dataset.take(train_size).shuffle(buffer_size=train_size).batch(batch_size)
)
test_dataset = dataset.skip(train_size).batch(batch_size)
return train_dataset, test_dataset
hidden_units = [8, 8]
learning_rate = 0.001
def run_experiment(model, loss, train_dataset, test_dataset):
model.compile(
optimizer=keras.optimizers.RMSprop(learning_rate=learning_rate),
loss=loss,
metrics=[keras.metrics.RootMeanSquaredError()],
)
print("开始训练模型...")
model.fit(train_dataset, epochs=num_epochs, validation_data=test_dataset)
print("模型训练完成。")
_, rmse = model.evaluate(train_dataset, verbose=0)
print(f"训练RMSE: {round(rmse, 3)}")
print("评估模型性能...")
_, rmse = model.evaluate(test_dataset, verbose=0)
print(f"测试RMSE: {round(rmse, 3)}")
FEATURE_NAMES = [
"固定酸度",
"挥发性酸度",
"柠檬酸",
"残余糖",
"氯化物",
"游离二氧化硫",
"总二氧化硫",
"密度",
"pH",
"硫酸盐",
"酒精",
]
def create_model_inputs():
inputs = {}
for feature_name in FEATURE_NAMES:
inputs[feature_name] = layers.Input(
name=feature_name, shape=(1,), dtype=tf.float32
)
return inputs
我们创建一个标准的确定性神经网络模型作为基线。
def create_baseline_model():
inputs = create_model_inputs()
input_values = [value for _, value in sorted(inputs.items())]
features = keras.layers.concatenate(input_values)
features = layers.BatchNormalization()(features)
# 使用Dense层创建具有确定性权重的隐藏层。
for units in hidden_units:
features = layers.Dense(units, activation="sigmoid")(features)
# 输出是确定性的:一个单点估计。
outputs = layers.Dense(units=1)(features)
model = keras.Model(inputs=inputs, outputs=outputs)
return model
让我们将葡萄酒数据集分成训练集和测试集,分别占例子的85%和15%。
dataset_size = 4898
batch_size = 256
train_size = int(dataset_size * 0.85)
train_dataset, test_dataset = get_train_and_test_splits(train_size, batch_size)
现在让我们训练基线模型。我们使用MeanSquaredError
作为损失函数。
num_epochs = 100
mse_loss = keras.losses.MeanSquaredError()
baseline_model = create_baseline_model()
run_experiment(baseline_model, mse_loss, train_dataset, test_dataset)
开始训练模型...
Epoch 1/100
17/17 [==============================] - 1s 53ms/step - loss: 37.5710 - root_mean_squared_error: 6.1294 - val_loss: 35.6750 - val_root_mean_squared_error: 5.9729
Epoch 2/100
17/17 [==============================] - 0s 7ms/step - loss: 35.5154 - root_mean_squared_error: 5.9594 - val_loss: 34.2430 - val_root_mean_squared_error: 5.8518
Epoch 3/100
17/17 [==============================] - 0s 7ms/step - loss: 33.9975 - root_mean_squared_error: 5.8307 - val_loss: 32.8003 - val_root_mean_squared_error: 5.7272
Epoch 4/100
17/17 [==============================] - 0s 12ms/step - loss: 32.5928 - root_mean_squared_error: 5.7090 - val_loss: 31.3385 - val_root_mean_squared_error: 5.5981
Epoch 5/100
17/17 [==============================] - 0s 7ms/step - loss: 30.8914 - root_mean_squared_error: 5.5580 - val_loss: 29.8659 - val_root_mean_squared_error: 5.4650
...
Epoch 95/100
17/17 [==============================] - 0s 6ms/step - loss: 0.6927 - root_mean_squared_error: 0.8322 - val_loss: 0.6901 - val_root_mean_squared_error: 0.8307
Epoch 96/100
17/17 [==============================] - 0s 6ms/step - loss: 0.6929 - root_mean_squared_error: 0.8323 - val_loss: 0.6866 - val_root_mean_squared_error: 0.8286
Epoch 97/100
17/17 [==============================] - 0s 6ms/step - loss: 0.6582 - root_mean_squared_error: 0.8112 - val_loss: 0.6797 - val_root_mean_squared_error: 0.8244
Epoch 98/100
17/17 [==============================] - 0s 6ms/step - loss: 0.6733 - root_mean_squared_error: 0.8205 - val_loss: 0.6740 - val_root_mean_squared_error: 0.8210
Epoch 99/100
17/17 [==============================] - 0s 7ms/step - loss: 0.6623 - root_mean_squared_error: 0.8138 - val_loss: 0.6713 - val_root_mean_squared_error: 0.8193
Epoch 100/100
17/17 [==============================] - 0s 6ms/step - loss: 0.6522 - root_mean_squared_error: 0.8075 - val_loss: 0.6666 - val_root_mean_squared_error: 0.8165
模型训练完成。
训练RMSE: 0.809
评估模型性能...
测试RMSE: 0.816
我们从测试集中抽取一个样本,使用模型来获取它们的预测。注意,由于基线模型是确定性的,因此我们为每个测试示例获得一个点估计的预测,没有关于模型或预测不确定性的信息。
sample = 10
examples, targets = list(test_dataset.unbatch().shuffle(batch_size * 10).batch(sample))[
0
]
predicted = baseline_model(examples).numpy()
for idx in range(sample):
print(f"预测: {round(float(predicted[idx][0]), 1)} - 实际: {targets[idx]}")
预测: 6.0 - 实际: 6.0
预测: 6.2 - 实际: 6.0
预测: 5.8 - 实际: 7.0
预测: 6.0 - 实际: 5.0
预测: 5.7 - 实际: 5.0
预测: 6.2 - 实际: 7.0
预测: 5.6 - 实际: 5.0
预测: 6.2 - 实际: 6.0
预测: 6.2 - 实际: 6.0
预测: 6.2 - 实际: 7.0
贝叶斯方法建模神经网络的目标是捕捉认知不确定性,这种不确定性是由于有限的训练数据导致的模型适应性不确定性。
其想法是,与其在神经网络中学习特定的权重(和偏置)值,不如贝叶斯方法学习权重分布 - 我们可以从中采样以产生给定输入的输出 - 来编码权重的不确定性。
因此,我们需要定义这些权重的先验和后验分布,训练过程是学习这些分布的参数。
# 将先验权重分布定义为均值为0,标准差为1的正态分布。
# 请注意,在此示例中,先验分布不可训练,
# 因为我们固定了其参数。
def prior(kernel_size, bias_size, dtype=None):
n = kernel_size + bias_size
prior_model = keras.Sequential(
[
tfp.layers.DistributionLambda(
lambda t: tfp.distributions.MultivariateNormalDiag(
loc=tf.zeros(n), scale_diag=tf.ones(n)
)
)
]
)
return prior_model
# 将变分后验权重分布定义为多元高斯分布。
# 请注意,该分布的可学习参数是均值、方差和协方差。
def posterior(kernel_size, bias_size, dtype=None):
n = kernel_size + bias_size
posterior_model = keras.Sequential(
[
tfp.layers.VariableLayer(
tfp.layers.MultivariateNormalTriL.params_size(n), dtype=dtype
),
tfp.layers.MultivariateNormalTriL(n),
]
)
return posterior_model
我们在神经网络模型中使用 tfp.layers.DenseVariational
层,而不是标准的 keras.layers.Dense
层。
def create_bnn_model(train_size):
inputs = create_model_inputs()
features = keras.layers.concatenate(list(inputs.values()))
features = layers.BatchNormalization()(features)
# 使用 DenseVariational 层创建带有权重不确定性的隐藏层。
for units in hidden_units:
features = tfp.layers.DenseVariational(
units=units,
make_prior_fn=prior,
make_posterior_fn=posterior,
kl_weight=1 / train_size,
activation="sigmoid",
)(features)
# 输出是确定性的:单一的点估计。
outputs = layers.Dense(units=1)(features)
model = keras.Model(inputs=inputs, outputs=outputs)
return model
随着训练数据集大小的增加,认知不确定性可以减少。也就是说,BNN 模型看到的数据越多,它对权重(分布参数)的估计就越确定。我们通过在一个小的训练子集上训练 BNN 模型,然后在完整的训练集上进行训练,以比较输出方差,从而测试这种行为。
num_epochs = 500
train_sample_size = int(train_size * 0.3)
small_train_dataset = train_dataset.unbatch().take(train_sample_size).batch(batch_size)
bnn_model_small = create_bnn_model(train_sample_size)
run_experiment(bnn_model_small, mse_loss, small_train_dataset, test_dataset)
开始训练模型...
训练轮次 1/500
5/5 [==============================] - 2s 123ms/步 - 损失: 34.5497 - 均方根误差: 5.8764 - 验证损失: 37.1164 - 验证均方根误差: 6.0910
训练轮次 2/500
5/5 [==============================] - 0s 28ms/步 - 损失: 36.0738 - 均方根误差: 6.0007 - 验证损失: 31.7373 - 验证均方根误差: 5.6322
训练轮次 3/500
5/5 [==============================] - 0s 29ms/步 - 损失: 33.3177 - 均方根误差: 5.7700 - 验证损失: 36.2135 - 验证均方根误差: 6.0164
训练轮次 4/500
5/5 [==============================] - 0s 30ms/步 - 损失: 35.1247 - 均方根误差: 5.9232 - 验证损失: 35.6158 - 验证均方根误差: 5.9663
训练轮次 5/500
5/5 [==============================] - 0s 23ms/步 - 损失: 34.7653 - 均方根误差: 5.8936 - 验证损失: 34.3038 - 验证均方根误差: 5.8556
...
训练轮次 495/500
5/5 [==============================] - 0s 24ms/步 - 损失: 0.6978 - 均方根误差: 0.8162 - 验证损失: 0.6258 - 验证均方根误差: 0.7723
训练轮次 496/500
5/5 [==============================] - 0s 22ms/步 - 损失: 0.6448 - 均方根误差: 0.7858 - 验证损失: 0.6372 - 验证均方根误差: 0.7808
训练轮次 497/500
5/5 [==============================] - 0s 23ms/步 - 损失: 0.6871 - 均方根误差: 0.8121 - 验证损失: 0.6437 - 验证均方根误差: 0.7825
训练轮次 498/500
5/5 [==============================] - 0s 23ms/步 - 损失: 0.6213 - 均方根误差: 0.7690 - 验证损失: 0.6581 - 验证均方根误差: 0.7922
训练轮次 499/500
5/5 [==============================] - 0s 22ms/步 - 损失: 0.6604 - 均方根误差: 0.7913 - 验证损失: 0.6522 - 验证均方根误差: 0.7908
训练轮次 500/500
5/5 [==============================] - 0s 22ms/步 - 损失: 0.6190 - 均方根误差: 0.7678 - 验证损失: 0.6734 - 验证均方根误差: 0.8037
模型训练完成。
训练均方根误差: 0.805
评估模型性能...
测试均方根误差: 0.801
由于我们已经训练了一个 BNN 模型,模型每次在相同输入下调用时都会产生不同的输出,因为每次都会从分布中抽样一组新的权重来构建网络并生成输出。模式权重的不确定性越小,我们将看到的相同输入的输出的可变性(范围越宽)就越大。
def compute_predictions(model, iterations=100):
predicted = []
for _ in range(iterations):
predicted.append(model(examples).numpy())
predicted = np.concatenate(predicted, axis=1)
prediction_mean = np.mean(predicted, axis=1).tolist()
prediction_min = np.min(predicted, axis=1).tolist()
prediction_max = np.max(predicted, axis=1).tolist()
prediction_range = (np.max(predicted, axis=1) - np.min(predicted, axis=1)).tolist()
for idx in range(sample):
print(
f"预测均值: {round(prediction_mean[idx], 2)}, "
f"最小值: {round(prediction_min[idx], 2)}, "
f"最大值: {round(prediction_max[idx], 2)}, "
f"范围: {round(prediction_range[idx], 2)} - "
f"实际值: {targets[idx]}"
)
compute_predictions(bnn_model_small)
num_epochs = 500
bnn_model_full = create_bnn_model(train_size)
run_experiment(bnn_model_full, mse_loss, train_dataset, test_dataset)
compute_predictions(bnn_model_full)
开始训练模型...
Epoch 1/500
17/17 [==============================] - 2s 32ms/step - loss: 25.4811 - root_mean_squared_error: 5.0465 - val_loss: 23.8428 - val_root_mean_squared_error: 4.8824
Epoch 2/500
17/17 [==============================] - 0s 7ms/step - loss: 23.0849 - root_mean_squared_error: 4.8040 - val_loss: 24.1269 - val_root_mean_squared_error: 4.9115
Epoch 3/500
17/17 [==============================] - 0s 7ms/step - loss: 22.5191 - root_mean_squared_error: 4.7449 - val_loss: 23.3312 - val_root_mean_squared_error: 4.8297
Epoch 4/500
17/17 [==============================] - 0s 7ms/step - loss: 22.9571 - root_mean_squared_error: 4.7896 - val_loss: 24.4072 - val_root_mean_squared_error: 4.9399
Epoch 5/500
17/17 [==============================] - 0s 6ms/step - loss: 21.4049 - root_mean_squared_error: 4.6245 - val_loss: 21.1895 - val_root_mean_squared_error: 4.6027
...
Epoch 495/500
17/17 [==============================] - 0s 7ms/step - loss: 0.5799 - root_mean_squared_error: 0.7511 - val_loss: 0.5902 - val_root_mean_squared_error: 0.7572
Epoch 496/500
17/17 [==============================] - 0s 6ms/step - loss: 0.5926 - root_mean_squared_error: 0.7603 - val_loss: 0.5961 - val_root_mean_squared_error: 0.7616
Epoch 497/500
17/17 [==============================] - 0s 7ms/step - loss: 0.5928 - root_mean_squared_error: 0.7595 - val_loss: 0.5916 - val_root_mean_squared_error: 0.7595
Epoch 498/500
17/17 [==============================] - 0s 7ms/step - loss: 0.6115 - root_mean_squared_error: 0.7715 - val_loss: 0.5869 - val_root_mean_squared_error: 0.7558
Epoch 499/500
17/17 [==============================] - 0s 7ms/step - loss: 0.6044 - root_mean_squared_error: 0.7673 - val_loss: 0.6007 - val_root_mean_squared_error: 0.7645
Epoch 500/500
17/17 [==============================] - 0s 7ms/step - loss: 0.5853 - root_mean_squared_error: 0.7550 - val_loss: 0.5999 - val_root_mean_squared_error: 0.7651
模型训练完成。
训练RMSE: 0.762
评估模型性能...
测试RMSE: 0.759
预测均值: 5.41, 最小值: 5.06, 最大值: 5.9, 范围: 0.84 - 实际: 6.0
预测均值: 6.5, 最小值: 6.16, 最大值: 6.61, 范围: 0.44 - 实际: 6.0
预测均值: 5.59, 最小值: 4.96, 最大值: 6.0, 范围: 1.04 - 实际: 7.0
预测均值: 5.67, 最小值: 5.25, 最大值: 6.01, 范围: 0.76 - 实际: 5.0
预测均值: 6.02, 最小值: 5.68, 最大值: 6.39, 范围: 0.71 - 实际: 5.0
预测均值: 6.35, 最小值: 6.11, 最大值: 6.52, 范围: 0.41 - 实际: 7.0
预测均值: 5.21, 最小值: 4.85, 最大值: 5.68, 范围: 0.83 - 实际: 5.0
预测均值: 6.53, 最小值: 6.35, 最大值: 6.64, 范围: 0.28 - 实际: 6.0
预测均值: 6.3, 最小值: 6.05, 最大值: 6.47, 范围: 0.42 - 实际: 6.0
预测均值: 6.44, 最小值: 6.19, 最大值: 6.59, 范围: 0.4 - 实际: 7.0
注意,使用完整训练数据集训练的模型在相同输入的预测值范围(不确定性)较小, 与使用训练数据集的子集训练的模型相比。
到目前为止,我们构建的标准和贝叶斯NN模型的输出是 确定性的,即为给定示例生成一个点估计作为预测。 我们可以通过让模型输出一个分布来创建一个概率神经网络。 在这种情况下,模型还捕捉到不确定性, 这是由于数据中的不可减少噪声,或生成数据的过程的随机特性。
在此示例中,我们将输出建模为IndependentNormal
分布,
具有可学习的均值和方差参数。如果任务是分类,
我们将使用IndependentBernoulli
与二元类,以及OneHotCategorical
与多个类,以建模模型输出的分布。
def create_probablistic_bnn_model(train_size):
inputs = create_model_inputs()
features = keras.layers.concatenate(list(inputs.values()))
features = layers.BatchNormalization()(features)
# 使用DenseVariational层创建具有权重不确定性的隐藏层。
for units in hidden_units:
features = tfp.layers.DenseVariational(
units=units,
make_prior_fn=prior,
make_posterior_fn=posterior,
kl_weight=1 / train_size,
activation="sigmoid",
)(features)
# 创建一个概率输出(正态分布),并使用`Dense`层
# 来生成分布的参数。
# 我们设置units=2以学习正态分布的均值和方差。
distribution_params = layers.Dense(units=2)(features)
outputs = tfp.layers.IndependentNormal(1)(distribution_params)
model = keras.Model(inputs=inputs, outputs=outputs)
return model
由于模型的输出是一个分布,而不是一个点估计,我们使用负对数似然作为我们的损失函数,以计算从模型产生的估计分布中看到真实数据(目标)的可能性。
def negative_loglikelihood(targets, estimated_distribution):
return -estimated_distribution.log_prob(targets)
num_epochs = 1000
prob_bnn_model = create_probablistic_bnn_model(train_size)
run_experiment(prob_bnn_model, negative_loglikelihood, train_dataset, test_dataset)
开始训练模型...
Epoch 1/1000
17/17 [==============================] - 2s 36ms/step - loss: 11.2378 - root_mean_squared_error: 6.6758 - val_loss: 8.5554 - val_root_mean_squared_error: 6.6240
Epoch 2/1000
17/17 [==============================] - 0s 7ms/step - loss: 11.8285 - root_mean_squared_error: 6.5718 - val_loss: 8.2138 - val_root_mean_squared_error: 6.5256
Epoch 3/1000
17/17 [==============================] - 0s 7ms/step - loss: 8.8566 - root_mean_squared_error: 6.5369 - val_loss: 5.8749 - val_root_mean_squared_error: 6.3394
Epoch 4/1000
17/17 [==============================] - 0s 7ms/step - loss: 7.8191 - root_mean_squared_error: 6.3981 - val_loss: 7.6224 - val_root_mean_squared_error: 6.4473
Epoch 5/1000
17/17 [==============================] - 0s 7ms/step - loss: 6.2598 - root_mean_squared_error: 6.4613 - val_loss: 5.9415 - val_root_mean_squared_error: 6.3466
...
Epoch 995/1000
17/17 [==============================] - 0s 7ms/step - loss: 1.1323 - root_mean_squared_error: 1.0431 - val_loss: 1.1553 - val_root_mean_squared_error: 1.1060
Epoch 996/1000
17/17 [==============================] - 0s 7ms/step - loss: 1.1613 - root_mean_squared_error: 1.0686 - val_loss: 1.1554 - val_root_mean_squared_error: 1.0370
Epoch 997/1000
17/17 [==============================] - 0s 7ms/step - loss: 1.1351 - root_mean_squared_error: 1.0628 - val_loss: 1.1472 - val_root_mean_squared_error: 1.0813
Epoch 998/1000
17/17 [==============================] - 0s 7ms/step - loss: 1.1324 - root_mean_squared_error: 1.0858 - val_loss: 1.1527 - val_root_mean_squared_error: 1.0578
Epoch 999/1000
17/17 [==============================] - 0s 7ms/step - loss: 1.1591 - root_mean_squared_error: 1.0801 - val_loss: 1.1483 - val_root_mean_squared_error: 1.0442
Epoch 1000/1000
17/17 [==============================] - 0s 7ms/step - loss: 1.1402 - root_mean_squared_error: 1.0554 - val_loss: 1.1495 - val_root_mean_squared_error: 1.0389
模型训练完成。
训练 RMSE: 1.068
评估模型性能...
测试 RMSE: 1.068
现在让我们从模型中生成给定测试样本的输出。输出现在是一个分布,我们可以使用它的均值和方差来计算预测的置信区间(CI)。
prediction_distribution = prob_bnn_model(examples)
prediction_mean = prediction_distribution.mean().numpy().tolist()
prediction_stdv = prediction_distribution.stddev().numpy()
# 95% CI 计算为 mean ± (1.96 * stdv)
upper = (prediction_mean + (1.96 * prediction_stdv)).tolist()
lower = (prediction_mean - (1.96 * prediction_stdv)).tolist()
prediction_stdv = prediction_stdv.tolist()
for idx in range(sample):
print(
f"预测均值: {round(prediction_mean[idx][0], 2)}, "
f"标准差: {round(prediction_stdv[idx][0], 2)}, "
f"95% CI: [{round(upper[idx][0], 2)} - {round(lower[idx][0], 2)}]"
f" - 实际: {targets[idx]}"
)
预测均值: 5.29, 标准差: 0.66, 95% CI: [6.58 - 4.0] - 实际: 6.0
预测均值: 6.49, 标准差: 0.81, 95% CI: [8.08 - 4.89] - 实际: 6.0
预测均值: 5.85, 标准差: 0.7, 95% CI: [7.22 - 4.48] - 实际: 7.0
预测均值: 5.59, 标准差: 0.69, 95% CI: [6.95 - 4.24] - 实际: 5.0
预测均值: 6.37, 标准差: 0.87, 95% CI: [8.07 - 4.67] - 实际: 5.0
预测均值: 6.34, 标准差: 0.78, 95% CI: [7.87 - 4.81] - 实际: 7.0
预测均值: 5.14, 标准差: 0.65, 95% CI: [6.4 - 3.87] - 实际: 5.0
预测均值: 6.49, 标准差: 0.81, 95% CI: [8.09 - 4.89] - 实际: 6.0
预测均值: 6.25, 标准差: 0.77, 95% CI: [7.76 - 4.74] - 实际: 6.0
预测均值: 6.39, 标准差: 0.78, 95% CI: [7.92 - 4.85] - 实际: 7.0