深度学习-基础知识

深度学习-基础知识

线性回归

线性回归输出是一个连续值,因此适用于回归问题。回归问题在实际中很常见,如预测房屋价格、气温、销售额等连续值的问题。与回归问题不同,分类问题中模型的最终输出是一个离散值。我们所说的图像分类、垃圾邮件识别、疾病检测等输出为离散值的问题都属于分类问题的范畴。softmax回归则适用于分类问题。

一个例子

形如

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pgL7NG6L-1687395257219)(image/手动深度学习/1683386246721.png)]

其损失函数可表示为

在这里插入图片描述

通常,我们用训练数据集中所有样本误差的平均来衡量模型预测的质量

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S6Ahktxo-1687395257223)(image/手动深度学习/1683386291351.png)]

优化算法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xZN4Cwsq-1687395257223)(image/手动深度学习/1683386319531.png)]

神经网络图

在深度学习中,我们可以使用神经网络图直观地表现模型结构。为了更清晰地展示线性回归作为神经网络的结构,图3.1使用神经网络图表示本节中介绍的线性回归模型。神经网络图隐去了模型参数权重和偏差。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dWMW2e9t-1687395257224)(image/手动深度学习/1683386398108.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KOe9WrJo-1687395257225)(image/手动深度学习/1683386435791.png)]

矢量计算表达式

先考虑对两个向量相加的两种方法

#定义两个1000维的向量
import torch
from time import time
a = torch.ones(1000)
b = torch.ones(1000)
#向量相加的一种方法是,将这两个向量按元素逐一做标量加法
start = time()
c = torch.zeros(1000)
for i in range(1000):
    c[i] = a[i] + b[i]
print(time() - start)
#output
0.028502464294433594

#向量相加的另一种方法是,将这两个向量直接做矢量加法。
start = time()
d = a + b
print(time() - start)
#output
0.0008330345153808594

'''结果很明显,后者比前者更省时。因此,我们应该尽可能采用矢量计算,以提升计算效率'''

从0开始实现线性回归

首先,导入本节中实验所需的包或模块,其中的matplotlib包可用于作图,且设置成嵌入显示。

%matplotlib inline
import torch
from IPython import display
from matplotlib import pyplot as plt
import numpy as np
import random
from IPython.core.pylabtools import figsize

1.生成数据集

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-38t3aBlW-1687395257226)(image/手动深度学习/1683387771954.png)]

import matplotlib_inline
import matplotlib
import torch
from IPython import display
from IPython.core.pylabtools import figsize
from matplotlib import pyplot as plt
import numpy as np
import random

num_inputs = 2
num_examples = 1000
true_w = [2, -3.4]
true_b = 4.2
features = torch.randn(num_examples, num_inputs, dtype=torch.float32)
labels = true_w[0] * features[:, 0] + true_w[1] * features[:, 1] + true_b
labels += torch.tensor(np.random.normal(0, 0.01, size=labels.size()), dtype=torch.float32)
'''注意,features的每一行是一个长度为2的向量,而labels的每一行是一个长度为1的向量(标量)'''
print(features[0], labels[0])
#output
tensor([ 1.4038, -0.7774]) tensor(9.6383)

'''通过生成第二个特征features[:, 1]和标签 labels 的散点图,可以更直观地观察两者间的线性关系'''
def use_svg_display():
    # 用矢量图显示
    display.set_matplotlib_formats('svg')
def set_figsize(figsize=(3.5, 2.5)):
    use_svg_display()
    # 设置图的尺寸
    plt.rcParams['figure.figsize'] = figsize
set_figsize()
plt.scatter(features[:, 1].numpy(), labels.numpy(), 1);#画图
plt.show()#显示图像

生成的图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G9GYo307-1687395257226)(image/手动深度学习/1683389828521.png)]

读取数据
'''在训练模型的时候,我们需要遍历数据集并不断读取小批量数据样本。这里我们定义一个函数:它每次返回 `batch_size`(批量大小)个随机样本的特征和标签'''
def data_iter(batch_size, features, labels):
    num_examples = len(features)
    indices = list(range(num_examples))
    random.shuffle(indices)  # 样本的读取顺序是随机的
    for i in range(0, num_examples, batch_size):
        j = torch.LongTensor(indices[i: min(i + batch_size, num_examples)]) # 最后一次可能不足一个batch
        yield  features.index_select(0, j), labels.index_select(0, j)
'''让我们读取第一个小批量数据样本并打印。每个批量的特征形状为(10, 2),分别对应批量大小和输入个数;标签形状为批量大小。'''
batch_size = 10
for x, y in data_iter(batch_size, features, labels):
    print(x, y)
    break
#output
tensor([[ 1.4410,  0.5240],
        [ 0.1334,  0.8395],
        [ 0.4540, -0.5836],
        [-1.6548, -1.2607],
        [ 1.2573,  1.7597],
        [-0.1178, -0.4633],
        [-0.6796,  1.9484],
        [-0.7199,  0.5652],
        [-0.2284,  0.1166],
        [-0.2384,  2.2917]]) tensor([ 5.3098,  1.6058,  7.1165,  5.1518,  0.7346,  5.5258, -3.7823,  0.8459,
         3.3366, -4.0580])
初始化模型参数
'''我们将权重初始化成均值为0、标准差为0.01的正态随机数,偏差则初始化成0'''
w = torch.tensor(np.random.normal(0, 0.01, (num_inputs, 1)), dtype=torch.float32)
b = torch.zeros(1, dtype=torch.float32)
'''之后的模型训练中,需要对这些参数求梯度来迭代参数的值,因此我们要让它们的requires_grad=True'''
定义模型
'''我们使用上一节描述的平方损失来定义线性回归的损失函数。在实现中,我们需要把真实值y变形成预测值y_hat的形状。以下函数返回的结果也将和y_hat的形状相同'''
def squared_loss(y_hat, y): 
    return (y_hat - y.view(y_hat.size()))**2 / 2 # 注意这里返回的是向量, 另外, pytorch里的MSELoss并没有除以 2
定义优化算法
'''以下的sgd函数实现了上一节中介绍的小批量随机梯度下降算法。它通过不断迭代模型参数来优化损失函数。这里自动求梯度模块计算得来的梯度是一个批量样本的梯度和。我们将它除以批量大小来得到平均值'''
def sgd(params, lr, batch_size):
    for param in params:
        param.data -= lr * param.grad / batch_size # 注意这里更改param时用的param.data

训练模型

在训练中,我们将多次迭代模型参数。在每次迭代中,我们根据当前读取的小批量数据样本(特征 X和标签 y),通过调用反向函数 backward计算小批量随机梯度,并调用优化算法 sgd迭代模型参数。由于我们之前设批量大小 batch_size为10,每个小批量的损失 l的形状为(10, 1)。回忆一下自动求梯度一节。由于变量 l并不是一个标量,所以我们可以调用 .sum()将其求和得到一个标量,再运行 l.backward()得到该变量有关模型参数的梯度。注意在每次更新完参数后不要忘了将参数的梯度清零。

在一个迭代周期(epoch)中,我们将完整遍历一遍 data_iter函数,并对训练数据集中所有样本都使用一次(假设样本数能够被批量大小整除)。这里的迭代周期个数 num_epochs和学习率 lr都是超参数,分别设3和0.03。在实践中,大多超参数都需要通过反复试错来不断调节。虽然迭代周期数设得越大模型可能越有效,但是训练时间可能过长。

完整代码
import matplotlib
import torch
from IPython import display
from matplotlib import pyplot as plt
import numpy as np
import random
'''构造一个简单的人工训练数据集'''
num_inputs = 2  # 特征数(输入个数)为2
num_examples = 1000  # 样本数1000
true_w = [2, -3.4]  # 线性回归真实权重
true_b = 4.2  # 偏差
features = torch.randn(num_examples, num_inputs, dtype=torch.float32)  # 特征(就是x)
labels = true_w[0] * features[:, 0] + true_w[1] * features[:, 1] + true_b  # 标签(true_y)
labels += torch.tensor(np.random.normal(0, 0.01, size=labels.size()), dtype=torch.float32)  # 加上噪声
'''
在深度学习中,添加噪声有时被用于正则化模型,可以帮助防止过拟合,提高模型的泛化能力。
在这个例子中,噪声被添加到标签中,这可以帮助模型更好地学习数据的分布,防止过度拟合。
添加的噪声是从正态分布中随机采样得到的,其均值为0,标准差为0.01。
这个噪声的大小足够小,不会严重改变标签的分布,但足以产生一定的噪声,帮助模型更好地学习数据的特征。
'''

'''读取数据:在训练模型的时候,我们需要遍历数据集并不断读取小批量数据样本。这里我们定义一个函数:它每次返回batch_size(批量大小)个随机样本的特征和标签'''
def data_iter(batch_size, features, labels):
    num_examples = len(features)  # 获取数据集的样本数
    indices = list(range(num_examples))  # 生成所有样本的索引列表
    random.shuffle(indices)  # 将所有样本的索引随机打乱,实现样本的随机读取
    for i in range(0, num_examples, batch_size):  # 循环读取所有小批量样本
        j = torch.LongTensor(indices[i: min(i + batch_size, num_examples)])  # 最后一次可能不足一个batch
        yield features.index_select(0, j), labels.index_select(0, j)  # 函数根据随机索引选取小批量的特征和标签,并通过 yield 关键字返回这些数据


'''将权重初始化成均值为0、标准差为0.01的正态随机数,偏差则初始化成0。'''
batch_size = 10
w = torch.tensor(np.random.normal(0, 0.01, (num_inputs, 1)), dtype=torch.float32, requires_grad=True)
b = torch.zeros(1, dtype=torch.float32, requires_grad=True)  # requires_grad=True别漏

'''线性回归的矢量计算表达式的实现,使用mm函数做矩阵乘法'''
def linreg(X, w, b):
    return torch.mm(X, w) + b


'''定义线性回归的损失函数'''
def squared_loss(y_hat, y):
    # 注意这里返回的是向量, 另外, pytorch里的MSELoss并没有除以 2
    return (y_hat - y.view(y_hat.size())) ** 2 / 2


'''以下的sgd函数实现了上一节中介绍的小批量随机梯度下降算法。它通过不断迭代模型参数来优化损失函数。这里自动求梯度模块计算得来的梯度是一个批量样本的梯度和。我们将它除以批量大小来得到平均值'''
def sgd(params, lr, batch_size):  # 小批量随机梯度下降算法
    for param in params:
        param.data -= lr * param.grad / batch_size  # 这个公式可以理解为每次更新参数时,将参数沿着负梯度方向移动一步,步长为 lr * param.grad / batch_size
        param.grad.data.zero_()  # 清零梯度
        ''' param.data 是一个张量(tensor),它包含了该参数的实际数值。在 PyTorch 中,模型的参数被存储在一个叫做 Parameter 的类中,Parameter 对象其实是 Tensor 的子类,
        它除了拥有 Tensor 的所有属性和方法之外,还有一个额外的属性 grad,用于存储该参数的梯度值。
        在训练过程中,我们需要对模型参数进行更新。而直接修改 Parameter 对象是不安全的,因为它可能会打破计算图的连续性。
        为了安全地更新模型参数,我们可以通过修改 param.data 属性来更新该参数的实际数值,而不会破坏计算图的连续性'''

