动手学深度学习--多层感知机篇(MLP)

多层感知机

前言:本章分为8小章

1.多层感知机

  • 线性模型的缺陷:

具有单调性:即W增大 output增大,W减小 output减小,而现实中存在许多违反单调性的例子:

①体温预测死亡率 ②收入变化与还款可能性

  • 隐藏层与多层感知机(multilayer perceptron MLP)

将前 L-1层看作表示,把最后一层看作线性预测器,这种架构 常称为多层感知机 (此处线性存疑)

在这里插入图片描述

缺点: 具有全连接层的多层感知机的参数开销过大

  • 激活函数:从线性到非线性

多层感知机公式在这里插入图片描述

(H:Hidden O:output),经化简可发现 所谓多层感知机仍可蜕变为:O=XW+b ,摆脱不了其单调性

因此:

我们引入激活函数,增强网络的非线性表征能力,公式如下:

在这里插入图片描述

为了构建更通用的多层感知机, 我们可以继续堆叠这样的隐藏层 :例如

在这里插入图片描述

通过不断堆叠,从而产生更有表达能力的模型

  • 激活函数

①ReLU函数(Rectified linear unit) 中文名:修正线性单元

优点:求导表现好,参数只能消失或者通过,便于优化

公式如下:

在这里插入图片描述

示例:

x = torch.arange(-8.0, 8.0, 0.1, requires_grad=True)
y = torch.relu(x)
d2l.plot(x.detach(), y.detach(), 'x', 'relu(x)', figsize=(5, 2.5))

可以由函数曲线图看出,ReLU是分段线性的

在这里插入图片描述

接下来我们观察ReLU函数的导数图

实现代码:

y.backward(torch.ones_like(x), retain_graph=True)
d2l.plot(x.detach(), x.grad, 'x', 'grad of relu', figsize=(5, 2.5))
#此处我们规定当输入值精确为0时,默认使用左侧导数‘0’

在这里插入图片描述

**此外,ReLU函数 有许多变体,包括参数化ReLU(Parameterized ReLU,pReLU) 函数 该变体为ReLU添加了一个线性项,因此即使参数是负的,某些信息仍然可以通过: **

在这里插入图片描述

②sigmoid函数 又称挤压函数(squashing function)

公式如下:

在这里插入图片描述

该激活函数强制将范围**(-∞,+∞)的任意输入压缩为(0,1)**的某一值

示例:

y = torch.sigmoid(x)
d2l.plot(x.detach(), y.detach(), 'x', 'sigmoid(x)', figsize=(5, 2.5))

在这里插入图片描述

sigmoid函数的导数公式如下:

在这里插入图片描述

示例:

# 清除以前的梯度
x.grad.data.zero_()
y.backward(torch.ones_like(x),retain_graph=True)
d2l.plot(x.detach(), x.grad, 'x', 'grad of sigmoid', figsize=(5, 2.5))

在这里插入图片描述

③tanh函数 又称挤压函数(squashing function)

公式:

在这里插入图片描述

该激活函数强制将范围**(-∞,+∞)的任意输入压缩为(0,1)**的某一值

示例:

y = torch.tanh(x)
d2l.plot(x.detach(), y.detach(), 'x', 'tanh(x)', figsize=(5, 2.5))

在这里插入图片描述

导数公式:

在这里插入图片描述

# 清除以前的梯度
x.grad.data.zero_()
y.backward(torch.ones_like(x),retain_graph=True)
d2l.plot(x.detach(), x.grad, 'x', 'grad of tanh', figsize=(5, 2.5))

在这里插入图片描述


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

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

batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
  • 初始化参数模型

1一般选择2的幂作为层宽度。 因为内存在硬件中的分配和寻址方式,这么做使计算上更高效

2.该数据集中每个图像包含784个灰度像素值,所有图像共分为10类

3.构建单隐藏层的多层感知机(具有256个隐藏单元)

#构建单隐藏层的多层感知机
num_inputs, num_outputs, num_hiddens = 784, 10, 256
# 每个图像包含784个特征  所有图像共有10个类别   而MLP包含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]
  • 激活函数

此处我们手动实现

