# 初始

# 前言

使用 conda 安装时不用另外装 cuda 和 cudnn,它自己会去装

查看 cuda 版本 nvidia-smi

1666491480042

conda install pytorch torchvision torchaudio cudatoolkit=11.0 -c pytorch

pip3 install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu110

实测 cuda11.0 可以 cuda10.2 的

# 换源

conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/

conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/pytorch/

conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main/

conda config --add channels https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/

conda config --add channels

# 安装

1660615170928

1660615187691

1660615197665

1660615236813

1660615312892

1660615320212

1660615339935

# 什么是 pytorch?

pytorch 是一个基于 Python 的科学计算包,它主要有两个用途:

  • 类似于 Numpy 但是能利用 GPU 加速
  • 一个非常灵活和快速用于深度学习的研究平台。

# 检查 GPU 是否可用

import torch
torch.cuda.is_available() # True 为可用

# 两个法宝函数

dir(torch)
dir(torch.cuda)
help(torch.cuda.is_available)

# Dataset

创建类来加载数据集,

import os
from torch.utils.data import Dataset
from PIL import Image
class MyData(Dataset):
    def __init__(self, root_dir, label_dir):
        self.root_dir = root_dir
        self.label_dir = label_dir
        self.path = os.path.join(root_dir, label_dir)
        self.img_path = os.listdir(self.path)
    def __getitem__(self, idx):
        img_name = self.img_path[idx]
        img_item_path = os.path.join(self.root_dir, self.label_dir, img_name)
        img = Image.open(img_item_path)
        label = self.label_dir
        return img, label
    def __len__(self):
        return len(self.img_path)
if __name__ == '__main__':
    root_dir = "../dataset/train"
    ants_label_dir = "ants"
    ants_dataset = MyData(root_dir, ants_label_dir)
    bees_label_dir = "bees"
    bees_dataset = MyData(root_dir, bees_label_dir)
    train_dataset = ants_dataset + bees_dataset

# tensorboard

from torch.utils.tensorboard import SummaryWriter
writer = SummaryWriter("logs") # 文件名称
# y = x
for i in range(100):
    writer.add_scalar("y = x", i, i)
writer.close()

打开终端

在终端输入

conda activate pytorch
tensorboard --logdir=logs #事件文件所在文件夹
tensorboard --logdir=logs --port=6007 # 指定端口

# Transform

在 transforms.py 中有很多的类,相当于一个一个的工具

# Totensor

from torchvision import transforms
from PIL import Image
from torch.utils.tensorboard import SummaryWriter
img_path = "D:\\Projects\\pytorch-learn\\dataset\\train\\ants\\0013035.jpg"
img = Image.open(img_path)
tensor_trans = transforms.ToTensor() # 从 transforms 中返回类 ToTensor
tensor_img = tensor_trans(img) # 使用该类,调用的__call__方法
print(tensor_img)
print(tensor_img.max(), tensor_img.min())
writer = SummaryWriter('logs')
writer.add_image("Tensor_img", tensor_img)
writer.close()

# Normalize

from torchvision import transforms
from torch.utils.tensorboard import SummaryWriter
from PIL import Image
img_path = "D:\\Projects\\pytorch-learn\\dataset\\train\\ants\\0013035.jpg"
img = Image.open(img_path)
# ToTensor
tensor_trans = transforms.ToTensor() # 从 transforms 中返回类 ToTensor
tensor_img = tensor_trans(img) # 使用该类,调用的__call__方法
# Normalize
print(tensor_img[0][0][0])
trans_norm = transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5])
img_norm = trans_norm(tensor_img)
print(img_norm[0][0][0])

# Resize

from torchvision import transforms
from torch.utils.tensorboard import SummaryWriter
from PIL import Image
img_path = "D:\\Projects\\pytorch-learn\\dataset\\train\\ants\\0013035.jpg"
img = Image.open(img_path)
# ToTensor
tensor_trans = transforms.ToTensor()
writer = SummaryWriter('logs')
# Resize
trans_resize = transforms.Resize((512, 512)) # 指定图片的宽高,是对图片进行操作,不是对张量
img_resize = trans_resize(img)
img_resize = tensor_trans(img_resize) # 最后再转为张量
writer.add_image("resize", img_resize, 0)
print(img_resize)

# Compose

from torchvision import transforms
from torch.utils.tensorboard import SummaryWriter
from PIL import Image
img_path = "D:\\Projects\\pytorch-learn\\dataset\\train\\ants\\0013035.jpg"
img = Image.open(img_path)
# ToTensor
tensor_trans = transforms.ToTensor()
writer = SummaryWriter('logs')
# Resize
trans_resize_2 = transforms.Resize(512) # 图片等比例缩放
trans_compose = transforms.Compose([trans_resize_2, tensor_trans]) # 注意前后参数类型的匹配
img_resize_2 = trans_compose(img)
writer.add_image("resize", img_resize_2, 2)
print(img_resize_2)

# torchvision 数据集使用

官方文档

import torchvision
from torch.utils.tensorboard import SummaryWriter
train_set = torchvision.datasets.CIFAR10(root="../dataset", train=True, download=True)
test_set = torchvision.datasets.CIFAR10(root="../dataset", train=False, download=True)
print(test_set[0]) # 返回图片和类别(这里的类别是编号,编号和类别名称是对应的)
print(test_set.classes) # 类别的名称
img, target = test_set[0]
print(img)
print(target)
print(test_set.classes[target])
img.show()
print(test_set[0])
import torchvision
from torch.utils.tensorboard import SummaryWriter
dataset_transform = torchvision.transforms.Compose([
    torchvision.transforms.ToTensor()
])
# 对每张图片进行 transform
train_set = torchvision.datasets.CIFAR10(root="../dataset", train=True, transform=dataset_transform, download=True)
test_set = torchvision.datasets.CIFAR10(root="../dataset", train=False, transform=dataset_transform, download=True)
writer = SummaryWriter("logs")
for i in range(10):
    img, target = test_set[i]
    writer.add_image("test_set", img, i)
writer.close()

# dataloader

import torchvision
# 准备的测试数据集
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter
test_data = torchvision.datasets.CIFAR10("../dataset", train=False, transform=torchvision.transforms.ToTensor())
test_loader = DataLoader(dataset=test_data, batch_size=64, shuffle=True, num_workers=0, drop_last=True)
# 测试数据集中第一张图片及 target
img, target = test_data[0]
print(img.shape)  # torch.Size([3, 32, 32])
print(target)  # 3
for data in test_loader:
    imgs, targets = data
    print(imgs.shape)
    print(targets)
'''
torch.Size([64, 3, 32, 32])
tensor([3, 2, 9, 2, 7, 3, 0, 8, 7, 2, 8, 3, 0, 7, 7, 1, 7, 0, 5, 5, 5, 0, 4, 4,
        3, 1, 0, 3, 0, 5, 8, 7, 1, 1, 4, 8, 1, 5, 4, 7, 9, 8, 9, 1, 3, 2, 6, 0,
        2, 2, 1, 0, 4, 9, 5, 3, 8, 2, 6, 2, 0, 7, 4, 1])
'''
writer = SummaryWriter("dataloader")
for epoch in range(2):
    step = 0
    for data in test_loader:
        imgs, targets = data
        # print(imgs.shape)
        # print(targets)
        writer.add_images("Epoch: {}".format(epoch), imgs, step)
        step = step + 1
writer.close()

# nn

# 简单例子

import torch
from torch import nn
class Tudui(nn.Module):
    def __init__(self):
        super().__init__()
    def forward(self, input):
        output = input + 1
        return output
tudui = Tudui()
x = torch.tensor(1.0)
output = tudui(x)
print(output)

# 卷积

对应位相乘后最终再相加

import torch
import torch.nn.functional as F
input = torch.tensor([[1, 2, 0, 3, 1],
                      [0, 1, 2, 3, 1],
                      [1, 2, 1, 0, 0],
                      [5, 2, 3, 1, 1],
                      [2, 1, 0, 1, 1]])
kernel = torch.tensor([[1, 2, 1],
                       [0, 1, 0],
                       [2, 1, 0]])
input = torch.reshape(input, (1, 1, 5, 5))
kernel = torch.reshape(kernel, (1, 1, 3, 3))
print(input.shape)
print(kernel.shape)
output = F.conv2d(input, kernel, stride=1)
print(output)
output2 = F.conv2d(input, kernel, stride=2)
print(output2)
output3 = F.conv2d(input, kernel, stride=1, padding=1)
print(output3)
'''
torch.Size([1, 1, 5, 5])
torch.Size([1, 1, 3, 3])
tensor([[[[10, 12, 12],
          [18, 16, 16],
          [13,  9,  3]]]])
tensor([[[[10, 12],
          [13,  3]]]])
tensor([[[[ 1,  3,  4, 10,  8],
          [ 5, 10, 12, 12,  6],
          [ 7, 18, 16, 16,  8],
          [11, 13,  9,  3,  4],
          [14, 13,  9,  7,  4]]]])
'''

import torch
import torchvision
from torch import nn
from torch.nn import Conv2d
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter
dataset = torchvision.datasets.CIFAR10("../dataset", train=False, transform=torchvision.transforms.ToTensor(),
                                       download=True)
dataloader = DataLoader(dataset, batch_size=64)
class Tudui(nn.Module):
    def __init__(self):
        super(Tudui, self).__init__()
        self.conv1 = Conv2d(in_channels=3, out_channels=6, kernel_size=3, stride=1, padding=0)
    def forward(self, x):
        x = self.conv1(x)
        return x
tudui = Tudui()
print(tudui)
'''
Tudui(
  (conv1): Conv2d(3, 6, kernel_size=(3, 3), stride=(1, 1))
)
'''
writer = SummaryWriter("../logs")
step = 0
for data in dataloader:
    imgs, targets = data
    output = tudui(imgs)
    print(imgs.shape)
    print(output.shape)
    # torch.Size([64, 3, 32, 32])
    writer.add_images("input", imgs, step)
    # torch.Size([64, 6, 30, 30])  -> [xxx, 3, 30, 30]
    output = torch.reshape(output, (-1, 3, 30, 30))
    writer.add_images("output", output, step)
    step = step + 1
writer.close()

# 最大池化

注意池化核和卷积核在图像上的移动方式不同。

池化的目的是保留输入的特征,但同时把数据量减小。像把 1080p 的视频变成 720p 的

