学习笔记:深度学习(4)——卷积神经网络(CNN)PyTorch实践篇

学习时间:2022.04.12~2022.04.14
环境配置:Anaconda(Python 3.8)+ PyCharm

3. 卷积神经网络CNN

接上文:学习笔记:深度学习(3)——卷积神经网络(CNN)理论篇

3.5 使用PyTorch构建一个深度学习模型

本节大致有三部分,首先是PyTorch构建模型的一个概括性的了解;然后是使用PyTorch做一个CV的简单项目;最后尝试一下用CNN做Titanic的预测。
本部分主要来源:构建一个深度学习模型需要哪几步?一小时学会pytorchquickstart_tutorial
PyTorch:它是一个基于python的科学计算库,致力于为两类用户提供服务:

  • 一些想要找到Numpy搭建神经网络替代品的用户;
  • 寻找一个可提供极强可拓展性和运行速度的深度学习研究平台。

3.5.1 张量(tensor)

PyTorch中的张量(tensor)和Numpy中N维数组(ndarrays)的概念很相似,有了这个作为基础,张量也可以被运行在GPU上来加速计算。

张量、矩阵和向量区别:
首先,张量的维数等价于张量的阶数。
0维的张量就是标量,1维的张量就是向量,2维的张量就是矩阵,大于等于3维的张量没有名称,统一叫做张量。下面举例:

  • 标量:很简单,就是一个数,1,2,5,108等等
  • 向量:[1,2],[1,2,3],[1,2,3,4],[3,5,67,·······,n]都是向量
  • 矩阵:[[1,3],[3,5]],[[1,2,3],[2,3,4],[3,4,5]],[[4,5,6,7,8],[3,4,7,8,9],[2,11,34,56,18]]是矩阵
  • 3维张量:[[[1,2],[3,4]],[[1,2],[3,4]]]

但是混淆的地方来了,就是数学里面会使用3维向量,n维向量的说法,这其实指的是1维张量(即向量)的形状,即它所含分量的个数,
比如[1,3]这个向量的维数为2,它有1和3这两个分量;[1,2,3,······,4096]这个向量的维数为4096,它有1、2······4096这4096个分量,
都是说的向量的形状。你不能说[1,3]这个“张量”的维数是2,只能说[1,3]这个“1维张量”的维数是2。
维度要看张量的最左边有多少个左中括号,有n个,则这个张量就是n维张量

(1)张量的生成

  1. torch.empty创建一个填充了未初始化数据的张量。(并非全零)
    torch.empty(*size, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False, pin_memory=False, memory_format=torch.contiguous_format)
    其中,size的指定方式是类似(1, 2, 3, ……)的形式,第1个数字代表最外层的数量,第2个数字代表次外层的数量,第3个数字代表第三层的数量……
    dtype指定张量的所需数据类型(形式是“torch.dtype”);device指定张量的所需设备(默认即根据当前设备设置)
import torch
x = torch.empty(5, 3)
print(x)
'''
tensor([[2.7712e+35, 4.5886e-41, 7.2927e-04],
        [3.0780e-41, 3.8725e+35, 4.5886e-41],
        [4.4446e-17, 4.5886e-41, 3.9665e+35],
        [4.5886e-41, 3.9648e+35, 4.5886e-41],
        [3.8722e+35, 4.5886e-41, 4.4446e-17]])
'''
  1. torch.rand生成一个初始化的、均匀分布的、每个元素从0~1的张量。
    torch.rand(*size, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False)
x = torch.rand(2, 3, 2)
print(x)
'''
tensor([[[0.7350, 0.1058],
         [0.1558, 0.3330],
         [0.9874, 0.9351]],

        [[0.6613, 0.4773],
         [0.9103, 0.2991],
         [0.6107, 0.5941]]])
'''
  1. torch.zeros生成一个初始化的全0张量。
    torch.zeros(*size, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False)
x = torch.zeros(3, 2, 4, dtype=torch.long)
print(x)
'''
tensor([[[0, 0, 0, 0],
         [0, 0, 0, 0]],

        [[0, 0, 0, 0],
         [0, 0, 0, 0]],

        [[0, 0, 0, 0],
         [0, 0, 0, 0]]])
'''
  1. torch.tensor通过复制创建一个张量。
    torch.tensor(data, *, dtype=None, device=None, requires_grad=False, pin_memory=False)
    data是张量的初始数据。可以是列表,元组,NumPy,标量和其他类型。
x = torch.tensor([[5, 3], [6, 8], [7, 1]])
print(x)
'''
tensor([[5, 3],
        [6, 8],
        [7, 1]])
'''
  1. Tensor.new_ones(从已有张量)返回一个全填充1的张量。
    Tensor.new_ones(size, dtype=None, device=None, requires_grad=False)
    size定义输出张量形状的列表、元组或整数。默认情况下返回的Tensor与已有张量具有相同的 torch.dtype 和 torch.device。
x = torch.tensor([[2, 2, 2], [2, 2, 2]])
x = x.new_ones(2, 3)
print(x)
'''
tensor([[1, 1, 1],
        [1, 1, 1]])
'''
  1. torch.randn_like生成一个张量,其大小与输入tensor相同,其填充实满足均值为0且方差为1的正态分布的随机数。
    torch.randn_like(input, *, dtype=None, layout=None, device=None, requires_grad=False, memory_format=torch.preserve_format)
x = torch.tensor([[2, 2, 2], [2, 2, 2]])
x = torch.rand_like(x, dtype=torch.float)
print(x)
'''
tensor([[0.9673, 0.5070, 0.2757],
        [0.0980, 0.1018, 0.4406]])
'''

(2)张量的操作

此部分列举4个基础操作,详细链接在这

  1. Tensor.size获取张量的形状
    Tensor.size(dim=None)
    Tips: torch.Size是一个元组,所以还支持元组的操作。
x = torch.tensor([[2, 2, 2], [2, 2, 2]])
print(x.size())
'''
torch.Size([2, 3])
'''
  1. Tensor.item查看张量的值(张量的大小)
    只能查看单个tensor的值。