def relu(X):
    a = torch.zeros_like(X)
    return torch.max(X, a)
#torch,max(X,a) 将两tensor按元素比较,将最大值作为新元素,最终返回每个元素均最大的tensor
  • 模型

#此处构建忽略了图像的空间结构,直接reshape为向量
def net(X):
    X = X.reshape((-1, num_inputs))
    H = relu(X@W1 + b1)  # 这里“@”代表矩阵乘法
    return (H@W2 + b2)
  • 损失函数

loss = nn.CrossEntropyLoss(reduction='none')
  • 训练

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)

在这里插入图片描述

  • 在测试集预测

d2l.predict_ch3(net, test_iter)

在这里插入图片描述


3.多层感知机的简洁实现

高级API简化过程

  • 模型

net = nn.Sequential(nn.Flatten(),#将矩阵按需求自动展平为向量
                    nn.Linear(784, 256),
                    nn.ReLU(),
                    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);
  • 训练

batch_size, lr, num_epochs = 256, 0.1, 10
loss = nn.CrossEntropyLoss(reduction='none')
trainer = torch.optim.SGD(net.parameters(), lr=lr)
#net.parameters()必须填写,此处是梯度更新
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)

在这里插入图片描述


4.模型选择、欠拟合和过拟合

  • 术语

①泛化:即模式能发现训练集潜在的总体规律,并能将其运用到未曾见过的案例并作出正确的预测

过拟合: 将模型在训练数据上拟合的比在潜在分布中更接近的现象称为过拟合(overfitting)

③欠拟合:模型过于简单(表达能力不足),不能捕捉其模式

正则化: 用于对抗过拟合的技术称为正则化(regularization)

  • 模型复杂性

  1. 可调整参数的数量。当可调整参数的数量(有时称为自由度)很大时,模型往往更容易过拟合。
  2. 参数采用的值。当权重的取值范围较大时,模型可能更容易过拟合。
  3. 训练样本的数量。即使你的模型很简单,也很容易过拟合只包含一两个样本的数据集。而过拟合一个有数百万个样本的数据集则需要一个极其灵活的模型。
  • 数据集分类

通常分为三类:训练集 测试集 验证集

  • K折交叉验证

当训练数据稀缺时,我们甚至可能无法提供足够的数据来构成一个合适的验证集。 这个问题的一个流行的解决方案是采用K折交叉验证。 这里,原始训练数据被分成K个不重叠的子集。 然后执行K次模型训练和验证,每次在K−1个子集上进行训练, 并在剩余的一个子集(在该轮中没有用于训练的子集)上进行验证。 最后,通过对K次实验的结果取平均来估计训练和验证误差。


5.权重衰减 (weight decay)

  • 权重衰减(L2正则化)

1.w范围大小决定模型复杂度,w范围过大会导致over-fitting

2.weight-decay提供减小函数复杂度的技术(即自动化调节w范围)

该项技术通过函数与零的举例来衡量函数的复杂度,因为在所有函数F中,F=0(所有输入为0)在某种意义上是最简单的

那么,如何衡量函数与0的距离呢?

本节采用线性函数 f(x)=wx 中的权重向量w的L2范数来衡量其复杂性,即||x||2

要保证权重向量较小,则将其作为惩罚项加入 argmin LOSS()中。

公式如下:

在这里插入图片描述

正则化常数 λ为非负超参数 weight decay。对于 λ=0,我们恢复了原来的损失函数。 对于λ>0,我们限制‖w‖的大小

L2正则化回归的小批量随机梯度下降更新如下式:

在这里插入图片描述

在这里插入图片描述

  • 权重衰减的示例

高维的线性回归

%matplotlib inline
import torch
from torch import nn
from d2l import torch as d2l

首先,我们按如下公式生成数据集:

在这里插入图片描述

为了使过拟合的效果更加明显,我们可以将问题的维数增加到d=200, 并使用一个只包含20个样本的小训练集。

n_train, n_test, num_inputs, batch_size = 20, 100, 200, 5
true_w, true_b = torch.ones((num_inputs, 1)) * 0.01, 0.05
train_data = d2l.synthetic_data(true_w, true_b, n_train)
train_iter = d2l.load_array(train_data, batch_size)
test_data = d2l.synthetic_data(true_w, true_b, n_test)
test_iter = d2l.load_array(test_data, batch_size, is_train=False)
①从零实现权重衰减

