丢弃法——从零开始

除了前两节介绍的权重衰减以外,深度学习模型常常使用丢弃法(dropout)来应对过拟合问题。丢弃法有一些不同的变体。本节中提到的丢弃法特指倒置丢弃法(inverted dropout)。它被广泛使用于深度学习。

方法和原理

为了确保测试模型的确定性,丢弃法的使用只发生在训练模型时,并非测试模型时。当神经网络中的某一层使用丢弃法时,该层的神经元将有一定概率被丢弃掉。设丢弃概率为\(p\)。具体来说,该层任一神经元在应用激活函数后,有\(p\)的概率自乘0,有\(1-p\)的概率自除以\(1-p\)做拉伸。丢弃概率是丢弃法的超参数。

我们在“多层神经网络”一节的图3.3中描述了一个未使用丢弃法的多层感知机。假设其中隐藏单元\(h_i\)\(i=1, \ldots, 5\))的计算表达式为

\[h_i = \phi(x_1 w_1^{(i)} + x_2 w_2^{(i)} + x_3 w_3^{(i)} + x_4 w_4^{(i)} + b^{(i)}),\]

其中\(\phi\)是激活函数,\(x_1, \ldots, x_4\)是输入,\(w_1^{(i)}, \ldots, w_4^{(i)}\)是权重参数,\(b^{(i)}\)是偏差参数。设丢弃概率为\(p\),并设随机变量\(\xi_i\)\(p\)概率为0,有\(1-p\)概率为1。那么,使用丢弃法的隐藏单元\(h_i\)的计算表达式变为

\[h_i = \frac{\xi_i}{1-p} \phi(x_1 w_1^{(i)} + x_2 w_2^{(i)} + x_3 w_3^{(i)} + x_4 w_4^{(i)} + b^{(i)}).\]

注意到测试模型时不使用丢弃法。由于\(\mathbb{E} (\xi_i) = 1-p\),同一神经元在模型训练和测试时的输出值的期望不变。

让我们对图3.3中的隐藏层使用丢弃法,一种可能的结果如图3.5所示。

隐藏层使用了丢弃法的多层感知机

隐藏层使用了丢弃法的多层感知机

以图3.5为例,每次训练迭代时,隐藏层中每个神经元都有可能被丢弃,即\(h_i\)\(i=1, \ldots, 5\))都有可能为0。因此,输出层每个单元的计算,例如\(o_1 = \phi(h_1 w_1^\prime + h_2 w_2^\prime + h_3 w_3^\prime + h_4 w_4^\prime + h_5 w_5^\prime + b^\prime)\),都无法过分依赖\(h_1, \ldots, h_5\)中的任一个。这样通常会造成\(o_1\)表达式中的权重参数\(w_1^\prime, \ldots ,w_5^\prime\)都接近0。因此,丢弃法可以起到正则化的作用,并可以用来应对过拟合。

实现丢弃法

根据丢弃法的定义,我们可以很容易地实现丢弃法。下面的dropout函数将以drop_prob的概率丢弃NDArray输入X中的元素。

In [1]:
import sys
sys.path.append('..')
import gluonbook as gb
from mxnet import autograd, gluon, nd
from mxnet.gluon import loss as gloss

def dropout(X, drop_prob):
    assert 0 <= drop_prob <= 1
    keep_prob = 1 - drop_prob
    # 这种情况下把全部元素都丢弃。
    if keep_prob == 0:
        return X.zeros_like()
    mask = nd.random.uniform(0, 1, X.shape) < keep_prob
    return mask * X / keep_prob

我们运行几个例子来验证一下dropout函数。

In [2]:
X = nd.arange(20).reshape((5, 4))
dropout(X, 0)
Out[2]:

[[  0.   1.   2.   3.]
 [  4.   5.   6.   7.]
 [  8.   9.  10.  11.]
 [ 12.  13.  14.  15.]
 [ 16.  17.  18.  19.]]
<NDArray 5x4 @cpu(0)>
In [3]:
dropout(X, 0.5)
Out[3]:

[[  0.   0.   0.   6.]
 [  0.  10.   0.   0.]
 [ 16.  18.  20.   0.]
 [ 24.  26.   0.   0.]
 [  0.  34.   0.   0.]]
<NDArray 5x4 @cpu(0)>
In [4]:
dropout(X, 1)
Out[4]:

[[ 0.  0.  0.  0.]
 [ 0.  0.  0.  0.]
 [ 0.  0.  0.  0.]
 [ 0.  0.  0.  0.]
 [ 0.  0.  0.  0.]]
<NDArray 5x4 @cpu(0)>

定义模型参数

实验中,我们依然使用“Softmax回归——从零开始”一节中介绍的Fashion-MNIST数据集。我们将定义一个包含两个隐藏层的多层感知机。其中两个隐藏层的输出个数都是256。

In [5]:
num_inputs = 784
num_outputs = 10
num_hiddens1 = 256
num_hiddens2 = 256

W1 = nd.random.normal(scale=0.01, shape=(num_inputs, num_hiddens1))
b1 = nd.zeros(num_hiddens1)
W2 = nd.random.normal(scale=0.01, shape=(num_hiddens1, num_hiddens2))
b2 = nd.zeros(num_hiddens2)
W3 = nd.random.normal(scale=0.01, shape=(num_hiddens2, num_outputs))
b3 = nd.zeros(num_outputs)

params = [W1, b1, W2, b2, W3, b3]
for param in params:
    param.attach_grad()

定义模型

我们的模型就是将全连接层和激活函数ReLU串起来,并对激活函数的输出使用丢弃法。我们可以分别设置各个层的丢弃概率。通常,建议把靠近输入层的丢弃概率设的小一点。在这个实验中,我们把第一个隐藏层的丢弃概率设为0.2,把第二个隐藏层的丢弃概率设为0.5。我们只需在训练模型时使用丢弃法。

In [6]:
drop_prob1 = 0.2
drop_prob2 = 0.5

def net(X):
    X = X.reshape((-1, num_inputs))
    H1 = (nd.dot(X, W1) + b1).relu()
    # 只在训练模型时使用丢弃法。
    if autograd.is_training():
        # 在第一层全连接后添加丢弃层。
        H1 = dropout(H1, drop_prob1)
    H2 = (nd.dot(H1, W2) + b2).relu()
    if autograd.is_training():
        # 在第二层全连接后添加丢弃层。
        H2 = dropout(H2, drop_prob2)
    return nd.dot(H2, W3) + b3

训练和测试模型

这部分和之前多层感知机的训练与测试类似。

In [7]:
num_epochs = 5
lr = 0.5
batch_size = 256
loss = gloss.SoftmaxCrossEntropyLoss()
train_iter, test_iter = gb.load_data_fashion_mnist(batch_size)
gb.train_cpu(net, train_iter, test_iter, loss, num_epochs, batch_size, params,
             lr)
epoch 1, loss 1.2108, train acc 0.535, test acc 0.765
epoch 2, loss 0.5956, train acc 0.780, test acc 0.832
epoch 3, loss 0.5001, train acc 0.817, test acc 0.845
epoch 4, loss 0.4544, train acc 0.834, test acc 0.854
epoch 5, loss 0.4264, train acc 0.844, test acc 0.852

小结

  • 我们可以通过使用丢弃法应对过拟合。
  • 只需在训练模型时使用丢弃法。

练习

  • 尝试不使用丢弃法,看看这个包含两个隐藏层的多层感知机可以得到什么结果。
  • 如果把本节中的两个丢弃概率超参数对调,会有什么结果?

扫码直达讨论区