batch-norm.md 12.0 KB
Newer Older
A
rename  
Aston Zhang 已提交
1
# 批量归一化
M
Mu Li 已提交
2

A
Aston Zhang 已提交
3
这一节我们介绍批量归一化(batch normalization)层,它能让较深的神经网络的训练变得更加容易 [1]。在 [“实战Kaggle比赛:预测房价”](../chapter_deep-learning-basics/kaggle-house-price.md) 小节里,我们对输入数据做了标准化处理:处理后的任意一个特征在数据集中所有样本上的均值为0、标准差为1。标准化处理输入数据使各个特征的分布相近:这往往更容易训练出有效的模型。
M
Mu Li 已提交
4

A
Aston Zhang 已提交
5 6
通常来说,数据标准化预处理对于浅层模型就足够有效了。随着模型训练的进行,当每层中参数更新时,靠近输出层的输出较难出现剧烈变化。但对于深层神经网络来说,即使输入数据已做标准化,训练中模型参数的更新依然很容易造成靠近输出层输出的剧烈变化。这种计算数值的不稳定性通常令我们难以训练出有效的深度模型。

A
Aston Zhang 已提交
7
批量归一化的提出正是为了应对深度模型训练的挑战。在模型训练时,批量归一化利用小批量上的均值和标准差,不断调整神经网络中间输出,从而使得整个神经网络在各层的中间输出的数值更稳定。批量归一化和下一节将要介绍的残差网络为训练和设计深度模型提供了两类重要思路。
8 9


M
muli 已提交
10
## 批量归一化层
M
Mu Li 已提交
11

A
Aston Zhang 已提交
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
对全连接层和卷积层做批量归一化的方法稍有不同。下面我们将分别介绍。

### 对全连接层做批量归一化

我们先考虑如何对全连接层做批量归一化。通常,我们将批量归一化层置于全连接层中的仿射变换和激活函数之间。设全连接层的输入为$\boldsymbol{u}$,权重参数和偏差参数分别为$\boldsymbol{W}$和$\boldsymbol{b}$,激活函数为$\phi$。设批量归一化的操作符为$\text{BN}$。那么,使用批量归一化的全连接层的输出为

$$\phi(\text{BN}(\boldsymbol{x})),$$

其中批量归一化输入$\boldsymbol{x}$由仿射变换

$$\boldsymbol{x} = \boldsymbol{W\boldsymbol{u} + \boldsymbol{b}}$$

得到。考虑一个由$m$个样本组成的小批量,仿射变换的输出为一个新的小批量$\mathcal{B} = \{\boldsymbol{x}^{(1)}, \ldots, \boldsymbol{x}^{(m)} \}$。它们正是批量归一化层的输入。对于小批量$\mathcal{B}$中任意样本$\boldsymbol{x}^{(i)}, 1 \leq  i \leq m$,批量归一化层的输出

$$\boldsymbol{y}^{(i)} = \text{BN}(\boldsymbol{x}^{(i)})$$

由以下几步求得。首先,对小批量$\mathcal{B}$求均值和方差:

$$\boldsymbol{\mu}_\mathcal{B} \leftarrow \frac{1}{m}\sum_{i = 1}^{m} \boldsymbol{x}^{(i)},$$
$$\boldsymbol{\sigma}_\mathcal{B}^2 \leftarrow \frac{1}{m} \sum_{i=1}^{m}(\boldsymbol{x}^{(i)} - \boldsymbol{\mu}_\mathcal{B})^2,$$

其中的平方计算是按元素求平方。接下来,我们使用按元素开方和按元素除法对$\boldsymbol{x}^{(i)}$标准化:

$$\hat{\boldsymbol{x}}^{(i)} \leftarrow \frac{\boldsymbol{x}^{(i)} - \boldsymbol{\mu}_\mathcal{B}}{\sqrt{\boldsymbol{\sigma}_\mathcal{B}^2 + \epsilon}},$$

这里$\epsilon > 0$是一个很小的常数,保证分母大于0。在上面标准化的基础上,批量归一化层引入了两个可以学习的模型参数,拉升(scale)参数 $\boldsymbol{\gamma}$ 和偏移(shift)参数 $\boldsymbol{\beta}$。这两个参数和$\boldsymbol{x}^{(i)}$形状相同,并与$\boldsymbol{x}^{(i)}$分别做按元素乘法(符号$\odot$)和加法计算:
38