lr = 0.03  # 学习率
num_epochs = 3  # 迭代周期个数
net = linreg
loss = squared_loss

for epoch in range(num_epochs):  # 训练模型一共需要num_epochs个迭代周期
    # 在每一个迭代周期中,会使用训练数据集中所有样本一次(假设样本数能够被批量大小整除. X和y分别是小批量样本的特征和标签)
    for X, y in data_iter(batch_size, features, labels):
        l = loss(net(X, w, b), y).sum()  # l是有关小批量X和y的损失
        l.backward()  # 小批量的损失对模型参数求梯度
        sgd([w, b], lr, batch_size)  # 使用小批量随机梯度下降迭代模型参数

        # 不要忘了梯度清零
        w.grad.data.zero_()
        b.grad.data.zero_()

    train_l = loss(net(features, w, b), labels)
    print('epoch %d, loss %f' % (epoch + 1, train_l.mean().item()))

print(true_w, '\n', w)
print(true_b, '\n', b)
#output
epoch 1, loss 0.035728
epoch 2, loss 0.000133
epoch 3, loss 0.000049
[2, -3.4] 
 tensor([[ 1.9999],
        [-3.3998]], requires_grad=True)
4.2 
 tensor([4.1997], requires_grad=True)

线性回归的简洁实现

生成数据集(同上)
读取数据

PyTorch提供了 data包来读取数据。由于 data常用作变量名,我们将导入的 data模块用 Data代替。在每一次迭代中,我们将随机读取包含10个数据样本的小批量

import torch.utils.data as Data
batch_size = 10
dataset = Data.TensorDataset(features, labels)# 将训练数据的特征和标签组合
data_iter = Data.DataLoader(dataset, batch_size, shuffle=True)# 随机读取小批量
定义模型

在上一节中,我们需要定义模型参数,并使用它们一步步描述模型是怎样计算的。当模型结构变得更复杂时,这些步骤将变得更繁琐。其实,PyTorch提供了大量预定义的层,这使我们只需关注使用哪些层来构造模型。下面将介绍如何使用PyTorch更简洁地定义线性回归。

首先,导入 torch.nn模块。实际上,“nn”是neural networks(神经网络)的缩写。顾名思义,该模块定义了大量神经网络的层。之前我们已经用过了 autograd,而 nn就是利用 autograd来定义模型。nn的核心数据结构是 Module,它是一个抽象概念,既可以表示神经网络中的某个层(layer),也可以表示一个包含很多层的神经网络。在实际使用中,最常见的做法是继承 nn.Module,撰写自己的网络/层。一个 nn.Module实例应该包含一些层以及返回输出的前向传播(forward)方法。下面先来看看如何用 nn.Module实现一个线性回归模型

class LinearNet(nn.Module):
    def __init__(self, n_feature):
        super(LinearNet, self).__init__()
        self.linear = nn.Linear(n_feature, 1)
    # forward 定义前向传播
    def forward(self, x):
        y = self.linear(x)
        return y

net = LinearNet(num_inputs)
print(net) # 使用print可以打印出网络的结构

#output
LinearNet(
  (linear): Linear(in_features=2, out_features=1, bias=True)
)

事实上我们还可以用 nn.Sequential来更加方便地搭建网络,Sequential是一个有序的容器,网络层将按照在传入 Sequential的顺序依次被添加到计算图中

# 写法一
net = nn.Sequential(
    nn.Linear(num_inputs, 1)
    # 此处还可以传入其他层
    )

# 写法二
net = nn.Sequential()
net.add_module('linear', nn.Linear(num_inputs, 1))
# net.add_module ......

# 写法三
from collections import OrderedDict
net = nn.Sequential(OrderedDict([
          ('linear', nn.Linear(num_inputs, 1))
          # ......
        ]))

print(net)
print(net[0])

#output
Sequential(
  (linear): Linear(in_features=2, out_features=1, bias=True)
)
Linear(in_features=2, out_features=1, bias=True)

可以通过 net.parameters()来查看模型所有的可学习参数,此函数将返回一个生成器

for param in net.parameters():
    print(param)
#output
Parameter containing:
tensor([[-0.0277,  0.2771]], requires_grad=True)
Parameter containing:
tensor([0.3395], requires_grad=True)
初始化模型参数

在使用 net前,我们需要初始化模型参数,如线性回归模型中的权重和偏差。PyTorch在 init模块中提供了多种参数初始化方法。这里的 initinitializer的缩写形式。我们通过 init.normal_将权重参数每个元素初始化为随机采样于均值为0、标准差为0.01的正态分布。偏差会初始化为零

from torch.nn import init

init.normal_(net[0].weight, mean=0, std=0.01)
init.constant_(net[0].bias, val=0)  # 也可以直接修改bias的data: net[0].bias.data.fill_(0)
定义损失函数

PyTorch在 nn模块中提供了各种损失函数,这些损失函数可看作是一种特殊的层,PyTorch也将这些损失函数实现为 nn.Module的子类。我们现在使用它提供的均方误差损失作为模型的损失函数

loss = nn.MSELoss()
定义优化算法

同样,我们也无须自己实现小批量随机梯度下降算法。torch.optim模块提供了很多常用的优化算法比如SGD、Adam和RMSProp等。下面我们创建一个用于优化 net所有参数的优化器实例,并指定学习率为0.03的小批量随机梯度下降(SGD)为优化算法

import torch.optim as optim

optimizer = optim.SGD(net.parameters(), lr=0.03)
print(optimizer)

#output
SGD (
Parameter Group 0
    dampening: 0
    lr: 0.03
    momentum: 0
    nesterov: False
    weight_decay: 0
)

我们还可以为不同子网络设置不同的学习率,这在finetune时经常用到

optimizer =optim.SGD([
                # 如果对某个参数不指定学习率,就使用最外层的默认学习率
                {'params': net.subnet1.parameters()}, # lr=0.03
                {'params': net.subnet2.parameters(), 'lr': 0.01}
            ], lr=0.03)

有时候我们不想让学习率固定成一个常数,那如何调整学习率呢?主要有两种做法。一种是修改 optimizer.param_groups中对应的学习率,另一种是更简单也是较为推荐的做法——新建优化器,由于optimizer十分轻量级,构建开销很小,故而可以构建新的optimizer。但是后者对于使用动量的优化器(如Adam),会丢失动量等状态信息,可能会造成损失函数的收敛出现震荡等情况

# 调整学习率
for param_group in optimizer.param_groups:
    param_group['lr'] *= 0.1 # 学习率为之前的0.1倍
训练模型

在使用Gluon训练模型时,我们通过调用 optim实例的 step函数来迭代模型参数。按照小批量随机梯度下降的定义,我们在 step函数中指明批量大小,从而对批量中样本梯度求平均

num_epochs = 3
for epoch in range(1, num_epochs + 1):
    for X, y in data_iter:
        output = net(X)
        l = loss(output, y.view(-1, 1))
        optimizer.zero_grad() # 梯度清零,等价于net.zero_grad()
        l.backward()
        optimizer.step()
    print('epoch %d, loss: %f' % (epoch, l.item()))

#output
epoch 1, loss: 0.000457
epoch 2, loss: 0.000081
epoch 3, loss: 0.000198
完整代码
import torch
import numpy as np
from torch import nn
from torch.nn import init
import torch.utils.data as Data
import torch.optim as optim

'''生成数据集'''
num_inputs = 2  # 特征数(输入个数)为2
num_examples = 1000  # 样本数1000
true_w = [2, -3.4]  # 线性回归真实权重
true_b = 4.2  # 偏差
features = torch.randn(num_examples, num_inputs, dtype=torch.float32)  # 特征(就是x)
labels = true_w[0] * features[:, 0] + true_w[1] * features[:, 1] + true_b  # 标签(true_y)
labels += torch.tensor(np.random.normal(0, 0.01, size=labels.size()), dtype=torch.float32)  # 加上噪声

'''读取数据:PyTorch提供了data包来读取数据。由于data常用作变量名,我们将导入的data模块用Data代替。在每一次迭代中,我们将随机读取包含10个数据样本的小批量'''
batch_size = 10
dataset = Data.TensorDataset(features, labels)  # 将训练数据的特征和标签组合
data_iter = Data.DataLoader(dataset, batch_size, shuffle=True)  # 随机读取小批量
'''TensorDataset是一个PyTorch提供的将数据集和标签组合在一起的工具,用于创建一个包含特征张量和标签张量的数据集。
在这个代码中,features是一个形状为(num_examples, num_inputs)的张量,代表有num_examples个样本,每个样本有num_inputs个特征。
labels是形状为(num_examples,)的张量,代表每个样本的标签。将这两个张量作为参数传递给TensorDataset,就会创建一个包含(features, labels)的数据集。

DataLoader是一个PyTorch提供的用于数据读取的工具,它可以从一个数据集中读取数据并生成一个迭代器,每次迭代返回一个小批量的数据。
这里的data_iter就是一个这样的迭代器,它从dataset中读取数据,每次返回batch_size个样本。
通过设置shuffle=True,数据在每个epoch之前会被打乱顺序,从而使模型更好地学习到样本之间的关系。'''


'''定义模型:'''
'''首先,导入torch.nn模块。实际上,“nn”是neural networks(神经网络)的缩写。顾名思义,该模块定义了大量神经网络的层。之前我们已经用过了autograd,而nn就是利用autograd来定义模型。
nn的核心数据结构是Module,它是一个抽象概念,既可以表示神经网络中的某个层(layer),也可以表示一个包含很多层的神经网络。
在实际使用中,最常见的做法是继承nn.Module,撰写自己的网络/层。一个nn.Module实例应该包含一些层以及返回输出的前向传播(forward)方法'''
net = nn.Sequential()  # 定义一个空的神经网络
net.add_module('linear', nn.Linear(num_inputs, 1))  # 加上一层,我们将线性层的名称定义为'linear',可以使用该名称来访问该层的权重和偏差

# 可以通过net.parameters()来查看模型所有的可学习参数,此函数将返回一个生成器。
# for param in net.parameters():
# print(param)

'''初始化模型参数'''
'''在使用net前,我们需要初始化模型参数,如线性回归模型中的权重和偏差。PyTorch在init模块中提供了多种参数初始化方法。
这里的init是initializer的缩写形式。我们通过init.normal_将权重参数每个元素初始化为随机采样于均值为0、标准差为0.01的正态分布。偏差会初始化为零'''
init.normal_(net[0].weight, mean=0, std=0.01)  # 对第一层权重进行初始化
init.constant_(net[0].bias, val=0)  # 对第一层的偏置向量进行常数初始化

'''定义损失函数'''
loss = nn.MSELoss()

'''定义优化算法'''
optimizer = optim.SGD(net.parameters(), lr=0.03)  # 学习率设为0.03
print(optimizer)

