0


学习笔记:深度学习(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的预测。
本部分主要来源:构建一个深度学习模型需要哪几步?、一小时学会pytorch、quickstart_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指定张量的所需设备(默认即根据当前设备设置)
  1. import torch
  2. x = torch.empty(5,3)print(x)'''
  3. tensor([[2.7712e+35, 4.5886e-41, 7.2927e-04],
  4. [3.0780e-41, 3.8725e+35, 4.5886e-41],
  5. [4.4446e-17, 4.5886e-41, 3.9665e+35],
  6. [4.5886e-41, 3.9648e+35, 4.5886e-41],
  7. [3.8722e+35, 4.5886e-41, 4.4446e-17]])
  8. '''
  1. torch.rand生成一个初始化的、均匀分布的、每个元素从0~1的张量。torch.rand(*size, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False)
  1. x = torch.rand(2,3,2)print(x)'''
  2. tensor([[[0.7350, 0.1058],
  3. [0.1558, 0.3330],
  4. [0.9874, 0.9351]],
  5. [[0.6613, 0.4773],
  6. [0.9103, 0.2991],
  7. [0.6107, 0.5941]]])
  8. '''
  1. torch.zeros生成一个初始化的全0张量。torch.zeros(*size, *, out=None, dtype=None, layout=torch.strided, device=None, requires_grad=False)
  1. x = torch.zeros(3,2,4, dtype=torch.long)print(x)'''
  2. tensor([[[0, 0, 0, 0],
  3. [0, 0, 0, 0]],
  4. [[0, 0, 0, 0],
  5. [0, 0, 0, 0]],
  6. [[0, 0, 0, 0],
  7. [0, 0, 0, 0]]])
  8. '''
  1. torch.tensor通过复制创建一个张量。torch.tensor(data, *, dtype=None, device=None, requires_grad=False, pin_memory=False) data是张量的初始数据。可以是列表,元组,NumPy,标量和其他类型。
  1. x = torch.tensor([[5,3],[6,8],[7,1]])print(x)'''
  2. tensor([[5, 3],
  3. [6, 8],
  4. [7, 1]])
  5. '''
  1. Tensor.new_ones(从已有张量)返回一个全填充1的张量。Tensor.new_ones(size, dtype=None, device=None, requires_grad=False) size定义输出张量形状的列表、元组或整数。默认情况下返回的Tensor与已有张量具有相同的 torch.dtype 和 torch.device。
  1. x = torch.tensor([[2,2,2],[2,2,2]])
  2. x = x.new_ones(2,3)print(x)'''
  3. tensor([[1, 1, 1],
  4. [1, 1, 1]])
  5. '''
  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)
  1. x = torch.tensor([[2,2,2],[2,2,2]])
  2. x = torch.rand_like(x, dtype=torch.float)print(x)'''
  3. tensor([[0.9673, 0.5070, 0.2757],
  4. [0.0980, 0.1018, 0.4406]])
  5. '''

(2)张量的操作

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

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

或者采用加法的变体:

  1. Tensor.add_(other, *, alpha=1)

,注意有一个‘_’,这个符号在所有替换自身操作符的末尾都有。
另外,输出的方式还可以像python一样。

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

3.5.2 自动微分

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

torch.Tensor是这个包的核心类,如果你设置了它的参数

  1. .requires_grad=true

的话,它将会开始去追踪所有的在这个张量上面的运算。当你完成你得计算的时候,你可以调用

  1. backwward()

来计算所有的微分。这个向量的梯度将会自动被保存在

  1. grad

这个属性里面。

如果想要阻止张量跟踪历史数据,你可以调用

  1. detach()

来将它从计算历史中分离出来,当然未来所有计算的数据也将不会被保存。或者你可以使用

  1. with torch.no_grad()

来调用代码块,不光会阻止梯度计算,还会避免使用储存空间,这个在计算模型的时候将会有很大的用处,因为模型梯度计算的这个属性默认是开启的,而我们可能并不需要。

第二个非常重要的类是Function,Tensor和Function,他们两个是相互联系的并且可以搭建一个非循环的运算图。
每一个张量都有一个

  1. grad_fn

