如何使用归一化层来改进深度学习模型

718 阅读14分钟

你可能已经被告知要对你的模型进行标准化或规范化输入以提高性能。但什么是标准化,我们如何在我们的深度学习模型中轻松实现它以提高性能?归一化我们的输入旨在创建一组相互之间处于同一尺度的特征,我们将在本文中进一步探讨。

另外,想一想,在神经网络中,每一层的输出都是下一层的输入,所以要问的一个自然问题是。如果对模型的输入进行标准化有助于提高模型的性能,那么对每一层的输入进行标准化是否也有助于提高模型的性能?

大多数时候,答案是肯定的!然而,与规范化整个模型的输入不同,规范化中间层的输入要稍微复杂一些,因为激活是不断变化的。因此,在整个训练集上一遍又一遍地持续计算统计数据是不可行的,或者至少是计算上很昂贵的。在这篇文章中,我们将探讨规范化层,以规范你的模型输入,以及批量规范化,这是一种将各层的输入跨批次标准化的技术。

让我们开始吧!

使用归一化层来改善深度学习模型

概述

本教程分为6个部分;它们是:。

  • 什么是归一化,为什么它有帮助?
  • 在TensorFlow中使用归一化层
  • 什么是批量规范化,为什么要使用它?
  • 批量规范化。在引擎盖下
  • 归一化和批量归一化的操作

什么是归一化,为什么它有帮助?

对一组数据进行归一化,可以将这组数据转化为类似的规模。对于机器学习模型来说,我们的目标通常是重新定位和调整我们的数据,使其处于0和1或-1和1之间,具体取决于数据本身。实现这一目标的一个常见方法是计算数据集上的平均值和标准差,并通过减去平均值和除以标准差来转换每个样本,如果我们假设数据遵循正态分布,这是很好的方法,因为这种方法可以帮助我们规范数据,实现标准的正常分布。

归一化可以帮助我们的神经网络训练,因为不同的特征处于相似的尺度上,这有助于稳定梯度下降步骤,使我们能够使用更大的学习速率,或者帮助模型在给定的学习速率下更快地收敛。

在Tensorflow中使用归一化层

为了使TensorFlow中的输入正常化,我们可以使用Keras中的Normalization层。首先,让我们定义一些样本数据。

sample1 = np.array([
    [1, 1, 1],
    [1, 1, 1],
    [1, 1, 1]
], dtype=np.float32)

sample2 = np.array([
    [2, 2, 2],
    [2, 2, 2],
    [2, 2, 2]
], dtype=np.float32)

sample3 = np.array([
    [3, 3, 3],
    [3, 3, 3],
    [3, 3, 3]
], dtype=np.float32)

然后我们初始化我们的归一化层。

normalization_layer = Normalization()

然后,为了获得数据集的平均值和标准差,并设置我们的归一化层使用这些参数,我们可以在我们的数据上调用Normalization.adapt()方法。

combined_batch = tf.constant(np.expand_dims(np.stack([sample1, sample2, sample3]), axis=-1), dtype=tf.float32)

normalization_layer = Normalization()

normalization_layer.adapt(combined_batch)

在这种情况下,我们使用expand_dims 来增加一个额外的维度,因为归一化层默认是沿着最后一个维度进行归一化的(最后一个维度的每个索引都有自己在训练集上计算的平均值和方差参数),因为这被认为是特征维度,对于RGB图像来说,这通常只是不同颜色的维度。

然后,为了使我们的数据正常化,我们可以在这些数据上调用正常化层,如图所示。

normalization_layer(sample1)

这就得到了输出

<tf.Tensor: shape=(1, 1, 3, 3), dtype=float32, numpy=
array([[[[-1.2247449, -1.2247449, -1.2247449],
         [-1.2247449, -1.2247449, -1.2247449],
         [-1.2247449, -1.2247449, -1.2247449]]]], dtype=float32)>