x = torch.randn(1)
print(x.item())
y = torch.randn(4)
print(x[:2].item())
'''
-0.2167293280363083
-0.2167293280363083
'''
  1. Tensor.view调整张量的形状
    Tensor.view(*shape)返回一个新张量,其数据与tensor相同,但具有不同的形状
    返回的张量共享相同的数据,并且必须具有相同数量的元素,但可能具有不同的大小。
x = torch.randn(4, 4)
y = x.view(16)
z = x.view(-1, 8)  # -1是由别的维度推断出来的
f = x.view(2, 8)
print(x.size(), y.size(), z.size(), f.size())
'''
torch.Size([4, 4]) torch.Size([16]) torch.Size([2, 8]) torch.Size([2, 8])
'''
  1. torch.add加法运算
    torch.add(input, other, *, alpha=1, out=None)
    input是要加的张量,other是要加的张量或数字,alpha是other的缩放比例(默认不缩放),out是可以额外指定的结果输出的参数。
    此外,tensor也支持运算符,如:print(x + y)
x = torch.zeros(5, 3)
y = torch.ones_like(x)
z = torch.empty(5, 3)
torch.add(x, y, out=z)
print('-------------\n', z)
torch.add(x, 20, out=z)
print('-------------\n', z)
'''
-------------
 tensor([[1., 1., 1.],
        [1., 1., 1.],
        [1., 1., 1.],
        [1., 1., 1.],
        [1., 1., 1.]])
-------------
 tensor([[20., 20., 20.],
        [20., 20., 20.],
        [20., 20., 20.],
        [20., 20., 20.],
        [20., 20., 20.]])

Process finished with exit code 0

'''

或者采用加法的变体:Tensor.add_(other, *, alpha=1),注意有一个‘_’,这个符号在所有替换自身操作符的末尾都有。
另外,输出的方式还可以像python一样。

x.add_(y)
print(x)
'''
tensor([[1., 1., 1.],
        [1., 1., 1.],
        [1., 1., 1.],
        [1., 1., 1.],
        [1., 1., 1.]])
'''
print(x[:, 1])
'''
tensor([1., 1., 1., 1., 1.])
'''
  1. 张量和Numpy的相互转换
  • Tensor到Nump:Tensor.numpy()
  • Numpy到Tensor:torch.from_numpy(ndarray)
    在使用CPU的情况下,tensor和array将共享他们的物理位置,改变其中一个的值,另一个也会随之变化。
a = torch.ones(5)  # torch.ones返回一个填充有标量值1的张量,其形状由变量参数size定义。
b = a.numpy()
print(a, '\n', b)
'''
tensor([1., 1., 1., 1., 1.]) 
 [1. 1. 1. 1. 1.]
 '''
c = torch.from_numpy(b)
print(b, '\n', c)
'''
[1. 1. 1. 1. 1.] 
 tensor([1., 1., 1., 1., 1.])
'''

3.5.2 自动微分

在pytorch中,神经网络的核心是自动微分,在本节中我们会初探这个部分,也会训练一个小型的神经网络。
自动微分包会提供自动微分的操作,它是一个取决于每一轮的运行的库,你的下一次的结果会和你上一轮运行的代码有关,因此,每一轮的结果,有可能都不一样。

torch.Tensor是这个包的核心类,如果你设置了它的参数 .requires_grad=true 的话,它将会开始去追踪所有的在这个张量上面的运算。当你完成你得计算的时候,你可以调用backwward()来计算所有的微分。这个向量的梯度将会自动被保存在grad这个属性里面。

如果想要阻止张量跟踪历史数据,你可以调用detach()来将它从计算历史中分离出来,当然未来所有计算的数据也将不会被保存。或者你可以使用with torch.no_grad()来调用代码块,不光会阻止梯度计算,还会避免使用储存空间,这个在计算模型的时候将会有很大的用处,因为模型梯度计算的这个属性默认是开启的,而我们可能并不需要。

第二个非常重要的类是Function,Tensor和Function,他们两个是相互联系的并且可以搭建一个非循环的运算图。
每一个张量都有一个grad_fn的属性,它可以调用Function来创建Tensor,当然,如果用户自己创建了Tensor的话,那这个属性自动设置为None。

如果你想要计算引出量的话,你可以调用.backward()在Tensor上面,如果Tensor是一个纯数的话,那么你将不必要指明任何参数;如果它不是纯数的话,你需要指明一个和张量形状匹配的梯度的参数。下面来看一些例程。

import torch
x = torch.ones(2, 2, requires_grad=True)
print(x)
"""
tensor([[1., 1.],
        [1., 1.]], requires_grad=True)
"""
y = x + 2
print(y)
"""
tensor([[3., 3.],
        [3., 3.]], grad_fn=<AddBackward0>)
"""
print(y.grad_fn)
"""
<AddBackward0 object at 0x7fc6bd199ac8>
"""
z = y * y * 3
out = z.mean()
print(z, out)
"""
tensor([[27., 27.],
        [27., 27.]], grad_fn=<MulBackward0>) tensor(27., grad_fn=<MeanBackward0>)
"""
a = torch.randn(2, 2)
a = ((a * 3) / (a - 1))
print(a.requires_grad)
a.requires_grad_(True)
print(a.requires_grad)
b = (a * a).sum()
print(b.grad_fn)
"""
False
True
<SumBackward0 object at 0x7fc6bd1b02e8>
"""

3.5.3 优化器类

优化器(optimizer),可以理解为torch为我们封装的用来进行更新参数的方法,比如常见的随机梯度下降(stochastic gradient descent,SGD)。
优化器类都是由torch.optim提供的,例如:

  • torch.optim.SGD(参数,学习率)
  • torch.optim.Adam(参数,学习率)

注意:

  • 参数可以使用model.parameters()来获取,获取模型中所有requires_grad=True的参数
  • 优化类的使用方法:实例化 → 所有参数的梯度,将其值置为0 → 反向传播计算梯度 → 更新参数值
from torch import optim

optimizer = optim.SGD(model.parameters(), lr=1e-3)  # 1. 实例化
optimizer.zero_grad()  # 2. 梯度置为0
loss.backward()  # 3. 计算梯度
optimizer.step()   # 4. 更新参数的值