的属性,它可以调用Function来创建Tensor,当然,如果用户自己创建了Tensor的话,那这个属性自动设置为None。

如果你想要计算引出量的话,你可以调用

  1. .backward()

在Tensor上面,如果Tensor是一个纯数的话,那么你将不必要指明任何参数;如果它不是纯数的话,你需要指明一个和张量形状匹配的梯度的参数。下面来看一些例程。

  1. import torch
  2. x = torch.ones(2,2, requires_grad=True)print(x)"""
  3. tensor([[1., 1.],
  4. [1., 1.]], requires_grad=True)
  5. """
  6. y = x +2print(y)"""
  7. tensor([[3., 3.],
  8. [3., 3.]], grad_fn=<AddBackward0>)
  9. """print(y.grad_fn)"""
  10. <AddBackward0 object at 0x7fc6bd199ac8>
  11. """
  12. z = y * y *3
  13. out = z.mean()print(z, out)"""
  14. tensor([[27., 27.],
  15. [27., 27.]], grad_fn=<MulBackward0>) tensor(27., grad_fn=<MeanBackward0>)
  16. """
  17. a = torch.randn(2,2)
  18. a =((a *3)/(a -1))print(a.requires_grad)
  19. a.requires_grad_(True)print(a.requires_grad)
  20. b =(a * a).sum()print(b.grad_fn)"""
  21. False
  22. True
  23. <SumBackward0 object at 0x7fc6bd1b02e8>
  24. """

3.5.3 优化器类

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

  1. torch.optim

提供的,例如:

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

注意:

  • 参数可以使用model.parameters()来获取,获取模型中所有requires_grad=True的参数
  • 优化类的使用方法:实例化 → 所有参数的梯度,将其值置为0 → 反向传播计算梯度 → 更新参数值
  1. from torch import optim
  2. optimizer = optim.SGD(model.parameters(), lr=1e-3)# 1. 实例化
  3. optimizer.zero_grad()# 2. 梯度置为0
  4. loss.backward()# 3. 计算梯度
  5. optimizer.step()# 4. 更新参数的值

3.5.4 损失函数

torch中也预测了很多损失函数,比如:均方误差:

  1. nn.MSELoss()

,常用于回归问题;交叉熵损失:

  1. nn.CrossEntropyLoss()

,常用于分类问题。
使用方法:

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

3.5.5 在GPU上运行代码

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

  1. 判断GPU是否可用torch.cuda.is_available()
  1. import torch
  2. print(torch.device("cuda:0"if torch.cuda.is_available()else"cpu"))'''
  3. 输出:
  4. cuda:0 # 是使用gpu
  5. cpu # 是使用cpu
  6. '''
  1. 把模型参数和input数据转化为cuda的支持类型:
  1. model.to(device)# devicecpucuda
  2. x_true.to(device)
  1. 在GPU上计算结果也为cuda的数据类型,需要转化为 numpy 或者 torch的cpu的tensor 类型:
  1. 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的签名文档如下:

  1. [docs]classDataset(Generic[T_co]):r"""An abstract class representing a :class:`Dataset`.
  2. All datasets that represent a map from keys to data samples should subclass
  3. it. All subclasses should overwrite :meth:`__getitem__`, supporting fetching a
  4. data sample for a given key. Subclasses could also optionally overwrite
  5. :meth:`__len__`, which is expected to return the size of the dataset by many
  6. :class:`~torch.utils.data.Sampler` implementations and the default options
  7. of :class:`~torch.utils.data.DataLoader`.
  8. .. note::
  9. :class:`~torch.utils.data.DataLoader` by default constructs a index
  10. sampler that yields integral indices. To make it work with a map-style
  11. dataset with non-integral indices/keys, a custom sampler must be provided.
  12. """def__getitem__(self, index)-> T_co:raise NotImplementedError
  13. 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
  1. torch.utils.data.Dataset(*args **kwds)

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

  1. classMyDataset(Dataset):def__init__(self, x, y):super().__init__()
  2. ……
  3. def__getitem__(self):return ……
  4. def__len__(self):return ……