'''训练模型'''
'''在使用Gluon训练模型时,我们通过调用optim实例的step函数来迭代模型参数。按照小批量随机梯度下降的定义,我们在step函数中指明批量大小,从而对批量中样本梯度求平均'''
num_epochs = 3
for epoch in range(1, num_epochs + 1):
    for X, y in data_iter:  # 内层循环遍历数据集中的每个样本。其中,X 是特征,y 是标签
        output = net(X)  # 将样本 X 作为输入,通过网络 net 进行前向传播,得到模型的输出结果 output
        l = loss(output, y.view(-1, 1))
        optimizer.zero_grad()  # 梯度清零,等价于net.zero_grad()
        l.backward()
        optimizer.step()  # 优化器根据梯度自动调整模型参数,使得模型在这次迭代中的损失函数值下降
    print('epoch %d, loss: %f' % (epoch, l.item()))  # l.item() 表示损失函数的值(取一个标量)
# 验证
dense = net[0]
print(true_w, dense.weight)
print(true_b, dense.bias)

#output:
epoch 1, loss: 0.000131
epoch 2, loss: 0.000051
epoch 3, loss: 0.000058
[2, -3.4] Parameter containing:
tensor([[ 2.0000, -3.4002]], requires_grad=True)
4.2 Parameter containing:
tensor([4.2002], requires_grad=True)

softmax回归

线性回归模型适用于输出为连续值的情景。在另一类情景中,模型输出可以是一个像图像类别这样的离散值。对于这样的离散值预测问题,我们可以使用诸如softmax回归在内的分类模型。和线性回归不同,softmax回归的输出单元从一个变成了多个,且引入了softmax运算使输出更适合离散值的预测和训练。

分类问题

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4rQ13g0f-1687395257227)(image/手动深度学习/1683461988163.png)]

softmax回归模型

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3SY3czi0-1687395257227)(image/手动深度学习/1683462071244.png)]

单样本分类的矢量计算表达式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oE1aL8Gt-1687395257228)(image/手动深度学习/1683462410432.png)]

小批量样本分类的矢量计算表达式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-owUiYrX3-1687395257228)(image/手动深度学习/1683462535309.png)]

交叉熵损失函数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0blOP3te-1687395257229)(image/手动深度学习/1683462600749.png)]

图像分类数据集

图像分类数据集中最常用的是手写数字识别数据集MNIST。但大部分模型在MNIST上的分类精度都超过了95%。为了更直观地观察算法之间的差异,我们将使用一个图像内容更加复杂的数据集Fashion-MNIST(这个数据集也比较小,只有几十M,没有GPU的电脑也能吃得消)。

'''本节我们将使用torchvision包,它是服务于PyTorch深度学习框架的,主要用来构建计算机视觉模型。torchvision主要由以下几部分构成:'''
torchvision.datasets: 一些加载数据的函数及常用的数据集接口;
torchvision.models: 包含常用的模型结构(含预训练模型),例如AlexNet、VGG、ResNet等;
torchvision.transforms: 常用的图片变换,例如裁剪、旋转等;
torchvision.utils: 其他的一些有用的方法。
获取数据集

我们通过torchvision的 torchvision.datasets来下载这个数据集。第一次调用时会自动从网上获取数据。我们通过参数 train来指定获取训练数据集或测试数据集(testing data set)。测试数据集也叫测试集(testing set),只用来评价模型的表现,并不用来训练模型。

另外我们还指定了参数 transform = transforms.ToTensor()使所有数据转换为 Tensor,如果不进行转换则返回的是PIL图片。transforms.ToTensor()将尺寸为 (H x W x C) 且数据位于[0, 255]的PIL图片或者数据类型为 np.uint8的NumPy数组转换为尺寸为(C x H x W)且数据类型为 torch.float32且位于[0.0, 1.0]的 Tensor

mnist_train = torchvision.datasets.FashionMNIST(root='~/Datasets/FashionMNIST', train=True, download=True, transform=transforms.ToTensor())
mnist_test = torchvision.datasets.FashionMNIST(root='~/Datasets/FashionMNIST', train=False, download=True, transform=transforms.ToTensor())
'''上面的mnist_train和mnist_test都是torch.utils.data.Dataset的子类,所以我们可以用len()来获取该数据集的大小,还可以用下标来获取具体的一个样本。训练集中和测试集中的每个类别的图像数分别为6,000和1,000。因为有10个类别,所以训练集和测试集的样本数分别为60,000和10,000'''
print(type(mnist_train))
print(len(mnist_train), len(mnist_test))
#output
<class 'torchvision.datasets.mnist.FashionMNIST'>
60000 10000

'''可以通过下标来访问任意一个样本'''
feature, label = mnist_train[0]
print(feature.shape, label)  # Channel x Height x Width
#output
torch.Size([1, 28, 28]) 9

'''变量feature对应高和宽均为28像素的图像。由于我们使用了transforms.ToTensor(),所以每个像素的数值为[0.0, 1.0]的32位浮点数。需要注意的是,feature的尺寸是 (C x H x W) 的,而不是 (H x W x C)。第一维是通道数,因为数据集中是灰度图像,所以通道数为1。后面两维分别是图像的高和宽'''

Fashion-MNIST中一共包括了10个类别,分别为t-shirt(T恤)、trouser(裤子)、pullover(套衫)、dress(连衣裙)、coat(外套)、sandal(凉鞋)、shirt(衬衫)、sneaker(运动鞋)、bag(包)和ankle boot(短靴)。以下函数可以将数值标签转成相应的文本标签

def get_fashion_mnist_labels(labels):
    text_labels = ['t-shirt', 'trouser', 'pullover', 'dress', 'coat',
                   'sandal', 'shirt', 'sneaker', 'bag', 'ankle boot']
    return [text_labels[int(i)] for i in labels]

下面定义一个可以在一行里画出多张图像和对应标签的函数

def show_fashion_mnist(images, labels):
    d2l.use_svg_display()
    # 这里的_表示我们忽略(不使用)的变量
    _, figs = plt.subplots(1, len(images), figsize=(12, 12))
    for f, img, lbl in zip(figs, images, labels):
        f.imshow(img.view((28, 28)).numpy())
        f.set_title(lbl)
        f.axes.get_xaxis().set_visible(False)
        f.axes.get_yaxis().set_visible(False)
    plt.show()

现在,我们看一下训练数据集中前10个样本的图像内容和文本标签。

X, y = [], []
for i in range(10):
    X.append(mnist_train[i][0])
    y.append(mnist_train[i][1])
show_fashion_mnist(X, get_fashion_mnist_labels(y))

output:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-muwz3NK0-1687395257229)(image/手动深度学习/1683474022470.png)]

读取小批量

我们将在训练数据集上训练模型,并将训练好的模型在测试数据集上评价模型的表现。前面说过,mnist_traintorch.utils.data.Dataset的子类,所以我们可以将其传入 torch.utils.data.DataLoader来创建一个读取小批量数据样本的DataLoader实例。

在实践中,数据读取经常是训练的性能瓶颈,特别当模型较简单或者计算硬件性能较高时。PyTorch的 DataLoader中一个很方便的功能是允许使用多进程来加速数据读取。这里我们通过参数 num_workers来设置4个进程读取数据。

batch_size = 256
if sys.platform.startswith('win'):
    num_workers = 0  # 0表示不用额外的进程来加速读取数据
else:
    num_workers = 4
train_iter = torch.utils.data.DataLoader(mnist_train, batch_size=batch_size, shuffle=True, num_workers=num_workers)
test_iter = torch.utils.data.DataLoader(mnist_test, batch_size=batch_size, shuffle=False, num_workers=num_workers)

'''查看读取一遍训练数据需要的时间'''
start = time.time()
for X, y in train_iter:
    continue
print('%.2f sec' % (time.time() - start))

完整代码

import torch
import torchvision
import torchvision.transforms as transforms
import matplotlib.pyplot as plt
import d2lzh_pytorch as d2l
import sys
import time

'''
通过torchvision的torchvision.datasets来下载这个数据集
指定参数transform = transforms.ToTensor()使所有数据转换为Tensor
'''
mnist_train = torchvision.datasets.FashionMNIST(root='~/Datasets/FashionMNIST', train=True, download=True, transform=transforms.ToTensor())
mnist_test = torchvision.datasets.FashionMNIST(root='~/Datasets/FashionMNIST', train=False, download=True, transform=transforms.ToTensor())
'''
这两行代码是用于创建训练集和测试集的数据集对象,这里使用了FashionMNIST数据集,数据集已经在torchvision中预处理好了。
具体来说,这两行代码中的root参数指定了数据集存放的路径,train参数指定了是训练集还是测试集,download参数指定是否需要下载数据集(如果已经下载则可以设为False)
transform参数指定对数据集进行的变换,这里使用transforms.ToTensor()将数据转换为PyTorch中的Tensor类型。
'''

# print(type(mnist_train))
# print(len(mnist_train), len(mnist_test)) # len()来获取该数据集的大小
feature, label = mnist_train[0]  # 可以通过下标来访问任意一个样本
# print(feature.shape, label)  # Channel x Height x Width


def get_fashion_mnist_labels(labels):  # 将数值标签转成相应的文本标签
    text_labels = ['t-shirt', 'trouser', 'pullover', 'dress', 'coat',
                   'sandal', 'shirt', 'sneaker', 'bag', 'ankle boot']
    return [text_labels[int(i)] for i in labels]


def show_fashion_mnist(images, labels):  # 定义一个可以在一行里画出多张图像和对应标签的函数
    d2l.use_svg_display()
    _, figs = plt.subplots(1, len(images), figsize=(12, 12))  # 这里的_表示我们忽略(不使用)的变量
    for f, img, lbl in zip(figs, images, labels):
        f.imshow(img.view((28, 28)).numpy())
        f.set_title(lbl)
        f.axes.get_xaxis().set_visible(False)
        f.axes.get_yaxis().set_visible(False)
    plt.show()


'''
现在,我们看一下训练数据集中前10个样本的图像内容和文本标签
X, y = [], []
for i in range(10):
    X.append(mnist_train[i][0])
    y.append(mnist_train[i][1])
show_fashion_mnist(X, get_fashion_mnist_labels(y))
'''

'''
我们将在训练数据集上训练模型,并将训练好的模型在测试数据集上评价模型的表现。前面说过,mnist_train是torch.utils.data.Dataset的子类,
所以我们可以将其传入torch.utils.data.DataLoader来创建一个读取小批量数据样本的DataLoader实例。
在实践中,数据读取经常是训练的性能瓶颈,特别当模型较简单或者计算硬件性能较高时。PyTorch的DataLoader中一个很方便的功能是允许使用多进程来加速数据读取。
这里我们通过参数num_workers来设置4个进程读取数据。
'''
batch_size = 256
if sys.platform.startswith('win'):  # 判断是不是windows系统
    num_workers = 0  # 0表示不用额外的进程来加速读取数据
else:
    num_workers = 4
train_iter = torch.utils.data.DataLoader(mnist_train, batch_size=batch_size, shuffle=True, num_workers=num_workers)  # 创建训练和测试数据的迭代器
test_iter = torch.utils.data.DataLoader(mnist_test, batch_size=batch_size, shuffle=False, num_workers=num_workers)
'''查看读取一遍训练数据需要的时间'''
start = time.time()
for X, y in train_iter:
    continue
