《动手学深度学习》学习笔记 第4章 多层感知机

发布时间:2024年01月09日

本系列为《动手学深度学习》学习笔记

书籍链接:动手学深度学习

笔记是从第四章开始,前面三章为基础知道,有需要的可以自己去看看

关于本系列笔记: 书里为了让读者更好的理解,有大篇幅的描述性的文字,内容很多,笔记只保留主要内容,同时也是对之前知识的查漏补缺

《动手学深度学习》学习笔记 第4章 多层感知机
《动手学深度学习》学习笔记 第5章 深度学习计算
《动手学深度学习》学习笔记 第6章 卷积神经网络
《动手学深度学习》学习笔记 第7章 现代卷积神经网络

4. 多层感知机

??最简单的深度网络称为多层感知机。多层感知机由多层神经元组成,每一层与它的上一层相连,从中接收输入;同时每一层也与它的下一层相连,影响当前层的神经元。

4.1 多层感知机

4.1.1 隐藏层

??仿射变换,它是一种带有偏置项的线性变换。但是,仿射变换中的线性是一个很强的假设。

线性模型可能会出错

??例如,线性意味着单调假设:任何特征的增大都会导致模型输出的增大(如果对应的权重为正),或者导致模
型输出的减小(如果对应的权重为负)。

有时这是有道理的。例如,如果我们试图预测一个人是否会偿还贷款。
我们可以认为,在其他条件不变的情况下,收入较高的申请人比收入较低的申请人更有可能偿还贷款。
但它们不是线性相关的。收入从0增加到5万,可能比从100万增加到105万 带来更大的还款可能性。

??对于深度神经网络,我们使用观测数据来联合学习隐藏层表示和应用于该表示的线性预测器。

在网络中加入隐藏层

??可以通过在网络中加入一个或多个隐藏层来克服线性模型的限制,下面,以图的方式描述了多层感知机(multilayerperceptron),通常缩写为MLP。
在这里插入图片描述

??这个多层感知机有4个输入,3个输出,其隐藏层包含5个隐藏单元。

??具有全连接层的多层感知机的参数开销可能会高得令人望而却步。即使在不改变输入或输出大小的情况下,可能在参数节约和模型有效性之间进行权衡

从线性到非线性

对于具有 h h h个隐藏单元的单隐藏层多层感知机:

  • 矩阵 X ∈ R n × d X ∈ R^{n×d} XRn×d来表示: n n n个样本的小批量,其中每个样本具有 d d d个输入特征。
  • 矩阵 H ∈ R n × h H ∈ R^{n×h} HRn×h表示:隐藏层的输出,称为隐藏表示(hidden representations)。

??在数学或代码中, H H H也被称为隐藏层变量(hidden‐layer variable)或隐藏变量(hidden variable)。

因为隐藏层和输出层都是全连接的,所以我们有:

  • 隐藏层权重 W ( 1 ) ∈ R d × h W^{(1)} ∈ R^{d×h} W(1)Rd×h
  • 隐藏层偏置 b ( 1 ) ∈ R 1 × h b^{(1)} ∈ R^{1×h} b(1)R1×h
  • 输出层权重 W ( 2 ) ∈ R h × q W^{(2)} ∈ R^{h×q} W(2)Rh×q
  • 输出层偏置 b ( 2 ) ∈ R 1 × q b^{(2)} ∈ R^{1×q} b(2)R1×q

形式上,我们按如下方式计算单隐藏层多层感知机的输出 O ∈ R n × q O ∈ R^{n×q} ORn×q

H = X W ( 1 ) + b ( 1 ) , O = H W ( 2 ) + b ( 2 ) . H = XW^{(1)} + b^{(1)}, O = HW^{(2)} + b^{(2)}. H=XW(1)+b(1),O=HW(2)+b(2).

注意在添加隐藏层之后,模型现在需要跟踪和更新额外的参数。
但上面的隐藏单元由输入的仿射函数给出,而输出(softmax操作前)只是隐藏单元的仿射函数。
仿射函数的仿射函数本身就是仿射函数,但是我们之前的线性模型已经能够表示任何仿射函数。
总之,目前除了增加额外的参数,性能没有任何提升(相较于线性模型)