import torch
import torchvision
from torch import nn
from torch.nn import MaxPool2d
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter
dataset = torchvision.datasets.CIFAR10("../data", train=False, download=True,
                                       transform=torchvision.transforms.ToTensor())
dataloader = DataLoader(dataset, batch_size=64)
class Tudui(nn.Module):
    def __init__(self):
        super(Tudui, self).__init__()
        self.maxpool1 = MaxPool2d(kernel_size=3, ceil_mode=False)
    def forward(self, input):
        output = self.maxpool1(input)
        return output
tudui = Tudui()
writer = SummaryWriter("../logs_maxpool")
step = 0
for data in dataloader:
    imgs, targets = data
    writer.add_images("input", imgs, step)
    output = tudui(imgs)
    writer.add_images("output", output, step)
    step = step + 1
writer.close()

# 非线性激活

  • relu
  • sigmoid

import torch
import torchvision
from torch import nn
from torch.nn import ReLU, Sigmoid
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter
input = torch.tensor([[1, -0.5],
                      [-1, 3]])
input = torch.reshape(input, (-1, 1, 2, 2))
print(input.shape)
class Tudui(nn.Module):
    def __init__(self):
        super(Tudui, self).__init__()
        self.relu1 = ReLU()
    def forward(self, input):
        output = self.relu1(input)
        return output
tudui = Tudui()
output = tudui(input)
print(output)
'''
torch.Size([1, 1, 2, 2])
tensor([[[[1., 0.],
          [0., 3.]]]])
'''

import torch
import torchvision
from torch import nn
from torch.nn import ReLU, Sigmoid
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter
dataset = torchvision.datasets.CIFAR10("../dataset", train=False, download=True,
                                       transform=torchvision.transforms.ToTensor())
dataloader = DataLoader(dataset, batch_size=64)
class Tudui(nn.Module):
    def __init__(self):
        super(Tudui, self).__init__()
        self.relu1 = ReLU()
        self.sigmoid1 = Sigmoid()
    def forward(self, input):
        output = self.sigmoid1(input)
        return output
tudui = Tudui()
writer = SummaryWriter("./logs_relu")
step = 0
for data in dataloader:
    imgs, targets = data
    writer.add_images("input", imgs, global_step=step)
    output = tudui(imgs)
    writer.add_images("output", output, step)
    step += 1
writer.close()

非线性变换的目的主要是为网络引入非线性特征,非线性越多,才能训练出符合各种曲线,符合各种特征的模型,加强模型的泛化能力。

# 线性层及其他层

# 正则化层

加快网络的训练速度

# 线性层

import torch
import torchvision
from torch import nn
from torch.nn import Linear
from torch.utils.data import DataLoader
dataset = torchvision.datasets.CIFAR10("../dataset", train=False, transform=torchvision.transforms.ToTensor(),
                                       download=True)
dataloader = DataLoader(dataset, batch_size=64, drop_last=True)
class Tudui(nn.Module):
    def __init__(self):
        super(Tudui, self).__init__()
        self.linear1 = Linear(196608, 10)
    def forward(self, input):
        output = self.linear1(input)
        return output
tudui = Tudui()
for data in dataloader:
    imgs, targets = data
    print(imgs.shape)
    output = torch.reshape(imgs, (1, 1, 1, -1))
    print(output.shape)
    output = tudui(output)
    print(output.shape)
'''
torch.Size([64, 3, 32, 32])
torch.Size([1, 1, 1, 196608])
torch.Size([1, 1, 1, 10])
线性层
'''

import torch
import torchvision
from torch import nn
from torch.nn import Linear
from torch.utils.data import DataLoader
dataset = torchvision.datasets.CIFAR10("../dataset", train=False, transform=torchvision.transforms.ToTensor(),
                                       download=True)
dataloader = DataLoader(dataset, batch_size=64, drop_last=True)
class Tudui(nn.Module):
    def __init__(self):
        super(Tudui, self).__init__()
        self.linear1 = Linear(196608, 10)
    def forward(self, input):
        output = self.linear1(input)
        return output
tudui = Tudui()
for data in dataloader:
    imgs, targets = data
    print(imgs.shape)
    output = torch.flatten(imgs)
    print(output.shape)
    output = tudui(output)
    print(output.shape)
'''
torch.Size([64, 3, 32, 32])
torch.Size([196608])
torch.Size([10])
'''

# Sequential

求取卷积时候的是 padding 和 stride

dilation 默认值是 1

import torch
from torch import nn
from torch.nn import Conv2d, MaxPool2d, Flatten, Linear, Sequential
from torch.utils.tensorboard import SummaryWriter
class Tudui(nn.Module):
    def __init__(self):
        super(Tudui, self).__init__()
        self.model1 = Sequential(
            Conv2d(3, 32, 5, padding=2),
            MaxPool2d(2),
            Conv2d(32, 32, 5, padding=2),
            MaxPool2d(2),
            Conv2d(32, 64, 5, padding=2),
            MaxPool2d(2),
            Flatten(),
            Linear(1024, 64),
            Linear(64, 10)
        )
    def forward(self, x):
        x = self.model1(x)
        return x
tudui = Tudui()
print(tudui)
input = torch.ones((64, 3, 32, 32))
output = tudui(input)
print(output.shape)
writer = SummaryWriter("./logs_seq")
writer.add_graph(tudui, input)
writer.close()

# 损失函数和反向传播

针对分类问题

import torch
from torch.nn import L1Loss
from torch import nn
inputs = torch.tensor([1, 2, 3], dtype=torch.float32)
targets = torch.tensor([1, 2, 5], dtype=torch.float32)
inputs = torch.reshape(inputs, (1, 1, 1, 3))
targets = torch.reshape(targets, (1, 1, 1, 3))
loss = L1Loss(reduction='sum')
result = loss(inputs, targets)
loss_mse = nn.MSELoss()
result_mse = loss_mse(inputs, targets)
print(result)
print(result_mse)
x = torch.tensor([0.1, 0.2, 0.3])
y = torch.tensor([1])
x = torch.reshape(x, (1, 3))
loss_cross = nn.CrossEntropyLoss()
result_cross = loss_cross(x, y)
print(result_cross)
import torchvision
from torch import nn
from torch.nn import Sequential, Conv2d, MaxPool2d, Flatten, Linear
from torch.utils.data import DataLoader
dataset = torchvision.datasets.CIFAR10("../dataset", train=False, transform=torchvision.transforms.ToTensor(),
                                       download=True)
dataloader = DataLoader(dataset, batch_size=1)
class Tudui(nn.Module):
    def __init__(self):
        super(Tudui, self).__init__()
        self.model1 = Sequential(
            Conv2d(3, 32, 5, padding=2),
            MaxPool2d(2),
            Conv2d(32, 32, 5, padding=2),
            MaxPool2d(2),
            Conv2d(32, 64, 5, padding=2),
            MaxPool2d(2),
            Flatten(),
            Linear(1024, 64),
            Linear(64, 10)
        )
    def forward(self, x):
        x = self.model1(x)
        return x
loss = nn.CrossEntropyLoss()
tudui = Tudui()
for data in dataloader:
    imgs, targets = data
    outputs = tudui(imgs)
    result_loss = loss(outputs, targets)
    print(result_loss)

# 优化器

# 基本使用

import torch
import torchvision
from torch import nn
from torch.nn import Sequential, Conv2d, MaxPool2d, Flatten, Linear
from torch.optim.lr_scheduler import StepLR
from torch.utils.data import DataLoader
dataset = torchvision.datasets.CIFAR10("../dataset", train=False, transform=torchvision.transforms.ToTensor(),
                                       download=True)
dataloader = DataLoader(dataset, batch_size=1)
class Tudui(nn.Module):
    def __init__(self):
        super(Tudui, self).__init__()
        self.model1 = Sequential(
            Conv2d(3, 32, 5, padding=2),
            MaxPool2d(2),
            Conv2d(32, 32, 5, padding=2),
            MaxPool2d(2),
            Conv2d(32, 64, 5, padding=2),
            MaxPool2d(2),
            Flatten(),
            Linear(1024, 64),
            Linear(64, 10)
        )
    def forward(self, x):
        x = self.model1(x)
        return x
loss = nn.CrossEntropyLoss()
tudui = Tudui()
optim = torch.optim.SGD(tudui.parameters(), lr=0.01)
for epoch in range(20):
    running_loss = 0.0
    for data in dataloader:
        imgs, targets = data
        outputs = tudui(imgs)
        result_loss = loss(outputs, targets)
        optim.zero_grad()  # 把模型中可被调节的参数的梯度设置为 0
        result_loss.backward()  # 得到可被调节的参数的梯度
        optim.step()  # 对参数调优
        running_loss = running_loss + result_loss
    print(running_loss)

理解:在优化器的创建中传入了网络模型的参数,根据后文优化器能够对模型的参数进行修改可以推测,在创建中传入的参数是引用参数,所以在反向传播计算出梯度之后,优化器能够直接修改到模型的参数。

# 优化器的选择

Pytorch 中有四种常用的优化器,SGD、Momentum、RMSProp、Adam,那我们该如何选择呢。

# 1.SGD

参数介绍:

--lr (float) : 学习率

--momentum (float,可选):动量因子(默认为 0)

--weight_decay (float,可选):权重衰减(L2 惩罚,默认为 0)

--dampening (float,可选):动量的抑制因子(默认为 0)

--nesterov (bool,可选):使用 Nesterov 动量(默认为 false)

示例代码:

optimizer = torch.optim.SGD(model.parameters(), lr=0.01)

优缺点:

优点:使用 mini-batch 的时候,模型可以收敛的更快

缺点:在随机选择梯度的时候会引入噪声,使得权重更新的方向不一定正确。

不能解决局部最优解的问题。

推荐指数: 0 星

# 2.RMSProp (Root Mean Square Prop,均方根传递)

RMSProp 是 RProp 的改进版,同时也是 Adagard 的改进版,它的思想上:在梯度震动较大的项,在下降时,减小其下降速度;对于震动幅度较小的项,在下降时,加速其下降速度。同时 RMSProp 采用均方根作为分母,可以缓解 Adagard 学习率下降过快的问题。

其对于 RNN 具有很好的效果。

参数介绍:

--lr (float) : 学习率

--momentum (float,可选):动量因子(默认为 0)

--alpha(float,可选):平滑常数(默认:0.99)

--eps (float, 可选):为了增加数值计算的稳定性而加到分母里的项(默认:1e-8)

--centered (bool, 可选):如果为 True,计算中心化的 RMSProp,并且用它的方差预测值对梯度进行归一化