print('%.2f sec' % (time.time() - start))


softmax回归的从零开始实现

导入本节实现所需的包或模块
import torch
import torchvision
import numpy as np
import sys
import d2lzh_pytorch as d2l
获取和读取数据

我们将使用Fashion-MNIST数据集,并设置批量大小为256

batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)

初始化模型参数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KgawM6Ac-1687395257230)(image/手动深度学习/1683529763001.png)]

num_inputs = 784
num_outputs = 10

W = torch.tensor(np.random.normal(0, 0.01, (num_inputs, num_outputs)), dtype=torch.float)
b = torch.zeros(num_outputs, dtype=torch.float)
'''同之前一样,我们需要模型参数梯度'''
W.requires_grad_(requires_grad=True)
b.requires_grad_(requires_grad=True) 

实现softmax运算

在介绍如何定义softmax回归之前,我们先描述一下对如何对多维 Tensor按维度操作。在下面的例子中,给定一个 Tensor矩阵 X。我们可以只对其中同一列(dim=0)或同一行(dim=1)的元素求和,并在结果中保留行和列这两个维度(keepdim=True

X = torch.tensor([[1, 2, 3], [4, 5, 6]])
print(X.sum(dim=0, keepdim=True))
print(X.sum(dim=1, keepdim=True))

下面我们就可以定义前面小节里介绍的softmax运算了。在下面的函数中,矩阵 X的行数是样本数,列数是输出个数。为了表达样本预测各个输出的概率,softmax运算会先通过 exp函数对每个元素做指数运算,再对 exp矩阵同行元素求和,最后令矩阵每行各元素与该行元素之和相除。这样一来,最终得到的矩阵每行元素和为1且非负。因此,该矩阵每行都是合法的概率分布。softmax运算的输出矩阵中的任意一行元素代表了一个样本在各个输出类别上的预测概率

def softmax(X):
    X_exp = X.exp()
    partition = X_exp.sum(dim=1, keepdim=True)
    return X_exp / partition  # 这里应用了广播机制

可以看到,对于随机输入,我们将每个元素变成了非负数,且每一行和为1

X = torch.rand((2, 5))
X_prob = softmax(X)
print(X_prob, X_prob.sum(dim=1))
#output
tensor([[0.2206, 0.1520, 0.1446, 0.2690, 0.2138],
        [0.1540, 0.2290, 0.1387, 0.2019, 0.2765]]) tensor([1., 1.])

定义模型

这里通过 view函数将每张原始图像改成长度为 num_inputs的向量

def net(X):
    return softmax(torch.mm(X.view((-1, num_inputs)), W) + b)

定义损失函数

为了得到标签的预测概率,我们可以使用 gather函数。在下面的例子中,变量 y_hat是2个样本在3个类别的预测概率,变量 y是这2个样本的标签类别。通过使用 gather函数,我们得到了2个样本的标签的预测概率。

y_hat = torch.tensor([[0.1, 0.3, 0.6], [0.3, 0.2, 0.5]])
y = torch.LongTensor([0, 2])
y_hat.gather(1, y.view(-1, 1))
#output
tensor([[0.1000],
        [0.5000]])

下面实现了交叉熵损失函数

def cross_entropy(y_hat, y):
    return - torch.log(y_hat.gather(1, y.view(-1, 1)))

计算分类准确率

给定一个类别的预测概率分布 y_hat,我们把预测概率最大的类别作为输出类别。如果它与真实类别 y一致,说明这次预测是正确的。分类准确率即正确预测数量与总预测数量之比。

为了演示准确率的计算,下面定义准确率 accuracy函数。其中 y_hat.argmax(dim=1)返回矩阵 y_hat每行中最大元素的索引,且返回结果与变量 y形状相同。相等条件判断式 (y_hat.argmax(dim=1) == y)是一个类型为 ByteTensorTensor,我们用 float()将其转换为值为0(相等为假)或1(相等为真)的浮点型 Tensor

def accuracy(y_hat, y):
    return (y_hat.argmax(dim=1) == y).float().mean().item()

类似地,我们可以评价模型 net在数据集 data_iter上的准确率

def evaluate_accuracy(data_iter, net):
    acc_sum, n = 0.0, 0
    for X, y in data_iter:
        acc_sum += (net(X).argmax(dim=1) == y).float().sum().item()
        n += y.shape[0]
    return acc_sum / n

训练模型

训练softmax回归的实现跟3.2(线性回归的从零开始实现)一节介绍的线性回归中的实现非常相似。我们同样使用小批量随机梯度下降来优化模型的损失函数。在训练模型时,迭代周期数 num_epochs和学习率 lr都是可以调的超参数。改变它们的值可能会得到分类更准确的模型。

num_epochs, lr = 5, 0.1

def train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size,
              params=None, lr=None, optimizer=None):
    for epoch in range(num_epochs):
        train_l_sum, train_acc_sum, n = 0.0, 0.0, 0
        for X, y in train_iter:
            y_hat = net(X)
            l = loss(y_hat, y).sum()

            if optimizer is not None: # 梯度清零
                optimizer.zero_grad()
            elif params is not None and params[0].grad is not None:
                for param in params:
                    param.grad.data.zero_()

            l.backward()
            if optimizer is None:
                d2l.sgd(params, lr, batch_size)
            else:
                optimizer.step()  # “softmax回归的简洁实现”一节将用到

            train_l_sum += l.item()
            train_acc_sum += (y_hat.argmax(dim=1) == y).sum().item()
            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))

train_ch3(net, train_iter, test_iter, cross_entropy, num_epochs, batch_size, [W, b], lr)

预测

训练完成后,现在就可以演示如何对图像进行分类了。给定一系列图像(第三行图像输出),我们比较一下它们的真实标签(第一行文本输出)和模型预测结果(第二行文本输出)。

X, y = iter(test_iter).next()

true_labels = d2l.get_fashion_mnist_labels(y.numpy())
pred_labels = d2l.get_fashion_mnist_labels(net(X).argmax(dim=1).numpy())
titles = [true + '\n' + pred for true, pred in zip(true_labels, pred_labels)]

d2l.show_fashion_mnist(X[0:9], titles[0:9])

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bLOgDJN7-1687395257230)(image/手动深度学习/1683530253069.png)]

完整代码

import torch
import torchvision
import numpy as np
import sys
import d2lzh_pytorch as d2l

'''读取和获取数据:使用Fashion-MNIST数据集,并设置批量大小为256'''
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)

'''
初始化模型参数:
使用向量表示每个样本。已知每个样本输入是高和宽均为28像素的图像。模型的输入向量的长度是 28×28=784
28×28=784:该向量的每个元素对应图像中每个像素。由于图像有10个类别,单层神经网络输出层的输出个数为10,因此softmax回归的权重和偏差参数分别为784×10
784×10和1×10
1×10的矩阵
'''
num_inputs = 784  # 设置输入层神经元数量(28*28)
num_outputs = 10  # 设置输出层神经元数量(十个类别)
W = torch.tensor(np.random.normal(0, 0.01, (num_inputs, num_outputs)), dtype=torch.float)  # 生成随机数,转化成tensor类型
b = torch.zeros(num_outputs, dtype=torch.float)  # 把偏置b初始化为全0向量
W.requires_grad_(requires_grad=True)  # 需要模型参数梯度
b.requires_grad_(requires_grad=True)

'''
实现softmax运算:
在介绍如何定义softmax回归之前,我们先描述一下对如何对多维Tensor按维度操作。
在下面的例子中,给定一个Tensor矩阵X。我们可以只对其中同一列(dim=0)或同一行(dim=1)的元素求和,并在结果中保留行和列这两个维度(keepdim=True)。
'''
# X = torch.tensor([[1, 2, 3], [4, 5, 6]])
# print(X.sum(dim=0, keepdim=True))
# print(X.sum(dim=1, keepdim=True))

'''
下面我们就可以定义前面小节里介绍的softmax运算了。在下面的函数中,矩阵X的行数是样本数,列数是输出个数。
为了表达样本预测各个输出的概率,softmax运算会先通过exp函数对每个元素做指数运算,再对exp矩阵同行元素求和,最后令矩阵每行各元素与该行元素之和相除。
这样一来,最终得到的矩阵每行元素和为1且非负。因此,该矩阵每行都是合法的概率分布。
softmax运算的输出矩阵中的任意一行元素代表了一个样本在各个输出类别上的预测概率。
'''

def softmax(X):
    X_exp = X.exp()  # 将输入张量 X 上的每个元素都取指数,得到一个新的张量 X_exp
    partition = X_exp.sum(dim=1, keepdim=True)  # 对于每行,计算所有元素之和,并保持二维形状以便广播
    return X_exp / partition  # 这里应用了广播机制

X = torch.rand((2, 5))
X_prob = softmax(X)
# print(X_prob, X_prob.sum(dim=1))

'''
定义模型:
有了softmax运算,我们可以定义上节描述的softmax回归模型了。
这里通过view函数将每张原始图像改成长度为num_inputs的向量。
'''
def net(X):
    return softmax(torch.mm(X.view((-1, num_inputs)), W) + b)
'''首先通过X.view((-1, num_inputs))将X重新变形为(batch_size, num_inputs)的形状,
然后将它与W相乘,再加上偏差b,得到一个(batch_size, num_outputs)的结果。
最后对这个结果使用softmax函数进行处理,并将处理后的结果返回'''

'''
定义损失函数:
上一节中,我们介绍了softmax回归使用的交叉熵损失函数。为了得到标签的预测概率,我们可以使用gather函数。
在下面的例子中,变量y_hat是2个样本在3个类别的预测概率,变量y是这2个样本的标签类别。通过使用gather函数,我们得到了2个样本的标签的预测概率。
与3.4节(softmax回归)数学表述中标签类别离散值从1开始逐一递增不同,在代码中,标签类别的离散值是从0开始逐一递增的。
'''
y_hat = torch.tensor([[0.1, 0.3, 0.6], [0.3, 0.2, 0.5]])  # 预测概率
y = torch.LongTensor([0, 2])  # 真实标签
y_hat.gather(1, y.view(-1, 1))

def cross_entropy(y_hat, y):  # 交叉熵损失函数
    return - torch.log(y_hat.gather(1, y.view(-1, 1)))

'''
计算分类准确率:
给定一个类别的预测概率分布y_hat,我们把预测概率最大的类别作为输出类别。
如果它与真实类别y一致,说明这次预测是正确的。分类准确率即正确预测数量与总预测数量之比。
为了演示准确率的计算,下面定义准确率accuracy函数。其中y_hat.argmax(dim=1)返回矩阵y_hat每行中最大元素的索引,且返回结果与变量y形状相同。
相等条件判断式(y_hat.argmax(dim=1) == y)是一个类型为ByteTensor的Tensor,我们用float()将其转换为值为0(相等为假)或1(相等为真)的浮点型Tensor。
'''
def accuracy(y_hat, y):
    return (y_hat.argmax(dim=1) == y).float().mean().item()  # y_hat.argmax(dim=1)获取预测中最大概率的类别
# print(accuracy(y_hat, y))