??还需要一个额外的关键要素:在仿射变换之后对每个隐藏单元应用非线性的激活函数(activation function) σ σ σ。激活函数的输出(例如, σ ( ? ) σ(·) σ(?))被称为活性值(activations)。
一般来说,有了激活函数,就不可能再将我们的多层感知机退化成线性模型:

通用近似定理

??虽然一个单隐层网络能学习任何函数,但并不意味着我们应该尝试使用单隐藏层网络来解决所有问题。事实上,通过使用更深(而不是更广)的网络,我们可以更容易地逼近许多函数。

4.1.2 激活函数

??激活函数(activation function)通过计算加权和并加上偏置来确定神经元是否应该被激活??,它们将输入信号转换为输出的可微运算。

??大多数激活函数都是非线性的。

常见的激活函数
修正线性单元(Rectified linear unit,ReLU)

R e L U ( x ) = m a x ( x , 0 ) . ReLU(x) = max(x, 0). ReLU(x)=max(x,0).
在这里插入图片描述

ReLU函数的导数。
在这里插入图片描述

??使用ReLU的原因是,它求导表现得特别好:**要么让参数消失,要么让参数通过。**这使得优化表现得更好,并
且ReLU减轻了困扰以往神经网络的梯度消失问题。

??ReLU函数有许多变体,包括参数化ReLU(Parameterized ReLU,pReLU)函数 。该变体为ReLU添加了一个线性项,因此即使参数是负的,某些信息仍然可以通过:
p R e L U ( x ) = m a x ( 0 , x ) + α m i n ( 0 , x ) . pReLU(x) = max(0, x) + α min(0, x). pReLU(x)=max(0,x)+αmin(0,x).

sigmoid函数

??sigmoid通常称为挤压函数(squashing function):它将范围(‐inf, inf)中的任意输入压缩到区间(0, 1)中的某个值:
s i g m o i d ( x ) = 1 1 + e x p ( ? x ) sigmoid(x) = \frac{1}{1 + exp(?x)} sigmoid(x)=1+exp(?x)1?
??sigmoid函数是一个自然的选择,因为它是一个平滑的、可微的阈值单元近似。
在这里插入图片描述

??sigmoid函数的导数图像如下所示。
在这里插入图片描述

??然而,sigmoid在隐藏层中已经较少使用,它在大部分时候被更简单、更容易训练的ReLU所取代。

tanh函数

??与sigmoid函数类似,tanh(双曲正切)函数也能将其输入压缩转换到区间(‐1, 1)上。tanh函数的公式如下:
t a n h ( x ) = 1 ? e x p ( ? 2 x ) 1 + e x p ( ? 2 x ) . tanh(x) = \frac{ 1 ? exp(?2x)}{1 + exp(?2x)}. tanh(x)=1+exp(?2x)1?exp(?2x)?.
??注意,当输入在0附近时,tanh函数接近线性变换。函数的形状类似于sigmoid函数,不同的是tanh函数关于坐标系原点中心对称。
在这里插入图片描述

tanh函数的导数图像如下所示:
在这里插入图片描述

4.2 多层感知机的从零开始实现

??使用Fashion‐MNIST图像分类数据集:
??Fashion‐MNIST中的每个图像由 28 × 28 = 784个灰度像素值组成。
所有图像共分为10个类别。
??忽略像素之间的空间结构,我们可以将每个图像视为具有784个输入特征和10个类的简单分类数据集。
??首先,我们将实现一个具有单隐藏层的多层感知机,它包含256个隐藏单元。

import torch
from torch import nn
from d2l import torch as d2l

# =========激活函数=========
def relu(X):
	a = torch.zeros_like(X)
	return torch.max(X, a)
# =======定义模型==========
def net(X):
	X = X.reshape((-1, num_inputs))
	H = relu(X@W1 + b1) # 这里“@”代表矩阵乘法
	return (H@W2 + b2)
# ======交叉熵损失======
loss = nn.CrossEntropyLoss(reduction='none')

包含256个隐藏单元
#=========读取数据集============
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)

# =======超参数:784个输入特征,10个类,包含256个隐藏单元======
num_inputs, num_outputs, num_hiddens = 784, 10, 256
W1 = nn.Parameter(torch.randn(num_inputs, num_hiddens, requires_grad=True) * 0.01)
b1 = nn.Parameter(torch.zeros(num_hiddens, requires_grad=True))
W2 = nn.Parameter(torch.randn(num_hiddens, num_outputs, requires_grad=True) * 0.01)
b2 = nn.Parameter(torch.zeros(num_outputs, requires_grad=True))
params = [W1, b1, W2, b2]