A
Aston Zhang 已提交
39
$$\hat{\boldsymbol{y}}^{(i)} \leftarrow \boldsymbol{\gamma} \odot \hat{\boldsymbol{x}}^{(i)} + \boldsymbol{\beta}.$$
40

A
Aston Zhang 已提交
41 42
至此,我们得到了$\boldsymbol{x}^{(i)}$的批量归一化的输出$\boldsymbol{y}^{(i)}$。
值得注意的是,可学习的拉升和偏移参数保留了不对$\hat{\boldsymbol{x}}^{(i)}$做批量归一化的可能:此时只需学出$\boldsymbol{\gamma} = \sqrt{\boldsymbol{\sigma}_\mathcal{B}^2 + \epsilon}$和$\boldsymbol{\beta} = \boldsymbol{\mu}_\mathcal{B}$。我们可以对此这样理解:如果批量归一化无益,理论上学出的模型可以不使用批量归一化。
43

M
update  
Mu Li 已提交
44

A
Aston Zhang 已提交
45
### 对卷积层做批量归一化
M
Mu Li 已提交
46

A
Aston Zhang 已提交
47
对卷积层来说,批量归一化发生在卷积计算之后、应用激活函数之前。如果卷积计算输出多个通道,我们需要对这些通道的输出分别做批量归一化,且每个通道都拥有独立的拉升和偏移参数。设小批量中有$m$个样本。在单个通道上,假设卷积计算输出的高和宽分别为$p$和$q$。我们需要对该通道中$m \times p \times q$个元素同时做批量归一化。对这些元素做标准化计算时,我们使用相同的均值和方差,即该通道中$m \times p \times q$个元素的均值和方差。
48

M
Mu Li 已提交
49

A
Aston Zhang 已提交
50
### 预测时的批量归一化
51

A
Aston Zhang 已提交
52
使用批量归一化训练时,我们可以将批量大小设的大一点,从而使批量内样本的均值和方差的计算都较为准确。给定训练好的模型用来预测时,我们希望模型对于任意输入都有确定的输出。因此,单个样本的输出不应取决于批量归一化所需要的随机小批量中的均值和方差。一种常用的方法是通过移动平均估算整个训练数据集的样本均值和方差,并在预测时使用它们得到确定的输出。
M
Mu Li 已提交
53

A
Aston Zhang 已提交
54 55 56 57 58 59
可见,和丢弃层一样,批量归一化层在训练模式和预测模式下的计算结果也是不一样的。


## 批量归一化层的实现

下面我们通过NDArray来实现批量归一化层。
60

M
muli 已提交
61
```{.python .input  n=72}
62 63 64
import sys
sys.path.insert(0, '..')

65
import gluonbook as gb
M
muli 已提交
66
from mxnet import nd, gluon, init, autograd
A
Aston Zhang 已提交
67
from mxnet.gluon import loss as gloss, nn
M
muli 已提交
68

A
Aston Zhang 已提交
69
def batch_norm(X, gamma, beta, moving_mean, moving_var, eps, momentum):
A
Aston Zhang 已提交
70
    # 通过 autograd 来获取是不是在训练模式下。
M
muli 已提交
71 72 73
    if not autograd.is_training():
        # 如果是在预测模式下,直接使用传入的移动平滑均值和方差。
        X_hat = (X - moving_mean) / nd.sqrt(moving_var + eps)
M
Mu Li 已提交
74
    else:
M
muli 已提交
75
        assert len(X.shape) in (2, 4)
A
Aston Zhang 已提交
76
        # 使用全连接层的情况,计算特征维上的均值和方差。
M
muli 已提交
77 78
        if len(X.shape) == 2:
            mean = X.mean(axis=0)
A
Aston Zhang 已提交
79
            var = ((X - mean) ** 2).mean(axis=0)
A
Aston Zhang 已提交
80
        # 使用二维卷积层的情况,计算通道维上(axis=1)的均值和方差。这里我们需要保持 X
M
muli 已提交
81 82
        # 的形状以便后面可以正常的做广播运算。
        else:
A
Aston Zhang 已提交
83 84
            mean = X.mean(axis=(0, 2, 3), keepdims=True)
            var = ((X - mean) ** 2).mean(axis=(0, 2, 3), keepdims=True)
A
Aston Zhang 已提交
85
        # 训练模式下用当前的均值和方差做标准化。
M
muli 已提交
86 87 88 89
        X_hat = (X - mean) / nd.sqrt(var + eps)
        # 更新移动平滑均值和方差。
        moving_mean = momentum * moving_mean + (1.0 - momentum) * mean
        moving_var = momentum * moving_var + (1.0 - momentum) * var
A
Aston Zhang 已提交
90
    # 拉升和偏移。
M
muli 已提交
91
    Y = gamma * X_hat + beta
A
Aston Zhang 已提交
92
    return Y, moving_mean, moving_var
M
Mu Li 已提交
93 94
```

