首页 > 技术知识 > 正文

【深度学习】Dropout、正反向传播、计算图等的介绍和实现(Pytorch)

【深度学习】Dropout、正反向传播、计算图等的介绍和实现(Pytorch)

文章目录 1 Dropout概述 2 实践中的dropout 2.1 从零开始实现 2.2 定义模型参数 2.3 定义模型 2.4 训练和测试 2.5 简洁代码 3 正向传播、反向传播和计算图 3.1 正向传播计算图 3.2 反向传播 4 总结 5 代码解析 1 Dropout概述

当面对更多的特征而样本不足时,线性模型往往会过度拟合。当给出更多样本而不是特征,我们通常可以指望线性模型不会过拟合。不幸的是,线性模型泛化的可靠性是有代价的。简单地说,线性模型没有考虑到特征之间的交互作用。对于每个特征,线性模型必须指定正的或负的权重,而忽略上下文。

在传统说法中,泛化性和灵活性之间的这种基本权衡被描述为偏差-方差权衡。线性模型有很高的偏差:它们只能表示一小类函数。然而,这些模型的方差很低:它们在不同的随机数据样本上给出了相似的结果。

深度神经网络位于偏差-方差谱的另一端。与线性模型不同,神经网络并不局限于单独查看每个特征。它们可以学习特征之间的交互。例如,它们可能推断“尼日利亚”和“西联汇款”一起出现在电子邮件中表示垃圾邮件,但单独出现则不表示垃圾邮件。

即使我们有比特征多得多的样本,深度神经网络也有可能过拟合。2017年,一组研究人员通过在随机标记的图像上训练深度网络。这展示了神经网络的极大灵活性。因为没有任何真实的模式将输入和输出联系起来,但他们发现,通过随机梯度下降优化的神经网络可以完美地标记训练集中的每一幅图像。想一想这意味着什么。如果标签是随机均匀分配的,并且有10个类别,那么在保留数据上没有分类器会取得高于10%的准确率。这里的泛化差距高达90%。如果我们的模型具有这么强的表达能力,以至于它们可以如此严重地过拟合,那么我们指望在什么时候它们不会过拟合呢?

深度网络有着令人费解的泛化性质,而这种泛化性质的数学基础仍然是悬而未决的研究问题,我们鼓励面向理论的读者更深入地研究这个主题。目前,我们转向对实际工具的探究,这些工具倾向于经验上改进深层网络的泛化性。

2 实践中的dropout

通常,我们在测试时禁用dropout。给定一个训练好的模型和一个新的样本,我们不会丢弃任何节点,因此不需要标准化。然而,也有一些例外:一些研究人员使用测试时的dropout作为估计神经网络预测的“不确定性”的启发式方法:如果预测在许多不同的dropout掩码上都是一致的,那么我们可以说网络更有自信心。

可以把dropout理解成删除隐藏层的节点和点。

2.1 从零开始实现

要实现单层的dropout函数,我们必须从伯努利(二元)随机变量中提取与我们的层的维度一样多的样本,其中随机变量以概率 1−𝑝 取值 1 (保持),以概率 𝑝 取值 0 (丢弃)。实现这一点的一种简单方式是首先从均匀分布 𝑈[0,1] 中抽取样本。那么我们可以保留那些对应样本大于 𝑝 的节点,把剩下的丢弃。

在下面的代码中,(我们实现 dropout_layer 函数,该函数以dropout的概率丢弃张量输入X中的元素),如上所述重新缩放剩余部分:将剩余部分除以1.0-dropout。 import torch from torch import nn from d2l import torch as d2l def dropout_layer(X, dropout): assert 0 <= dropout <= 1 # 在本情况中,所有元素都被丢弃。 if dropout == 1: return torch.zeros_like(X) # 在本情况中,所有元素都被保留。 if dropout == 0: return X mask = (torch.Tensor(X.shape).uniform_(0, 1) > dropout).float() return mask * X / (1.0 – dropout)

我们可以通过几个例子来[测试dropout_layer函数]。在下面的代码行中,我们将输入X通过dropout操作,丢弃概率分别为0、0.5和1。