#=======训练=======
num_epochs, lr = 10, 0.1
updater = torch.optim.SGD(params, lr=lr)
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, updater)

4.3 多层感知机的简洁实现

import torch
from torch import nn
from d2l import torch as d2l

#torch.nn.Sequential类来实现简单的顺序连接模型。这个模型也是继承自Module类
net = nn.Sequential(nn.Flatten(), 
		nn.Linear(784, 256),
		nn.ReLU(),
		nn.Linear(256, 10))
		
def init_weights(m):
	if type(m) == nn.Linear:
		# 给tensor初始化,一般是给网络中参数weight初始化,初始化参数值符合正态分布。
		nn.init.normal_(m.weight, std=0.01)
	
net.apply(init_weights)
batch_size, lr, num_epochs = 256, 0.1, 10

loss = nn.CrossEntropyLoss(reduction='none')
trainer = torch.optim.SGD(net.parameters(), lr=lr)

train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)

4.4 模型选择、欠拟合和过拟合

4.4.1 训练误差和泛化误差

  • 训练误差(training error) 是指,模型在训练数据集上计算得到的误差;
  • 泛化误差(generalization error) 是指,模型应用在同样从原始样本的分布中抽取的无限多数据样本时,模型误差的期望。

( 我们永远不能准确地计算出泛化误差。这是因为无限多的数据样本是一个虚构的对象。在实际中,我们只能通过将模型应用于一个独立的测试集来估计泛化误差,该测试集由随机选取的、未曾在训练集中出现的数据样本构成。)

统计学习理论

??独立同分布假设(i.i.d. assumption):们假设训练数据和测试数据都是从相同的分布中独立提取的

这意味着对数据进行采样的过程没有进行“记忆”。换句话说,抽取的第2个样本和第3个样本的相关性,并不比抽取的第2个样本和第200万个样本的相关性更强。

模型复杂性(可以不看,跳过)

模型复杂性由什么构成是一个复杂的问题。一个模型是否能很好地泛化取决于很多因素。

我们很难比较本质上不同大类的模型之间(例如,决策树与神经网络)的复杂性。
就目前而言,一条简单的经验法则相当有用:统计学家认为,能够轻松解释任意事实的模型是复杂的,而表达能力有限但仍能很好地解释数据的模型可能更有现实用途
在哲学上,这与波普尔的科学理论的可证伪性标准密切相关:**如果一个理论能拟合数据,且有具体的测试可以用来证明它是错误的,那么它就是好的。**这一点很重要,因为所有的统计估计都是事后归纳。也就是说,我们在观察事实之后进行估计,因此容易受到相关谬误的影响。

4.4.2 模型选择

??验证集: 是将数据分成三份,除了训练和测试数据集之外,还增加一个验证数据集(val‐idation dataset),也叫验证集(validation set)。

??K折交叉验证: 当训练数据稀缺时,我们甚至可能无法提供足够的数据来构成一个合适的验证集。这个问题的一个流行的解决方案是采用K折交叉验证。

4.4.3 欠拟合还是过拟合?

  • 欠拟合: 如果模型不能降低训练误差,这可能意味着模型过于简单(即表达能力不足),无法捕获试图学习的模式。
  • 过拟合: 当我们的训练误差明显低于验证误差

在这里插入图片描述
图4.4.1: 模型复杂度对欠拟合和过拟合的影响

4.4.4 多项式回归

import math
import numpy as np
import torch
from torch import nn
from d2l import torch as d2l
#=================生成数据集========================
max_degree = 20 # 多项式的最大阶数
n_train, n_test = 100, 100 # 训练和测试数据集大小
true_w = np.zeros(max_degree) # 分配大量的空间
true_w[0:4] = np.array([5, 1.2, -3.4, 5.6])

features = np.random.normal(size=(n_train + n_test, 1)) 
np.random.shuffle(features)

poly_features = np.power(features, np.arange(max_degree).reshape(1, -1))
for i in range(max_degree):
	poly_features[:, i] /= math.gamma(i + 1) # gamma(n)=(n-1)!