我们可以通过在我们的原始数据上运行np.meannp.std 来验证这是否是预期的行为,它给我们的平均值是2.0,标准差是0.8164966。

现在我们已经看到了如何规范化我们的输入,让我们来看看另一种规范化方法,即批量规范化。

什么是批量归一化,我们为什么要使用它?

来源:arxiv.org/pdf/1803.08…

从名字上看,你可能可以猜到,批处理规范化一定与训练时的批次有关。简单地说,批归一化将一个层的输入在一个批次中标准化。

你可能会想,为什么我们不能直接计算某一层的均值和方差,并以这种方式进行标准化呢?当我们训练我们的模型时,问题就来了,因为参数在训练过程中会发生变化,因此中间层的激活是不断变化的,计算每次迭代的整个训练集的均值和方差会很耗时,而且可能毫无意义,因为无论如何激活会在每次迭代中发生变化。这就是批量归一化的作用。

在 "批量归一化 "中介绍过。在谷歌的Ioffe和Szegedy撰写的 "批量归一化:通过减少内部协变量偏移加速深层网络训练 "中,批量归一化着眼于标准化一个层的输入,以减少内部协变量偏移的问题。在论文中,内部协变量偏移被定义为 "在训练过程中,随着前几层参数的变化,每层输入的分布也会发生变化 "的问题。

批量归一化解决内部协变量偏移问题的想法受到了争议,特别是在Santurkar等人的 "批量归一化如何帮助优化?"中,有人提出批量归一化反而有助于平滑参数空间上的损失函数。虽然不一定很清楚批归一化是如何做到的,但它在许多不同的问题和模型上都取得了良好的经验结果。

还有一些证据表明,批量归一化可以大大有助于解决深度学习模型常见的梯度消失问题。在最初的ResNet论文中,He等人在分析ResNet与普通网络时提到,即使在普通网络中,"向后传播的梯度在BN(批归一化)下也表现出健康的规范"。

也有人认为,批量规范化还有其他好处,比如允许我们使用更高的学习率,因为批量规范化可以帮助稳定参数增长。它还可以帮助规范化模型。从最初的批归一化论文来看。

"当使用批归一化训练时,一个训练实例与小批中的其他实例结合起来看,训练网络不再为一个给定的训练实例产生确定的值 在我们的实验中,我们发现这种效果对网络的泛化是有利的"

批量规范化。引擎盖下

那么,批量归一化实际上是做什么的?

首先,我们需要计算批次统计,特别是计算每个不同激活的批次的平均值和方差。由于每一层的输出都是神经网络中下一层的输入,通过标准化各层的输出,我们也在标准化我们模型中下一层的输入(尽管在实践中,原始论文中建议在激活函数之前实现批归一化,但对此有一些争议)。

因此,我们计算出

批量上的样本均值和方差

然后,对于每个激活图,我们使用各自的统计数据将每个值归一化

特别是对于卷积神经网络(CNN),我们在同一通道的所有位置上计算这些统计数据。来自原始浴场归一化的论文。

"对于卷积层,我们另外希望归一化服从于卷积属性--这样,同一特征图的不同元素,在不同的位置,以同样的方式进行归一化"

现在我们已经看到了如何计算归一化的激活图,让我们来探索如何使用Numpy数组来实现。

假设我们有这些激活图,所有的激活图都代表一个通道。

import numpy as np

activation_map_sample1 = np.array([
    [1, 1, 1],
    [1, 1, 1],
    [1, 1, 1]
], dtype=np.float32)

activation_map_sample2 = np.array([
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
], dtype=np.float32)

activation_map_sample3 = np.array([
    [9, 8, 7],
    [6, 5, 4],
    [3, 2, 1]
], dtype=np.float32)

然后,我们想把激活图中的每个元素在所有位置和不同的样本中标准化。为了标准化,我们用以下方法计算其平均值和标准差

