深度学习Pytorch框架的入门简易代码模板及解析

Posted 追风赶月。

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深度学习Pytorch框架的入门简易代码模板及解析相关的知识,希望对你有一定的参考价值。

  记录本人学习深度学习的过程,采用的是Pytorch框架,代码中间会有自己的理解,可能会有错误,希望各位大佬批评指正,也与各位在AI路上的学习者共勉!

一、前期操作

  这是在命令行或者终端中输入的代码,可以用于查询自己GPU的显存、利用率和CUDA的版本,仅对NVIDIA英伟达独立显卡有用,集成显卡是不能用的。一般来说,显卡的种类决定了CUDA的版本,CUDA的版本跟要安装的Pytorch版本有关系,而GPU的利用率越高,比如100%,说明了处于满负荷的状态,能够较大的发挥显卡的性能,可以实时查看一下,如果利用率很低就要检查一下自己的代码了。

!nvidia-smi

  正常会输出类似的东西,可以看到CUDA的Version是11.2,,显存是15109MiB,相当于是15G了,右边显示0%,说明目前的显卡利用率是0%,并且下面的Processes显示没有进程。

+-----------------------------------------------------------------------------+
| NVIDIA-SMI 460.32.03    Driver Version: 460.32.03    CUDA Version: 11.2     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|                               |                      |               MIG M. |
|===============================+======================+======================|
|   0  Tesla T4            Off  | 00000000:00:04.0 Off |                    0 |
| N/A   59C    P0    28W /  70W |      0MiB / 15109MiB |      0%      Default |
|                               |                      |                  N/A |
+-------------------------------+----------------------+----------------------+
                                                                               
+-----------------------------------------------------------------------------+
| Processes:                                                                  |
|  GPU   GI   CI        PID   Type   Process name                  GPU Memory |
|        ID   ID                                                   Usage      |
|=============================================================================|
|  No running processes found                                                 |
+-----------------------------------------------------------------------------+

二、导入函数库

  导入一些需要用到的函数库,比如torch、torch.nn、DataLoader等等,这里导入time函数库是为了记录一下训练网络的时间。

import torch
import torch.nn as nn
import torchvision
from torch.utils.data import DataLoader
import time
import torch.nn.functional as F

三、定义随机种子

  如果只是自己学习,这一步应该是不需要的,固定随机种子的目的是为了保证自己的模型结果是可以复现的,这在科研工作中十分重要,如果你的实验只有你自己能做出最好的结果,别人都做不出来,那你的研究就没有什么意义了。
  说到固定随机种子,就想到一篇很有意思的论文《Torch.manual_seed(3407) is all you need: On the influence of random seeds in deep learning architectures for computer vision》,中文译名是Torch.manual_seed(3407) is all you need:论随机种子对计算机视觉深度学习架构的影响,作者探究了随机种子选择对准确性的影响,用了10000个种子进行搜索,最终发现3407这个种子相较于平均值特别好,所以如果不知道设置什么种子,不如就设置它哈哈哈。
  随机种子固定后,随机梯度下降的方向就会一样的,不带有随机性了(避免深度炼丹),而且用到Dataloader的时候也会让它加载的数据序列是一样的。

import os
import random
import numpy as np
def my_seed(seed=3407):
	random.seed(seed)
	np.random.seed(seed)
	torch.manual_seed(seed)
	torch.cuda.manual_seed(seed)
# 调用一下
my_seed(seed=3407)

四、定义网络结构Network

  个人的写代码顺序比较喜欢先写网络结构,毕竟它是我们之后学习和“魔改”的主要对象。下面写一个自己设计的简单网络,考虑到我们等会用的数据集是CIFAR10,是一个10分类的数据集,所以out_channels是10,而我们用的数据集图片是3个RGB通道的,所以in_channels是3。
  我们自己定义网络结构的时候,要首先继承nn.Module,并且重写构造函数__init__和forward方法。
  (1)__init__构造函数中包含了网络中可学习参数的层,比如全连接、池化层、卷积层等,不含可学习参数的层其实也可以放在里面,比如各种激活函数,但是个人更推荐直接放在forward方法里面,用nn.functional as F来方便调用,形式就像F.relu()。
  (2)forward方法是必须要重写的,它是实现网络模型具体功能的方法,用于数据流通、连接各个层的。一定要按照顺序来写。