# labels的维度:(n_train+n_test,)
labels = np.dot(poly_features, true_w)
labels += np.random.normal(scale=0.1, size=labels.shape)

# NumPy ndarray转换为tensor
true_w, features, poly_features, labels = [torch.tensor(x, dtype= torch.float32) for x in [true_w, features, poly_features, labels]]
''' 输出:
features[:2], poly_features[:2, :], labels[:2]
(tensor([[ 1.6580],[-1.6392]]),
tensor([[ 1.0000e+00, 1.6580e+00, 1.3745e+00, 7.5967e-01, 3.1489e-01,
	  1.0442e-01, 2.8855e-02, 6.8346e-03, 1.4165e-03, 2.6096e-04,
	  4.3267e-05, 6.5217e-06, 9.0110e-07, 1.1493e-07, 1.3611e-08,
	  1.5045e-09, 1.5590e-10, 1.5206e-11, 1.4006e-12, 1.2223e-13],
	[ 1.0000e+00, -1.6392e+00, 1.3435e+00, -7.3408e-01, 3.0082e-01,
	  -9.8622e-02, 2.6944e-02, -6.3094e-03, 1.2928e-03, -2.3546e-04,
	  3.8597e-05, -5.7516e-06, 7.8567e-07, -9.9066e-08, 1.1599e-08,
	  -1.2676e-09, 1.2986e-10, -1.2522e-11, 1.1403e-12, -9.8378e-14]]),
tensor([ 6.6262, -5.4505]))
'''

首先让我们实现一个函数来评估模型在给定数据集上的损失。

def evaluate_loss(net, data_iter, loss): #@save
	"""评估给定数据集上模型的损失"""
	metric = d2l.Accumulator(2) # 损失的总和,样本数量
	for X, y in data_iter:
	out = net(X)
	y = y.reshape(out.shape)
	l = loss(out, y)
	metric.add(l.sum(), l.numel())
	return metric[0] / metric[1]

现在定义训练函数。