'''类似地,我们可以评价模型net在数据集data_iter上的准确率'''
def evaluate_accuracy(data_iter, net):
    acc_sum, n = 0.0, 0
    for X, y in data_iter:  # 遍历数据集中的每个样本,使用net(X)获取模型对该样本的预测结果,然后将预测结果与真实类别进行比较
        acc_sum += (net(X).argmax(dim=1) == y).float().sum().item()  # 比较结果被累加到acc_sum中
        n += y.shape[0]  # 获取数据集中样本的总数
    return acc_sum / n  # acc_sum被除以n,得到数据集上的平均准确率

# 因为我们随机初始化了模型net,所以这个随机模型的准确率应该接近于类别个数10的倒数即0.1
# print(evaluate_accuracy(test_iter, net))

'''
训练模型:
训练softmax回归的实现跟3.2(线性回归的从零开始实现)一节介绍的线性回归中的实现非常相似。
我们同样使用小批量随机梯度下降来优化模型的损失函数。
在训练模型时,迭代周期数num_epochs和学习率lr都是可以调的超参数。改变它们的值可能会得到分类更准确的模型
'''
num_epochs, lr = 5, 0.1

def train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size,
              params=None, lr=None, optimizer=None):
    for epoch in range(num_epochs):  # 先遍历整个训练集
        train_l_sum, train_acc_sum, n = 0.0, 0.0, 0
        for X, y in train_iter:
            y_hat = net(X)  # 对于每个小批量数据,将其输入模型得到预测结果 y_hat
            l = loss(y_hat, y).sum()  # 计算损失函数值l

            if optimizer is not None:  # 如果优化器不为空,则将优化器的梯度清零;否则,手动将参数的梯度清零
                optimizer.zero_grad()
            elif params is not None and params[0].grad is not None:
                for param in params:
                    param.grad.data.zero_()

            l.backward()
            if optimizer is None:
                d2l.sgd(params, lr, batch_size)
            else:
                optimizer.step()  # 计算损失函数的梯度,并使用优化器更新模型参数

            train_l_sum += l.item()
            train_acc_sum += (y_hat.argmax(dim=1) == y).sum().item()  # 累加训练集上的损失和准确率
            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))


train_ch3(net, train_iter, test_iter, cross_entropy, num_epochs, batch_size, [W, b], lr)
'''
epoch 1, loss 0.7909, train acc 0.745, test acc 0.791
epoch 2, loss 0.5713, train acc 0.813, test acc 0.810
epoch 3, loss 0.5256, train acc 0.826, test acc 0.818
epoch 4, loss 0.5011, train acc 0.833, test acc 0.820
epoch 5, loss 0.4852, train acc 0.837, test acc 0.829
'''

'''
预测:
训练完成后,现在就可以演示如何对图像进行分类了。给定一系列图像(第三行图像输出),我们比较一下它们的真实标签(第一行文本输出)和模型预测结果(第二行文本输出)
'''
X, y = next(iter(test_iter))

true_labels = d2l.get_fashion_mnist_labels(y.numpy())
pred_labels = d2l.get_fashion_mnist_labels(net(X).argmax(dim=1).numpy())
titles = [true + '\n' + pred for true, pred in zip(true_labels, pred_labels)]

d2l.show_fashion_mnist(X[0:9], titles[0:9])

softmax回归的简洁实现

读取和获取模型
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
定义和初始化模型

softmax回归的输出层是一个全连接层,所以我们用一个线性模块就可以了。因为前面我们数据返回的每个batch样本 x的形状为(batch_size, 1, 28, 28), 所以我们要先用 view()x的形状转换成(batch_size, 784)才送入全连接层

num_inputs = 784
num_outputs = 10

class LinearNet(nn.Module):
    def __init__(self, num_inputs, num_outputs):
        super(LinearNet, self).__init__()
        self.linear = nn.Linear(num_inputs, num_outputs)
    def forward(self, x): # x shape: (batch, 1, 28, 28)
        y = self.linear(x.view(x.shape[0], -1))
        return y

net = LinearNet(num_inputs, num_outputs)

我们将对 x的形状转换的这个功能自定义一个 FlattenLayer

class FlattenLayer(nn.Module):
    def __init__(self):
        super(FlattenLayer, self).__init__()
    def forward(self, x): # x shape: (batch, *, *, ...)
        return x.view(x.shape[0], -1)

这样我们就可以更方便地定义我们的模型

from collections import OrderedDict

net = nn.Sequential(
    # FlattenLayer(),
    # nn.Linear(num_inputs, num_outputs)
    OrderedDict([
        ('flatten', FlattenLayer()),
        ('linear', nn.Linear(num_inputs, num_outputs))
    ])
)

然后,我们使用均值为0、标准差为0.01的正态分布随机初始化模型的权重参数

init.normal_(net.linear.weight, mean=0, std=0.01)
init.constant_(net.linear.bias, val=0) 
损失函数

PyTorch提供了一个包括softmax运算和交叉熵损失计算的函数。它的数值稳定性更好

loss = nn.CrossEntropyLoss()
定义优化算法

我们使用学习率为0.1的小批量随机梯度下降作为优化算法

optimizer = torch.optim.SGD(net.parameters(), lr=0.1)
训练模型
num_epochs = 5
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size, None, None, optimizer)
#output
epoch 1, loss 0.0031, train acc 0.752, test acc 0.795
epoch 2, loss 0.0022, train acc 0.813, test acc 0.803
epoch 3, loss 0.0021, train acc 0.826, test acc 0.819
epoch 4, loss 0.0020, train acc 0.833, test acc 0.822
epoch 5, loss 0.0019, train acc 0.837, test acc 0.823

完整代码

import torch
from torch import nn
from torch.nn import init
import numpy as np
import sys
import d2lzh_pytorch as d2l
from collections import OrderedDict

'''读取数据:我们仍然使用Fashion-MNIST数据集和上一节中设置的批量大小'''
batch_size = 256  # 设置批量大小
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)  # 加载模型

'''
定义和初始化模型:
softmax回归的输出层是一个全连接层,所以我们用一个线性模块就可以了。因为前面我们数据返回的每个batch样本x的形状为(batch_size, 1, 28, 28), 
所以我们要先用view()将x的形状转换成(batch_size, 784)才送入全连接层
'''
num_inputs = 784  # 模型的输入为28x28的灰度图像,每个像素点取值范围为0-255,经过转换后被展平成一维向量,即784维。
num_outputs = 10  # 输出为一个10维向量,每一维对应数字0-9的概率

class LinearNet(nn.Module):
    def __init__(self, num_inputs, num_outputs):  # 定义构造函数
        super(LinearNet, self).__init__()
        self.linear = nn.Linear(num_inputs, num_outputs)  # 创建一个nn.Linear对象

    def forward(self, x):  # x shape: (batch, 1, 28, 28)
        y = self.linear(x.view(x.shape[0], -1))  # 将x作为参数传递给self.linear对象
        return y

net = LinearNet(num_inputs, num_outputs)  # 创建一个实例化对象

class FlattenLayer(nn.Module):  # 对x的形状转换的这个功能自定义一个FlattenLayer,用于将输入的数据展平
    def __init__(self):
        super(FlattenLayer, self).__init__()

    def forward(self, x): # x shape: (batch, *, *, ...)
        return x.view(x.shape[0], -1)  # 将输入张量x按照batch大小进行展平,返回展平后的结果

net = nn.Sequential(  # 定义神经网络(跟38行作用一样)
    # FlattenLayer(),
    # nn.Linear(num_inputs, num_outputs)
    OrderedDict([
        ('flatten', FlattenLayer()),
        ('linear', nn.Linear(num_inputs, num_outputs))
    ])
)

init.normal_(net.linear.weight, mean=0, std=0.01)  # 用均值为0、标准差为0.01的正态分布随机初始化模型的权重参数
init.constant_(net.linear.bias, val=0)

'''定义损失函数:PyTorch提供了一个包括softmax运算和交叉熵损失计算的函数'''
loss = nn.CrossEntropyLoss()

'''定义优化算法:使用学习率为0.1的小批量随机梯度下降作为优化算法'''
optimizer = torch.optim.SGD(net.parameters(), lr=0.1)

'''训练模型'''
num_epochs = 5
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size, None, None, optimizer)

多层感知机

隐藏层

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Zd0XQWxb-1687395257231)(image/手动深度学习/1683644422402.png)]

激活函数

上述问题的根源在于全连接层只是对数据做仿射变换(affine transformation),而多个仿射变换的叠加仍然是一个仿射变换。解决问题的一个方法是引入非线性变换,例如对隐藏变量使用按元素运算的非线性函数进行变换,然后再作为下一个全连接层的输入。这个非线性函数被称为激活函数(activation function)。下面我们介绍几个常用的激活函数。

ReLu函数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gf1lDe9S-1687395257231)(image/手动深度学习/1683644853754.png)]

import torch
import matplotlib.pylab as plt
import d2lzh_pytorch as d2l

def xyplot(x_vals, y_vals, name):
    d2l.set_figsize(figsize=(5, 2.5))
    d2l.plt.plot(x_vals.detach().numpy(), y_vals.detach().numpy())
    d2l.plt.xlabel('x')
    d2l.plt.ylabel(name + '(x)')

'''接下来通过Tensor提供的relu函数来绘制ReLU函数。可以看到,该激活函数是一个两段线性函数'''
x = torch.arange(-8.0, 8.0, 0.1, requires_grad=True)
y = x.relu()  # 调用函数
xyplot(x, y, 'relu')
plt.show()  # 记得调用这个函数,不然显示不出来

'''
显然,当输入为负数时,ReLU函数的导数为0;当输入为正数时,ReLU函数的导数为1。
尽管输入为0时ReLU函数不可导,但是我们可以取此处的导数为0。下面绘制ReLU函数的导数
'''
y.sum().backward()
xyplot(x, x.grad, 'grad of relu')
plt.show()  # 记得调用这个函数,不然显示不出来

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4UnUhUOA-1687395257232)(image/手动深度学习/1683645659637.png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yi4Bsw4F-1687395257232)(image/手动深度学习/1683645670323.png)]

sigmoid函数

sigmoid函数可以将元素的值变换到0和1之间:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XNI8xe78-1687395257233)(image/手动深度学习/1683648474704.png)]

sigmoid函数在早期的神经网络中较为普遍,但它目前逐渐被更简单的ReLU函数取代。

import torch
import matplotlib.pylab as plt
import d2lzh_pytorch as d2l

def xyplot(x_vals, y_vals, name):
    d2l.set_figsize(figsize=(5, 2.5))
    d2l.plt.plot(x_vals.detach().numpy(), y_vals.detach().numpy())
    d2l.plt.xlabel('x')
    d2l.plt.ylabel(name + '(x)')

'''下面绘制了sigmoid函数。当输入接近0时,sigmoid函数接近线性变换'''
x = torch.arange(-8.0, 8.0, 0.1, requires_grad=True)
y = x.sigmoid()
# xyplot(x, y, 'sigmoid')
# plt.show()

'''依据链式法则,sigmoid函数的导数sigmoid′(x)=sigmoid(x)(1−sigmoid(x))'''
# x.grad.zero_()  # 下面绘制了sigmoid函数的导数
y.sum().backward()
xyplot(x, x.grad, 'grad of sigmoid')
plt.show()