#get mean across the different samples in batch for each activation
activation_mean_bn = np.mean([activation_map_sample1, activation_map_sample2, activation_map_sample3], axis=0)

#get standard deviation across different samples in batch for each activation
activation_std_bn = np.std([activation_map_sample1, activation_map_sample2, activation_map_sample3], axis=0)

print (activation_mean_bn)
print (activation_std_bn)

来计算它们的平均数和标准差,从而输出

3.6666667
2.8284268

然后,我们可以通过以下方式对激活图进行标准化

#get batch normalized activation map for sample 1
activation_map_sample1_bn = (activation_map_sample1 - activation_mean_bn) / activation_std_bn

和这些存储的输出

activation_map_sample1_bn:
[[-0.94280916 -0.94280916 -0.94280916]
 [-0.94280916 -0.94280916 -0.94280916]
 [-0.94280916 -0.94280916 -0.94280916]]

activation_map_sample2_bn:
[[-0.94280916 -0.58925575 -0.2357023 ]
 [ 0.11785112  0.47140455  0.82495797]
 [ 1.1785114   1.5320647   1.8856182 ]]

activation_map_sample3_bn:
[[ 1.8856182   1.5320647   1.1785114 ]
 [ 0.82495797  0.47140455  0.11785112]
 [-0.2357023  -0.58925575 -0.94280916]]

但是,当涉及到推理时间时,我们遇到了一个障碍。如果我们在推理时没有成批的例子怎么办,即使我们有,如果输出是由输入确定地计算出来的,仍然是最好的。所以,我们需要计算出一组固定的参数,在推理时使用。为此,我们为均值和方差存储一个移动平均数,而在推理时使用它来计算各层的输出。

然而,以这种方式简单地将模型的输入标准化的另一个问题也改变了各层的表示能力。批量规范化论文中提出的一个例子是sigmoid非线性函数,规范化输入会将其限制在sigmoid函数的线性系统中。为了解决这个问题,添加了另一个线性层来缩放和重新定位数值,同时添加了两个可训练的参数来学习应该使用的适当的缩放和中心。

在TensorFlow中实现批量归一化

现在我们了解了批处理归一化在引擎盖下的情况,让我们看看我们如何使用Keras的批处理归一化层作为我们深度学习模型的一部分。

为了在Tensorflow中实现批量归一化作为我们深度学习模型的一部分,我们可以使用keras.layers.BatchNormalization 层。使用我们之前的例子中的Numpy数组,我们可以对其实现批处理正常化。

import tensorflow as tf
import tensorflow.keras as keras
from tensorflow.keras.layers import BatchNormalization
import numpy as np

#expand dims to create the channels
activation_maps = tf.constant(np.expand_dims(np.stack([activation_map_sample1, activation_map_sample2, activation_map_sample3]), axis=0), dtype=tf.float32)

print (f"activation_maps: \n{activation_maps}\n")

print (BatchNormalization(axis=0)(activation_maps, training=True))

这给了我们一个输出

activation_maps: 
[[[[1. 1. 1.]
   [1. 1. 1.]
   [1. 1. 1.]]

   [[1. 2. 3.]
   [4. 5. 6.]
   [7. 8. 9.]]

  [[9. 8. 7.]
   [6. 5. 4.]
   [3. 2. 1.]]]]

tf.Tensor(
[[[[-0.9427501  -0.9427501  -0.9427501 ]
   [-0.9427501  -0.9427501  -0.9427501 ]
   [-0.9427501  -0.9427501  -0.9427501 ]]

  [[-0.9427501  -0.5892188  -0.2356875 ]
   [ 0.11784375  0.471375    0.82490635]
   [ 1.1784375   1.5319688   1.8855002 ]]

  [[ 1.8855002   1.5319688   1.1784375 ]
   [ 0.82490635  0.471375    0.11784375]
   [-0.2356875  -0.5892188  -0.9427501 ]]]], shape=(1, 3, 3, 3), dtype=float32)