A
Aston Zhang 已提交
95
接下来我们自定义一个BatchNorm层。它保存参与求导和更新的模型参数`beta``gamma`,同时也维护移动平均得到的均值和方差,以能够在模型预测时使用。
M
muli 已提交
96 97 98 99 100

```{.python .input  n=73}
class BatchNorm(nn.Block):
    def __init__(self, num_features, num_dims, **kwargs):
        super(BatchNorm, self).__init__(**kwargs)
A
Aston Zhang 已提交
101 102
        shape = (1, num_features) if num_dims == 2 else (1, num_features, 1,
                                                         1)
M
muli 已提交
103 104
        # 参与求导和更新的模型参数,分别初始化成 0 和 1。
        self.gamma = self.params.get('gamma', shape=shape, init=init.One())
A
Aston Zhang 已提交
105
        self.beta = self.params.get('beta', shape=shape, init=init.Zero())
M
muli 已提交
106 107
        # 不参与求导的模型参数。全在 CPU 上初始化成 0。
        self.moving_mean = nd.zeros(shape)
A
Aston Zhang 已提交
108 109
        self.moving_var = nd.zeros(shape)

M
muli 已提交
110 111 112 113
    def forward(self, X):
        # 如果 X 不在 CPU 上,将 moving_mean 和 moving_varience 复制到对应设备上。
        if self.moving_mean.context != X.context:
            self.moving_mean = self.moving_mean.copyto(X.context)
A
Aston Zhang 已提交
114
            self.moving_var = self.moving_var.copyto(X.context)
M
muli 已提交
115
        # 保存更新过的 moving_mean 和 moving_var。
A
Aston Zhang 已提交
116
        Y, self.moving_mean, self.moving_var = batch_norm(
M
Mu Li 已提交
117
            X, self.gamma.data(), self.beta.data(), self.moving_mean,
A
Aston Zhang 已提交
118
            self.moving_var, eps=1e-5, momentum=0.9)
M
muli 已提交
119
        return Y
M
Mu Li 已提交
120 121
```

M
muli 已提交
122 123
## 使用批量归一化层的LeNet

A
Aston Zhang 已提交
124
下面我们修改[“卷积神经网络”](lenet.md)这一节介绍的LeNet来使用批量归一化层。我们在所有的卷积层和全连接层之后、激活层之前加入批量归一化层。
M
muli 已提交
125 126 127 128 129 130 131 132 133 134 135 136 137 138

```{.python .input  n=74}
net = nn.Sequential()
net.add(
    nn.Conv2D(6, kernel_size=5),
    BatchNorm(6, num_dims=4),
    nn.Activation('sigmoid'),
    nn.MaxPool2D(pool_size=2, strides=2),
    nn.Conv2D(16, kernel_size=5),
    BatchNorm(16, num_dims=4),
    nn.Activation('sigmoid'),
    nn.MaxPool2D(pool_size=2, strides=2),
    nn.Dense(120),
    BatchNorm(120, num_dims=2),
M
Mu Li 已提交
139
    nn.Activation('sigmoid'),
M
muli 已提交
140 141 142 143 144
    nn.Dense(84),
    BatchNorm(84, num_dims=2),
    nn.Activation('sigmoid'),
    nn.Dense(10)
)
M
Mu Li 已提交
145 146
```

A
Aston Zhang 已提交
147
下面我们训练模型。
M
Mu Li 已提交
148