--weight_decay (float,可选):权重衰减(L2 惩罚,默认为 0)

示例代码:

optimizer = torch.optim.RMSprop(model.parameters(), lr=0.01)

优缺点:

优点:可缓解 Adagrad 学习率下降较快的问题,并且引入均方根,可以减少摆动,适合处理非平稳目标,对于 RNN 效果很好。

缺点:依然依赖于全局学习率。

推荐程度:四星半 RMSProp 算法已被证明是一种有效且实用的深度神经网络优化算法。目前它是深度学习从业者经常采用的优化算法之一。

# 3.Adam(AMSGrad)

它是一种将 Momentum 算法和 RMSProp 算法结合起来而使用的一种算法,既使用动量来累计梯度,又使得收敛速度更快同时使得波动的幅度更小,并进行偏差修正。

参数介绍:

--lr (float) : 学习率

--betas (Tuple [float,float], 可选):用于计算梯度以及梯度平方的运行平均值的系数(默认:0.9,0.999)

--eps (float, 可选):为了增加数值计算的稳定性而加到分母里的项(默认:1e-8)

--weight_decay (float,可选):权重衰减(L2 惩罚,默认为 0)

示例代码:

optimizer = torch.optim.Adam(model.parameters(),lr=0.01)

优点:

对目标函数没有平稳要求,即 loss function 可以随着时间变化。

参数的更新不受梯度的伸缩变换影响。

更新的步长能够被限制在大致的范围内(初始学习率)。

能较好的处理噪音样本,能天然地实现步长退火过程(自动调整学习率)。

很适合应用于大规模的数据及参数的场景、不稳定目标函数、梯度稀疏或梯度存在很大噪声的问题。

推荐程度:五星。 非常推荐,基本上是目前最常用的优化方法。

四种常用优化器 Loss 函数比较图

图 1

对于重要参数学习率,这里推荐设置 1e-3 或者 1e-2.

图 2

# 现有模型的修改和使用

import torchvision
# train_data = torchvision.datasets.ImageNet("../data_image_net", split='train', download=True,
#                                            transform=torchvision.transforms.ToTensor())
from torch import nn
vgg16_false = torchvision.models.vgg16(pretrained=False)
vgg16_true = torchvision.models.vgg16(pretrained=True)
print(vgg16_true)
train_data = torchvision.datasets.CIFAR10('../data', train=True, transform=torchvision.transforms.ToTensor(),
                                          download=True)
# vgg16_true.add_module('add_linear', nn.Linear(1000, 10))
vgg16_true.classifier.add_module('add_linear', nn.Linear(1000, 10))
print(vgg16_true)
print(vgg16_false)
vgg16_false.classifier[6] = nn.Linear(4096, 10)
print(vgg16_false)

# 网络模型保存与读取

# 保存

# -*- coding: utf-8 -*-
# 作者:小土堆
# 公众号:土堆碎念
import torch
import torchvision
from torch import nn
vgg16 = torchvision.models.vgg16(pretrained=False)
# 保存方式 1, 模型结构 + 模型参数
torch.save(vgg16, "vgg16_method1.pth")
# 保存方式 2,模型参数(官方推荐) 以字典的形式保存模型参数
torch.save(vgg16.state_dict(), "vgg16_method2.pth")
# 陷阱
class Tudui(nn.Module):
    def __init__(self):
        super(Tudui, self).__init__()
        self.conv1 = nn.Conv2d(3, 64, kernel_size=3)
    def forward(self, x):
        x = self.conv1(x)
        return x
tudui = Tudui()
torch.save(tudui, "tudui_method1.pth")

# 读取

# -*- coding: utf-8 -*-
# 作者:小土堆
# 公众号:土堆碎念
import torch
from model_save import *
# 方式 1-》保存方式 1,加载模型
import torchvision
from torch import nn
model = torch.load("vgg16_method1.pth")
# print(model)
# 方式 2,加载模型
vgg16 = torchvision.models.vgg16(pretrained=False)
vgg16.load_state_dict(torch.load("vgg16_method2.pth"))
# model = torch.load("vgg16_method2.pth")
# print(vgg16)
# 陷阱 1
# class Tudui(nn.Module):
#     def __init__(self):
#         super(Tudui, self).__init__()
#         self.conv1 = nn.Conv2d(3, 64, kernel_size=3)
#
#     def forward(self, x):
#         x = self.conv1(x)
#         return x
model = torch.load('tudui_method1.pth')
print(model)

# 利用 GPU 进行训练

找到网络模型、数据(输入、标注)和标注、损失函数, .cuda() 后返回即可。

# 方式一

import torch
import torchvision
from torch.utils.tensorboard import SummaryWriter
# 准备数据集
from torch import nn
from torch.utils.data import DataLoader
train_data = torchvision.datasets.CIFAR10(root="../dataset", train=True, transform=torchvision.transforms.ToTensor(),
                                          download=True)
test_data = torchvision.datasets.CIFAR10(root="../dataset", train=False, transform=torchvision.transforms.ToTensor(),
                                         download=True)
# length 长度
train_data_size = len(train_data)
test_data_size = len(test_data)
# 如果 train_data_size=10, 训练数据集的长度为:10
print("训练数据集的长度为:{}".format(train_data_size))
print("测试数据集的长度为:{}".format(test_data_size))
# 利用 DataLoader 来加载数据集
train_dataloader = DataLoader(train_data, batch_size=64)
test_dataloader = DataLoader(test_data, batch_size=64)
# 创建网络模型
class Tudui(nn.Module):
    def __init__(self):
        super(Tudui, self).__init__()
        self.model = nn.Sequential(
            nn.Conv2d(3, 32, 5, 1, 2),
            nn.MaxPool2d(2),
            nn.Conv2d(32, 32, 5, 1, 2),
            nn.MaxPool2d(2),
            nn.Conv2d(32, 64, 5, 1, 2),
            nn.MaxPool2d(2),
            nn.Flatten(),
            nn.Linear(64*4*4, 64),
            nn.Linear(64, 10)
        )
    def forward(self, x):
        x = self.model(x)
        return x
tudui = Tudui()
# 先检测 GPU 是否可用
if torch.cuda.is_available():
    tudui = tudui.cuda()
# 损失函数
loss_fn = nn.CrossEntropyLoss()
if torch.cuda.is_available():
    loss_fn = loss_fn.cuda()
# 优化器
# learning_rate = 0.01
# 1e-2=1 x (10)^(-2) = 1 /100 = 0.01
learning_rate = 1e-2
optimizer = torch.optim.SGD(tudui.parameters(), lr=learning_rate)
# 设置训练网络的一些参数
# 记录训练的次数
total_train_step = 0
# 记录测试的次数
total_test_step = 0
# 训练的轮数
epoch = 10
# 添加 tensorboard
# writer = SummaryWriter("../logs_train")
for i in range(epoch):
    print("-------第 {} 轮训练开始-------".format(i+1))
    # 训练步骤开始
    tudui.train()
    for data in train_dataloader:
        imgs, targets = data
        if torch.cuda.is_available():
            imgs = imgs.cuda()
            targets = targets.cuda()
        outputs = tudui(imgs)
        loss = loss_fn(outputs, targets)
        # 优化器优化模型
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        total_train_step = total_train_step + 1
        if total_train_step % 100 == 0:
            print("训练次数:{}, Loss: {}".format(total_train_step, loss.item()))
            # writer.add_scalar("train_loss", loss.item(), total_train_step)
    # 测试步骤开始
    tudui.eval()
    total_test_loss = 0
    total_accuracy = 0
    with torch.no_grad():
        for data in test_dataloader:
            imgs, targets = data
            if torch.cuda.is_available():
                imgs = imgs.cuda()
                targets = targets.cuda()
            outputs = tudui(imgs)
            loss = loss_fn(outputs, targets)
            total_test_loss = total_test_loss + loss.item()
            accuracy = (outputs.argmax(1) == targets).sum()
            total_accuracy = total_accuracy + accuracy
    print("整体测试集上的Loss: {}".format(total_test_loss))
    print("整体测试集上的正确率: {}".format(total_accuracy/test_data_size))
    # writer.add_scalar("test_loss", total_test_loss, total_test_step)
    # writer.add_scalar("test_accuracy", total_accuracy/test_data_size, total_test_step)
    total_test_step = total_test_step + 1
    torch.save(tudui, "tudui_{}.pth".format(i))
    print("模型已保存")
# writer.close()

# 方式二

import torch
import torchvision
from torch.utils.tensorboard import SummaryWriter
# 准备数据集
from torch import nn
from torch.utils.data import DataLoader
# 定义训练的设备
device = torch.device("cuda")
train_data = torchvision.datasets.CIFAR10(root="../dataset", train=True, transform=torchvision.transforms.ToTensor(),
                                          download=True)
test_data = torchvision.datasets.CIFAR10(root="../dataset", train=False, transform=torchvision.transforms.ToTensor(),
                                         download=True)
# length 长度
train_data_size = len(train_data)
test_data_size = len(test_data)
# 如果 train_data_size=10, 训练数据集的长度为:10
print("训练数据集的长度为:{}".format(train_data_size))
print("测试数据集的长度为:{}".format(test_data_size))
# 利用 DataLoader 来加载数据集
train_dataloader = DataLoader(train_data, batch_size=64)
test_dataloader = DataLoader(test_data, batch_size=64)
# 创建网络模型
class Tudui(nn.Module):
    def __init__(self):
        super(Tudui, self).__init__()
        self.model = nn.Sequential(
            nn.Conv2d(3, 32, 5, 1, 2),
            nn.MaxPool2d(2),
            nn.Conv2d(32, 32, 5, 1, 2),
            nn.MaxPool2d(2),
            nn.Conv2d(32, 64, 5, 1, 2),
            nn.MaxPool2d(2),
            nn.Flatten(),
            nn.Linear(64*4*4, 64),
            nn.Linear(64, 10)
        )
    def forward(self, x):
        x = self.model(x)
        return x