X= torch.arange(16, dtype = torch.float32).reshape((2, 8)) print(X) print(dropout_layer(X, 0.)) print(dropout_layer(X, 0.5)) print(dropout_layer(X, 1.))

tensor([[ 0., 1., 2., 3., 4., 5., 6., 7.],

[ 8., 9., 10., 11., 12., 13., 14., 15.]])

tensor([[ 0., 1., 2., 3., 4., 5., 6., 7.],

[ 8., 9., 10., 11., 12., 13., 14., 15.]])

tensor([[ 0., 0., 0., 0., 8., 10., 0., 14.],

[16., 18., 0., 0., 24., 26., 28., 30.]])

tensor([[0., 0., 0., 0., 0., 0., 0., 0.],

[0., 0., 0., 0., 0., 0., 0., 0.]])

2.2 定义模型参数

同样,我们使用 :numref:sec_fashion_mnist 中引入的Fashion-MNIST数据集。我们[定义具有两个隐藏层的多层感知机,每个隐藏层包含256个单元]。

num_inputs, num_outputs, num_hiddens1, num_hiddens2 = 784, 10, 256, 256

2.3 定义模型

下面的模型将dropout应用于每个隐藏层的输出(在激活函数之后)。我们可以分别为每一层设置丢弃概率。

一种常见的技巧是在靠近输入层的地方设置较低的丢弃概率。

下面,我们将第一个和第二个隐藏层的丢弃概率分别设置为0.2和0.5。我们确保dropout只在训练期间有效。 dropout1, dropout2 = 0.2, 0.5 class Net(nn.Module): def __init__(self, num_inputs, num_outputs, num_hiddens1, num_hiddens2, is_training = True): super(Net, self).__init__() self.num_inputs = num_inputs self.training = is_training self.lin1 = nn.Linear(num_inputs, num_hiddens1) self.lin2 = nn.Linear(num_hiddens1, num_hiddens2) self.lin3 = nn.Linear(num_hiddens2, num_outputs) self.relu = nn.ReLU() def forward(self, X): H1 = self.relu(self.lin1(X.reshape((-1, self.num_inputs)))) # 只有在训练模型时才使用dropout if self.training == True: # 在第一个全连接层之后添加一个dropout层 H1 = dropout_layer(H1, dropout1) H2 = self.relu(self.lin2(H1)) if self.training == True: # 在第二个全连接层之后添加一个dropout层 H2 = dropout_layer(H2, dropout2) out = self.lin3(H2) return out net = Net(num_inputs, num_outputs, num_hiddens1, num_hiddens2)
<

2.4 训练和测试

num_epochs, lr, batch_size = 10, 0.5, 256 loss = nn.CrossEntropyLoss() train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size) trainer = torch.optim.SGD(net.parameters(), lr=lr) d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)

2.5 简洁代码

对于高级API,我们所需要做的就是在每个全连接层之后添加一个Dropout层,将丢弃概率作为唯一的参数传递给它的构造函数。在训练过程中,Dropout层将根据指定的丢弃概率随机丢弃上一层的输出(相当于下一层的输入)。当不处于训练模式时,Dropout层仅在测试时传递数据。