def train(train_features, test_features, train_labels, test_labels,num_epochs=400):
	loss = nn.MSELoss(reduction='none')
	input_shape = train_features.shape[-1]
	# 不设置偏置,因为我们已经在多项式中实现了它
	net = nn.Sequential(nn.Linear(input_shape, 1, bias=False))
	batch_size = min(10, train_labels.shape[0])
	train_iter = d2l.load_array((train_features, train_labels.reshape(-1,1),batch_size)
	test_iter = d2l.load_array((test_features, test_labels.reshape(-1,1)),batch_size, is_train=False)
	trainer = torch.optim.SGD(net.parameters(), lr=0.01)
	animator = d2l.Animator(xlabel='epoch', ylabel='loss', yscale='log',xlim=[1, num_epochs], ylim=[1e-3,1e2],legend=['train','test'])
	for epoch in range(num_epochs):
		d2l.train_epoch_ch3(net, train_iter, loss, trainer)
		if epoch == 0 or (epoch + 1) % 20 == 0:
			animator.add(epoch + 1, (evaluate_loss(net, train_iter, loss),evaluate_loss(net, test_iter, loss)))
	print('weight:', net[0].weight.data.numpy())

三阶多项式函数拟合(正常)

# 从多项式特征中选择前4个维度,即1,x,x^2/2!,x^3/3!
train(poly_features[:n_train, :4], poly_features[n_train:, :4],labels[:n_train], labels[n_train:])

在这里插入图片描述

线性函数拟合(欠拟合)

# 从多项式特征中选择前2个维度,即1和x
train(poly_features[:n_train, :2], poly_features[n_train:, :2],labels[:n_train], labels[n_train:])

在这里插入图片描述

高阶多项式函数拟合(过拟合)

 从多项式特征中选取所有维度
train(poly_features[:n_train, :], poly_features[n_train:, :],labels[:n_train], labels[n_train:], num_epochs=1500)

在这里插入图片描述

小结:

  • 欠拟合是指指模型无法继续减少训练误差。过拟合是指训练误差远小于验证误差。
  • 由于不能基于训练误差来估计泛化误差,因此简单地最小化训练误差并不一定意味着泛化误差的减小。机器学习模型需要注意防止过拟合,即防止泛化误差过大。
  • 验证集可以用于模型选择,但不能过于随意地使用它。
  • 我们应该选择一个复杂度适当的模型,避免使用数量不足的训练样本。

4.5 权重衰减

本节我们将介绍一些正则化模型的技术

我们总是可以通过去收集更多的训练数据来缓解过拟合。但这可能成本很高,耗时颇多,或者完全超出我们的控制,因而在短期内不可能做到。假设我们已经拥有尽可能多的高质量数据,我们便可以将重点放在正则化技术上。

权重衰减(weight decay) 是最广泛使用的正则化的技术之一,它通常也被称为L2正则化。

这项技术通过函数与零的距离来衡量函数的复杂度,因为在所有函数f中,函数f =0(所有输入都得到值0)在某种意义上是最简单的。但是我们应该如何精确地测量一个函数和零之间的距离呢?没有一个正确的答案。事实上,函数分析和巴拿赫空间理论的研究,都在致力于回答这个问题。

一种简单的方法是通过线性函数 f ( x ) = w ? x f(x) = w^?x f(x)=w?x 中的权重向量的某个范数来度量其复杂性,例 ∣ ∣ w ∥ 2 ||w∥^2 ∣∣w2。要保证权重向量比较小,最常用方法是将其范数作为惩罚项加到最小化损失的问题中。将原来的训练目标最小化训练标签上的预测损失,调整为最小化预测损失和惩罚项之和
具体来说:

损失函数为
L ( W , b ) = 1 n ∑ i = 1 n 1 2 ( w ? x ( i ) + b ? y ( i ) ) 2 L(W,b) = \frac{1}{n}\sum_{i=1}^{n} {\frac{1}{2}}(w^?x^{(i)}+ b ? y^{(i)})^2 L(W,b)=n1?i=1n?21?(w?x(i)+b?y(i))2
其中:

  • x ( i ) x^{(i)} x(i)是样本i的特征
  • y ( i ) y^{(i)} y(i)是样本i的标签
  • ( w , b ) (w, b) (w,b)是权重和偏置参数。

为了惩罚权重向量的大小,必须以某种方式在损失函数中添加 ∣ ∣ w ∥ 2 ||w∥^2 ∣∣w2.

通过正则化常数λ来描述这种权衡,这是一个非负超参数,使用验证数据拟合:
L ( W , b ) + λ 2 ∣ ∣ w ∥ 2 L(W,b) + \frac{λ}{2}||w∥^2 L(W,b)+2λ?∣∣w2

  • λ = 0,即为原来的损失函数。
  • λ > 0,我们限制 ∣ ∣ w ∥ 2 ||w∥^2 ∣∣w2的大小。
  • 这里除以2:当取一个二次函数的导数时,2和1/2会抵消

为什么在这里我们使用平方范数而不是标准范数(即欧几里得距离)?
我们这样做是为了便于计算(处处可导)。

通过平方L2范数,我们去掉平方根,留下权重向量每个分量的平方和。这使得惩罚的导数很容易计算:导数的和等于和的导数。

为什么我们使用L2范数,而不是L1范数?

  • L2正则化线性模型构成经典的岭回归(ridge regression)算法,
  • L1正则化线性回归是统计学中类似的基本模型,通常被称为套索回归(lasso regression)。

使用L2范数的一个原因是它对权重向量的大分量施加了巨大的惩罚。这使得我们的学习算法偏向于在大量特征上均匀分布权重的模型。在实践中,这可能使它们对单个变量中的观测误差更为稳定。相比之下,L1惩罚会导致模型将权重集中在一小部分特征上,而将其他权重清除为零

为什么称为权重衰减?

根据估计值与观测值之间的差异来更新w。同时也在试图将w的大小缩小到零。
与特征选择相比,权重衰减提供了一种连续的机制来调整函数的复杂度。较小的λ值对应较少约束的w,而较大的λ值对w的约束更大。

代码实现:

trainer = torch.optim.SGD([
							{"params":net[0].weight,'weight_decay': wd},
							{"params":net[0].bias}], lr=lr)

们在实例化优化器时直接通过weight_decay指定weight decay超参数。默认情况下,Py‐Torch同时衰减权重和偏移。上述代码只为权重设置了weight_decay,所以偏置参数b不会衰减。

小结

  • 正则化是处理过拟合的常用方法:在训练集的损失函数中加入惩罚项,以降低学习到的模型的复杂度。
  • 保持模型简单的一个特别的选择是使用L2惩罚的权重衰减。这会导致学习算法更新步骤中的权重衰减。
  • 权重衰减功能在深度学习框架的优化器中提供。
  • 在同一训练代码实现中,不同的参数集可以有不同的更新行为。

4.6 暂退法(Dropout)

4.6.1 重新审视过拟合

线性模型:

泛化性和灵活性之间的这种基本权衡被描述为偏差-方差权衡(bias‐variance tradeoff)。

  • 线性模型有很高的偏差:它们只能表示一小类函数。
  • 然而,这些模型的方差很低:它们在不同的随机数据样本上可以得出相似的结果。

与线性模型不同,神经网络并不局限于单独查看每个特征,而是学习特征之间的交互。

深度网络的泛化性质令人费解,而这种泛化性质的数学基础仍然是悬而未决的研究问题。

4.6.2 扰动的稳健性

暂退法(dropout):训练过程中,在计算后续层之前向网络的每一层注入噪声。因为当训练一个有多层的深层网络时,注入噪声只会在输入‐输出映射上增强平滑性。

下面是关于这个方法的来源,有助于理解:

  • 经典泛化理论认为,为了缩小训练和测试性能之间的差距,应该以简单的模型为目标。
  • 简单性的另一个角度是平滑性,即函数不应该对其输入的微小变化敏感。(例如,当我们对图像进行分类时,我们预计向像素添加一些随机噪声应该是基本无影响的。)
  • 1995年,克里斯托弗·毕晓普证明了具有输入噪声的训练等价于Tikhonov正则化 (Bishop, 1995)。
  • 然后在2014年,斯里瓦斯塔瓦等人 (Srivastava et al., 2014) 就如何将毕晓普的想法应用于网络的内部层提出了一个想法:在训练过程中,他们建议在计算后续层之前向网络的每一层注入噪声。因为当训练一个有多层的深层网络时,注入噪声只会在输入‐输出映射上增强平滑性。

4.6.3 实践中的暂退法

当我们将暂退法应用到隐藏层,以p的概率将隐藏单元置为零时,结果可以看作一个只包含原始神经元子集的网络。
在这里插入图片描述

通常,我们在测试时不用暂退法。
给定一个训练好的模型和一个新的样本,我们不会丢弃任何节点,因此不需要标准化。
然而也有一些例外:一些研究人员在测试时使用暂退法,用于估计神经网络预测的“不确定性”:
如果通过许多不同的暂退法遮盖后得到的预测结果都是一致的,那么我们可以说网络发挥更稳定。

4.6.4 从零开始实现

定义 dropout_layer 函数:该函数以dropout的概率丢弃张量输入X中的元素,如上所述重新缩放剩余部分:将剩余部分除以1.0-dropout。

def dropout_layer(X, dropout):
	assert 0 <= dropout <= 1
	# 在本情况中,所有元素都被丢弃
	if dropout == 1:
		return torch.zeros_like(X)
	# 在本情况中,所有元素都被保留
	if dropout == 0:
		return X
	mask = (torch.rand(X.shape) > dropout).float()
	return mask * X / (1.0 - dropout)

使用dropout_layer 函数

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)