2. TensorDataset

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

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

  1. [docs]classTensorDataset(Dataset[Tuple[Tensor,...]]):r"""Dataset wrapping tensors.
  2. Each sample will be retrieved by indexing tensors along the first dimension.
  3. Args:
  4. *tensors (Tensor): tensors that have the same size of the first dimension.
  5. """
  6. tensors: Tuple[Tensor,...]def__init__(self,*tensors: Tensor)->None:assertall(tensors[0].size(0)== tensor.size(0)for tensor in tensors),"Size mismatch between tensors"
  7. self.tensors = tensors
  8. def__getitem__(self, index):returntuple(tensor[index]for tensor in self.tensors)def__len__(self):return self.tensors[0].size(0)
  1. torch.utils.data.TensorDataset(*tensors)

要求

  1. *tensors

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

  1. my_dataset = TensorDataset(tenso_x, tensor_y)
3. DataLoader

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

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

  1. [docs]classDataLoader(Generic[T_co]):r"""
  2. Data loader. Combines a dataset and a sampler,and provides an iterable over
  3. the given dataset.
  4. The :class:`~torch.utils.data.DataLoader` supports both map-style and
  5. iterable-style datasets with single-or multi-process loading, customizing
  6. loading order and optional automatic batching (collation)and memory pinning.
  7. See :py:mod:`torch.utils.data` documentation page for more details.
  8. ……
  1. 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即可。

简单的使用样例如下:

  1. 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的签名文档:

  1. classModule:r"""Base class for all neural network modules.
  2. Your models should also subclass this class.
  3. Modules can also contain other Modules, allowing to nest them in
  4. a tree structure. You can assign the submodules as regular attributes::
  5. import torch.nn as nn
  6. import torch.nn.functional as F
  7. class Model(nn.Module):
  8. def __init__(self):
  9. super().__init__()
  10. self.conv1 = nn.Conv2d(1, 20, 5)
  11. self.conv2 = nn.Conv2d(20, 20, 5)
  12. def forward(self, x):
  13. x = F.relu(self.conv1(x))
  14. return F.relu(self.conv2(x))
  15. Submodules assigned in this way will be registered, and will have their
  16. parameters converted too when you call :meth:`to`, etc.
  17. .. note::
  18. As per the example above, an ``__init__()`` call to the parent class
  19. must be made before assignment on the child.
  20. :ivar training: Boolean represents whether this module is in training or
  21. evaluation mode.
  22. :vartype training: bool
  23. """

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

  • __init__需要调用super方法,继承父类的属性和方法
  • farward方法必须实现,用来定义我们的网络的向前计算的过程
  1. import torch.nn as nn
  2. import torch.nn.functional as F
  3. classModel(nn.Module):def__init__(self):super().__init__()
  4. self.conv1 = nn.Conv2d(1,20,5)
  5. self.conv2 = nn.Conv2d(20,20,5)defforward(self, x):
  6. x = F.relu(self.conv1(x))return F.relu(self.conv2(x))

(3) 模型训练

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

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

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

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

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

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

  1. 首先载入数据:
  1. from sklearn.datasets import load_digits
  2. from sklearn.model_selection import train_test_split
  3. X, y = load_digits(return_X_y=True)
  4. X_train, X_test, y_train, y_test = train_test_split(X, y)
  1. 构建Dataset类型数据集:
  1. import torch
  2. from torch.utils.data import TensorDataset, DataLoader
  3. X_train_tensor = torch.Tensor(X_train)
  4. y_train_tensor = torch.Tensor(y_train).long()# 主要标签需要用整数形式,否则后续用于计算交叉熵损失时报错
  5. dataset = TensorDataset(X_train_tensor, y_train_tensor)# 直接调用TensorDataset加以包裹使用
  6. dataloader = DataLoader(dataset, batch_size=128, shuffle=True)# 128个样本为一个batch,训练时设为随机
  7. X_test_tensor = torch.Tensor(X_test)# 测试集只需转化为tensor即可
  8. y_test_tensor = torch.Tensor(y_test).long()
  1. 自定义一个网络模型,仅使用Linear网络层:
  1. from torch import nn, optim
  2. classModel(nn.Module):# 继承Module基类def__init__(self, n_input=64, n_hidden=32, n_output=10):# 定义一个含有单隐藏层的全连接网络,其中输入64为手写数字数据集的特征数,输出10为类别数,隐藏层神经元数量设置32super().__init__()# 对继承自父类nn.Module的属性进行初始化。而且是用nn.Module的初始化方法来初始化继承的属性。(也可用自己的方法)# 使用全连接层和ReLU激活函数搭建网络模型
  3. self.dnn = nn.Sequential(
  4. nn.Linear(n_input, n_hidden),# nn.Lineartorch预定义好的线性模型,也被称为全链接层,
  5. nn.ReLU(),
  6. nn.Linear(n_hidden, n_output)# Linear传入的参数为输入的数量(特征数、列数),输出的数量(in_features, out_features))defforward(self, x):# 重载forward函数,从输入到输出return self.dnn(x)
  1. 八股文式的深度学习训练流程:
  1. model = Model()# 初始化模型
  2. creterion = nn.CrossEntropyLoss()# 选用交叉熵损失函数
  3. optimizer = optim.Adam(model.parameters(), lr=0.001)# 选用Adam优化器,传入模型参数,设置学习率for epoch inrange(50):# 50epochfor data, label in dataloader:# DataLoader是一个可迭代对象
  4. optimizer.zero_grad()# 待优化参数梯度清空
  5. prob = model(data)# 执行一次前向传播,计算预测结果
  6. loss = creterion(prob, label)# 评估模型损失
  7. loss.backward()# 损失反向传播,完成对待优化参数的梯度求解
  8. optimizer.step()# 参数更新if(epoch +1)%5==0:# 每隔5epoch打印当前模型训练效果with torch.no_grad():
  9. train_prob = model(X_train_tensor)
  10. train_pred = train_prob.argmax(dim=1)
  11. acc_train =(train_pred == y_train_tensor).float().mean()
  12. test_prob = model(X_test_tensor)
  13. test_pred = test_prob.argmax(dim=1)
  14. acc_test =(test_pred == y_test_tensor).float().mean()print(f"epoch: {epoch}, train_accuracy: {acc_train}, test_accuracy: {acc_test} !")

输出:

  1. epoch:4, train_accuracy:0.8507795333862305, test_accuracy:0.8577777743339539 !
  2. epoch:9, train_accuracy:0.948775053024292, test_accuracy:0.9200000166893005 !
  3. epoch:14, train_accuracy:0.9717891812324524, test_accuracy:0.9444444179534912 !
  4. epoch:19, train_accuracy:0.9799554347991943, test_accuracy:0.9577777981758118 !
  5. epoch:24, train_accuracy:0.9866369962692261, test_accuracy:0.9644444584846497 !
  6. epoch:29, train_accuracy:0.9925761222839355, test_accuracy:0.9644444584846497 !
  7. epoch:34, train_accuracy:0.9925761222839355, test_accuracy:0.9644444584846497 !
  8. epoch:39, train_accuracy:0.9962880611419678, test_accuracy:0.9666666388511658 !
  9. epoch:44, train_accuracy:0.9970304369926453, test_accuracy:0.9711111187934875 !
  10. epoch:49, train_accuracy:0.9970304369926453, test_accuracy:0.9711111187934875 !

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

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

  1. 首先是mnist数据集的准备,可直接使用torchvision包在线下载:
  1. from torchvision import datasets
  2. from torch.utils.data import DataLoader, TensorDataset
  3. train = datasets.MNIST('data/', download=True, train=True)# 从库中导出数据
  4. test = datasets.MNIST('data/', download=True, train=False)
  5. X_train = train.data.unsqueeze(1)/255.0
  6. y_train = train.targets
  7. trainloader = DataLoader(TensorDataset(X_train, y_train), batch_size=256, shuffle=True)
  8. X_test = test.data.unsqueeze(1)/255.0
  9. y_test = test.targets
  1. 然后是LeNet5的网络模型(torchvision中内置了部分经典模型,但LeNet5由于比较简单,不在其中):
  1. import torch
  2. from torch import nn, optim
  3. import torch.nn.functional as F
  4. classLeNet5(nn.Module):def__init__(self):super().__init__()
  5. self.conv1 = nn.Conv2d(1,6,5, padding=2)
  6. self.pool1 = nn.MaxPool2d((2,2))
  7. self.conv2 = nn.Conv2d(6,16,5)
  8. self.pool2 = nn.MaxPool2d((2,2))
  9. self.fc1 = nn.Linear(16*5*5,120)
  10. self.fc2 = nn.Linear(120,84)
  11. self.fc3 = nn.Linear(84,10)defforward(self, x):
  12. x = F.relu(self.conv1(x))
  13. x = self.pool1(x)
  14. x = F.relu(self.conv2(x))
  15. x = self.pool2(x)
  16. x = x.view(len(x),-1)
  17. x = F.relu(self.fc1(x))
  18. x = F.relu(self.fc2(x))
  19. x = self.fc3(x)return x
  1. 最后是模型的训练过程:
  1. model = LeNet5()
  2. optimizer = optim.Adam(model.parameters())
  3. criterion = nn.CrossEntropyLoss()for epoch inrange(10):for X, y in trainloader:
  4. pred = model(X)
  5. loss = criterion(pred, y)
  6. optimizer.zero_grad()
  7. loss.backward()
  8. optimizer.step()with torch.no_grad():
  9. y_pred = model(X_train)
  10. acc_train =(y_pred.argmax(dim=1)== y_train).float().mean().item()
  11. y_pred = model(X_test)
  12. acc_test =(y_pred.argmax(dim=1)== y_test).float().mean().item()print(epoch, acc_train, acc_test)

输出:

  1. 00.93718332052230830.940100014209747310.96698331832885740.970000028610229520.97701668739318850.978699982166290330.98091667890548710.983200013637542740.98460000753402710.985700011253356950.98669999837875370.986800014972686860.98911666870117190.987399995326995870.9899833202362060.988200008869171180.99183332920074460.98979997634887790.99089998006820680.9890000224113464

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

  1. from torchinfo import summary
  2. model = LeNet5()
  3. batch_size =256
  4. summary(model, input_size=(batch_size,1,28,28))

输出:

  1. ==========================================================================================
  2. Layer (type:depth-idx) Output Shape Param #==========================================================================================
  3. LeNet5 ----
  4. ├─Conv2d:1-1[256,6,28,28]156
  5. ├─MaxPool2d:1-2[256,6,14,14]--
  6. ├─Conv2d:1-3[256,16,10,10]2,416
  7. ├─MaxPool2d:1-4[256,16,5,5]--
  8. ├─Linear:1-5[256,120]48,120
  9. ├─Linear:1-6[256,84]10,164
  10. ├─Linear:1-7[256,10]850==========================================================================================
  11. Total params:61,706
  12. Trainable params:61,706
  13. Non-trainable params:0
  14. Total mult-adds (M):108.30==========================================================================================
  15. Input size (MB):0.80
  16. Forward/backward pass size (MB):13.35
  17. Params size (MB):0.25
  18. 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. 首先加载数据,查看各数值之间的基本情况,进行数据探索:
  1. import pandas as pd
  2. import pandas_profiling as pp
  3. df = pd.read_csv('train.csv')
  4. report = pp.ProfileReport(df)
  5. 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. 进行数据预处理
  1. # 数据预处理:列出需要用众数处理的列的列表
  2. missing_column =['HomePlanet','CryoSleep','Cabin','Destination','VIP','RoomService','FoodCourt','ShoppingMall','Spa','VRDeck']# 列出要弃掉的列的列表
  3. drop_column =['PassengerId','Cabin','Name']# 创建一个填充缺失值的函数,填充完整所有需要的数据defmissing_fill(df):for feature in missing_column:# 循环missing_column列表
  4. df[feature]= df[feature].fillna(df[feature].mode()[0])# 用众数填充缺失值
  5. df['Age']= df['Age'].fillna(df['Age'].mean())# 用平均值填充Agereturn df
  6. # 创建一个用于Cabin分列和数据类型转换的函数defcolumn_split_trans(df):# 将布尔类型转换成字符串类型
  7. bool_features =['CryoSleep','VIP']for bool_ft in bool_features:
  8. df[bool_ft]= df[bool_ft].astype('str')# 拆分'Cabin'
  9. column ='Cabin'
  10. df['Deck']= df['Cabin'].str.split('/').str[0]
  11. df['Num']= df[column].str.split('/').str[1]
  12. df['Num']= df['Num'].astype('int')# num列从字符转化成整数
  13. df['Side']= df[column].str.split('/').str[2]return df
  14. # 定义一个删除列的函数defdrop_features(df):
  15. drop_feature =['PassengerId','Cabin','Name']for ft in drop_feature:
  16. df = df.drop(ft, axis=1)# axis=1时,数组的变化是横向的,而体现出来的是列的增加或者减少return df
  17. # 定义一个编码的函数from sklearn.preprocessing import LabelEncoder
  18. defencoder(df):# 'Deck'采用LabelEncoder编码
  19. label_en = LabelEncoder()
  20. label_en.fit(df['Deck'])
  21. df['Deck']= label_en.transform(df['Deck'])# 对剩下的所有字符数据进行OneHot编码
  22. df = pd.get_dummies(df)# 当应用于DataFrame数据时,get_dummies方法只对字符串列进行转换,而其它的列保持不变return df
  23. # 将所有的数据处理流程合并,组成一个新函数defdata_processing(df):
  24. df = missing_fill(df)
  25. df = column_split_trans(df)
  26. df = drop_features(df)
  27. df = encoder(df)return df
  28. # 对数据集应用数据处理方法
  29. train_df_after = data_processing(train_df)
  30. valid_df_after = data_processing(valid_df)print(train_df_after.info())# 查看转换后的数据情况# 划分数据集(x)和预测值(y)。 由于读入的numpy数组里的元素是object类型,无法将这种类型转换成tensor。所以强制转换格式:from torch.utils.data import TensorDataset
  31. import torch
  32. train_df_x = train_df_after.drop('Transported', axis=1).astype(float)
  33. train_df_y = train_df_after.Transported.astype(float)# 将训练数据集划分为训练集和验证集,并将其转为tensor格式from sklearn.model_selection import train_test_split
  34. import numpy as np
  35. x_train, x_test, y_train, y_test = train_test_split(train_df_x, train_df_y, test_size=0.2, random_state=42)
  36. x_train = np.array(x_train)
  37. x_test = np.array(x_test)
  38. y_train = np.array(y_train)
  39. y_test = np.array(y_test)
  40. 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
  41. x_train_tensor = x_train_tensor.view(6954,1,1,20)
  42. x_test_tensor = x_test_tensor.view(1739,1,1,20)# 将标签转为long格式
  43. y_train_tensor = y_train_tensor.long()
  44. y_test_tensor = y_test_tensor.long()# 类似处理验证集(只有数据,没有标签)
  45. valid_df_x = valid_df_after.astype(float)
  46. valid_tensor_x = torch.tensor(valid_df_x.values)
  47. x_valid_tensor = valid_tensor_x.view(4277,1,1,20)# 将训练集放进的dataset,然后转换成dataloaderfrom torch.utils.data import DataLoader
  48. train_dataset = TensorDataset(x_train_tensor, y_train_tensor)
  49. train_dr = DataLoader(train_dataset, batch_size=128, shuffle=True)
  1. 构建模型
  1. # 定义网络模型from torch import nn
  2. import torch.nn.functional as F
  3. classLeNet5(nn.Module):def__init__(self):super().__init__()
  4. self.conv1 = nn.Conv2d(1,16,(1,2))# 输入数据:输入通道数(1)×输出通道数(16)×卷积核大小((1×2))
  5. self.pool1 = nn.MaxPool2d((1,2))
  6. self.conv2 = nn.Conv2d(16,32,(1,2))
  7. self.pool2 = nn.MaxPool2d((1,2))
  8. self.fc1 = nn.Linear(8*4*4,128)# 输入数据:输入特征数(8*4*4)×输出特征数(128
  9. self.fc2 = nn.Linear(128,32)
  10. self.fc3 = nn.Linear(32,2)defforward(self, out):
  11. out = out.to(torch.float32)# 解决数据类型的问题
  12. out = F.relu(self.conv1(out))
  13. out = self.pool1(out)
  14. out = F.relu(self.conv2(out))
  15. out = self.pool2(out)
  16. out = out.view(len(out),-1)# 光栅化
  17. out = F.relu(self.fc1(out))
  18. out = F.relu(self.fc2(out))
  19. out = self.fc3(out)return out
  1. 训练模型
  1. # 设置随机数种子,保证结果可复现
  2. seed =548
  3. torch.manual_seed(seed)# 设置CPU# torch.cuda.manual_seed(seed) # 设置GPU# 训练模型from torch import optim
  4. model = LeNet5()# 实例化模型
  5. optimizer = optim.Adam(model.parameters())# 选择优化器
  6. criterion = nn.CrossEntropyLoss()# 选用交叉熵作为损失函数Loss
  7. epoch =30# 设定迭代次数for epoch inrange(epoch+1):# 开始迭代循环for x, y in train_dr:# 从dataloader中取x,y
  8. pred = model(x)# 正向传播
  9. loss = criterion(pred, y)# 计算损失函数
  10. optimizer.zero_grad()# 优化器的梯度清零
  11. loss.backward()# 反向传播
  12. optimizer.step()# 参数更新# 计算准确率with torch.no_grad():# 在该模块下,所有计算得出的tensorrequires_grad都自动设置为False
  13. y_pred = model(x_train_tensor)# 得到训练集的预测标签
  14. acc_train =(y_pred.argmax(dim=1)== y_train_tensor).float().mean().item()# 计算训练集的准确率
  15. y_pred = model(x_test_tensor)# 得到测试集的预测标签
  16. 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. 模型总结
  1. # 模型总结from torchinfo import summary
  2. model = LeNet5()
  3. batch_size =128
  4. summary(model, input_size=(batch_size,1,1,20))

7.输出:

  1. RangeIndex:8693 entries,0 to 8692
  2. Data columns (total 21 columns):# Column Non-Null Count Dtype ----------------------------0 Age 8693 non-null float64
  3. 1 RoomService 8693 non-null float64
  4. 2 FoodCourt 8693 non-null float64
  5. 3 ShoppingMall 8693 non-null float64
  6. 4 Spa 8693 non-null float64
  7. 5 VRDeck 8693 non-null float64
  8. 6 Transported 8693 non-null bool7 Deck 8693 non-null int32
  9. 8 Num 8693 non-null int32
  10. 9 HomePlanet_Earth 8693 non-null uint8
  11. 10 HomePlanet_Europa 8693 non-null uint8
  12. 11 HomePlanet_Mars 8693 non-null uint8
  13. 12 CryoSleep_False 8693 non-null uint8
  14. 13 CryoSleep_True 8693 non-null uint8
  15. 14 Destination_55 Cancri e 8693 non-null uint8
  16. 15 Destination_PSO J318.5-228693 non-null uint8
  17. 16 Destination_TRAPPIST-1e8693 non-null uint8
  18. 17 VIP_False 8693 non-null uint8
  19. 18 VIP_True 8693 non-null uint8
  20. 19 Side_P 8693 non-null uint8
  21. 20 Side_S 8693 non-null uint8
  22. dtypes:bool(1), float64(6), int32(2), uint8(12)
  23. memory usage:585.9 KB
  24. None
  25. epoch:0 Accuracy for train:0.6711245179176331 Accuracy for test:0.6589994430541992
  26. epoch:1 Accuracy for train:0.7778257131576538 Accuracy for test:0.76365727186203
  27. epoch:2 Accuracy for train:0.7920621037483215 Accuracy for test:0.77400803565979
  28. epoch:3 Accuracy for train:0.7877480387687683 Accuracy for test:0.7694076895713806
  29. epoch:4 Accuracy for train:0.791343092918396 Accuracy for test:0.7711328268051147
  30. epoch:5 Accuracy for train:0.7929249405860901 Accuracy for test:0.7717078924179077
  31. epoch:6 Accuracy for train:0.7981017827987671 Accuracy for test:0.78435879945755
  32. epoch:7 Accuracy for train:0.7976704239845276 Accuracy for test:0.7728579640388489
  33. epoch:8 Accuracy for train:0.7995398044586182 Accuracy for test:0.7768832445144653
  34. epoch:9 Accuracy for train:0.7963761687278748 Accuracy for test:0.7774583101272583
  35. epoch:10 Accuracy for train:0.791630744934082 Accuracy for test:0.7803335189819336
  36. epoch:11 Accuracy for train:0.7965199947357178 Accuracy for test:0.7797584533691406
  37. epoch:12 Accuracy for train:0.8016968369483948 Accuracy for test:0.78435879945755
  38. epoch:13 Accuracy for train:0.7988207936286926 Accuracy for test:0.7809085845947266
  39. epoch:14 Accuracy for train:0.7950819730758667 Accuracy for test:0.7803335189819336
  40. epoch:15 Accuracy for train:0.800690233707428 Accuracy for test:0.78435879945755
  41. epoch:16 Accuracy for train:0.7996836304664612 Accuracy for test:0.7791834473609924
  42. epoch:17 Accuracy for train:0.8031348586082458 Accuracy for test:0.7797584533691406
  43. epoch:18 Accuracy for train:0.8029910922050476 Accuracy for test:0.7780333757400513
  44. epoch:19 Accuracy for train:0.8050042986869812 Accuracy for test:0.7860839366912842
  45. epoch:20 Accuracy for train:0.8015530705451965 Accuracy for test:0.7809085845947266
  46. epoch:21 Accuracy for train:0.8025596737861633 Accuracy for test:0.7860839366912842
  47. epoch:22 Accuracy for train:0.8025596737861633 Accuracy for test:0.7895342111587524
  48. epoch:23 Accuracy for train:0.7999712228775024 Accuracy for test:0.7832087278366089
  49. epoch:24 Accuracy for train:0.8031348586082458 Accuracy for test:0.7826337218284607
  50. epoch:25 Accuracy for train:0.802128255367279 Accuracy for test:0.7814835906028748
  51. epoch:26 Accuracy for train:0.8041415214538574 Accuracy for test:0.78435879945755
  52. epoch:27 Accuracy for train:0.802128255367279 Accuracy for test:0.78435879945755
  53. epoch:28 Accuracy for train:0.7989646196365356 Accuracy for test:0.7803335189819336
  54. epoch:29 Accuracy for train:0.8034225106239319 Accuracy for test:0.78435879945755
  55. epoch:30 Accuracy for train:0.7989646196365356 Accuracy for test:0.784933865070343==========================================================================================
  56. Layer (type:depth-idx) Output Shape Param #==========================================================================================
  57. LeNet5 ----
  58. ├─Conv2d:1-1[128,16,1,19]48
  59. ├─MaxPool2d:1-2[128,16,1,9]--
  60. ├─Conv2d:1-3[128,32,1,8]1,056
  61. ├─MaxPool2d:1-4[128,32,1,4]--
  62. ├─Linear:1-5[128,128]16,512
  63. ├─Linear:1-6[128,32]4,128
  64. ├─Linear:1-7[128,2]66==========================================================================================
  65. Total params:21,810
  66. Trainable params:21,810
  67. Non-trainable params:0
  68. Total mult-adds (M):3.85==========================================================================================
  69. Input size (MB):0.01
  70. Forward/backward pass size (MB):0.74
  71. Params size (MB):0.09
  72. Estimated Total Size (MB):0.84==========================================================================================
  73. Finish!!
  74. Process finished with exit code 0

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


本文转载自: https://blog.csdn.net/Morganfs/article/details/124180808
版权归原作者 新四石路打卤面 所有, 如有侵权,请联系我们删除。

“学习笔记:深度学习(4)&mdash;&mdash;卷积神经网络(CNN)PyTorch实践篇”的评论:

还没有评论