在这一节中,我们将会动手实现一个批量归一化层,来验证批量归一化的实际作用。
反向传播
在上一节中,我们知道了批量归一化的正向计算过程,这一节中,为了实现完整的批量归一化层,我们首先需要推导它的反向传播公式,然后用代码实现。本节中的公式序号接上一节,以便于说明。
首先假设已知从上一层回传给批量归一化层的误差矩阵是:
\delta = {dJ \over dZ},\delta_i = {dJ \over dz_i} \tag{10}
求批量归一化层参数梯度
则根据公式9,求\gamma 和 \beta的梯度:
{dJ \over d\gamma} = \sum{i=1}^m {dJ \over dz_i}{dz_i \over d\gamma}=\sum{i=1}^m \delta_i \cdot n_i \tag{11}
{dJ \over d\beta} = \sum{i=1}^m {dJ \over dz_i}{dz_i \over d\beta}=\sum{i=1}^m \delta_i \tag{12}
注意\gamma和\beta的形状与批大小无关,只与特征值数量有关,我们假设特征值数量为1,所以它们都是一个标量。在从计算图看,它们都与N,Z的全集相关,而不是某一个样本,因此会用求和方式计算。
求批量归一化层的前传误差矩阵
下述所有乘法都是element-wise的矩阵点乘,不再特殊说明。
从正向公式中看,对z有贡献的数据链是:
- $$z_i \leftarrow n_i \leftarrow x_i$$
- $$z_i \leftarrow n_i \leftarrow \mu_B \leftarrow x_i$$
- $$z_i \leftarrow n_i \leftarrow \sigma^2_B \leftarrow x_i$$
- $$z_i \leftarrow n_i \leftarrow \sigma^2_B \leftarrow \mu_B \leftarrow x_i$$
从公式8,9:
{dJ \over dx_i} = {dJ \over d n_i}{d n_i \over dx_i} + {dJ \over d \sigma^2_B}{d \sigma^2_B \over dx_i} + {dJ \over d \mu_B}{d \mu_B \over dx_i} \tag{13}
公式13的右侧第一部分(与全连接层形式一样):
{dJ \over d n_i}= {dJ \over dz_i}{dz_i \over dn_i} = \delta_i \cdot \gamma\tag{14}
上式等价于:
{dJ \over d N}= \delta \cdot \gamma\tag{14}
公式14中,我们假设样本数为64,特征值数为10,则得到一个64x10的结果矩阵(因为1x10的矩阵会被广播为64x10的矩阵):
$$
\delta^{(64 \times 10)} \odot \gamma^{(1 \times 10)}=R^{(64 \times 10)}
$$
公式13的右侧第二部分,从公式8:
{d n_i \over dx_i}={1 \over \sqrt{\sigma^2_B + \epsilon}} \tag{15}
公式13的右侧第三部分,从公式8(注意$\sigma^2_B$是个标量,而且与X,N的全集相关,要用求和方式):
\begin{aligned} {dJ \over d \sigma^2B} = \sum{i=1}^m {dJ \over d n_i}{d n_i \over d \sigma^2_B} \ &= -{1 \over 2}(\sigma^2_B + \epsilon)m {dJ \over d n_i} \cdot (x_i-\mu_B) \end{aligned} \tag{16}
公式13的右侧第四部分,从公式7:
{d \sigma^2_B \over dx_i} = {2(x_i - \mu_B) \over m} \tag{17}
公式13的右侧第五部分,从公式7,8:
{dJ \over d \mu_B}={dJ \over d n_i}{d n_i \over d \mu_B} + {dJ \over d\sigma^2_B}{d \sigma^2_B \over d \mu_B} \tag{18}
公式18的右侧第二部分,根据公式8:
{d n_i \over d \mu_B}={-1 \over \sqrt{\sigma^2_B + \epsilon}} \tag{19}
公式18的右侧第四部分,根据公式7(\sigma^2_B和\mu_B与全体x_i相关,所以要用求和):
{d \sigma^2B \over d \mu_B}=-{2 \over m}\sum{i=1}^m (x_i- \mu_B) \tag{20}
所以公式18是:
{dJ \over d \mu_B}=-{\delta \cdot \gamma \over \sqrt{\sigma^2_B + \epsilon}} - {2 \over m}{dJ \over d \sigmam (x_i- \mu_B) \tag{18}
公式13的右侧第六部分,从公式6:
{d \mu_B \over dx_i} = {1 \over m} \tag{21}
所以,公式13最后是这样的:
{dJ \over dx_i} = {\delta \cdot \gamma \over \sqrt{\sigma^2_B + \epsilon}} + {dJ \over d\sigma^2_B} \cdot {2(x_i - \mu_B) \over m} + {dJ \over d\mu_B} \cdot {1 \over m} \tag{13}
代码实现
初始化类
class BnLayer(CLayer):def __init__(self, input_size, momentum=0.9):self.gamma = np.ones((1, input_size))self.beta = np.zeros((1, input_size))self.eps = 1e-5self.input_size = input_sizeself.output_size = input_sizeself.momentum = momentumself.running_mean = np.zeros((1,input_size))self.running_var = np.zeros((1,input_size))
后面三个变量,momentum、running_mean、running_var,是为了计算/记录历史方差均差的。
前向计算
def forward(self, input, train=True):......
前向计算完全按照上一节中的公式6到公式9实现。要注意在训练/测试阶段的不同算法,用train是否为True来做分支判断。
反向传播
def backward(self, delta_in, flag):......
d_norm_x需要多次使用,所以先计算出来备用,以增加代码性能。
公式16中有一个(\sigma^2_B + \epsilon)^{-3/2}次方,在前向计算中,我们令:
self.var = np.mean(self.x_mu**2, axis=0, keepdims=True) + self.epsself.std = np.sqrt(self.var)
则:
$$
self.var \times self.std = self.var \times self.var{(3/2)}
$$
放在分母中就是(-3/2)次方了。
另外代码中有很多np.sum(…, axis=0, keepdims=True),这个和全连接层中的多样本计算一个道理,都是按样本数求和,并保持维度,便于后面的矩阵运算。
更新参数
def update(self, learning_rate=0.1):self.gamma = self.gamma - self.d_gamma * learning_rateself.beta = self.beta - self.d_beta * learning_rate
更新\gamma和\beta时,我们使用0.1作为学习率。在初始化代码中,并没有给批量归一化层指定学习率,如果有需求的话,读者可以自行添加这部分逻辑。
批量归一化层的实际应用
首先回忆一下MNIST的图片分类网络,当时的模型如图15-15所示。

当时用了6个epoch(5763个Iteration),达到了0.12的预计loss值而停止训练。我们看看使用批量归一化后的样子,如图15-16所示。

在全连接层和激活函数之间,加入一个批量归一化层,最后的分类函数Softmax前面不能加批量归一化。
主程序代码
if __name__ == '__main__':......params = HyperParameters_4_1(learning_rate, max_epoch, batch_size,net_type=NetType.MultipleClassifier,init_method=InitialMethod.MSRA,stopper=Stopper(StopCondition.StopLoss, 0.12))net = NeuralNet_4_1(params, "MNIST")fc1 = FcLayer_1_1(num_input, num_hidden1, params)net.add_layer(fc1, "fc1")bn1 = BnLayer(num_hidden1)net.add_layer(bn1, "bn1")r1 = ActivationLayer(Relu())net.add_layer(r1, "r1")......
前后都省略了一些代码,注意上面代码片段中的bn1,就是应用了批量归一化层。
运行结果
为了比较,我们使用与14.6中完全一致的参数设置来训练这个有批量归一化的模型,得到如图15-17所示的结果。

打印输出的最后几行如下:
......epoch=4, total_iteration=4267loss_train=0.079916, accuracy_train=0.968750loss_valid=0.117291, accuracy_valid=0.967667time used: 19.44783306121826save parameterstesting...0.9663
列表15-12比较一下使用批量归一化前后的区别。
表15-12 批量归一化的作用
| 不使用批量归一化 | 使用批量归一化 | |
|---|---|---|
| 停止条件 | loss < 0.12 | loss < 0.12 |
| 训练次数 | 6个epoch(5763次迭代) | 4个epoch(4267次迭代) |
| 花费时间 | 17秒 | 19秒 |
| 准确率 | 96.97% | 96.63% |
使用批量归一化后,迭代速度提升,但是花费时间多了2秒,这是因为批量归一化的正向和反向计算过程还是比较复杂的,需要花费一些时间,但是批量归一化确实可以帮助网络快速收敛。如果使用GPU的话,花费时间上的差异应该可以忽略。
在准确率上的差异可以忽略,由于样本误差问题和随机初始化参数的差异,会造成最后的训练结果有细微差别。
代码位置
原代码位置:ch15, Level6
个人代码:Mnist_BN**
keras实现
from ExtendedDataReader.MnistImageDataReader import *from keras.models import Sequentialfrom keras.layers import Dense, BatchNormalizationimport matplotlib.pyplot as pltimport osos.environ['KMP_DUPLICATE_LIB_OK']='True'def load_data():dataReader = MnistImageDataReader(mode="vector")dataReader.ReadData()dataReader.NormalizeX()dataReader.NormalizeY(NetType.MultipleClassifier)dataReader.GenerateValidationSet(k=20)x_train, y_train = dataReader.XTrain, dataReader.YTrainx_test, y_test = dataReader.XTest, dataReader.YTestx_val, y_val = dataReader.XDev, dataReader.YDevx_train = x_train.reshape(x_train.shape[0], 28 * 28)x_test = x_test.reshape(x_test.shape[0], 28 * 28)x_val = x_val.reshape(x_val.shape[0], 28 * 28)return x_train, y_train, x_test, y_test, x_val, y_valdef build_model():model = Sequential()model.add(Dense(128, activation='relu', input_shape=(784, )))model.add(BatchNormalization())model.add(Dense(64, activation='relu'))model.add(BatchNormalization())model.add(Dense(32, activation='relu'))model.add(BatchNormalization())model.add(Dense(16, activation='relu'))model.add(BatchNormalization())model.add(Dense(10, activation='softmax'))model.compile(optimizer='Adam',loss='categorical_crossentropy',metrics=['accuracy'])return model#画出训练过程中训练和验证的精度与损失def draw_train_history(history):plt.figure(1)# summarize history for accuracyplt.subplot(211)plt.plot(history.history['accuracy'])plt.plot(history.history['val_accuracy'])plt.title('model accuracy')plt.ylabel('accuracy')plt.xlabel('epoch')plt.legend(['train', 'validation'])# summarize history for lossplt.subplot(212)plt.plot(history.history['loss'])plt.plot(history.history['val_loss'])plt.title('model loss')plt.ylabel('loss')plt.xlabel('epoch')plt.legend(['train', 'validation'])plt.show()if __name__ == '__main__':x_train, y_train, x_test, y_test, x_val, y_val = load_data()# print(x_train.shape)# print(x_test.shape)# print(x_val.shape)model = build_model()history = model.fit(x_train, y_train, epochs=20, batch_size=64, validation_data=(x_val, y_val))draw_train_history(history)loss, accuracy = model.evaluate(x_test, y_test)print("test loss: {}, test accuracy: {}".format(loss, accuracy))weights = model.get_weights()print("weights: ", weights)
模型输出
test loss: 0.08176513702145312, test accuracy: 0.978600025177002
模型损失以及准确率曲线

