本节我们介绍批量归一化(batch normalization)层,它能让较深的神经网络的训练变得更加容易 [1]。在 “实战Kaggle比赛:预测房价” 一节里,我们对输入数据做了标准化处理:处理后的任意一个特征在数据集中所有样本上的均值为0、标准差为1。标准化处理输入数据使各个特征的分布相近:这往往更容易训练出有效的模型。
通常来说,数据标准化预处理对于浅层模型就足够有效了。随着模型训练的进行,当每层中参数更新时,靠近输出层的输出较难出现剧烈变化。但对深层神经网络来说,即使输入数据已做标准化,训练中模型参数的更新依然很容易造成靠近输出层输出的剧烈变化。这种计算数值的不稳定性通常令我们难以训练出有效的深度模型。
批量归一化的提出正是为了应对深度模型训练的挑战。在模型训练时,批量归一化利用小批量上的均值和标准差,不断调整神经网络中间输出,从而使整个神经网络在各层的中间输出的数值更稳定。批量归一化和下一节将要介绍的残差网络为训练和设计深度模型提供了两类重要思路。
对全连接层和卷积层做批量归一化的方法稍有不同。下面我们将分别介绍这两种情况下的批量归一化。
我们先考虑如何对全连接层做批量归一化。通常,我们将批量归一化层置于全连接层中的仿射变换和激活函数之间。设全连接层的输入为$\boldsymbol{u}$,权重参数和偏差参数分别为$\boldsymbol{W}$和$\boldsymbol{b}$,,激活函数为ϕ。设批量归一化的运算符为$\boldsymbol{BN}$。那么,使用批量归一化的全连接层的输出为:
对卷积层来说,批量归一化发生在卷积计算之后、应用激活函数之前。如果卷积计算输出多个通道,我们需要对这些通道的输出分别做批量归一化,且每个通道都拥有独立的拉伸和偏移参数,并均为标量。设小批量中有 m个样本。在单个通道上,假设卷积计算输出的高和宽分别为 p和 q 。我们需要对该通道中 m×p×q个元素同时做批量归一化。对这些元素做标准化计算时,我们使用相同的均值和方差,即该通道中 m×p×q个元素的均值和方差。
使用批量归一化训练时,我们可以将批量大小设得大一点,从而使批量内样本的均值和方差的计算都较为准确。将训练好的模型用于预测时,我们希望模型对于任意输入都有确定的输出。因此,单个样本的输出不应取决于批量归一化所需要的随机小批量中的均值和方差。一种常用的方法是通过移动平均估算整个训练数据集的样本均值和方差,并在预测时使用它们得到确定的输出。可见,和丢弃层一样,批量归一化层在训练模式和预测模式下的计算结果也是不一样的。
下面我们通过numpy中的ndarray来实现批量归一化层。
def batch_norm(is_training,X, gamma, beta, moving_mean, moving_var, eps, momentum):
# 判断是当前模式是训练模式还是预测模式
if not is_training:
# 如果是在预测模式下,直接使用传入的移动平均所得的均值和方差
X_hat = (X - moving_mean) / np.sqrt(moving_var + eps)
else:
assert len(X.shape) in (2, 4)
if len(X.shape) == 2:
# 使用全连接层的情况,计算特征维上的均值和方差
mean = X.mean(axis=0)
var = ((X - mean) ** 2).mean(axis=0)
else:
# 使用二维卷积层的情况,计算通道维上(axis=1)的均值和方差。这里我们需要保持
# X的形状以便后面可以做广播运算
mean = X.mean(axis=(0, 2, 3), keepdims=True)
var = ((X - mean) ** 2).mean(axis=(0, 2, 3), keepdims=True)
# 训练模式下用当前的均值和方差做标准化
X_hat = (X - mean) / np.sqrt(var + eps)
# 更新移动平均的均值和方差
moving_mean = momentum * moving_mean + (1.0 - momentum) * mean
moving_var = momentum * moving_var + (1.0 - momentum) * var
Y = gamma * X_hat + beta # 拉伸和偏移
return Y, moving_mean, moving_var
接下来,我们自定义一个BatchNorm层。它保存参与求梯度和迭代的拉伸参数gamma和偏移参数beta,同时也维护移动平均得到的均值和方差,以便能够在模型预测时被使用。BatchNorm实例所需指定的num_features参数对于全连接层来说应为输出个数,对于卷积层来说则为输出通道数。该实例所需指定的num_dims参数对于全连接层和卷积层来说分别为2和4。
class BatchNormalization(tf.keras.layers.Layer):
def __init__(self, decay=0.9, epsilon=1e-5, **kwargs):
self.decay = decay
self.epsilon = epsilon
super(BatchNormalization, self).__init__(**kwargs)
def build(self, input_shape):
self.gamma = self.add_weight(name='gamma',
shape=[input_shape[-1], ],
initializer=tf.initializers.ones,
trainable=True)
self.beta = self.add_weight(name='beta',
shape=[input_shape[-1], ],
initializer=tf.initializers.zeros,
trainable=True)
self.moving_mean = self.add_weight(name='moving_mean',
shape=[input_shape[-1], ],
initializer=tf.initializers.zeros,
trainable=False)
self.moving_variance = self.add_weight(name='moving_variance',
shape=[input_shape[-1], ],
initializer=tf.initializers.ones,
trainable=False)
super(BatchNormalization, self).build(input_shape)
def assign_moving_average(self, variable, value):
"""
variable = variable * decay + value * (1 - decay)
"""
delta = variable * self.decay + value * (1 - self.decay)
return variable.assign(delta)
@tf.function
def call(self, inputs, training):
if training:
batch_mean, batch_variance = tf.nn.moments(inputs, list(range(len(inputs.shape) - 1)))
mean_update = self.assign_moving_average(self.moving_mean, batch_mean)
variance_update = self.assign_moving_average(self.moving_variance, batch_variance)
self.add_update(mean_update)
self.add_update(variance_update)
mean, variance = batch_mean, batch_variance
else:
mean, variance = self.moving_mean, self.moving_variance
output = tf.nn.batch_normalization(inputs,
mean=mean,
variance=variance,
offset=self.beta,
scale=self.gamma,
variance_epsilon=self.epsilon)
return output
def compute_output_shape(self, input_shape):
return input_shape
下面我们修改“卷积神经网络(LeNet)”这一节介绍的LeNet模型,从而应用批量归一化层。我们在所有的卷积层或全连接层之后、激活层之前加入批量归一化层。
net = tf.keras.models.Sequential(
[tf.keras.layers.Conv2D(filters=6,kernel_size=5),
BatchNormalization(),
tf.keras.layers.Activation('sigmoid'),
tf.keras.layers.MaxPool2D(pool_size=2, strides=2),
tf.keras.layers.Conv2D(filters=16,kernel_size=5),
BatchNormalization(),
tf.keras.layers.Activation('sigmoid'),
tf.keras.layers.MaxPool2D(pool_size=2, strides=2),
tf.keras.layers.Flatten(),
tf.keras.layers.Dense(120),
BatchNormalization(),
tf.keras.layers.Activation('sigmoid'),
tf.keras.layers.Dense(84),
BatchNormalization(),
tf.keras.layers.Activation('sigmoid'),
tf.keras.layers.Dense(10,activation='sigmoid')]
)
下面我们训练修改后的模型。
(x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data()
x_train = x_train.reshape((60000, 28, 28, 1)).astype('float32') / 255
x_test = x_test.reshape((10000, 28, 28, 1)).astype('float32') / 255
net.compile(loss='sparse_categorical_crossentropy',
optimizer=tf.keras.optimizers.RMSprop(),
metrics=['accuracy'])
history = net.fit(x_train, y_train,
batch_size=64,
epochs=5,
validation_split=0.2)
test_scores = net.evaluate(x_test, y_test, verbose=2)
print('Test loss:', test_scores[0])
print('Test accuracy:', test_scores[1])
Train on 48000 samples, validate on 12000 samples
Epoch 1/5
48000/48000 [==============================] - 25s 526us/sample - loss: 0.4030 - accuracy: 0.9416 - val_loss: 0.2181 - val_accuracy: 0.9346
Epoch 2/5
48000/48000 [==============================] - 23s 470us/sample - loss: 0.0823 - accuracy: 0.9773 - val_loss: 0.0782 - val_accuracy: 0.9774
Epoch 3/5
48000/48000 [==============================] - 22s 457us/sample - loss: 0.0616 - accuracy: 0.9816 - val_loss: 0.0935 - val_accuracy: 0.9721
Epoch 4/5
48000/48000 [==============================] - 24s 498us/sample - loss: 0.0508 - accuracy: 0.9847 - val_loss: 0.0947 - val_accuracy: 0.9720
Epoch 5/5
48000/48000 [==============================] - 28s 574us/sample - loss: 0.0447 - accuracy: 0.9861 - val_loss: 0.2494 - val_accuracy: 0.9176
10000/1 - 1s - loss: 0.2494 - accuracy: 0.9216
Test loss: 0.24052143813222646
Test accuracy: 0.9216
最后我们查看第一个批量归一化层学习到的拉伸参数gamma和偏移参数beta。
net.get_layer(index=1).gamma,net.get_layer(index=1).beta
(<tf.Variable 'sequential/batch_normalization/gamma:0' shape=(6,) dtype=float32, numpy=
array([0.93455726, 1.0476167 , 1.0069158 , 0.9169525 , 1.4070569 ,
0.9973122 ], dtype=float32)>,
<tf.Variable 'sequential/batch_normalization/beta:0' shape=(6,) dtype=float32, numpy=
array([-0.64863527, -0.26409525, -0.60552806, 0.00235984, 0.25987023,
-0.38496298], dtype=float32)>)
与我们刚刚自己定义的BatchNorm
类相比,keras中layers
模块定义的BatchNorm
类使用起来更加简单。它不需要指定自己定义的BatchNorm
类中所需的num_features
和num_dims
参数值。在keras中,这些参数值都将通过延后初始化而自动获取。下面我们用keras实现使用批量归一化的LeNet。
net = tf.keras.models.Sequential()
net.add(tf.keras.layers.Conv2D(filters=6,kernel_size=5))
net.add(tf.keras.layers.BatchNormalization())
net.add(tf.keras.layers.Activation('sigmoid'))
net.add(tf.keras.layers.MaxPool2D(pool_size=2, strides=2))
net.add(tf.keras.layers.Conv2D(filters=16,kernel_size=5))
net.add(tf.keras.layers.BatchNormalization())
net.add(tf.keras.layers.Activation('sigmoid'))
net.add(tf.keras.layers.MaxPool2D(pool_size=2, strides=2))
net.add(tf.keras.layers.Flatten())
net.add(tf.keras.layers.Dense(120))
net.add(tf.keras.layers.BatchNormalization())
net.add(tf.keras.layers.Activation('sigmoid'))
net.add(tf.keras.layers.Dense(84))
net.add(tf.keras.layers.BatchNormalization())
net.add(tf.keras.layers.Activation('sigmoid'))
net.add(tf.keras.layers.Dense(10,activation='sigmoid'))
使用同样的超参数进行训练。
(x_train, y_train), (x_test, y_test) = tf.keras.datasets.mnist.load_data()
x_train = x_train.reshape((60000, 28, 28, 1)).astype('float32') / 255
x_test = x_test.reshape((10000, 28, 28, 1)).astype('float32') / 255
net.compile(loss='sparse_categorical_crossentropy',
optimizer=tf.keras.optimizers.RMSprop(),
metrics=['accuracy'])
history = net.fit(x_train, y_train,
batch_size=64,
epochs=5,
validation_split=0.2)
test_scores = net.evaluate(x_test, y_test, verbose=2)
print('Test loss:', test_scores[0])
print('Test accuracy:', test_scores[1])
Train on 48000 samples, validate on 12000 samples
Epoch 1/5
48000/48000 [==============================] - 27s 553us/sample - loss: 0.0383 - accuracy: 0.9877 - val_loss: 0.1457 - val_accuracy: 0.9581
Epoch 2/5
48000/48000 [==============================] - 25s 511us/sample - loss: 0.0342 - accuracy: 0.9890 - val_loss: 0.1359 - val_accuracy: 0.9613
Epoch 3/5
48000/48000 [==============================] - 22s 467us/sample - loss: 0.0296 - accuracy: 0.9907 - val_loss: 0.1943 - val_accuracy: 0.9402
Epoch 4/5
48000/48000 [==============================] - 23s 476us/sample - loss: 0.0285 - accuracy: 0.9910 - val_loss: 0.7860 - val_accuracy: 0.8291
Epoch 5/5
48000/48000 [==============================] - 23s 478us/sample - loss: 0.0234 - accuracy: 0.9927 - val_loss: 0.0897 - val_accuracy: 0.9732
10000/1 - 1s - loss: 0.0416 - accuracy: 0.9726
Test loss: 0.08218458472136408
Test accuracy: 0.9726
- 在模型训练时,批量归一化利用小批量上的均值和标准差,不断调整神经网络的中间输出,从而使整个神经网络在各层的中间输出的数值更稳定。
- 对全连接层和卷积层做批量归一化的方法稍有不同。
- 批量归一化层和丢弃层一样,在训练模式和预测模式的计算结果是不一样的。
- keras提供的BatchNorm类使用起来简单、方便。
- 能否将批量归一化前的全连接层或卷积层中的偏差参数去掉?为什么?(提示:回忆批量归一化中标准化的定义。)
- 尝试调大学习率。同“卷积神经网络(LeNet)”一节中未使用批量归一化的LeNet相比,现在是不是可以使用更大的学习率?
- 尝试将批量归一化层插入LeNet的其他地方,观察并分析结果的变化。
- 尝试一下不学习拉伸参数
gamma
和偏移参数beta
(构造的时候加入参数grad_req='null'
来避免计算梯度),观察并分析结果。 - 查看
BatchNorm
类的文档来了解更多使用方法,例如,如何在训练时使用基于全局平均的均值和方差。
[1] Ioffe, S., & Szegedy, C. (2015). Batch normalization: Accelerating deep network training by reducing internal covariate shift. arXiv preprint arXiv:1502.03167.
注:本节除了代码之外与原书基本相同,原书传送门