class DeepNet(nn.Module):
  def __init__(self, in_channels, out_channels):
    super(DeepNet, self).__init__()
    self.conv1 = nn.Conv2d(in_channels, 64, 3)
    self.conv2 = nn.Conv2d(64, 64, 3, stride=1, padding=1) 
    self.max_pooling = nn.MaxPool2d(2, stride=2, padding=1)
    self.bn = nn.BatchNorm2d(64)
    self.fc1 = nn.Linear(112*112*64, 64)
    self.fc2 = nn.Linear(64, out_channels)

  def forward(self, x):
    x = self.conv1(x)
    x = self.bn(x)
    x = F.relu(x)
    
    x = self.conv2(x)
    x = self.bn(x)
    x = F.relu(x)
    
    x = self.max_pooling(x)
    x = x.view(x.shape[0],-1)
    x = self.fc1(x)
    x = F.relu(x)
    x = self.fc2(x)

    return x

  这里输入的图像大小是224x224(写到这里,突然觉应该先写数据处理和数据集,因为网络结构跟数据有关系┭┮﹏┭┮,之后一定要注意),解释一下nn.Conv2d(in_channels, 64, 3),这里说明图像进入的通道数是in_channels=3,这很合理,然后出去的通道数是64,升维度了,然后卷积核Kernel_size是3,其他都是默认,比如padding默认是0,stride默认是1,经过这第一个卷积,图像的大小发生改变了,具体的变化跟kernel_size、padding、stride有关系,总之这里的输出的特征图大小由原本的224x224变为了222x222。

图像的输出长宽 = (图像输入的长宽-卷积核kernel_size+2*padding)/步长stride+1
222 = (224-3+0)/ 1+1

  这里先定义一个网络,然后把它打印出来看看:

net=DeepNet(3,10)
print(net)

DeepNet(
(conv1): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1))
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(max_pooling): MaxPool2d(kernel_size=2, stride=2, padding=1, dilation=1, ceil_mode=False)
(bn): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(fc1): Linear(in_features=802816, out_features=64, bias=True)
(fc2): Linear(in_features=64, out_features=10, bias=True)
)

四、定义Transform数据处理

  定义transform的相关操作,会在datasets中得到使用,这里定义了转换数据为tensor、规定图像的大小、均值化和随机水平翻转的操作。

transform = torchvision.transforms.Compose(
    [
        torchvision.transforms.ToTensor(),  # 将数据转换为pytorch的tensor
        torchvision.transforms.Resize(size=([224, 224])),
        torchvision.transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]),  # 均值、方差
        torchvision.transforms.RandomHorizontalFlip()  # 随机水平翻转
    ]
)
train_data=torchvision.datasets.CIFAR10(root="dataset",train=True,transform=transform,download=True)
test_data=torchvision.datasets.CIFAR10(root="dataset",train=False,transform=transform,download=True)
train_data_size=len(train_data)
test_data_size=len(test_data)
print("训练集的长度为".format(train_data_size))
print("测试集的长度为".format(test_data_size))

训练集的长度为50000
测试集的长度为10000

五、定义Dataloader

train_dataloader=DataLoader(train_data,batch_size=64)
test_dataloader=DataLoader(test_data,batch_size=64)

六、创建网络、损失函数和优化器,定义相关的超参数

  这里.cuda()是将网络和损失函数放到显卡里面去计算,如果没有显卡的话这里可以删除.cuda()

model=DeepNet(3,10).cuda()
loss_fn=nn.CrossEntropyLoss()
loss_fn=loss_fn.cuda()
lr_rate=1e-4
optimizer=torch.optim.Adam(model.parameters(),lr=lr_rate)

