临时

扫码查看

3.12 权重衰减

3.13 丢弃法

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

3.13.1 方法

 回忆一下,3.8节(多层感知机)的图3.3描述了一个单隐藏层的多层感知机。其中输入个数为4,隐藏单元个数为5,且隐藏单元$h_i (i=1,...,5)$的计算表达式为$$h_i=\phi (x_1 w_{1i}+x_2w_{2i}+x_3 w_{3i}+x_4 w_{4i}+b_i$$,整理$\phi$是激活函数,$x_1,...,x_4$是输入,隐藏单元$i$的权重参数为$w_{1i},...,w_{4i}$,偏差参数为$b_i$。当对该隐藏层使用丢弃法时,该层的隐藏单元将有一定概率被丢弃掉。设丢弃概率为$p$,那么有$p$的概率$h_i$会被清零,有$1-p$的概率$h_i$会除以$1−p$做拉伸。丢弃概率是丢弃法的超参数。具体来说,设随机变量$i$为0和1的概率分别为$p$和$1-p$。使用丢弃法时我们计算新的隐藏单元$h'_i$ \(h'_i = \frac{\xi _i}{1-p} h_i\)由于$E(\xi _i) = 1-p$,因此$$E(h'_i) = \frac{E(\xi _i)}{1-p} h_i = h_i$$​即丢弃法不改变其输入的期望值。让我们对图3.3中的隐藏层使用丢弃法,一种可能的结果如图3.5所示,其中$h_2$和$h_5$被清零。这时输出值的计算不再依赖$h_2$和$h_5$,在反向传播时,与这两个隐藏单元相关的权重的梯度均为0。由于在训练中隐藏层神经元的丢弃是随机的,即$h_1,...,h_5$都有可能被清零,输出层的计算无法过度依赖$h_1,...,h_5$中的任一个,从而在训练模型时起到正则化的作用,并可以用来应对过拟合。在测试模型时,我们为了拿到更加确定性的结果,一般不使用丢弃法。

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

从零开始实现

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

import tensorflow as tf
import numpy as np
from tensorflow import keras, nn, losses
from tensorflow.keras.layers import Dropout, Flatten, Dense
def dropout(x, drop_prob):
    assert 0 <= drop_prob <= 1
    keep_prob = 1 - drop_prob
    if keep_prob == 0:
        return tf.zeros_like(X)
    mask = tf.random.uniform(shape=X.shape, minval=0, maxval=1)<keep_prob #随机生成要drop的项
    return tf.cast(mask, dtype=tf.flaot32)*tf.cast(X,dtype=tf.float32)/keep_prob

我们运行几个例子来测试一下dropout函数。其中丢弃概率分别为0、0.5和1。X = tf.reshape(tf.range(0, 16), shape=(2, 8))dropout(X, 0)dropout(X, 0.5)dropout(X, 1.0)

3.13.2.1 定义模型参数

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

num_inputs, num_outputs, num_hiddens1, num_hiddens2 = 784, 10, 256, 256
W1 = tf.Variable(tf.random.normal(stddev=0.01, shape=(num_inputs, num_hiddens1)))
b1 = tf.Variable(tf.zeros(num_hiddens1))
W2 = tf.Variable(tf.random.normal(stddev=0.1, shape=(num_hiddens1, num_hiddens2)))
b2 = tf.Variable(tf.zeros(num_hiddens2))
W3 = tf.Variable(tf.random.truncated_normal(stddev=0.01, shape=(num_hiddens2, num_outputs)))
b3 = tf.Variable(tf.zeros(num_outputs))
params = [W1, b1, W2, b2, W3, b3]

3.13.2.2 定义模型

 下面定义的模型将全连接层和激活函数ReLU串起来,并对每个激活函数的输出使用丢弃法。我们可以分别设置各个层的丢弃概率。通常的建议是把靠近输入层的丢弃概率设得小一点。在这个实验中,我们把第一个隐藏层的丢弃概率设为0.2,把第二个隐藏层的丢弃概率设为0.5。我们可以通过参数is_training函数来判断运行模式为训练还是测试,并只需在训练模式下使用丢弃法。

drop_prob1, drop_prob2 = 0.2, 0.5
def net(X, is_training=False):
    X = tf.reshape(X, shape=(-1, num_inputs))
    H1 = tf.nn.relu(tf.matmul(X, W1) + b1))
    if is_training:
        H1 = dropout(H1, drop_prob1)
    H2 = nn.relu(tf.matmul(H1, W2) + b2)
    if is_training:
        H2 = dropout(H2, drop_prob2)
    return tf.math.softmax(tf.matmul(H2, W2) + b3)