默认情况下,BatchNormalization层对线性层使用的比例是1,中心是0,因此这些值与我们之前使用Numpy函数计算的值相似。

归一化和批处理归一化的操作

现在我们已经看到了如何在Tensorflow中实现归一化和批量归一化层,让我们来探索一个使用归一化和批量归一化层的LeNet-5模型,并将其与不使用这两层的模型进行比较。

首先,让我们得到我们的数据集,在这个例子中我们将使用CIFAR-10。

(trainX, trainY), (testX, testY) = keras.datasets.cifar10.load_data()

使用一个带有ReLU激活的LeNet-5模型。

from tensorflow.keras.layers import Dense, Input, Flatten, Conv2D, MaxPool2D
from tensorflow.keras.models import Model
import tensorflow as tf

class LeNet5(tf.keras.Model):
  def __init__(self):
    super(LeNet5, self).__init__()
  def call(self, input_tensor):
    self.conv1 = Conv2D(filters=6, kernel_size=(5,5), padding="same", activation="relu")(input_tensor)
    self.maxpool1 = MaxPool2D(pool_size=(2,2))(self.conv1)
    self.conv2 = Conv2D(filters=16, kernel_size=(5,5), padding="same", activation="relu")(self.maxpool1)
    self.maxpool2 = MaxPool2D(pool_size=(2, 2))(self.conv2)
    self.flatten = Flatten()(self.maxpool2)
    self.fc1 = Dense(units=120, activation="relu")(self.flatten)
    self.fc2 = Dense(units=84, activation="relu")(self.fc1)
    self.fc3 = Dense(units=10, activation="sigmoid")(self.fc2)
    return self.fc3

input_layer = Input(shape=(32,32,3,))
x = LeNet5()(input_layer)

model = Model(inputs=input_layer, outputs=x)

model.compile(optimizer="adam", loss=tf.keras.losses.SparseCategoricalCrossentropy(), metrics="acc")

history = model.fit(x=trainX, y=trainY, batch_size=256, epochs=10, validation_data=(testX, testY))

训练模型给我们的输出。

Epoch 1/10
196/196 [==============================] - 14s 15ms/step - loss: 3.8905 - acc: 0.2172 - val_loss: 1.9656 - val_acc: 0.2853
Epoch 2/10
196/196 [==============================] - 2s 12ms/step - loss: 1.8402 - acc: 0.3375 - val_loss: 1.7654 - val_acc: 0.3678
Epoch 3/10
196/196 [==============================] - 2s 12ms/step - loss: 1.6778 - acc: 0.3986 - val_loss: 1.6484 - val_acc: 0.4039
Epoch 4/10
196/196 [==============================] - 2s 12ms/step - loss: 1.5663 - acc: 0.4355 - val_loss: 1.5644 - val_acc: 0.4380
Epoch 5/10
196/196 [==============================] - 2s 12ms/step - loss: 1.4815 - acc: 0.4712 - val_loss: 1.5357 - val_acc: 0.4472
Epoch 6/10
196/196 [==============================] - 2s 12ms/step - loss: 1.4053 - acc: 0.4975 - val_loss: 1.4883 - val_acc: 0.4675
Epoch 7/10
196/196 [==============================] - 2s 12ms/step - loss: 1.3300 - acc: 0.5262 - val_loss: 1.4643 - val_acc: 0.4805
Epoch 8/10
196/196 [==============================] - 2s 12ms/step - loss: 1.2595 - acc: 0.5531 - val_loss: 1.4685 - val_acc: 0.4866
Epoch 9/10
196/196 [==============================] - 2s 12ms/step - loss: 1.1999 - acc: 0.5752 - val_loss: 1.4302 - val_acc: 0.5026
Epoch 10/10
196/196 [==============================] - 2s 12ms/step - loss: 1.1370 - acc: 0.5979 - val_loss: 1.4441 - val_acc: 0.5009

