本文是加拿大麦吉尔大学本科生、滑铁卢大学硕士林毅在实习期间所做的工作。发帖和分享主要针对PyTorch的初学者。
本文为PyTorch基础教程,适合有一定机器学习、深度学习和神经网络理论基础,接触过卷积神经网络,还没有使用PyTorch搭建神经网络的同学。本文将分为以下几个部分:
这部分参考了MIT的卷积图像课程,讲得很清楚。
图像卷积是一种处理图像的方式。首先,存储一张M×N像素的图像,也是一个M×N矩阵。卷积过程是利用卷积核(卷积核),通常是K×K矩阵,对原始图像的每个像素进行卷积计算,得到新的M×N图像。这个卷积过程可以对原始图像进行不同的操作:模糊、锐化、描边等,而这些操作都是由卷积核的属性决定的(这一点我们不会深入研究,有兴趣的读者可以直接观看视频)。我们用一个简单的例子来解释一下卷积的具体计算方法。
如图所示,这是一个3×3的卷积核,矩阵的每个元素都是1/9。在这一步计算中,原始图像的像素对应的卷积核的中心点就是我们要计算的目标。像素周围3×3范围内的每个元素都乘以卷积核位置对应的值(这里都是1/9)。这一步又称为元素相乘(矩阵元素相乘),最终得到9。乘积之和得到新图像中像素的值。卷积核扫完所有像素后得到的新图像如下
也就是说,卷积核在计算新像素时会考虑像素周围K×K范围内的信息。如何处理这些信息是由卷积核决定的。在上面的例子中,卷积核的每个值都是1/9并且均匀分布,所以我们会得到一个比原始图像更模糊的图像。
在图神经网络CNN中,CNN会学习一个或多个卷积核,以最大化图像中捕获的信息
本节主要介绍如何使用conda安装PyTorch。 PyTorch官网也推荐Anaconda作为管理Python包的工具,因为它的兼容性可以说非常优秀。
先决条件:
步骤
首先登录Pytorch官网:https://www.sychzs.cn/
找到 安装 PyTorch 的一部分
conda 安装 pytorch torchvision torchaudio cudatoolkit=10.2 -c pytorch
作者还会介绍如何判断平台当前是否正在使用CPU/GPU进行计算。
现在让我们开始使用 PyTorch。首先介绍的是Pytorch中最重要的数据结构:Tensors。 PyTorch中所有的模型输入、输出和模型参数都以Tensor
的形式存储Tensors 实际上与 NumPy 中的 numpy.array
非常相似,但最大的区别在于 tensor
可以在 GPU 或其他硬件加速器上计算。此外,tensor
还专门针对PyTorch模块之一自动微分进行了优化。如果读者之前使用过NumPy,那么整个Tensors系统上手并不困难!
进口火炬
将 numpy 导入为 np
如前所述,PyTorch 支持在 GPU 上进行计算。那么如何检查GPU是否可用呢?我们只需要执行以下命令:
device = torch.device("cuda:0" if www.sychzs.cn_available() else "cpu")
打印(设备)
出:
CPU
说明作者的虚拟机不支持或者没有cuda进行计算。如果输出为cuda:0
,则可以使用GPU进行计算。下面的文章将教你如何将变量迁移到cuda。
除了直接从数据创建Tensor之外,还可以与numpy.array
数据 = [[1, 2], [3, 4]]
x_tensor = torch.tensor(数据)
np_arr = np.array(数据)
x_from_np = torch.from_numpy(np_arr) # numpy --> 张量
x_from_tensor = x_data.numpy() # 张量 --> numpy
同时,tensor
也有很多类似于numpy.array
的属性:
print(f"张量形状:{x_tensor.shape}")
print(f"张量的数据类型:{x_tensor.dtype}")
print(f"设备张量存储在:{x_tensor.device}")
出:
张量形状:torch.Size([2, 2])
张量的数据类型:www.sychzs.cn64
设备张量存储在:cpu
相信大家从上面可以看出,numpy.array
和tensor
在很多指令上都高度相似,比如分割、数学运算等,作者就不介绍了这里一一介绍Tensor的一些操作说明。您可以参考此链接了解详细信息。这里我只讲解上面提到的Tensor迁移到GPU的方法,这个方法比较重要。
所有 Tensors 将默认在 CPU 上创建。如果要使用GPU计算Tensor,必须使用.to()
命令将Tensor迁移到GPU:
# 如果可用,我们将张量移至 GPU
如果 www.sychzs.cn_available():
x_tensor = x_www.sychzs.cn('cuda')
或者在创建Tensor时使用之前声明的device
变量来完成迁移:
x = torch.tensor(data).to(device)
讲完Tensor的一些基本介绍,我们来看看PyTorch有哪些处理数据集的模块。当我们在做深度学习项目时,通常希望将数据集的处理部分和训练部分分开。这样我们的代码就可以具有高可读性和高可修改性。
PyTorch 提供了两个非常优秀的数据模块:torch.utils.data.Dataset
和 torch.utils.data.Dataloader
。它们在PyTorch深度学习过程中的作用如下:我们使用Dataset
来定制数据集,然后将其传递给Dataloader
来迭代数据集并将其提供给模型为了训练。
进口火炬
从 www.sychzs.cn 导入数据集,DataLoader
从 torchvision 导入数据集
从 torchvision.transforms 导入 ToTensor将 matplotlib.pyplot 导入为 plt
PyTorch 准备了很多数据供我们使用和玩转,可以从torchvision.datasets
下载。这里不再赘述,你可以参考这个链接的第一部分。
这里我就用之前做过的一个项目来看看Dataset
和Dataloadeer
的强大之处。
首先介绍一下项目背景:数据源中的每条数据都是由MNIST Digits转变而来的多位数图片,即一张图片最多有5位数字。训练目标是能够识别图片中的数字(最多 5 个)。我们给出的想法是,将训练集中的每个数据(即图片)切割成由单个数字组成的图片,并将其作为新的数据集进行训练(train);在识别(预测)阶段,原始图片也是经过裁剪后一一识别出来的。
这里省略预处理部分。经过预处理后,数据集被重构为 280,000 张个位数图像。其中,每张图片为1*28*28 numpy.array
。
第一步是使用torch.utils.data.Dataset
导入这些数据集:任何自定义数据集都需要继承该类并重写相关方法。
类CustomedDataSet(数据集):
def __init__(self,train=True,train_x=None,train_y=None,test_x=None,test_y=None,val=False,transform=None):
self.train = 火车self.val = val
self.transform = 变换
如果自我训练:
self.dataset=train_x
self.labels=train_y
埃利夫瓦尔:
self.dataset=test_x
self.labels=test_y
别的:
self.dataset= test_x
def __getitem__(自身,索引):
如果自我训练:
return torch.Tensor(self.dataset[index].astype(float)).to(device), self.labels[index].to(device)
elif self.val:
return torch.Tensor(self.dataset[index].astype(float)).to(device), self.labels[index].to(device)
别的:
return torch.Tensor(self.dataset[index].astype(float)).to(device)
def __len__(自身):
返回 self.dataset.shape[0]
作者前期已经对数据进行了预处理,并且已经完成了读取数据文件(.h5)的部分,所以整个Class看起来比较简单明了。自定义 Dataset
类必须实现 3 个函数: __init__ 、 __getitem__ 和 __len__。我们一一解释。
__init__(self, ) :
和普通的Class一样,创建Dataset
时会执行__init__函数。该函数的输入变量是完全自由的,没有任何限制。如果需要从外部文件读取数据,通常在这部分完成,以避免后续函数中重复读取。这里我们只导入numpy.array
。其中,transform
将在下一章介绍。
__getitem__(自身,索引):
__getitem__ 函数根据索引index
返回数据集中的数据。这里,我们将数据从numpy.array
更改为tensor
,迁移到device
,然后输出。除了最基本的提取数据之外,我们还可以根据需要对这个函数中的数据进行一些更改。如果一开始就初始化了transform
,这里也会使用它。
__len__(自己):
此函数返回数据集中的数据总量。
不难看出,Dataset
给了用户非常高的自由度:如何读取数据集,如何简单地处理和改变数据集(操作、分割、重塑、正则) ...) 都可以在这个类中完成,而无需编写大量额外的分散块。使用方便,统一管理。
train_set = CustomedDataSet(train_x = new_train_x, train_y = new_train_y)
val_set = CustomedDataSet(test_x = new_val_x, test_y = new_val_y, train=False, val = True)
Dataset 负责建立从索引到样本的映射,而 DataLoader 负责以特定方式从数据集中迭代生成批量样本集合。
DataLoader(数据集,batch_size = 1,shuffle = False,采样器=无,
batch_sampler=无,num_workers=0,collate_fn=无,
pin_memory=False,drop_last=False,超时=0,
worker_init_fn=无)
我们重点关注几个比较重要的参数,其余的在这个环节详细介绍。
dataset
:定义的Dataset
类用作数据集。
batch_size
(可选):一个批次包含多少个样本,默认 = 1。
shuffle
(可选):每个epoch的batch样本是否随机,默认= False。
Sampler
(可选):Sampler类可以自定义数据集中的采样方法。如果是这样,则必须shuffle=False
采样器
作为自定义采样器,它与数据集
一样可编辑。我不会在这篇文章中详细介绍。你可以参考这个博客。
train_loader = DataLoader(数据集=train_set,
批量大小=60],
随机播放=真)
val_loader = DataLoader(数据集=val_set,
批量大小=5,
随机播放=假)
这里的batch_size其实是CNN的超参数之一,需要优化。和机器学习模型一样,深度学习模型也需要通过 Cross Validation 来优化模型,参数调整过程也非常相似。这部分在本文中不会详细介绍。 val_set
和val_loader
是CV中的验证集,不需要shuffle。
创建DataLoader
之后,调用起来非常方便。当需要迭代数据集时,调用Python内置函数enumerate
或iter Just
。
对于batch_idx,枚举(train_loader)中的(数据,目标):
...
...
在枚举
过程中,数据加载器实际上根据其参数指定的策略来调用其数据集的__getitem__
sampler
方法。这里需要注意的是,第一项enumerate
输出batch_idx
不是单个数据的索引,而是整个batch的索引。请确保不要使用此索引来定位单个数据集。
如上所述,我们可以使用torchvision.transform
对我们的数据集进行一些更改(正则化、切片、重塑...),使其符合训练模型输入的格式,或者进一步提高模型训练的质量。让我们简单看一下两个变换的例子。想要了解更多的读者可以参考官网给出的说明,或者这个中文解释。
ToTensor()
可以将PIL图像或numpy.array
转换为FloatTensor
,范围为[0, 1]缩放图像的像素强度值。
Lambda 转换可以使用任何定义的 lambda 函数。这里我们举一个使用lamder函数完成10个分类标签的one-hot编码的例子。
target_transform = Lambda(lambda y: torch.zeros(
10、dtype=torch.float).scatter_(dim=0,index=torch.tensor(y),value=1))
transform 可用作下载预备数据集时的调用参数,或用作自定义数据集的 __init__
输入。接下来我们看一个下载初步数据的例子。
来自torchvision导入数据集
从 torchvision.transforms 导入 ToTensor、Lambda
ds = 数据集.FashionMNIST(根=“数据”,
火车=真,
下载=真,
变换=ToTensor(),
target_transform=Lambda(lambda y: torch.zeros(10, dtype=torch.float).scatter_(0, torch.tensor(y), value=1))
)
这里transorm
仅用于特征转换,而target_transform
用于标签转换。
准备好数据集后,我们可以开始构建卷积神经网络(CNN)。我们先看一个 CNN 的例子,然后逐步解释。
进口炬视
将 matplotlib.pyplot 导入为 plt
从火炬导入 nn,优化
导入 torch.nn.function 作为 F
类 CNN(nn.Module):
def __init__(自身):
超级(网络,自我).__init__()
self.conv1 = nn.Conv2d(1,10,5)
self.conv2 = nn.Conv2d(10,20,3)
self.fc1 = nn.Linear(20*10*10,500)
self.fc2 = nn.Linear(500, 11)
def 前向(自身,x):
输入大小 = x.size(0)
# 输入:批次*1*28*28,输出:批次*10*24*24(28-5+1)
x = self.conv1(x)
# 输出:批次*10*24*24
x = F.relu(x)
# 输入:批次*10*24*24,输出:批次*10*12*12
x = F.max_pool2d(x,2,2)
# 输入:批次*10*12*12,输出:批次*20*10*10 (12-3+1)
x = self.conv2(x)x = F.relu(x)
# 20*10*10 = 2000
x = x.view(input_size,-1)
# 输入:批次*2000 输出:批次*500
x = self.fc1(x)
x = F.relu(x)
# 输入:批次*500 输出:批次*10
x = self.fc2(x)
返回 F.log_softmax(x)
我们首先通过继承nn.Module
来定义我们的CNN类,然后在__init__
中创建CNN的每一层。神经网络的所有操作都是通过forward
函数来实现。在这个CNN例子中,一共有两个2维卷积层和两个全连接的线性层,它们通过一些激活函数连接起来,最后输出softmax分类结果。
这里,作者将所有神经网络层放在___init___
中,所有激活函数放在forward
中。当然,你也可以使用nn.Sequential()
将这些激活函数和神经网络层按顺序放置在__init__
中。
因为这是一篇CNN文章,所以我们重点讨论卷积层。刚才的例子就是作者在上一篇文章中提到的个人项目中使用的CNN。因为要识别的是二维图像,所以使用了二维卷积层torch.nn.Conv2d
。这里我们重点关注这个Class。
torch.nn.Conv2d(in_channels, out_channels, kernel_size,
步长=1,填充=0,膨胀=1,组=1,
偏差=真,padding_mode='零')
我们来看看关键参数:
in_channels
(int): 输入图像通道号
out_channels
(int): 卷积产生的通道数
kernel_size
(整数或元组):卷积核大小
stride
(int,可选):卷积步长,默认为1
我们来看一个操作示例:
输入 = torch.randn(1,1,28,28)
conv1 = nn.Conv2d(1,10,5)
输出= 卷积1(输入)
打印(输入.形状)
打印(输出.形状)
出:
火炬.Size([1, 1, 28, 28])
火炬.Size([1, 10, 24, 24])
输入图像为(1*1*28*28):前1是batch size,这里可以忽略。这是单通道、28*28 的图像。卷积层的输入也是单通道,需要和图像的通道数一致!输出为10个通道,卷积核大小为5*5。所以我们的输出自然就是(1*10*24*24):其中batch size = 1不变,图像就变成了(24*24),24 = 28 – 5 + 1。
了解了卷积层的运行模式,我们来了解一下最基本的用法:创建了Net
类后,我们直接进入输入
,然后就可以通过了 forward函数得到结果。当然,这个例子只是为了演示。模型还没有经过训练,所以每次的输出并不准确。
网络 = Net()
输出=模型(输入)
pred = 输出.argmax(1)
打印(预测)
出:
张量([9])
到目前为止,我们已经导入了预处理的数据,创建了一个可以迭代数据的Dataloader,并构建了我们的卷积神经网络。在训练 CNN 之前,我们还缺少最后两个组件:损失函数(损失函数)和优化器(优化器)。
首先我们先定义这个模型的hyperparameter,这篇文章不做Cross Validation调参的介绍和应用。:
learning_rate = 1e-3
batch_size = 60
epochs = 5
我们都知道损失函数是计算预估值和实际值时间的损失量,也是在训练过程中想要优化的对象。通常所用的损函数有:回归任务用到的nn.MSELoss
(均方误差)以及分类任务用到的nn.NLLLoss
(负对数似然)。而nn.CrossEntropyLoss
前两者的结合,对一直以来熟悉机器学习的读者应该也不会陌生。这里我们因为是分类任务,所以选择负对数似然。
loss = F.nll_loss(output, target)
大家对优化算法应该也不陌生,最常见的优化算法就是梯度下降(Gradient Descent)。我们这里采用的是随机梯度下降(Stochastic Gradient Descent)。
optimizer = torch.optim.SGD(model.parameters(), lr=learning_rate)
在一个训练循环中,优化有3个步骤:
执行www.sychzs.cn_grad
来清空系统中积累的梯度,
通过调用loss.backward()
反向传播预测损失。PyTorch会储存每一个参数对应的损失梯度。
当得到损失梯度以后,调用optimizer.step()
来实现优化和调整参数。
训练:
def train(network, train_loader, optimizer):
network.train()
for batch_idx, (data, target) in enumerate(train_loader):
www.sychzs.cn_grad()
output = network(data)
loss = F.nll_loss(output, target)
loss.backward()
optimizer.step()
这里要重点提一句:network.train()
的作用是启动 Batch Normalization 和 Dropout。在训练一个模型时,这个指令可以完全改变训练的结果,请根据业务需求来选择是否使用。
Batch Normalization
其作用对网络中间的每层进行归一化处理,并且使用变换重构(Batch Normalization Transform)保证每层提取的特征分布不会被破坏。训练时是针对每个mini-batch的,但是测试是针对单张图片的,即不存在batch的概念。由于网络训练完成后参数是固定的,每个batch的均值和方差是不变的,因此直接结算所有batch的均值和方差。
Batch Normalization有非常多的好处,其中最直接的还是训练提速:该算法背后的理论可以支持我们选择比较大的初始学习率。我们也知道当学习率变大时,随机梯度下降算法收敛得也就越快。这样我们可以不用繁琐地调整学习率,大大提高了优化模型的效率。
想要进一步了解 BatchNormalization 的同学可以移步这篇博客。有英语基础的同学也可以直接阅读这个算法的源文献:《Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift》。
Dropout
其作用克服Overfitting,在每个训练批次中,通过忽略一半的特征检测器,可以明显的减少过拟合现象。
准确度:
def accuracy(epoch_idx, test_loader, network, set_type = None):
correct = 0
with www.sychzs.cn_grad():
for data, target in test_loader:
outputs = network(data)
_, predicted = torch.max(www.sychzs.cn, 1)
correct += (predicted == target).sum().item()
if set_type == "train":
print('\nEpoch{}: Train accuracy: {}/{} ({:.0f}%)\n'.format(
epoch_idx, correct, len(test_loader.dataset),
100. * correct / len(test_loader.dataset)))
if set_type == "val":
print('\nEpoch{}: Test accuracy: {}/{} ({:.0f}%)\n'.format(
epoch_idx, correct, len(test_loader.dataset),
100. * correct / len(test_loader.dataset)))
return correct / len(test_loader.dataset)
train_set = CustomedDataSet(train_x = new_train_x, train_y = new_train_y)
val_set = CustomedDataSet(test_x = new_val_x, test_y = new_val_y, train=False, val = True)
train_loader = DataLoader(dataset=train_set,batch_size=104,shuffle=True,)
val_loader = DataLoader(dataset=val_set,batch_size=batch_size_test,shuffle=False)
network = Net().to(device)
optimizer = optim.SGD(network.parameters(), lr=learning_rate,momentum=momentum)
for i in range(1,n_epochs+1):
print(f"Epoch {t+1}\n-------------------------------")
train(epoch_idx = i, train_loader = train_loader, optimizer = optimizer, network = network)
train_accuracy = accuracy(epoch_idx=i, test_loader = train_loader, network = network, set_type = "train")
val_accuracy = accuracy(epoch_idx=i, test_loader = val_loader, network = network, set_type = "val")
Pytorch支持把训练好的神经网络用以下两种不同的方式保存到到本地路径;相对的,导入方式也不太一样:
www.sychzs.cn(network, PATH) # PATH variable should be stated
trained_net = torch.load(PATH) # PATH same as above
保存网络中的参数
这样做的好处是速度快,占用的空间少。但是导入网络参数时,要先创建一个网络变量:
www.sychzs.cn(network.state_dict(), PATH) #保存参数
trained_net = Net().to(device) #先创建一个网络变量
trained_net.load_state_dict(torch.load('model_weights.pth')) # 再导入参数
trained_net.eval()
这里也是要提一嘴 Network.eval()
的作用:
不启用 Batch Normalization(BN) 和 Dropout,保证BN和dropout不发生变化,pytorch框架会自动把BN和Dropout固定住,不会取平均,而是用训练好的值,不然的话,一旦test的batch_size过小,很容易就会被BN层影响结果。
大家都知道任何机器学习和深度学习的模型都需要通过 Cross Validation 反复训练找到预测精准度最高的 hyperparameter 组合。像是 Grid Search 和 Random Search 相信大家也都不陌生了,不在这篇文章里做详细的介绍和实现了。这里给大家介绍CNN模型中的hyperparameter。
对于任何类型的神经网络来说,网络的结构(Architecture)都是影响预测精准性的第一因素。举个例子,对于图像识别来说,CNN肯定比一般的全连接线性网络有效,因为卷积层的性质就是用来解析图像的。那要用多少层卷积层?多少层线性层?卷积层的滤波和通道数分别是什么?
这些复杂的结构型问题显然不能通过 Cross Validation 来做出决策和判断。一个优秀的神经网络结构的背后是庞大的理论和科研支持。因此,笔者不建议大家花费大量的精力对一个CNN的结构进行优化,而是以需求为出发点去寻找目前为止被广泛认可的神经网络模型。
VGG – 图像识别(定位任务)
VGG的论文《Very Deep Convolutional Networks for Large-Scale Visual Recognition》探讨了CNN的深度对于图片识别精准度的影响。论文中对比了了从11到19层不同的CNN模型,其中16层的CNN(VGG16)被广泛使用,也同时被收录在PyTorch里可以直接调用。
GoogLeNet – 图像识别(分类任务)
如果你觉得VGG的19层已经很深了的话,那你就错了!Google研发的GoogLeNet深度已经达到了22层!但是,GoogLeNet属于稀疏CNN,其大小比VGG16小很多:GoogleNet参数为500万个(5M),VGG16参数是138M。其思想是:在神经网络结构中,去除“无效的”的激活函数,并使用很多Inception模块,利用不同大小的卷积核实现不同尺度的感知。
除了结构之外,其他的Hyperparameters就需要我们自己用Grid Search, Random Search去优化了。那这些hyperparametes包括:
Epochs数:即迭代次数
Learnin Rate(学习率):即优化器的学习率。
优化器:学习率是跟着优化器走的。上文中用的是随机梯度下降(SGD),除了学习率以外还可以调整 **动量(momentem)**和 批样本(mini batch size);在ADAM优化器里用到的是自适应性的学习率,就有其他的hyperparameters出现了。
这还是一篇基础向的文章,希望能够帮到刚刚接触PyTorch的人。
PyTorch 官网教程
Liang’s Blog
《Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift》
《Very Deep Convolutional Networks for Large-Scale Visual Recognition》
GoogLeNet
来源:juhanishen