我们在对模型评估的时候不应该进行丢弃,所以我们修改一下d2lzh_pytorch中的evaluate_accuracy函数:

#本函数已保存在d2lzh_pytorch,读取数据的代码不再重写
def evaluate_accuracy(data_iter, net):
    acc_sum, n = 0.0, 0
    for _, (X, y) in enumerate(data_iter):
        y = tf.cast(y,dtype=tf.int64)
        acc_sum += np.sum(tf.cast(tf.argmax(net(X), axis=1), dtype=tf.int64) == y)
        n += y.shape[0]
    return acc_sum / n

def train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size,
              params=None, lr=None, trainer=None):
    global sample_grads
    for epoch in range(num_epochs):
        train_l_sum, train_acc_sum, n = 0.0, 0.0, 0
        for X, y in train_iter:
            with tf.GradientTape() as tape:
                y_hat = net(X, is_training=True)
                l = tf.reduce_sum(loss(y_hat, tf.one_hot(y, depth=10, axis=-1, dtype=tf.float32)))

            grads = tape.gradient(l, params)
            if trainer is None:

                sample_grads = grads
                params[0].assign_sub(grads[0] * lr)
                params[1].assign_sub(grads[1] * lr)
            else:
                trainer.apply_gradients(zip(grads, params))  # “softmax回归的简洁实现”一节将用到

            y = tf.cast(y, dtype=tf.float32)
            train_l_sum += l.numpy()
            train_acc_sum += tf.reduce_sum(tf.cast(tf.argmax(y_hat, axis=1) == tf.cast(y, dtype=tf.int64), dtype=tf.int64)).numpy()
            n += y.shape[0]
        test_acc = evaluate_accuracy(test_iter, net)
        print('epoch %d, loss %.4f, train acc %.3f, test acc %.3f'
              % (epoch + 1, train_l_sum / n, train_acc_sum / n, test_acc))

注:将上述evaluate_accuracy写回d2lzh_pytorch后要重启一下jupyter kernel才会生效

3.13.2.3 训练和测试模型

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

num_epochs, lr, batch_size = 5, 0.5, 256
loss = tf.losses.CategoricalCrossentropy()
train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size,
              params, lr)

3.13.3 简洁实现

model = keras.Sequential([
    keras.layers.Flatten(input_shape=(28, 28)),
    keras.layers.Dense(256,activation='relu'),
    Dropout(0.2),
    keras.layers.Dense(256,activation='relu'),
    Dropout(0.5),
    keras.layers.Dense(10,activation=tf.nn.softmax)
])

下面训练并测试模型。

model.compile(optimizer=tf.keras.optimizers.Adam(),
              loss = 'sparse_categorical_crossentropy',
              metrics=['accuracy'])
model.fit(x_train,y_train,epochs=5,batch_size=256,validation_data=(x_test, y_test),
                    validation_freq=1)

keras.Sequential定义网络图结构,model.compile定义网络损失函数和优化方法,model.fit模型超参和训练模型

小结

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

3.14 正向传播、反向传播和计算图

 前面几节里我们使用了小批量随机梯度下降的优化算法来训练模型。在实现中,我们只提供了模型的正向传播(forward propagation)的计算,即对输入计算模型输出,然后通过autograd模块来调用系统自动生成的backward函数计算梯度。基于反向传播(back-propagation)算法的自动求梯度极大简化了深度学习模型训练算法的实现。本节我们将使用数学和计算图(computational graph)两个方式来描述正向传播和反向传播。具体来说,我们将以带$L_2$范数正则化的含单隐藏层的多层感知机为样例模型解释正向传播和反向传播。

3.14.1 正向传播

3.14.2 正向传播的计算图

3.14.3 反向传播