接下来,让我们看看如果我们添加正常化和批量正常化层会发生什么。我们通常会添加层归一化。修正我们的LeNet-5模型。

class LeNet5_Norm(tf.keras.Model):
  def __init__(self, norm_layer, *args, **kwargs):
    super(LeNet5_Norm, self).__init__()
    self.conv1 = Conv2D(filters=6, kernel_size=(5,5), padding="same")
    self.norm1 = norm_layer(*args, **kwargs)
    self.relu = relu
    self.max_pool2x2 = MaxPool2D(pool_size=(2,2))
    self.conv2 = Conv2D(filters=16, kernel_size=(5,5), padding="same")
    self.norm2 = norm_layer(*args, **kwargs)
    self.flatten = Flatten()
    self.fc1 = Dense(units=120)
    self.norm3 = norm_layer(*args, **kwargs)
    self.fc2 = Dense(units=84)
    self.norm4 = norm_layer(*args, **kwargs)
    self.fc3 = Dense(units=10, activation="softmax")
  def call(self, input_tensor):
    conv1 = self.conv1(input_tensor)
    conv1 = self.norm1(conv1)
    conv1 = self.relu(conv1)
    maxpool1 = self.max_pool2x2(conv1)
    conv2 = self.conv2(maxpool1)
    conv2 = self.norm2(conv2)
    conv2 = self.relu(conv2)
    maxpool2 = self.max_pool2x2(conv2)
    flatten = self.flatten(maxpool2)
    fc1 = self.fc1(flatten)
    fc1 = self.norm3(fc1)
    fc1 = self.relu(fc1)
    fc2 = self.fc2(fc1)
    fc2 = self.norm4(fc2)
    fc2 = self.relu(fc2)
    fc3 = self.fc3(fc2)
    return fc3

并再次运行训练,这次加入了归一化层。

normalization_layer = Normalization()
normalization_layer.adapt(trainX)

input_layer = Input(shape=(32,32,3,))
x = LeNet5_Norm(BatchNormalization)(normalization_layer(input_layer))

model = Model(inputs=input_layer, outputs=x)

model.compile(optimizer="adam", loss=tf.keras.losses.SparseCategoricalCrossentropy(), metrics="acc")

history = model.fit(x=trainX, y=trainY, batch_size=256, epochs=10, validation_data=(testX, testY))

我们看到,模型收敛得更快,并获得了更高的验证精度。

Epoch 1/10
196/196 [==============================] - 5s 17ms/step - loss: 1.4643 - acc: 0.4791 - val_loss: 1.3837 - val_acc: 0.5054
Epoch 2/10
196/196 [==============================] - 3s 14ms/step - loss: 1.1171 - acc: 0.6041 - val_loss: 1.2150 - val_acc: 0.5683
Epoch 3/10
196/196 [==============================] - 3s 14ms/step - loss: 0.9627 - acc: 0.6606 - val_loss: 1.1038 - val_acc: 0.6086
Epoch 4/10
196/196 [==============================] - 3s 14ms/step - loss: 0.8560 - acc: 0.7003 - val_loss: 1.0976 - val_acc: 0.6229
Epoch 5/10
196/196 [==============================] - 3s 14ms/step - loss: 0.7644 - acc: 0.7325 - val_loss: 1.1073 - val_acc: 0.6153
Epoch 6/10
196/196 [==============================] - 3s 15ms/step - loss: 0.6872 - acc: 0.7617 - val_loss: 1.1484 - val_acc: 0.6128
Epoch 7/10
196/196 [==============================] - 3s 14ms/step - loss: 0.6229 - acc: 0.7850 - val_loss: 1.1469 - val_acc: 0.6346
Epoch 8/10
196/196 [==============================] - 3s 14ms/step - loss: 0.5583 - acc: 0.8067 - val_loss: 1.2041 - val_acc: 0.6206
Epoch 9/10
196/196 [==============================] - 3s 15ms/step - loss: 0.4998 - acc: 0.8300 - val_loss: 1.3095 - val_acc: 0.6071
Epoch 10/10
196/196 [==============================] - 3s 14ms/step - loss: 0.4474 - acc: 0.8471 - val_loss: 1.2649 - val_acc: 0.6177