tudui = Tudui()
tudui = tudui.to(device)
# 损失函数
loss_fn = nn.CrossEntropyLoss()
loss_fn = loss_fn.to(device)
# 优化器
# learning_rate = 0.01
# 1e-2=1 x (10)^(-2) = 1 /100 = 0.01
learning_rate = 1e-2
optimizer = torch.optim.SGD(tudui.parameters(), lr=learning_rate)
# 设置训练网络的一些参数
# 记录训练的次数
total_train_step = 0
# 记录测试的次数
total_test_step = 0
# 训练的轮数
epoch = 10
# 添加 tensorboard
# writer = SummaryWriter("../logs_train")
for i in range(epoch):
    print("-------第 {} 轮训练开始-------".format(i+1))
    # 训练步骤开始
    tudui.train()
    for data in train_dataloader:
        imgs, targets = data
        imgs = imgs.to(device)
        targets = targets.to(device)
        outputs = tudui(imgs)
        loss = loss_fn(outputs, targets)
        # 优化器优化模型
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        total_train_step = total_train_step + 1
        if total_train_step % 100 == 0:
            print("训练次数:{}, Loss: {}".format(total_train_step, loss.item()))
            # writer.add_scalar("train_loss", loss.item(), total_train_step)
    # 测试步骤开始
    tudui.eval()
    total_test_loss = 0
    total_accuracy = 0
    with torch.no_grad():
        for data in test_dataloader:
            imgs, targets = data
            imgs = imgs.to(device)
            targets = targets.to(device)
            outputs = tudui(imgs)
            loss = loss_fn(outputs, targets)
            total_test_loss = total_test_loss + loss.item()
            accuracy = (outputs.argmax(1) == targets).sum()
            total_accuracy = total_accuracy + accuracy
    print("整体测试集上的Loss: {}".format(total_test_loss))
    print("整体测试集上的正确率: {}".format(total_accuracy/test_data_size))
    # writer.add_scalar("test_loss", total_test_loss, total_test_step)
    # writer.add_scalar("test_accuracy", total_accuracy/test_data_size, total_test_step)
    total_test_step = total_test_step + 1
    torch.save(tudui, "tudui_{}.pth".format(i))
    print("模型已保存")
# writer.close()

# 下面是原博客内容

# Tensor

Tensor 类似于 Numpy 的 ndarray,但是可以用 GPU 加速,使用前我们需要导入 torch 包。

import torch
# 下面代码构建一个 5x3 的未初始化的矩阵
x = torch.empty(5,3)
print(x)
# 输出
tensor([[9.9184e-39, 8.7245e-39, 9.2755e-39],
        [8.9082e-39, 9.9184e-39, 8.4490e-39],
        [9.6429e-39, 1.0653e-38, 1.0469e-38],
        [4.2246e-39, 1.0378e-38, 9.6429e-39],
        [9.2755e-39, 9.7346e-39, 1.0745e-38]])

下面代码分别构造一个零初始化的矩阵,它的类型 (dtype) 是 long:

x = torch.zeros(5,3 dtype=torch.long)
print(x)
# 输出
tensor([[0, 0, 0],
        [0, 0, 0],
        [0, 0, 0],
        [0, 0, 0],
        [0, 0, 0]])

我们可以从已有的 tensor 信息 (size 和 dtype) 来构造 tensor。但也可以用不同的 dtype 来构造。

x = x.new_ones(5, 3, dtype=torch.double)      # new_* methods take in sizes
print(x)
x = torch.randn_like(x, dtype=torch.float)    # override dtype!
print(x)

我们可以是用 size 函数来看它的 shape:

print(x.size())
#输出:
torch.Size([5, 3])

注意 torch.Size 其实是一个 tuple,因此它支持所有的 tuple 操作。

# Operation

接下来我们来学习一些 PyTorch 的 Operation。Operation 一般可以使用函数的方式使用,但是为了方便使用,PyTorch 重载了一些常见的运算符,因此我们可以这样来进行 Tensor 的加法:

y = torch.rand(5, 3)
print(x + y)

我们也可以用 add 函数来实现加法:

print(torch.add(x, y))

我们也可以给加法提供返回值 (而不是生成一个新的返回值):

result = torch.empty(5, 3)
torch.add(x, y, out=result) # x + y 的结果放到 result 里。
print(result)

我们也可以把相加的结果直接修改第一个被加数:

# 把 x 加到 y
y.add_(x)
print(y)

注意:就地修改 tensor 的 operation 以下划线结尾。比如: x.copy_(y), x.t_(), 都会修改 x。

# Tensor 的变换

我们也可以使用类似 numpy 的下标运算来操作 PyTorch 的 Tensor:

#打印 x 的第一列
print(x[:, 1])

如果想 resize 或者 reshape 一个 Tensor,我们可以使用 torch.view:

x = torch.randn(4, 4)
y = x.view(16)
z = x.view(-1, 8)  # -1 的意思是让 PyTorch 自己推断出第一维的大小。
print(x.size(), y.size(), z.size())

如果一个 tensor 只有一个元素,可以使用 item () 函数来把它变成一个 Python number:

x = torch.randn(1)
print(x)
#输出的是一个 Tensor
tensor([-0.6966])
print(x.item())
#输出的是一个数
-0.6966081857681274

# Tensor 与 Numpy 的互相转换

Torch Tensor 和 NumPy 数组的转换非常容易。它们会共享内存地址,因此修改一方会影响另一方。把一个 Torch Tensor 转换成 NumPy 数组的代码示例为:

a = torch.ones(5)
print(a)
#tensor([ 1.,  1.,  1.,  1.,  1.])
b = a.numpy()
print(b)
#[1. 1. 1. 1. 1.]

修改一个会影响另外一个:

a.add_(1)
print(a)
# tensor([ 2.,  2.,  2.,  2.,  2.])
print(b)
# [2. 2. 2. 2. 2.]

把把 NumPy 数组转成 Torch Tensor 的代码示例为:

import numpy as np
a = np.ones(5)
b = torch.from_numpy(a)
np.add(a, 1, out=a)
print(a)
# [2. 2. 2. 2. 2.]
print(b)
# tensor([ 2.,  2.,  2.,  2.,  2.], dtype=torch.float64)

CPU 上的所有类型的 Tensor (除了 CharTensor) 都可以和 Numpy 数组来回转换。

# CUDA Tensor

Tensor 可以使用 to () 方法来移到任意设备上:

# 如果有 CUDA
# 我们会使用 ``torch.device`` 来把 tensors 放到 GPU 上
if torch.cuda.is_available():
	device = torch.device("cuda")          # 一个 CUDA device 对象。
	y = torch.ones_like(x, device=device)  # 直接在 GPU 上创建 tensor
	x = x.to(device)                       # 也可以使用 ``.to ("cuda")`` 把一个 tensor 从 CPU 移到 GPU 上
	z = x + y
	print(z)
	print(z.to("cpu", torch.double))       # ``.to`` 也可以在移动的过程中修改 dtype
# 输出:
tensor([ 0.3034], device='cuda:0')
tensor([ 0.3034], dtype=torch.float64)

# Autograd: 自动求导

PyTorch 的核心是 autograd 包。 我们首先简单的了解一些,然后用 PyTorch 开始训练第一个神经网络。autograd 为所有用于 Tensor 的 operation 提供自动求导的功能。我们通过一些简单的例子来学习它基本用法。

# 从自动求导看 Tensor

torch.Tensor 是这个包的核心类。如果它的属性 requires_grad 是 True,那么 PyTorch 就会追踪所有与之相关的 operation。当完成 (正向) 计算之后, 我们可以调用 backward (),PyTorch 会自动的把所有的梯度都计算好。与这个 tensor 相关的梯度都会累加到它的 grad 属性里。

如果不想计算这个 tensor 的梯度,我们可以调用 detach (),这样它就不会参与梯度的计算了。为了阻止 PyTorch 记录用于梯度计算相关的信息 (从而节约内存),我们可以使用 with torch.no_grad ()。这在模型的预测时非常有用,因为预测的时候我们不需要计算梯度,否则我们就得一个个的修改 Tensor 的 requires_grad 属性,这会非常麻烦。

关于 autograd 的实现还有一个很重要的 Function 类。Tensor 和 Function 相互连接从而形成一个有向无环图,这个图记录了计算的完整历史。每个 tensor 有一个 grad_fn 属性来引用创建这个 tensor 的 Function (用户直接创建的 Tensor,这些 Tensor 的 grad_fn 是 None)。

如果你想计算梯度,可以对一个 Tensor 调用它的 backward () 方法。如果这个 Tensor 是一个 scalar (只有一个数),那么调用时不需要传任何参数。如果 Tensor 多于一个数,那么需要传入和它的 shape 一样的参数,表示反向传播过来的梯度。

创建 tensor 时设置属性 requires_grad=True,PyTorch 就会记录用于反向梯度计算的信息:

x = torch.ones(2, 2, requires_grad=True)
print(x)

然后我们通过 operation 产生新的 tensor:

y = x + 2
print(y)

是通过 operation 产生的 tensor,因此它的 grad_fn 不是 None。

print(y.grad_fn)
# <AddBackward0 object at 0x7f35409a68d0>

再通过 y 得到 z 和 out

z = y * y * 3
out = z.mean()
print(z, out)
# z = tensor([[ 27.,  27.],[ 27.,  27.]]) 
# out = tensor(27.)

requires_grad_() 函数会修改一个 Tensor 的 requires_grad。

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 0x7f35766827f0>

# 梯度

现在我们里反向计算梯度。因为 out 是一个 scalar,因此 out.backward () 等价于 out.backward (torch.tensor (1))。

out.backward()

我们可以打印梯度 d (out)/dx:

print(x.grad)
# tensor([[ 4.5000,  4.5000],
[ 4.5000,  4.5000]])

我们手动计算验证一下。为了简单,我们把 outout 记为 o。o=14izio=\frac{1}{4}\sum_iz_izi=3(xi+2)2z_i=3(x_i+2)^2,并且zixi=1=27z_i|_{x_i=1}=27

因此,oxi=32(xi+2)\frac{∂o}{∂x_i}=\frac{3}{2}(x_i+2), 因此,oxixi=1=92=4.5\frac{∂o}{∂x_i}|_{x_i=1}=\frac{9}{2}=4.5

我们也可以用 autograd 做一些很奇怪的事情!比如 y 和 x 的关系是 while 循环的关系 (似乎很难用一个函数直接表示 y 和 x 的关系?对 x 不断平方直到超过 1000,这是什么函数?)

x = torch.randn(3, requires_grad=True)
y = x * 2
while y.data.norm() < 1000:
	y = y * 2
print(y)
# tensor([ -692.4808,  1686.1211,   667.7313])
gradients = torch.tensor([0.1, 1.0, 0.0001], dtype=torch.float)
y.backward(gradients)
print(x.grad)
# tensor([  102.4000,  1024.0000,     0.1024])