3.5.4 损失函数

torch中也预测了很多损失函数,比如:均方误差:nn.MSELoss(),常用于回归问题;交叉熵损失:nn.CrossEntropyLoss(),常用于分类问题。
使用方法:

model = Lr()  #1. 实例化模型
criterion = nn.MSELoss()  # 2. 实例化损失函数
optimizer = optim.SGD(model.parameters(), lr=1e-3)  #3. 实例化优化器类
for i in range(100):
    y_predict = model(x_true)  # 4. 向前计算预测值
    loss = criterion(y_true,y_predict)  # 5. 调用损失函数传入真实值和预测值,得到损失结果
    optimizer.zero_grad()  # 5. 当前循环参数梯度置为0
    loss.backward()  # 6. 计算梯度
    optimizer.step()   # 7. 更新参数的值

3.5.5 在GPU上运行代码

当模型太大,或者参数太多的情况下,为了加快训练速度,经常会使用GPU来进行训练。此时我们的代码需要稍作调整:

  1. 判断GPU是否可用torch.cuda.is_available()
import torch
print(torch.device("cuda:0" if torch.cuda.is_available() else "cpu"))
'''
输出:
cuda:0  # 是使用gpu
cpu  # 是使用cpu
'''
  1. 把模型参数和input数据转化为cuda的支持类型:
model.to(device)  # device是cpu或cuda
x_true.to(device)
  1. 在GPU上计算结果也为cuda的数据类型,需要转化为 numpy 或者 torch的cpu的tensor 类型:
predict = predict.cpu().detach().numpy() 

3.5.6 构建一个深度学习模型

构建一个深度学习模型也可以将其分为三步:数据集准备、模型定义、模型训练;

(1) 数据集准备

理论上,深度学习中的数据集准备与经典机器学习中的数据集准备并无本质性差别,大体都是基于特定的数据构建样本和标签的过程,其中这里的样本依据应用场景的不同而有不同的样式,比如CV领域中典型的就是图片,而NLP领域中典型的就是一段段的文本。

但无论原始样本如何,最终都要将其转化为数值型的Tensor。

当然,将数据集转化为Tensor之后理论上即可用于深度学习模型的输入和训练,但为了更好的支持模型训练以及大数据集下的分batch进行训练,PyTorch中提供了标准的数据集类型(Dataset),而我们则一般是要继承此类来提供这一格式。这里主要介绍3个常用的数据集相关的类:

  • Dataset:所有自定义数据集的基类;
  • TensorDataset: Dataset的一个wrapper(封装),用于快速构建Dataset;
  • DataLoader: Dataset的一个wrapper,将Dataset自动划分为多个batch
1. Dataset

Dataset是PyTorch中提供的一个数据集基类,首先查看Dataset的签名文档如下:

[docs]class Dataset(Generic[T_co]):
    r"""An abstract class representing a :class:`Dataset`.

    All datasets that represent a map from keys to data samples should subclass
    it. All subclasses should overwrite :meth:`__getitem__`, supporting fetching a
    data sample for a given key. Subclasses could also optionally overwrite
    :meth:`__len__`, which is expected to return the size of the dataset by many
    :class:`~torch.utils.data.Sampler` implementations and the default options
    of :class:`~torch.utils.data.DataLoader`.

    .. note::
      :class:`~torch.utils.data.DataLoader` by default constructs a index
      sampler that yields integral indices.  To make it work with a map-style
      dataset with non-integral indices/keys, a custom sampler must be provided.
    """

    def __getitem__(self, index) -> T_co:
        raise NotImplementedError

    def __add__(self, other: 'Dataset[T_co]') -> 'ConcatDataset[T_co]':
        return ConcatDataset([self, other])


    # No `def __len__(self)` default?
    # See NOTE [ Lack of Default `__len__` in Python Abstract Base Classes ]
    # in pytorch/torch/utils/data/sampler.py

torch.utils.data.Dataset(*args, **kwds)表示数据集的抽象类。
从中可以看出,所有自定义的数据集都应继承此类,并重载其中的__getitem__和__len__两个方法即可。后者获取整个数据的大小,前者获取对应索引的数据。
当然,还需通过类初始化方法__init__来设置要加载的数据。典型的自定义一个Dataset的实现如下:

class MyDataset(Dataset):
    def __init__(self, x, y):
        super().__init__()
        ……

    def __getitem__(self):
        return ……

    def __len__(self):
        return ……
2. TensorDataset

上述通过Dataset的方式可以实现一个标准自定义数据集的构建,但如果对于比较简单的数据集仍需八股文似的重载__getitem__和__len__两个方法,则难免有些繁杂和俗套。而TensorDataset就是对上述需求的一个简化,即当仅需将特定的tensor包裹为一个Dataset类型作为自定义数据集时,那么直接使用TensorDataset即可。

这里仍然先给出其签名文档:

[docs]class TensorDataset(Dataset[Tuple[Tensor, ...]]):
    r"""Dataset wrapping tensors.

    Each sample will be retrieved by indexing tensors along the first dimension.

    Args:
        *tensors (Tensor): tensors that have the same size of the first dimension.
    """
    tensors: Tuple[Tensor, ...]

    def __init__(self, *tensors: Tensor) -> None:
        assert all(tensors[0].size(0) == tensor.size(0) for tensor in tensors), "Size mismatch between tensors"
        self.tensors = tensors

    def __getitem__(self, index):
        return tuple(tensor[index] for tensor in self.tensors)

    def __len__(self):
        return self.tensors[0].size(0)

torch.utils.data.TensorDataset(*tensors)要求*tensors具有与第一维相同大小的张量。
具体应用时,只需将若干个tensor格式的输入作为参数传入TensorDataset,而后返回结果即是一个标准的Dataset类型数据集。标准使用方式如下:

my_dataset = TensorDataset(tenso_x, tensor_y)
3. DataLoader

深度学习往往适用于大数据集场景,训练一个成熟的深度学习模型一般也需要足够体量的数据。
所以,在深度学习训练过程中一般不会每次都将所有训练集数据一次性的喂给模型,而是小批量分批次的训练,其中每个批量叫做一个batch,完整的训练集参与一次训练叫做一个epoch。实现小批量多批次的方式有很多,比如完全可以通过随机取一个索引分片的方式来实现这一工作,但更为标准和优雅的方式则是使用Dataloader。