4.6.5 简洁实现

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);

小结

  • 暂退法在前向传播过程中,计算每一内部层的同时丢弃一些神经元。
  • 暂退法可以避免过拟合,它通常与控制权重向量的维数和大小结合使用的。
  • 暂退法将活性值h替换为具有期望值h的随机变量。
  • 暂退法仅在训练期间使用。

4.7 前向传播、反向传播和计算图

4.7.1 前向传播

为了简单起见,我们假设

  • 输入样本: x ∈ R d x ∈ R^d xRd
  • 隐藏层(简单起见:不包括偏置项): z = W ( 1 ) x z = W^{(1)}x z=W(1)x (其中 W ( 1 ) ∈ R W^{(1)}∈ R W(1)R 是隐藏层的权重参数, z ∈ R h z ∈ R^h zRh
  • 激活函数 ? ? ? h = ? ( z ) . h = ?(z). h=?(z).
  • 输出层: o = W ( 2 ) h o = W^{(2)}h o=W(2)h (其中权重 W ( 2 ) ∈ R q × h W^{(2)} ∈ R^{q×h} W(2)Rq×h
  • 损失项: L = l ( o , y ) . L = l(o, y). L=l(o,y).(其中损失函数为l,样本标签为y)
  • 正则化项: s = λ 2 ( ∣ ∣ W ( 1 ) ∣ ∣ F 2 + ∣ ∣ W ( 2 ) ∣ ∣ F 2 ) s = \frac{λ}{2}(||W^{(1)}||^2_F+||W^{(2)}||^2_F) s=2λ?(∣∣W(1)F2?+∣∣W(2)F2?)(其中矩阵的Frobenius范数是将矩阵展平为向量后应用的L2范数)
  • 正则化损失: J = L + s . J = L + s. J=L+s. (即目标函数(objective function))

4.7.2 前向传播计算图

上述简单网络相对应的计算图:
在这里插入图片描述

4.7.3 反向传播

反向传播(backward propagation或backpropagation)指的是计算神经网络参数梯度的方法。(简言之,该方
法根据微积分中的链式规则,按相反的顺序从输出层到输入层遍历网络。)

应用链式法则,依次计算每个中间变量和参数的梯度。计算的顺序与前向传播中执行的顺序相反,因为我们需要从计算图的结果开始,并朝着参数的方向努力。

  • 计算目标函数 J = L + s J = L + s J=L+s相对于损失项 L L L正则项 s s s的梯度。
  • 计算目标函数关于输出层变量o的梯度
  • 计算目标函数 关于 最接近输出层的模型参数( W ( 2 ) W^{(2)} W(2) 的梯度
  • 计算目标函数 关于 模型参数( W ( 1 ) W^{(1)} W(1) 的梯度

在计算上述梯度的过程中还需要其他梯度的计算,可以根据计算图推倒出来:
文中计算过程如下(书 P 164 P_{164} P164?):
在这里插入图片描述

4.7.4 训练神经网络

在训练神经网络时,前向传播和反向传播相互依赖。

  • 对于前向传播,我们沿着依赖的方向遍历计算图并计算其路径上的所有变量。
  • 然后将这些用于反向传播,其中计算顺序与计算图的相反。

以上述简单网络为例:

  • 一方面,在前向传播期间计算正则项 (4.7.5)取决于模型参数 W ( 1 ) W^{(1)} W(1) W ( 2 ) W^{(2)} W(2)的当前值。它们是由优化算法根据最近迭代的反向传播给出的。

另一方面,反向传播期间参数 (4.7.11)的梯度计算,取决于由前向传播给出的隐藏变量h的当前值
在这里插入图片描述
小结

  • 前向传播在神经网络定义的计算图中按顺序计算和存储中间变量,它的顺序是从输入层到输出层。
  • 反向传播按相反的顺序(从输出层到输入层)计算和存储神经网络的中间变量和参数的梯度。
  • 在训练深度学习模型时,前向传播和反向传播是相互依赖的。
  • 训练比预测需要更多的内存。

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

初始化方案的选择在神经网络学习中起着举足轻重的作用,它对保持数值稳定性至关重要。
此外,这些初始化方案的选择可以与非线性激活函数的选择有趣的结合在一起。
我们选择哪个函数以及如何初始化参数可以决定优化算法收敛的速度有多快。糟糕选择可能会导致我们在训练时遇到梯度爆炸或梯度消失。

4.8.1 梯度消失和梯度爆炸

考虑一个具有 L L L层、输入 x x x和输出 o o o的深层网络。每一层l由变换 f l f_l fl?定义,该变换的参数为权重 W ( l ) W^{(l)} W(l),其隐藏变量是 h ( l ) (令 h ( 0 ) = x ) h^{(l)}(令 h^{(0)} = x) h(l)(令h(0)=x。网络可以表示为:
h ( l ) = f l ( h ( l ? 1 ) ) 因此 o = f L ? . . . ? f 1 ( x ) . h^{(l)} = f_l(h^{(l-1)}) 因此o = f_L ? . . . ? f_1(x). h(l)=fl?(h(l?1))因此o=fL??...?f1?(x).
如果所有隐藏变量和输入都是向量,我们可以将 o o o关于任何一组参数 W ( l ) W^{(l)} W(l)的梯度写为下式:
在这里插入图片描述
换言之,该梯度是 L ? l L ? l L?l个矩阵 M ( L ) ? . . . ? M ( l + 1 ) M^{(L)}· . . . · M^{(l+1)} M(L)?...?M(l+1)与梯度向量 v ( l ) v^{(l)} v(l)的乘积。

  • 当将太多的概率乘在一起时,会出现到数值下溢问题
  • 在处理概率时,一个常见的技巧是切换到对数空间。不幸的是,上面的问题更为严重。
  • 矩阵 M(l) 可能具有各种各样的特征值。(他们可能很小,也可能很大;他们的乘积可能非常大,也可能非常小)

梯度消失

由于早期的人工神经网络受到生物神经网络的启发,神经元要么完全激活要么完全不激活(就像生物神经元)的想法很有吸引力。所以,sigmoid函数 1 / ( 1 + e x p ( ? x ) ) 1/(1 + exp(?x)) 1/(1+exp(?x))很流行,因为它类似于阈值函数。然而,它却是导致梯度消失问题的一个常见的原因,
在这里插入图片描述
正如上图,当 s i g m o i d sigmoid sigmoid函数的输入很大或是很小时,它的梯度都会消失。因此,更稳定的 R e L U ReLU ReLU系列函数已经成为从业者的默认选择(虽然在神经科学的角度看起来不太合理)。

梯度爆炸
相反,梯度爆炸可能同样令人烦恼。矩阵乘积发生爆炸。当这种情况是由于深度网络的初始化所导致时,我们没有机会让梯度下降优化器收敛。

打破对称性
神经网络设计中的另一个问题是其参数化所固有的对称性。假设我们有一个简单的多层感知机,它有一个隐
藏层和两个隐藏单元。在这种情况下,我们可以对第一层的权重W(1)进行重排列,并且同样对输出层的权重
进行重排列,可以获得相同的函数。第一个隐藏单元与第二个隐藏单元没有什么特别的区别。换句话说,我
们在每一层的隐藏单元之间具有排列对称性。

4.8.2 参数初始化

解决(或至少减轻)上述问题的一种方法是进行参数初始化,优化期间的注意和适当的正则化也可以进一步
提高稳定性。

默认初始化
可以使用正态分布来初始化权重值。如果我们不指定初始化方法,框架将使用默认的随机初始化方法,对于中等难度的问题,这种方法通常很有效。

Xavier初始化

考虑一个简单的全连接层,该层接受 n i n n_{in} nin?个输入并产生 n o u t n_{out} nout?个输出。每个输出 o i o_i oi?可以表示为:
o i = ∑ j = 1 n i n w i j x j o_i = \sum_{j=1}^{n_{in}} w_{ij}x_j oi?=j=1nin??wij?xj?

权重 w i j w_{ij} wij?都是从同一分布中独立抽取的

此外,让我们假设该分布具有零均值方差 σ 2 σ^2 σ2。请注意,这并不意味着分布必须是高斯的,只是均值和方差需要存在。

现在,让我们假设 x j x_j xj?的输入也具有零均值和方差 γ 2 γ^2 γ2并且它们独立于 w i j w_{ij} wij?并且彼此独立

在这种情况下,我们可以按如下方式计算 o i o_i oi?的平均值和方差(忽略偏执和激活函数):
在这里插入图片描述
Xavier 初始化试图使得每一层的输出的方差接近于其输入的方差。具体地,它设置权重 w i j w_{ij} wij?的初始方差为:
1 2 ( n i n + n o u t ) σ 2 = 1 \frac{1}{2}(n_{in} + n_{out})σ2 = 1 21?(nin?+nout?)σ2=1
这样,无论 和 的大小如何,这一层的输出方差都接近于其输入方差。

作用:

  • **梯度消失和爆炸:**在深度网络中,梯度消失和梯度爆炸是一个常见的问题。如果每一层都将方差放大,那么在多层网络中,梯度可能会很快增长至非常大的值(爆炸),或者减小至接近零(消失)。Xavier初始化试图使得每一层的输出的方差接近于其输入的方差,从而避免梯度消失或梯度爆炸的问题。
  • 加速收敛: Xavier 初始化使得每一层的输出的方差接近于其输入的方差,从而使得每一层的梯度的方差接近于 1。这样,每一层的参数更新的幅度就不会相差太大,从而加速收敛。
文章来源:https://blog.csdn.net/weixin_42046845/article/details/135125463
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。