我们可以使用”with torch.no_grad ()” 来停止梯度的计算:

print(x.requires_grad)
print((x ** 2).requires_grad)
with torch.no_grad():
	print((x ** 2).requires_grad)

输出为:

True
True
False

# PyTorch 神经网络简介

神经网络可以通过 torch.nn 包来创建。我们之前简单的了解了 autograd,而 nn 会使用 autograd 来定义模型以及求梯度。一个 nn.Module 对象包括了许多网络层 (layer),并且有一个 forward (input) 方法来返回 output。如下图所示,我们会定义一个卷积网络来识别 mnist 图片。

图 1
图:识别 MNIST 数据的神经网络

训练一个神经网络通常需要如下步骤:

  • 定义一个神经网络,它通常有一些可以训练的参数
  • 迭代一个数据集 (dataset)
  • 处理网络的输入
  • 计算 loss (会调用 Module 对象的 forward 方法)
  • 计算 loss 对参数的梯度
  • 更新参数,通常使用如下的梯度下降方法来更新: weight = weight - learning_rate * gradient

# 定义网络

import torch
import torch.nn as nn
import torch.nn.functional as F
class Net(nn.Module):
	def __init__(self):
		super(Net, self).__init__()
		# 输入是 1 个通道的灰度图,输出 6 个通道 (feature map),使用 5x5 的卷积核
		self.conv1 = nn.Conv2d(1, 6, 5)
		# 第二个卷积层也是 5x5,有 16 个通道
		self.conv2 = nn.Conv2d(6, 16, 5)
		# 全连接层
		self.fc1 = nn.Linear(16 * 5 * 5, 120)
		self.fc2 = nn.Linear(120, 84)
		self.fc3 = nn.Linear(84, 10)
	def forward(self, x):
		# 32x32 -> 28x28 -> 14x14 
		x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2))
		# 14x14 -> 10x10 -> 5x5
		x = F.max_pool2d(F.relu(self.conv2(x)), 2)
		x = x.view(-1, self.num_flat_features(x))
		x = F.relu(self.fc1(x))
		x = F.relu(self.fc2(x))
		x = self.fc3(x)
		return x
	def num_flat_features(self, x):
		size = x.size()[1:]  # 除了 batch 维度之外的其它维度。
		num_features = 1
		for s in size:
		num_features *= s
		return num_features
net = Net()
print(net)
# Net(
(conv1): Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1))
(conv2): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1))
(fc1): Linear(in_features=400, out_features=120, bias=True)
(fc2): Linear(in_features=120, out_features=84, bias=True)
(fc3): Linear(in_features=84, out_features=10, bias=True)
)

我们只需要定义 forward 函数,而 backward 函数会自动通过 autograd 创建。在 forward 函数里可以使用任何处理 Tensor 的函数。我们可以使用函数 net.parameters () 来得到模型所有的参数。

params = list(net.parameters())
print(len(params))
# 10
print(params[0].size())  # conv1 的 weight
# torch.Size([6, 1, 5, 5])

# 测试网络

接着我们尝试一个随机的 32x32 的输入来检验 (sanity check) 网络定义没有问题。注意:这个网络 (LeNet) 期望的输入大小是 32x32。如果使用 MNIST 数据集 (28x28),我们需要缩放到 32x32。

input = torch.randn(1, 1, 32, 32)
out = net(input)
print(out)
# tensor([[-0.0198,  0.0438,  0.0930, -0.0267, -0.0344,  0.0330,  0.0664,
0.1244, -0.0379,  0.0890]])

默认的梯度会累加,因此我们通常在 backward 之前清除掉之前的梯度值:

net.zero_grad()
out.backward(torch.randn(1, 10))

注意:torch.nn 只支持 mini-batches 的输入。整个 torch.nn 包的输入都必须第一维是 batch,即使只有一个样本也要弄成 batch 是 1 的输入。

比如,nn.Conv2d 的输入是一个 4D 的 Tensor,shape 是 nSamples x nChannels x Height x Width。如果你只有一个样本 (nChannels x Height x Width),那么可以使用 input.unsqueeze (0) 来增加一个 batch 维。

# 损失函数

损失函数的参数是 (output, target) 对,output 是模型的预测,target 是实际的值。损失函数会计算预测值和真实值的差别,损失越小说明预测的越准。

PyTorch 提供了这里有许多不同的损失函数: http://pytorch.org/docs/nn.html#loss-functions。最简单的一个损失函数是:nn.MSELoss,它会计算预测值和真实值的均方误差。比如:

output = net(input)
target = torch.arange(1, 11)  # 随便伪造的一个 “真实值” 
target = target.view(1, -1)  # 把它变成 output 的 shape (1, 10) 
criterion = nn.MSELoss()
loss = criterion(output, target)
print(loss)

如果从 loss 往回走,需要使用 tensor 的 grad_fn 属性,我们 Negative 看到这样的计算图:

input -> conv2d -> relu -> maxpool2d -> conv2d -> relu -> maxpool2d
-> view -> linear -> relu -> linear -> relu -> linear
-> MSELoss
-> loss

因此当调用 loss.backward () 时,PyTorch 会计算这个图中所有 requires_grad=True 的 tensor 关于 loss 的梯度。

print(loss.grad_fn)  # MSELoss
print(loss.grad_fn.next_functions[0][0])  # Add
print(loss.grad_fn.next_functions[0][0].next_functions[0][0])  # Expand
#输出:
<MseLossBackward object at 0x7f445b3a2dd8>
<AddmmBackward object at 0x7f445b3a2eb8>
<ExpandBackward object at 0x7f445b3a2dd8>

# 计算梯度

在调用 loss.backward () 之前,我们需要清除掉 tensor 里之前的梯度,否则会累加进去。

net.zero_grad()     # 清掉 tensor 里缓存的梯度值。
print('conv1.bias.grad before backward')
print(net.conv1.bias.grad)
loss.backward()
print('conv1.bias.grad after backward')
print(net.conv1.bias.grad)

# 更新参数

更新参数最简单的方法是使用随机梯度下降 (SGD): weight=weight−learningrate∗gradientweight=weight−learningrate∗gradient 我们可以使用如下简单的代码来实现更新:

learning_rate = 0.01
for f in net.parameters():
	f.data.sub_(f.grad.data * learning_rate)

通常我们会使用更加复杂的优化方法,比如 SGD, Nesterov-SGD, Adam, RMSProp 等等。为了实现这些算法,我们可以使用 torch.optim 包,它的用法也非常简单:

import torch.optim as optim
# 创建 optimizer,需要传入参数和 learning rate
optimizer = optim.SGD(net.parameters(), lr=0.01)
# 清除梯度
optimizer.zero_grad()  
output = net(input)
loss = criterion(output, target)
loss.backward()
optimizer.step()    # optimizer 会自动帮我们更新参数

注意:即使使用 optimizer,我们也需要清零梯度。但是我们不需要一个个的清除,而是用 optimizer.zero_grad () 一次清除所有。

# 训练一个分类器

介绍了 PyTorch 神经网络相关包之后我们就可以用这些知识来构建一个分类器了。

# 如何进行数据处理

一般地,当我们处理图片、文本、音频或者视频数据的时候,我们可以使用 python 代码来把它转换成 numpy 数组。然后再把 numpy 数组转换成 torch.xxxTensor。

  • 对于处理图像,常见的 lib 包括 Pillow 和 OpenCV
  • 对于音频,常见的 lib 包括 scipy 和 librosa
  • 对于文本,可以使用标准的 Python 库,另外比较流行的 lib 包括 NLTK 和 SpaCy

对于视觉问题,PyTorch 提供了一个 torchvision 包 (需要单独安装),它对于常见数据集比如 Imagenet, CIFAR10, MNIST 等提供了加载的方法。并且它也提供很多数据变化的工具,包括 torchvision.datasets 和 torch.utils.data.DataLoader。这会极大的简化我们的工作,避免重复的代码。

在这个教程里,我们使用 CIFAR10 数据集。它包括十个类别:”airplane”, “automobile”, “bird”, “cat”, “deer”, “dog”, “frog”, “horse”, “ship”,”truck”。图像的对象是 3x32x32,也就是 3 通道 (RGB) 的 32x32 的图片。下面是一些样例图片。

图 2
图:cifar10 样例

# 训练的步骤

  • 使用 torchvision 加载和预处理 CIFAR10 训练和测试数据集。
  • 定义卷积网络
  • 定义损失函数
  • 用训练数据训练模型
  • 用测试数据测试模型

# 数据处理

通过使用 torchvision,我们可以轻松的加载 CIFAR10 数据集。首先我们导入相关的包:

import torch
import torchvision
import torchvision.transforms as transforms

torchvision 读取的 datasets 是 PILImage 对象,它的取值范围是 [0, 1],我们把它转换到范围 [-1, 1]。

