动手学深度学习——多层感知机(学习笔记)

动手学深度学习——多层感知机(学习笔记)

1.多层感知机

1.1隐藏层
线性模型可能会出错:线性意味着单调假设, 任何特征的增大都会导致模型输出的增大(对应的权重为正),或者导致模型输出的减小(对应的权重为负)。而我们也可以很容易找出违反单调性的例子,例如对猫狗的图像进行分类,在这里我们的数据会有一种表示,这种表示会考虑到我们在特征之间的相关交互作用。
1.1.1在网络中加入隐藏层
可通过在网络中加入一个或多个隐藏层来克服线性模型的限制,使其能处理更普遍的函数关系类型。最简单的方法是将许多全连接层堆叠在一起,形成多层感知机(multilayer perceptron)架构,缩写为MLP。
在这里插入图片描述
这是一个单隐藏层的多层感知机,具有5个隐藏单元,产生输出只需要实现隐藏层和输出层的计算,因此这个多层感知机的层数为2。注意,这两个层都是全连接的。 每个输入都会影响隐藏层中的每个神经元, 而隐藏层中的每个神经元又会影响输出层中的每个神经元。
1.1.2从线性到非线性
我们通过矩阵X表示n个样本的小批量, 其中每个样本具有d个输入特征。 对于具有h个隐藏单元的单隐藏层多层感知机,有隐藏层权重在这里插入图片描述和隐藏层偏置在这里插入图片描述以及输出层权重在这里插入图片描述
输出层偏置在这里插入图片描述
形式上,我们按如下方式计算单隐藏层多层感知机的输出 O:
在这里插入图片描述
此时 在仿射变换之后对每个隐藏单元应用非线性的激活函数使多层感知机不再退化成线性模型:
在这里插入图片描述
1.1.3通用近似原理

  • 多层感知机可以通过隐藏神经元,捕捉到输入之间复杂的相互作用, 这些神经元依赖于每个输入的值。我们可以设计隐藏节点来执行任意计算。
  • 使用更深的网络可以更容易地逼近许多函数。
    1. 2.激活函数**
  • 激活函数(activation function)通过计算加权和并加上偏置来确定神经元是否应该被激活, 它们将输入信号转换为输出的可微运算。
  • 大多数激活函数都是非线性的,如ReLU函数,sigmoid函数。
    1.2.1ReLU函数
    修正线性单元(Rectified linear unit,ReLU)被定义为取给定元素x与0的最大值,即仅保留正元素并丢弃所有负元素。它实现简单,同时在各种预测任务中表现良好。
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函数的导数

y.backward(torch.ones_like(x), retain_graph=True)
d2l.plot(x.detach(), x.grad, 'x', 'grad of relu', figsize=(5, 2.5))

在这里插入图片描述
当输入为负时,ReLU函数的导数为0,当输入为正时,ReLU函数的导数为1。 注意,当输入值精确等于0时,ReLU函数不可导。
1.2.2sigmoid函数
sigmoid通常称为挤压函数(squashing function): 它将范围(-inf, inf)中的任意输入压缩到区间(0, 1)中的某个值:在这里插入图片描述
sigmoid函数是一个平滑的、可微的阈值单元近似,当输入接近0时,sigmoid函数接近线性变换。在隐藏层中已经较少使用,大部分时候被更简单、更容易训练的ReLU所取代。

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

在这里插入图片描述
sigmoid函数的导数
在这里插入图片描述
当输入为0时,sigmoid函数的导数达到最大值0.25; 而输入在任一方向上越远离0点时,导数越接近0。

# 清除以前的梯度
x.grad.data.zero_()
y.backward(torch.ones_like(x),retain_graph=True)# 对函数 y 进行反向传播,计算关于输入 x 的梯度
d2l.plot(x.detach(), x.grad, 'x', 'grad of sigmoid', figsize=(5, 2.5))

在这里插入图片描述
1.2.3tanh函数
与sigmoid函数类似, tanh(双曲正切)函数也能将其输入压缩转换到区间(-1, 1)上,当输入在0附近时,tanh函数接近线性变换。 函数的形状类似于sigmoid函数, 不同的是tanh函数关于坐标系原点中心对称。
在这里插入图片描述

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

在这里插入图片描述
tanh函数的导数:当输入接近0时,tanh函数的导数接近最大值1,输入在任一方向上越远离0点,导数越接近0。
在这里插入图片描述

# 清除以前的梯度
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.多层感知机的从零开始实现

使用Fashion-MNIST图像分类数据集

from mxnet import gluon, np, npx
from d2l import mxnet as d2l
npx.set_np()
batch_size = 256# 指定批量大小为 256
train_iter,test_iter=d2l.load_data_fashion_mnist(batch_size)