M
muli 已提交
149
```{.python .input  n=77}
M
muli 已提交
150
lr = 1.0
A
Aston Zhang 已提交
151 152
num_epochs = 5
batch_size = 256
M
muli 已提交
153 154
ctx = gb.try_gpu()
net.initialize(force_reinit=True, ctx=ctx, init=init.Xavier())
M
muli 已提交
155
trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': lr})
A
Aston Zhang 已提交
156
loss = gloss.SoftmaxCrossEntropyLoss()
A
Aston Zhang 已提交
157 158 159
train_iter, test_iter = gb.load_data_fashion_mnist(batch_size)
gb.train_ch5(net, train_iter, test_iter, loss, batch_size, trainer, ctx,
             num_epochs)
M
muli 已提交
160
```
M
Mu Li 已提交
161

A
Aston Zhang 已提交
162
最后我们查看下第一个批量归一化层学习到的拉升参数`gamma`和偏移参数`beta`
M
Mu Li 已提交
163

M
muli 已提交
164
```{.python .input  n=60}
A
Aston Zhang 已提交
165 166 167
net[1].gamma.data().reshape((-1,)), net[1].beta.data().reshape((-1,))
```

A
Aston Zhang 已提交
168
## 批量归一化的Gluon实现
A
Aston Zhang 已提交
169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205

相比于我们刚刚自己定义的`BatchNorm`类,`nn`模块定义的`BatchNorm`类使用更加简单。它不需要指定输出数据的维度和特征维的大小,这些都将通过延后初始化来获取。下面我们用Gluon实现批量归一化的LeNet。

```{.python .input}
net = nn.Sequential()
net.add(
    nn.Conv2D(6, kernel_size=5),
    nn.BatchNorm(),
    nn.Activation('sigmoid'),
    nn.MaxPool2D(pool_size=2, strides=2),
    nn.Conv2D(16, kernel_size=5),
    nn.BatchNorm(),
    nn.Activation('sigmoid'),
    nn.MaxPool2D(pool_size=2, strides=2),
    nn.Dense(120),
    nn.BatchNorm(),
    nn.Activation('sigmoid'),
    nn.Dense(84),
    nn.BatchNorm(),
    nn.Activation('sigmoid'),
    nn.Dense(10)
)
```

使用同样的超参数进行训练。

```{.python .input}
lr = 1.0
num_epochs = 5
batch_size = 256
ctx = gb.try_gpu()
net.initialize(force_reinit=True, ctx=ctx, init=init.Xavier())
trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': lr})
loss = gloss.SoftmaxCrossEntropyLoss()
train_iter, test_iter = gb.load_data_fashion_mnist(batch_size)
gb.train_ch5(net, train_iter, test_iter, loss, batch_size, trainer, ctx,
             num_epochs)
M
Mu Li 已提交
206 207
```

A
Aston Zhang 已提交
208
## 小结
M
Mu Li 已提交
209

A
Aston Zhang 已提交
210 211
* 在模型训练时,批量归一化利用小批量上的均值和标准差,不断调整神经网络中间输出,从而使得整个神经网络在各层的中间输出的数值更稳定。
* 对全连接层和卷积层做批量归一化的方法稍有不同。
212
* 批量归一化层和丢弃层一样,在训练模式和预测模式的计算结果是不一样的。
A
Aston Zhang 已提交
213
* Gluon提供的BatchNorm在使用上更加简单。
M
Mu Li 已提交
214 215 216

## 练习

A
Aston Zhang 已提交
217 218 219 220 221 222
* 我们能否将批量归一化前的全连接仿射变换或卷积计算中的偏差参数去掉?为什么?
* 尝试调大学习率。跟前面的LeNet比,是不是可以使用更大的学习率?
* 尝试将批量归一化层插入到LeNet的其他地方,观察并分析结果的变化。
* 尝试下不学习`beta``gamma`(构造的时候加入这个参数`grad_req='null'`来避免计算梯度),观察并分析结果。
* 查看`BatchNorm`文档来了解更多使用方法,例如如何在训练时使用全局平均的均值和方差。

S
Sheng Zha 已提交
223

A
Aston Zhang 已提交
224 225
## 扫码直达[讨论区](https://discuss.gluon.ai/t/topic/1253)

A
Aston Zhang 已提交
226
![](../img/qr_batch-norm.svg)
M
muli 已提交
227 228 229

## 参考文献

A
Aston Zhang 已提交
230
[1] Ioffe, S., & Szegedy, C. (2015). Batch normalization: Accelerating deep network training by reducing internal covariate shift. arXiv preprint arXiv:1502.03167.