transform = transforms.Compose(
	[transforms.ToTensor(),
	transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
trainset = torchvision.datasets.CIFAR10(root='/path/to/data', train=True,
	download=True, transform=transform)
	trainloader = torch.utils.data.DataLoader(trainset, batch_size=4,
	shuffle=True, num_workers=2)
testset = torchvision.datasets.CIFAR10(root='/path/to/data', train=False,
	download=True, transform=transform)
	testloader = torch.utils.data.DataLoader(testset, batch_size=4,
	shuffle=False, num_workers=2)
classes = ('plane', 'car', 'bird', 'cat',
	'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

我们来看几张图片,如下图所示,显示图片的代码如下:

import matplotlib.pyplot as plt
import numpy as np
# 显示图片的函数
def imshow(img):
img = img / 2 + 0.5     #  [-1,1] -> [0,1]
npimg = img.numpy()
plt.imshow(np.transpose(npimg, (1, 2, 0))) # (channel, width, height) -> (width, height, channel)
# 随机选择一些图片
dataiter = iter(trainloader)
images, labels = dataiter.next()
# 显示图片
imshow(torchvision.utils.make_grid(images))
# 打印 label
print(' '.join('%5s' % classes[labels[j]] for j in range(4)))

图 3
图:随机选择的图片

# 定义卷积网络

网络结构和上一节的介绍类似,只是输入通道从 1 变成 3。

import torch.nn as nn
import torch.nn.functional as F
class Net(nn.Module):
	def __init__(self):
		super(Net, self).__init__()
		self.conv1 = nn.Conv2d(3, 6, 5)
		self.pool = nn.MaxPool2d(2, 2)
		self.conv2 = nn.Conv2d(6, 16, 5)
		self.fc1 = nn.Linear(16 * 5 * 5, 120)
		self.fc2 = nn.Linear(120, 84)
		self.fc3 = nn.Linear(84, 10)
	def forward(self, x):
		x = self.pool(F.relu(self.conv1(x)))
		x = self.pool(F.relu(self.conv2(x)))
		x = x.view(-1, 16 * 5 * 5)
		x = F.relu(self.fc1(x))
		x = F.relu(self.fc2(x))
		x = self.fc3(x)
		return x
net = Net()

\subsubsection {定义损失函数和 optimizer} 我们这里使用交叉熵损失函数,Optimizer 使用带冲量的 SGD。

import torch.optim as optim
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)

\subsubsection {训练网络} 我们遍历 DataLoader 进行训练。

for epoch in range(2):  # 这里只迭代 2 个 epoch,实际应该进行更多次训练 
	running_loss = 0.0
	for i, data in enumerate(trainloader, 0):
		# 得到输入
		inputs, labels = data
		# 梯度清零 
		optimizer.zero_grad()
		# forward + backward + optimize
		outputs = net(inputs)
		loss = criterion(outputs, labels)
		loss.backward()
		optimizer.step()
		# 定义统计信息
		running_loss += loss.item()
		if i % 2000 == 1999:
			print('[%d, %5d] loss: %.3f' %
				(epoch + 1, i + 1, running_loss / 2000))
		running_loss = 0.0
print('Finished Training')

# 在测试数据集上进行测试

我们进行了 2 轮迭代,可以使用测试数据集上的数据来进行测试。首先我们随机抽取几个样本来进行测试。

dataiter = iter(testloader)
images, labels = dataiter.next()
imshow(torchvision.utils.make_grid(images))
print('GroundTruth: ', ' '.join('%5s' % classes[labels[j]] for j in range(4)))

随机选择出来的测试样例如下图所示。

图 4
图:随机测试的结果

我们用模型来预测一下,看看是否正确预测:

outputs = net(images)

outputs 是 10 个分类的 logits。我们在训练的时候需要用 softmax 把它变成概率 (CrossEntropyLoss 帮我们做了),但是预测的时候没有必要,因为我们只需要知道哪个分类的概率大就行。

_, predicted = torch.max(outputs, 1)
print('Predicted: ', ' '.join('%5s' % classes[predicted[j]]
		for j in range(4)))
# cat  ship  ship  ship

预测中的四个错了一个,似乎还不错。接下来我们看看在整个测试集合上的效果:

correct = 0
total = 0
with torch.no_grad():
for data in testloader:
	images, labels = data
	outputs = net(images)
	_, predicted = torch.max(outputs.data, 1)
	total += labels.size(0)
	correct += (predicted == labels).sum().item()
print('Accuracy of the network on the 10000 test images: %d %%' % (
	100 * correct / total))
# Accuracy of the network on the 10000 test images: 55 %

看起来比随机的瞎猜要好,因为随机猜的准确率大概是 10% 的准确率,所以模型确实学到了一些东西。我们也可以看每个分类的准确率:

class_correct = list(0. for i in range(10))
class_total = list(0. for i in range(10))
with torch.no_grad():
	for data in testloader:
		images, labels = data
		outputs = net(images)
		_, predicted = torch.max(outputs, 1)
		c = (predicted == labels).squeeze()
		for i in range(4):
			label = labels[i]
			class_correct[label] += c[i].item()
			class_total[label] += 1
for i in range(10):
	print('Accuracy of %5s : %2d %%' % (
		classes[i], 100 * class_correct[i] / class_total[i]))

结果为:

Accuracy of plane : 52 %
Accuracy of   car : 66 %
Accuracy of  bird : 49 %
Accuracy of   cat : 34 %
Accuracy of  deer : 30 %
Accuracy of   dog : 45 %
Accuracy of  frog : 72 %
Accuracy of horse : 71 %
Accuracy of  ship : 76 %
Accuracy of truck : 55 %

# GPU 上训练

为了在 GPU 上训练,我们需要把 Tensor 移到 GPU 上。首先我们看看是否有 GPU,如果没有,那么我们还是 fallback 到 CPU。

device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print(device)
# cuda:0

用 GPU 进行训练:

class Net2(nn.Module):
def __init__(self):
super(Net2, self).__init__()
self.conv1 = nn.Conv2d(3, 6, 5).to(device)
self.pool = nn.MaxPool2d(2, 2).to(device)
self.conv2 = nn.Conv2d(6, 16, 5).to(device)
self.fc1 = nn.Linear(16 * 5 * 5, 120).to(device)
self.fc2 = nn.Linear(120, 84).to(device)
self.fc3 = nn.Linear(84, 10).to(device)
def forward(self, x):
x = self.pool(F.relu(self.conv1(x)))
x = self.pool(F.relu(self.conv2(x)))
x = x.view(-1, 16 * 5 * 5)
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x
net = Net2()
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)
for epoch in range(20):
	running_loss = 0.0
	for i, data in enumerate(trainloader, 0):
		# 得到输入
		inputs, labels = data 
		inputs, labels = inputs.to(device), labels.to(device) 
		# 梯度清零 
		optimizer.zero_grad()
		# forward + backward + optimize
		outputs = net(inputs)
		loss = criterion(outputs, labels)
		loss.backward()
		optimizer.step()
		# 定义统计信息
		running_loss += loss.item()
		if i % 2000 == 1999:
			print('[%d, %5d] loss: %.3f' %
				(epoch + 1, i + 1, running_loss / 2000))
			running_loss = 0.0
		print('Finished Training')

# 通过例子学 PyTorch

下面我们通过使用不同的方法来实现一个简单的三层 (一个隐层) 的全连接神经网络来熟悉 PyTorch 的常见用法。

# 使用 Numpy 实现三层神经网络

我们需要实现一个全连接的激活为 ReLU 的网络,它只有一个隐层,没有 bias,用于回归预测一个值,loss 是计算实际值和预测值的欧氏距离。这里完全使用 numpy 手动的进行前向和后向计算。numpy 数组就是一个 n 维的数值,它并不知道任何关于深度学习、梯度下降或者计算图的东西,它只是进行数值运算。

import numpy as np
# N 是 batch size;D_in 是输入大小
# H 是隐层的大小;D_out 是输出大小。
N, D_in, H, D_out = 64, 1000, 100, 10
# 随机产生输入与输出
x = np.random.randn(N, D_in)
y = np.random.randn(N, D_out)
# 随机初始化参数
w1 = np.random.randn(D_in, H)
w2 = np.random.randn(H, D_out)
learning_rate = 1e-6
for t in range(500):
	# 前向计算 y
	h = x.dot(w1)
	h_relu = np.maximum(h, 0)
	y_pred = h_relu.dot(w2)
	# 计算 loss
	loss = np.square(y_pred - y).sum()
	print(t, loss)
	# 反向计算梯度 
	grad_y_pred = 2.0 * (y_pred - y)
	grad_w2 = h_relu.T.dot(grad_y_pred)
	grad_h_relu = grad_y_pred.dot(w2.T)
	grad_h = grad_h_relu.copy()
	grad_h[h < 0] = 0
	grad_w1 = x.T.dot(grad_h)
	# 更新参数
	w1 -= learning_rate * grad_w1
	w2 -= learning_rate * grad_w2

# 使用 Tensor 来实现三层神经网络

和前面一样,我们还是实现一个全连接的 Relu 激活的网络,它只有一个隐层并且没有 bias。loss 是预测与真实值的欧氏距离。之前我们用 Numpy 实现,自己手动前向计算 loss,反向计算梯度。这里还是一样,只不过把 numpy 数组换成了 PyTorch 的 Tensor。但是使用 PyTorch 的好处是我们可以利用 GPU 来加速计算,如果想用 GPU 计算,我们值需要在创建 tensor 的时候指定 device 为 gpu。

import torch
dtype = torch.float
device = torch.device("cpu")
# device = torch.device ("cuda:0") # 如果想在 GPU 上运算,把这行注释掉。
N, D_in, H, D_out = 64, 1000, 100, 10
x = torch.randn(N, D_in, device=device, dtype=dtype)
y = torch.randn(N, D_out, device=device, dtype=dtype)
w1 = torch.randn(D_in, H, device=device, dtype=dtype)
w2 = torch.randn(H, D_out, device=device, dtype=dtype)
learning_rate = 1e-6
for t in range(500): 
	h = x.mm(w1)
	h_relu = h.clamp(min=0) # 使用 clamp (min=0) 来实现 ReLU
	y_pred = h_relu.mm(w2)
	loss = (y_pred - y).pow(2).sum().item()
	print(t, loss)
	grad_y_pred = 2.0 * (y_pred - y)
	grad_w2 = h_relu.t().mm(grad_y_pred)
	grad_h_relu = grad_y_pred.mm(w2.t())
	grad_h = grad_h_relu.clone()
	grad_h[h < 0] = 0
	grad_w1 = x.t().mm(grad_h)
	w1 -= learning_rate * grad_w1
	w2 -= learning_rate * grad_w2

# 实现 autograd 来实现三层神经网络

还是和前面一样实现一个全连接的网络,只有一个隐层而且没有 bias,使用欧氏距离作为损失函数。这个实现使用 PyTorch 的 Tensor 来计算前向阶段,然后使用 PyTorch 的 autograd 来自动帮我们反向计算梯度。PyTorch 的 Tensor 代表了计算图中的一个节点。如果 x 是一个 Tensor 并且 x.requires_grad=True,那么 x.grad 这个 Tensor 会保存某个 scalar (通常是 loss) 对 x 的梯度。

import torch
dtype = torch.float
device = torch.device("cpu")
# device = torch.device ("cuda:0") # 如果有 GPU 可以注释掉这行
# N 是 batch size;D_in 是输入大小
# H 是隐层的大小;D_out 是输出大小。
N, D_in, H, D_out = 64, 1000, 100, 10
# 创建随机的 Tensor 作为输入和输出
# 输入和输出需要的 requires_grad=False (默认),
# 因为我们不需要计算 loss 对它们的梯度。
x = torch.randn(N, D_in, device=device, dtype=dtype)
y = torch.randn(N, D_out, device=device, dtype=dtype)
# 创建 weight 的 Tensor,需要设置 requires_grad=True 
w1 = torch.randn(D_in, H, device=device, dtype=dtype, requires_grad=True)
w2 = torch.randn(H, D_out, device=device, dtype=dtype, requires_grad=True)
learning_rate = 1e-6
for t in range(500):
	# Forward 阶段: mm 实现矩阵乘法,但是它不支持 broadcasting。
	# 如果需要 broadcasting,可以使用 matmul
	# clamp 本来的用途是把值 clamp 到指定的范围,这里实现 ReLU。 
	y_pred = x.mm(w1).clamp(min=0).mm(w2)
	# pow (2) 实现平方计算。 
	# loss.item () 得到这个 tensor 的值。也可以直接打印 loss,这会打印很多附加信息。
	loss = (y_pred - y).pow(2).sum()
	print(t, loss.item())
	# 使用 autograd 进行反向计算。它会计算 loss 对所有对它有影响的
	# requires_grad=True 的 Tensor 的梯度。
	loss.backward()
	# 手动使用梯度下降更新参数。一定要把更新的代码放到 torch.no_grad () 里
	# 否则下面的更新也会计算梯度。后面我们会使用 torch.optim.SGD,
	# 它会帮我们管理这些用于更新梯度的计算。
	with torch.no_grad():
		w1 -= learning_rate * w1.grad
		w2 -= learning_rate * w2.grad
		# 手动把梯度清零 
		w1.grad.zero_()
		w2.grad.zero_()

# 使用自定义的 ReLU 函数

这里还是那个全连接网络的例子,不过这里我们不使用 clamp 来实现 ReLU,而是我们自己来实现一个 MyReLU 的函数。

import torch
class MyReLU(torch.autograd.Function):
	"""
	为了实现自定义的实现autograd的函数,我们需要基础torch.autograd.Function,
	然后再实现forward和backward两个函数。
	"""
	@staticmethod
	def forward(ctx, input):
		"""
		在forward函数,我们的输入是input,然后我们根据input计算输出。
		# 同时为了下面的 backward,
		我们需要使用save_for_backward来保存用于反向计算的数据到ctx里,
		# 这里我们需要保存 input。
		"""
		ctx.save_for_backward(input)
		return input.clamp(min=0)
	@staticmethod
	def backward(ctx, grad_output):
		"""
		从ctx.saved_tensors里恢复input
		然后用input计算梯度
		"""
		input, = ctx.saved_tensors
		grad_input = grad_output.clone()
		grad_input[input < 0] = 0
		return grad_input
dtype = torch.float
device = torch.device("cpu")
N, D_in, H, D_out = 64, 1000, 100, 10
x = torch.randn(N, D_in, device=device, dtype=dtype)
y = torch.randn(N, D_out, device=device, dtype=dtype)
w1 = torch.randn(D_in, H, device=device, dtype=dtype, requires_grad=True)
w2 = torch.randn(H, D_out, device=device, dtype=dtype, requires_grad=True)
learning_rate = 1e-6
for t in range(500):
	# 为了调用我们自定义的函数,我们需要使用 Function.apply 方法,把它命名为 'relu'
	relu = MyReLU.apply
	# 我们使用自定义的 ReLU 来进行 Forward 计算
	y_pred = relu(x.mm(w1)).mm(w2)
	loss = (y_pred - y).pow(2).sum()
	print(t, loss.item())
	loss.backward()
	with torch.no_grad():
		w1 -= learning_rate * w1.grad
		w2 -= learning_rate * w2.grad
		w1.grad.zero_()
		w2.grad.zero_()

# 和 Tensorflow 的对比

这里我们还是和前面一样,实现一个隐层的全连接神经网络,优化的目标函数是预测值和真实值的欧氏距离。这个实现使用基本的 Tensorflow 操作来构建一个计算图,然后多次执行这个计算图来训练网络。Tensorflow 和 PyTorch 最大的区别之一就是 Tensorflow 使用静态计算图和 PyTorch 使用动态计算图。在 Tensorflow 里,我们首先构建计算图,然后多次执行它。

import tensorflow as tf
import numpy as np
# 首先构建计算图。
# N 是 batch 大小;D_in 是输入大小。
# H 是隐单元个数;D_out 是输出大小。
N, D_in, H, D_out = 64, 1000, 100, 10
# 输入和输出是 placeholder,在用 session 执行 graph 的时候
# 我们会 feed 进去一个 batch 的训练数据。
x = tf.placeholder(tf.float32, shape=(None, D_in))
y = tf.placeholder(tf.float32, shape=(None, D_out))
# 创建变量,并且随机初始化。 
# 在 Tensorflow 里,变量的生命周期是整个 session,因此适合用它来保存模型的参数。
w1 = tf.Variable(tf.random_normal((D_in, H)))
w2 = tf.Variable(tf.random_normal((H, D_out)))
# Forward pass:计算模型的预测值 y_pred 
# 注意和 PyTorch 不同,这里不会执行任何计算,
# 而只是定义了计算,后面用 session.run 的时候才会真正的执行计算。
h = tf.matmul(x, w1)
h_relu = tf.maximum(h, tf.zeros(1))
y_pred = tf.matmul(h_relu, w2)
# 计算 loss 
loss = tf.reduce_sum((y - y_pred) ** 2.0)
# 计算梯度。 
grad_w1, grad_w2 = tf.gradients(loss, [w1, w2])
# 使用梯度下降来更新参数。assign同样也只是定义更新参数的操作,不会真正的执行。
# 在Tensorflow里,更新操作是计算图的一部分;
# 而在PyTorch里,因为是动态的”实时“的计算,
# 所以参数的更新只是普通的Tensor计算,不属于计算图的一部分。
learning_rate = 1e-6
new_w1 = w1.assign(w1 - learning_rate * grad_w1)
new_w2 = w2.assign(w2 - learning_rate * grad_w2)
# 计算图构建好了之后,我们需要创建一个session来执行计算图。
with tf.Session() as sess:
	# 首先需要用session初始化变量 
	sess.run(tf.global_variables_initializer())
	# 这是 fake 的训练数据
	x_value = np.random.randn(N, D_in)
	y_value = np.random.randn(N, D_out)
	for _ in range(500):
		# 用 session 多次的执行计算图。每次 feed 进去不同的数据。
		# 这里是模拟的,实际应该每次 feed 一个 batch 的数据。
		# run 的第一个参数是需要执行的计算图的节点,它依赖的节点也会自动执行,
		# 因此我们不需要手动执行 forward 的计算。
		# run 返回这些节点执行后的值,并且返回的是 numpy array
		loss_value, _, _ = sess.run([loss, new_w1, new_w2],
				feed_dict={x: x_value, y: y_value})
		print(loss_value)

# 使用 nn 模块来实现三层神经网络

我们接下来使用 nn 模块来实现这个简单的全连接网络。前面我们通过用 Tensor 和 Operation 等 low-level API 来创建 动态的计算图,这里我们使用更简单的 high-level API。

import torch
print(torch.__version__)
# N 是 batch size;D_in 是输入大小 # H 是隐层的大小;D_out 是输出大小。 N, D_in, H, D_out = 64, 1000, 100, 10
# 创建随机的 Tensor 作为输入和输出 x = torch.randn (N, D_in)
y = torch.randn(N, D_out)
# 使用 nn 包来定义网络。nn.Sequential 是一个包含其它模块 (Module) 的模块。 # 每个 Linear 模块使用线性函数来计算,它会内部创建需要的 weight 和 bias。 model = torch.nn.Sequential (
	torch.nn.Linear(D_in, H),
	torch.nn.ReLU(),
	torch.nn.Linear(H, D_out),
)
# 常见的损失函数在 nn 包里也有,不需要我们自己实现 loss_fn = torch.nn.MSELoss (size_average=False)
learning_rate = 1e-4
for t in range(500):
# 前向计算:通过 x 来计算 y。Module 对象会重写__call__函数, # 因此我们可以把它当成函数来调用。 y_pred = model (x)
# 计算 loss loss = loss_fn (y_pred, y)
print(t, loss.item())
# 梯度清空,调用 Sequential 对象的 zero_grad 后所有里面的变量都会清零梯度 model.zero_grad ()
# 反向计算梯度。我们通过 Module 定义的变量都会计算梯度。 loss.backward ()
# 更新参数,所有的参数都在 model.paramenters () 里 
with torch.no_grad():
	for param in model.parameters():
		param -= learning_rate * param.grad