3.14.4 训练深度学习模型

  在训练深度学习模型时,正向传播和反向传播之间相互依赖。下面我们仍然以本节中的样例模型分别阐述它们之间的依赖关系。一方面,正向传播的计算可能依赖于模型参数的当前值,而这些模型参数是在反向传播的梯度计算后通过优化算法迭代的。例如,计算正则化项$s=(\lambda /2) (||W^{(1)}||_F{(2)}||_F{(1)}$和$W^{(2)}\(的当前值,而这些当前值是优化算法最近一次根据反向传播算出梯度后迭代得到的。另一方面,反向传播的梯度计算可能依赖于各变量的当前值,而这些变量的当前值是通过正向传播计算得到的。举例来说,参数梯度\)\partial J / \partial W^{(2)} = (\partial J/ \partial o) h{(2)}$(2)的计算需要依赖隐藏层变量的当前值$h$。这个当前值是通过从输入层到输出层的正向传播计算并存储得到的。

因此,在模型参数初始化完成后,我们交替地进行正向传播和反向传播,并根据反向传播计算的梯度迭代模型参数。既然我们在反向传播中使用了正向传播中计算得到的中间变量来避免重复计算,那么这个复用也导致正向传播结束后不能立即释放中间变量内存。这也是训练要比预测占用更多内存的一个重要原因。另外需要指出的是,这些中间变量的个数大体上与网络层数线性相关,每个变量的大小跟批量大小和输入个数也是线性相关的,它们是导致较深的神经网络使用较大批量训练时更容易超内存的主要原因。

小结

 正向传播沿着从输入层到输出层的顺序,依次计算并存储神经网络的中间变量。 反向传播沿着从输出层到输入层的顺序,依次计算并存储神经网络中间变量和参数的梯度。 在训练深度学习模型时,正向传播和反向传播相互依赖。

3.15 数值稳定性和模型初始化

 理解了正向传播与反向传播以后,我们来讨论一下深度学习模型的数值稳定性问题以及模型参数的初始化方法。深度模型有关数值稳定性的典型问题是衰减(vanishing)和爆炸(explosion)。

3.15.1 衰减和爆炸

随着内容的不断深入,我们会在后面的章节进一步介绍深度学习的数值稳定性问题以及解决方法。

3.15.2 随机初始化模型参数

在神经网络中,通常需要随机初始化模型参数。下面我们来解释这样做的原因。

回顾3.8节(多层感知机)图3.3描述的多层感知机。为了方便解释,假设输出层只保留一个输出单元$o_1$(删除$o_2$和$o_3$以及指向它们的箭头),且隐藏层使用相同的激活函数。如果将每个隐藏单元的参数都初始化为相等的值,那么在正向传播时每个隐藏单元将根据相同的输入计算出相同的值,并传递至输出层。在反向传播中,每个隐藏单元的参数梯度值相等。因此,这些参数在使用基于梯度的优化算法迭代后值依然相等。之后的迭代也是如此。在这种情况下,无论隐藏单元有多少,隐藏层本质上只有1个隐藏单元在发挥作用。因此,正如在前面的实验中所做的那样,我们通常将神经网络的模型参数,特别是权重参数,进行随机初始化。

3.15.2.1 Tensorflow 2.0的默认随机初始化

  随机初始化模型参数的方法有很多。在3.3节(线性回归的简洁实现)中,我们使用kernel_initializer=init.RandomNormal(stddev=0.01)使模型model的权重参数采用正态分布的随机初始化方式。不过,Tensorflow中initializers的模块参数都采取了较为合理的初始化策略(不同类型的layer具体采样的哪一种初始化方法的可参考源代码),因此一般不用我们考虑。

3.15.2.2 Xavier随机初始化

 还有一种比较常用的随机初始化方法叫作Xavier随机初始化[1]。 假设某全连接层的输入个数为$a$,输出个数为$b$,Xavier随机初始化将使该层中权重参数的每个元素都随机采样于均匀分布$$U(-\sqrt {\frac{6}{a+b}}, \sqrt {\frac{6}{a+b}})$$。它的设计主要考虑到,模型参数初始化后,每层输出的方差不该受该层输入个数影响,且每层梯度的方差也不该受该层输出个数影响。

小结

 深度模型有关数值稳定性的典型问题是衰减和爆炸。当神经网络的层数较多时,模型的数值稳定性容易变差。 我们通常需要随机初始化神经网络的模型参数,如权重参数。

3.16 实战Kaggle比赛:房价预测

12-26 21:01
查看更多