其给出的签名文档节选如下:

[docs]class DataLoader(Generic[T_co]):
    r"""
    Data loader. Combines a dataset and a sampler, and provides an iterable over
    the given dataset.

    The :class:`~torch.utils.data.DataLoader` supports both map-style and
    iterable-style datasets with single- or multi-process loading, customizing
    loading order and optional automatic batching (collation) and memory pinning.

    See :py:mod:`torch.utils.data` documentation page for more details.
    ……

torch.utils.data.DataLoader(dataset, batch_size=1, shuffle=False, sampler=None, batch_sampler=None, num_workers=0, collate_fn=None, pin_memory=False, drop_last=False, timeout=0, worker_init_fn=None, multiprocessing_context=None, generator=None, *, prefetch_factor=2, persistent_workers=False)
数据加载程序。将数据集和采样器组合在一起,并提供对给定数据集的可迭代。
DataLoader 支持地图样式和可迭代样式的数据集,具有单进程或多进程加载、自定义加载顺序以及可选的自动批处理(排序规则)和内存固定功能。

  • batch_size(整数,可选)– 每批要加载的样本数(默认值:1);

  • shuffle(布尔,可选)– True设置为在每次迭代都重新洗牌数据(默认值:False);

可见,DataLoader大体上可以等价为对一个Dataset实现随机采样(sampler),而后对指定数据集提供可迭代的类型。相应的,其使用方式也相对简单:直接将一个Dataset类型的数据集作为参数传入DataLoader即可。

简单的使用样例如下:

dataloader = DataLoader(MyDataset, batch_size=128, shuffle=True)

以上是应用PyTorch构建数据集时常用的三种操作,基本可以覆盖日常使用的绝大部分需求,后面会结合实际案例加以完整演示。

(2) 网络架构定义

深度学习与经典机器学习的一个最大的区别在于模型结构方面,经典机器学习模型往往有着固定的范式和结构,例如:随机森林就是由指定数量的决策树构成,虽然这里的n_estimators可以任选,但整体来看随机森林模型的结构是确定的;而深度学习模型的基础在于神经网络,即由若干的神经网络层构成,每一层使用的神经网络模块类型可以不同(全连接层、卷积层等等),包含的神经元数量差异也会带来很大的不同。
也正因如此,深度学习给使用者提供了更大的设计创新空间。

当然,网络架构(Architecture)的设计不需要从零开始,PyTorch这些深度学习框架的一大功能就是提供了基础的神经网络模块(Module),而使用者仅需根据自己的设计意图将其灵活组装起来即可——就像搭积木一般!PyTorch中所有网络模块均位于torch.nn模块下(nn=nueral network),具体可见:https://pytorch.org/docs/stable/nn.html
这些模块数量庞大,功能各异,构成了深度学习模型的核心。但就其功能而言,大体分为以下几类:

  • 模型功能类:例如Linear、Conv2d,RNN等,分别对应全连接层、卷积层、循环神经网络层;
  • 激活函数:例如Sigmoid,Tanh,ReLU等;
  • 损失函数:CrossEntropyLoss,MSELoss等,其中前者是分类常用的损失函数,后者是回归常用的损失函数;
  • 规范化:LayerNorm等;
  • 防止过拟合:Dropout等;
  • 其他。

某种程度上讲,学习深度学习的主体在于理解掌握这些基础的网络模块其各自的功能和使用方法,在此基础上方可根据自己对数据和场景的理解来自定义设计网络架构,从而实现预期的模型效果。

在这些单个网络模块的基础上,构建的完整网络模型则需继承PyTorch中的Module类来加以实现(这一过程类似于继承Dataset类实现自定义数据集),这里仍然给出Module的签名文档:

class Module:
    r"""Base class for all neural network modules.

    Your models should also subclass this class.

    Modules can also contain other Modules, allowing to nest them in
    a tree structure. You can assign the submodules as regular attributes::

        import torch.nn as nn
        import torch.nn.functional as F

        class Model(nn.Module):
            def __init__(self):
                super().__init__()
                self.conv1 = nn.Conv2d(1, 20, 5)
                self.conv2 = nn.Conv2d(20, 20, 5)

            def forward(self, x):
                x = F.relu(self.conv1(x))
                return F.relu(self.conv2(x))

    Submodules assigned in this way will be registered, and will have their
    parameters converted too when you call :meth:`to`, etc.

    .. note::
        As per the example above, an ``__init__()`` call to the parent class
        must be made before assignment on the child.

    :ivar training: Boolean represents whether this module is in training or
                    evaluation mode.
    :vartype training: bool
    """

从中可以看出,所有自定义的网络模型均需继承Module类,并一般需要重写forward函数(用于实现神经网络的前向传播过程),例如下。
而后模型即完成了注册,并拥有了相应的可训练参数等。
当我们自定义网络的时候,有两个方法需要特别注意:

  • __init__需要调用super方法,继承父类的属性和方法
  • farward方法必须实现,用来定义我们的网络的向前计算的过程
import torch.nn as nn
import torch.nn.functional as F

class Model(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(1, 20, 5)
        self.conv2 = nn.Conv2d(20, 20, 5)

    def forward(self, x):
        x = F.relu(self.conv1(x))
        return F.relu(self.conv2(x))

(3) 模型训练

仍然与经典机器学习模型的训练不同,深度学习模型由于其网络架构一般是自定义设计的,
所以一般也不能简单的通过调用fit/predict的方式来实现简洁的模型训练/预测过程,而往往交由使用者自己去实现。

大体上,实现模型训练主要包含以下要素:

  • 完成数据集的准备;
  • 定义网络模型;
  • 指定一个损失函数,用于评估当前模型在指定数据集上的表现;
  • 指定一个优化器,用于"指导"模型朝着预期方向前进;
  • 写一个循环调度,实现模型训练的迭代和进化;

数据集的准备和模型定义部分就是前两小节所述内容;而损失函数,简单需求可以依据PyTorch提供的常用损失函数,
而更为复杂和个性化的损失函数则继承Module类的方式来加以自定义实现;优化器部分则无太多“花样”可言,一般直接调用内置的优化器即可,
例如Adam、SGD等等。这些操作结合后续的实践案例一并介绍。

(4) 一个简单的深度学习案例

有了前述小节的理论基础,就可以开始深度学习实践案例了,这里以sklearn中自带的手写数字分类作为目标来加以实践。

  1. 首先载入数据:
from sklearn.datasets import load_digits
from sklearn.model_selection import train_test_split

X, y = load_digits(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(X, y)
  1. 构建Dataset类型数据集:
import torch
from torch.utils.data import TensorDataset, DataLoader

X_train_tensor = torch.Tensor(X_train)
y_train_tensor = torch.Tensor(y_train).long()  # 主要标签需要用整数形式,否则后续用于计算交叉熵损失时报错
dataset = TensorDataset(X_train_tensor, y_train_tensor)  # 直接调用TensorDataset加以包裹使用
dataloader = DataLoader(dataset, batch_size=128, shuffle=True)  # 每128个样本为一个batch,训练时设为随机

X_test_tensor = torch.Tensor(X_test)  # 测试集只需转化为tensor即可
y_test_tensor = torch.Tensor(y_test).long()
  1. 自定义一个网络模型,仅使用Linear网络层:
from torch import nn, optim


class Model(nn.Module):  # 继承Module基类
    def __init__(self, n_input=64, n_hidden=32, n_output=10):
        # 定义一个含有单隐藏层的全连接网络,其中输入64为手写数字数据集的特征数,输出10为类别数,隐藏层神经元数量设置32
        super().__init__()  # 对继承自父类nn.Module的属性进行初始化。而且是用nn.Module的初始化方法来初始化继承的属性。(也可用自己的方法)
        # 使用全连接层和ReLU激活函数搭建网络模型
        self.dnn = nn.Sequential(
            nn.Linear(n_input, n_hidden),  # nn.Linear为torch预定义好的线性模型,也被称为全链接层,
            nn.ReLU(),
            nn.Linear(n_hidden, n_output)  # Linear传入的参数为输入的数量(特征数、列数),输出的数量(in_features, out_features)
        )

    def forward(self, x):
        # 重载forward函数,从输入到输出
        return self.dnn(x)
  1. 八股文式的深度学习训练流程:
model = Model()  # 初始化模型
creterion = nn.CrossEntropyLoss()  # 选用交叉熵损失函数
optimizer = optim.Adam(model.parameters(), lr=0.001)  # 选用Adam优化器,传入模型参数,设置学习率
for epoch in range(50):  # 50个epoch
    for data, label in dataloader:  # DataLoader是一个可迭代对象
        optimizer.zero_grad()  # 待优化参数梯度清空
        prob = model(data)  # 执行一次前向传播,计算预测结果
        loss = creterion(prob, label)  # 评估模型损失
        loss.backward()  # 损失反向传播,完成对待优化参数的梯度求解
        optimizer.step()  # 参数更新
    if (epoch + 1) % 5 == 0:  # 每隔5个epoch打印当前模型训练效果
        with torch.no_grad():
            train_prob = model(X_train_tensor)
            train_pred = train_prob.argmax(dim=1)
            acc_train = (train_pred == y_train_tensor).float().mean()
            test_prob = model(X_test_tensor)
            test_pred = test_prob.argmax(dim=1)
            acc_test = (test_pred == y_test_tensor).float().mean()
            print(f"epoch: {
      
      epoch}, train_accuracy: {
      
      acc_train}, test_accuracy: {
      
      acc_test} !")

输出:

epoch: 4, train_accuracy: 0.8507795333862305, test_accuracy: 0.8577777743339539 !
epoch: 9, train_accuracy: 0.948775053024292, test_accuracy: 0.9200000166893005 !
epoch: 14, train_accuracy: 0.9717891812324524, test_accuracy: 0.9444444179534912 !
epoch: 19, train_accuracy: 0.9799554347991943, test_accuracy: 0.9577777981758118 !
epoch: 24, train_accuracy: 0.9866369962692261, test_accuracy: 0.9644444584846497 !
epoch: 29, train_accuracy: 0.9925761222839355, test_accuracy: 0.9644444584846497 !
epoch: 34, train_accuracy: 0.9925761222839355, test_accuracy: 0.9644444584846497 !
epoch: 39, train_accuracy: 0.9962880611419678, test_accuracy: 0.9666666388511658 !
epoch: 44, train_accuracy: 0.9970304369926453, test_accuracy: 0.9711111187934875 !
epoch: 49, train_accuracy: 0.9970304369926453, test_accuracy: 0.9711111187934875 !

3.6 采用LeNet-5对手写数字进行分类

在以上部分的基础上,选用LeNet5对手写数字分类任务加以尝试,看看模型是怎么利用这一卷积操作。

  1. 首先是mnist数据集的准备,可直接使用torchvision包在线下载:
from torchvision import datasets
from torch.utils.data import DataLoader, TensorDataset

train = datasets.MNIST('data/', download=True, train=True)  # 从库中导出数据
test = datasets.MNIST('data/', download=True, train=False)

X_train = train.data.unsqueeze(1) / 255.0
y_train = train.targets
trainloader = DataLoader(TensorDataset(X_train, y_train), batch_size=256, shuffle=True)

X_test = test.data.unsqueeze(1) / 255.0
y_test = test.targets
  1. 然后是LeNet5的网络模型(torchvision中内置了部分经典模型,但LeNet5由于比较简单,不在其中):
import torch
from torch import nn, optim
import torch.nn.functional as F


class LeNet5(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(1, 6, 5, padding=2)
        self.pool1 = nn.MaxPool2d((2, 2))
        self.conv2 = nn.Conv2d(6, 16, 5)
        self.pool2 = nn.MaxPool2d((2, 2))
        self.fc1 = nn.Linear(16*5*5, 120)
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 10)

    def forward(self, x):
        x = F.relu(self.conv1(x))
        x = self.pool1(x)
        x = F.relu(self.conv2(x))
        x = self.pool2(x)
        x = x.view(len(x), -1)
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x
  1. 最后是模型的训练过程:
model = LeNet5()
optimizer = optim.Adam(model.parameters())
criterion = nn.CrossEntropyLoss()

for epoch in range(10):
    for X, y in trainloader:
        pred = model(X)
        loss = criterion(pred, y)
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
    with torch.no_grad():
        y_pred = model(X_train)
        acc_train = (y_pred.argmax(dim=1) == y_train).float().mean().item()
        y_pred = model(X_test)
        acc_test = (y_pred.argmax(dim=1) == y_test).float().mean().item()
        print(epoch, acc_train, acc_test)

输出:

0 0.9371833205223083 0.9401000142097473
1 0.9669833183288574 0.9700000286102295
2 0.9770166873931885 0.9786999821662903
3 0.9809166789054871 0.9832000136375427
4 0.9846000075340271 0.9857000112533569
5 0.9866999983787537 0.9868000149726868
6 0.9891166687011719 0.9873999953269958
7 0.989983320236206 0.9882000088691711
8 0.9918333292007446 0.989799976348877
9 0.9908999800682068 0.9890000224113464

最后还可以用torchinfo包查看模型的结构:

from torchinfo import summary

model = LeNet5()
batch_size = 256
summary(model, input_size=(batch_size, 1, 28, 28))

输出:

==========================================================================================
Layer (type:depth-idx)                   Output Shape              Param #
==========================================================================================
LeNet5                                   --                        --
├─Conv2d: 1-1                            [256, 6, 28, 28]          156
├─MaxPool2d: 1-2                         [256, 6, 14, 14]          --
├─Conv2d: 1-3                            [256, 16, 10, 10]         2,416
├─MaxPool2d: 1-4                         [256, 16, 5, 5]           --
├─Linear: 1-5                            [256, 120]                48,120
├─Linear: 1-6                            [256, 84]                 10,164
├─Linear: 1-7                            [256, 10]                 850
==========================================================================================
Total params: 61,706
Trainable params: 61,706
Non-trainable params: 0
Total mult-adds (M): 108.30
==========================================================================================
Input size (MB): 0.80
Forward/backward pass size (MB): 13.35
Params size (MB): 0.25
Estimated Total Size (MB): 14.40
==========================================================================================

3.7 Spaceship Titanic案例实践

Spaceship Titanic具体的案例背景信息可点进Kaggle官网查看。
训练集(train.csv)中,有以下12个字段:

  • PassengerId:乘客ID(共8693条);
  • HomePlanet:乘客离开的星球,通常是他们永久居住的星球;分类数据、缺失201条;
  • CryoSleep:是否冷冻睡眠,二分类(Boolean)、缺失217条;
  • Cabin:客舱号(采用deck/num/side形式),字符串,缺失199条;
  • Destination:目的地,分类数据,缺失182条;
  • Age:年龄;数值数据,缺失179条,还有178条为0;
  • VIP:是否是VIP,二分类(Boolean),缺失203条;
  • RoomService:该服务支付的费用,数值数据,缺失181条;
  • FoodCourt:该服务支付的费用,数值数据,缺失183条;
  • ShoppingMall:该服务支付的费用,数值数据,缺失208条;
  • Spa:该服务支付的费用,数值数据,缺失183条;
  • VRDeck:该服务支付的费用,数值数据,缺失188条;
  • Name:姓名,字符串,缺失200条;
  • Transported:是否被传送,预测值,二分类;
  1. 首先加载数据,查看各数值之间的基本情况,进行数据探索:
import pandas as pd
import pandas_profiling as pp

df = pd.read_csv('train.csv')
report = pp.ProfileReport(df)
report.to_file('report.html')  # 读取数据报告,研究怎么进行数据预处理
  1. 思考如何处理数据:
  • PassengerId:索引,删除;
  • HomePlanet:分类数据(3类)、缺失201条,用’Earth’填充(众数);转换成onehot编码(get_dummies());
  • CryoSleep:二分类(Boolean)、缺失217条,astype()先转成str,再用False填充(众数);转换成onehot编码(get_dummies());
  • Cabin:(deck/num/side形式),字符串,用’/'分列,然后删除Cabin;缺失199条、先众数填充;
    • deck列,采用标签编码(LabelEncoder());
    • num列,从字符串转换成整数数据(astype());
    • side列,转换成onehot编码(get_dummies());
  • Destination:分类数据,缺失182条,众数’TRAPPIST-1e’填充;转换成onehot编码(get_dummies());
  • Age:数值数据,缺失179条,还有178条为0;
  • VIP:二分类(Boolean),缺失203条;astype()先转成str,再用False填充(众数);转换成onehot编码(get_dummies());
  • RoomService:该服务支付的费用,数值数据,缺失181条、用众数填充;
  • FoodCourt:该服务支付的费用,数值数据,缺失183条、用众数填充;
  • ShoppingMall:该服务支付的费用,数值数据,缺失208条、用众数填充;
  • Spa:该服务支付的费用,数值数据,缺失183条、用众数填充;
  • VRDeck:该服务支付的费用,数值数据,缺失188条、用众数填充;
  • Name:姓名,字符串,缺失200条,但与预测无关,删除;
  • Transported:是否被传送,预测值,二分类;
  1. 进行数据预处理
# 数据预处理:列出需要用众数处理的列的列表
missing_column = ['HomePlanet', 'CryoSleep', 'Cabin', 'Destination', 'VIP', 'RoomService', 'FoodCourt', 'ShoppingMall', 'Spa', 'VRDeck']
# 列出要弃掉的列的列表
drop_column = ['PassengerId', 'Cabin', 'Name']


# 创建一个填充缺失值的函数,填充完整所有需要的数据
def missing_fill(df):
    for feature in missing_column:  # 循环missing_column列表
        df[feature] = df[feature].fillna(df[feature].mode()[0])  # 用众数填充缺失值
    df['Age'] = df['Age'].fillna(df['Age'].mean())  # 用平均值填充Age
    return df


# 创建一个用于Cabin分列和数据类型转换的函数
def column_split_trans(df):
    # 将布尔类型转换成字符串类型
    bool_features = ['CryoSleep', 'VIP']
    for bool_ft in bool_features:
        df[bool_ft] = df[bool_ft].astype('str')
    # 拆分'Cabin'列
    column = 'Cabin'
    df['Deck'] = df['Cabin'].str.split('/').str[0]
    df['Num'] = df[column].str.split('/').str[1]
    df['Num'] = df['Num'].astype('int')  # 将num列从字符转化成整数
    df['Side'] = df[column].str.split('/').str[2]

    return df


# 定义一个删除列的函数
def drop_features(df):
    drop_feature = ['PassengerId', 'Cabin', 'Name']
    for ft in drop_feature:
        df = df.drop(ft, axis=1)  # 当axis=1时,数组的变化是横向的,而体现出来的是列的增加或者减少
    return df


# 定义一个编码的函数
from sklearn.preprocessing import LabelEncoder


def encoder(df):
    # 对'Deck'采用LabelEncoder编码
    label_en = LabelEncoder()
    label_en.fit(df['Deck'])
    df['Deck'] = label_en.transform(df['Deck'])
    # 对剩下的所有字符数据进行OneHot编码
    df = pd.get_dummies(df)  # 当应用于DataFrame数据时,get_dummies方法只对字符串列进行转换,而其它的列保持不变
    return df


# 将所有的数据处理流程合并,组成一个新函数
def data_processing(df):
    df = missing_fill(df)
    df = column_split_trans(df)
    df = drop_features(df)
    df = encoder(df)
    return df


# 对数据集应用数据处理方法
train_df_after = data_processing(train_df)
valid_df_after = data_processing(valid_df)
print(train_df_after.info())  # 查看转换后的数据情况


# 划分数据集(x)和预测值(y)。  由于读入的numpy数组里的元素是object类型,无法将这种类型转换成tensor。所以强制转换格式:
from torch.utils.data import TensorDataset
import torch

train_df_x = train_df_after.drop('Transported', axis=1).astype(float)
train_df_y = train_df_after.Transported.astype(float)


# 将训练数据集划分为训练集和验证集,并将其转为tensor格式
from sklearn.model_selection import train_test_split
import numpy as np

x_train, x_test, y_train, y_test = train_test_split(train_df_x, train_df_y, test_size=0.2, random_state=42)
x_train = np.array(x_train)
x_test = np.array(x_test)
y_train = np.array(y_train)
y_test = np.array(y_test)
x_train_tensor, x_test_tensor, y_train_tensor, y_test_tensor = map(torch.tensor, (x_train, x_test, y_train, y_test))


# 将数据维度从2D升维到4D
x_train_tensor = x_train_tensor.view(6954, 1, 1, 20)
x_test_tensor = x_test_tensor.view(1739, 1, 1, 20)


# 将标签转为long格式
y_train_tensor = y_train_tensor.long()
y_test_tensor = y_test_tensor.long()


# 类似处理验证集(只有数据,没有标签)
valid_df_x = valid_df_after.astype(float)
valid_tensor_x = torch.tensor(valid_df_x.values)
x_valid_tensor = valid_tensor_x.view(4277, 1, 1, 20)


# 将训练集放进的dataset,然后转换成dataloader
from torch.utils.data import DataLoader

train_dataset = TensorDataset(x_train_tensor, y_train_tensor)
train_dr = DataLoader(train_dataset, batch_size=128, shuffle=True)
  1. 构建模型
# 定义网络模型
from torch import nn
import torch.nn.functional as F


class LeNet5(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(1, 16, (1, 2))  # 输入数据:输入通道数(1)×输出通道数(16)×卷积核大小((1×2))
        self.pool1 = nn.MaxPool2d((1, 2))
        self.conv2 = nn.Conv2d(16, 32, (1, 2))
        self.pool2 = nn.MaxPool2d((1, 2))
        self.fc1 = nn.Linear(8*4*4, 128)  # 输入数据:输入特征数(8*4*4)×输出特征数(128)
        self.fc2 = nn.Linear(128, 32)
        self.fc3 = nn.Linear(32, 2)

    def forward(self, out):
        out = out.to(torch.float32)  # 解决数据类型的问题
        out = F.relu(self.conv1(out))
        out = self.pool1(out)
        out = F.relu(self.conv2(out))
        out = self.pool2(out)
        out = out.view(len(out), -1)  # 光栅化
        out = F.relu(self.fc1(out))
        out = F.relu(self.fc2(out))
        out = self.fc3(out)
        return out
  1. 训练模型
# 设置随机数种子,保证结果可复现
seed = 548
torch.manual_seed(seed)  # 设置CPU
# torch.cuda.manual_seed(seed)  # 设置GPU

# 训练模型
from torch import optim

model = LeNet5()  # 实例化模型
optimizer = optim.Adam(model.parameters())  # 选择优化器
criterion = nn.CrossEntropyLoss()  # 选用交叉熵作为损失函数Loss
epoch = 30  # 设定迭代次数

for epoch in range(epoch+1):  # 开始迭代循环
    for x, y in train_dr:  # 从dataloader中取x,y
        pred = model(x)  # 正向传播
        loss = criterion(pred, y)  # 计算损失函数
        optimizer.zero_grad()  # 优化器的梯度清零
        loss.backward()  # 反向传播
        optimizer.step()  # 参数更新
    # 计算准确率
    with torch.no_grad():  # 在该模块下,所有计算得出的tensor的requires_grad都自动设置为False。
        y_pred = model(x_train_tensor)  # 得到训练集的预测标签
        acc_train = (y_pred.argmax(dim=1) == y_train_tensor).float().mean().item()  # 计算训练集的准确率
        y_pred = model(x_test_tensor)  # 得到测试集的预测标签
        acc_test = (y_pred.argmax(dim=1) == y_test_tensor).float().mean().item()  # 计算训练集的准确率
        print('epoch:', epoch, '  Accuracy for train:', acc_train, '  Accuracy for test:', acc_test)
  1. 模型总结
# 模型总结
from torchinfo import summary

model = LeNet5()
batch_size = 128
summary(model, input_size=(batch_size, 1, 1, 20))

7.输出:

RangeIndex: 8693 entries, 0 to 8692
Data columns (total 21 columns):
 #   Column                     Non-Null Count  Dtype  
---  ------                     --------------  -----  
 0   Age                        8693 non-null   float64
 1   RoomService                8693 non-null   float64
 2   FoodCourt                  8693 non-null   float64
 3   ShoppingMall               8693 non-null   float64
 4   Spa                        8693 non-null   float64
 5   VRDeck                     8693 non-null   float64
 6   Transported                8693 non-null   bool   
 7   Deck                       8693 non-null   int32  
 8   Num                        8693 non-null   int32  
 9   HomePlanet_Earth           8693 non-null   uint8  
 10  HomePlanet_Europa          8693 non-null   uint8  
 11  HomePlanet_Mars            8693 non-null   uint8  
 12  CryoSleep_False            8693 non-null   uint8  
 13  CryoSleep_True             8693 non-null   uint8  
 14  Destination_55 Cancri e    8693 non-null   uint8  
 15  Destination_PSO J318.5-22  8693 non-null   uint8  
 16  Destination_TRAPPIST-1e    8693 non-null   uint8  
 17  VIP_False                  8693 non-null   uint8  
 18  VIP_True                   8693 non-null   uint8  
 19  Side_P                     8693 non-null   uint8  
 20  Side_S                     8693 non-null   uint8  
dtypes: bool(1), float64(6), int32(2), uint8(12)
memory usage: 585.9 KB
None
epoch: 0   Accuracy for train: 0.6711245179176331   Accuracy for test: 0.6589994430541992
epoch: 1   Accuracy for train: 0.7778257131576538   Accuracy for test: 0.76365727186203
epoch: 2   Accuracy for train: 0.7920621037483215   Accuracy for test: 0.77400803565979
epoch: 3   Accuracy for train: 0.7877480387687683   Accuracy for test: 0.7694076895713806
epoch: 4   Accuracy for train: 0.791343092918396   Accuracy for test: 0.7711328268051147
epoch: 5   Accuracy for train: 0.7929249405860901   Accuracy for test: 0.7717078924179077
epoch: 6   Accuracy for train: 0.7981017827987671   Accuracy for test: 0.78435879945755
epoch: 7   Accuracy for train: 0.7976704239845276   Accuracy for test: 0.7728579640388489
epoch: 8   Accuracy for train: 0.7995398044586182   Accuracy for test: 0.7768832445144653
epoch: 9   Accuracy for train: 0.7963761687278748   Accuracy for test: 0.7774583101272583
epoch: 10   Accuracy for train: 0.791630744934082   Accuracy for test: 0.7803335189819336
epoch: 11   Accuracy for train: 0.7965199947357178   Accuracy for test: 0.7797584533691406
epoch: 12   Accuracy for train: 0.8016968369483948   Accuracy for test: 0.78435879945755
epoch: 13   Accuracy for train: 0.7988207936286926   Accuracy for test: 0.7809085845947266
epoch: 14   Accuracy for train: 0.7950819730758667   Accuracy for test: 0.7803335189819336
epoch: 15   Accuracy for train: 0.800690233707428   Accuracy for test: 0.78435879945755
epoch: 16   Accuracy for train: 0.7996836304664612   Accuracy for test: 0.7791834473609924
epoch: 17   Accuracy for train: 0.8031348586082458   Accuracy for test: 0.7797584533691406
epoch: 18   Accuracy for train: 0.8029910922050476   Accuracy for test: 0.7780333757400513
epoch: 19   Accuracy for train: 0.8050042986869812   Accuracy for test: 0.7860839366912842
epoch: 20   Accuracy for train: 0.8015530705451965   Accuracy for test: 0.7809085845947266
epoch: 21   Accuracy for train: 0.8025596737861633   Accuracy for test: 0.7860839366912842
epoch: 22   Accuracy for train: 0.8025596737861633   Accuracy for test: 0.7895342111587524
epoch: 23   Accuracy for train: 0.7999712228775024   Accuracy for test: 0.7832087278366089
epoch: 24   Accuracy for train: 0.8031348586082458   Accuracy for test: 0.7826337218284607
epoch: 25   Accuracy for train: 0.802128255367279   Accuracy for test: 0.7814835906028748
epoch: 26   Accuracy for train: 0.8041415214538574   Accuracy for test: 0.78435879945755
epoch: 27   Accuracy for train: 0.802128255367279   Accuracy for test: 0.78435879945755
epoch: 28   Accuracy for train: 0.7989646196365356   Accuracy for test: 0.7803335189819336
epoch: 29   Accuracy for train: 0.8034225106239319   Accuracy for test: 0.78435879945755
epoch: 30   Accuracy for train: 0.7989646196365356   Accuracy for test: 0.784933865070343
==========================================================================================
Layer (type:depth-idx)                   Output Shape              Param #
==========================================================================================
LeNet5                                   --                        --
├─Conv2d: 1-1                            [128, 16, 1, 19]          48
├─MaxPool2d: 1-2                         [128, 16, 1, 9]           --
├─Conv2d: 1-3                            [128, 32, 1, 8]           1,056
├─MaxPool2d: 1-4                         [128, 32, 1, 4]           --
├─Linear: 1-5                            [128, 128]                16,512
├─Linear: 1-6                            [128, 32]                 4,128
├─Linear: 1-7                            [128, 2]                  66
==========================================================================================
Total params: 21,810
Trainable params: 21,810
Non-trainable params: 0
Total mult-adds (M): 3.85
==========================================================================================
Input size (MB): 0.01
Forward/backward pass size (MB): 0.74
Params size (MB): 0.09
Estimated Total Size (MB): 0.84
==========================================================================================
Finish!!

Process finished with exit code 0

总结:虽然是已经完成用CNN去预测Spaceship Titanic的案例,但好像CNN并不适合用来做这种数据(或者说,至少是用经典的LeNet来修改去做的话不太合适),在Kaggle上的评分只有0.58826,直接是1300多名。接下来可以尝试下用别的网络来做一做。

猜你喜欢

转载自blog.csdn.net/Morganfs/article/details/124180808