# 使用 optim 包

前面我们使用 nn 模块时是自己来更新模型参数的,PyTorch 也提供了 optim 包,我们可以使用里面的 Optimizer 来自动的更新模型参数。除了最基本的 SGD 算法,这个包也实现了常见的 SGD+momentum, RMSProp, Adam 等算法。

import torch
N, D_in, H, D_out = 64, 1000, 100, 10
x = torch.randn(N, D_in)
y = torch.randn(N, D_out)
model = torch.nn.Sequential(
	torch.nn.Linear(D_in, H),
	torch.nn.ReLU(),
	torch.nn.Linear(H, D_out),
)
loss_fn = torch.nn.MSELoss(size_average=False)
# 使用 Adam 算法,需要提供模型的参数和 learning rate learning_rate = 1e-4
optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)
for t in range(500): 
	y_pred = model(x)
	loss = loss_fn(y_pred, y)
	print(t, loss.item())
	# 梯度清零,原来调用的是 model.zero_grad,现在调用的是 optimizer 的 zero_grad 	optimizer.zero_grad ()
	loss.backward()
	# 调用 optimizer.step 实现参数更新 	optimizer.step ()

# 自定义 nn 模块

对于复杂的网络结构,我们可以通过基础 Module 了自定义 nn 模块。这样的好处是用一个类来同样管理,而且更容易复用代码。

import torch
class TwoLayerNet(torch.nn.Module):
	def __init__(self, D_in, H, D_out):
		""" 在构造函数里,我们定义两个nn.Linear模块,把它们保存到self里。 """
		super(TwoLayerNet, self).__init__()
		self.linear1 = torch.nn.Linear(D_in, H)
		self.linear2 = torch.nn.Linear(H, D_out)
	def forward(self, x):
		""" 在forward函数里,我们需要根据网络结构来实现前向计算。 通常我们会上定义的模块来计算。 """
		h_relu = self.linear1(x).clamp(min=0)
		y_pred = self.linear2(h_relu)
		return y_pred
N, D_in, H, D_out = 64, 1000, 100, 10
x = torch.randn(N, D_in)
y = torch.randn(N, D_out)
model = TwoLayerNet(D_in, H, D_out)
criterion = torch.nn.MSELoss(size_average=False)
optimizer = torch.optim.SGD(model.parameters(), lr=1e-4)
for t in range(500): 
	y_pred = model(x)
	loss = criterion(y_pred, y)
	print(t, loss.item())
	optimizer.zero_grad()
	loss.backward()
	optimizer.step()

