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

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


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

A
Aston Zhang 已提交
12
对全连接层和卷积层做批量归一化的方法稍有不同。下面我们将分别介绍这两种情况下的批量归一化。
A
Aston Zhang 已提交
13 14 15

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

A
Aston Zhang 已提交
16
我们先考虑如何对全连接层做批量归一化。通常,我们将批量归一化层置于全连接层中的仿射变换和激活函数之间。设全连接层的输入为$\boldsymbol{u}$,权重参数和偏差参数分别为$\boldsymbol{W}$和$\boldsymbol{b}$,激活函数为$\phi$。设批量归一化的运算符为$\text{BN}$。那么,使用批量归一化的全连接层的输出为
A
Aston Zhang 已提交
17 18 19 20 21 22 23

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

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

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

A
Aston Zhang 已提交
24
得到。考虑一个由$m$个样本组成的小批量,仿射变换的输出为一个新的小批量$\mathcal{B} = \{\boldsymbol{x}^{(1)}, \ldots, \boldsymbol{x}^{(m)} \}$。它们正是批量归一化层的输入。对于小批量$\mathcal{B}$中任意样本$\boldsymbol{x}^{(i)} \in \mathbb{R}^d, 1 \leq  i \leq m$,批量归一化层的输出同样是$d$维向量
A
Aston Zhang 已提交
25

A
Aston Zhang 已提交
26
$$\boldsymbol{y}^{(i)} = \text{BN}(\boldsymbol{x}^{(i)}),$$
A
Aston Zhang 已提交
27

A
Aston Zhang 已提交
28
并由以下几步求得。首先,对小批量$\mathcal{B}$求均值和方差:
A
Aston Zhang 已提交
29 30 31 32

$$\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,$$

A
Aston Zhang 已提交
33
其中的平方计算是按元素求平方。接下来,使用按元素开方和按元素除法对$\boldsymbol{x}^{(i)}$标准化:
A
Aston Zhang 已提交
34 35 36

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

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

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

A
Aston Zhang 已提交
41
至此,我们得到了$\boldsymbol{x}^{(i)}$的批量归一化的输出$\boldsymbol{y}^{(i)}$。
A
Aston Zhang 已提交
42
值得注意的是,可学习的拉伸和偏移参数保留了不对$\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

M
muli 已提交
55
## 从零开始实现
A
Aston Zhang 已提交
56

A
Aston Zhang 已提交
57
下面我们通过`NDArray`来实现批量归一化层。
58

M
muli 已提交
59
```{.python .input  n=72}
A
Aston Zhang 已提交
60
import d2lzh as d2l
A
Aston Zhang 已提交
61
from mxnet import autograd, gluon, init, nd
M
muli 已提交
62
from mxnet.gluon import nn
M
muli 已提交
63

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

A
Aston Zhang 已提交
89
接下来,我们自定义一个`BatchNorm`层。它保存参与求梯度和迭代的拉伸参数`gamma`和偏移参数`beta`,同时也维护移动平均得到的均值和方差,以便能够在模型预测时被使用。`BatchNorm`实例所需指定的`num_features`参数对于全连接层来说应为输出个数,对于卷积层来说则为输出通道数。该实例所需指定的`num_dims`参数对于全连接层和卷积层来说分别为2和4。
M
muli 已提交
90 91 92 93 94

```{.python .input  n=73}
class BatchNorm(nn.Block):
    def __init__(self, num_features, num_dims, **kwargs):
        super(BatchNorm, self).__init__(**kwargs)
M
muli 已提交
95 96 97 98
        if num_dims == 2:
            shape = (1, num_features)
        else:
            shape = (1, num_features, 1, 1)
A
Aston Zhang 已提交
99
        # 参与求梯度和迭代的拉伸和偏移参数,分别初始化成0和1
M
muli 已提交
100
        self.gamma = self.params.get('gamma', shape=shape, init=init.One())
A
Aston Zhang 已提交
101
        self.beta = self.params.get('beta', shape=shape, init=init.Zero())
A
Aston Zhang 已提交
102
        # 不参与求梯度和迭代的变量,全在内存上初始化成0
M
muli 已提交
103
        self.moving_mean = nd.zeros(shape)
A
Aston Zhang 已提交
104 105
        self.moving_var = nd.zeros(shape)

M
muli 已提交
106
    def forward(self, X):
A
Aston Zhang 已提交
107
        # 如果X不在内存上,将moving_mean和moving_var复制到X所在显存上
M
muli 已提交
108 109
        if self.moving_mean.context != X.context:
            self.moving_mean = self.moving_mean.copyto(X.context)