sigmoid :[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-w4dq1Ebo-1687395257233)(image/手动深度学习/1683649330423.png)]

sigmoid’ : [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2t07GTcV-1687395257234)(image/手动深度学习/1683649339885.png)]

tanh函数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qgFn1MNG-1687395257234)(image/手动深度学习/1683649484580.png)]

import torch
import matplotlib.pylab as plt
import d2lzh_pytorch as d2l

def xyplot(x_vals, y_vals, name):
    d2l.set_figsize(figsize=(5, 2.5))
    d2l.plt.plot(x_vals.detach().numpy(), y_vals.detach().numpy())
    d2l.plt.xlabel('x')
    d2l.plt.ylabel(name + '(x)')

'''当输入接近0时,tanh函数接近线性变换。虽然该函数的形状和sigmoid函数的形状很像,但tanh函数在坐标系的原点上对称'''
x = torch.arange(-8.0, 8.0, 0.1, requires_grad=True)
y = x.tanh()
xyplot(x, y, "tanh")
plt.show()

'''下面绘制了tanh函数的导数。当输入为0时,tanh函数的导数达到最大值1;当输入越偏离0时,tanh函数的导数越接近0'''
x.grad.zero_()
y.sum().backward()
xyplot(x, x.grad, 'grad of tanh')
plt.show()

tanh :[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9DTIDs7w-1687395257234)(image/手动深度学习/1683649623550.png)]  tanh': [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IFYWDwPe-1687395257235)(image/手动深度学习/1683649632304.png)]

多层感知机

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mjBbDyOu-1687395257235)(image/手动深度学习/1683649720316.png)]

多层感知机的从0开始实现

import torch
import torchvision
import numpy as np
import sys
import d2lzh_pytorch as d2l
'''获取和读取数据:我们将使用Fashion-MNIST数据集,并设置批量大小为256'''
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)

'''
定义模型参数:
Fashion-MNIST数据集中图像形状为 28×28
28×28,类别数为10。本节中我们依然使用长度为 28×28=784
28×28=784 的向量表示每一张图像。因此,输入个数为784,输出个数为10。实验中,我们设超参数隐藏单元个数为256
'''
num_inputs, num_outputs, num_hiddens = 784, 10, 256

W1 = torch.tensor(np.random.normal(0, 0.01, (num_inputs, num_hiddens)), dtype=torch.float)  # 用随机数初始化第一个全连接层的权重矩阵W1
b1 = torch.zeros(num_hiddens, dtype=torch.float)  # 初始化第一个全连接层的偏置向量b1为0
W2 = torch.tensor(np.random.normal(0, 0.01, (num_hiddens, num_outputs)), dtype=torch.float)  # 同上
b2 = torch.zeros(num_outputs, dtype=torch.float)

params = [W1, b1, W2, b2]  # 将模型参数存储在列表params中
for param in params:
    param.requires_grad_(requires_grad=True)  # 模型参数的requires_grad属性设置为True,表示需要计算梯度

'''定义激活函数:使用基础的max函数来实现ReLU,而非直接调用relu函数'''
def relu(X):
    return torch.max(input=X, other=torch.tensor(0.0))

'''
定义模型:
同softmax回归一样,我们通过view函数将每张原始图像改成长度为num_inputs的向量。
然后我们实现上一节中多层感知机的计算表达式
'''
def net(X):  # 定义神经网络
    X = X.view((-1, num_inputs))
    H = relu(torch.matmul(X, W1) + b1)  # 计算第一个全连接层的输出,输入层到隐藏层的计算
    return torch.matmul(H, W2) + b2  # 隐藏层到输出层的计算

'''定义损失函数:为了得到更好的数值稳定性,我们直接使用PyTorch提供的包括softmax运算和交叉熵损失计算的函数'''
loss = torch.nn.CrossEntropyLoss()

'''
训练模型:
我们直接调用d2lzh_pytorch包中的train_ch3函数,我们在这里设超参数迭代周期数为5,学习率为100.0
'''
num_epochs, lr = 5, 100.0
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size, params, lr)


多层感知机的简洁实现

定义模型

和softmax回归唯一的不同在于,我们多加了一个全连接层作为隐藏层。它的隐藏单元个数为256,并使用ReLU函数作为激活函数

num_inputs, num_outputs, num_hiddens = 784, 10, 256

net = nn.Sequential(
        d2l.FlattenLayer(),
        nn.Linear(num_inputs, num_hiddens),
        nn.ReLU(),
        nn.Linear(num_hiddens, num_outputs), 
        )

for params in net.parameters():
    init.normal_(params, mean=0, std=0.01)
读取数据并训练模型
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
loss = torch.nn.CrossEntropyLoss()

optimizer = torch.optim.SGD(net.parameters(), lr=0.5)

num_epochs = 5
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size, None, None, optimizer)

#output
epoch 1, loss 0.0030, train acc 0.712, test acc 0.744
epoch 2, loss 0.0019, train acc 0.823, test acc 0.821
epoch 3, loss 0.0017, train acc 0.844, test acc 0.842
epoch 4, loss 0.0015, train acc 0.856, test acc 0.842
epoch 5, loss 0.0014, train acc 0.864, test acc 0.818

完整代码

import torch
from torch import nn
from torch.nn import init
import numpy as np
import sys
import d2lzh_pytorch as d2l

'''
定义模型
和softmax回归唯一的不同在于,我们多加了一个全连接层作为隐藏层。
它的隐藏单元个数为256,并使用ReLU函数作为激活函数
'''
num_inputs, num_outputs, num_hiddens = 784, 10, 256

net = nn.Sequential(
        d2l.FlattenLayer(),  # 将输入数据展平,将每个样本变成一个向量
        nn.Linear(num_inputs, num_hiddens),  # 一个全连接层,它将输入数据的每个特征连接到隐藏层的每个神经元
        nn.ReLU(),  # 使用ReLU函数作为激活函数,对隐藏层的输出进行非线性变换
        nn.Linear(num_hiddens, num_outputs),  # 一个全连接层,它将隐藏层的每个神经元连接到输出层的每个神经元
        )

for params in net.parameters():  # 对神经网络中的参数进行初始化
    init.normal_(params, mean=0, std=0.01)

'''读取数据并训练模型'''
batch_size = 256
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
loss = torch.nn.CrossEntropyLoss()

optimizer = torch.optim.SGD(net.parameters(), lr=0.5)

num_epochs = 5
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size, None, None, optimizer)

模型选择,欠拟合和过拟合

在前几节基于Fashion-MNIST数据集的实验中,我们评价了机器学习模型在训练数据集和测试数据集上的表现。如果你改变过实验中的模型结构或者超参数,你也许发现了:当模型在训练数据集上更准确时,它在测试数据集上却不一定更准确。这是为什么呢?

训练误差和泛化误差

在解释上述现象之前,我们需要区分训练误差(training error)和泛化误差(generalization error)。通俗来讲,前者指模型在训练数据集上表现出的误差,后者指模型在任意一个测试数据样本上表现出的误差的期望,并常常通过测试数据集上的误差来近似。计算训练误差和泛化误差可以使用之前介绍过的损失函数,例如线性回归用到的平方损失函数和softmax回归用到的交叉熵损失函数。

让我们以高考为例来直观地解释训练误差和泛化误差这两个概念。训练误差可以认为是做往年高考试题(训练题)时的错误率,泛化误差则可以通过真正参加高考(测试题)时的答题错误率来近似。假设训练题和测试题都随机采样于一个未知的依照相同考纲的巨大试题库。如果让一名未学习中学知识的小学生去答题,那么测试题和训练题的答题错误率可能很相近。但如果换成一名反复练习训练题的高三备考生答题,即使在训练题上做到了错误率为0,也不代表真实的高考成绩会如此。

在机器学习里,我们通常假设训练数据集(训练题)和测试数据集(测试题)里的每一个样本都是从同一个概率分布中相互独立地生成的。基于该独立同分布假设,给定任意一个机器学习模型(含参数),它的训练误差的期望和泛化误差都是一样的。例如,如果我们将模型参数设成随机值(小学生),那么训练误差和泛化误差会非常相近。但我们从前面几节中已经了解到,模型的参数是通过在训练数据集上训练模型而学习出的,参数的选择依据了最小化训练误差(高三备考生)。所以,训练误差的期望小于或等于泛化误差。也就是说,一般情况下,由训练数据集学到的模型参数会使模型在训练数据集上的表现优于或等于在测试数据集上的表现。由于无法从训练误差估计泛化误差,一味地降低训练误差并不意味着泛化误差一定会降低。

机器学习模型应关注降低泛化误差。

模型选择

在机器学习中,通常需要评估若干候选模型的表现并从中选择模型。这一过程称为模型选择(model selection)。可供选择的候选模型可以是有着不同超参数的同类模型。以多层感知机为例,我们可以选择隐藏层的个数,以及每个隐藏层中隐藏单元个数和激活函数。为了得到有效的模型,我们通常要在模型选择上下一番功夫。下面,我们来描述模型选择中经常使用的验证数据集(validation data set)。

验证数据集

从严格意义上讲,测试集只能在所有超参数和模型参数选定后使用一次。不可以使用测试数据选择模型,如调参。由于无法从训练误差估计泛化误差,因此也不应只依赖训练数据选择模型。鉴于此,我们可以预留一部分在训练数据集和测试数据集以外的数据来进行模型选择。这部分数据被称为验证数据集,简称验证集(validation set)。例如,我们可以从给定的训练集中随机选取一小部分作为验证集,而将剩余部分作为真正的训练集。

然而在实际应用中,由于数据不容易获取,测试数据极少只使用一次就丢弃。因此,实践中验证数据集和测试数据集的界限可能比较模糊。从严格意义上讲,除非明确说明,否则本书中实验所使用的测试集应为验证集,实验报告的测试结果(如测试准确率)应为验证结果(如验证准确率)。

k折交叉验证

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JLhnLtao-1687395257236)(image/手动深度学习/1683776660930.png)]

欠拟合和过拟合

接下来,我们将探究模型训练中经常出现的两类典型问题:一类是模型无法得到较低的训练误差,我们将这一现象称作欠拟合(underfitting);另一类是模型的训练误差远小于它在测试数据集上的误差,我们称该现象为过拟合(overfitting)。在实践中,我们要尽可能同时应对欠拟合和过拟合。虽然有很多因素可能导致这两种拟合问题,在这里我们重点讨论两个因素:模型复杂度和训练数据集大小。

模型复杂度

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bgoI4jKG-1687395257236)(image/手动深度学习/1683776975625.png)]

训练数据集大小

影响欠拟合和过拟合的另一个重要因素是训练数据集的大小。一般来说,如果训练数据集中样本数过少,特别是比模型参数数量(按元素计)更少时,过拟合更容易发生。此外,泛化误差不会随训练数据集里样本数量增加而增大。因此,在计算资源允许的范围之内,我们通常希望训练数据集大一些,特别是在模型复杂度较高时,例如层数较多的深度学习模型。

多项式函数拟合实验
生成数据集

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NUOneh9g-1687395257236)(image/手动深度学习/1683777226774.png)]