total_train_step=0
total_test_step=0
epoch=10
start_time=time.time()
min_loss=100

七、开始训练、测试并保存模型

  本质上其实是两层循环,首先第一层训练是训练次数i的循环,在这一层循环中,如果模型中有BN层(Batch Normalization)和Dropout,需要在训练时添加model.train(),将模型调整为训练状态,从而启用BN层和Dropout,我们这里有用BN层,所以要用。同时,在测试时添加model.eval(),作用是不启用BN和Dropout层。
  我们都知道,在训练模式中,BN层作用是去计算数据的方差和均值,对不同样本的同一个通道直接做归一化处理;Dropout会按照设置的参数决定保留激活单元的概率。那么在model.eval()下,Dropout层会让所有的激活单元都通过,而BN层会停止计算和更新数据的方差和均值。同时的话,模型在测试模式下不会更新权重(不可能在测试集上进行训练),但是不会影响各层的梯度计算行为,会像训练模式一样梯度计算和存储,只是不进行反向传播,因此才能在测试模型下计算损失。
  第二层训练就是数据在dataloader中的迭代。当一个完整的数据集通过了神经网络一次并且返回了一次,这个过程称为一轮epoch,而一轮epoch训练中包含了多次的迭代过程,这就有了batch的概念,也就是批次。当不能将数据一次性通过神经网络的时候(因此显卡的显存是有限的),就需要将数据集分成几个batch,比如设置batch=100,训练集的总量是1000,那么一轮epoch训练就有1000/100=10次迭代(iteration)。
  在每轮epoch中,我们都要获得数据和标签,这里的数据形式是图像,也就是imgs,标签是targets,把它们存到显卡中计算,然后去计算图像在模型中训练获得的结果,对应了outputs=model(imgs),outputs就是获得的模型预测标签,既然是训练就可能有误差,从而我们计算损失,对应了loss=loss_fn(outputs, targets),注意这里的targets是指数据集中给出的正确标签,去计算它们之间的差距。
  optimizer.zero_grad()、loss.backward()和optimizer.step()分别是说,先将梯度归零,然后反向传播计算每个参数的梯度值,最后通过梯度下降进行参数更新,也就完成了一次迭代过程。
  测试过程是类似的,只不过多了计算accuracy的过程,accuracy=(outputs.argmax(1)==targets).sum()是指判断模型返回向量中值最大的那个标签,是否跟真实标签一致,如果是那么就求和算进accuracy。

for i in range(epoch):
  print("==========================第轮训练开始==========================".format(i+1))
  model.train()
  for data in train_dataloader:
    imgs, targets = data
    imgs=imgs.cuda()
    targets=targets.cuda()
    outputs=model(imgs)
    loss=loss_fn(outputs,targets)

    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    total_train_step+=1

    if total_train_step%100==0:
      end_time=time.time()
      print("训练时间:".format(end_time-start_time))
      print("训练次数:,Loss:".format(total_train_step, loss))

  model.eval()
  total_test_loss = 0
  total_accuracy = 0
  with torch.no_grad():
    for data in test_dataloader:
      imgs, targets = data
      imgs=imgs.cuda()
      targets=targets.cuda()
      outputs=model(imgs)
      loss=loss_fn(outputs,targets)
      total_test_loss=total_test_loss+loss
      accuracy=(outputs.argmax(1)==targets).sum()
      total_accuracy+=accuracy
  print("整体测试集上的loss:".format(total_test_loss))
  print("整体测试集上的accuracy:".format(total_accuracy/test_data_size))
  total_test_step+=1
  
  if total_test_loss<min_loss:
    min_loss=total_test_loss
    torch.save(model,"model_".format(i))
    print("最好的模型已经保存,在第轮".format(i))
      

  后续会尝试用tqdm函数库来显示训练的进度条,以提升炼丹过程中的愉悦感,这套代码将网络结构换一下就可以接着用,不过整个框架还不是非常完善,还缺少单独的predict等可视化操作,还有待进一步改进!