绘制两个模型的训练和验证准确率。

LeNet-5的训练和验证准确率

添加了归一化和批量归一化的LeNet-5的训练和验证准确率

在使用批量归一化的时候要注意一些问题,一般不建议将批量归一化和dropout一起使用,因为批量归一化有正则化的作用。另外,太小的批处理可能是批归一化的一个问题,因为计算的统计数据(平均值和方差)的质量受到批处理的影响,非常小的批处理可能会导致问题,极端的情况是如果看简单的神经网络,一个样本的所有激活都是0。如果你考虑使用小批处理量,可以考虑使用层规范化(更多资源在下面的进一步阅读部分)。

下面是带有规范化的模型的完整代码。

from tensorflow.keras.layers import Dense, Input, Flatten, Conv2D, BatchNormalization, MaxPool2D, Normalization
from tensorflow.keras.models import Model
import tensorflow as tf
import tensorflow.keras as keras

class LeNet5_Norm(tf.keras.Model):
  def __init__(self, norm_layer, *args, **kwargs):
    super(LeNet5_Norm, self).__init__()
    self.conv1 = Conv2D(filters=6, kernel_size=(5,5), padding="same")
    self.norm1 = norm_layer(*args, **kwargs)
    self.relu = relu
    self.max_pool2x2 = MaxPool2D(pool_size=(2,2))
    self.conv2 = Conv2D(filters=16, kernel_size=(5,5), padding="same")
    self.norm2 = norm_layer(*args, **kwargs)
    self.flatten = Flatten()
    self.fc1 = Dense(units=120)
    self.norm3 = norm_layer(*args, **kwargs)
    self.fc2 = Dense(units=84)
    self.norm4 = norm_layer(*args, **kwargs)
    self.fc3 = Dense(units=10, activation="softmax")
  def call(self, input_tensor):
    conv1 = self.conv1(input_tensor)
    conv1 = self.norm1(conv1)
    conv1 = self.relu(conv1)
    maxpool1 = self.max_pool2x2(conv1)
    conv2 = self.conv2(maxpool1)
    conv2 = self.norm2(conv2)
    conv2 = self.relu(conv2)
    maxpool2 = self.max_pool2x2(conv2)
    flatten = self.flatten(maxpool2)
    fc1 = self.fc1(flatten)
    fc1 = self.norm3(fc1)
    fc1 = self.relu(fc1)
    fc2 = self.fc2(fc1)
    fc2 = self.norm4(fc2)
    fc2 = self.relu(fc2)
    fc3 = self.fc3(fc2)
    return fc3

# load dataset, using cifar10 to show greater improvement in accuracy
(trainX, trainY), (testX, testY) = keras.datasets.cifar10.load_data()

normalization_layer = Normalization()
normalization_layer.adapt(trainX)

input_layer = Input(shape=(32,32,3,))
x = LeNet5_Norm(BatchNormalization)(normalization_layer(input_layer))

model = Model(inputs=input_layer, outputs=x)

model.compile(optimizer="adam", loss=tf.keras.losses.SparseCategoricalCrossentropy(), metrics="acc")

history = model.fit(x=trainX, y=trainY, batch_size=256, epochs=10, validation_data=(testX, testY))

总结

在这篇文章中,你已经发现了规范化和批量规范化是如何工作的,以及如何在TensorFlow中实现它们。你还看到了使用这些层可以帮助显著提高我们的机器学习模型的性能。

具体来说,你已经学会了。

  • 归一化和批量归一化的作用是什么
  • 如何在TensorFlow中使用规范化和批量规范化
  • 在你的机器学习模型中使用批量归一化时的一些提示