n_train, n_test, true_w, true_b = 100, 100, [1.2, -3.4, 5.6], 5
features = torch.randn((n_train + n_test, 1))
poly_features = torch.cat((features, torch.pow(features, 2), torch.pow(features, 3)), 1) 
labels = (true_w[0] * poly_features[:, 0] + true_w[1] * poly_features[:, 1]
          + true_w[2] * poly_features[:, 2] + true_b)
labels += torch.tensor(np.random.normal(0, 0.01, size=labels.size()), dtype=torch.float)
定义,训练和测试模型

我们先定义作图函数 semilogy,其中 y 轴使用了对数尺度

def semilogy(x_vals, y_vals, x_label, y_label, x2_vals=None, y2_vals=None,
             legend=None, figsize=(3.5, 2.5)):
    d2l.set_figsize(figsize)
    d2l.plt.xlabel(x_label)
    d2l.plt.ylabel(y_label)
    d2l.plt.semilogy(x_vals, y_vals)
    if x2_vals and y2_vals:
        d2l.plt.semilogy(x2_vals, y2_vals, linestyle=':')
        d2l.plt.legend(legend)

和线性回归一样,多项式函数拟合也使用平方损失函数。因为我们将尝试使用不同复杂度的模型来拟合生成的数据集,所以我们把模型定义部分放在 fit_and_plot函数中。多项式函数拟合的训练和测试步骤与3.6节(softmax回归的从零开始实现)介绍的softmax回归中的相关步骤类似

num_epochs, loss = 100, torch.nn.MSELoss()

def fit_and_plot(train_features, test_features, train_labels, test_labels):
    net = torch.nn.Linear(train_features.shape[-1], 1)
    # 通过Linear文档可知,pytorch已经将参数初始化了,所以我们这里就不手动初始化了

    batch_size = min(10, train_labels.shape[0])  
    dataset = torch.utils.data.TensorDataset(train_features, train_labels)
    train_iter = torch.utils.data.DataLoader(dataset, batch_size, shuffle=True)

    optimizer = torch.optim.SGD(net.parameters(), lr=0.01)
    train_ls, test_ls = [], []
    for _ in range(num_epochs):
        for X, y in train_iter:
            l = loss(net(X), y.view(-1, 1))
            optimizer.zero_grad()
            l.backward()
            optimizer.step()
        train_labels = train_labels.view(-1, 1)
        test_labels = test_labels.view(-1, 1)
        train_ls.append(loss(net(train_features), train_labels).item())
        test_ls.append(loss(net(test_features), test_labels).item())
    print('final epoch: train loss', train_ls[-1], 'test loss', test_ls[-1])
    semilogy(range(1, num_epochs + 1), train_ls, 'epochs', 'loss',
             range(1, num_epochs + 1), test_ls, ['train', 'test'])
    print('weight:', net.weight.data,
          '\nbias:', net.bias.data)
三阶多项式函数拟合(正常)

我们先使用与数据生成函数同阶的三阶多项式函数拟合。实验表明,这个模型的训练误差和在测试数据集的误差都较低。训练出的模型参数也接近真实值

fit_and_plot(poly_features[:n_train, :], poly_features[n_train:, :], 
            labels[:n_train], labels[n_train:])
#output
final epoch: train loss 0.0001063426025211811 test loss 8.885039278538898e-05
weight: tensor([[ 1.1973, -3.3998,  5.6003]]) 
bias: tensor([4.9989])
线性函数拟合(欠拟合)

我们再试试线性函数拟合。很明显,该模型的训练误差在迭代早期下降后便很难继续降低。在完成最后一次迭代周期后,训练误差依旧很高。线性模型在非线性模型(如三阶多项式函数)生成的数据集上容易欠拟合

fit_and_plot(features[:n_train, :], features[n_train:, :], labels[:n_train],
             labels[n_train:])
#output
final epoch: train loss 300.3141784667969 test loss 168.74282836914062
weight: tensor([[22.8361]]) 
bias: tensor([-0.3143])
训练样本不足(过拟合)

事实上,即便使用与数据生成模型同阶的三阶多项式函数模型,如果训练样本不足,该模型依然容易过拟合。让我们只使用两个样本来训练模型。显然,训练样本过少了,甚至少于模型参数的数量。这使模型显得过于复杂,以至于容易被训练数据中的噪声影响。在迭代过程中,尽管训练误差较低,但是测试数据集上的误差却很高。这是典型的过拟合现象

fit_and_plot(poly_features[0:2, :], poly_features[n_train:, :], labels[0:2],
             labels[n_train:])
#output
final epoch: train loss 0.3701784312725067 test loss 38.0950813293457
weight: tensor([[1.7402, 1.9848, 2.7818]]) 
bias: tensor([1.9855])

完整代码

import torch
import numpy as np
import sys
import matplotlib
import d2lzh_pytorch as d2l

'''生成数据集'''
n_train, n_test, true_w, true_b = 100, 100, [1.2, -3.4, 5.6], 5  # 训练数据集大小为100,测试数据集大小为100,真实的模型参数是[1.2, -3.4, 5.6]和5
features = torch.randn((n_train + n_test, 1))  # 生成一个大小为(n_train + n_test, 1)的特征矩阵,每个元素都是从标准正态分布中随机采样得到的。
poly_features = torch.cat((features, torch.pow(features, 2), torch.pow(features, 3)), 1)  # 生成一个大小为(n_train + n_test, 3)的多项式特征矩阵,每个元素都是从features中得到的元素的平方和立方
labels = (true_w[0] * poly_features[:, 0] + true_w[1] * poly_features[:, 1]  # 用真实的模型参数和多项式特征矩阵计算标签
          + true_w[2] * poly_features[:, 2] + true_b)
labels += torch.tensor(np.random.normal(0, 0.01, size=labels.size()), dtype=torch.float)
'''
看一看生成的数据集的前两个样本
var = features[:2], poly_features[:2], labels[:2]
print(var)
'''

'''定义,训练和测试模型'''
'''我们先定义作图函数semilogy,其中y轴使用了对数尺度'''
def semilogy(x_vals, y_vals, x_label, y_label, x2_vals=None, y2_vals=None,
             legend=None, figsize=(3.5, 2.5)):
    d2l.set_figsize(figsize)
    d2l.plt.xlabel(x_label)
    d2l.plt.ylabel(y_label)
    d2l.plt.semilogy(x_vals, y_vals)
    if x2_vals and y2_vals:
        d2l.plt.semilogy(x2_vals, y2_vals, linestyle=':')
        d2l.plt.legend(legend)
'''
和线性回归一样,多项式函数拟合也使用平方损失函数。
因为我们将尝试使用不同复杂度的模型来拟合生成的数据集,所以我们把模型定义部分放在fit_and_plot函数中。
多项式函数拟合的训练和测试步骤与3.6节(softmax回归的从零开始实现)介绍的softmax回归中的相关步骤类似
'''
num_epochs, loss = 100, torch.nn.MSELoss()

def fit_and_plot(train_features, test_features, train_labels, test_labels):  # 这个函数接受训练和测试数据集的特征和标签,并返回一个训练好的神经网络模型
    net = torch.nn.Linear(train_features.shape[-1], 1)  # 定义了一个包含一个线性层的神经网络模型,它的输入维度是训练特征的最后一个维度,输出维度为1
    # 通过Linear文档可知,pytorch已经将参数初始化了,所以我们这里就不手动初始化了

    batch_size = min(10, train_labels.shape[0])  # 创建了一个大小为batch_size的随机训练数据迭代器,用于将训练数据划分为小批量,以便进行随机梯度下降
    dataset = torch.utils.data.TensorDataset(train_features, train_labels)
    train_iter = torch.utils.data.DataLoader(dataset, batch_size, shuffle=True)

    optimizer = torch.optim.SGD(net.parameters(), lr=0.01)  # 优化器
    train_ls, test_ls = [], []
    for _ in range(num_epochs):  # num_epochs次循环,每次循环通过训练数据的小批量对神经网络模型进行更新,并记录训练和测试损失
        for X, y in train_iter:
            l = loss(net(X), y.view(-1, 1))
            optimizer.zero_grad()
            l.backward()
            optimizer.step()  # 更新模型参数,即执行一步梯度下降更新参数。
        train_labels = train_labels.view(-1, 1)
        test_labels = test_labels.view(-1, 1)  # 将训练标签和测试标签转换为列向量
        train_ls.append(loss(net(train_features), train_labels).item())
        test_ls.append(loss(net(test_features), test_labels).item())  # 分别计算训练集和测试集的损失,并将损失值加入到对应的列表train_ls和test_ls中
    print('final epoch: train loss', train_ls[-1], 'test loss', test_ls[-1])
    semilogy(range(1, num_epochs + 1), train_ls, 'epochs', 'loss',
             range(1, num_epochs + 1), test_ls, ['train', 'test'])
    print('weight:', net.weight.data,
          '\nbias:', net.bias.data)

'''三阶多项式函数拟合'''
fit_and_plot(poly_features[:n_train, :], poly_features[n_train:, :],
            labels[:n_train], labels[n_train:])

'''线性函数拟合(欠拟合)'''
fit_and_plot(features[:n_train, :], features[n_train:, :], labels[:n_train],
             labels[n_train:])

'''训练样本不足(过拟合)'''
fit_and_plot(poly_features[0:2, :], poly_features[n_train:, :], labels[0:2],
             labels[n_train:])


权重衰减

方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lg3XxPJJ-1687395257237)(image/手动深度学习/1683853194677.png)]

高维线性回归实验

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-77Czr05M-1687395257237)(image/手动深度学习/1683853362077.png)]

权重衰减从0开始实现
import matplotlib
import torch
import torch.nn as nn
import numpy as np
import sys
import d2lzh_pytorch as d2l

n_train, n_test, num_inputs = 20, 100, 200
true_w, true_b = torch.ones(num_inputs, 1) * 0.01, 0.05

features = torch.randn((n_train + n_test, num_inputs))
labels = torch.matmul(features, true_w) + true_b
labels += torch.tensor(np.random.normal(0, 0.01, size=labels.size()), dtype=torch.float)
train_features, test_features = features[:n_train, :], features[n_train:, :]
train_labels, test_labels = labels[:n_train], labels[n_train:]

'''初始化模型参数:首先,定义随机初始化模型参数的函数。该函数为每个参数都附上梯度'''
def init_params():
    w = torch.randn((num_inputs, 1), requires_grad=True)
    b = torch.zeros(1, requires_grad=True)
    return [w, b]

'''定义l2范数惩罚项'''
def l2_penalty(w):
    return (w**2).sum() / 2

'''定义训练和测试'''
batch_size, num_epochs, lr = 1, 100, 0.003
net, loss = d2l.linreg, d2l.squared_loss

dataset = torch.utils.data.TensorDataset(train_features, train_labels)
train_iter = torch.utils.data.DataLoader(dataset, batch_size, shuffle=True)