2.1初始化模型参数

  • Fashion-MNIST中的每个图像由28*28=784个灰度像素值组成,所有图像共分为10个类别。我们将每个图像视为具有784个输入特征和10个类的简单分类数据集。
  • 实现一个具有单隐藏层的多层感知机, 它包含256个隐藏单元。
  • 注意,对于每一层都要记录一个权重矩阵和一个偏置向量。
# 定义模型的输入层、输出层和隐藏层的维度
num_inputs, num_outputs, num_hiddens = 784, 10, 256
# 初始化模型的权重参数 W1, W2 和偏置参数 b1, b2
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]

2.2激活函数

def relu(X):
    a = torch.zeros_like(X)
    return torch.max(X, a)

2.3模型
忽略了空间结构,使用reshape将每个二维图像转换为一个长度为num_inputs的向量。

def net(X):
    X = X.reshape((-1, num_inputs))
    H = relu(X@W1 + b1)  # 这里“@”代表矩阵乘法
    return (H@W2 + b2)

2.4损失函数

loss = nn.CrossEntropyLoss(reduction='none')

2.5训练
与softmax回归的训练过程完全相同,可以直接调用d2l包的train_ch3函数。

#迭代周期数设置为10,学习率设置为0.1
num_epochs, lr = 10, 0.1
# 创建SGD 优化器,传入需优化的参数列表 params 和学习率 lr
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)

在这里插入图片描述

多层感知机的简洁实现

通过高级API更简洁地实现多层感知机。

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

3.1模型
与softmax回归的简洁实现相比,唯一的区别是添加了2个全连接层(之前只添加了1个全连接层)。第一层是隐藏层,包含256个隐藏单元,并使用了ReLU激活函数。第二层是输出层。

net = nn.Sequential(nn.Flatten(),
                    nn.Linear(784, 256),
                    nn.ReLU(),
                    nn.Linear(256, 10))
#对模型的权重进行初始化
def init_weights(m):
#如果当前层是全连接层 nn.Linear
    if type(m) == nn.Linear:
      # 对该层的权重进行正态分布初始化,标准差为 0.01
        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.模型选择、欠拟合和过拟合

过拟合(overfitting)是指模型在训练数据上拟合的比在潜在分布中更接近的现象,正则化(regularization)用于对抗这种现象。在前几次的实验中,我们可能会发现,调整模型架构或超参数使模型在训练集上达到完美的精度,但此时测试集的准确性却下降了。

4.1训练误差和泛化误差
训练误差(training error)是指, 模型在训练数据集上计算得到的误差。 泛化误差(generalization error)是指, 模型应用在同样从原始样本的分布中抽取的无限多数据样本时,模型误差的期望。在实际中,我们通过将模型应用于一个由随机选取的、未曾在训练集中出现的数据样本构成的独立测试集来估计泛化误差。
4.2模型选择
在机器学习中,我们通常在评估几个候选模型后选择最终的模型。 这个过程叫做模型选择。需要比较的模型可能是本质不同的模型,例如决策树与线性模型,也可能是不同超参数设置下的同一模型,例如,比较 具有不同数量的隐藏层、不同数量的隐藏单元的多层感知机模型。为了确定候选模型中的最佳模型,我们通常会使用验证集。
4.2.1验证集
如果我们在模型选择过程中使用测试数据,则存在过拟合测试数据的风险,所以原则上,在确定所有超参数之前,我们不希望用到测试集。但我们也不能仅仅依靠训练数据来选择模型,这样一来无法估计训练数据的泛化误差,实际情况中测试数据很少在用过一次后就被丢弃。所以我们将数据分成三份, 除了训练和测试数据集之外,还增加一个验证数据集(validation dataset), 也叫验证集(validation set)。
4.2.2K折交叉验证
在数据稀缺时,我们采用K折交叉验证构成合适的验证集。原始训练数据被分成K个不重叠的子集。 然后执行K次模型训练和验证,每次在K-1个子集上进行训练, 并在剩余的一个子集(在该轮中没有用于训练的子集)上进行验证。 最后,通过对K次实验的结果取平均来估计训练和验证误差。
4.3过拟合、欠拟合?
4.3.1模型复杂度
以多项式为例,给定由单个特征x和对应实数标签y组成的训练集,目标是找到d阶多项式来估计标签y。
在这里插入图片描述
这是一个线性回归问题,特征是x的幂给出的,模型的权重是wi给出的,偏置是w0给出的 (因为对于所有的x都有x的0次幂等于1),我们可以使用平方误差作为我们的损失函数。
高阶多项式函数比低阶多项式函数复杂得多。 高阶多项式的参数较多,模型函数的选择范围较广。 因此在固定训练数据集的情况下, 高阶多项式函数相对于低阶多项式的训练误差应该始终更低(最坏也是相等)。 事实上,当数据样本包含了x的不同值时, 函数阶数等于数据样本数量的多项式函数可以完美拟合训练集。下图直观地描述了多项式的阶数和欠拟合与过拟合之间的关系。
在这里插入图片描述
4.3.2数据集大小
训练数据集中的样本越少,越有可能(且更严重地)过拟合。 随着训练数据量的增加,泛化误差通常会减小。一般来说,更多的数据不会有什么坏处。 对于固定的任务和数据分布,模型复杂性和数据集大小之间通常存在关系。数据越多,我们可能会尝试拟合一个更复杂的模型,这往往是有益的。如果没有足够的数据,简单的模型可能更有用。
4.4多项式回归

