学习资料:开源组织DataWhale整理的《深入浅出PyTorch》教程
写作目的:记录教程中自己觉得重要的和不熟悉的地方。
PyTorch是由Facebook人工智能研究小组开发的一种基于Lua编写的Torch库的Python实现的深度学习库。
PyTorch会借助ONNX所带来的落地能力在工业界逐渐走向主导地位。
待查询:ONNX是什么?
电脑之前安装过,直接输入以下代码验证是否安装成功。详细的安装步骤以后有空再整理。
import torch
torch.cuda.is_available() # 返回True表示能调用GPU
PyTorch学习资源
张量是一种对数据的描述,也就是数据以张量的形式进行表示。0维张量代表的是标量(数字);1维张量代表的是向量,2维张量代表的是矩阵;3维张量代表时间序列数据、股价、文本数据、单张彩色图片(RGB);4维张量代表视频。
张量的核心是一个数据容器,多数情况下,它包含数字,有时候它也包含字符串,但这种情况比较少。因此可以把它想象成一个数字的水桶。
在PyTorch中,torch.Tensor是存储和变换数据的主要工具。Tensor和NumPy的多维数组非常类似。然而,Tensor提供GPU计算和自动求梯度等更多功能,这些使Tensor这一数据类型更加适合深度学习。
几种创建tensor的方法
import torch
### torch.rand()
# 随机初始化矩阵
x = torch.rand(4, 3)
print(x)
### torch.zeros()
# 全0矩阵
zero_0 = torch.zeros(4, 3, dtype=torch.long)
print(zero_0)
### torch.zero_()和torch.zeros_like()
# 将现有矩阵转换为全0矩阵
zero_1 = torch.zero_(x)
print(zero_1)
zero_2 = torch.zeros_like(x)
print(zero_2)
### torch.tensor()
# 有数据,直接创建tensor
x = torch.tensor([5.5, 3])
print(x)
### torch.randn_like()
# 基于已存在的tensor,创建一个新tensor
# step1 先创建一个新的全1矩阵tensor
x = x.new_ones(4, 3, dtype=torch.double) # 等价于 x = torch.ones(4, 3, dtype=torch.double)
print(x)
# step2 基于上面的全1矩阵,创建和他维度一样的随机初始化矩阵
y = torch.randn_like(x)
print(y)
# step3 对比。
# y与x有相同的torch.size, torch.shape, torch.dtype和torch.device
print(x.size())
print(y.size()) # torch.Size([张量的长, 张量的宽]) tuple类型
print(x.shape)
print(y.shape)
print(x.dtype)
print(y.dtype)
print(x.device)
print(y.device)
tensor([[0.6386, 0.8119, 0.9890],
[0.0047, 0.5326, 0.0725],
[0.6947, 0.6405, 0.9789],
[0.2853, 0.7300, 0.9825]])
tensor([[0, 0, 0],
[0, 0, 0],
[0, 0, 0],
[0, 0, 0]])
tensor([[0., 0., 0.],
[0., 0., 0.],
[0., 0., 0.],
[0., 0., 0.]])
tensor([[0., 0., 0.],
[0., 0., 0.],
[0., 0., 0.],
[0., 0., 0.]])
tensor([5.5000, 3.0000])
tensor([[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.]], dtype=torch.float64)
tensor([[-0.9246, 1.1305, 0.8360],
[ 1.3431, 0.6830, 2.4770],
[ 0.3879, 0.1182, 0.2054],
[-0.1367, 1.2852, -0.3673]], dtype=torch.float64)
torch.Size([4, 3])
torch.Size([4, 3])
torch.Size([4, 3])
torch.Size([4, 3])
torch.float64
torch.float64
cpu
cpu
还有一些其他的构造Tensor的方法,以后用到再记录。
case1:x + y
case2:torch.add(x, y)
case3:y.add_(x) (无需赋值,in-place,y的原值被修改)
注意:索引出来的结果与原数据共享内存,修改一个,另一个会跟着修改。如果不想修改,可以考虑使用copy()等方法
import torch
x = torch.rand(4,3)
print(x)
# 取第二列
print(x[:, 1])
# 取第一行,然后第一行所有值+1,打印
y = x[0,:]
y += 1
print(y)
print(x[0, :]) # 源tensor也被修改了
tensor([[0.9957, 0.0253, 0.6871],
[0.8044, 0.5909, 0.2902],
[0.3093, 0.5548, 0.6380],
[0.6114, 0.3955, 0.6012]])
tensor([0.0253, 0.5909, 0.5548, 0.3955])
tensor([1.9957, 1.0253, 1.6871])
tensor([1.9957, 1.0253, 1.6871])
注意: torch.view() 返回的新tensor与源tensor共享内存(其实是同一个tensor),更改其中的一个,另外一个也会跟着改变。(顾名思义,view()仅仅是改变了对这个张量的观察角度)
import torch
x = torch.randn(4, 4)
y = x.view(16)
z = x.view(-1, 8) # -1是指这一维的维数由其他维度决定
print(x.size(), y.size(), z.size())
print(x)
print(y)
print(z)
print()
# xyz共享内存
x += 1
print(x) # 每一元素都+1
print(y) # 每一元素也+1了
torch.Size([4, 4]) torch.Size([16]) torch.Size([2, 8])
tensor([[-0.3599, -0.5624, -1.0338, -0.1857],
[-0.5154, -1.0594, 0.7002, -0.8297],
[ 0.2035, -0.3801, -2.1994, -2.0044],
[-0.5964, -2.0699, 0.2447, 0.2832]])
tensor([-0.3599, -0.5624, -1.0338, -0.1857, -0.5154, -1.0594, 0.7002, -0.8297,
0.2035, -0.3801, -2.1994, -2.0044, -0.5964, -2.0699, 0.2447, 0.2832])
tensor([[-0.3599, -0.5624, -1.0338, -0.1857, -0.5154, -1.0594, 0.7002, -0.8297],
[ 0.2035, -0.3801, -2.1994, -2.0044, -0.5964, -2.0699, 0.2447, 0.2832]])
tensor([[ 0.6401, 0.4376, -0.0338, 0.8143],
[ 0.4846, -0.0594, 1.7002, 0.1703],
[ 1.2035, 0.6199, -1.1994, -1.0044],
[ 0.4036, -1.0699, 1.2447, 1.2832]])
tensor([ 0.6401, 0.4376, -0.0338, 0.8143, 0.4846, -0.0594, 1.7002, 0.1703,
1.2035, 0.6199, -1.1994, -1.0044, 0.4036, -1.0699, 1.2447,
为了使创建的张量和原始张量不共享内存,我们需要使用第二种方法torch.reshape(), 同样可以改变张量的形状,但是此函数并不能保证返回的是其拷贝值,所以官方不推荐使用。
推荐的方法是我们先用 clone() 创造一个张量副本然后再使用 torch.view()进行函数维度变换 。
注:使用 clone() 还有一个好处是会被记录在计算图中,即梯度回传到副本时也会传到源 Tensor 。
待查询:这句是什么意思?
只有一个元素的tensor ,使用 .item() 来获得这个value
import torch
x = torch.randn(1)
print(type(x))
print(type(x.item()))
print(x)
print(x.item())
x = torch.randn(4, 4)
print(type(x))
print(type(x.item()))
print(x)
print(x.item())
<class 'torch.Tensor'>
<class 'float'>
tensor([0.8245])
0.8244644999504089
<class 'torch.Tensor'>
---> 10 print(type(x.item()))
11 print(x)
12 print(x.item())
ValueError: only one element tensors can be converted to Python scalars
PyTorch中的 Tensor 支持超过一百种操作,包括转置、索引、切片、数学运算、线性代数、随机数等等,具体使用方法可参考官方文档。
import torch
x = torch.arange(1, 3).view(1, 2) # 一行两列
print(x)
y = torch.arange(1, 4).view(3, 1)# 三行一列
print(y)
print(x + y) # 三行两列
tensor([[1, 2]])
tensor([[1],
[2],
[3]])
tensor([[2, 3],
[3, 4],
[4, 5]])
PyTorch 中,所有神经网络的核心是autograd包。autograd包为张量上的所有操作提供了自动求导机制。它是一个在运行时定义 (define-by-run)的框架,这意味着反向传播是根据代码如何运行来决定的,并且每次迭代可以是不同的。
利用GPU的性能来减少模型训练时间。
CUDA是NVIDIA(GPU的提供商)提供的GPU并行计算框架。对于GPU本身的编程,使用的是CUDA语言来实现的。
在PyTorch中,使用 CUDA表示要求我们的模型或者数据使用GPU。在编写程序中,当我们使用了 .cuda() 时,其功能是让我们的模型或者数据从CPU迁移到GPU(0)当中,通过GPU开始计算。
注:
为什么使用GPU时使用的是.cuda()而不是使用.gpu()?
因为现阶段PyTorch只支持部分NVIDIA的GPU,而这些GPU的编程接口都采用CUDA。像AMD的GPU编程接口采用的是OpenCL,PyTorch不支持。
数据在GPU和CPU之间进行传递时会比较耗时,我们应当尽量避免数据的切换。
GPU运算很快,但是在使用简单的操作时,我们应该尽量使用CPU去完成。
当我们的服务器上有多个GPU,我们应该指明我们使用的GPU是哪一块,如果我们不设置的话,tensor.cuda()方法会默认将tensor保存到第一块GPU上,等价于tensor.cuda(0),这将会导致爆出out of memory的错误。我们可以通过以下两种方式继续设置。
# case 1
import os
# 设置在文件最开始部分
os.environ['CUDA_VISIBLE_DEVICE'] = "2" # 设置默认的显卡
os.environ
environ{......
'CUDA_PATH': 'C:\\Program Files\\NVIDIA GPU Computing Toolkit\\CUDA\\v10.2',
'CUDA_PATH_V10_2': 'C:\\Program Files\\NVIDIA GPU Computing Toolkit\\CUDA\\v10.2',
.......
'CUDA_VISIBLE_DEVICE': '2'}
# case 2
CUDA_VISBLE_DEVICE=0,1 python train.py # 使用0,1两块GPU
在刚开始做模型并行的时候,这个方案使用的比较多。其中主要的思路是,将一个模型的各个部分拆分,然后将不同的部分放入到GPU来做不同任务的计算。
这里遇到的问题就是,不同模型组件在不同的GPU上时,GPU之间的传输就很重要,对于GPU之间的通信是一个考验。但是GPU的通信在这种密集任务中很难办到,所以这个方式慢慢淡出了视野。
第二种方式就是,同一层的模型做一个拆分,让不同的GPU去训练同一层模型的部分任务。
这样可以保证在不同组件之间传输的问题,但是在我们需要大量的训练,同步任务加重的情况下,会出现和第一种方式一样的问题。
第三种方式有点不一样,它的逻辑是,我不再拆分模型,我训练的时候模型都是一整个模型。但是我将输入的数据拆分。所谓的拆分数据就是,同一个模型在不同GPU中训练一部分数据,然后再分别计算一部分数据之后,只需要将输出的数据做一个汇总,然后再反传。
这种方式可以解决之前模式遇到的通讯问题。现在的主流方式是数据并行的方式(Data parallelism)
首先需要对数据进行预处理(数据格式的统一和必要的数据变换),同时划分训练集和测试集。接下来选择模型,并设定损失函数和优化方法,以及对应的超参数(当然可以使用sklearn这样的机器学习库中模型自带的损失函数和优化器)。最后用模型去拟合训练集数据,并在验证集/测试集上计算模型表现。
数据加载
首先,由于深度学习所需的样本量很大,一次加载全部数据运行可能会超出内存容量而无法实现;同时还有批(batch)训练等提高模型表现的策略,需要每次训练读取固定数量的样本送入模型中训练,因此深度学习在数据加载上需要有专门的设计。
模型实现
由于深度神经网络层数往往较多,同时会有一些用于实现特定功能的层(如卷积层、池化层、批正则化层、LSTM层等),因此深度神经网络往往需要“逐层”搭建,或者预先定义好可以实现特定功能的模块,再把这些模块组装起来。
损失函数和优化器
由于模型设定的灵活性,因此损失函数和优化器要能够保证反向传播能够在用户自行定义的模型结构上实现。
GPU的配置和操作
程序默认是在CPU上运行的,因此在代码实现中,需要把模型和数据“放到”GPU上去做运算,同时还需要保证损失函数和优化器能够在GPU上工作。如果使用多张GPU进行训练,还需要考虑模型和数据分配、整合的问题。此外,后续计算一些指标还需要把数据“放回”CPU。这里涉及到了一系列有关于GPU的配置和操作。
总结
深度学习中训练和验证过程最大的特点在于读入数据是按批的,每次读入一个批次的数据,放入GPU中训练,然后将损失函数反向传播回网络最前面的层,同时使用优化器调整网络参数。这里会涉及到各个模块配合的问题。训练/验证后还需要根据设定好的指标计算模型表现。
知道了深度学习任务各个部分所需的功能,下面要了解一下PyTorch是如何实现各个部分的。
在深度学习/机器学习中常用到的包
import os
import numpy as np
import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader
import torch.optim as optimizer
设置常用的超参数
batch_size = 16
# 批次的大小
lr = 1e-4
# 优化器的学习率
max_epochs = 100
GPU设置
# 方案一:使用os.environ,这种情况如果使用GPU不需要设置
os.environ['CUDA_VISIBLE_DEVICES'] = '0,1'
# 方案二:使用“device”,后续对要使用GPU的变量用.to(device)即可
device = torch.device("cuda:1" if torch.cuda.is_available() else "cpu")
PyTorch数据读入是通过Dataset+DataLoader的方式完成的,Dataset定义好数据的格式和数据变换形式,DataLoader用iterative的方式不断读入批次数据。
下面以cifar10数据集为例展示PyTorch数据读入。
待参考文章:
https://cloud.tencent.com/developer/article/2030188
https://blog.csdn.net/weixin_43229819/article/details/125973191
数据集官网
我们可以定义自己的Dataset类来实现灵活的数据读取,定义的类需要继承PyTorch自身的Dataset类。
class MyDataset(Dataset):
_init_: 用于向类中传入外部参数,同时定义样本集
def __init__(self, data_dir, info_csv, image_list, transform=None):
"""
Args:
data_dir: path to image directory.
info_csv: path to the csv file containing image indexes
with corresponding labels.
image_list: path to the txt file contains image names to training/validation set
transform: optional transform to be applied on a sample.
# data_dir: 图像目录的路径
# info_csv: 包含图像对应标签的csv文件的路径
# image_list: 指向训练集和测试集的图像名称
# transform: 应用于样本的可选转换
"""
label_info = pd.read_csv(info_csv)
image_file = open(image_list).readlines()
self.data_dir = data_dir
self.image_file = image_file
self.label_info = label_info
self.transform = transfor
_getitem_: 用于逐个读取样本集合中的元素,可以进行一定的变换,并将返回训练/验证所需的数据
def __getitem__(self, index):
"""
Args:
index: the index of item
# index: 项目的索引
Returns:
image and its labels
# 图像及其标签
"""
image_name = self.image_file[index].strip('\n')
raw_label = self.label_info.loc[self.label_info['Image_index'] == image_name]
label = raw_label.iloc[:,0]
image_name = os.path.join(self.data_dir, image_name)
image = Image.open(image_name).convert('RGB')
if self.transform is not None:
image = self.transform(image)
return image, label
_len_: 用于返回数据集的样本数
def __len__(self):
return len(self.image_file)
from torch.utils.data import DataLoader
# batch_size:样本是按批读入的,batch_size就是每次读入的样本数
# num_workers:有多少个进程用于读取数据
# shuffle:是否将读入的数据打乱
# drop_last:对于样本最后一部分没有达到批次数的样本,使其不再参与训练
train_loader = torch.utils.data.DataLoader(train_data, batch_size=batch_size, num_workers=4, shuffle=True, drop_last=True)
val_loader = torch.utils.data.DataLoader(val_data, batch_size=batch_size, num_workers=4, shuffle=False)
import matplotlib.pyplot as plt
images, labels = next(iter(val_loader))
print(images.shape)
plt.imshow(images[0].transpose(1,2,0))
plt.show()
未完待续,周末补全。