完整的代码(有英伟达显卡的可以直接跑,没有的话要删掉.cuda()就行)

import torch
import torch.nn as nn
import torchvision
from torch.utils.data import DataLoader
import time
import torch.nn.functional as F

class DeepNet(nn.Module):
  def __init__(self, in_channels, out_channels):
    super(DeepNet, self).__init__()
    self.conv1 = nn.Conv2d(in_channels, 64, 3)
    self.conv2 = nn.Conv2d(64, 64, 3, stride=1, padding=1) 
    self.max_pooling = nn.MaxPool2d(2, stride=2, padding=1)
    self.bn = nn.BatchNorm2d(64)
    self.fc1 = nn.Linear(112*112*64, 64)
    self.fc2 = nn.Linear(64, 10)

  def forward(self, x):
    x = self.conv1(x)
    x = self.bn(x)
    x = F.relu(x)
    
    x = self.conv2(x)
    x = self.bn(x)
    x = F.relu(x)
    
    x = self.max_pooling(x)
    x = x.view(x.shape[0],-1)
    x = self.fc1(x)
    x = F.relu(x)
    x = self.fc2(x)

    return x

transform = torchvision.transforms.Compose(
    [
        torchvision.transforms.ToTensor(),  # 将数据转换为pytorch的tensor
        torchvision.transforms.Resize(size=([224, 224])), # 将图像的大小设置为224*224
        torchvision.transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]),  # 均值、方差
        torchvision.transforms.RandomHorizontalFlip()  # 随机水平翻转
    ]
)
train_data=torchvision.datasets.CIFAR10(root="dataset",train=True,transform=transform,download=True)
test_data=torchvision.datasets.CIFAR10(root="dataset",train=False,transform=transform,download=True)

train_data_size=len(train_data)
test_data_size=len(test_data)
print("训练集的长度为".format(train_data_size))
print("测试集的长度为".format(test_data_size))

train_dataloader=DataLoader(train_data,batch_size=64)
test_dataloader=DataLoader(test_data,batch_size=64)

model=DeepNet(3,10).cuda()
loss_fn=nn.CrossEntropyLoss()
loss_fn=loss_fn.cuda()

lr_rate=1e-4
optimizer=torch.optim.Adam(model.parameters(),lr=lr_rate)
total_train_step=0
total_test_step=0
epoch=10
start_time=time.time()
min_loss=100

for i in range(epoch):
  print("==========================第轮训练开始==========================".format(i+1))
  model.train()
  for data in train_dataloader:
    imgs, targets = data
    imgs=imgs.cuda()
    targets=targets.cuda()
    outputs=model(imgs)
    loss=loss_fn(outputs,targets)

    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    total_train_step+=1

    if total_train_step%100==0:
      end_time=time.time()
      print("训练时间:".format(end_time-start_time))
      print("训练次数:,Loss:".format(total_train_step, loss))

  model.eval()
  total_test_loss = 0
  total_accuracy = 0
  with torch.no_grad():
    for data in test_dataloader:
      imgs, targets = data
      imgs=imgs.cuda()
      targets=targets.cuda()
      outputs=model(imgs)
      loss=loss_fn(outputs,targets)
      total_test_loss=total_test_loss+loss
      accuracy=(outputs.argmax(1)==targets).sum()
      total_accuracy+=accuracy
  print以上是关于深度学习Pytorch框架的入门简易代码模板及解析的主要内容,如果未能解决你的问题,请参考以下文章

PyTorch简易入门

最新翻译的官方 PyTorch 简易入门教程

每月好书深度学习框架PyTorch入门与实践

书籍深度学习框架:PyTorch入门与实践(附代码)

简易的深度学习框架Keras代码解析与应用

技术观点简易的深度学习框架Keras代码解析与应用