1.初始化模型参数

def init_params():
    w = torch.normal(0, 1, size=(num_inputs, 1), requires_grad=True)
    b = torch.zeros(1, requires_grad=True)
    return [w, b]

2.定义L2函数惩罚 (对所有项求平方后求和)

def l2_penalty(w):
    return torch.sum(w.pow(2)) / 2

3.定义训练代码

def train(lambd):
    w, b = init_params()
    net, loss = lambda X: d2l.linreg(X, w, b), d2l.squared_loss
    num_epochs, lr = 100, 0.003
    animator = d2l.Animator(xlabel='epochs', ylabel='loss', yscale='log',
                            xlim=[5, num_epochs], legend=['train', 'test'])
    for epoch in range(num_epochs):
        for X, y in train_iter:
            # 增加了L2范数惩罚项,
            # 广播机制使l2_penalty(w)成为一个长度为batch_size的向量
            l = loss(net(X), y) + lambd * l2_penalty(w)#增加了L2范数惩罚项
            l.sum().backward()
            d2l.sgd([w, b], lr, batch_size)
        if (epoch + 1) % 5 == 0:
            animator.add(epoch + 1, (d2l.evaluate_loss(net, train_iter, loss),
                                     d2l.evaluate_loss(net, test_iter, loss)))
    print('w的L2范数是:', torch.norm(w).item())#得到W的L2范数,即平方和

4.1忽略正则化直接训练

lambd=0

train(lambd=0)
=>   w的L2范数是: 13.756134986877441

在这里插入图片描述

由上图可看出,当未采用weight decay时,w范围较大,模型在数据集上出现了严重的over fitting,验证了本小节开头所说的话

4.2 使用权重衰减训练

train(lambd=3)
w的L2范数是: 0.3605247735977173

在这里插入图片描述

可以由上图看出,test_loss相较于lamba=0时下降很多,说明weight-decay可以有效缓解过拟合现象

①简洁实现

由于跟新的权重衰减部分仅依赖于每个参数的当前值,因此优化器(SGD)必须至少接触每个参数一次

在下面的代码中,我们在实例化优化器时直接通过weight_decay指定weight decay超参数。 默认情况下,PyTorch同时衰减权重和偏移。 这里我们只为权重设置了weight_decay,所以偏置参数b不会衰减。

def train_concise(wd):
    net = nn.Sequential(nn.Linear(num_inputs, 1))
    for param in net.parameters():
        param.data.normal_()
    loss = nn.MSELoss(reduction='none')
    num_epochs, lr = 100, 0.003
    # 偏置参数没有衰减
    #实例化优化器,通过'weight_decay:wd' 指定lambd超参数 即 λ
    # wd通常设置为1e-3
    #而从零实现中的步骤:l = loss(net(X), y) + lambd * l2_penalty(w)在这里自动化实现
    trainer = torch.optim.SGD([
        {"params":net[0].weight,'weight_decay': wd},
        {"params":net[0].bias}], lr=lr)
    animator = d2l.Animator(xlabel='epochs', ylabel='loss', yscale='log',
                            xlim=[5, num_epochs], legend=['train', 'test'])
    for epoch in range(num_epochs):
        for X, y in train_iter:
            trainer.zero_grad()
            l = loss(net(X), y)
            l.mean().backward()
            trainer.step()
        if (epoch + 1) % 5 == 0:
            animator.add(epoch + 1,
                         (d2l.evaluate_loss(net, train_iter, loss),
                          d2l.evaluate_loss(net, test_iter, loss)))
    print('w的L2范数:', net[0].weight.norm().item())
  1. wd=0,即无惩罚项
train_concise(0)
=》   w的L2范数: 13.47992992401123

在这里插入图片描述

​ 2.wd=3,即有惩罚项

train_concise(3)
=>      w的L2范数: 0.37981685996055603

在这里插入图片描述


6.暂退法(Dropout)

前言:

​ 经典泛化理论认为,为缩小训练和测试性能的差距,应该以简单的模型为目标

简单性:以较小维度的形式展现,另一角度是权重衰减中的W范数,而本节简单性的角度是平滑性,即函数不应该对其输入的微小变化敏感。 例如,当我们对图像进行分类时,我们预计向像素添加一些随机噪声应该是基本无影响的。

​ 由此产生暂退法:在计算后续层之前向网络每一层注入噪声,这样会在输入-输出映射上增强平滑性

本节暂退法中的噪声具体为如下形式实现:

在training时,每一层随机丢弃一些神经元。为什么这么做?因为神经网络过拟合与每一·· 层都依赖于前一层激活值相关,称这种情况为**“共适应性**”。 作者认为,暂退法会破坏共适 应性,就像有性生殖会破坏共适应的基因一样。

如何实现随机丢弃?方法如下:

​ 每个中间活性值h暂退概率p由随机变量h′替换

在这里插入图片描述

​ 通过计算可得:E[h’]=h,即drop out 后,期望不变

  • 实践中的暂退法

    在这里插入图片描述

h即为hidden层,也为活性值,drop-out h2,h5即为将h2,h5的输出活性值置为0

当删除h2和h5, 输出的计算也不再依赖于h2或h5,并且它们各自的梯度在执行反向传播时也会消失。 这样,输出层的计算不能过度依赖于h1,…,h5的任何一个元素。

Ⅰ.从零开始实现

#保留大于的节点,把剩下的丢弃
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.rand(X.shape) > dropout).float()return mask * X / (1.0 - dropout)

#①讲解:(torch.rand(X.shape)>dropout).float() 生成了一个与输入tensor形状相同的bool矩阵,并将其float化赋值给mask,便于后续操作
#②讲解:mask * X / (1.0 - dropout) 两个tensor按元素相乘,并将结果按元素除以(1.0-dropout) 得到添加了随机噪声后的输出值,即下一层的输入值
#符合之前的随机丢弃公式
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.))

result:

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., 12.,  0.],
        [16.,  0., 20., 22., 24.,  0., 28., 30.]])
tensor([[0., 0., 0., 0., 0., 0., 0., 0.],
        [0., 0., 0., 0., 0., 0., 0., 0.]])
  • 定义模型参数

定义具有two-hidden的多层MLP,每个hidden包含256个单元

num_inputs, num_outputs, num_hiddens1, num_hiddens2 = 784, 10, 256, 256
  • 定义模型

#将暂退法应用于每个隐藏层的输出,并且可以为每一层分别设置暂退概率: 常见的技巧是在靠近输入层的地方设置较低的暂退概率。 下面的模型将第一个和第二个隐藏层的暂退概率分别设置为0.2和0.5, 并且暂退法只在训练期间有效。


#从零实现中,在前向传播进行了DROP-OUT操作
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()
#从零实现中,在前向传播进行了DROP-OUT操作
    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)
  • 训练和测试

num_epochs, lr, batch_size = 10, 0.5, 256
loss = nn.CrossEntropyLoss(reduction='none')
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)

在这里插入图片描述

Ⅱ. 简洁实现

#在简洁实现中,因高级API特性,我们只需要在每个Linner后添加一个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)

在这里插入图片描述


7.前向传播、反向传播和计算图

前言:前面我们只调用了深度学习框架提供的反向传播函数,而不知道其运行原理。本小节旨在深入了解神经网络 前向传播反向传播的原理与联系

  • 前向传播(forward [propagation|pass])

在这里插入图片描述

  • 前向传播计算图

在这里插入图片描述

  • 反向传播 (backward propagation |backpropagation)

指的是计算神经网络参数梯度的方法,运用了微积分中的链式法则,按相反顺序从输出层到输入层遍历网络

按前向传播中的举例为例进行反向传播演示

反向传播目的是计算梯度:Loss关于W的导数(此处为**∂j/∂W(1)**和 ∂j/∂W(2) ),因为我们需要从计算图的结果开始,并朝参数的方向努力。

∂j/∂W(1)

可以从计算图看出求导路线:

J
s
W1
L
o
h
z

求得导数为:

在这里插入图片描述