def fit_and_plot(lambd):  # 其中 lambd 是一个参数,用于控制 L2 范数的惩罚力度
    w, b = init_params()  # 返回参数 w 和 b 的初始化值
    train_ls, test_ls = [], []  # 创建了空列表 train_ls 和 test_ls 用于存储训练和测试集的损失函数值
    for _ in range(num_epochs):
        for X, y in train_iter:
            # 添加了L2范数惩罚项
            l = loss(net(X, w, b), y) + lambd * l2_penalty(w)
            l = l.sum()  # 通过调用l.sum()将其所有元素求和为一个标量值。这是为了得到一个平均的损失值,用于监测训练进程
            if w.grad is not None:
                w.grad.data.zero_()
                b.grad.data.zero_()
            l.backward()
            d2l.sgd([w, b], lr, batch_size)
        train_ls.append(loss(net(train_features, w, b), train_labels).mean().item())
        test_ls.append(loss(net(test_features, w, b), test_labels).mean().item())  # 计算训练集和测试集上的损失值,并将其添加到相应的列表train_ls和test_ls中
    d2l.semilogy(range(1, num_epochs + 1), train_ls, 'epochs', 'loss',
                 range(1, num_epochs + 1), test_ls, ['train', 'test'])  # 用两个集合里的点来画图
    print('L2 norm of w:', w.norm().item())

'''观察过拟合'''
fit_and_plot(lambd=0)

'''使用权重衰减'''
fit_and_plot(lambd=3)

#output
# L2 norm of w: 12.395933151245117
# L2 norm of w: 0.03533049672842026

过拟合:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0WOTDnfp-1687395257238)(image/手动深度学习/1683854760690.png)]

添加正则化:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UmHOV2tT-1687395257238)(image/手动深度学习/1683854775726.png)]

简洁实现
import matplotlib
import torch
import torch.nn as nn
import numpy as np
import sys
import d2lzh_pytorch as d2l

n_train, n_test, num_inputs = 20, 100, 200
true_w, true_b = torch.ones(num_inputs, 1) * 0.01, 0.05

features = torch.randn((n_train + n_test, num_inputs))
labels = torch.matmul(features, true_w) + true_b
labels += torch.tensor(np.random.normal(0, 0.01, size=labels.size()), dtype=torch.float)
train_features, test_features = features[:n_train, :], features[n_train:, :]
train_labels, test_labels = labels[:n_train], labels[n_train:]

'''初始化模型参数:首先,定义随机初始化模型参数的函数。该函数为每个参数都附上梯度'''
def init_params():
    w = torch.randn((num_inputs, 1), requires_grad=True)
    b = torch.zeros(1, requires_grad=True)
    return [w, b]

'''定义l2范数惩罚项'''
def l2_penalty(w):
    return (w**2).sum() / 2

'''定义训练和测试'''
batch_size, num_epochs, lr = 1, 100, 0.003
net, loss = d2l.linreg, d2l.squared_loss

dataset = torch.utils.data.TensorDataset(train_features, train_labels)
train_iter = torch.utils.data.DataLoader(dataset, batch_size, shuffle=True)

def fit_and_plot_pytorch(wd):
    # 对权重参数衰减。权重名称一般是以weight结尾
    net = nn.Linear(num_inputs, 1)
    nn.init.normal_(net.weight, mean=0, std=1)
    nn.init.normal_(net.bias, mean=0, std=1)
    optimizer_w = torch.optim.SGD(params=[net.weight], lr=lr, weight_decay=wd) # 对权重参数衰减
    optimizer_b = torch.optim.SGD(params=[net.bias], lr=lr)  # 不对偏差参数衰减

    train_ls, test_ls = [], []
    for _ in range(num_epochs):
        for X, y in train_iter:
            l = loss(net(X), y).mean()
            optimizer_w.zero_grad()
            optimizer_b.zero_grad()
            l.backward()
            # 对两个optimizer实例分别调用step函数,从而分别更新权重和偏差
            optimizer_w.step()
            optimizer_b.step()
        train_ls.append(loss(net(train_features), train_labels).mean().item())
        test_ls.append(loss(net(test_features), test_labels).mean().item())
    d2l.semilogy(range(1, num_epochs + 1), train_ls, 'epochs', 'loss',
                 range(1, num_epochs + 1), test_ls, ['train', 'test'])
    print('L2 norm of w:', net.weight.data.norm().item())

'''观察过拟合'''
fit_and_plot_pytorch(0)

'''使用权重衰减'''
fit_and_plot_pytorch(3)

#output
L2 norm of w: 13.874862670898438
L2 norm of w: 0.049207866191864014
输出的图像跟上面差不多

丢弃法

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

方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-OjDs3D1h-1687395257239)(image/手动深度学习/1684148931156.png)]

从0开始实现

import torch
import torch.nn as nn
import numpy as np
import sys
import d2lzh_pytorch as d2l

def dropout(X, drop_prob):  # 以drop_prob的概率丢弃X中的元素
    X = X.float()
    assert 0 <= drop_prob <= 1  # 断言,如果不在范围内抛出异常,确保传递参数范围[0,1]
    keep_prob = 1 - drop_prob  # 算出不丢弃的概率
    if keep_prob == 0:  # 这种情况下把全部元素都丢弃-dropout生效
        return torch.zeros_like(X)  # 返回一个与 X 相同形状的全 0 张量
    mask = (torch.rand(X.shape) < keep_prob).float()  # mask是随机变量
    return mask * X / keep_prob  # 套公式

'''定义模型参数:定义一个包含两个隐藏层的多层感知机,其中两个隐藏层的输出个数都是256'''
num_inputs, num_outputs, num_hiddens1, num_hiddens2 = 784, 10, 256, 256

W1 = torch.tensor(np.random.normal(0, 0.01, size=(num_inputs, num_hiddens1)), dtype=torch.float, requires_grad=True)
b1 = torch.zeros(num_hiddens1, requires_grad=True)
W2 = torch.tensor(np.random.normal(0, 0.01, size=(num_hiddens1, num_hiddens2)), dtype=torch.float, requires_grad=True)
b2 = torch.zeros(num_hiddens2, requires_grad=True)
W3 = torch.tensor(np.random.normal(0, 0.01, size=(num_hiddens2, num_outputs)), dtype=torch.float, requires_grad=True)
b3 = torch.zeros(num_outputs, requires_grad=True)

params = [W1, b1, W2, b2, W3, b3]

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

def net(X, is_training=True):
    X = X.view(-1, num_inputs)
    H1 = (torch.matmul(X, W1) + b1).relu()  # 1
    if is_training:  # 只在训练模型时使用丢弃法
        H1 = dropout(H1, drop_prob1)  # 在第一层全连接后添加丢弃层
    H2 = (torch.matmul(H1, W2) + b2).relu()  # 2
    if is_training:
        H2 = dropout(H2, drop_prob2)  # 在第二层全连接后添加丢弃层
    return torch.matmul(H2, W3) + b3  # 输出

'''训练和测试模型:类似于多层感知机'''
num_epochs, lr, batch_size = 5, 100.0, 256
loss = torch.nn.CrossEntropyLoss()
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size, params, lr)

#output
epoch 1, loss 0.0045, train acc 0.553, test acc 0.629
epoch 2, loss 0.0023, train acc 0.786, test acc 0.745
epoch 3, loss 0.0019, train acc 0.821, test acc 0.799
epoch 4, loss 0.0018, train acc 0.838, test acc 0.846
epoch 5, loss 0.0016, train acc 0.850, test acc 0.847

简洁实现(主要改了net代码)

import torch
import torch.nn as nn
import numpy as np
import sys
import d2lzh_pytorch as d2l

def dropout(X, drop_prob):  # 以drop_prob的概率丢弃X中的元素
    X = X.float()
    assert 0 <= drop_prob <= 1  # 断言,如果不在范围内抛出异常,确保传递参数范围[0,1]
    keep_prob = 1 - drop_prob  # 算出不丢弃的概率
    if keep_prob == 0:  # 这种情况下把全部元素都丢弃-dropout生效
        return torch.zeros_like(X)  # 返回一个与 X 相同形状的全 0 张量
    mask = (torch.rand(X.shape) < keep_prob).float()  # mask是随机变量
    return mask * X / keep_prob  # 套公式

'''定义模型参数:定义一个包含两个隐藏层的多层感知机,其中两个隐藏层的输出个数都是256'''
num_inputs, num_outputs, num_hiddens1, num_hiddens2 = 784, 10, 256, 256

W1 = torch.tensor(np.random.normal(0, 0.01, size=(num_inputs, num_hiddens1)), dtype=torch.float, requires_grad=True)
b1 = torch.zeros(num_hiddens1, requires_grad=True)
W2 = torch.tensor(np.random.normal(0, 0.01, size=(num_hiddens1, num_hiddens2)), dtype=torch.float, requires_grad=True)
b2 = torch.zeros(num_hiddens2, requires_grad=True)
W3 = torch.tensor(np.random.normal(0, 0.01, size=(num_hiddens2, num_outputs)), dtype=torch.float, requires_grad=True)
b3 = torch.zeros(num_outputs, requires_grad=True)

params = [W1, b1, W2, b2, W3, b3]

'''
定义模型:
下面定义的模型将全连接层和激活函数ReLU串起来,并对每个激活函数的输出使用丢弃法。我们可以分别设置各个层的丢弃概率。通常的建议是把靠近输入层的丢弃概率设得小一点。
在这个实验中,我们把第一个隐藏层的丢弃概率设为0.2,把第二个隐藏层的丢弃概率设为0.5。
'''
drop_prob1, drop_prob2 = 0.2, 0.5

net = nn.Sequential(         # 这样写多简洁QWQ
        d2l.FlattenLayer(),
        nn.Linear(num_inputs, num_hiddens1),
        nn.ReLU(),
        nn.Dropout(drop_prob1),
        nn.Linear(num_hiddens1, num_hiddens2),
        nn.ReLU(),
        nn.Dropout(drop_prob2),
        nn.Linear(num_hiddens2, 10)  # 在训练模型时,Dropout层将以指定的丢弃概率随机丢弃上一层的输出元素;
                                     # 在测试模型时(即model.eval()后),Dropout层并不发挥作用
        )

for param in net.parameters():
    nn.init.normal_(param, mean=0, std=0.01)


'''训练和测试模型:类似于多层感知机'''
num_epochs, lr, batch_size = 5, 100.0, 256
loss = torch.nn.CrossEntropyLoss()
train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
optimizer = torch.optim.SGD(net.parameters(), lr=0.5)
d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, batch_size, None, None, optimizer)


正向传播,反向传播和计算图

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

正向传播

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AoqPOGFQ-1687395257239)(image/手动深度学习/1684152005037.png)]

正向传播的计算图

在这里插入图片描述

反向传播

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b8o8jLOg-1687395257240)(image/手动深度学习/1684153279238.png)]

训练深度学习模型

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-slSFLkIw-1687395257241)(image/手动深度学习/1684153342602.png)]

数值稳定性和模型初始化

衰减和爆炸

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qytCtzVY-1687395257241)(image/手动深度学习/1684754404723.png)]

随机初始化模型参数

**![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0TmfHl1k-1687395257242)(image/手动深度学习/1684754452330.png)]](https://img-blog.csdnimg.cn/328026a9358843e5bc78a0bbfb602746.png)
**

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值