学习 Pytorch 就像打游戏,想停也停不下来(1)

·  阅读 4167
学习 Pytorch 就像打游戏,想停也停不下来(1)

希望将学习变为一场游戏,但是学习毕竟不是游戏。而是自己自发地感觉自己沉迷于 pytorch。

003.jpeg

我们如何用 Pytorch 实现一个网络,其实现在网络写得是越来越复杂,一打眼很难将作者思路缕清,我们应该如何可以读懂当下一些热门论文的实现呢。其实我们读不通通常是基础知识不牢,没有像作者一步一步走来,而是插入到这个行业,不急,今天开始我们就从简单开始。搭建一个简单网络通常需要以下基本。

  • 分解任务问题
  • 然后找到一个合适函数集,通常我们都是用具有一定结构网络来模拟复杂函数集
  • 收集合适数据,数据需要能够正确反映我们任务
  • 然后设定一个目标,对于神经网络就是损失函数,这样模型参数调整就用了方向
  • 设定调整参数的策略,也就是优化器
  • 设定,也就是我们任务更关心那些指标,这些指标可以反映任务能力,例如准确率、精准率和召回率

我们一切都是 pytorch 为基础做的系列分享,大家喜欢 pytorch 理由就是提供优雅基于模块和类的设计,

import torch
import torch.nn as nn
复制代码

首先导入的是 torch 这个全局对象,然后就是导入 torch.nn 这个模块主要和网络设计相关模块,提供我们设计一个神经网络所需要类。

import torch.nn.functional as F
复制代码

在这个模块中,提供了一些定义卷积、激活函数等方法,用过一段时间 pytorch 大家可能会发现 torch.nn.functional 和 torch.nn 提供运算会有一定交叉,那么他们两者具体有什么区别呢? 具体来说 torch.nn 提供方法都是继承于 nn.Module 都是具有状态的,而 torch.nn.functional 只是运算没有状态,需要输入参数,而是具有其内部参数的。例如,一个nn.Conv2d模块会有一些内部属性,如self.weight。然而,F.conv2d只是定义了操作,需要传递所有的参数(包括权重和偏置)。

说白了也就是相比于 torch.nn.functional 来说 torch.nn 更加全面,既然如此为什么我们还需要 torch.nn.functional,虽然nn.module 能够满足大多数情况,但是 nn.functional 没有什么额外包袱,所以他相对于 torch.nn 更加灵活性,所以也是不可缺少。

x = torch.randn(1, 1)
w = nn.Parameter(torch.randn(1, 1))

output = x * w
print(output)
复制代码

如果变量需要参与到梯度计算,那么变量参与计算返回 tensor 就具有一个 grad_fn 属性,并且在输出 tensor 进行求解反向传播后,就可以在参数的 grad 属性。

tensor([[-0.1428]], grad_fn=<MulBackward0>)
复制代码
output.backward()
print(w.grad)
复制代码
tensor([[-0.2470]])
复制代码

001.jpeg

完成一个小插曲,我们继续主线任务,继续引入我们需要包

from torch.utils.data import DataLoader

import torchvision.datasets as datasets 
import torchvision.transforms as transforms
复制代码

torch.utils.data 导入 DataLoader 用于加载数据集,随后我们会分享关于如何自定义数据集,torchvision 之前给大家介绍这个模块,是利用 pytorch 提供一些视觉方面预定义模型或者数据集,这些工具帮助我们方便地进行视觉方向研究。

002.png

定义网络

class Net(nn.Module):
  def __init__(self,input_size,num_classes):
    super(Net,self).__init__()
    self.hidden_dim = 30
    self.fc1 = nn.Linear(input_size,self.hidden_dim)
    self.fc2 = nn.Linear(self.hidden_dim,num_classes)

  def forward(self,x):
    x = F.relu(self.fc1(x))
    x = self.fc2(x)
    return x
复制代码

通常我们会在 init 也就是初始化时,定义一些网络出现基本模块或者层,然后 forward 里我们要做的就是将这些层或者模块有效组织在一起。网络结构是由两个全连接组成,中间用 ReLU 激活函数,input_size 用于指定样本的大小,而 num_classes 指定类别数量。然后前向传播时需要输入 x

model = Net(784,10)
x = torch.randn(64,784)
print(model(x).shape)
复制代码