import math
import numpy as np
import torch
from torch import nn
from d2l import torch as d2l

4.4.1生成数据集
给定x,我们将使用以下三阶多项式来生成训练和测试数据的标签
在这里插入图片描述

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]

4.4.2对模型进行训练和测试
首先实现一个函数来评估模型在给定数据集上的损失。

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)
        # 每隔20个epoch记录一次训练和测试集的损失
        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.4.3. 三阶多项式函数拟合(正常)
首先使用三阶多项式函数,它与数据生成函数的阶数相同。 结果表明,该模型能有效降低训练损失和测试损失。

# 从多项式特征中选择前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:])

在这里插入图片描述
4.4.4. 线性函数拟合(欠拟合)
线性函数拟合,减少该模型的训练损失相对困难。 在最后一个迭代周期完成后,训练损失仍然很高。 当用来拟合非线性模式(如这里的三阶多项式函数)时,线性模型容易欠拟合。

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

在这里插入图片描述
4.4.5. 高阶多项式函数拟合(过拟合)
使用一个阶数过高的多项式来训练模型,没有足够的数据用于学到高阶系数应该具有接近于零的值,因此这个过于复杂的模型会轻易受到训练数据中噪声的影响。 虽然训练损失可以有效地降低,但测试损失仍然很高,对数据造成了过拟合。

# 从多项式特征中选取所有维度
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) 在某种意义上是最简单的。
在线性回归中损失公式为在这里插入图片描述
为了惩罚权重向量的大小, 我们必须以某种方式在损失函数中添加||w||的平方,我们通过正则化常数 λ \lambda λ来描述这种权衡, 这是一个非负超参数,我们使用验证数据拟合:在这里插入图片描述
λ \lambda λ=0,恢复了原来的损失函数, λ \lambda λ>0,限制||w||的大小。通过平方L2范数,我们去掉平方根,留下权重向量每个分量的平方和。 这使得惩罚的导数很容易计算:导数的和等于和的导数。
L2正则化线性模型构成经典的岭回归(ridge regression)算法,
L1正则化线性回归是统计学中类似的基本模型, 通常被称为套索回归(lasso regression)。 使用L2范数的一个原因是它对权重向量的大分量施加了巨大的惩罚。 这使得我们的学习算法偏向于在大量特征上均匀分布权重的模型。 在实践中,这可能使它们对单个变量中的观测误差更为稳定。 相比之下,L1惩罚会导致模型将权重集中在一小部分特征上, 而将其他权重清除为零。
L2正则化回归的小批量随机梯度下降更新如下式:在这里插入图片描述
4.5.1高维线性回归

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

生成数据公式:在这里插入图片描述

#训练集大小、测试集大小、输入特征数量和批量大小
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)
#False表示不进行训练
test_iter = d2l.load_array(test_data, batch_size, is_train=False)

4.5.2从零开始实现
4.5.2.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]

4.5.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)
            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())

4.5.2.4忽略正则化直接训练
用lambd = 0禁用权重衰减后运行这个代码。 注意,这里训练误差有了减少,但测试误差没有减少, 这意味着出现了严重的过拟合。

train(lambd=0)

在这里插入图片描述
4.5.2.5使用权重衰减
使用权重衰减来运行代码,训练误差增大,但测试误差减小。 这正是我们期望从正则化中得到的效果。

train(lambd=3)

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

def train_concise(wd):
  #Sequential 模块定义简洁的模型
    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
    # 偏置参数没有衰减
    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())
    train_concise(0)

在这里插入图片描述

train_concise(3)

在这里插入图片描述
这些图看起来和从零开始实现权重衰减时的图相同,但它们运行得更快,更容易实现,对于更复杂的问题,优势更加明显。

  • 38
    点赞
  • 42
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值