学习时间: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)张量的生成
- 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]])
'''
- 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]]])
'''
- 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]]])
'''
- 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]])
'''
- 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]])
'''
- 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个基础操作,详细链接在这。
- 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])
'''
- Tensor.item查看张量的值(张量的大小)
只能查看单个tensor的值。
x = torch.randn(1)
print(x.item())
y = torch.randn(4)
print(x[:2].item())
'''
-0.2167293280363083
-0.2167293280363083
'''
- 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])
'''
- 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.])
'''
- 张量和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来进行训练。此时我们的代码需要稍作调整:
- 判断GPU是否可用
torch.cuda.is_available()
:
import torch
print(torch.device("cuda:0" if torch.cuda.is_available() else "cpu"))
'''
输出:
cuda:0 # 是使用gpu
cpu # 是使用cpu
'''
- 把模型参数和input数据转化为cuda的支持类型:
model.to(device) # device是cpu或cuda
x_true.to(device)
- 在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 indi