A
Aston Zhang 已提交
110
            self.moving_var = self.moving_var.copyto(X.context)
A
Aston Zhang 已提交
111
        # 保存更新过的moving_mean和moving_var
A
Aston Zhang 已提交
112
        Y, self.moving_mean, self.moving_var = batch_norm(
M
Mu Li 已提交
113
            X, self.gamma.data(), self.beta.data(), self.moving_mean,
A
Aston Zhang 已提交
114
            self.moving_var, eps=1e-5, momentum=0.9)
M
muli 已提交
115
        return Y
M
Mu Li 已提交
116 117
```

A
Aston Zhang 已提交
118
### 使用批量归一化层的LeNet
M
muli 已提交
119

A
Aston Zhang 已提交
120
下面我们修改[“卷积神经网络(LeNet)”](lenet.md)这一节介绍的LeNet模型,从而应用批量归一化层。我们在所有的卷积层或全连接层之后、激活层之前加入批量归一化层。
M
muli 已提交
121 122 123

```{.python .input  n=74}
net = nn.Sequential()
M
muli 已提交
124 125 126 127 128 129 130 131 132 133 134 135 136 137 138
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),
        nn.Activation('sigmoid'),
        nn.Dense(84),
        BatchNorm(84, num_dims=2),
        nn.Activation('sigmoid'),
        nn.Dense(10))
M
Mu Li 已提交
139 140
```

A
Aston Zhang 已提交
141
下面我们训练修改后的模型。
M
Mu Li 已提交
142

M
muli 已提交
143
```{.python .input  n=77}
A
Aston Zhang 已提交
144
lr, num_epochs, batch_size, ctx = 1.0, 5, 256, d2l.try_gpu()
M
muli 已提交
145
net.initialize(ctx=ctx, init=init.Xavier())
M
muli 已提交
146
trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': lr})
A
Aston Zhang 已提交
147
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
A
Aston Zhang 已提交
148 149
d2l.train_ch5(net, train_iter, test_iter, batch_size, trainer, ctx,
              num_epochs)
M
muli 已提交
150
```
M
Mu Li 已提交
151

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

M
muli 已提交
154
```{.python .input  n=60}
A
Aston Zhang 已提交
155 156 157
net[1].gamma.data().reshape((-1,)), net[1].beta.data().reshape((-1,))
```

A
Aston Zhang 已提交
158
## 简洁实现
A
Aston Zhang 已提交
159

A
Aston Zhang 已提交
160
与我们刚刚自己定义的`BatchNorm`类相比,Gluon中`nn`模块定义的`BatchNorm`类使用起来更加简单。它不需要指定自己定义的`BatchNorm`类中所需的`num_features``num_dims`参数值。在Gluon中,这些参数值都将通过延后初始化而自动获取。下面我们用Gluon实现使用批量归一化的LeNet。
A
Aston Zhang 已提交
161 162 163

```{.python .input}
net = nn.Sequential()
M
muli 已提交
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178
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))
A
Aston Zhang 已提交
179 180 181 182 183
```

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

```{.python .input}
M
muli 已提交
184
net.initialize(ctx=ctx, init=init.Xavier())
A
Aston Zhang 已提交
185
trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': lr})
A
Aston Zhang 已提交
186 187
d2l.train_ch5(net, train_iter, test_iter, batch_size, trainer, ctx,
              num_epochs)
M
Mu Li 已提交
188 189
```

A
Aston Zhang 已提交
190
## 小结
M
Mu Li 已提交
191

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

## 练习

A
Aston Zhang 已提交
199 200 201 202 203
* 能否将批量归一化前的全连接层或卷积层中的偏差参数去掉?为什么?(提示:回忆批量归一化中标准化的定义。)
* 尝试调大学习率。同[“卷积神经网络(LeNet)”](lenet.md)一节中未使用批量归一化的LeNet相比,现在是不是可以使用更大的学习率?
* 尝试将批量归一化层插入LeNet的其他地方,观察并分析结果的变化。
* 尝试一下不学习拉伸参数`gamma`和偏移参数`beta`(构造的时候加入参数`grad_req='null'`来避免计算梯度),观察并分析结果。
* 查看`BatchNorm`类的文档来了解更多使用方法,例如,如何在训练时使用基于全局平均的均值和方差。
A
Aston Zhang 已提交
204

S
Sheng Zha 已提交
205

A
Aston Zhang 已提交
206

M
muli 已提交
207 208 209

## 参考文献

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

## 扫码直达[讨论区](https://discuss.gluon.ai/t/topic/1253)

![](../img/qr_batch-norm.svg)