A Bayesian CNN: asking a network several times¶
Backend: TensorFlow, JAX or PyTorch
Dataset: Fashion-MNIST, MNIST
Techniques: Conv2DFlipout, DenseFlipout
Average a Bayesian CNN's repeated predictions, and use their disagreement to flag unfamiliar inputs.
A Bayesian network learns a distribution over its weights instead of a single value. Each prediction samples new weights, like asking a slightly different expert. When the experts agree, the network is sure; when they disagree, it is not. Two benefits follow:
- Averaging several predictions is usually more accurate than any single one.
- The disagreement between them measures what the network does not know, and flags inputs unlike the training data.
Here, a Bayesian CNN learns Fashion-MNIST and is then shown MNIST digits.
# Install keras-proba, then choose a backend: "jax", "tensorflow" or "torch".
!pip install -q keras-proba
import os
os.environ["KERAS_BACKEND"] = "jax"
Load the data¶
Fashion-MNIST clothing images for training and testing, and MNIST digits as unfamiliar inputs.
import matplotlib.pyplot as plt
import numpy as np
import keras
import keras_proba
keras.utils.set_random_seed(0)
(x_train, y_train), (x_test, y_test) = keras.datasets.fashion_mnist.load_data()
(_, _), (x_digits, _) = keras.datasets.mnist.load_data()
x_train, x_test, x_digits = (images[..., None].astype("float32") / 255 for images in (x_train, x_test, x_digits))
A Bayesian CNN¶
Conv2DFlipout and DenseFlipout are the Bayesian versions of Conv2D and Dense. Training
fits the data while keeping each weight distribution close to a prior; kl_weight (the standard
choice is 1 / number of training examples) sets how much the prior counts.
kl_weight = 1.0 / len(x_train)
model = keras.Sequential(
[
keras.Input((28, 28, 1)),
keras_proba.layers.Conv2DFlipout(16, 3, padding="same", activation="relu", kl_weight=kl_weight),
keras.layers.MaxPooling2D(),
keras_proba.layers.Conv2DFlipout(32, 3, padding="same", activation="relu", kl_weight=kl_weight),
keras.layers.MaxPooling2D(),
keras.layers.Flatten(),
keras_proba.layers.DenseFlipout(128, activation="relu", kl_weight=kl_weight),
keras_proba.layers.DenseFlipout(10, kl_weight=kl_weight),
]
)
model.compile(optimizer="adam", loss=keras.losses.SparseCategoricalCrossentropy(from_logits=True), metrics=["accuracy"])
model.fit(x_train, y_train, epochs=5, batch_size=128, verbose=0)
Ask the network 20 times¶
Every predict call samples new weights. We keep all 20 predicted class distributions.
def predict_samples(x, num_samples=20):
logits = [model.predict(x, batch_size=512, verbose=0) for _ in range(num_samples)]
return np.stack([keras.ops.convert_to_numpy(keras.ops.softmax(l)) for l in logits]) # (samples, images, classes)
samples_test, samples_digits = predict_samples(x_test), predict_samples(x_digits)
single = (samples_test[0].argmax(-1) == y_test).mean()
averaged = (samples_test.mean(0).argmax(-1) == y_test).mean()
print(f"Accuracy: one prediction {single:.1%}, average of 20 predictions {averaged:.1%}")
Measure the disagreement¶
The mutual information between the predictions and the weights measures how much the 20 predictions disagree: the entropy of their average minus their average entropy. It is near zero when all samples predict the same thing, and large when they contradict each other.
def entropy(p):
return -np.sum(p * np.log(p + 1e-12), axis=-1)
def mutual_information(samples):
return entropy(samples.mean(0)) - entropy(samples).mean(0)
plt.figure(figsize=(7, 3))
plt.hist(mutual_information(samples_test), bins=50, range=(0, 1), alpha=0.6, density=True, label="clothing (seen)")
plt.hist(mutual_information(samples_digits), bins=50, range=(0, 1), alpha=0.6, density=True, label="digits (never seen)")
plt.xlabel("disagreement between the 20 predictions (mutual information)")
plt.legend()
plt.show()
def auroc(familiar, unfamiliar):
ranks = np.concatenate([familiar, unfamiliar]).argsort().argsort() + 1
n, m = len(familiar), len(unfamiliar)
return (ranks[n:].sum() - m * (m + 1) / 2) / (n * m)
print(f"Disagreement separates digits from clothing with AUROC {auroc(mutual_information(samples_test), mutual_information(samples_digits)):.3f} (0.5 = chance)")
What we learned¶
- Bayesian layers are drop-in replacements for
Conv2DandDense; every prediction samples new weights. - Averaging repeated predictions improves accuracy, and their disagreement flags inputs the network does not know.
- The cost is several forward passes per prediction. SNGP detects unfamiliar inputs in a single pass.