from d2l import tensorflow as d2l
import tensorflow as tfBatch Normalization (Ioffe & Szegedy, 2015) is the single-biggest stability win in modern deep learning.
At each layer, normalize activations within the minibatch to zero mean / unit variance, then rescale with learned \gamma and \beta:
\text{BN}(\mathbf{x}) = \gamma \cdot \frac{\mathbf{x} - \hat\mu_\mathcal{B}}{\sqrt{\hat\sigma_\mathcal{B}^2 + \epsilon}} + \beta.
Compute per-channel mean and variance over the minibatch (and spatial dims, for conv); normalize, then scale + shift:
ModuleBuffers for moving_mean / moving_var (updated only during training); learnable gamma / beta parameters:
class BatchNorm(tf.keras.layers.Layer):
def __init__(self, **kwargs):
super(BatchNorm, self).__init__(**kwargs)
def build(self, input_shape):
weight_shape = [input_shape[-1], ]
# The scale parameter and the shift parameter (model parameters) are
# initialized to 1 and 0, respectively
self.gamma = self.add_weight(name='gamma', shape=weight_shape,
initializer=tf.initializers.ones, trainable=True)
self.beta = self.add_weight(name='beta', shape=weight_shape,
initializer=tf.initializers.zeros, trainable=True)
# The variables that are not model parameters are initialized to 0
self.moving_mean = self.add_weight(name='moving_mean',
shape=weight_shape, initializer=tf.initializers.zeros,
trainable=False)
self.moving_variance = self.add_weight(name='moving_variance',
shape=weight_shape, initializer=tf.initializers.ones,
trainable=False)
super(BatchNorm, self).build(input_shape)
def assign_moving_average(self, variable, value):
momentum = 0.1
delta = (1.0 - momentum) * variable + momentum * value
return variable.assign(delta)
def call(self, inputs, training=False):
if training:
axes = list(range(len(inputs.shape) - 1))
batch_mean = tf.reduce_mean(inputs, axes, keepdims=True)
batch_variance = tf.reduce_mean(tf.math.squared_difference(
inputs, tf.stop_gradient(batch_mean)), axes, keepdims=True)
batch_mean = tf.squeeze(batch_mean, axes)
batch_variance = tf.squeeze(batch_variance, axes)
self.assign_moving_average(
self.moving_mean, batch_mean)
self.assign_moving_average(
self.moving_variance, batch_variance)
mean, variance = batch_mean, batch_variance
else:
mean, variance = self.moving_mean, self.moving_variance
output = batch_norm(inputs, moving_mean=mean, moving_var=variance,
beta=self.beta, gamma=self.gamma, eps=1e-5)
return outputDrop a BatchNorm layer between each conv/linear and its activation:
class BNLeNetScratch(d2l.Classifier):
def __init__(self, lr=0.1, num_classes=10):
super().__init__()
self.save_hyperparameters()
self.net = tf.keras.models.Sequential([
tf.keras.Input(shape=(28, 28, 1)),
tf.keras.layers.Conv2D(filters=6, kernel_size=5),
BatchNorm(), tf.keras.layers.Activation('sigmoid'),
tf.keras.layers.AvgPool2D(pool_size=2, strides=2),
tf.keras.layers.Conv2D(filters=16, kernel_size=5),
BatchNorm(), tf.keras.layers.Activation('sigmoid'),
tf.keras.layers.AvgPool2D(pool_size=2, strides=2),
tf.keras.layers.Flatten(), tf.keras.layers.Dense(120),
BatchNorm(), tf.keras.layers.Activation('sigmoid'),
tf.keras.layers.Dense(84), BatchNorm(),
tf.keras.layers.Activation('sigmoid'),
tf.keras.layers.Dense(num_classes)])Trains noticeably faster than vanilla LeNet — same accuracy in fewer epochs:
After training, gamma and beta are non-trivial — the layer learned the scale/shift it wants:
(<tf.Tensor: shape=(6,), dtype=float32, numpy=
array([2.6330233, 2.25818 , 3.374007 , 3.820023 , 2.5220754, 2.6115816],
dtype=float32)>,
<tf.Tensor: shape=(6,), dtype=float32, numpy=
array([ 2.1658266 , -2.5676103 , -1.7646055 , -2.344429 , 2.7539537 ,
-0.35519245], dtype=float32)>)
nn.BatchNorm2d for conv layers, nn.BatchNorm1d for linear layers — same idea, much faster, handles the eval/training mode switch automatically:
class BNLeNet(d2l.Classifier):
def __init__(self, lr=0.1, num_classes=10):
super().__init__()
self.save_hyperparameters()
self.net = tf.keras.models.Sequential([
tf.keras.Input(shape=(28, 28, 1)),
tf.keras.layers.Conv2D(filters=6, kernel_size=5),
tf.keras.layers.BatchNormalization(),
tf.keras.layers.Activation('sigmoid'),
tf.keras.layers.AvgPool2D(pool_size=2, strides=2),
tf.keras.layers.Conv2D(filters=16, kernel_size=5),
tf.keras.layers.BatchNormalization(),
tf.keras.layers.Activation('sigmoid'),
tf.keras.layers.AvgPool2D(pool_size=2, strides=2),
tf.keras.layers.Flatten(), tf.keras.layers.Dense(120),
tf.keras.layers.BatchNormalization(),
tf.keras.layers.Activation('sigmoid'),
tf.keras.layers.Dense(84),
tf.keras.layers.BatchNormalization(),
tf.keras.layers.Activation('sigmoid'),
tf.keras.layers.Dense(num_classes)])BatchNorm couples examples through the batch statistics:
GroupNorm (Wu & He, 2018): standardize each example within groups of channels. No batch in the statistics: batch size 1 works, and training equals serving.
Per-(example, group) mean 0 / variance 1, at any batch size:
X = tf.random.normal((32, 8, 8, 16)) # Channels last, as Keras expects
gn = tf.keras.layers.GroupNormalization(groups=4) # Groups the last axis
Y = gn(X)
# Collect each (example, group) pair's elements
G = tf.reshape(tf.transpose(Y, (0, 3, 1, 2)), (32, 4, -1))
(tf.reduce_max(tf.abs(tf.reduce_mean(G, axis=2))),
tf.reduce_mean(tf.math.reduce_variance(G, axis=2)),
bool(tf.reduce_all(tf.abs(gn(X[:1]) - Y[:1]) < 1e-5)))(<tf.Tensor: shape=(), dtype=float32, numpy=2.60770320892334e-08>,
<tf.Tensor: shape=(), dtype=float32, numpy=0.9989977478981018>,
True)
The default in detection / segmentation heads and diffusion U-Nets, where per-device batches are small.