∂j/∂W(2)

可以从计算图看出求导路线:

J
s
W2
L
o

求得导数为:

在这里插入图片描述

  • 缺点

因为反向传播为避免重复计算,需要重复利用前向传播中存储的中间值,直到反向传播完成,所以训练时容易导致内存不足(OUT OF MEMEORY)


8.数值稳定性与模型初始化

前言:关于初始化模型参数的方案选择在神经网络学习中有重要作用,对保持数值稳定性至关重要。糟糕选择可能导致训练时遇到梯度爆炸或者梯度消失

  • 梯度消失和梯度爆炸

在这里插入图片描述

导数为L-l个矩阵相乘,若数量过大容易导致数值下溢问题的影响。

梯度消失(gradient exploding)

类比:0.8100=2.0370359763344860862684456884094e-10

参数更新过小,在每次更新时几乎不会移动,导致模型无法学习

例:sigmoid函数是导致gradient消失问题的一个常见原因,具体如下

%matplotlib inline
import torch
from d2l import torch as d2l

x = torch.arange(-8.0, 8.0, 0.1, requires_grad=True)
y = torch.sigmoid(x)
y.backward(torch.ones_like(x))

d2l.plot(x.detach().numpy(), [y.detach().numpy(), x.grad.numpy()],
         legend=['sigmoid', 'gradient'], figsize=(4.5, 2.5))
#画出sigmoid函数曲线图及其导数图

在这里插入图片描述

可以看到,当输入很大或是很小时,它的梯度都会消失。在反向传播过程中,除非在恰好的位置使得sigmoid函数接近0,否则整个反向传播的梯度累乘可能消失。所以人们选择了ReLU函数

梯度爆炸(gradient vanishing)

类比:1.2100=82,817,974.5

参数更新过大,破坏了模型的稳定收敛

例:

M = torch.normal(0, 1, size=(4,4))
print('一个矩阵 \n',M)
for i in range(100):
    M = torch.mm(M,torch.normal(0, 1, size=(4, 4)))

print('乘以100个矩阵后\n', M)
result:
一个矩阵
 tensor([[ 0.6466, -0.4633,  1.3049,  0.0947],
        [ 1.0616, -0.8544,  0.6223,  0.0820],
        [ 0.2280,  0.2577,  0.0492, -0.8014],
        [ 0.2167, -0.2449, -1.2867,  0.9079]])
乘以100个矩阵后
 tensor([[-7.4314e+24, -6.0255e+22, -6.1447e+24,  2.3064e+24],
        [-3.0697e+24, -2.4889e+22, -2.5382e+24,  9.5270e+23],
        [-2.6589e+24, -2.1564e+22, -2.1985e+24,  8.2520e+23],
        [ 8.8963e+24,  7.2139e+22,  7.3560e+24, -2.7610e+24]])
  • 打破对称性

    梯度爆炸或者消失使神经网络不能学习,而在神经网络设计中的另一个问题是其进行相同的参数化导致的神经网络对称性

    对称性

    成立前提:因为对于同一层的不同神经元所使用的前向传播与反向传播算法相同,当将模型的每一层参数初始化为分别同一值时,不同神经元正向传播值相同,反向传播梯度相同,经过SGD后,新的w仍相同。即SGD不能打破对称性

    设我们有一个简单的多层感知机,它有一个隐藏层和两个隐藏单元。 即使我们将每层的参数进行重排列,仍然可以获得相同的函数, 第一个隐藏单元与第二个隐藏单元没有什么特别的区别。这就是排列对称性

    为什么要打破对称性:例如两个神经元的重排列不改变函数,则两个神经元可以化简为一个神经元,而这样的缺点限制了神经网络的表达能力

  • 如何打破对称性

使用参数初始化,因为参数初始化与正则化可以进一步提高稳定性

参数初始化
系统默认初始化
Xavier初始化
其他初始化方法

这里我们介绍XAvier初始化:

在这里插入图片描述

在这里插入图片描述

即Xavier初始化通常从均值为0,方差为 σ2=2/(nin+nout) 的高斯分布中采样权重。

nin为该层输入的数量,nout为该层输出的数量


  • 6
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值