定义一个网络指定输入样本大小为 28 x 28 也就是 784,一看就是 MINIST 数据集,每次输入是 28 x 28 图片,然后将这个只有一个通道 28 x 28 大小图展平为 784 维向量输入到网络。

005.png

x = torch.randn(64,784) 中这里 64 表示一个批次输入大小为 64 个样本,然后对于每一个样本会输出其属于 10 类别的概率分布。

torch.Size([64, 10])
复制代码

006.png

设定网络运行设备

然后就是我们模型是运行 cpu 上还是运行在 gpu 上,这里说以一句,像 python 这样语言可以在赋值用于一个条件语句来进行赋值,从一个侧面反映了 python 是一门运行解释性语言。

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
复制代码

设置超参数

input_size = 784
num_classes = 10
learning_rate = 0.001
batch_size = 64
num_epochs = 1
复制代码

所谓超参数就是超出网络控制,这些参数是不受网络支配,网络不用去学习的参数,分别是指定输入样本的大小,样本类别、学习率、每次输入模型样本数量,以及一共在总体样本上学习次数,epochs 表示模型将所有样本过次数。

准备数据集

train_dataset = datasets.MNIST(root='dataset/',train=True,transform=transforms.ToTensor(),download=True)
train_loader = DataLoader(dataset=train_dataset,batch_size=batch_size,shuffle=True)

test_dataset = datasets.MNIST(root='dataset/',train=False,transform=transforms.ToTensor(),download=True)
test_loader = DataLoader(dataset=test_dataset,batch_size=batch_size,shuffle=True)
复制代码

datasets 模块提供多个数据集,其中包括 MNIST 数据集,root 指定数据集存放位置 train 表示该数据集中用于训练样本,transform 表示对图像做预处理、download 表示是否下载,加载数据集,数据集提供加载数据方式,以及每次从数据集中获取样本的数据结构,DataLoader 是数据加载方式,也就是我们每次喂给数据数量也就是批次数量,以及每次喂给模型前是否打乱顺序。

初始化模型

model = Net(input_size=input_size,num_classes=num_classes).to(device)
复制代码

损失函数和优化器

criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(),lr=learning_rate)
复制代码

优化器选择的是交叉熵,通常分类问题都会选择这个分类器

criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(),lr=learning_rate)
复制代码

训练模型

开始训练模型了,这是一个漫长过程,也是一个最能看出我们经验,有点

training.jfif

for epoch in range(num_epochs):
  for index,(data,targets) in enumerate(train_loader):
    data = data.to(device)
    targets = targets.to(device) 
    print(data.shape)
    break
    
复制代码

首先我们通过 dataLoader 将数据加载器,其实 dataLoader 有点类似于 generator 每次遍历数据加载器会根据事先设置 batch size 提供一定量样本数据

torch.Size([64, 1, 28, 28])
复制代码

64 表示每一个批次提供图像数量、1 表示图像的通道数和 28 和 28 分别代表图像宽和高度

for epoch in range(num_epochs):
  for index,(data,targets) in enumerate(train_loader):
    data = data.to(device)
    targets = targets.to(device) 
    data = data.reshape(data.shape[0],-1)

    # 前向传播
    pred = model(data)
    loss = criterion(pred,targets)

    # 反向传播
    optimizer.zero_grad()
    loss.backward()

    # 更新参数
    optimizer.step()
复制代码

验证

验证主要是在迭代一定次数后,通过对比某一个指标,看一看当前迭代更新得到模型是否是效果最好的模型,从而对模型进行保存。

def validation_acc(loader,model):
  num_correct = 0
  num_samples = 0
  model.eval()

  with torch.no_grad():
    for x,y in loader:
      x = x.to(device)
      y = y.to(device)

      x = x.reshape(x.shape[0],-1)

      y_hat = model(x)
      _,pred = y_hat.max(1)

      num_correct += torch.eq(pred,y).sum()
      num_samples += pred.size(0)
    print(f"acc {float(num_correct)/float(num_samples)}")
  acc = float(num_correct)/float(num_samples)
  model.train()
  return acc
复制代码

首先需要将模型切换到 eval 模式,当模型切换到 eval 对于一些特殊层或者部分会和其训练有不同行为,如 Dropouts Layers、 BatchNorm Layers,在评估验证是需要将这些层关闭,另外还需要使用 torch.no_grad() 来关闭梯度更新。

validation_acc(test_loader,model)
复制代码

输出

acc 0.9215
复制代码
收藏成功!
已添加到「」, 点击更改