net = nn.Sequential(nn.Flatten(), nn.Linear(784, 256), nn.ReLU(), # 在第一个全连接层之后添加一个dropout层 nn.Dropout(dropout1), nn.Linear(256, 256), nn.ReLU(), # 在第二个全连接层之后添加一个dropout层 nn.Dropout(dropout2), nn.Linear(256, 10)) def init_weights(m): if type(m) == nn.Linear: nn.init.normal_(m.weight, std=0.01) net.apply(init_weights); trainer = torch.optim.SGD(net.parameters(), lr=lr) d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer) 3 正向传播、反向传播和计算图

到目前为止,我们已经用小批量随机梯度下降训练了我们的模型。然而,当我们实现该算法时,我们只考虑了通过模型 正向传播(forward propagation)所涉及的计算。在计算梯度时,我们只调用了深度学习框架提供的反向传播函数。

梯度的自动计算(自动微分)大大简化了深度学习算法的实现。在自动微分之前,即使是对复杂模型的微小调整也需要手工重新计算复杂的导数。学术论文也不得不分配大量页面来推导更新规则。我们必须继续依赖于自动微分,这样我们就可以专注于有趣的部分,但是如果你想超过对深度学习的浅薄理解,你应当知道这些梯度是如何计算出来的。

在本节中,我们将深入探讨 反向传播(backward propagation 或 backpropagation)的细节。为了传达对这些技术及其实现的一些见解,我们依赖一些基本的数学和计算图。

3.1 正向传播计算图

绘制计算图有助于我们可视化计算中操作符和变量的依赖关系。上述简单网络相对应的计算图,其中正方形表示变量,圆圈表示操作符。左下角表示输入,右上角表示输出。注意显示数据流的箭头方向主要是向右和向上的。

3.2 反向传播

反向传播指的是计算神经网络参数梯度的方法。简言之,该方法根据微积分中的链式规则,按相反的顺序从输出层到输入层遍历网络。该算法存储了计算某些参数梯度时所需的任何中间变量(偏导数)。假设我们有函数$\mathsf{Y}=f(\mathsf{X})$和$\mathsf{Z}=g(\mathsf{Y})$,其中输入和输出$\mathsf{X}, \mathsf{Y}, \mathsf{Z}$是任意形状的张量。利用链式法则,我们可以计算$\mathsf{Z}$关于$\mathsf{X}$的导数

$$\frac{\partial \mathsf{Z}}{\partial \mathsf{X}} = \text{prod}\left(\frac{\partial \mathsf{Z}}{\partial \mathsf{Y}}, \frac{\partial \mathsf{Y}}{\partial \mathsf{X}}\right).$$

在这里,我们使用$\text{prod}$运算符在执行必要的操作(如换位和交换输入位置)后将其参数相乘。对于向量,这很简单:它只是矩阵-矩阵乘法。对于高维张量,我们使用适当的对应项。运算符$\text{prod}$指代了所有的这些符号。

回想一下,在计算图 中的单隐藏层简单网络的参数是$\mathbf{W}^{(1)}$和$\mathbf{W}^{(2)}$。反向传播的目的是计算梯度$\partial J/\partial \mathbf{W}^{(1)}$和$\partial J/\partial \mathbf{W}^{(2)}$。为此,我们应用链式法则,依次计算每个中间变量和参数的梯度。计算的顺序与正向传播中执行的顺序相反,因为我们需要从计算图的结果开始,并朝着参数的方向努力。第一步是计算目标函数$J=L+s$相对于损失项$L$和正则项$s$的梯度。

$$\frac{\partial J}{\partial L} = 1 \; \text{and} \; \frac{\partial J}{\partial s} = 1.$$

接下来,我们根据链式法则计算目标函数关于输出层变量$\mathbf{o}$的梯度:

$$

\frac{\partial J}{\partial \mathbf{o}}

= \text{prod}\left(\frac{\partial J}{\partial L}, \frac{\partial L}{\partial \mathbf{o}}\right)

= \frac{\partial L}{\partial \mathbf{o}}

\in \mathbb{R}^q.

$$

接下来,我们计算正则化项相对于两个参数的梯度:

$$\frac{\partial s}{\partial \mathbf{W}^{(1)}} = \lambda \mathbf{W}^{(1)}

\; \text{and} \;

\frac{\partial s}{\partial \mathbf{W}^{(2)}} = \lambda \mathbf{W}^{(2)}.$$

现在我们可以计算最接近输出层的模型参数的梯度$\partial J/\partial \mathbf{W}^{(2)} \in \mathbb{R}^{q \times h}$。使用链式法则得出:

$$\frac{\partial J}{\partial \mathbf{W}^{(2)}}= \text{prod}\left(\frac{\partial J}{\partial \mathbf{o}}, \frac{\partial \mathbf{o}}{\partial \mathbf{W}^{(2)}}\right) + \text{prod}\left(\frac{\partial J}{\partial s}, \frac{\partial s}{\partial \mathbf{W}^{(2)}}\right)= \frac{\partial J}{\partial \mathbf{o}} \mathbf{h}^\top + \lambda \mathbf{W}^{(2)}.$$

为了获得关于$\mathbf{W}^{(1)}$的梯度,我们需要继续沿着输出层到隐藏层反向传播。关于隐藏层输出的梯度$\partial J/\partial \mathbf{h} \in \mathbb{R}^h$由下式给出:

$$

\frac{\partial J}{\partial \mathbf{h}}

= \text{prod}\left(\frac{\partial J}{\partial \mathbf{o}}, \frac{\partial \mathbf{o}}{\partial \mathbf{h}}\right)

= {\mathbf{W}^{(2)}}^\top \frac{\partial J}{\partial \mathbf{o}}.

$$

由于激活函数$\phi$是按元素计算的,计算中间变量$\mathbf{z}$的梯度$\partial J/\partial \mathbf{z} \in \mathbb{R}^h$需要使用按元素乘法运算符,我们用$\odot$表示:

$$

\frac{\partial J}{\partial \mathbf{z}}

= \text{prod}\left(\frac{\partial J}{\partial \mathbf{h}}, \frac{\partial \mathbf{h}}{\partial \mathbf{z}}\right)

= \frac{\partial J}{\partial \mathbf{h}} \odot \phi\left(\mathbf{z}\right).

$$

最后,我们可以得到最接近输入层的模型参数的梯度$\partial J/\partial \mathbf{W}^{(1)} \in \mathbb{R}^{h \times d}$。根据链式法则,我们得到:

$$

\frac{\partial J}{\partial \mathbf{W}^{(1)}}

= \text{prod}\left(\frac{\partial J}{\partial \mathbf{z}}, \frac{\partial \mathbf{z}}{\partial \mathbf{W}^{(1)}}\right) + \text{prod}\left(\frac{\partial J}{\partial s}, \frac{\partial s}{\partial \mathbf{W}^{(1)}}\right)

= \frac{\partial J}{\partial \mathbf{z}} \mathbf{x}^\top + \lambda \mathbf{W}^{(1)}.

$$ 4 总结 正向传播在神经网络定义的计算图中按顺序计算和存储中间变量。它的顺序是从输入层到输出层。 反向传播按相反的顺序计算和存储神经网络的中间变量和参数的梯度。 在训练深度学习模型时,正向传播和反向传播是相互依赖的。 训练比预测需要更多的内存(显存)。 5 代码解析 class Layer: 中间层类 self.W # (input_dim, output_dim) self.b # (1, output_dim) self.activate(a) = sigmoid(a)/tanh(a)/ReLU(a)/Softmax(a) def forward(self, input_data): # input_data: (1, input_dim) 单个样本的前向传播 input_data · self.W + self.b = a # a: (1, output_dim) h = self.activate(a) # h: (1, output_dim) return h def backward(input_grad): 单个样本的反向传播 a_grad = input_grad * activate’(a) # (1, output_dim) b_grad = a_grad # (1, output_dim) W_grad = (input_data.T) · a_grad # (input_dim, output_dim) self.b -= learning_rate * b_grad self.W -= learning_rate * W_grad return a_grad · (self.W).T # (1, input_dim)
<

链式法则是反向传播的基本传递方式,它大大简化了反向传播计算的复杂程度。在本例中可能还不太明显,在有些非常复杂的网络中,它的好处会更加显而易见。

另外反向传播的各个节点的算法也是比较重要的内容,本文介绍了常用的节点的反向传播计算结果,实际应用中可能会有更多的形式。不过不用担心,google一下,你就知道。

参数更新是反向传播的目的,结合例子来看可能会更容易理解。下一篇文章会不使用任何框架,纯手写一个我们之前提到的神经网络,并实现象限分类的问题。

至此,我们介绍了反向传播的相关知识,一开始看反向传播资料的时候总觉得相对独立,这个教材这么讲,另一篇博客又换一个讲法,始终不能很好的理解其中的含义,到目前为止,思路相对清晰。我们先从大致流程上介绍了反向传播的来龙去脉,接着用链式求导法则来计算权重以及偏置的偏导,进而我们推出了跟经典著作一样样儿的结论,因此本人觉得较为详细,应该对初学者有一定的借鉴意义,希望对大家有所帮助。

猜你喜欢