# 流程控制和参数共享

为了展示 PyTorch 的动态图的能力,我们这里会实现一个很奇怪模型:这个全连接的网络的隐层个数是个 1 到 4 之间的随机数,而且这些网络层的参数是共享的。

import random
import torch
class DynamicNet(torch.nn.Module):
	def __init__(self, D_in, H, D_out):
		""" 构造3个nn.Linear实例。 """
		super(DynamicNet, self).__init__()
		self.input_linear = torch.nn.Linear(D_in, H)
		self.middle_linear = torch.nn.Linear(H, H)
		self.output_linear = torch.nn.Linear(H, D_out)
	def forward(self, x):
		# 输入和输出层是固定的,但是中间层的个数是随机的 (0,1,2), 		# 并且中间层的参数是共享的。 
		# 因为每次计算的计算图是动态 (实时) 构造的, 		# 所以我们可以使用普通的 Python 流程控制代码比如 for 循环 		# 来实现。读者可以尝试一下怎么用 TensorFlow 来实现。 		# 另外一点就是一个 Module 可以多次使用,这样就 		# 可以实现参数共享。 
		h_relu = self.input_linear(x).clamp(min=0)
		for _ in range(random.randint(0, 3)):
		h_relu = self.middle_linear(h_relu).clamp(min=0)
		y_pred = self.output_linear(h_relu)
		return y_pred
N, D_in, H, D_out = 64, 1000, 100, 10
x = torch.randn(N, D_in)
y = torch.randn(N, D_out)
model = DynamicNet(D_in, H, D_out)
criterion = torch.nn.MSELoss(size_average=False)
optimizer = torch.optim.SGD(model.parameters(), lr=1e-4, momentum=0.9)
for t in range(500): 
	y_pred = model(x)
	loss = criterion(y_pred, y)
	print(t, loss.item())
	optimizer.zero_grad()
	loss.backward()
	optimizer.step()

# 迁移学习示例

在这个教程里,我们会学习怎么使用迁移学习来训练模型。通常我们的训练数据量不会很大,很难达到像 ImageNet 那样上百万的标注数据集。我们可以使用迁移学习来解决训练数据不足的问题。迁移学习里,我们根据训练数据的多少通常可以采取如下方法:

  • 训练数据很少 那么我们通常把一个 pretraning 的网络的大部分固定住,然后只是把最后一个全连接层换成新的 (最后一层通常是不一样的,因为分类的数量不同),然后只训练这一层
  • 训练数据较多 我们可以把 pretraining 的网络的前面一些层固定住,但后面的层不固定,把最后一层换新的,然后训练
  • 训练数据很多 所有的 pretraining 的层都可以 fine-tuning,只是用 pretraining 的参数作为初始化参数。

首先我们引入依赖:

from __future__ import print_function, division
import torch
import torch.nn as nn
import torch.optim as optim
from torch.optim import lr_scheduler
import numpy as np
import torchvision
from torchvision import datasets, models, transforms
import matplotlib.pyplot as plt
import time
import os
import copy
plt.ion()

# 加载数据

我们使用 torchvision 和 torch.utils.data 包来加载数据。我们要解决的问题是训练一个模型来区分蚂蚁和蜜蜂,每个类别我们大概有 120 个训练数据,另外每个类有 75 个验证数据。这是一个很小的训练集,如果直接用一个神经网络来训练,效果会很差。现在我们用迁移学习来解决这个问题。数据可以在这里下载,下载后请解压到 data 目录下。

# 训练的时候会做数据增强和归一化
# 而验证的时候只做归一化
data_transforms = {
	'train': transforms.Compose([
		transforms.RandomResizedCrop(224),
		transforms.RandomHorizontalFlip(),
		transforms.ToTensor(),
		transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
	]),
	'val': transforms.Compose([
		transforms.Resize(256),
		transforms.CenterCrop(224),
		transforms.ToTensor(),
		transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
	]),
}
data_dir = '../data/hymenoptera_data'
image_datasets = {x: datasets.ImageFolder(os.path.join(data_dir, x),
		data_transforms[x]) 
	for x in ['train', 'val']}
dataloaders = {x: torch.utils.data.DataLoader(image_datasets[x], batch_size=4,
		shuffle=True, num_workers=4)
	for x in ['train', 'val']}
dataset_sizes = {x: len(image_datasets[x]) for x in ['train', 'val']}
class_names = image_datasets['train'].classes
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

# 可视化图片

我们来显示几张图片看看,下图是一个 batch 的图片,显示的代码如下:

def imshow(inp, title=None):
	inp = inp.numpy().transpose((1, 2, 0))
	mean = np.array([0.485, 0.456, 0.406])
	std = np.array([0.229, 0.224, 0.225])
	inp = std * inp + mean
	inp = np.clip(inp, 0, 1)
	plt.imshow(inp)
	if title is not None:
		plt.title(title)
	plt.pause(0.001)
# 得到一个 batch 的数据
inputs, classes = next(iter(dataloaders['train']))
# 把 batch 张图片拼接成一个大图
out = torchvision.utils.make_grid(inputs)
imshow(out, title=[class_names[x] for x in classes])

图 5
图:迁移学习数据示例

# 训练模型

现在我们来实现一个用于训练模型的通用函数。这里我们会演示怎么实现:

  • learning rate 的自适应
  • 保存最好的模型

在下面的函数中,参数 scheduler 是来自 torch.optim.lr_scheduler 的 LR scheduler 对象 (_LRScheduler 的子类)

def train_model(model, criterion, optimizer, scheduler, num_epochs=25):
	since = time.time()
	best_model_wts = copy.deepcopy(model.state_dict())
	best_acc = 0.0
	for epoch in range(num_epochs):
		print('Epoch {}/{}'.format(epoch, num_epochs - 1))
		print('-' * 10)
		# 每个 epoch 都分为训练和验证阶段
		for phase in ['train', 'val']:
			if phase == 'train':
				scheduler.step()
				model.train()  # 训练阶段
			else:
				model.eval()   # 验证阶段
	
			running_loss = 0.0
			running_corrects = 0
	
			# 变量数据集
			for inputs, labels in dataloaders[phase]:
				inputs = inputs.to(device)
				labels = labels.to(device)
	
			# 参数梯度清空
			optimizer.zero_grad()
	
			# forward
			# 只有训练的时候 track 用于梯度计算的历史信息。
			with torch.set_grad_enabled(phase == 'train'):
				outputs = model(inputs)
				_, preds = torch.max(outputs, 1)
				loss = criterion(outputs, labels)
		
				# 如果是训练,那么需要 backward 和更新参数 
				if phase == 'train':
					loss.backward()
					optimizer.step()
	
			# 统计
			running_loss += loss.item() * inputs.size(0)
			running_corrects += torch.sum(preds == labels.data)
	
			epoch_loss = running_loss / dataset_sizes[phase]
			epoch_acc = running_corrects.double() / dataset_sizes[phase]
	
			print('{} Loss: {:.4f} Acc: {:.4f}'.format(
				phase, epoch_loss, epoch_acc))
	
			# 保存验证集上的最佳模型
			if phase == 'val' and epoch_acc > best_acc:
				best_acc = epoch_acc
				best_model_wts = copy.deepcopy(model.state_dict())
	
			print()
	time_elapsed = time.time() - since
	print('Training complete in {:.0f}m {:.0f}s'.format(
		time_elapsed // 60, time_elapsed % 60))
	print('Best val Acc: {:4f}'.format(best_acc))
	# 加载最优模型
	model.load_state_dict(best_model_wts)
	return model

# 可视化预测结果的函数

def visualize_model(model, num_images=6):
	was_training = model.training
	model.eval()
	images_so_far = 0
	fig = plt.figure()
	with torch.no_grad():
		for i, (inputs, labels) in enumerate(dataloaders['val']):
			inputs = inputs.to(device)
			labels = labels.to(device)
	
			outputs = model(inputs)
			_, preds = torch.max(outputs, 1)
	
			for j in range(inputs.size()[0]):
				images_so_far += 1
				ax = plt.subplot(num_images//2, 2, images_so_far)
				ax.axis('off')
				ax.set_title('predicted: {}'.format(class_names[preds[j]]))
				imshow(inputs.cpu().data[j])
		
				if images_so_far == num_images:
					model.train(mode=was_training)
					return
		model.train(mode=was_training)

# fine-tuning 所有参数

我们首先加载一个预训练的模型 (imagenet 上的 resnet),因为我们的类别数和 imagenet 不同,所以我们需要删掉原来的全连接层,换成新的全连接层。这里我们让所有的模型参数都可以调整,包括新加的全连接层和预训练的层。

model_ft = models.resnet18(pretrained=True)
num_ftrs = model_ft.fc.in_features
model_ft.fc = nn.Linear(num_ftrs, 2)
model_ft = model_ft.to(device)
criterion = nn.CrossEntropyLoss()
# 所有的参数都可以训练
optimizer_ft = optim.SGD(model_ft.parameters(), lr=0.001, momentum=0.9)
# 每 7 个 epoch learning rate 变为原来的 10% 
exp_lr_scheduler = lr_scheduler.StepLR(optimizer_ft, step_size=7, gamma=0.1)
model_ft = train_model(model_ft, criterion, optimizer_ft, exp_lr_scheduler,
	num_epochs=25)

最终我们得到的分类准确率大概在 94.7%。

# fine-tuning 最后一层参数

我们用可以固定住前面层的参数,只训练最后一层。这比之前要快将近一倍,因为反向计算梯度只需要计算最后一层。但是前向计算的时间是一样的。

model_conv = torchvision.models.resnet18(pretrained=True)
for param in model_conv.parameters():
	param.requires_grad = False
# 新加的层默认 requires_grad=True 
num_ftrs = model_conv.fc.in_features
model_conv.fc = nn.Linear(num_ftrs, 2)
model_conv = model_conv.to(device)
criterion = nn.CrossEntropyLoss()
# 值训练最后一个全连接层。
optimizer_conv = optim.SGD(model_conv.fc.parameters(), lr=0.001, momentum=0.9)
exp_lr_scheduler = lr_scheduler.StepLR(optimizer_conv, step_size=7, gamma=0.1)
model_conv = train_model(model_conv, criterion, optimizer_conv,
	exp_lr_scheduler, num_epochs=25)

最终我们得到的分类准确率大概在 96%。