读书:《深度学习框架PyTorch入门与实践》初注

记录读《深度学习框架PyTorch入门与实践》所思所想所实践。

文章目录

读《深度学习框架PyTorch入门与实践》

  • 本书读者先掌握基础的numpy使用,numpy的基础知识可以参考CS231n上关于numpy的教程。

1 快速入门

Pyorch对应的Python包名为torch而非pytorch。

1.1 Tensor

Tensor是PyTorch中重要数据结构,可认为是一个高维数组。Tensor和numpy的ndarrays类似,但Tensor可以使用GPU加速。

  • 函数名后面带下划线_的函数会修改Tensor本身,例如 x.add_(y),x.grad.data.zero_()以下画线结束的函数是inplace操作;
  • Tensor不支持的操作,可以先转为numpy数组处理,之后再转回Tensor:
b=a.numpy()Tensor->Numpy
b=t.from_numpy(a)Numpy->Tensor
  • Tensor和numpy对象共享内存,所以转换很快,而且几乎不会消耗资源,同时也意味着,如果其中一个变了,另外一个也随之改变;
  • Tensor可通过x.cuda()方法转为GPU的Tensor,从而享受GPU带来的加速运算,但是将数据从内存转移到显存是需要花费额外开销,GPU的优势是在大规模数据和复杂运算下才能体现出来;

1.2 Autograd自动微分

深度学习算法本质是通过反向传播求导数,PyTorch的Autograd模块实现了此功能。在Tensor上的所有操作,Autograd都能为他们自动提供微分,避免手动计算导数的复杂过程。
autograd.Variable是Autograd中的核心类(from torch.autograd import Variable),它简单封装了Tensor,并支持几乎所有Tensor的操作。可调用它的.backward实现反向传播,自动计算所有梯度。
( a u t o g r a d . ) V a r i a b l e { d a t a g r a d g r a d _ f n (autograd.)Variable\left\{\begin{matrix} data \\grad \\grad\_fn\end{matrix}\right. (autograd.)Variable datagradgrad_fn

Variable主要包含三个属性:
①data,保存Variable所包含的Tensor
②grad,保存data对应的梯度,grad也是个Variable,而不是Tensor,它和data的形状一样
③grad_fn,指向一个Function对象,这个Function用来反向传播计算输入的梯度

y.backward()#反向传播,计算梯度
x.grad.data.zero_()

  • grad在反向传播过程中是累加的(accumulated),这意味着每次运行反向传播梯度都会累加之前的梯度,所以反向传播之前需把梯度清零;
  • Variable和Tensor具有近乎一致的接口,在实际中可以无缝切换;

1.3 神经网络

Autograd实现了反向传播功能,但是直接用来写深度学习代码在很多情况下还是稍显复杂,torch.nn是专门为神经网络设计的模块化接口。
nn构建于Autograd之上,可用于定义和运行神经网络。nn.Module是nn中最重要的类,可以把它看作一个网络的封装,包含网络各层定义及forward方法,调用forward(input)方法,可返回前向传播的结果。
以最早的卷积神经网络LeNet为例,这是一个基础的前向传播(feed-forward)网络。
在这里插入图片描述

1.3.1 定义网络

定义网络时,需要继承nn.Module,并实现它的forward方法,网络中具有可学习参数的层放在构造函数__init__中;如果某一层不具有可学习的参数,则既可以放在构造函数中,也可以不放。

import torch.nn as nn
import torch.nn.functional as F

class Net(nn.Module):
    def __init__(self):
        # nn.Module子类的函数必须在构造函数中执行父类的构造函数
        # 下式等价于nn.Module.__init__(self)
        super(Net,self).__init__()
        # 卷积层'1'表示输入图片为单通道,'6'表示输出通道数
        # '5'表示卷积核为5*5
        self.conv1 = nn.Conv2d(1,6,5)
        # 卷积层
        self.conv2 = nn.Conv2d(6,16,5)
        # 仿射层/全连接层,y=Wx+b
        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=F.max_pool2d(F.relu(self.conv1(x)),(2,2))
        x=F.max_pool2d(F.relu(self.conv2(x)),2)
        # reshape,'-1'表示自适应
        x = x.view(x.size()[0],-1)
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x

net = Net()
print(net) 

只要在nn.Module的子类中定义了forward函数,backward函数就会自动实现(利用Autograd)。在forward函数中可以使用任何Variable支持的函数,还可以使用if、for循环、print、log等Python语法,写法和标准的Python写法一致。

网络的可学习参数通过net.parameters()返回,net.named_parameters可同时返回可学习的参数及名称。

params = list(net.parameters())
for name,parameters in net.named_parameters():
print(name,‘:’,parameters.size())

forward函数的输入和输出都是Variable,只有Variable才具有自动求导功能,Tensor是没有的,所以在输入时,需要把Tensor封装成Variable。

input = Variable(t.randn(1,1,32,32))
out = net(input)

net.zero_grad()#所有参数的梯度清零
out.backward(Variable(t.ones(1,10)))#反向传播

需要注意的是,torch.nn只支持mini-batches,不支持一次只输入一个样本,即一次必须是一个batch。如果只想输入一个样本,则用input.unsqueeze(0)将batch_size设为1。如nn.Conv2d输入必须是4维的,形如nSamplesXnChannelsXHeightXWidth,可将nSample设为1,即1XnChannelsXHeightXWidth。

1.3.2 损失函数

nn实现了神经网络中大多数的损失函数,例如nn.MSELoss用来计算均方误差,nn.CrossEntropyLoss用来计算交叉熵损失。

output = net(input)
target = Variable(t.arange(0,10))
criterion = nn.MSELoss()
loss = criterion(output,target)

如果对loss进行反向传播溯源(使用grad_fn属性),可看到它的计算图如下:

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

当调用loss.backward()时,该图会动态生成并自动微分,也会自动计算图中参数(Parameter)的导数。

1.3.3 优化器

在反向传播计算完所有参数的梯度后,还需要使用优化方法更新网络的权重和参数。如,随机梯度下降法(SGD)的更新策略是:weight=weight-learning_rate*gradient。

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

torch.optim中实现了深度学习中绝大多数优化方法,如RMSProp、Adam、SGD等,因此通常并不需要手动写上述代码。

import torch.optim as optim
# 新建一个优化器,指定要调整的参数和学习率
optimizer = optim.SGD(net.parameters(),lr=0.01)
# 在训练过程中
# 先梯度清零(与net.zero_grad()效果一样)
optimizer.zero_grad()
# 计算损失
output = net(input)
loss = criterion(output,target)
# 反向传播
loss.backward()
# 更新参数
optimizer.step()

1.3.4 数据加载与预处理

在深度学习中数据加载及预处理是非常复杂繁琐的,但PyTorch提供了一些可极大简化和加快数据处理流程的工具。同时对于常用数据集PyTorch也提供了封装好的接口供用户快速调用,这些数据集主要保存在torchvision中。
torchvision实现了常用的图像数据加载功能,如Imagenet,CIFAR10、MNIST等,以及常用的数据转换操作,这极大地方便了数据加载。

1.4 小试牛刀:CIFAR-10分类

下面尝试实现对CIFAR-10数据集分类,步骤如下:
(1)使用torchvision加载并预处理CIFAR-10数据集
(2)定义网络
(3)定义损失函数和优化器
(4)训练网络并更新网络参数
(5)测试网络

1.4.0 实践问题及解决

1.4.0.1 学习1:土堆深度学习教程

PyTorch深度学习快速入门教程(绝对通俗易懂!)【小土堆】

(1)python-pytorch学习中两大法宝工具:
dir()函数:能让我们知道工具箱以及工具箱中的分隔区有什么东西;
help()函数:能让我们知道每个工具是如何使用的,工具的使用方法。
(2)pytorch中数据加载:
主要涉及两个类DataSet和DataLoader
DataSet:提供一种方式从海量数据池中获取需要的数据及其label。主要实现两种功能:如何获取每一个数据及其label和告诉我们总共有多少数据;
DataLoader:将获取到的数据进行整理、打包、压缩,为后面的网络提供不同的数据形式
(3)TensorBoard的使用,tensorflow中有,在pytorch1.7之后加入;
需要安装TensorBoard包,打开TensorBoard:tensorboard --logdir=logs(目录路径) --port=6007(可选,指定端口)

from torch.utils.tensorboard import SummaryWriter #向log_dir文件输入数据,能被TensorBoard解析
writer =SummaryWriter(“logs”) #存储文件夹
writer.add_image()
writer.add_scalar()

(4)torchvision中的transforms
transforms.py工具箱,将输入的图片经过totensor、resize等工具,得到输出结果

//土堆的视频后续再继续看

1.4.0.2 问题1:Downloading data from https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz国内下载速度缓慢的问题:

<1>下载数据集(官网页面:http://www.cs.toronto.edu/~kriz/cifar.html 下载地址:http://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz)
<2>把cifar-10-python.tar.gz更名为cifar-10-batches-py.tar.gz并放到本地文件夹里,这里放到./data目录下,然后用 tar -zxvf cifar-10-batches-py.tar.gz 命令解压。
<3>加载数据,记得设置download = False。如果上一步不知道该把数据集放到哪里,可以先设置为True,然后看下载位置在哪,之后替换掉。

方法二:pycharm中添加源,
新版Pycharm修改源不再是setting-Project-Python-InterPreter中‘+’号,通过manage repositories按钮很方便地管理Pycharm中的源,但是新版已取消该按钮。

配置源方法:
调出python package包管理界面:view - Tool windows - Python Packages
点开它,再点击小齿轮,就可以添加Pycharm内置的软件包安装源了

豆瓣:http://pypi.douban.com/simple/ 阿里云:http://mirrors.aliyun.com/pypi/simple/ 清华:https://pypi.tuna.tsinghua.edu.cn/simple/

在这里插入图片描述

1.4.1 CIFA-10 数据加载及预处理

CIFAR-10是一个常用的彩色图片数据集,它有10个类别airplane、automobile、bird、cat、deer、dog、frog、horse、ship和truck。每张图片都是3X32X32,也即3通道彩色图片,分辨率为32X32。

1.4.1.1 下载加载显示CIFA10数据集
1.4.1.1.1 代码部分
import torch as t
import torchvision as tv
import torchvision.transforms as transforms
from torchvision.transforms import ToPILImage

trans_to_image = ToPILImage() #可以把Tensor转成Image,方便可视化

#第一次运行程序torchvision会自动下载CIFAR-10数据集
#大约100MB,需花费一定的时间,
#如果已经下载有CIFAR-10,可通过root参数指定

#定义对数据的预处理
transform =transforms.Compose([transforms.ToTensor(),#转为Tensor
                               transforms.Normalize((0.5,0.5,0.5),(0.5,0.5,0.5)), #归一化
                               ])

#torchvision格式的数据集的范围为[0,1],我们这里使用transforms方法将它归一化为[-1,1]的张量。

#训练集
trainset = tv.datasets.CIFAR10(
                    root='./data',
                    train=True,
                    download=False,
                    transform=transform)
trainloader = t.utils.data.DataLoader(
                    trainset,
                    batch_size=4,
                    shuffle=True,
                    num_workers=2)

# 测试集
testset = tv.datasets.CIFAR10(
                    './data',
                    train=False,
                    download=False,
                    transform=transform)
testloader = t.utils.data.DataLoader(
                    testset,
                    batch_size=4,
                    shuffle=False,
                    num_workers=2)

classes = ('plane', 'car', 'bird', 'cat',
           'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

'''
Using downloaded and verified file: ./data/cifar-10-python.tar.gz
Files already downloaded and verified
'''
#Dataset对象是一个数据集,可以按下标访问,返回形如(data, label)的数据。

print(len(trainset))
print(trainset[0][0].size())
print(trainset[0][1])
print(classes[trainset[0][1]])
'''
50000
torch.Size([3, 32, 32])
6
frog
'''

# 测试数据集中第一张图片及target
img, target = testset[0]
print(img.shape)
print(target)
'''
torch.Size([3, 32, 32]),RGB3通道,32x32
3
'''

(data, label) = trainset[100] # trainset里的样本是一个tuple
print(data.size()) # data是一个Tensor
print(label) # label是int
print(classes[label])

import matplotlib.pyplot as plt

# (data + 1) / 2是为了还原被归一化的数据
c = (data + 1) / 2 #tensor格式
(trans_to_image((data + 1) / 2).resize((100, 100))).show()
'''
torch.Size([3, 32, 32])
8
ship
'''

1.4.1.1.1 补充1 Transforms相关的知识及使用

对于视觉方向的图像处理方面,PyTorch提供了很好的预处理接口,对于图像的转换处理,使用 torchvision.tranforms 模块使得这些操作非常高效。
   在transforms.py文件中定义了许多常用的类,通过查看其定义文件(其中的__all__变量),可看到文件中定义的全部类的信息。

关于Tensorboard的基本使用,可以参照这个Tensorboard博文中的内容

#代码中主要使用了transorfoms中的Tensor类,其中SummaryWriter用于tensorboard展示图片。
from PIL import Image
from torch.utils.tensorboard import SummaryWriter
from torchvision import transforms

# python的用法-》tensor数据类型
# 通过transforms.ToTensor去解决两个问题

# 2、为什么我们需要Tensor数据类型
img_path = "dataset/train/ants_image/0013035.jpg"
img = Image.open(img_path)
# writer用于tensorboard显示图片
writer = SummaryWriter('logs')

# 1、transsforms该如何使用
tensor_trans = transforms.ToTensor()
tensor_img = tensor_trans(img)

writer.add_image("Tensor_img", tensor_img)
writer.close()
# 在上段代码中用到了Tensorboard,右键运行main.py文件,会在项目结果中生成logs文件
# 在PyCharm中的Terminal界面中输入代码:python3 -m tensorboard.main --logdir=logs
# 该代码用于设置tensorboard的文件夹
# 如果不显示上面的结果,可试试下面的代码:tensorboard --logdir=logs
# 直接点击http://localhost:6007,或者在浏览器输入上述地址,在浏览器中展示Tensorboard的内容

常用的Transforms:

  • ToTensor是实现将图像转换为tensor张量类型,Normalize是实现对张量的归一化操作。
from PIL import Image
from torch.utils.tensorboard import SummaryWriter
from torchvision import transforms

writer = SummaryWriter("logs")
img = Image.open("images/pytorch.jpg").convert('RGB')    
#convert('RGB')实现将图片映射到RGB三通道上面
print(img)

# Totensor的使用  ToTensor是指把PIL.Image(RGB) 或者numpy.ndarray(H x W x C) 从0到255的值映射到0到1的范围内,并转化成Tensor格式
trans_totensor = transforms.ToTensor()
img_tensor = trans_totensor(img)
print(img_tensor.shape)
writer.add_image("ToTensor", img_tensor)

# Normalize的使用 Normalize(mean,std)是通过下面公式实现数据归一化,mean表示平均值,std表示标准差
# channel=(channel-mean)/std
print(img_tensor[0][0][0])  # 输出第一个像素点归一化之前的值
trans_norm = transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5])  
# 假设均值为[0.5, 0.5, 0.5],标准差为[0.5, 0.5, 0.5]
img_norm = trans_norm(img_tensor)    
# 将图片转换为tensor之后,对tensor进行归一化处理
print(img_norm[0][0][0])  
# 输出第一个像素点归一化之后的值
writer.add_image("Normalize", img_norm, 1)
writer.close()
  • Resize的使用
    Resize是修改原来图像的尺寸信息
from PIL import Image
from torch.utils.tensorboard import SummaryWriter
from torchvision import transforms

writer = SummaryWriter("logs")
img = Image.open("images/pytorch.jpg").convert('RGB')
# convert('RGB')实现将图片映射到RGB三通道上面
print(img)

# Totensor的使用  ToTensor是指把PIL.Image(RGB) 或者numpy.ndarray(H x W x C) 从0到255的值映射到0到1的范围内,并转化成Tensor格式
trans_totensor = transforms.ToTensor()
img_tensor = trans_totensor(img)
print(img_tensor.shape)
writer.add_image("ToTensor", img_tensor)

# Normalize的使用 Normalize(mean,std)是通过下面公式实现数据归一化,mean表示平均值,std表示标准差
# channel=(channel-mean)/std
print(img_tensor[0][0][0])  # 输出第一个像素点归一化之前的值
trans_norm = transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5])  # 假设均值为[0.5, 0.5, 0.5],标准差为[0.5, 0.5, 0.5]
img_norm = trans_norm(img_tensor)    # 将图片转换为tensor之后,对tensor进行归一化处理
print(img_norm[0][0][0])  # 输出第一个像素点归一化之后的值
writer.add_image("Normalize", img_norm, 1)

# Resize的使用
print(img.size)
trans_resize = transforms.Resize((512, 512))
# img PIL -> resize -> img_resize PIL
img_resize = trans_resize(img)
# img_resize PIL -> totensor -> img_resize tensor
img_resize = trans_totensor(img_resize)
writer.add_image("Resize", img_resize, 0)
print(img_resize)

# compose - resize - 2
trans_resize_2 = transforms.Resize(512)
# PIL -> PIL -> tensor
trans_compose = transforms.Compose([trans_resize_2, trans_totensor])
img_resize_2 = trans_compose(img)
writer.add_image("Compose", img_resize_2, 1)

# RandomCrop随机裁剪
trans_random = transforms.RandomCrop(512)  #裁剪图像尺寸为512×512
trans_compose_2 = transforms.Compose([trans_random, trans_totensor])
for i in range(10):   #对原始图像随机裁剪0-9一共10个图像
    img_crop = trans_compose_2(img)
    writer.add_image("RandomCrop", img_crop, i)

writer.close()
  • Compose的使用
    Compose类的主要作用是串联多个图片变换的操作

  • RandomCrop的使用
    RandomCrop类的主要作用是实现对图片随机裁剪,裁剪的尺寸根据给定的参数进行,如果给定的是1个参数,则裁剪成正方形,如果是两个参数,则裁剪成两个参数指定的长和宽。

1.4.1.1.2 补充2 Torchvision基本知识

torchvision是pytorch的一个图形库,它服务于PyTorch深度学习框架的,主要用来构建计算机视觉模型。torchvision.transforms主要是用于常见的一些图形变换。以下是torchvision的基本构成:
1)torchvision.datasets: 一些加载数据的函数及常用的数据集接口;
2)torchvision.models:包含常用的模型结构(含预训练模型),例如AlexNet、VGG、ResNet等;
3)torchvision.transforms:常用的图片变换,例如裁剪、旋转等;
4)torchvision.utils: 其他的一些有用的方法。
如何查看这些数据集?现在介绍的是通过官网的方法查看。官网链接为:PyTorch官网Docs->torchvision
在pytrch中包含了很多模块,方便处理各种类型的数据,包括:torchtext(处理自然语言的),torchaudio(处理音频的),torchvision(以及处理图像视频的)。
torchvision的链接如下:torchvision链接,在该页面中能看到torchvision模块中包含的数据集。

1.4.1.1.2 补充3 ToPILImage()基础知识
  • ToPILImage基础
    “toPILImage”函数,它是PyTorch中的一个数据转换函数,用于将张量(Tensor)对象转换为PIL图像对象。PIL(Python Imaging Library)是一个Python图像处理库,可以进行图像的读取、处理和保存。
# 要使用“toPILImage”函数,首先需要导入相应的库:
import torch
from torchvision.transforms import ToPILImage
# 在导入库后,我们可以创建一个张量对象:
# 创建一个随机的3通道图片张量
tensor = torch.randn(3, 224, 224)
# 现在,我们可以使用“toPILImage”函数将张量转换为PIL图像对象:
to_pil = ToPILImage()
image = to_pil(tensor)
# 有了PIL图像对象,我们可以将其保存成图像文件。下面是一个示例:
# 将PIL图像保存为JPEG文件
image.save("image.jpg")
# 这样就将张量保存成了名为”image.jpg”的JPEG格式文件。

# 通过使用“toPILImage”函数,我们可以将张量对象转换为PIL图像对象,从而方便地显示图像。下面是一个示例:
import matplotlib.pyplot as plt
# 将PIL图像转换为NumPy数组
img_array = np.array(image)
# 显示图像
plt.imshow(img_array)
plt.axis('off')
plt.show()
# 这样就可以显示出由张量生成的图像。

# 输入的张量应该是三维张量(通道,高度,宽度);
# 张量中的数值范围应该是[0, 1]之间,如果数值范围超出该范围,图像可能无法正确显示;
# 默认情况下,toPILImage会将张量的数值范围从[0, 1]转换为[0, 255]。如果需要不同的范围,可以通过参数指定。
# 先导torchvision包
from PIL import Image
from torchvision.transforms import ToTensor,ToPILImage
# 定义转换操作
img_to_tensor = ToTensor() # img -> tensor
tensor_to_pil = ToPILImage() # tensor -> img
# 读取图片
img = Image.open('../test.jpg') # ‘’ 引号内为要读取图片的相对路径
# 把读取的图片转换成tensor进而对其操作,
# unsqueeze(0)是在给转换后的tensor加一个维度
input = img_to_tensor(img).unsqueeze(0) #torch.Size([1, 3, 960, 720])
# 对图像进行一个简单的操作,此处用的3*3的kernel进行锐化卷积
kernel = t.ones(3,3)/-9.
kernel[1][1] = 1
conv = nn.Conv2d(1,1,(3,3),1,bias=False) #卷积
conv.weight.data = kernel.view(1,1,3,3) #权重
# 将图片传入卷积层,并输出
out = conv(V(input)) 
tensor_to_pil(out.data.squeeze(0)).show()
# 注意,此处若不用.show()则输出台无显示。
# 另附torchvision.transforms.ToTensor及torchvision.transforms.ToPILImage的转换过程
# 

1.4.1.2 输出测试集数据

Dataloader是一个可迭代的对象,它将dataset返回的每一条数据拼接成一个batch,并提供多线程加速优化和数据打乱等操作。当程序对dataset的所有数据遍历完一遍之后,相应的对Dataloader也完成了一次迭代。

  • Python基础知识
    python中的两个概念:可迭代对象,迭代器:
    可迭代对象:实现了__iter__方法,该方法返回一个迭代器对象。
    迭代器:
    a)一个带状态的对象,内部持有一个状态,该状态用于记录当前迭代所在的位置,以方便下次迭代的时候获取正确的元素。
    b)迭代器含有__iter__和__next__方法。当调用__iter__返回迭代器自身,当调用next()方法的时候,返回容器中的下一个值。
    c)迭代器就像一个懒加载的工厂,等到有人需要的时候才给它生成值返回,没调用的时候就处于休眠状态等待下一次调用。

此处务必再读看Pytorch(三):Dataset和Dataloader的理解

  • DataLoader基础知识
    torch.utils.data.Dataset是代表这一数据的抽象类(也就是基类)。我们可以通过继承和重写这个抽象类实现自己的数据类,只需要定义__len__和__getitem__这个两个函数。
        DataLoader是Pytorch中用来处理模型输入数据的一个工具类。组合了数据集(dataset) + 采样器(sampler),并在数据集上提供单线程或多线程(num_workers )的可迭代对象。在DataLoader中有多个参数,这些参数中重要的几个参数的含义说明如下:
  1. epoch:所有的训练样本输入到模型中称为一个epoch;
  2. iteration:一批样本输入到模型中,成为一个Iteration;
  3. batchszie:批大小,决定一个epoch有多少个Iteration;
  4. 迭代次数(iteration)=样本总数(epoch)/批尺寸(batchszie)
  5. dataset (Dataset) – 决定数据从哪读取或者从何读取;
  6. batch_size (python:int, optional) – 批尺寸(每次训练样本个数,默认为1)
  7. shuffle (bool, optional) –每一个 epoch是否为乱序 (default: False);
  8. num_workers (python:int, optional) – 是否多进程读取数据(默认为0);
  9. drop_last (bool, optional) – 当样本数不能被batchsize整除时,最后一批数据是否舍弃(default: False)
  10. pin_memory(bool, optional) - 如果为True会将数据放置到GPU上去(默认为false)

由于tensorvision中自带了很多数据集,对于练手和神经网络的训练都十分有利,因此需要用Dataset和DataLoader来帮助我们学习。

 shuffle=True对数据读取的作用,每个epoch读完数据之后,在下次读取数据时是否会将数据打乱顺序,如果设置shuffle=True,那么在下一次epoch时,会将数据打乱顺序,然后再进行下一次读取,从而两次epoch读到的数据顺序是不同的;如果设置shuffle=False,那么在下一次数据读取时,不会打乱数据的顺序,从而两次读取到的数据顺序是相同的。

trainloader = t.utils.data.DataLoader(
trainset,
batch_size=4,
shuffle=True,
num_workers=0)
在num_workers设置为0的时候,可以对数据进行读取,batch_size=4表示一次性读取数据集中的4张图片,并且集合在一起进行返回
for data in test_loader:
imgs, targets = data
print(imgs.shape)
print(targets)
‘’‘
torch.Size([4, 3, 32, 32]) #4表示一次取出4张图片,后面三个参数表示每张图片信息,3通道,尺寸32x32
tensor([0, 2, 9, 0]) #表示一次性取出的4张图片target信息
’‘’

from torch.utils.tensorboard import SummaryWriter
在定义test_loader时,设置了batch_size=4,表示一次性从数据集中取出4个数据
writer = SummaryWriter(“logs”)
for epoch in range(2):
step = 0
for data in test_loader:
imgs, targets = data
writer.add_images(“Epoch: {}”.format(epoch), imgs, step)
step = step + 1
writer.close()

  • 代码
    此处在在num_workers设置不为0的时候运行失败,多线程方式获取数据,暂不知如何修改。
dataiter = iter(trainloader)
images, labels = dataiter.next() # 返回4张图片及标签
print(' '.join('%11s'%classes[labels[j]] for j in range(4)))
show(tv.utils.make_grid((images+1)/2)).resize((400,100))

//未完,待后续

1.4.1.3 定义网络
1.4.1.3.1 nn.Module简介

torcn.nn是专门为神经网络设计的模块化接口. nn构建于autograd之上,可以用来定义和运行神经网络。
    nn.Module是nn中十分重要的类,包含网络各层的定义及forward方法,在用户自定义神经网络时,需要继承自nn.Module类。
    在PyTorch官网中有关于torch.nn的详细情况介绍:torch.nn模块介绍

import torch.nn as nn
import torch.nn.functional as F

class Model(nn.Module):  # Model类继承自nn.Module
    def __init__(self):   # 构造函数
        super(Model, self).__init__()  # 在构造函数中要调用Module的构造函数
        self.conv1 = nn.Conv2d(1, 20, 5)  # 卷积操作
        self.conv2 = nn.Conv2d(20, 20, 5) # 卷积操作

    def forward(self, x): # 前向传播函数
        x = F.relu(self.conv1(x))  # 先卷积操作conv1,后非线性操作relu
        return F.relu(self.conv2(x))
        
# 现在尝试搭建自己的神经网络,创建一个名字为Test的类
import torch
from torch import nn

class Test(nn.Module):
    def __init__(self):
        super().__init__()

    def forward(self, input):
        output = input + 1
        return output

test = Test()
x = torch.tensor(1.0)
output = test(x)
print(output)

1.4.1.3.2 神经网络
# 仿LeNet网络
import torch.nn as nn
import torch.nn.functional as F

class Net(nn.Module):
    def __init__(self):
        # nn.Module子类函数必须在构造函数中执行父类的构造函数
        # 下式等价于nn.Module.__init__()
        super(Net, self).__init__()
        # 卷积层‘1’表示输入图片为单通道,‘6’表示输出通道数,‘5’表示卷积核为5x5
        self.conv1 = nn.Conv2d(1, 6, 5)
        # 卷积层
        self.conv2 = nn.Conv2d(6, 16, 5)
        # 放射层/全连接层,y=wx+b
        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 = F.max_pool2d(F.relu(self.conv1(x)), (2, 2))
        x = F.max_pool2d(F.relu(self.conv2(x)), 2)
        # reshape '-1'表示自适应
        x = x.view(x.size()[0], -1)
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x
net = Net()
print(net)

只要在nn.Module的子类中定义了forward函数,backward函数就会被自动实现(利用Autograd)。在forward函数中可使用任何Variable支持的函数,还可以使用Python等语法。
网络的可学习参数通过net.parameters()返回,net.named_parameters可同时返回可学习的参数及名称。

# 网络的可学习参数通过net.parameters()返回,net.named_parameters可同时返回可学习的参数及名称。
params = list(net.parameters())
print(len(params))

for name,parameters in net.named_parameters():
    print(name,':',parameters.size())

'''
print:
10
conv1.weight : torch.Size([6, 1, 5, 5])
conv1.bias : torch.Size([6])
conv2.weight : torch.Size([16, 6, 5, 5])
conv2.bias : torch.Size([16])
fc1.weight : torch.Size([120, 400])
fc1.bias : torch.Size([120])
fc2.weight : torch.Size([84, 120])
fc2.bias : torch.Size([84])
fc3.weight : torch.Size([10, 84])
fc3.bias : torch.Size([10])
'''

forward函数的输入和输出都是Variable,只有Variable才具有自动求导功能,Tensor是没有的,所以在输入时,需要把Tensor封装成Variable。

from torch.autograd import Variable
input = Variable(t.randn(1,1,32,32))
out = net(input)
print(out.size())

net.zero_grad()# 所有参数的梯度清零
out.backward(Variable(t.ones(1,10)))# 反向传播

需要注意的是,torch.nn只支持mini-batches,不支持一次只输入一个样本,即一次必须是一个batch。如果只想输入一个样本,则用input.unsqueeze(0)将batch_size设为1。例如,nn.Conv2d输入必须是4维的,形如nSamples x nChannels x Height x Width。可将nSample设为1,即1 x nChannels x Height x Width。

1.4.1.3.3 定义损失函数和优化器(loss和optimizer)

在反向传播计算完所有参数的梯度后,还需要使用优化方法更新网络的权重和参数。例如,随机梯度下降算法(SGD)的更新策略如下:

weight = weight - learning_rate * gradient
手动实现如下:
learning_rate = 0.01
for f in net.parameters():
f.data.sub_(f.grad.data * learning_rate) # inplace减法
print(f.data)

import torch.optim as optim
# 新建一个优化器,指定要调整的参数和学习率
optimizer = optim.SGD(net.parameters(),lr=0.01)
# 在训练过程中
# 先梯度清零(与net.zero_grad()效果一样)
optimizer.zero_grad()
# 计算损失
output = net(input)
loss = criterion(output,target)
# 反向传播
loss.backward()
# 更新参数
optimizer.step()

定义损失函数和优化器(loss和optimizer)

from torch import optim
criterion = nn.CrossEntropyLoss() # 交叉熵损失函数
optimizer = optim.SGD(net.parameters(),lr=0.001,momentum=0.9)

1.4.1.3.4 训练网络

所有网络的训练流程都是类似的,不断地执行如下流程:

  • 输入数据
  • 前向传播+反向传播
  • 更新参数
# 训练网络
from torch.autograd import Variable
for epoch in range(2):
    running_loss = 0.0
    for i,data in enumerate(trainloader,0):
        # 输入数据
        inputs,labels = data
        inputs,labels = Variable(inputs),Variable(labels)
        # 梯度清零
        optimizer.zero_grad()
        # forward+backward
        outputs = net(inputs)
        loss = criterion(outputs,labels)
        loss.backward()
        # 更新参数
        optimizer.step()

        # 打印log信息
        running_loss += loss.data
        if i % 2000 == 1999: # 每2000个batch打印一次训练状态
            print('[%d,%5d] loss: %.3f'\
                  %(epoch+1,i+1,running_loss/2000))
            running_loss = 0.0
print('Finished Training')

此处仅训练了2个epoch(遍历完一遍数据集称为一个epoch),我们来看看网络有没有效果,将测试图片输入网络,计算它的label,然后与实际的label进行比较。

1.4.1.3.5 验证网络效果
  • 批图像显示问题
    一般来说,需要将tensor转变为numpy类型的数组从而保存图片,这样的过程比较繁琐,Pytorch(torchvision.utils包)提供了save_image()函数,可直接将tensor保存为图片,其中如果tensor由很多小图片组成,则会自动调用make_grid()函数将小图片拼接为大图片再保存。
    save_image()函数,若图像tensor在cuda上也会移到CPU中进行保存。
    其实save_image()函数从第三个参数开始为函数make_grid()的参数。
    根据官方文档的描述,make_grid()函数主要用于生成雪碧图,何为雪碧图(sprite image),即由很多张小图片组成的一张大图。
    (如果使用torchvision.utils.save_image是没有必要写torch.utils.make_grid的,torchvision.utils.save_image内部会进行make_grid操作)
    其实是用torchvision.utils.make_grid将多个图片拼在一起,Pycharm中显示借助

img_grid = tv.utils.make_grid(dataiter_imgs
tv.utils.save_image(img_grid,“./batch.bmp”)

但是显示得借助

import matplotlib.pyplot as plt

TODO: 如何显示还要研究

  • 训练和网络结果输出
# 仿LeNet网络                                                                                         
import torch.nn as nn                                                                              
import torch.nn.functional as F                                                                    
                                                                                                   
class Net(nn.Module):                                                                              
    def __init__(self):                                                                            
        # nn.Module子类函数必须在构造函数中执行父类的构造函数                                                           
        # 下式等价于nn.Module.__init__()                                                                
        super(Net, self).__init__()                                                                
        # 卷积层‘1’表示输入图片为单通道,‘6’表示输出通道数,‘5’表示卷积核为5x5                                                 
        self.conv1 = nn.Conv2d(3, 6, 5)                                                            
        # 卷积层                                                                                      
        self.conv2 = nn.Conv2d(6, 16, 5)                                                           
        # 放射层/全连接层,y=wx+b                                                                          
        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 = F.max_pool2d(F.relu(self.conv1(x)), (2, 2))                                            
        x = F.max_pool2d(F.relu(self.conv2(x)), 2)                                                 
        # reshape '-1'表示自适应                                                                        
        x = x.view(x.size()[0], -1)                                                                
        x = F.relu(self.fc1(x))                                                                    
        x = F.relu(self.fc2(x))                                                                    
        x = self.fc3(x)                                                                            
        return x                                                                                   
                                                                                                   
net = Net()                                                                                        
print(net)                                                                                         

from torch import optim
criterion = nn.CrossEntropyLoss() # 交叉熵损失函数
optimizer = optim.SGD(net.parameters(),lr=0.001,momentum=0.9)


# 训练网络
from torch.autograd import Variable
for epoch in range(2):
    running_loss = 0.0
    for i,data in enumerate(trainloader,0):
        # 输入数据
        inputs,labels = data
        inputs,labels = Variable(inputs),Variable(labels)
        # 梯度清零
        optimizer.zero_grad()
        # forward+backward
        outputs = net(inputs)
        loss = criterion(outputs,labels)
        loss.backward()
        # 更新参数
        optimizer.step()

        # 打印log信息
        running_loss += loss.data
        if i % 2000 == 1999: # 每2000个batch打印一次训练状态
            print('[%d,%5d] loss: %.3f'\
                  %(epoch+1,i+1,running_loss/2000))
            running_loss = 0.0
print('Finished Training')
                                                                                                   
#输出统计数据
correct = 0 # 预测正确的图片数
total = 0 # 总共的图片数
for dataiter in testloader: #一个batch返回4张图片
    dataiter_imgs, dataiter_targets = dataiter
    print('实际的label:', ' '.join( \
        '%08s' % classes[dataiter_targets[j]] for j in range(4)))
    #dataiter_imgs = [ToPILImage((dataiter_imgs[j]/ 2 - 0.5).resize(400, 100)) for j in range(4)]
    #dataiter_imgs[0].show()
    #print(dataiter_imgs[0].size())
    img_grid = tv.utils.make_grid(dataiter_imgs)
    #tv.utils.save_image(img_grid,"./batch.bmp")

    # 接着计算网络预测的label
    outputs = net(dataiter_imgs)
    # 得分最高的那个类
    _,predicted = t.max(outputs.data,1)
    total += dataiter_targets.size(0)
    correct += (predicted==dataiter_targets).sum()
    print('预测结果:',' '.join('%5s'%classes[predicted[j]] for j in range(4)))
print('10000张测试集中的准确率为:%d %%' %(100*correct/total))

1.4.1.3.6 在GPU上训练

就像之前把Tensor从CPU转到GPU一样,模型也可以类似地从CPU转到GPU。

if t.cuda.is_available():
net.cuda()
images = images.cuda()
labels = labels.cuda()
output = net(Variable(images))
loss = criterion(output,Variable(labels))

完整代码:

import torch as t
import torchvision as tv
import torchvision.transforms as transforms
from torchvision.transforms import ToPILImage

#定义对数据的预处理
transform = transforms.Compose([transforms.ToTensor(),#转为Tensor
                               transforms.Normalize((0.5,0.5,0.5),(0.5,0.5,0.5)), #归一化
                               ])

#torchvision格式的数据集的范围为[0,1],我们这里使用transforms方法将它归一化为[-1,1]的张量。

#训练集
trainset = tv.datasets.CIFAR10(
                    root='./data',
                    train=True,
                    download=False,
                    transform=transform)
trainloader = t.utils.data.DataLoader(
                    trainset,
                    batch_size=4,
                    shuffle=True,
                    num_workers=2)

# 测试集
testset = tv.datasets.CIFAR10(
                    './data',
                    train=False,
                    download=False,
                    transform=transform)
testloader = t.utils.data.DataLoader(
                    testset,
                    batch_size=4,
                    shuffle=False,
                    num_workers=0)

classes = ('plane', 'car', 'bird', 'cat',
           'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
#Dataset对象是一个数据集,可以按下标访问,返回形如(data, label)的数据

# 仿LeNet网络
import torch.nn as nn
import torch.nn.functional as F

class Net(nn.Module):
    def __init__(self):
        # nn.Module子类函数必须在构造函数中执行父类的构造函数
        # 下式等价于nn.Module.__init__()
        super(Net, self).__init__()
        # 卷积层‘1’表示输入图片为单通道,‘6’表示输出通道数,‘5’表示卷积核为5x5
        self.conv1 = nn.Conv2d(3, 6, 5)
        # 卷积层
        self.conv2 = nn.Conv2d(6, 16, 5)
        # 放射层/全连接层,y=wx+b
        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 = F.max_pool2d(F.relu(self.conv1(x)), (2, 2))
        x = F.max_pool2d(F.relu(self.conv2(x)), 2)
        # reshape '-1'表示自适应
        x = x.view(x.size()[0], -1)
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x

net = Net()
print(net)

if t.cuda.is_available():
    net.cuda()

from torch import optim
criterion = nn.CrossEntropyLoss() # 交叉熵损失函数
optimizer = optim.SGD(net.parameters(),lr=0.001,momentum=0.9)

# 训练网络
from torch.autograd import Variable
for epoch in range(2):
    running_loss = 0.0
    for i,data in enumerate(trainloader,0):
        # 输入数据
        inputs,labels = data
        inputs,labels = Variable(inputs),Variable(labels)

        if t.cuda.is_available():
            inputs = inputs.cuda()
            labels = labels.cuda()

        # 梯度清零
        optimizer.zero_grad()
        # forward+backward
        outputs = net(inputs)
        loss = criterion(outputs,labels)
        loss.backward()
        # 更新参数
        optimizer.step()

        # 打印log信息
        running_loss += loss.data
        if i % 2000 == 1999: # 每2000个batch打印一次训练状态
            print('[%d,%5d] loss: %.3f'\
                  %(epoch+1,i+1,running_loss/2000))
            running_loss = 0.0
print('Finished Training')

#输出统计数据
correct = 0 # 预测正确的图片数
total = 0 # 总共的图片数
for dataiter in testloader: #一个batch返回4张图片
    dataiter_imgs, dataiter_targets = dataiter
    print('实际的label:', ' '.join( \
        '%08s' % classes[dataiter_targets[j]] for j in range(4)))
    #dataiter_imgs = [ToPILImage((dataiter_imgs[j]/ 2 - 0.5).resize(400, 100)) for j in range(4)]
    #dataiter_imgs[0].show()
    #print(dataiter_imgs[0].size())
    img_grid = tv.utils.make_grid(dataiter_imgs)
    #tv.utils.save_image(img_grid,"./batch.bmp")

    dataiter_imgs = dataiter_imgs.cuda()
    dataiter_targets = dataiter_targets.cuda()
    # 接着计算网络预测的label
    outputs = net(dataiter_imgs)
    # 得分最高的那个类
    _,predicted = t.max(outputs.data,1)
    total += dataiter_targets.size(0)
    correct += (predicted==dataiter_targets).sum()
    print('预测结果:',' '.join('%5s'%classes[predicted[j]] for j in range(4)))
print('10000张测试集中的准确率为:%d %%' %(100*correct/total))

如果发现在GPU上训练的速度并没有比在CPU上提速很多,实际是因为网络比较小,GPU没有完全发挥自己的真正实力。

对Pyorch的基础介绍至此。总结本节内容:
1)Tensor:类似numpy数组的数据结构,与numpy接口类似,可方便地互相转换
2)autograd/Variable:Variable封装了Tensor,并提供自动求导功能
3)nn:专门为神经网络设计的接口,提供了很多有用的功能(神经网络层、损失函数、优化器等)
4)神经网络训练:以CIFAR10分类为例演示了神经网络的训练流程,包括数据加载、网络搭建、训练及测试

配置环境:PyTorch+Jupyter+IPython

2 Tensor和autograd

几乎所有的深度学习框架背后设计的核心都是张量和计算图,PyTorch也不例外。

2.1 Tensor

张量不仅在PyTorch中出现,也是Theano、TensorFlow、Torch和MXNet中重要的数据结构。张量的本质不乏深度剖析的文章,但从工程角度讲可简单地认为它就是一个支持高效科学计算的数组。Tensor和numpy的ndarrays类似,但PyTorch的tensor支持GPU加速。
本节将系统讲解tensor的使用,但不会具体涉及每个函数,可通过在IPython/Notebook中使用?查看帮助文档,或查阅PyTorch的官方文档。

from future import print_function
import torch as t

2.1.1 基础操作

tensor的接口设计得与numpy类似,从接口角度讲,对tensor的操作可分为两类,对tensor的大部分操作同时支持这两类接口,功能等价,本书不作具体区分:
(1)torch.function,如torch.save等,torch.sum(a,b)
(2)tensor.function,如tensor.view等,a.sum(b)
从存储角度讲,对tensor的操作可分为两类:
(1)不会修改自身数据,如a.add(b),加法结果会返回一个新的tensor
(2)会修改自身数据,如a.add_(b),加法结果会修改存储在a中
函数名以_结尾的都是inplace方式,即会修改调用自己的数据,在实际应用中需加以区分。

2.1.1.1 创建Tensor

常见的新建tensor的方法

函数功能
Tensor(*sizes)基础构造函数
ones(*sizes)全1Tensor
zeros(*sizes)全0Tensor
eye(*sizes)对角线为1,其他为0
arange(s,e,step)从s到e,步长为step
linspace(s,e,steps)从s到e,均匀切分成steps份
rand/randn(*sizes)均匀/标准分布
normal(mean,std)/uniform(from,to)正态分布/均匀分布
randperm(m)随机排列
  • 其中使用Tensor函数新建tensor是最复杂多变的方式,它既可以接收一个list,并根据list数据新建tensor,也能根据指定的形状新建tensor,还能传入其他的tensor,如:

a = t.Tensor(2,3) # a 数值取决于内存空间的状态
b = t.Tensor([[1,2,3],[4,5,6]])
b.tolist() # 把tensor转为list
b.size()
b.numel() # b中元素总个数,2*3,等价于 b.nelement()

  • tensor.size()返回torch.Size对象,它是tuple的子类,但其使用方式与tuple略有区别。
    除了tensor.size(),还可以利用tensor.shape直接查看tensor的形状,tensor.shape等价于tensor.size()。
  • 需要注意的是,t.Tensor(*sizes)创建tensor时,系统不会马上分配空间,只会计算剩余的内存是否足够使用,使用到tensor时才会分配,而其他操作都是在创建完tensor后马上进行空间分配。
2.1.1.2 常见Tensor操作
  • tensor.view方法可以调整tensor的形状,但必须保证调整前后元素总数一致。view不会修改自身数据,返回的新tensor与源tensor共享内存,即更改其中一个,另外一个也会跟随改变。
  • 在实际应用中,可能需要添加或减少某一维度,这时squeeze和unsqueeze两个函数就派上了用场。

a=t.arange(0,6)
a.view(2,3)
b=a.view(-1,3) # 当某一维为-1时,会自动计算它的大小
b.unsqueeze(1) # 注意形状,在第1维(下标从0开始)
b.unsqueeze(-2) # -2表示倒数第二个维度
c=b.view(1,1,1,2,3)
c.squeeze(0) # 压缩第0维的”1“ ?

  • resize是另外一种可以调整size的方法,但与view不同,它可以修改rensor的尺寸。如果新尺寸超过了原尺寸,会自动分配新的内存空间,而如果新尺寸小于原尺寸,则之前的数据依旧会被保存。
2.1.1.3 索引操作
  • Tensor支持与numpy.ndarray类似的索引操作,语法也类似。如无特殊说明,索引出来的结果与源tensor共享内存,即修改一个另一个也会跟着修改。

a[0] # 第0行(下标从0开始)
a[:,0] # 第0列
a[0,-1] # 第0行最后一个元素
a[:2] # 前两行
a[:2,0:2] # 前两行,第0,1列
a[0:1,:2] # 第0行,前两列
a[0,:2] # 第0行,前两列,注意两者的区别:形状不同
a>1 #返回一个ByteTensor
a[a>1] # 等价于a.masked_select(a>1),选择结果与原tensor不共享内存空间
a[t.LongTensor([0,1])] # 第0行和第1行

  • 其他常用的选择函数
函数功能
index_select(input,dim,index)在指定维度dim上选取,例如选取某些行、某些列
masked_select(input,mask)a[a>0],使用ByteTensor进行选取
non_zero(input)非0元素的下标
gather(input,dim,index)根据index,在dim维度上选取数据,输出的size与index一样

//待后续

2.1.1.4 高级索引

高级索引可看成是普通索引操作的扩展,但是高级索引操作的结果一般不和原始的Tensor共享内存。

2.1.1.5 Tensor类型

Tensor有不同的数据类型,如下表,每种类型分别对应有CPU和GPU版本(HalfTensor除外)。默认的tensor是FloatTensor,可通过t.set_default_tensor_type修改默认tensor类型(如果默认类型为GPU tensor,则操作都在GPU上进行)。
Tensor的类型对分析内存占用很有帮助,例如,一个size为(1000,1000,1000)的Float-Tensor,它有1000x1000x1000=10^9个元素,每个元素占32bit/8=4Byte内存,所以共占大约4GB内存/显存。HalfTensor是专门为GPU版本设计,同样的元素个数,显存占用只有FloatTensor的一半,所以可以极大地缓解GPU显存不足问题,但由于HalfTensor所能表示的数值大小和精度有限,所以可能出现溢出等问题。

表3-3 tensor数据类型

数据类型CPU tensorGPU tensor
32bit浮点torch.FloatTensortorch.cuda.FloatTensor
64bit浮点torch.DoubleTensortorch.cuda.DoubleTensor
16bit半精度浮点N/Atorch.cuda.HalfTensor
8bit无符号整型(0~255)torch.ByteTensortorch.cuda.ByteTensor
8bit有符号整型(-128~127)torch.CharTensortorch.cuda.CharTensor
16bit有符号整型torch.ShortTensortorch.cuda.ShortTensor
32bit有符号整型torch.IntTensortorch.cuda.IntTensor
64bit有符号整型torch.LongTensortorch.cuda.LongTensor

各数据类型间可互相转换,type(new_type)是通用做法,同时还有float、long、half等快捷方法。
CPU tensor与GPU tensor之间的互相转换通过tensor.cuda和tensor.cup的方法实现。Tensor还有一个new方法,用法与t.Tensor一样,会调用该tensor对应类型的构造函数,生成与当前tensor类型一致的tensor。

t.set_default_tensor_type(‘torch.IntTensor’) # 设置默认tensor,注意参数是字符串
a=t.Tensor(2,3) # a现在是IntTensor
b=a.float() # 把a转成FloatTensor,等价于b=a.type(t.FloatTensor)
c=a.type_as(b)
d=a.new(2,3) # 等价于torch.IntTensor(3,4)
a.new?? # 查看函数new的源码
t.set_default_tensor_type(‘torch.FloatTensor’) # 恢复之前的默认设置

2.1.1.6 逐元素操作

逐元素操作会对tensor的每一个元素(point-wise,又名element-wise)进行操作,输入输出形状一致,常用操作如下表所示。

函数功能
abs/sqrt/div/exp/fmod/log/pow…绝对值/平方根/除法/指数/求余/求幂
cos/sin/asin/atan2/cosh三角函数
ceil/round/floor/trunc上取整/四舍五入/下取整/只保留整数部分
clamp(input,min,max)超过min和max部分截断
sigmod/tanh…激活函数

对于很多操作,如div、mul、pow、fmod等,PyTorch都实现了运算符重载,所以可以直接使用运算符。如,a**2 等价于torch.pow(a,2),a*2等价于torch.mul(a,2)。
其中clamp(x,min,max)的输出满足以下公式:
y i = { m i n , i f   x i < m i n x i , i f   m i n ≤ x i ≤ m a x m a x , i f   x i > m a x y_{i} = \left\{\begin{matrix} min,\mathrm{if} \ x_i < \mathrm{min} \\x_i,\mathrm{if} \ \mathrm{min} \le x_i \le \mathrm{max} \\max,\mathrm{if} \ x_i > \mathrm{max}\end{matrix}\right. yi= min,if xi<minxi,if minximaxmax,if xi>max

clamp常用在某些需要比较大小的地方,如取一个tensor的每个元素与另一个数的较大值

a =[0,1,2;3,4,5]
t.clamp(a,min=3) #[3,3,3;3,4,5]

2.1.1.7 归并操作

此类操作会使输出形状小于输入形状,并可以沿着某一维度进行指定操作。如加法sum,既可以计算整个tensor的和,也可以计算tensor中每一行或每一列的和。常用归并操作如下表。

函数功能
mean/sum/median/mode均值/和/中位数/众数
norm/dist范数/距离
std/var标准差/方差
cumsum/cumprod累加/累乘

以上大多数函数都有一个参数dim,用来指定这些操作是在哪个维度上执行的。关于dim(对应于Numpy中的axis)的解释众说纷纭,这里提供一个简单的记忆方式。

假设输入的形状是(m,n,k):

  • 如果指定dim=0,输出的形状就是(1,n,k)或者(n,k)
  • 如果指定dim=1,输出的形状就是(m,1,k)或者(m,k)
  • 如果指定dim=2,输出的形状就是(m,n,1)或者(m,n)

size中是否有“1”,取决于参数keepdim,keepdim=True会保留维度1。从PyTorch0.2.0版本起,keepdim默认为False。注意以上只是经验总结,并非所有函数都符合这种变化方式,如cumsum。

2.1.1.8 比较

比较函数中有一些是逐元素比较,操作类似于逐元素操作,还有些则类似于归并操作,常用的比较函数如下表所示。

函数功能
gt/lt/ge/le/eq/ne大于/小于/大于等于/小于等于/等于/不等
topk最大的k个数
sort排序
max/min比较两个tensor的最大值和最小值

表中第一行的比较操作已经实现了运算符重载,因此可以使用a>=b、a>b、a!=b和a==b,其返回结果是一个ByteTensor,可以用来选取元素。max/min这两个操作比较特殊,以max为例,它有以下三种使用情况。

t.max(tensor)# 返回tensor中最大的一个数
t.max(tensor,dim)#指定维度上最大的数,返回tensor和下标
t.max(tensor1,tensor2)#比较两个tensor相比较大的元素
t.clamp(a,min=10)#比较一个tensor和一个数们可以使用clamp函数

2.1.1.9 线性代数

PyTorch的线性代数函数主要封装了Blas和Lapack,其用法和接口都类似,常用的线性代数函数如下表。

函数功能
trace对角线元素之和
diag对角线元素
triu/tril矩阵的上三角/下三角,可指定偏移量
mm/bmm矩阵乘法,batch的矩阵乘法
addmm/addbmm/addmv矩阵运算
t转置
dot/cross内积/外积
inverse求逆矩阵
svd奇异值分解
具体使用说明参见官方文档,需要注意的是,矩阵的转置会导致存储空间不连续,需调用它的.contiguous方法将其转为连续。

b.is_contiguous()
b.contiguous()

2.1.2 Tensor和Numpy

  • Tensor和Numpy数组间具有很高的相似性,彼此间的互操作也非常简单高效。需注意的是,Numpy和Tensor共享内存。由于Numpy历史悠久,支持丰富的操作,所以当遇到Tensor不支持的操作时,可先转成Numpy数组,处理后再转回tensor,其转换开销很小。
  • 广播法则(Broadcast)是科学运算中经常使用的一个技巧,它在快速执行向量化的同时不会占用额外的内存/显存。Numpy的广播法则定义如下:
    (1)让所有输入数组都向其中shape最长的数组看齐,shape中不足的部分通过在前面加1补齐;
    (2)两个数组要么在某一维度的长度一致,要么其中一个为1,否则不能计算;
    (3)当输入数组的某个维度的长度为1时,计算时沿此维度复制扩充成一样的形状。
  • PyTorch已支持自动广播法则,但笔者建议读者通过下面两个函数的组合手动实现广播法则,这样更直观,不易出错:
    (1)unsqueeze或view:为数据某一维的形状补1,实现法则1;
    (2)expand或expand_as:重复数组,实现法则3;该操作不会复制数组,所以不会占用额外的空间
    (3)注意:repeat实现与expand相类似的功能,但是repeat会把相同数据复制多份,因此会占用额外的空间。

a=t.ones(3,2)
b=t.zeros(2,3,1)
#自动广播法则:
第一步:a是二维,b是三维,所以先在较小的a前面补1,
即:a.unsqueeze(0),a的形状变成(1,3,2),b的形状是(2,3,1),
第二步:a和b在第一维和第三维的形状不一样,其中一个为1,
可以利用广播法则扩展,两个形状都变成了(2,3,2)

#手动广播法则
或者a.view(1,3,2).expand(2,3,2)+b.expand(2,3,2)
a.unsqueeze(0).expand(2,3,2)+b.expand(2,3,2)

#expand不会占用额外空间,只会在需要时才扩充,可极大地节省内存
e=a.unsqueeze(0).expand(100000000,3.2)

2.1.3 内部结构

tensor的数据结构如下图所示。tensor分为头信息区(Tensor)和存储区(Storage),信息区主要保存着tensor的形状(size)、步长(stride)、数据类型(type)等信息,而真正的数据则保存成连续数组。由于数据动辄成千上万,因此信息区元素占用内存较少,主要内存占用取决于tensor中元素的数目,即存储区的大小。
在这里插入图片描述
一般来说,一个tensor有着与之相对应的storage,storage是在data之上封装的接口,便于使用。不同tensor的头信息一般不同,但却可能使用相同的storage。

a.storage()
一个对象的id值可以看作它在内存中的地址
id(b.storage())==id(a.storage())
c.data_ptr() # data_ptr返回tensor首元素的内存地址,每个元素占4个字节(float)
a,storage_offset()
e=b[::2,::2] # 隔2行/列取一个元素

绝大多数操作并不修改tensor的数据,只是修改了tensor的头信息。这种做法更节省内存,同时提升处理速度。此外,还有些操作会导致tensor不连续,这时需要调用tensor.contiguous方法将它们变成连续的数据,该方法复制数据到新的内存,不再与原来的数据共享storage。
之前说的高级索引一般不共享storage,而普通索引共享storage。主要是因为普通索引可以通过修改tensor的offset、stride和size实现,不修改storage的数据,高级索引则不行。

2.1.4 其他Tensor话题

2.1.4.1 持久化

Tensor的保存和加载十分简单,使用t.save和t.load即可完成相应的功能。 在save/load时可指定使用的pickle模块,在load时还可将GPU tensor映射到CPU或其他GPU上。

if t.cuda.is_available():
a=a.cuda(1) # 把a转为GPU1上的tensor
t.save(a,‘a.pth’)
t.load(‘a.pth’,map_location=lambda storage,loc:storage) # 存储于CPU上

2.1.4.2 向量化
  • 向量化计算是一种特殊的并行计算方式,一般程序在同一时间只执行一个操作的方式,它可在同一时间执行多个操作,通常是对不同的数据执行同样的一个或一批指令,或者说把指令应用于一个数组/向量上。向量化可极大地提高科学运算的效率,Python本身是一门高级语言,使用很方便,但许多操作很低效,尤其是for循环。在科学计算程序中应当极力避免使用Python原生的for循环,尽量使用向量化的数值计算。在实际使用中应尽量调用内建函数(builtin-function),这些函数底层由C/C++实现,能通过执行底层优化实现高效计算。因此应尽量养成向量化思维的习惯。
  • 大多数t.function都有一个参数out,这时产生的结果将保存在out指定的tensor之中
  • t.set_num_threads可以设置PyTorch进行CPU多线程并行计算时所占用的线程数,用来限制PyTorch所占用的CPU数目
  • t.set_printoptions可以用来设置打印tensor时的数值精度和格式

2.1.5 实践:线性回归

2.1.5.1 函数理解补充
  • unsqueeze()函数
    起升维的作用,参数表示在哪个地方加一个维度
    在第一个维度(中括号)的每个元素加中括号
    0表示在张量最外层加一个中括号变成第一维

input=torch.arange(0,6)
print(input.shape)
out:tensor([0, 1, 2, 3, 4, 5]);torch.Size([6])
///
print(input.unsqueeze(0))
print(input.unsqueeze(0).shape)
out:tensor([[0, 1, 2, 3, 4, 5]])
torch.Size([1, 6])
///
print(input.unsqueeze(1))
print(input.unsqueeze(1).shape)
out:tensor([[0],
[1],
[2],
[3],
[4],
[5]])
torch.Size([6, 1])

  • squeeze
    (a) squeeze(1)和squeeze(-1)
    两者的效果一样,都是给张量tensor降维,但不是啥张量都可以用这两个函数来降维,它只能降维一种情况下张量的维度。就是我的张量tensor是一个n1维度的张量,例如:张量[[1], [2], [3]]是一个31维的

a=torch.tensor([[1],[2],[3]])
b=a.squeeze(1);c=a.squeeze(-1)
out:tensor([[1],
[2],
[3]])
tensor([1,2,3]);tensor([1,2,3])

但是如果不是n1的这种2维张量的话,如本就是1维的,或者mn(其中m和n都是大于1的)这种的话,调用这个函数一点效果没有。(具体而言,如果一个张量有四个维度的,squeeze(index)会将张量中第index维度,且大小为1的维度进行去除,从而减少张量的维度。如果index是负整数,那就是倒数第index个维度)

(b) squeeze(0)
当张量是一个1n维度的张量时,例如:张量[[1, 2, 3]]是一个13维的

a=torch.tensor([[1,2,3]])
b=a.squeeze(0)
out:tensor([[1,2,3]])
tensor([1,2,3])

如果一个张量有五个维度的,unsqueeze(index)会在该张量的第index维度上增加一个维度值为1的维度,例如维度是(3, 2, 1),index是2的话,就会解压成(3, 2, 1, 1)。如果index是负整数,那就是倒数第index个维度。

  • 自动广播法则

a=t.ones(3,2)
b=t.zeros(2,3,1)
第一步:a是二维,b是三维,所以先在较小的a前面补1
即:a.unsqueeze(0),a的形状变成(1,3,2),b的形状是(2,3,1)
第二步:a和b在第一维和第三维的形状不一样,其中一个为1
可以利用广播法则扩展,两个形状都变成了(2,3,2)
a+b
手动广播法则:
a.view(1,3,2).expand(2,3,2)+b.expand(2,3,2)
a.unsqueeze(0).expand(2,3,2)+b.expand(2,3,2)
expand不会占用额外空间,只会在需要时才扩充,可极大地节省内存
e=a.unqueeze(0).expand(100000000,3,2)

  • Matplotlib 绘图支持 tensor 数据类型
    Matplotlib 绘图是直接支持 tensor 数据类型的,这样的话以后在机器学习可视化中就无需对 tensor 数据进行 numpy 数据转换了。

import torch
import matplotlib.pyplot as plt
a = torch.linspace(0., 2. * torch.pi, steps=25, requires_grad=True)
b = torch.sin(a)
plt.figure(figsize=[8, 5])
plt.plot(a.detach(), b.detach())
plt.show()

  • x.mm()函数
    在前向计算中x.mm的作用是将参数和变量相乘,但是这里有两个要注意的地方:
    1是w和x相乘的时候的顺序是x*w
    2是w和x的类型,x必须是tensor,如果w需要训练的话则需要将其加入到Module中的parameter(参数迭代器)中,特别注意一点的是:在pytorch中tensor是不能进行训练的。而且input送入网络计算之前,需要将tensor变成变量。
2.1.5.2 线性回归

线性回归是机器学习的入门知识,应用十分广泛。线性回归利用数理统计中的回归分析来确定两种或两种以上变量间相互依赖的定量关系,其表达形式为y=wx+b+e,误差e服从均值为0的正态分布。线性回归的损失函数是: l o s s = ∑ i N 1 2 ( y i − ( w x i + b ) ) 2 loss=\sum_{i}^{N}\frac{1}{2}(y_i - (wx_i+b))^2 loss=iN21(yi(wxi+b))2
利用随机剃度下降法更新参数w和b来最小化损失函数,最终学得w和b的数值。

#第3章 Tensor和autograd
# 对tensor的操作可分为两类
# (1)torch.function,如torch,save等
# (2)tensor.function,如tensor.view等
# 例如:torch.sum(a,b)与a.sum(b)
import torch as t
import numpy as np
from sympy.physics.quantum.circuitplot import matplotlib
#matplotlib inline
from matplotlib import pyplot as plt

# 设置随机数种子,保证在不同计算机上运行时下面的输出一致
t.manual_seed(1000)

def get_fake_data(batch_size=8):
    '''产生随机数据,y=x*2+3,加上了一些噪声'''
    x=t.rand(batch_size,1)*20
    y=x*2+(1+t.randn(batch_size,1))*3
    return x,y

x,y=get_fake_data(111)
# 利用squeeze()函数将表示向量的数组转换为秩为1的数组,这样利用matplotlib库函数画图时,就可以正常的显示结果了
plt.scatter(x.detach(),y.detach())
plt.show()

# 随机初始化参数
w=t.rand(1,1)
b=t.zeros(1,1)
lr=0.001#学习率

for ii in range(20000):
    x,y=get_fake_data()

    #forward:计算loss
    y_pred=x.mm(w)+b.expand_as(y)
    #将张量扩展为参数大小
    #mm为矩阵乘法,expand_as为扩展数据,将b(1*1)扩展成y(8*1)的数据格式
    loss=0.5*(y_pred-y)**2 #均方误差
    loss=loss.sum()

    #手动计算梯度
    dloss = 1
    dy_pred = dloss * (y_pred - y)

    dw = x.t().mm(dy_pred)
    db = dy_pred.sum()
    # 更新参数
    w.sub_(lr * dw)
    b.sub_(lr * db)

    if ii % 1000 == 0:
        x = t.arange(0.0, 20.0).view(-1, 1)
        y = x.mm(w) + b.expand_as(x)
        plt.plot(x, y)

        x2, y2 = get_fake_data(batch_size=20)
        plt.scatter(x2, y2)

        plt.xlim(0, 20)
        plt.ylim(0, 40)
        plt.show()
        plt.pause(1)
        print(w[0], b[0])

2.2 autograd

用Tensor训练网络很方便,但从1.2.1.5线性回归实践来看,反向传播过程需要手动实现,这对线性回归这种较简单的模型来说还比较容易,但实际使用中经常出现非常复杂的网络结构,如果此时手动实现反向传播,不仅费时费力而且容易出错。torch.autograd就是为方便用户使用,专门开发的一套自动求导引擎,它能够根据输入和前向传播过程自动构建计算图,并执行方向传播。
计算图(Computation Graph)是现代深度学习框架(如PyTorch和TensorFlow等)的核心,它为自动求导算法——反向传播(Back Propogation)提供了理论支持,了解计算图在实际写程序过程中会有极大帮助。关于计算图的基础知识推荐阅读Christopher Olah(http://colah.github.io/posts/2015-08-Backprop/)的文章。

2.2.1 Variable

如1.1.2 Autograd自动微分部分介绍的Variable,此处继续补充。
PyTorch在autograd模块中实现了计算图的相关功能,autograd中的核心数据结构是Variable。Variable封装了tensor,并记录对tensor的操作记录用来构建计算图。

( a u t o g r a d . ) V a r i a b l e { d a t a g r a d g r a d _ f n (autograd.)Variable\left\{\begin{matrix} data \\grad \\grad\_fn\end{matrix}\right. (autograd.)Variable datagradgrad_fn

Variable主要包含三个属性:
(1)data,保存Variable所包含的Tensor
(2)grad,保存data对应的梯度,grad也是个Variable,而不是Tensor,它和data的形状一样
(3)grad_fn,指向一个Function对象,这个Function用来反向传播计算输入的梯度。用来记录variable的操作历史,即它是什么操作的输出,用来构建计算图。如果某一个变量是由用户创建的,则它为叶子节点,对应的grad_fn等于None。

Variable的构造函数需要传入tensor,同时有两个可选参数:
(1)requires_grad(bool):是否需要对该variable进行求导
(2)volatile(bool):“爆炸性的”,设置为True,构建在该variable之上的图都不会求导,转为推理阶段设计。

Variable支持大部分tensor支持的函数,但其不支持部分inplace函数,因为这些函数会修改tensor自身,而在反向传播中,variable需要缓存原来的tensor来计算梯度。如果想计算各个Variable的梯度,只需调用根节点variable的backward方法,autograd会自动沿着计算图反向传播,计算每一个叶子节点的梯度。

variable.backward(grad_variables=None,retain_graph=None,create_graph=None):
(1)grad_variables:形状与variable一致,对于y.backward(),grad_variables相当于链式法则 ∂ z ∂ x = ∂ z ∂ y × ∂ y ∂ x \frac{\partial z}{\partial x} = \frac{\partial z}{\partial y} \times \frac{\partial y}{\partial x} xz=yz×xy中的 ∂ z ∂ y \frac{\partial z}{\partial y} yz。grad_variables也可以是tensor或序列;
(2)retain_graph:反向传播需要缓存一些中间结果,反向传播之后,这些缓存就被清空,可通过指定这个参数不清空缓存,用来多次反向传播;
(3)create_graph:对反向传播过程再次构建计算图,可通过backward of backward实现求高阶导数;
例:

from future import print_function
import torch as t
from torch.autograd import Variable as V
a=V(t.ones(3,4),requires_grad=True)#从tensor中创建variable,指定需要求导
b=V(t.ones(3,4))
c=a.add(b)#函数的使用与tensor一致,也可写成c=a+b
d=c.sum()
d.backward()#反向传播
c.data.sum(),c.sum()#前者在取data后变为tensor,从tensor计算sum得到float;后者计算sum后仍然是Variable
a.grad
a.requires_grad,b.requires_grad,c.requires_grad #Out:True,False,True #此处虽然没有指定c需要求导,但c依赖于a,而a需要求导,因此c的requires_grad属性会自动设为True
a.is_leaf,b.is_leaf,c.is_leaf #Out:True,True,False #由用户创建的variable属于叶子节点,对应的grad_fn是None
c.grad is None #Out:True # c.grad是None,c不是叶子节点,它的梯度是用来计算a的梯度;虽然c.requires_grad=True,但其梯度计算完之后立即释放

接下来看看autograd计算的导数和我们手动推导的导数区别:
例: y = x 2 e x y=x^2e^x y=x2ex d y d x = 2 x e x + x 2 e x \frac{\mathrm{d} y}{\mathrm{d} x} = 2xe^x+x^2e^x dxdy=2xex+x2ex

def f(x):
‘’‘计算y’‘’
y=x**2*torch.exp(x)
return y
def gradf(x):
‘’‘手动求导函数’‘’
dx=2*x*t.exp(x)+x**2*t.exp(x)
return dx

x=V(t.randn(3,4),requires_grad=True)
y=f(x)
y.backward(t.ones(y.size())) # grad_variables形状与y一致
x.grad
gradf(x) # autograd的计算结果与利用公式手动计算的结果一致

2.2.2 计算图

PyTorch中autograd的底层采用了计算图,计算图是一种特殊的有向无环图(DAG),用于记录算子与变量之间的关系。一般用矩形表示算子,椭圆形表示变量。
在这里插入图片描述
如上有向无环图中,X和b是叶子节点(leaf node),这些节点通常由用户自己创建,不依赖于其他变量。z称为根节点,是计算图的最终目标。利用链式法则很容易求得各个叶子节点的梯度。而有了计算图,上述链式求导即可利用计算图的反向传播自动完成。
在PyTorch实现中,autograd会随着用户的操作,记录生产当前variable的所有操作,并由此建立一个有向无环图。用户每进行一个操作,相应的计算图就会发生改变。更底层的实现中,图中记录了操作Function,每一个变量在图中的位置可通过其grad_fn属性在图中的位置推测得到。在反向传播过程中,autograd沿着这个图从当前变量(根节点z)溯源,可以利用链式求导法则计算所有叶子节点的梯度。每一个前向传播操作的函数都有与之对应的反向传播函数用来计算输入的各个variable的梯度,这些函数的函数名通常以Backward结尾。

x=V(t.ones(1))
b=V(t.rand(1),requires_grad=True)
w=V(t.rand(a),requires_grad=True)
y=w*x #等价于y=w.mul(x)
z=y+b #等价于z=y.add(b)
x.requires_grad,b.requires_grad,w.requires_grad # Out:False,True,True
y.requires_grad # Out:True # 虽然未指定y.requires_grad为True,但由于y依赖于需要求导的w,故而y.requires_grad为True
x.is_leaf,w.is_leaf,b.is_leaf # Out:True,True,True
y.is_leaf,z.is_leaf # Out:False,False

z.grad_fn # Out:<torch.autograd.function.AddBackward at 0x7f6ca301b050>
#grad_fn可以查看这个variable的反向传播函数,z是add函数的输出,所以它的反向传播函数是AddBackward
z.grad_fn.next_functions # next_functions保存grad_fn的输入,grad_fn的输入是一个tuple:第一个是y,它是乘法(mul)的输出,所以对应的反向传播函数y.grad_fn是MulBackward;第二个是b,它是叶子节点,由用户创建,grad_fn为None,但是有输出显示?
#Out:((<torch.autograd.function.MulBackward at 0x7f6ca3032ed8>,0),(<AccumulateGrad at 0x7f6ca3007510>,0))
z.grad_fn.next_functions[0][0] == y.grad_fn # Out:True # variable的grad_fn对应着图中的function
y.grad_fn.next_functions # 第一个是w,叶子节点,需要求导,梯度是累加的;第二个是x,叶子节点,不需要求导,所以为None。next_function是指grad_fn函数的输入?
Out:((<AccumulateGrad at 0x7f6ca30076d0>,0),(<None>,0))
w.grad_fn,x.grad_fn #Out:None,None #叶子节点的grad_fn是None

计算w的梯度时需要用到x的数值( ∂ y ∂ w = x \frac{\partial y}{\partial w} = x wy=x),这些数值在前向传播过程中会保存成buffer,在计算完梯度之后会自动清空。为了能多次反向传播需要指定retain_graph来保留这些buffer。

y.grad_fn.saved_variables # Out:Variable containing:0.2473 [torch.FloatTensor of size 1],Variable containing: [torch.FloatTensor of size 1]
z.backward(retain_graph=True) # 使用retain_graph保存buffer
w.grad #Out:1
z.backward() # 多次反向传播,梯度累加,这也就是w中AccumulateGrad标识的含义
z.backward()
w.grad # Out:2
y.grad_fn.saved_variables # 会报错,此时保存的buffer已经被清空了

PyTorch使用的是动态图,它的计算图在每次前向传播时都是从头开始构建的,所以它能够使用Python控制语句(如for、if等),根据需求创建计算图。这一点在自然语言处理领域中很有用,它意味着你不需要事先构建所有可能用到的图的路径,图在运行时才构建。
变量的requires_grad属性默认为False,如果某一个节点requires_grad被设置为True,那么所有依赖它的节点requires_grad都是True。这其实很好理解,对于x->y->z,x.requires_grad=True。当需要计算z对x的偏导时,根据链式法则,自然也需要求z对y偏导,所以y.requires_grad会被自动标为True。
volatile=True是另外一个很重要的标识,它能够将所有依赖于它的节点全部设为volatile=True,其优先级比requires_grad=True高。volatile=True的节点不会求导,即使requires_grad=True,也无法进行反向传播。对于不需要反向传播的情景(如inference,即测试推理时),该参数可实现一定程度的速度提升,并节省约一半显存,因为其不需要分配空间保存梯度。

x=V(t.ones(1),volatile=True)
w=V(t.rand(1),requires_grad=True)
y=x*w # y依赖于w和x,但x.volatile=True,w.requires_grad=True
x.requires+grad,w.requires_grad,y.requires_grad #Out: False,True,False
x.volatile,w.volatile,y.volatile #Out: True,False,True

在反向传播过程中非叶子节点的导数计算完之后即被清空。若想查看这些变量的梯度,有以下两种方法:
(1)使用autograd.grad函数
(2)使用hook
autograd.grad和hook方法都是很强大的工具,更详细的用法参考官方api文档,这里只举例说明其基础的使用方法。推荐使用hook方法,但在实际使用中应尽量避免修改grad的值。

查阅PyTorch的官方文档之后,发现Variable已经被放弃使用了,因为tensor自己已经支持自动求导的功能了,只要把requires_grad属性设置成True就可以了。
在这里插入图片描述

from __future__ import print_function
import torch as t

x = t.tensor(t.ones(3), requires_grad = True)
w = t.tensor(t.rand(3),requires_grad=True)
#报警告:UserWarning: To copy construct from a tensor, it is recommended to use sourceTensor.clone().detach() or sourceTensor.clone().detach().requires_grad_(True), rather than torch.tensor(sourceTensor).

#改为如下:
x = t.ones(3, requires_grad = True)
w = t.rand(3, requires_grad = True)

y = x*w
#y依赖于w,而w.requires_grad = True
z=y.sum()
print(x.requires_grad)
print(w.requires_grad)
print(y.requires_grad)

#非叶子节点grad计算完之后自动清空,y.grad是none
z.backward()
print(x.grad) #tensor([0.0634, 0.7840, 0.5469])
print(w.grad) #tensor([1., 1., 1.])
print(y.grad) #None

#获取中间变量的梯度
#第一种方法,使用grad获取中间变量的梯度
#z对y的梯度,隐式调用backward
y.retain_grad()
#非叶子节点grad计算完之后自动清空,y.grad是none
z.backward()
print(x.grad) #tensor([0.0634, 0.7840, 0.5469])
print(w.grad) #tensor([1., 1., 1.])
print(y.grad) #tensor([1., 1., 1.])

#获取中间变量的梯度
#第二种方法,使用hook
#hook是一个函数,输入是梯度,不应该有返回值
def variable_hook(grad):
    print('=y的梯度:\r\n',grad)
#注册hook
hook_handle=y.register_hook(variable_hook)
z.backward()
print(x.grad) 
print(w.grad) 
print(y.grad)
#除非你每次都要用hook,否则用完之后记得移除hook
hook_handle.remove()

最后在来看看grad属性和backward函数grad_variables参数的含义。
y.backward(grad_variables)中的grad_variables相当于链式求导法则中的 ∂ z ∂ x = ∂ z ∂ y ∂ y ∂ x \frac{\partial z}{\partial x} = \frac{\partial z}{\partial y}\frac{\partial y}{\partial x} xz=yzxy中的 ∂ z ∂ y \frac{\partial z}{\partial y} yz。z是目标函数,一般是个标量,故而 ∂ z ∂ y \frac{\partial z}{\partial y} yz的形状与y的形状一致。z.backward()等价于y.backward(grad_y)。z.backward()省略了grad_variables参数,是因为z是一个标量,而 ∂ z ∂ z = 1 \frac{\partial z}{\partial z}=1 zz=1

在Pytorch中计算图的特点可总结如下:

  • autograd根据用户对tensor(Variable)的操作构建计算图。
  • 由用户创建的节点称为叶子节点,叶子节点的grad_fn为None。叶子节点中需要求导的ariable,具有AccumulateGrad标识,因其梯度是累加的。
  • tensor默认是不需要求导的,即requires_grad属性默认为False。如果某一个节点requires_grad属性被设置为True,那么所有依赖它的节点requires_grad都为True。
  • variable的volatile属性默认为False,如果某一个variable的volatile属性被设置为True,那么所有依赖它的节点volatile属性都为True。volatile属性为True的节点不会求导,volatile的优先级比requires_grad高。
  • 多次反向传播时,梯度是累加的。反向传播的中间缓存会被清空,为进行多次反向传播需指定retain_graph=True来保存这些缓存。
  • 非叶子节点的梯度计算完之后即被清空,可以使用autograd.grad或hook技术获取非叶子节点梯度的值。
  • variable的grad与data形状一致,应避免直接修改variable.data,因为对data的直接操作无法利用autograd进行反向传播。
  • 反向传播函数backward的参数grad_variables可以看成链式求导的中间结果,如果是标量可以省略,默认为1。
  • Pytorch采用动态图设计,可以很方便地查看中间层的输出,动态地设计计算图结构。

2.2.3 扩展autograd

目前,绝大多数函数都可以使用autograd实现反向求导,但如果需要自己写一个复杂的函数,不支持自动反向求导怎么办?答案是写一个Function,实现它的前向传播和反向传播代码,Function对应于计算图中的矩形,它接收参数,计算并返回结果。
如下示例可能在新版本中已不适用,需查阅新版文档

#自定义的Function需要继承sutograd.Function,没有构造函数__init__,forward和backward函数都是静态方法
class Mul(Function):
    # forward函数的输入和输出都是tensor
    @staticmethod
    def forward(ctx,w,x,b,x_requires_grad=True):
        ctx.x_requires_grad=x_requires_grad
        ctx.save_for_backwardf(w,x)
        output = w*x+b
        return output

    # backward函数的输入和输出都是variable
    # backward函数的输出和forward函数的输入一一对应,backward函数的输入和forward函数的输出一一对应
    # backward函数的grad_output参数即t.autograd.backward中的grad_variables
    @staticmethod
    def backward(ctx,grad_output):
        w,x=ctx.saved_variables
        grad_w=grad_output*x
        if ctx.x_requires_grad:
            grad_x=grad_output*w
        else:
            grad_x=None
        grad_b = grad_output*1
        return grad_w,grad_x,grad_b,None

2.2.4 实践:用(Tensor)Variable 实现线性回归

用autograd实现的线性回归最大的不同点就在于利用autogard不需要手动计算梯度,可以自动微分。另外,需要注意的是在每次反向传播之前要记得先把梯度清零。

import torch as t
from matplotlib import pyplot as plt

#设置随机种子,在不同的计算机上运行输出一致
t.manual_seed(1000)
def get_fake_data(batch_size=8):
    '''产生随机数据:y=x*2+3'''
    x=t.rand(batch_size,1)*20
    y=x*2+(1+t.randn(batch_size,1))*3
    return x,y

#输出显示x-y分布
x,y = get_fake_data(110)
#plt.scatter(x.numpy(),y.numpy())
#plt.show()

#随机初始化参数
w=t.rand(1,1,requires_grad=True)
b=t.zeros(1,1,requires_grad=True)

lr=0.001 #学习率
for ii in range(8000):
    x,y=get_fake_data()

    #forward:计算loss
    y_pred=x.mm(w)+b.expand_as(y)
    loss=0.5*(y_pred-y)**2
    loss=loss.sum()

    #backward:自动计算梯度
    loss.backward()

    #更新参数
    w.data.sub_(lr*w.grad.data)
    b.data.sub_(lr*b.grad.data)

    #梯度清零
    w.grad.data.zero_()
    b.grad.data.zero_()

    if ii%1000 ==0:
        #画图
        x=t.arange(0.0,20.0).view(-1,1)
        y=x.mm(w.data)+b.data.expand_as(x)
        plt.plot(x.numpy(),y.numpy()) #predicted

        x2,y2=get_fake_data(20)
        plt.scatter(x2.numpy(),y2.numpy()) #true data

        plt.xlim(0,20)
        plt.ylim(0,41)
        plt.show()
        plt.pause(0.5)

    print(w.data.numpy(),b.data.numpy())

3 神经网络工具箱nn

autograd实现了自动微分系统,然而对于深度学习来说过于地层。nn模块是构建于autograd之上的神经网络模块。除了nn之外,我们还会介绍神经网络中常用的工具,比如优化器optim、初始化init等。

3.1 nn.Module

使用autograd实现深度学习模型,其抽象程度较低,如果用来实现深度学习模型,则需要编写大量代码。torch.nn应运而生,专门为深度学习设计的模块。torch.nn的核心数据结构是Module,它是一个抽象的概念,既可以表示神经网络中的某个层(layer),也可以表示一个包含很多层的神经网络。在实际使用中,最常见的做法是继承nn.Module,撰写自己的网络/层。
下面先来看如何使用nn.Module实现自己的全连接层。全连接层,又名仿射层,输出y和输入x满足y=Wx+b,W和b是可学习的参数。

import torch as t
from torch import nn
from torch.ao.nn.quantized import LayerNorm

class Linear(nn.Module):#继承nn.Module
    def __init__(self,in_features,out_features):
        super(Linear,self).__init__()#等价于nn.Module.__init__(self)
        self.w=nn.Parameter(t.randn(in_features,out_features))
        self.b =nn.Parameter(t.randn(out_features))

    def forward(self, x):
        x=x.mm(self.w)
        return x+self.b.expand_as(x)

layer =Linear(4,3)
input=t.randn(2,4)
output=layer(input)
print(output)

for name,parameter in layer.named_parameters():
    print(name,parameter) #w and b

可见,全连接层的实现非常简单,其代码量不超过10行,但需注意以下几点。

  • 自定义层Linear必须继承nn.Module,并且在其构造函数中需调用nn.Module的构造函数,即super(Linear,self)._init__()或nn.Module._init__(self)
  • 在构造函数__init__中必须自己定义可学习的参数,并封装成Parameter,如在上例中把w和b封装成Parameter。Parameter是一种特殊的Variable,但其默认需要求导(requires_grad=True),可以通过nn.Parameter??查看Parameter类的源码
  • forward函数实现前向传播过程,其输入可以是一个或多个variable ,对x的任何操作也必须是variable支持的操作
  • 无须写反向传播函数,因其前向传播都是对variable 进行操作,nn.Module能够利用autograd自动实现反向传播,这一点比Function简单许多
  • 使用时,直观上可将layer看成数学概念中的函数,调用layer(input)即可得到input对应的结果。它等价于layers._call__(input),在_call__函数中主要调用的是layer.forward(x),另外还对钩子做了一些处理。所以在实际使用中应尽量使用layer(x)而不是使用layer.forward(x)
  • Module中的可学习参数可以通过named_parameters()或者parameters()返回迭代器,前者会给每个parameter附上名字,使其更具有辨识度
    可见利用|Module实现的全连接层,比利用Function实现的更简单,因其不再需要写反向传播函数。
    Module能够自动检测到自己的parameter,并将其作为学习参数。除了parameter,Module还包含了Module,主Module能够递归查找子Module中的parameter。

下面来稍微复杂一点的网络:多层感知机MLP。
多层感知机的网络结构如图所示,它由两个全连接层组成,采用sigmoid函数作为激活函数。
在这里插入图片描述

import torch as t
from torch import nn
from torch.ao.nn.quantized import LayerNorm

# 全连接网络
class Linear(nn.Module):#继承nn.Module
    def __init__(self,in_features,out_features):
        super(Linear,self).__init__()#等价于nn.Module.__init__(self)
        self.w = nn.Parameter(t.randn(in_features,out_features))
        self.b = nn.Parameter(t.randn(out_features))

    def forward(self, x):
        x=x.mm(self.w)
        return x+self.b.expand_as(x)

layer =Linear(4,3)
input=t.randn(2,4)
output=layer(input)
print(output)

for name,parameter in layer.named_parameters():
    print(name,parameter) #w and b

#多层感知机MLP
class Perceptron(nn.Module):
    def __init__(self,in_features,hidden_features,out_features):
        nn.Module.__init__(self)
        self.layer1=Linear(in_features,hidden_features)#前面定义的全连接层
        self.layer2=Linear(hidden_features,out_features)
    def forward(self,x):
        x=self.layer1(x)
        x=t.sigmoid(x)
        return self.layer2(x)

perceptron =Perceptron(3,4,1)
for name,param in perceptron.named_parameters():
    print(name,param.size())
  • 构造函数__init__中可利用前面自定义的Linear层(module)作为当前module对象的一个子module,它的可学习参数,也会成为当前module的可学习参数
  • 在前向传播函数中,我们有意思地将输出变量都命名成x,是为了能让Python回收一些中间层的输出,从而节省内存。但不是所有的中间结果都会被回收,有些variable虽然名字被覆盖,但其在反向传播时仍需要用到,此时Python的内存回收模块将通过检查引用计数,不会回收这一部分内存。
  • module中parameter的全局命名规范如下:
    Parameter直接命名,如self.param_name=nn.Parameter(t.randn(3,4)),命名为param_name
    子module中的parameter,会在其名字之前加上当前module的名字,如self.sub_module=SubModule(),Subodule中有个parameter的名字也叫作|param_name,那么二者拼接而成的parameter name就是sub_module.param_name

为方便用户,Pyorch实现了神经网络中绝大多数的layer,这些layer都继承于nn.Module,封装了可学习参数parameter,并实现了forward函数,且专门针对GPU运算进行了CuDNN优化,其速度和性能都十分优异。具体内容可参照官方文档,需注意以下几点:

  • 构造函数的参数,如nn.Linear(in_features,out_features,bias),需关注这三个参数的作用
  • 属性、可学习参数和子module,如nn.Linear中有weight和bias两个可学习参数,不包含子module
  • 输入输出的形状,如nn.linear的输入形状是(N,input_features),输出为(N,output_features),N是batch_size
  • 这些自定义layer对输入形状都有假设,输入的不是单个数据,而是一个batch。若想输入一个数据,必须调用unsqueeze(0)函数将数据伪装成batch_size=1的batch

3.2 常见的神经网络层

补充知识:

  • Pillow读取显示
    以灰度图像方式读取图像 image.convert(“L”)
    显示图像 image.show()
    保存图像 image.save()
from PIL import Image  #使用Pillow库
image = Image.open("image adress") #读取图像
image1 = image.convert("L") #以灰度图像读取
image.show()  #将原始图像展示出来
image1.show()  #将改变后的灰度图像展示出来
image1.save("new_image adress") #保存图像到新的地址
  • nn.Conv2d函数
    Conv2d(in_channels, out_channels, kernel_size, stride=1,padding=0, dilation=1, groups=1,bias=True, padding_mode=‘zeros’)
    输入的通道数目;输出的通道数目;卷积核的大小,类型为int 或者元组,当卷积是方形的时候,只需要一个整数边长即可,卷积不是方形,要输入一个元组表示 高和宽;卷积每次滑动的步长为多少,默认是 1 ;设置在所有边界增加 值为 0 的边距的大小(也就是在feature map 外围增加几圈 0 ),例如当 padding =1 的时候,如果原来大小为 3 × 3 ,那么之后的大小为 5 × 5 。即在外围加了一圈 0 ;控制卷积核之间的间距;dilation:控制卷积核之间的间距(如果设置的是dilation=1,卷积核点与输入之间距离为1的值相乘来得到输出);groups:控制输入和输出之间的连接;bias: 是否将一个 学习到的 bias 增加输出中;padding_mode : 字符串类型,接收的字符串只有 “zeros” 和 “circular”
    注意:参数 kernel_size,stride,padding,dilation 都可以是一个整数或者是一个元组,一个值的情况将会同时作用于高和宽 两个维度,两个值的元组情况代表分别作用于高和宽维度。
    在这里插入图片描述
  • tensor.view函数
    view函数,返回一个有相同数据但不同大小的 Tensor,就是改变矩阵维度,相当于 Numpy 中的resize() 或者 Tensorflow 中的 reshape() 。

3.2.1 图像相关层

图像相关层主要包括卷积层(Conv)、池化层(Pool)等,这些层在实际使用中可分为一维(1D)、二维(2D)和三维(3D),池化方式又分为平均池化(AvgPool)、最大值池化(MaxPool)、自适应池化(AdaptiveAvgPool)等。卷积层除了常用的前向卷积外,还有逆卷积(TransposeConv)。

  • nn.Conv2d
from PIL import Image
import torch as t
from torch import nn
from torchvision.transforms import ToTensor,ToPILImage

lena = Image.open('/home/zhouyang/图片/test1.png').convert("L")
#lena.show()
#lena = Image.open('/home/zhouyang/图片/test1.png')

#输入是一个batch,batch_size=1
input = (ToTensor()(lena))

#锐化卷积核
kernel = t.ones(3,3)/-9.0
kernel[1][1] = 1
conv = nn.Conv2d(1,1,(3,3),1,bias=False)
conv.weight.data=kernel.view(1,1,3,3)
out = conv(input)
outImg = ToPILImage()(out.data)
outImg.show()

池化层可以看作是一种特殊的卷积层,用来下采样,但池化层没有可学习参数,其weight是固定的。

  • nn.AvgPool2d
pool =nn.AvgPool2d(2,2)
print(list(pool.parameters()))#[]
out = pool(input)
outImg2 = ToPILImage()(out.data)
outImg2.show()
  • Linear全连接层
  • BatchNorm批规范化层,分为1D、2D和3D。BatchNorm是深度网络中经常用到的加速神经网络训练,加速收敛速度及稳定性的算法,是深度网络训练必不可少的一部分,几乎成为标配;将每个batch的数据规范化为统一的分布,帮助网络训练, 对输入数据做规范化,称为Covariate shift。
    数据经过一层层网络计算后,数据的分布也在发生着变化,因为每一次参数迭代更新后,上一层网络输出数据,经过这一层网络参数的计算,数据的分布会发生变化,这就为下一层网络的学习带来困难 – 也就是在每一层都进行批规范化(Internal Covariate shift),方便网络训练,因为神经网络本身就是要学习数据的分布;
    首先,BatchNorm后是不改变输入的shape的

nn.BatchNorm1d: N * d --> N * d
nn.BatchNorm2d: N * C * H * W – > N * C * H * W
nn.BatchNorm3d: N * C * d * H * W --> N * C * d * H * W

(1)nn.BatchNorm1d

CLASStorch.nn.BatchNorm1d(num_features, eps=1e-05, momentum=0.1, affine=True,
track_running_stats=True, device=None, dtype=None)

num_features: 输入维度,也就是数据的特征维度;
eps: 是在分母上加的一个值,是为了防止分母为0的情况,让其能正常计算;
affine: 是仿射变化,将,分别初始化为1和0;

主要作用在特征上,比如输入维度为N*d, N代表batchsize大小,d代表num_features;
而nn.BatchNorm1d是对num_features做归一化处理,也就是对批次内的特征进行归一化;
如输入 N = 5(batch_size = 5), d = 3(数据特征维度为3);
在这里插入图片描述
上图中的r, b是可学习的参数,文档中成为放射变换,文档中称为, 可以使用x.weight 和 x.bias获得, r初始化值为1,b初始化值为0;
上图中方差的计算是采用的有偏估计;
归一化处理公式:
在这里插入图片描述
E(x)表示均值, Var(x)表示方差;表示为上述参数的eps,防止分母为0 的情况;

# batchNorm试验
bn_m = nn.BatchNorm1d(3)  # 首先要实例化,才能使用,3 对应输入特征,也就是number_features
#m.weight  # 对应r ,初始化值为1
print(bn_m.weight) #Parameter containing: tensor([1., 1., 1.], requires_grad=True)
# m.bias  # 对应b,初始化为0
print(bn_m.bias)#Parameter containing: tensor([0., 0., 0.], requires_grad=True)

#输入batch_size=2,维度3
input = t.randn(2,3) # 数据N要大于等于2
linear = nn.Linear(3,3)
h = linear(input)
bn_out = bn_m(h)

print(bn_out.mean(dim=0))  # 归一化后,平均值都是0, e-08 实际上也就是0了
#tensor([0.0000e+00, -1.1921e-08, -2.3842e-08], grad_fn= < MeanBackward1 >)
print(bn_out.std(dim=0, unbiased=False))  # 标准差为1, 有偏估计,所以unbiased = False
#tensor([1.0000, 1.0000, 1.0000], grad_fn= < StdBackward0 >)

print("普通方法实现BatchNorm:")
# 普通方法实现BatchNorm
#x = t.randn(5,3)
mean = h.mean(dim = 0)
print(mean)
std = t.sqrt(1e-5 + t.var(h,dim = 0, unbiased = False))
print("std")
print(std)
print(h - mean)/std # unsupported operand type(s) for /: 'NoneType' and 'Tensor'

(2)nn.BatchNorm2d
CLASStorch.nn.BatchNorm2d(num_features, eps=1e-05, momentum=0.1, affine=True,
track_running_stats=True, device=None, dtype=None)

主要作用在特征上,比如输入维度为BCH*W, B代表batchsize大小,C代表channel,H代表图片的高度维度,W代表图片的宽度维度;
而nn.BatchNorm2d是对channel做归一化处理,也就是对批次内的特征进行归一化;
如输入B * C * H * W = (2 * 3 * 2 * 2):
在这里插入图片描述计算的均值和方差的方式实际上是把batch内对应通道的数据拉平计算;

y = t.randn(2,3,2,2)
print(y)
n = nn.BatchNorm2d(3)
print(n.weight)
print(n.bias)
print(n(y))

#输出如下:
tensor([[[[ 2.2127,  0.1815],
          [ 0.6606,  1.1299]],

         [[ 0.2360, -0.9037],
          [ 0.1487, -0.1558]],

         [[ 0.6241, -0.8527],
          [ 0.0867, -0.4060]]],


        [[[-1.8447, -0.9500],
          [ 0.9744, -0.0745]],

         [[-0.5506,  0.8648],
          [-0.8307, -1.2125]],

         [[ 0.1974, -0.2643],
          [-1.9011, -0.5072]]]])
Parameter containing:
tensor([1., 1., 1.], requires_grad=True)
Parameter containing:
tensor([0., 0., 0.], requires_grad=True)
tensor([[[[ 1.6248, -0.0883],
          [ 0.3157,  0.7115]],

         [[ 0.8220, -0.9242],
          [ 0.6882,  0.2216]],

         [[ 1.3951, -0.6612],
          [ 0.6468, -0.0392]]],


        [[[-1.7972, -1.0426],
          [ 0.5804, -0.3042]],

         [[-0.3832,  1.7854],
          [-0.8123, -1.3974]],

         [[ 0.8010,  0.1582],
          [-2.1207, -0.1800]]]], grad_fn=<NativeBatchNormBackward0>)


#均值方差计算演示
z = [-1.2111,  1.0613, 0.6797, -1.4823, -1.0742,  1.0204, 0.6549,  0.3513] # 每个通道拉平计算
import numpy as np
print(np.mean(z)) # 10的-17次方就是0
print(np.std(z)) # numpy默认是有偏的, torch的模式是无偏的
  • InstanceNorm层,风格迁移中常用到
  • Dropout层,用来防止过拟合,同样分为1D、2D和3D
# 每个元素以0.5的概率舍弃
dropout = nn.Dropout(0.5)
o=dropout(bn_out)
print(o)

3.3 激活函数

PyTorch实现了常见的激活函数,其具体的接口信息可参见官方文档。这些激活函数可作为独立的layer使用,这里介绍常用的激活函数ReLU: R e L U ( x ) = m a x ( 0 , x ) ReLU(x)=max(0,x) ReLU(x)=max(0,x)

from PIL import Image
import torch as t
from torch import nn
from torchvision.transforms import ToTensor,ToPILImage

relu = nn.ReLU(inplace=True)
input= t.randn(2,3)
print(input)
output =relu(input)
print(output)#小于0的都被截断为0
#等价于input.clamp(min=0)

ReLU函数有个inplace参数,如果设置为True,它会把输出直接覆盖到输入中,这样可以节省内存/显存。之所以可以覆盖是因为在计算ReLU的反向传播时,只需根据输出就能够推算出反向传播的梯度。但是只有少数的autograd操作支持inplace操作(如variable.sigmoid_()),除非你明确地知道自己在做什么,否则一般不要使用inplace操作。在以上例子中,都是将每一层的输出直接作为下一层的输入,这种网络称为前馈传播网络(Feedforward Neural Network)。对于此类网络,如果每次都写复杂的forward函数会有些麻烦,在此就有两种简化方式,ModuleList和Sequential。其中Sequential是一个特殊的Module,它包含几个子module,前向传播时会将输入一层接一层地传递下去。ModuleList也是一个特殊的Module,可以包含几个子module,可以像用list一样使用它,但不能直接把输入传给ModuleList。

# Sequential的三种写法
net1 = nn.Sequential()
net1.add_module('conv',nn.Conv2d(3,3,3))
net1.add_module('batchnorm',nn.BatchNorm2d(3))
net1.add_module('activation_layer',nn.ReLU())

net2 = nn.Sequential(nn.Conv2d(3,3,3),
                     nn.BatchNorm2d(3),
                     nn.ReLU())

from collections import OrderedDict
net3 = nn.Sequential(OrderedDict([
    ('conv1',nn.Conv2d(3,3,3)),
    ('bn1',nn.BatchNorm2d(3)),
    ('relu1',nn.ReLU())]))

print('net1:',net1)
print('net2:',net2)
print('net3:',net3)

#输出
net1: Sequential(
  (conv): Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1))
  (batchnorm): BatchNorm2d(3, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
  (activation_layer): ReLU()
)
net2: Sequential(
  (0): Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1))
  (1): BatchNorm2d(3, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
  (2): ReLU()
)
net3: Sequential(
  (conv1): Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1))
  (bn1): BatchNorm2d(3, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
  (relu1): ReLU()
)

#可根据名字或序号取出子module
print(net1.conv)
print(net2[0])
print(net3.conv1)
#输出
Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1))
Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1))
Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1))

如下演示nn.ModuleList方法:

input = t.rand(1,3,4,4)
output=net1(input)
output=net2(input)
output=net3(input)
output=net3.relu1(net1.batchnorm(net1.conv(input)))

modelList = nn.ModuleList([nn.Linear(3,4),
                           nn.ReLU(),
                           nn.Linear(4,2)])
input=t.randn(1,3)
for model in modelList:
    input = model(input)
#下面会报错,因为modellist没有实现forward方法
#output = modelList(input)

此处,为何不直接使用Python中自带的list,而非要多此一举?这是因为ModuleList是Module的子类,当在Module中使用它时,就能自动识别为子module。

class MyModule(nn.Module):
    def __init__(self):
        super(MyModule,self).__init__()
        self.list = [nn.Linear(3,4),nn.ReLU()]
        self.module_list = nn.ModuleList([nn.Conv2d(3,3,3),nn.ReLU()])

    def forward(self):
        pass

model = MyModule()
print(model)

for name,param in model.named_parameters():
    print(name,param.size())

#输出
MyModule(
  (module_list): ModuleList(
    (0): Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1))
    (1): ReLU()
  )
)

module_list.0.weight torch.Size([3, 3, 3, 3])
module_list.0.bias torch.Size([3])

可见,list不能被子module识别,而且ModuleList能够被主module识别。这意味着如果用list保存子module,将无法调整其参数,因其未加入主module的参数中。
除ModuleList之外还有个ParameterList,它是一个可以包含多个parameter的类list对象。在实际应用中,使用方式与ModuleList类似。在构造函数init中用到list、tuple、dict等对象时,一定要思考是否该用ModuleList或Parameterist代替。

3.4 循环神经网络RNN

随着深度学习和自然语言处理的结合加深,循环神经网络RNN的使用也越来越多,关于RNN的基础知识,推荐阅读colah的文章(http://colah.github.io/posts/2015-08-Understanding-LSTMs/)。PyTorch中实现了如今最常用的三种RNN:RNN(vanilla RNN)、LSTM和GRU。此外还有对应的三种RNNCell。
RNN和RNNCell层的区别在于前者能够处理整个序列,而后者一次只处理序列中一个时间点的数据,前者封装更完备易于使用,后者更具灵活性,RNN层可以通过组合调用RNNCell来实现。

3.4.1 补充:LSTM长短期记忆递归神经网络

参考:
1.LSTM长短期记忆递归神经网络

LSTM,Long Short Term Memory (长短期记忆) 是一种特殊的递归神经网络 。这种网络与一般的前馈神经网络不同,LSTM可以利用时间序列对输入进行分析;简而言之,当使用前馈神经网络时,神经网络会认为我们t时刻输入的内容与t+1时刻输入的内容完全无关,对于许多情况,例如图片分类识别,这是毫无问题的,可是对于一些情景,例如自然语言处理 (NLP, Natural Language Processing) 或者我们需要分析类似于连拍照片这样的数据时,合理运用t或之前的输入来处理t+n时刻显然可以更加合理的运用输入的信息。为了运用到时间维度上信息,人们设计了递归神经网络 (RNN, Recurssion Neural Network),一个简单的递归神经网络可以用这种方式表示。在图中, x t x_t xt是在t时刻的输入信息, h t h_t ht是在t时刻的输入信息,我们可以看到神经元A会递归的调用自身并且将t-1时刻的信息传递给t时刻。
在这里插入图片描述递归神经网络在许多情况下运行良好,特别是在对短时间序列数据的分析时十分方便。上图所示的简单递归神经网络存在一个“硬伤“,长期依赖问题:递归神经网络只能处理我们需要较接近的上下文的情况:在实验中简单的理想状态下,经过精心调节的RNN超参数可以良好的将非常远的上文信息向后传递。可是在现实的情况中,基本没有RNN可以做到这一点。一些学者后来研究发现RNN的长期依赖问题是这种网络结构本身的问题。
不但如此,相比于一般的神经网络,这种简单的RNN还很容易出现两种在神经网络中臭名昭著的问题:梯度消失问题(神经网络的权重/偏置梯度极小,导致神经网络参数调整速率急剧下降)和梯度爆炸问题(神经网络的权重/偏置梯度极大,导致神经网络参数调整幅度过大,矫枉过正)。相信大家都看过一个著名的鸡汤, ( 0.99 ) ( 365 ) (0.99)^(365) (0.99)(365) ( 1.01 ) ( 365 ) (1.01)^(365) (1.01)(365)的对比。实际上,这个鸡汤非常好的描述了梯度问题的本质:对于任意信息递归使用足够多次同样的计算,都会导致极大或极小的结果,也就是说…
根据微分链式法则,在RNN中,神经元的权重的梯度可以被表示为一系列函数的微分的连乘。因为神经元的参数(权重与偏置)都是基于学习速率(一般为常数)和参数梯度相反数(使得神经网络输出最快逼近目标输出)得到的,一个过小或过大的梯度会导致我们要么需要极长的训练时间(本来从-2.24 调节到 -1.99 只用500个样本,由于梯度过小,每次只调 1 0 ( − 6 ) 10^(-6) 10(6) ,最后用了几万个样本),要么会导致参数调节过度(例如本来应该从-10.02调节到-9.97,由于梯度过大,直接调成了+20.3)

LSTM从被设计之初就被用于解决一般递归神经网络中普遍存在的长期依赖问题,使用LSTM可以有效的传递和表达长时间序列中的信息并且不会导致长时间前的有用信息被忽略(遗忘)。与此同时,LSTM还可以解决RNN中的梯度消失/爆炸问题。
LSTM 的直觉解释,后略,详见参考链接

参考:
2.深入浅出LSTM及其Python代码实现

  • 传统神经网络结构的缺陷
    从传统的神经网络结构我们可以看出,信号流从输入层到输出层依次流过,同一层级的神经元之间,信号是不会相互传递的。这样就会导致一个问题,输出信号只与输入信号有关,而与输入信号的先后顺序无关。并且神经元本身也不具有存储信息的能力,整个网络也就没有“记忆”能力,当输入信号是一个跟时间相关的信号时,如果我们想要通过这段信号的“上下文”信息来理解一段时间序列的意思,传统的神经网络结构就显得无力了。与我们人类的理解过程类似,我们听到一句话时往往需要通过这句话中词语出现的顺序以及我们之前所学的关于这些词语的意思来理解整段话的意思,而不是简单的通过其中的几个词语来理解。
    因此,我们需要构建具有“记忆”能力的神经网络模型,用来处理需要理解上下文意思的信号,也就是时间序列数据。循环神经网络(RNN)就是用来处理这类信号的,RNN之所以能够有效的处理时间序列数据,主要是基于它比较特殊的运行原理。
    这样链式的结构揭示了RNN本质上是与序列相关的,是对于时间序列数据最自然的神经网络架构。并且理论上,RNN可以保留以前任意时刻的信息。RNN在语音识别、自然语言处理、图片描述、视频图像处理等领域已经取得了一定的成果,而且还将更加大放异彩。在实际使用的时候,用得最多的一种RNN结构是LSTM。
    RNN虽然在理论上可以保留所有历史时刻的信息,但在实际使用时,信息的传递往往会因为时间间隔太长而逐渐衰减,传递一段时刻以后其信息的作用效果就大大降低了。因此,普通RNN对于信息的长期依赖问题没有很好的处理办法。为了克服这个问题,Hochreiter等人在1997年改进了RNN,提出了一种特殊的RNN模型——LSTM网络,可以学习长期依赖信息,在后面的20多年被改良和得到了广泛的应用,并且取得了极大的成功。
    LSTM:
    长短期记忆(Long Short Term Memory,LSTM)网络是一种特殊的RNN模型,其特殊的结构设计使得它可以避免长期依赖问题,记住很早时刻的信息是LSTM的默认行为,而不需要专门为此付出很大代价。
    普通的RNN模型中,其重复神经网络模块的链式模型如下图所示,这个重复的模块只有一个非常简单的结构,一个单一的神经网络层(例如tanh层),这样就会导致信息的处理能力比较低。
    而LSTM在此基础上将这个结构改进了,不再是单一的神经网络层,而是4个,并且以一种特殊的方式进行交互。
    在这里插入图片描述

3.4.2 补充:nn.LSTM()函数

nn.LSTM():

input_size :输入的维度,输入数据的特征维数,通常就是embedding_dim(词向量的维度)
hidden_size:h的维度,LSTM中隐层的维度
num_layers:堆叠LSTM的层数,默认值为1,循环神经网络的层数
bias:偏置 ,默认值:True
batch_first: 如果是True,则input为(batch, seq, input_size)。默认值为:False(seq_len, batch, input_size);这个要注意,通常我们输入的数据shape=(batch_size,seq_length,embedding_dim),而batch_first默认是False,所以我们的输入数据最好送进LSTM之前将batch_size与seq_length这两个维度调换。
dropout 默认是0,代表不用dropout
bidirectional :是否双向传播,默认值为False,代表不用双向LSTM

(input_size,hideen_size):以训练句子为例,假如每个词是100维的向量,每个句子含有24个单词,一次训练10个句子。那么batch_size=10,seq=24,input_size=100。(seq指的是句子的长度,input_size作为一个xt的输入) ,所以在设置LSTM网络的过程中input_size=100。由于seq的长度是24,那么这个LSTM结构会循环24次最后输出预设的结果。

3.4.3 代码

t.manual_seed(1000)
#输入batch_size=3,序列长度为2,序列中每个元素占4维
input = t.randn(2,3,4)
#lstm输入向量4维,3个隐藏元,1层
lstm = nn.LSTM(4,3,1)
#初始状态:1层,batch_size=3,3个隐藏元
h0=t.randn(1,3,3)
c0=t.randn(1,3,3)
out,hn=lstm(input,(h0,c0))
print(out)
t.manual_seed(1000)
input = t.randn(2,3,4)
#一个LSTMCell对应的层数只能是一层
lstm = nn.LSTMCell(4,3)
hx=t.randn(3,3)
cx=t.randn(3,3)
out =[]
for i_ in input:
    hx,cx =lstm(i_,(hx,cx))
    out.append(hx)
t.stack(out)
print(out)

词向量在自然语言中应用十分广泛,Pyorch同样提供了Embedding层:

#有4个词,每个词用5维的向量表示
embeding = nn.Embedding(4,5)
#可以用预训练好的词向量初始化embedding
embeding.weight.data = t.arange(0,20).view(4,5)
input = t.arange(3,0,-1).long()
output = embeding(input)
print(output)

3.5 损失函数

在深度学习中要用到各种各样的损失函数(Loss Function),这些损失函数可看作是一种特殊的layer,PyTorch也将这些损失函数实现为nn.Module的子类。然而在实际使用中通常将这些损失函数专门提取出来,作为独立的一部分。详细loss使用参照官方文档,这里以分类中最常用的交叉商损失CrossEntropyloss为例讲解。

#batch_size=3,计算对应每个类别的分数,只有两个类例
score=t.randn(3,2)
# 三个样本分别属于1.0.1类,label必须是Longensor
label = t.Tensor([1,0,1]).long()
#loss与普通的layer无差异
criterion = nn.CrossEntropyLoss()
loss = criterion(score,label)
print(loss)

3.6 优化器

Pyorch将深度学习中常用的优化方法全部封装在torch.optim中,其设计十分灵活,能够很方便地扩展成自定义的优化方法。
所有的优化方法都是继承基类optim.Optimizer,并实现了自己的优化步骤。下面就以最基本的优化方法—随机梯度下降法(SGD)举例说明。这里需要重点掌握:优化方法的基本使用方法,如何对模型的不同部分设置不同的学习率,如何调整学习率。

#首先定义一个LeNet网络
class Net(nn.Module):
    def __init__(self):
        super(Net,self).__init__()
        self.features = nn.Sequential(
            nn.Conv2d(3,6,5),
            nn.ReLU(),
            nn.MaxPool2d(2,2),
            nn.Conv2d(6,16,5),
            nn.ReLU(),
            nn.MaxPool2d(2,2)
        )
        self.classifier = nn.Sequential(
            nn.Linear(16*5*5,120),
            nn.ReLU(),
            nn.Linear(120,84),
            nn.ReLU(),
            nn.Linear(84,10)
        )

    def forward(self,x):
        x = self.features(x)
        x = x.view(-1,16*5*5)
        x=self.classifier(x)
        return x

net = Net()

from torch import optim
optimizer = optim.SGD(params=net.parameters(),lr=1)
optimizer.zero_grad()#梯度清零,等价于net.zero_grad()

input = t.randn(1,3,32,32)
output = net(input)
#output.backward(output)#fake backward

optimizer.step() #执行优化

#为不同子网络设置不同的学习率,在finetune中经常用到
#如果对某个参数不指定学习率,就使用默认学习率
optimizer = optim.SGD([{'params':net.features.parameters()},#学习率为1e-5
                       {'params':net.classifier.parameters(),'lr':1e-2}],lr=1e-5)
#只为两个全连接层设置较大的学习率,其余层的学习率较小
special_layers = nn.ModuleList([net.classifier[0],net.classifier[3]])
special_layers_params = list(map(id,special_layers.parameters()))
base_params = filter(lambda p : id(p) not in special_layers_params,net.parameters())

#调整学习率,新建一个optimizer
optimizer = t.optim.SGD([{'params':base_params},
    {'params':special_layers.parameters(),'lr':0.01}],lr=0.01)

调整学习率主要有两种做法。一种是修改optimizer.param_groups中对应的学习率,另一种是新建优化器(更简单也是更推荐的做法),由于optimizer十分轻量级,构建开销很小,故可以构建新的optimizer。但是新建优化器会重新初始化动量等状态信息,这对使用动量的优化器来说(如带momentum的sgd),可能会造成损失函数在收敛过程中出现振荡。

3.7 nn.functional

nn中还有一个很常用的模块:nn.functional。nn中的大多数layer在functional中都有一个与之相对应的函数。nn.functional中的函数和nn,Module的主要区别在于,用nn.Module实现的layers是一个特殊的的类,都是由class Layer(nn.Module)定义,会自动提取可学习的参数;而nn.functional中的函数更像是纯函数,由def function(input)定义。下面举例说明functional的使用,并对比二者的不同之处。

input = t.randn(2,3)
model = nn.Linear(3,4)
output1 = model(input)
output2 = nn.functional.linear(input,model.weight,model.bias)
print(output1 == output2)

b=nn.functional.relu(input)
b2=nn.ReLU()(input)
print(b==b2)

什么时候使用nn.Model和什么时候使用nn,functional呢?答案很简单,如果模型有可学习参数,最好用nn.Module,否则既可以使用nn.functional也可以使用nn.Module,二者在性能上没有太大差异,具体的使用方式取决于个人喜好。
由于激活函数(ReLU、sigmoid、tanh)、池化(MaxPool)等层没有可学习参数,可以使用对应的functional函数代替,而卷积、全连等具有学习参数的网络建议使用nn.Module。
另外,虽然dropout操作也没有可学习参数,但建议还是使用nn.Dropout而不是nn.functional,因为dropout在训练和测试两个阶段的行为有所差别,使用nn.Module对象能够通过model.eval操作加以区分。
下面举例说明如何在模型中搭配使用nn.Module和nn.functional。

from torch.nn import functional as F
class Net(nn.Module):
    def __init__(self):
        super(Net,self).__init__()
        self.conv1 = nn.Conv2d(3,6,5)
        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):
        #不具备可学习参数的层(激活层、池化层等),将他们用函数代替,这样可以不用放置在构造函数init中
        #有可学习参数的模块,也可以用functional代替,只不过实现起来比较繁琐,需要手动定义参数parameter,如前面实现自定义的全连接层,
        #就可以将weight和bias两个参数单独拿出来,在构造函数中初始化为parameter。
        x=F.pool(F.relu(self.conv1(x)),2)
        x=F.pool(F.relu(self.conv2(x),2))
        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

#不具备可学习参数的层(激活层、池化层等),将他们用函数代替,这样可以不用放置在构造函数init中。有可学习参数的模块,也可以用functional代替,只不过实现起来比较繁琐,需要手动定义参数parameter,如前面实现自定义的全连接层,就可以将weight和bias两个参数单独拿出来,在构造函数中初始化为parameter。
class MyLinear(nn.Module):
    def __init__(self):
        super(MyLinear,self).__init__()
        self.weight = nn.Parameter(t.randn(3,4))
        self.bias = nn.Parameter(t.zeros(3))
    def forward(self):
        return F.linear(input,self.weight,self.bias)

不具备可学习参数的层(激活层、池化层等),将他们用函数代替,这样可以不用放置在构造函数init中。有可学习参数的模块,也可以用functional代替,只不过实现起来比较繁琐,需要手动定义参数parameter,如前面实现自定义的全连接层,就可以将weight和bias两个参数单独拿出来,在构造函数中初始化为parameter。

3.8 初始化策略

良好的初始化能让模型更快收敛,并达到更高水平,而糟糕的初始化可能使模型迅速崩溃。PyTorch中nn.Module的模块参数都采取了较合理的初始化策略,因此一般不用我们考虑。
当然我们也可以用自定义初始化代替系统的默认初始化。当我们使用Parameter时,自定义初始化尤为重要,因为t.Tensor()返回的是内存中的随机数,很可能会有极大值,这在实际训练网络中会造成溢出或者梯度消失。
PyTorch中的nn.init模块专门为初始化设计,实现了常用的初始化策略。如果某种初始化策略nn.init不提供,用户也可以自己直接初始化。

# 利用nn.init初始化
from torch.nn import init
linear = nn.Linear(3,4)
t.manual_seed(1)
#等价于linear.weight.data.normal_(0,std)
print(init.xavier_normal(linear.weight))

#直接初始化
import math
t.manual_seed(1)
#xavier初始化的计算公式
std = math.sqrt(2)/math.sqrt(7.)
linear.weight.data.normal_(0,std)

#对模型的所有参数进行初始化
for name,params in net.named_parameters():
    if name.find('linear')!=-1:
        #init linear
        params[0] #weight
        params[1] #bias
    elif name.find('conv')!=-1:
        pass
    elif name.find('norm')!=-1:
        pass

3.9 nn.Modul深入分析

3.9.1 nn.Modul分析

nn.Module基类的构造函数源码剖析:

def init(self):
self._parameters = OrderedDict()
self._modules = OrderedDict()
self._buffers = OrderedDict()
self._backward_hooks = OrderedDict()
self._forward_hooks = OrderedDict()
self.training = True

_parameters字典,保存用户直接设置的parameter,self.param1 = nn.Parameter(t.randn(3,3))会被检测到,在字典中加入一个key为param,value为对应parameter的item,而self.submodule=nn.Linear(3,4)中的parameters则不会存于此。
_modules,子module。通过self.submodel =nn.Linear(3,4)指定的子module会保存于此。
_buffers,缓存,如batchnorm使用momentum机制,每次前向传播需用到上一次前向传播的结果
_backward_hooks和_forward_hooks,钩子技术,用来提取中间变量,类似variable的hook。
training,BatchNorm与Dropout层在训练阶段和测试阶段中采取的策略不同,通过判断training值决定前向传播策略。
上述几个属性中,_parameters、_modules和_buffers这三个字典中的键值,都可以通过self.key方式获得,效果等价于self._parameters[‘key’]。

class Net(nn.Module):
    def __init__(self):
        super(Net,self).__init__()
        #等价于self.register_parameter('param1',nn.Parameter(t.randn(3,3)))
        self.param1 = nn.Parameter(t.rand(3,3))
        self.submodel1 = nn.Linear(3,4)

    def forward(self,input):
        x= self.param1@input
        x=self.submodel1(x)
        return x

net =Net()

print(net)
print(net._modules)
print(net._parameters)
print(net.param1)

for name,param in net.named_parameters():
    print(name,param.size())

for name,submodel in net.named_modules():
    print(name,submodel)

print('=========')
bn=nn.BatchNorm1d(2)
input = t.rand(3,2,requires_grad=True)
output = bn(input)
print(bn._buffers)

nn.Module在实际使用中可能层层嵌套,一个module包含若干个子module,每一个子module又包含了更多的子module。为方便用户访问各个子module,nn.Module实现了很多方法,如函数children可以查看直接子module,函数modules可以查看所有的子module(包括当前module)。与之相对应的还有函数named_childen和named_modules,能够在返回module列表的同时返回他们的名字。

input = t.arange(0,12).view(3,4)
model = nn.Dropout()
#在训练阶段,会有一半左右的数据被随机置为0
model(input)

model.training = False
#在测试阶段,dropout什么都不做
model(input)

对batchnorm、dropout、instancenorm等在训练和测试阶段行为差距较大的层,如果在测试时不将其training值设为False,则可能会有很大影响,这在实际使用中要千万注意。
虽然可通过直接设置training属性将子module设为train和eval模式,但这种方式比较繁琐,因为一个模型如果具有多个dropout层,就需要为每个dropout层指定training属性。推荐的做法是调用model.train()函数,它会将当前module及其子module中的所有training属性都设为True。相应地,model.eval()函数会把trainint属性都设为False。

print(net.train,net.submodel1.training)
net.eval()
print(net.training,net.submodel1.training)
print(list(net.named_modules()))

register_forward_hook和register_backward_hoo函数功能类似于variable的register_hook,可在module前向传播或反向传播时注册钩子。每次前向传播执行结束后会执行钩子函数(hook)。前向传播的钩子函数具有如下形式:hook(module,input,output)->none,而反向传播则具有如下形式:hook(module,grad_input,grad_output)->Tensor or none。钩子函数不应修改输入和输出,并且在使用后应及时删除,以避免每次都运行钩子增加运行负载。钩子函数主要用在获取某些中间结果的情景,如中间某一层的输出或某一层的梯度。这些结果本应写在forward函数中,但如果在forward函数中加上这些处理,可能会使处理逻辑比较复杂,这时使用钩子技术就更合适。
例如:有一个预训练模型,需要提取模型的某一层(不是最后一层)的输出作为特征进行分类,希望不修改其原有的模型定义文件,这时就可以利用钩子函数。

#伪代码
model = VGG()
features = t.Tensor()
def hook(module,input,output):
    '''当前层的输出复制到features中'''
    features.copy_(output.data)

handle = model.layer8.register_forward_hook(hook)
_ = model(input)
#用完hook后删除
handle.remove()

nn.Module对象在构造函数中的行为看起来有些怪异,想要真正掌握其原理,需要看两个魔法属性(方法)_getattr_()和__setattr__()。在Python中有两个常用的builtin方法:getattr和setattr
getattr(obj,‘attr1’)等价于obj.attr,如果getattr函数无法找到所需属性,Python会调用obj._getattr_(‘attr1’)方法, 即getattr函数无法找到的交给_getattr_函数处理;如果这个对象没有实现__getattr__方法,程序就会抛出异常AttributeError。setattr(obj,‘name’,value)等价于obj.name=value,如果obj对象实现了__setattr__方法,setattr会直接调用obj._setattr_(‘name’,value),否则调用builtin方法。总结如下:

  • result = obj.name会调用builtin函数getattr(obj,‘name’),如果该属性找不到,会调用obj._getattr_(‘name’)
  • obj.name=value会调用builtin函数setattr(obj,‘name’,value),如果obj对象实现了__setattr__方法,setattr会直接调用obj._setattr_(‘name’,value)

nn.Module实现了自定义的__setattr__函数,当执行module.name=value时,会在__setattr__中判断value是否为Parameter或nn.Module对象,如果是则将这些对象加到_parameters和_modules两个字典中;如果是其他类型的对象,如Variable、list、dice等,则调用默认的操作,将这个值保存在__dict__中。(觉得书中写得十分啰嗦,并且还没有说清楚,这两个魔法方法不就是python中的基础知识吗)

module = nn.Module()
module.param = nn.Paeameter(t.ones(2,2))
print(module._parameters)

submodule1=nn.Linear(2,2)
submodule2=nn.Linear(2,2)
module_list=[submodule1,submodule2]
对于list对象,调用builtin函数,保存在__dict__中
module.submodules = module_list
print('modules:',module.modules)
print("
_dict
_[‘submodules’]:",module._dict_.get(‘submodules’))

module_list = nn.ModuleList(module_list)
module.submodules=module_list
print(‘ModuleList is instance of nn.Module:’,isinstance(module_list,nn.Module))
print(‘_modules:’,module._modules)
print(“dict[‘submodules’]:”,module.__dict__get(‘submodules’))

因_modules和_parameters中的item未保存在__dict__中,所以默认的getattr方法无法获取它,因而nn.Module实现了自定义的_getattr_方法。如果默认的getattr无法处理,就调用自定义的__getattr__方法,尝试从_modules、_parameters和_buffers这三个字典中获取。

getattr(module,‘training’)#等价于module.training Out:True
module.__getattr(‘training’) #error

module.attr1=2
getattr(module,‘attr1’) #2
module.__getattr(attr1) #error

moudle.param,会调用module.__getattr(‘param’)
getattr(module,‘param’)

3.9.2 模型保存

在PyTorch中保存模型十分简单,所有的Module对象都具有state_dict()函数,返回当前Module所有的状态数据。将这些状态数据保存后,下次使用模型时即可利用model.load_state_dict()函数将状态加载进来。优化器(optimizer)也有类似的机制,不过一般并不需要保存优化器的运行状态。

保存模型
t.save(net.state_dict(),‘net.pth’)
加载已保存的模型
net2 = Net()
net2.load_state_dict(t.load(‘net.pth’))

还有另一种保存模型的方法,但因其严重依赖模型定义方式及文件路径结构等,所以很容易出问题,因而不建议使用。

t.save(net,‘net_all.pth’)
net2=t.load(‘net_all.pth’)
print(net2)

3.9.3 Module放在GPU上运行

将Module放在GPU上运行也十分简单,只需以下两步:

  • model = model.cuda() :将模型的所有参数转存到GPU
  • input.cuda():将输入数据放置到GPU上

至于如何在多个GPU上并行计算,PyTorch也提供了两个函数,可实现简单高效的并行GPU计算。

  • nn.parallel.data_parallel(module,inputs,device_ids=None,output_device=None,dim=0,module_kwargs=None)
  • class torch.nn.DataParallel(module,device_ids=None,output_device=None,dim=0)

可见两者参数十分相似,通过device_ids参数可以指定在那些GPU上进行优化,output_device指定输出到那个GPU上。唯一的不同在于前者直接利用多GPU并行计算得道结果,后则则返回一个新的module,能够自动在多GPU上进行并行加速。

method 1
new_net = nn.DataParallel(net,device_ids=[0,1])
output = new_net(input)

method 2
output = nn.parallel.data_parallel(net,input,device_ids=[0,1])

DataParallel并行的方式,是将输入一个batch的数据均分成多份,分别送到对应的GPU进行计算,然后将各个GPU里面得到的梯度相加。与Module相关的所有数据也会以浅复制的方式复制多份。

3.10 nn和autograd的关系

nn.Module利用的是autograd技术,其主要工作是实现前向传播。在forward函数中,nn.Module对输入的Variable进行各种操作,本质上都用到了autograd技术。这里需要对比autograd.Function和nn.Moduel之间的区别。

  • autograd.Function利用Tensor对autograd技术的扩展,为autograd实现了新的运算op,不仅要实现前向传播还要手动实现反向传播。
  • nn.Module利用了autograd技术,对nn的功能进行扩展,实现了深度学习中更多的层。只需实现前向传播功能,autograd即会自动实现反向传播。
  • nn.functional是一些autograd操作的集合,是经过封装的函数。
    作为两种扩充PyTorch接口的方法,我们在实际使用中如何选择呢?如果某一个操作在gutograd中尚未支持,那么需要利用Function手动实现对应的前向传播和反向传播。如果某些时候利用autograd接口比较复杂,则可以利用Function将多个操作聚合,实现优化,正如第3章实现的Sigmoid一样,比直接利用autograd底级别的操作要快。如果只是想在深度学习中增加某一层,使用nn.Module进行封装则更简单高效。

3.11 小试牛刀:用50行代码搭建ResNet

3.11.1 ResNet简介

Kaiming He的深度残差网络(ResNet)在深度学习的发展中起到了很重要的作用,ResNet不仅一举拿下2015年多个计算机视觉比赛项目的冠军,更重要的是这一结构解决了训练极深网络时的梯度消失问题。
这里选用ResNet34讲解ResNet的网络结构。除了最开始的卷积池化和最后的池化全连接之外,网络中有很多结构相似的单元,这些重复单元的共同点就是有个跨层连接的shortcut。ResNet中将一个跨层直连的单元称为Residual block。左边部分是普通的卷积网络结构,右边是直连,如果输入和输出的通道数不一致,或其步长不为1,就需要有一个专门的单元将二者转成一致,使其可以相加。
另外,可以发现Residual block的大小也是有规律的,在最开始的pool之后有连续的几个一模一样的Residual block单元,这些单元的通道数一样,在这里我们将这几个拥有多个Residual block单元的结构称之为layer,注意要和之前讲的layer区分开,这里的layer是几个层的集合。
考虑到Residual block和layer出现了多次,我们可以把他们实现为一个子Module或函数。这里我们将Residual block实现为一个子module,而将layer实现为一个函数。
代码实现,规律总结如下:

  • 对模型中的重复部分,实现为子module或用函数生成相应的module
  • nn.Module和nn.Functional结合使用
  • 尽量使用nn.Seqential
from torch import nn
import torch as t
from torch.nn import functional as F

class ResidualBlock(nn.Module):
    '''
    实现子module:Residual Block
    '''
    def __init__(self,inchannel,outchannel,stride=1,shortcut=None):
        super(ResidualBlock,self).__init__()
        self.left = nn.Sequential(
            nn.Conv2d(inchannel,outchannel,3,stride,1,bias=False),
            nn.BatchNorm2d(outchannel),
            nn.ReLU(inplace=True),
            nn.Conv2d(outchannel,outchannel,3,1,1,bias=False),
            nn.BatchNorm2d(outchannel))
        self.right = shortcut

    def forward(self,x):
        out = self.left(x)
        residual = x if self.right is None else self.right(x)
        out+=residual
        return F.relu(out)

class ResNet(nn.Module):
    '''
    实现主module:ResNet34
    ResNet34包含多个layer,每个layer又包含多个residual block
    用子module实现residual block,用_make_layer函数实现layer
    '''
    def __init__(self,num_classes=1000):
        super(ResNet,self).__init__()
        #前几层图像转换
        self.pre = nn.Sequential(
            nn.Conv2d(3,64,7,2,3,bias=False),
            nn.BatchNorm2d(64),
            nn.ReLU(inplace=True),
            nn.MaxPool2d(3,2,1))
        #重复的layer,分别有3,4,6,3个residual block
        self.layer1 = self._make_layer(64,128,3)
        self.layer2 = self._make_layer(128, 256, 4,stride=2)
        self.layer3 = self._make_layer(256, 512, 6,stride=2)
        self.layer4 = self._make_layer(512, 512, 3,stride=2)
        #分类用的全连接
        self.fc=nn.Linear(512,num_classes)

    def _make_layer(self, inchannel, outchannel, block_num, stride=1):
        '''构建layer,包含多个residual block'''
        shortcut = nn.Sequential(
            nn.Conv2d(inchannel,outchannel,1,stride,bias=False),
            nn.BatchNorm2d(outchannel))
        layers = []
        layers.append(ResidualBlock(inchannel,outchannel,stride,shortcut))

        for i in range(1,block_num):
            layers.append(ResidualBlock(outchannel,outchannel))
        return nn.Sequential(*layers)

    def forward(self,x):
        x=self.pre(x)

        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)

        x = F.avg_pool2d(x,7)
        x = x.view(x.size(0),-1)
        return self.fc(x)

model = ResNet()
input = t.randn(1,3,224,224,requires_grad=True)
out = model(input)
print(out)

PyTorch配套的图像工具包torchvision已经实现了深度学习中大多数经典的模型,其中就包括ResNet34,可通过如下代码使用:

from torchvision import models
input2 = t.randn(1,3,224,224,requires_grad=True)
model2 = models.resnet34()
out2 = model2(input2)
print(out2)

本例中ResNet34的实现参考了torchvision中的实现并作了简化,可阅读相应的源码,这里的实现和torchvision中的不同。

4 PyTorch中常用的工具

在训练神经网络的过程中需要用到很多工具,其中最重要的三部分是数据、可视化和GPU加速。本章主要介绍PyTorch在这几方面常用的工具,合理使用这些工具能极大地提高编码效率。

4.1 数据处理

在解决深度学习问题的过程中,往往需要花费大量的精力去处理数据,包括图像、文本、语音或其他二进制数据等。数据的处理对训练神经网络十分重要,良好的数据处理不仅会加速模型训练,也会提高模型效果。考虑到这一点,PyTorch提供了几个高效便捷的工具,以便使用者进行数据处理或增强等操作,同时可通过并行化加速数据加载。

4.1.1 数据加载

4.1.1.0 补充知识
  • 数据集下载:
    dogs-vs-cats,在kaggle上需要注册才能下载,比较困难。在CSDN上下载,居然要收费。在网上搜索时,无意间进入了微软的网站,居然也提供下载链接,而且下载速度还不慢,在这里分享给大家。
    https://www.microsoft.com/en-us/download/details.aspx?id=54765
4.1.1.2 正文
4.1.1.2 自建数据集加载

在PyTorch中,数据加载可通过自定义的数据集对象实现。数据集对象被抽象为Dataset类,实现自定义的数据集需要继承Dataset,并实现两个Python魔法:

  • _getitem_:返回一条数据或一个样本。obj[index]等价于obj._getitem_(index)
  • _len_:返回样本的数量。len(obj)等价于obj._len_()

如下,以Kaggle经典挑战赛"Dogs vs Cat"的数据为例,讲解如何处理数据。"Dogs vs Cat"是一个分类问题,判断一张图片是猫还是狗,其所有图片都存放在一个文件夹下,根据文件名的前缀判断是狗还是猫。

import torch as t
from torch.utils import data

import os
from PIL import Image
import numpy as np

class DogCat(data.Dataset):
    def __init__(self,root):
        imgs = os.listdir(root)
        #所有图片的绝对路径
        #这里不实际加载图片,只是指定路径
        #当调用_getitm时才会真正读图片
        self.imgs = [os.path.join(root,img) for img in imgs]

    def __getitem__(self, index):
        img_path = self.imgs[index]
        #dog->1 cat->0
        label = 1 if 'dog' in img_path.split('/')[-1] else 0
        pil_img = Image.open(img_path)
        array = np.asarray(pil_img)
        data = t.from_numpy(array)
        return data,label

    def __len__(self):
        return len(self.imgs)

dataset = DogCat('/mydogcat/')
img,label = dataset[0] #相当于调用dataset.__getitem__(0)
for img,label in dataset:
    print(img.size(),img.float().mean(),label)

通过上面的代码,我们学习了如何自定义自己的数据集,并可以依次获取。但这里返回的数据不合适实际使用,因其具有如下两方面问题:

  • 返回样本的形状不一,每张图片的大小不一样,这对于需要取batch训练的神经网络来说很不友好
  • 返回样本的数值较大,未归一化至[-1,1]

针对上述问题,PyTorch提供了torchvision。它是一个视觉工具包,提供了很多视觉图像处理的工具,其中transforms模块提供了对PIL Image对象和Tensor对象的常用操作。
对PIL Imaged的常见操作如下:

  • Resize:调整图片尺寸
  • CenterCrop、RandomCrop、RandomSizedCrop:裁减图片
  • Pad:填充
  • ToTensor:将PIL Image对象转成Tensor,会自动将[0,255]归一化至[0,1]
    对Tensor的常见操作如下:
  • Normalize:标准化,即减均值,除以标准差
  • ToPILImage:将Tensor转为PIL IMage对象

如果要对图片进行多个操作,可通过Compose将这些操作拼接起来,类似于nn.Sequential。注意,这些操作定以后是以对象的形式存在,真正使用时需要调用它的_call方法,类似于nn.Module。例如,要将图片调整为224x224,首先应构建操作trans = Scale(224,224),然后调用trans(img)。下面我们就用transforms的这些操作来优化上面实现的dataset。

import torch as t
from torch.utils import data

import os
from PIL import Image
import numpy as np

from torchvision import transforms as T

transform = T.Compose([
    T.Resize(224),#缩放图片Image,保持长宽比不变,最短边为224像素
    T.CenterCrop(224),#从图片中间切出224x224的图片
    T.ToTensor(),#将图片(Image)转成Tensor,归一化至[0,1]
    T.Normalize(mean=[.5,.5,.5],std=[.5,.5,.5]) #标准化至[-1,1]
])

class DogCat(data.Dataset):
    def __init__(self,root,transforms=None):
        imgs = os.listdir(root)
        #所有图片的绝对路径
        #这里不实际加载图片,只是指定路径
        #当调用_getitm时才会真正读图片
        self.imgs = [os.path.join(root,img) for img in imgs]
        self.transforms = transforms

    def __getitem__(self, index):
        img_path = self.imgs[index]
        #dog->1 cat->0
        label = 0 if 'dog' in img_path.split('/')[-1] else 1
        pil_img = Image.open(img_path)
        if self.transforms:
            pil_img = self.transforms(pil_img)
        array = np.asarray(pil_img)
        data = t.from_numpy(array)
        return data,label

    def __len__(self):
        return len(self.imgs)

dataset = DogCat('/home/zhouyang/PythonStudyFiles/Pytorch/pytorch-chengyun/CIFAR10/mydogcat/',transform)
img,label = dataset[0] #相当于调用dataset.__getitem__(0)
for img,label in dataset:
    print(img.size(),img.float().mean(),label)

除了上述操作之外,transforms还可以通过Lambda封装自定义的转换策略。例如,相对PIL Image进行随机旋转,则可写成trans=T.Lambda(lambda img:img.rotate(random()*360))。

4.1.1.2 Dataset-ImageFolder数据集加载

torchvision已经预先实现了常用的Dataset,包括前面使用过的CIFAR-10,以及ImageNet、COCO、MNIST、LSUN等数据集,可通过调用torchvision.datasets下相应对象来调用相关数据集,具体使用方法见官方文档。
本节介绍一个读者经常使用到的Dataset-ImageFolder,它的实现和上述DogCat很相似。ImageFolder假设所有文件按文件夹保存,每个文件夹下存储同一个类别的图片,文件夹名为类名,其构造函数如下:

ImageFolder(root,transform=None,target_transform=None,loader=default_loader)
1)root:在root指定的路径下寻找图片
2)transform:对PIL Image进行转换操作,transform的输入是使用loader读取图片的返回对象
3)target_transform:对label的转换
4)loader:指定加载图片的函数,默认操作是读取为PIL Image对象

label是按照文件夹名顺序排序后存成字典的,即{类名:类序号(从0开始)},一般最好直接将文件夹命名为从0开始的数字,这样会和ImageFolder实际的label一致,如果不是这种命名规范,建议通过self.class_to_idx属性了解label和文件夹名的映射关系。

from torchvision.datasets import ImageFolder
dataset = ImageFolder('/home/zhouyang/PythonStudyFiles/Data/kagglecatsanddogs_5340/PetImages/')
print(dataset.class_to_idx) #Out:{'Cat': 0, 'Dog': 1}
#所有图片的路径和对应的label
#print(dataset.imgs)
#没有任何的transform,所以返回的还是PIL Image对象
print(dataset[0][1]) #第一维是第几张图,第二维是1返回label,第二维为0返回图片数据
dataset[0][0].show()
#加上transform
normalize = T.Normalize(mean=[.4,.4,.4],std=[.2,.2,.2])
transform = T.Compose([
    T.Resize(224),
    T.CenterCrop(224),
    T.RandomHorizontalFlip(),
    T.ToTensor(),
    normalize
])

from torchvision.datasets import ImageFolder
dataset = ImageFolder(
    '/home/zhouyang/PythonStudyFiles/Data/kagglecatsanddogs_5340/PetImages/',
    transform=transform)
print(dataset.class_to_idx) #Out:{'Cat': 0, 'Dog': 1}
#所有图片的路径和对应的label
#print(dataset.imgs)
#没有任何的transform,所以返回的还是PIL Image对象
print(dataset[0][1]) #第一维是第几张图,第二维是1返回label,第二维为0返回图片数据
#在深度学习中,图片数据一般保存为CxHxW,即通道数x图片高x图片宽
print(dataset[0][0].size())#torch.Size([3, 224, 224])
to_img =T.ToPILImage()
#0.2和0.4是标准差和均值的近似
to_img(dataset[0][0]*0.2+0.4).show()
#//dataset[0][0].show()
4.1.1.3 DataLoader数据集加载

Dataset只负责数据的抽象,一次调用_getitem只返回一个样本。前面提到过,在训练神经网络时,是对一个batch的数据进行操作,同时还需要对数据进行shuffle和并行加速等。对此,PyTorch提供了DataLoader帮助我们实现这些功能。

DataLoader(dataset,batch_size=1,shuffle=False,sampler=None,num_workers=0,collate_fn=default_collate,pin_memory=False,drop_last=False)
1)dataset:加载的数据集(Dataset对象)
2)batch_size:batch size(批大小)
3)shuffle:是否将数据打乱
4)sampler:样本抽样
5)num_workers:使用多进程加载的进程数,0代表不使用多进程
6)collate_fn:如何将多个样本数据拼接成一个batch,一般使用默认的拼接方式即可
7)pin_memory:是否将数据保存在pin_memory区,pin_memory中的数据转到GPU会快一些
8)drop_last:dataset中的数据个数可能不是batch_size的整数倍,drop_last为True会将多出来不足一个batch的数据丢弃

from torch.utils.data import DataLoader
dataloader =DataLoader(dataset,batch_size=3,shuffle=True,num_workers=0,drop_last=False)
dataiter = iter(dataloader)
imgs,labels =next(dataiter)
print(imgs.size())#batch_size,channel,height,weight #torch.Size([3, 3, 224, 224])

dataloader是一个可迭代的对象,可以像使用迭代器一样使用它,如

for batch_data ,batch_labels in dataloader
train()

dataiter = iter(dataloader)
batch_datas,batch_labesl = next(dataiter)

在数据处理中,有时会出现某个样本无法读取等问题,例如某张图片损坏。这时在_getitem函数中将出现异常,此时最好的解决方案即是将错误的样本剔除。如果遇到这种情况无法处理,则可以返回None对象,然后在Dataloader中实现自定义的collate_fn,将空对象过滤掉。但要注意,此时dataloader返回的一个batch的样本数目会少于batch_size。

import torch as t
from torch.utils import data

import os
from PIL import Image
import numpy as np

from torchvision import transforms as T



transform = T.Compose([
    T.Resize(224),#缩放图片Image,保持长宽比不变,最短边为224像素
    T.CenterCrop(224),#从图片中间切出224x224的图片
    T.ToTensor(),#将图片(Image)转成Tensor,归一化至[0,1]
    T.Normalize(mean=[.5,.5,.5],std=[.5,.5,.5]) #标准化至[-1,1]
])

class DogCat(data.Dataset):
    def __init__(self,root,transforms=None):
        imgs = os.listdir(root)
        #所有图片的绝对路径
        #这里不实际加载图片,只是指定路径
        #当调用_getitm时才会真正读图片
        self.imgs = [os.path.join(root,img) for img in imgs]
        self.transforms = transforms

    def __getitem__(self, index):
        img_path = self.imgs[index]
        #dog->1 cat->0
        label = 0 if 'dog' in img_path.split('/')[-1] else 1
        pil_img = Image.open(img_path)
        if self.transforms:
            pil_img = self.transforms(pil_img)
        array = np.asarray(pil_img)
        data = t.from_numpy(array)
        return data,label

    def __len__(self):
        return len(self.imgs)

dataset = DogCat('/home/zhouyang/PythonStudyFiles/Pytorch/pytorch-chengyun/CIFAR10/mydogcat/',transform)
img,label = dataset[0] #相当于调用dataset.__getitem__(0)
for img,label in dataset:
    print(img.size(),img.float().mean(),label)

##################

class NewDogCat(DogCat):#继承前面实现的DogCat数据集
    def __getitem__(self, index):
        try:
            #调用父类的获取函数,即DogCat.__getitem__(self, index)
            return super(NewDogCat,self).__getitem__(index)
        except:
            return None,None

from torch.utils.data.dataloader import default_collate  # 导入默认的拼接方式
def my_collate_fn(batch):
    '''batch中每个元素形如(data,label)'''
    #过滤为None的数据
    batch =list(filter(lambda x:x[0] is not None,batch))
    return default_collate(batch) #用默认方式拼接过滤后的batch数据

dataset2 = NewDogCat('/home/zhouyang/PythonStudyFiles/Pytorch/pytorch-chengyun/CIFAR10/mydogcat/',transform)
#print(dataset2[15])

from torch.utils.data import DataLoader
dataloader = DataLoader(dataset2,2,collate_fn=my_collate_fn,num_workers=1)
for batch_datas,batch_labels in dataloader:
    print(batch_datas.size(),batch_labels.size())

通过观察batch_size的大小,可以知道每batch中size大小,最后一个batch的数据少于batch_size,可通过指定drop_last=True丢弃最后一个样本数目不足batch_size的batch。

对样本损坏或数据集加载异常等情况,还可以通过其他方式解决,如遇到异常情况,可随机取一张图片代替。

class NewDogCat(DogCat):
def getitem(self, index):
try:
return super(NewDogCat,self).getitem(index)
except:
new_index = random.randint(0,len(self)-1)
return self[new_index]

相比较丢弃异常图片而言,这种做法会更好一些,因为它能保证每个batch样本的数目仍是batch_size。但在大多数情况下,最好的方式还是对数据进行彻底清洗。

4.1.1.4 DataLoader多线程加载注意问题

DataLoader里并没有太多的魔法方法,它封装了Python的标准库multiprocessing,使其能够实现多进程加速。在Dataset和DataLoader的使用方面有以下建议:
1)高负载的操作放在_getitem中,如加载图片等。因为多线程会并行地调用_getitem函数,将负载高的放在_getitem中能实现并行加速;因为dataloader使用多线程加载,如果在Dataset中使用了可变对象,可能会有意想不到的冲突。
在多线程中,修改一个可变对象需要加锁,但是dataloader的设计使得其很难加锁(在实际使用中也应尽量避免锁的存在),因此最后避免在dataset中修改可变对象。下面是个不好例子,在多线程处理中self.num可能与预期不符,这种问题不会报错,难以发现。如果一定要修改可变对象,建议使用Python标准库Queue中的相关数据结构。

class BadDataset(Dataset):
def init(self):
self.datas = range(100)
self.num = 0 #取数据的次数
def getitem(self, index):
self.num += 1
return self.datas[index]

2)dataset中应尽量只包含只读对象,避免修改任何可变对象

使用Python multiprocessing库的另一个问题是,在使用多进程时,如果主程序异常终止(比如用ctrl+c强行退出),相应的数据加载进程可能无法正常退出。这时你可能会发现程序已经退出了,但GPU显存和内存依旧被占用着,通过top、ps aux依旧能够看到已经退出的程序,这时就需要手动强行终止进程,建议如下命令:

ps x (获取当前用户的所有进程) | grep <cmdline> (找到已经停止的Pytorch程序进程,如通过Python train.py启动的,那就需要写grep ‘python train.py’) | awk ‘{print $1}’ (获取进程的pid) | xargs kill (终止进程,根据需要可能要写成xargs kill -9强制终止进程)

在执行这句命令之前,建议先打印确认进程:ps x | grep <cmdline>

4.1.1.5 sampler采样模块

PyTorch中还单独提供了一个sampler模块,用来对数据进行采样。常用的有随机采样器RandomSampler,当dataloader的shuffle参数为True时,系统会自动调用这个采样器,实现打乱数据。默认的采样器是SequentialSampler,它会按顺序一个一个进行采样。
这里介绍一个很有用的采样方法:WeightedRandomSampler,它会根据每个样本的权重选取数据,在样本比例不均衡问题中,可用它进行重采样。
构建WeightedRandomSampler时需要提供两个参数:每个样本的权重weights、共选取的样本总数num_samples,以及一个可选参数replacement。权重越大的样本被选中的概率越大,待选取的样本数目一般小于全部的样本数目。replacement用于指定是否可以重复选取某一个样本,默认为True,即允许在一个epoch中重复采样某一个数据。如果为False,则当某一类样本被全部选取完,但其样本数目仍未达到num_samples时,sampler将不会再从该类中选择数据,此时可能导致weights参数失效。

dataset = DogCat('/home/zhouyang/PythonStudyFiles/Pytorch/pytorch-chengyun/CIFAR10/mydogcat/',transform)
#狗的图片被取出的概率时猫的概率的两倍
#两类图片被取出的概率与weights的绝对大小无关,只和比值有关
weights = [2 if label == 1 else 1 for data ,label in dataset]
print(weights)

from torch.utils.data.sampler import WeightedRandomSampler
sampler = WeightedRandomSampler(weights,
                                num_samples=9,
                                replacement=True)
dataloader = DataLoader(dataset,
                        batch_size=3,
                        sampler=sampler)
for datas,labels in dataloader:
    print(labels.tolist())

从上面的例子可见sampler在样本采样中的作用:如果指定了sampler,shuffle将不再生效。并且sampler.num_sampler会覆盖dataset的实际大小,即一个epoch返回的图片总数取决于sampler.num_samples。

4.2 计算机视觉工具包:torchvision

为方便研究者使用,Pyorch团队专门开发了一个视觉工具包torchvision,这个包独立于PyTorch,需通过pip install torchvision安装,之前已经使用过部分功能,这里再做一个系统性的介绍。
torchvision主要包含以下三部分:

  • models:提供深度学习中各种经典网络结构及预训练好的模型,包括AlexNet、VGG系列、ResNet系列、Inception系列等
  • datasets:提供常用的数据集加载,设计上都是继承torch.utils.data.Dataset,主要包括MNIST、CIFAR10/100、ImageNet、COCO等
  • transforms:提供常用的数据预处理操作,主要包括对Tensor及PIL Image对象的操作
from torchvision import models
from torch import nn
#加载预训练好的模型,如果不存在会下载
#预训练好的模型保存在当前目录下面
resnet34 = models.resnet34(pretrained = True,num_classes = 1000)
#修改最后的全连接层为10分类问题(默认是ImageNet上的1000分类)
resnet34.fc = nn.Linear(512,10)

from torchvision import datasets
#指定数据集路径为data,如果数据集不存在则进行下载
#通过train=False获取测试集
datasets = datasets.MNIST('data/',download=True,train=False)

Transforms中涵盖了大部分对Tensor和PIL Image的常用处理,这些已在上文提到,本节不再详细介绍。需要注意的是转换分为两步,第一步构建转换操作,如transf=transforms.Normalize(mean=x,std=y);第二步,执行转换操作,如output=transf(input)。另外,还可以将多个处理操作用Compose拼接起来,构成一个处理转换流程。

from torchvision import trasforms
to_pil = transforms.ToPILImage()
to_pil(t.randn(3,64,64))

torchvision还提供了两个常用的函数,一个是make_grid,它能将多张图片拼接在一个网格中;另一个是save_img,它能将Tensor保存成图片。

TODO:DataLoader如何网格显示,总是失败?(下面代码DataLoader对象数据访问失败)

import torch as t
from torch.utils import data
import os
from PIL import Image
import numpy as np
from torchvision import transforms as T

from torchvision import models
from torch import nn
#加载预训练好的模型,如果不存在会下载
#预训练好的模型保存在当前目录下面
resnet34 = models.resnet34(num_classes = 1000)
#修改最后的全连接层为10分类问题(默认是ImageNet上的1000分类)
resnet34.fc = nn.Linear(512,10)

from torchvision import datasets
#指定数据集路径为data,如果数据集不存在则进行下载
#通过train=False获取测试集
datasets = datasets.MNIST('data/',download=False,train=False)

print(len(datasets))

from torch.utils.data import DataLoader
dataloader = DataLoader(datasets,shuffle=True,batch_size=16)
from torchvision.utils import make_grid,save_image
dataiter = iter(dataloader)
imgs= next(dataiter)
img = make_grid(imgs,4)#拼成4✖4网格图片,且会转成3通道
from torchvision import transforms as T
T.ToPILImage()(img).show()

4.3 可视化工具

训练神经网络时,希望能更直观地了解训练情况,包括损失曲线、输入图片、输出图片、卷积核参数分布等信息。这些信息能帮助我们更好地监督网络的训练过程,并为参数优化提供方向和依据。最简单的办法就是打印输出,但其只能打印数值信息,不够直观,同时无法查看分布、图片、声音等。本节介绍两个深度学习中常用的可视化工具:Tensoroard和visdom。

4.3.1 TensorBoard

最初,TensorBoard是作为TensorFlow的可视化工具迅速流行开来的。作为和TensorFlow深度集成的工具,TensorBoard能够展现TensorFlow网络计算图。同时TensorBoard也是一个相对对立的工具,只要用户保存的数据遵循相应的格式,TensorBoard就能读取这些数据并进行可视化。
这里我们将主要介绍如何在PyTorch中使用tensorboard_logger进行训练损失的可视化。Tensorboard_logger是TeamHG-Memex开发的一款轻量级工具,它将TensorBoard的功能抽出来,使非TensorFlow用户也能使用它进行可视化,但其支持的功能有限。

tensorboard_logger的安装主要分为以下两步:(首选需要安装CPU版本的tensorflow,然后再安装tensorboard_logger)

  • 安装TensorFlow:如果计算机中已经安装完TensorFlow可以跳过这一步,如果计算机中尚未安装,建议安装CPU-Only的版本,具体安装教程见TensorFlow官网,或使用pip直接安装,教育网用户可以通过清华开源镜像提高速度。
  • 安装tensorboard_logger:可通过pip install tensorboard_logger命令直接安装
    tensorboard_logger的使用非常简单,首先用如下命令启动TensorBoard:

tensorboard --logdir <your/running/dir> --port <your_bind_port>

如何安装tensorflow:

下面举例说明tensorboard_logger的使用。

4.3.2 visdom

4.4 使用GPU加速:cuda

与对GPU完全透明的Theano相比,在PyTorch中使用GPU会复杂一些,但这也意味着对GPU资源更加灵活高效的控制。这部分内容在前面介绍Tensor、Module时大多都提到过 ,本节做个总结,并介绍相关应用。
在PyTorch中以下数据结构(Tensor、nn.Module)分为CPU和GPU两个版本,他们都带有一个.cuda方法,调用此方法即可将其转为对应的GPU对象。注意,tensor.cuda会返回一个新对象,这个新对象的数据已转移至GPU而之前的tensor数据还在原来的设备上(CPU)。module.cuda会将所有的数据都迁移至GPU,并返回自己。所以module = module.cuda()和module.cuda()的效果相同。
其本质上还是利用了Tensor在GPU和CPU之间的转换,nn.module的cuda方法是将nn.Module下的所有parameter(包括子module的parameter)都转移至GPU,而Parameter本质上也是Tensor。

将数据转移至GPU方法叫作.cuda而不是.gpu,主要是因为GPU编程接口采用CUDA,只有部分NVIDIA的GPU才支持,而Pytorch还预留着未来AMD的GPU,AMD的GPU编程接口采用OpenCL,预留着.cl方法。

import torch as t
import torch.nn as nn

tensor = t.Tensor(3,4)
tensor_cuda = tensor.cuda(0)#返回一个新的tensor,保存在cuda上;但原来的tensor并没有改变
print(tensor.is_cuda)#False
print(tensor_cuda.is_cuda)#True

module = nn.Linear(3,4)
module.cuda(0)
print(module.weight.is_cuda)#True

print("#########")
class VeryBigModule(nn.Module):
    def __init__(self):
        super(VeryBigModule,self).__init__()
        self.GiantParameter1 = t.nn.Parameter(t.randn(100,100)).cuda()
        self.GiantParameter2 = t.nn.Parameter(t.randn(100,100)).cuda()
        
    def forward(self,x):
        x = self.GiantParameter1.mm(x.cuda(0))
        x = self.GiantParameter2.mm(x.cuda(1))#分布到不同的GPU中
        return x

关于使用GPU的一些建议:

  • 一些简单的操作可以直接用CPU完成
  • 数据在CPU和GPU间传递比较耗时,应当尽量避免
  • 在进行底精度计算时,可考虑HalfTensor,相比FloatTensor能节省一半的显存,但千万注意数值溢出

注意:大部分损失函数也属于nn.Module,但在使用GPU时,很多时候我们都忘记使用它的.cuda方法,在大多数情况下不会报错,是因为损失函数本身没有可学习的参数(learnable parameters)。但在某些情况下会出现问题,为了保险起见同时也为了代码规范,应记得调用criterion.cuda。

# 交叉商损失函数,带权重
criterion = t.nn.CrossEntropyLoss(weight=t.Tensor([1,3]))

input = t.randn(4,2).cuda()
target = t.tensor([1,0,0,1]).long().cuda()

#loss = criterion(input,target) #报错,weight未转移至GPU

criterion.cuda()
loss = criterion(input,target)
print(criterion._buffers)

除了调用对象的.cuda方法外,还可以使用torch.cuda.device指定默认使用那块GPU,或使用torch.set_default_tensor_type使程序默认使用GPU。
t.cuda.set_device(1)
设置环境变量CUDA_VISIBLE_DEVICES,决定默认使用设备下标起始号

#指定默认tensor的类型为GPU上的Floatensor
t.set_default_tensor_type('torch.cuda.FloatTensor')
a = t.ones(2,3)
print(a.is_cuda)

4.5 持久化

在Pytorch中,Tensor、nn.Module、Optimizer等对象可以持久化到硬盘,并通过相应的方法加载到内存中。本质上,上述这些信息最终都保存成Tensor。
Tensor的保存和加载十分简单,使用t.save和t.load即可完成相应的功能。在save/load时可指定使用的pickle模块,在load时还可以将GPU tensor映射到CPU或其他GPU上。
可以通过t.save(obj,file_name)等方法保存任意可序列化的对象,然后通过obj=t.load(file_name)方法加载保存的数据。
对Module和Optimizer对象,这里建议保存对应的state_dict,而不是直接保存整个Module/Optimizer对象。Optimizer对象保存的是参数及动量信息,通过加载之前的动量信息,能够有效地减少模型的振荡。

# 1.存tensor
a = t.Tensor(3,4)
if t.cuda.is_available():
    a=a.cuda(0)
    t.save(a,'a.pth')

    #加载,存储于GPU0上,因为保存时tensor就在GPU0上
    b = t.load('a.pth')
    print(b.device)

    #加载,存储于CPU
    c = t.load('a.pth',map_location=lambda storage,loc:storage)
    print(c.device)

    #加载,存储于GPU1上
    #d = t.load('a.pth',map_location={'cuda:0':'cuda:1'})
    #print(d.device)


#2.存model
#t.set_default_tensor_type('torch.FloatTensor') deprecated
#torch.set_default_dtype() and torch.set_default_device()
from torchvision.models import AlexNet
model = AlexNet()
# module的dtate_dice是一个字典
print(model.state_dict().keys())

t.save(model.state_dict(),'alexnet.pth')
model.load_state_dict(t.load('alexnet.pth'))

#3存optimizer
optimizer = t.optim.Adam(model.parameters(),lr=0.1)
t.save(optimizer.state_dict(),'optimizer.pth')
optimizer.load_state_dict(t.load('optimizer.pth'))

all_data = dict(
    optimizer = optimizer.state_dict(),
    model =model.state_dict(),
    info = u'模型和优化器的所有参数'
)
t.save(all_data,'all.pth')

all_data = t.load('all.pth')
print(all_data.keys())

5.PyTorch实战指南

本章重点不在于如何使用Pytorch的接口,而在于合理的设计程序的结构,使得程序更具可读性、易用性。

在做深度学习实验或项目时,为了得到最后的模型结果,中间往往需要很多次尝试和修改。合理的文件组织结构,以及一些小技巧可以极大地提高代码的易读易用性。根据笔者个人经验,在从事大多数深度学习研究时,程序都需要实现以下几个功能:

  • 模型定义
  • 数据处理和加载
  • 训练模型(Train&Validate)
  • 训练过程的可视化
  • 测试(Test&Inference)

另外,程序还应该满足以下几个要求:

  • 模型需要具有高度可配置性,便于修改参数、修改模型和反复实验;
  • 代码应具有良好的组织结构,一目了然;
  • 代码应具有良好的说明,使其他人能够理解。

讲解如何利用PyTorch完成Kaggle上的经典比赛:Dogs vs Cats。

5.1猫狗二分类

Dogs vs. Cats是一个传统的二分类问题,其训练集包含25000张图片,均放置在同一文件夹下,命名格式为<category>.<num>.jpg, 如cat.10000.jpg、dog.100.jpg,测试集包含12500张图片,命名为<num>.jpg,如1000.jpg。参赛者需根据训练集的图片训练模型,并在测试集上进行预测,输出它是狗的概率。最后提交的csv文件如下,第一列是图片的<num>,第二列是图片为狗的概率。

id,label
10001,0.889
10002,0.01

疑惑:下载的新数据集猫狗单独存放于文件夹中

查找到对应的数据集:[kaggle猫狗大战包含训练(25000张猫狗照片)和测试数据集(12500张猫狗照片)

kaggle比赛猫狗数据集百度网盘分享:
打开链接:https://pan.baidu.com/s/1PGDXtCWm1cpVlg8IwmFJMg
提取码:x45f

5.1.1 文件组织架构

模型定义
数据加载
训练和测试

首先来看程序文件的组织结构:

├── checkpoints/ ---->用于保存训练好的模型,可使程序在异常退出后仍能重新载入模型,恢复训练
├── data/ ---->数据相关操作,包括数据预处理、dataset实现等
│ ├── init.py
│ ├── dataset.py
│ └── get_data.sh
├── models/ ---->模型定义,可以有多个模型,例如上面的AlexNet和ResNet34,一个模型对应一个文件
│ ├── init.py
│ ├── AlexNet.py
│ ├── BasicModule.py
│ └── ResNet34.py
└── utils/ ---->可能用到的工具函数,在本次实验中主要是封装了可视化工具
│ ├── init.py
│ └── visualize.py
├── config.py ---->配置文件,所有可配置的变量都集中在此,并提供默认值
├── main.py ---->主文件,训练和测试程序的入口,可通过不同的命令来指定不同的操作和参数
├── requirements.txt ---->程序依赖的第三方库
├── README.md ---->提供程序的必要说明

  • 关于__init__.py 文件
    一个目录如果包含了__init__.py 文件,那么它就变成了一个包(package)。__init__.py可以为空,也可以定义包的属性和方法,但其必须存在,其它程序才能从这个目录中导入相应的模块或函数。例如在data/文件夹下有__init__.py,则在main.py 中就可以from data.dataset import DogCat。而如果在__init__.py中写入from .dataset import DogCat,则在main.py中就可以直接写为:from data import DogCat,或者import data; dataset = data.DogCat,相比于from data.dataset import DogCat更加便捷。

5.1.2 数据加载

数据的相关处理主要保存在data/dataset.py中。关于数据加载的相关操作,在上一章中我们已经提到过,其基本原理就是使用Dataset提供数据集的封装,再使用Dataloader实现数据并行加载。Kaggle提供的数据包括训练集和测试集,而我们在实际使用中,还需专门从训练集中取出一部分作为验证集。对于这三类数据集,其相应操作也不太一样,而如果专门写三个Dataset,则稍显复杂和冗余,因此这里通过加一些判断来区分。对于训练集,我们希望做一些数据增强处理,如随机裁剪、随机翻转、加噪声等,而验证集和测试集则不需要。下面看dataset.py的代码:

# coding:utf-8
import os
from PIL import Image
from torch.utils import data
import numpy as np
from torchvision import transforms as T

class DogCat(data.Dataset):

    def __init__(self, root, transforms=None, train=True, test=False):
        """
        主要目标: 获取所有图片的地址,并根据训练,验证,测试划分数据
        """
        self.test = test
        imgs = [os.path.join(root, img) for img in os.listdir(root)]

        # 训练集和验证集的文件命名不一样
        # train: data/train/cat.10004.jpg
        # test1: data/test1/8973.jpg
        if self.test:
            imgs = sorted(imgs, key=lambda x: int(x.split('.')[-2].split('/')[-1]))
        else:
            imgs = sorted(imgs, key=lambda x: int(x.split('.')[-2]))

        imgs_num = len(imgs)

        # shuffle imgs   看作者知乎代码加的
        np.random.seed(100)
        imgs = np.random.permutation(imgs)

        # 划分训练、验证集,训练:验证 = 7:3
        if self.test:
            self.imgs = imgs
        elif train:
            self.imgs = imgs[:int(0.7 * imgs_num)]
        else:
            self.imgs = imgs[int(0.7 * imgs_num):]   # 训练集中的30%用作验证集

        if transforms is None:

            # 数据转换操作,测试验证和训练的数据转换有所区别
            normalize = T.Normalize(mean=[0.485, 0.456, 0.406],
                                    std=[0.229, 0.224, 0.225])   # 怎么来的??

            # 测试集和验证集不用数据增强
            if self.test or not train:
                self.transforms = T.Compose([
                    T.Resize(224),
                    T.CenterCrop(224),
                    T.ToTensor(),
                    normalize
                ])
            # 训练集需要数据增强
            else:
                self.transforms = T.Compose([
                    T.Resize(256),
                    T.RandomResizedCrop(224),   # 有改动 RandomReSizedCrop -> RandomResizedCrop
                    T.RandomHorizontalFlip(),
                    T.ToTensor(),
                    normalize
                ])

    def __getitem__(self, index):
        """
        一次返回一张图片的数据
        如果是测试集,没有图片的id,如1000.jpg返回1000
        """
        # train: data/train/cat.10004.jpg
        # test1: data/test1/8973.jpg
        img_path = self.imgs[index]
        if self.test:
            label = int(self.imgs[index].split('.')[-2].split('/')[-1])
        else:
            label = 1 if 'dog' in img_path.split('/')[-1] else 0   # 狗1 猫0
        data = Image.open(img_path)
        data = self.transforms(data)
        return data, label

    def __len__(self):
        '''
        返回数据集中所有图片的个数
        :return:
        '''
        return len(self.imgs)

关于数据集使用的注意事项,之前已经提到,将文件读取等费时操作放在__getitem__函数中,利用多进程加速。一次性将所有图片都读进内存,不仅费时也会占用较大内存,而且不易进行数据增强等操作。另外在这里,我们将训练集中的30%作为验证集,可用来检查模型的训练效果,避免过拟合。在使用时,我们可通过dataloader加载数据。

train_dataset = DogCat(opt.train_data_root, train=True)   # 训练集
trainloader = DataLoader(train_dataset,
                        batch_size = opt.batch_size,
                        shuffle = True,
                        num_workers = opt.num_workers)
                  
for ii, (data, label) in enumerate(trainloader):
	train()

5.1.3 模型定义

模型的定义主要保存在models/目录下,其中BasicModule是对nn.Module的简易封装,提供快速加载和保存模型的接口。

class BasicModule(t.nn.Module):
    """
    简易封装了nn.Module,主要是提供了save和load两个方法
    """

    def __init__(self):
        super(BasicModule, self).__init__()
        self.model_name = str(type(self))  # 模型的默认名字

    def load(self, path):
        """
        可加载指定路径的模型
        """
        self.load_state_dict(t.load(path))

    def save(self, name=None):
        """
        保存模型,默认使用“模型名字+时间”作为文件名
        如:resnet34_05-30_22.29.46.pth
        """
        if name is None:
            prefix = 'checkpoints/' + self.model_name + '_'
            name = time.strftime(prefix + '%m-%d_%H.%M.%S.pth')  # 有改动,windows文件名不支持冒号:
        t.save(self.state_dict(), name)
        return name

    def get_optimizer(self, lr, weight_decay):
        return t.optim.Adam(self.parameters(), lr=lr, weight_decay=weight_decay)

在实际使用中,直接调用model.save()及model.load(opt.load_path)即可。
其它自定义模型一般继承BasicModule,然后实现自己的模型。其中AlexNet.py实现了AlexNet,ResNet34实现了ResNet34。在models/__init__py中,代码如下:

from .AlexNet import AlexNet
from .ResNet34 import ResNet34

这样在主函数中就可以写成:

from models import AlexNet

import models
model = models.AlexNet()

import models
model = getattr(‘models’, ‘AlexNet’)()

其中最后一种写法最为关键,这意味着我们可以通过字符串直接指定使用的模型,而不必使用判断语句,也不必在每次新增加模型后都修改代码。新增模型后只需要在models/__init__.py中加上from .new_module import new_module即可。

其它关于模型定义的注意事项,之前已详细讲解,这里就不再赘述,总结起来就是:

  • 尽量使用nn.Sequential(比如AlexNet)
  • 将经常使用的结构封装成子Module(比如GoogLeNet的Inception结构,ResNet的Residual Block结构)
  • 将重复且有规律性的结构,用函数生成(比如VGG的多种变体,ResNet多种变体都是由多个重复卷积层组成)

5.1.4 工具函数

在项目中,我们可能会用到一些helper方法,这些方法可以统一放在utils/文件夹下,需要使用时再引入。在本例中主要是封装了可视化工具visdom的一些操作,其代码如下,在本次实验中只会用到plot方法,用来统计损失信息。如下visualize.py文件:

# coding:utf-8
import visdom
import time
import numpy as np


class Visualizer(object):
    """
    封装了visdom的基本操作,但是你仍然可以通过`self.vis.function`
    或'self.function'调用原生的visdom接口
    """

    def __init__(self, env='default', **kwargs):
        self.vis = visdom.Visdom(env=env, use_incoming_socket=False, **kwargs)

        # 画的第几个数,相当于横坐标
        # 保存(’loss',23) 即loss的第23个点
        self.index = {}
        self.log_text = ''

    def reinit(self, env='default', **kwargs):
        """
        修改visdom的配置
        """
        self.vis = visdom.Visdom(env=env, **kwargs)
        return self

    def plot_many(self, d):
        """
        一次plot多个
        @params d: dict (name,value) i.e. ('loss',0.11)
        """
        for k, v in d.items():
            self.plot(k, v)

    def img_many(self, d):
        for k, v in d.items():
            self.img(k, v)

    def plot(self, name, y, **kwargs):
        """
        self.plot('loss',1.00)
        """
        x = self.index.get(name, 0)
        self.vis.line(Y=np.array([y]), X=np.array([x]),
                      win=name,
                      opts=dict(title=name),
                      update=None if x == 0 else 'append',
                      **kwargs
                      )
        self.index[name] = x + 1

    def img(self, name, img_, **kwargs):
        """
        self.img('input_img',t.Tensor(64,64))
        self.img('input_imgs',t.Tensor(3,64,64))
        self.img('input_imgs',t.Tensor(100,1,64,64))
        self.img('input_imgs',t.Tensor(100,3,64,64),nrows=10)

        !!!don‘t ~~self.img('input_imgs',t.Tensor(100,64,64),nrows=10)~~!!!
        """
        self.vis.images(img_.cpu().numpy(),
                        win=name,
                        opts=dict(title=name),
                        **kwargs
                        )

    def log(self, info, win='log_text'):
        """
        self.log({'loss':1,'lr':0.0001})
        """

        self.log_text += ('[{time}] {info} <br>'.format(
            time=time.strftime('%m%d_%H%M%S'),
            info=info))
        self.vis.text(self.log_text, win)

    def __getattr__(self, name):

        """
        自定义的plot,image,log,plot_many等除外
        self.function等价于self.vis.function
        """

        return getattr(self.vis, name)

5.1.5 配置文件

在模型定义、数据处理和训练等过程都有很多变量,这些变量应提供默认值,并统一放置在配置文件中,这样在后期调试、修改代码或迁移程序时会比较方便,在这里我们将所有可配置项放在config.py中。

# coding:utf-8
import warnings
import torch as t


class DefaultConfig(object):
    env = 'default'  # visdom 环境
    vis_port = 8097  # visdom 端口
    model = 'ResNet34'  # 使用的模型,名字必须与models/__init__.py中的名字一致   SqueezeNet -> ResNet34

    train_data_root = './data/train/'  # 训练集存放路径   default: './data/train/'
    test_data_root = './data/test1'  # 测试集存放路径     default: './data/test1'
    load_model_path = 'None'  # 加载预训练的模型的路径,为None代表不加载  default: None

    batch_size = 32  # batch size   default: 32  4
    use_gpu = True  # user GPU or not
    num_workers = 4  # how many workers for loading data
    print_freq = 20  # print info every N batch

    debug_file = '/tmp/debug'  # if os.path.exists(debug_file): enter ipdb
    result_file = 'result.csv'

    max_epoch = 10  # default: 10  100
    lr = 0.001  # initial learning rate
    lr_decay = 0.95  # when val_loss increase, lr = lr*lr_decay   default: 0.5
    weight_decay = 0e-5  # 损失函数  default: 0e-5    1e-5

	# 这里暂时可以先不管,后面会讲
    def _parse(self, kwargs):
        """
        根据字典kwargs 更新 config参数
        """
        # 更新配置参数
        for k, v in kwargs.items():
            if not hasattr(self, k):
                # 警告还是报错,取决于个人喜好
                warnings.warn("Warning: opt has not attribut %s" % k)
            setattr(self, k, v)
        
        opt.device = t.device('cuda') if opt.use_gpu else t.device('cpu')  # 原来没有这一行

        # 打印配置信息
        print('user config:')
        for k, v in self.__class__.__dict__.items():
            if not k.startswith('_'):
                print(k, getattr(self, k))

opt = DefaultConfig()

可配置的参数主要包括:数据集参数(文件路径、batch_size等);训练参数(学习率、训练epoch等);模型参数

这样我们在程序中就可以这样使用:

import models
from config import DefaultConfig
opt = DefaultConfig()
lr = opt.lr
model = getattr(models, opt.model)
dataset = DogCat(opt.train_data_root)

这些都只是默认参数,在这里还提供了更新函数,根据字典更新配置参数。

def _parse(self, kwargs):
    """
    根据字典kwargs 更新 config参数
    """
    # 更新配置参数
    for k, v in kwargs.items():
        if not hasattr(self, k):
            # 警告还是报错,取决于个人喜好
            warnings.warn("Warning: opt has not attribut %s" % k)
        setattr(self, k, v)
    
    opt.device = t.device('cuda') if opt.use_gpu else t.device('cpu')  # 原来没有这一行

    # 打印配置信息
    print('user config:')
    for k, v in self.__class__.__dict__.items():
        if not k.startswith('_'):
            print(k, getattr(self, k))

这样我们在实际使用时,并不需要每次都修改config.py,只需要通过命令行传入所需参数,覆盖默认配置即可。如:

opt = DefaultConfig()
new_config = {‘lr’:0.1,‘use_gpu’:False}
opt.parse(new_config)
opt.lr == 0.1 # True

5.1.6 main.py

在讲解主程序main.py之前,我们先来看看2017年3月谷歌开源的一个命令行工具fire2 ,通过pip install fire即可安装。

在主程序main.py中,主要包含四个函数,其中三个需要命令行执行,main.py的代码组织结构如下:

def train(**kwargs):
    """
    训练
    """
    pass
	 
def val(model, dataloader):
    """
    计算模型在验证集上的准确率等信息,用以辅助训练
    """
    pass

def test(**kwargs):
    """
    测试(inference)
    """
    pass

def help():
    """
    打印帮助的信息 
    """
    print('help')

if __name__=='__main__':
    import fire
    fire.Fire()

5.1.7 训练

训练的主要步骤如下:

  • 定义网络
  • 定义数据
  • 定义损失函数和优化器
  • 计算重要指标
  • 开始训练
    训练网络
    可视化各种指标
    计算在验证集上的指标

训练函数的代码如下:

def train(**kwargs):

    # 根据命令行参数更新配置
    opt._parse(kwargs)
    vis = Visualizer(opt.env, port=opt.vis_port)

    # step1: configure model 模型
    model = getattr(models, opt.model)()  # 最后的()不要忘
    if opt.load_model_path:
        model.load(opt.load_model_path)
    model.to(opt.device)   # 这一行和书中相比,改过

    # step2: data  数据
    train_dataset = DogCat(opt.train_data_root, train=True)   # 训练集
    train_dataloader = DataLoader(train_dataset,
                                  opt.batch_size,
                                  shuffle=True,
                                  num_workers=opt.num_workers)

    val_dataset = DogCat(opt.train_data_root, train=False)    # 交叉验证集
    val_dataloader = DataLoader(val_dataset,
                                opt.batch_size,
                                shuffle=False,
                                num_workers=opt.num_workers)
    
    # step3: criterion and optimizer   目标函数和优化器
    criterion = t.nn.CrossEntropyLoss()
    lr = opt.lr
    optimizer = model.get_optimizer(lr, opt.weight_decay)
        
    # step4: meters  统计指标:平滑处理之后的损失,还有混淆矩阵
    loss_meter = meter.AverageValueMeter()
    confusion_matrix = meter.ConfusionMeter(2)
    previous_loss = 1e10

    # train  训练
    for epoch in range(opt.max_epoch):
        
        loss_meter.reset()
        confusion_matrix.reset()

        for ii, (data, label) in tqdm(enumerate(train_dataloader)):

            # train model 训练模型参数
            input_batch = data.to(opt.device)
            label_batch = label.to(opt.device)

            optimizer.zero_grad()  # 梯度清零
            score = model(input_batch)
            loss = criterion(score, label_batch)
            loss.backward()    # 反向传播
            optimizer.step()   # 优化

            # meters update and visualize  更新统计指标及可视化
            loss_meter.add(loss.item())

            # detach 一下更安全保险
            confusion_matrix.add(score.detach(), label_batch.detach())

            if (ii + 1) % opt.print_freq == 0:
                # vis.plot('loss', loss_meter.value()[0])  # 先不可视化了!!!
                print('   loss: ', loss_meter.value()[0])
                
                # 如果需要的话,进入debug模式
                if os.path.exists(opt.debug_file):
                    import ipdb;
                    ipdb.set_trace()
        model.save()

        # validate and visualize  计算验证集上的指标及可视化
        val_cm, val_accuracy = val(model, val_dataloader)
        vis.plot('val_accuracy', val_accuracy)
        vis.log("epoch:{epoch},lr:{lr},loss:{loss},train_cm:{train_cm},val_cm:{val_cm}".format(
                    epoch=epoch, loss=loss_meter.value()[0], val_cm=str(val_cm.value()),
                    train_cm=str(confusion_matrix.value()), lr=lr))

        # update learning rate  如果损失不再下降,则降低学习率
        if loss_meter.value()[0] > previous_loss:          
            lr = lr * opt.lr_decay
            # 第二种降低学习率的方法:不会有moment等信息的丢失
            for param_group in optimizer.param_groups:
                param_group['lr'] = lr

        previous_loss = loss_meter.value()[0]

        print('第', str(epoch), '个迭代已结束')
        print("验证集准确率为: ", str(val_accuracy))
        print('---' * 50)

这里用到了PyTorchNet4(pip install torchnet)里面的一个工具: meter。meter提供了一些轻量级的工具,用于帮助用户快速统计训练过程中的一些指标。AverageValueMeter能够计算所有数的平均值和标准差,这里用来统计一个epoch中损失的平均值。confusionmeter用来统计分类问题中的分类情况,是一个比准确率更详细的统计指标。例如对于表格6-1,共有50张狗的图片,其中有35张被正确分类成了狗,还有15张被误判成猫;共有100张猫的图片,其中有91张被正确判为了猫,剩下9张被误判成狗。相比于准确率等统计信息,混淆矩阵更能体现分类的结果,尤其是在样本比例不均衡的情况下。

表6-1 混淆矩阵

样本判为狗判为猫
实际是狗3515
实际是猫991

PyTorchNet从TorchNet5迁移而来,提供了很多有用的工具,但其目前开发和文档都还不是很完善,本书不做过多的讲解。

5.1.8 验证

验证相对来说比较简单,但要注意需将模型置于验证模式(model.eval()),验证完成后还需要将其置回为训练模式(model.train()),这两句代码会影响BatchNorm和Dropout等层的运行模式。验证模型准确率的代码如下。

@t.no_grad()
def val(model, dataloader):
    """
    计算模型在验证集上的准确率等信息
    """

    # 把模型设为验证模式
    model.eval()

    confusion_matrix = meter.ConfusionMeter(2)
    for ii, (val_input, label) in tqdm(enumerate(dataloader)):
        val_input = val_input.to(opt.device)
        score = model(val_input)
        confusion_matrix.add(score.detach().squeeze(), label.type(t.LongTensor))

    # 把模型恢复为训练模式
    model.train()

    cm_value = confusion_matrix.value()
    accuracy = 100. * (cm_value[0][0] + cm_value[1][1]) / (cm_value.sum())

    print('accuracy: ', str(accuracy))

    return confusion_matrix, accuracy

5.1.9 测试

测试时,需要计算每个样本属于狗的概率,并将结果保存成csv文件。测试的代码与验证比较相似,但需要自己加载模型和数据。

@t.no_grad()  # pytorch>=0.5
def test(**kwargs):
    opt._parse(kwargs)

    # configure model  模型
    model = getattr(models, opt.model)().eval()
    if opt.load_model_path:
        model.load(opt.load_model_path)
    model.to(opt.device)

    # data  数据
    test_data = DogCat(opt.test_data_root, test=True)
    test_dataloader = DataLoader(test_data,
                                 batch_size=opt.batch_size,
                                 shuffle=False,
                                 num_workers=opt.num_workers)

    results = []
    for ii, (data, path) in tqdm(enumerate(test_dataloader)):
        test_input = data.to(opt.device)
        test_score = model(test_input)
        probability = t.nn.functional.softmax(test_score, dim=1)[:, 1].detach().tolist()  # 这里改过,github代码有误
        # label = score.max(dim = 1)[1].detach().tolist()
        
        batch_results = [(path_.item(), probability_) for path_, probability_ in zip(path, probability)]
        results += batch_results

    write_csv(results, opt.result_file)

    return results

5.1.10 帮助函数

为了方便他人使用, 程序中还应当提供一个帮助函数,用于说明函数是如何使用。程序的命令行接口中有众多参数,如果手动用字符串表示不仅复杂,而且后期修改config文件时,还需要修改对应的帮助信息,十分不便。这里使用了Python标准库中的inspect方法,可以自动获取config的源代码。help的代码如下:

def help():
    """
    打印帮助的信息: python file.py help
    """
    
    print("""
    usage : python {0} <function> [--args=value]
    <function> := train | test | help
    example: 
            python {0} train --env='env0701' --lr=0.01
            python {0} test --dataset='path/to/dataset/root/'
            python {0} help
    avaiable args:""".format(__file__))

    from inspect import getsource
    source = (getsource(opt.__class__))  # DefaultConfig类
    print(source)

当用户执行python main.py help的时候,会打印如下帮助信息:

    usage : python main.py <function> [--args=value]
    <function> := train | test | help
    example:
            python main.py train --env='env0701' --lr=0.01
            python main.py test --dataset='path/to/dataset/root/'
            python main.py help
    avaiable args:
class DefaultConfig(object):
    env = 'default'  # visdom 环境
    vis_port = 8097  # visdom 端口
    model = 'ResNet34'  # 使用的模型,名字必须与models/__init__.py中的名字一致   SqueezeNet -> ResNet34

    train_data_root = './data/train/'  # 训练集存放路径   default: './data/train/'
    test_data_root = './data/test1'  # 测试集存放路径     default: './data/test1'
    load_model_path = 'None'  # 加载预训练的模型的路径,为None代表不加载  default: None   ./checkpoints/resnet34_05-30_22.29.46.pth

    batch_size = 32  # batch size   default: 32  4
    use_gpu = True  # user GPU or not
    num_workers = 4  # how many workers for loading data
    print_freq = 20  # print info every N batch

    debug_file = '/tmp/debug'  # if os.path.exists(debug_file): enter ipdb
    result_file = 'result.csv'

    max_epoch = 10  # default: 10  100
    lr = 0.001  # initial learning rate
    lr_decay = 0.95  # when val_loss increase, lr = lr*lr_decay   default: 0.5
    weight_decay = 0e-5  # 损失函数  default: 0e-5    1e-5

    def _parse(self, kwargs):
        """
        根据字典kwargs 更新 config参数
        """
        # 更新配置参数
        for k, v in kwargs.items():
            if not hasattr(self, k):
                # 警告还是报错,取决于个人喜好
                warnings.warn("Warning: opt has not attribut %s" % k)
            setattr(self, k, v)

        opt.device = t.device('cuda') if opt.use_gpu else t.device('cpu')  # 原来没有这一行

        # 打印配置信息
        print('user config:')
        for k, v in self.__class__.__dict__.items():
            if not k.startswith('_'):
                print(k, getattr(self, k))

当用户执行python main.py help的时候,会打印如下帮助信息:

    usage : python main.py <function> [--args=value]
    <function> := train | test | help
    example:
            python main.py train --env='env0701' --lr=0.01
            python main.py test --dataset='path/to/dataset/root/'
            python main.py help
    avaiable args:
class DefaultConfig(object):
    env = 'default'  # visdom 环境
    vis_port = 8097  # visdom 端口
    model = 'ResNet34'  # 使用的模型,名字必须与models/__init__.py中的名字一致   SqueezeNet -> ResNet34

    train_data_root = './data/train/'  # 训练集存放路径   default: './data/train/'
    test_data_root = './data/test1'  # 测试集存放路径     default: './data/test1'
    load_model_path = 'None'  # 加载预训练的模型的路径,为None代表不加载  default: None   ./checkpoints/resnet34_05-30_22.29.46.pth

    batch_size = 32  # batch size   default: 32  4
    use_gpu = True  # user GPU or not
    num_workers = 4  # how many workers for loading data
    print_freq = 20  # print info every N batch

    debug_file = '/tmp/debug'  # if os.path.exists(debug_file): enter ipdb
    result_file = 'result.csv'

    max_epoch = 10  # default: 10  100
    lr = 0.001  # initial learning rate
    lr_decay = 0.95  # when val_loss increase, lr = lr*lr_decay   default: 0.5
    weight_decay = 0e-5  # 损失函数  default: 0e-5    1e-5

    def _parse(self, kwargs):
        """
        根据字典kwargs 更新 config参数
        """
        # 更新配置参数
        for k, v in kwargs.items():
            if not hasattr(self, k):
                # 警告还是报错,取决于个人喜好
                warnings.warn("Warning: opt has not attribut %s" % k)
            setattr(self, k, v)

        opt.device = t.device('cuda') if opt.use_gpu else t.device('cpu')  # 原来没有这一行

        # 打印配置信息
        print('user config:')
        for k, v in self.__class__.__dict__.items():
            if not k.startswith('_'):
                print(k, getattr(self, k))

5.1.11 使用

正如help函数的打印信息所述,可以通过命令行参数指定变量名.下面是三个使用例子,fire会将包含-的命令行参数自动转层下划线_,也会将非数值的值转成字符串。所以–train-data-root=data/train和–train_data_root='data/train’是等价的。

# 训练模型
python main.py train 
        --train-data-root=data/train/ 
        --lr=0.005 
        --batch-size=32 
        --model='ResNet34'  
        --max-epoch = 20

# 测试模型
python main.py test
       --test-data-root=data/test1 
       --load-model-path='checkpoints/resnet34_00:23:05.pth' 
       --batch-size=128 
       --model='ResNet34' 
       --num-workers=12

# 打印帮助信息
python main.py help

5.1.12 争议

以上的程序设计规范带有作者强烈的个人喜好,并不想作为一个标准,而是作为一个提议和一种参考。上述设计在很多地方还有待商榷,例如对于训练过程是否应该封装成一个trainer对象,或者直接封装到BaiscModule的train方法之中。对命令行参数的处理也有不少值得讨论之处。因此不要将本文中的观点作为一个必须遵守的规范,而应该看作一个参考。
本章中的设计可能会引起不少争议,其中比较值得商榷的部分主要有以下两个方面:

  • 命令行参数的设置。目前大多数程序都是使用Python标准库中的argparse来处理命令行参数,也有些使用比较轻量级的click。这种处理相对来说对命令行的支持更完备,但根据作者的经验来看,这种做法不够直观,并且代码量相对来说也较多。比如argparse,每次增加一个命令行参数,都必须写如下代码:

parser.add_argument(‘-save-interval’, type=int, default=500, help=‘how many steps to wait before saving [default:500]’)

在读者眼中,这种实现方式远不如一个专门的config.py来的直观和易用。尤其是对于使用Jupyter notebook或IPython等交互式调试的用户来说,argparse较难使用。

  • 模型训练。有不少人喜欢将模型的训练过程集成于模型的定义之中,代码结构如下所示:
  class MyModel(nn.Module):
  	
      def __init__(self,opt):
          self.dataloader = Dataloader(opt)
          self.optimizer  = optim.Adam(self.parameters(),lr=0.001)
          self.lr = opt.lr
          self.model = make_model()
      
      def forward(self,input):
          pass
      
      def train_(self):
          # 训练模型
          for epoch in range(opt.max_epoch)
          	for ii,data in enumerate(self.dataloader):
              	train_epoch()
              
          	model.save()
  	
      def train_epoch(self):
          pass

抑或是专门设计一个Trainer对象,形如:

    """
  code simplified from:
  https://github.com/pytorch/pytorch/blob/master/torch/utils/trainer/trainer.py
  """
  import heapq
  from torch.autograd import Variable

  class Trainer(object):

      def __init__(self, model=None, criterion=None, optimizer=None, dataset=None):
          self.model = model
          self.criterion = criterion
          self.optimizer = optimizer
          self.dataset = dataset
          self.iterations = 0

      def run(self, epochs=1):
          for i in range(1, epochs + 1):
              self.train()

      def train(self):
          for i, data in enumerate(self.dataset, self.iterations + 1):
              batch_input, batch_target = data
              self.call_plugins('batch', i, batch_input, batch_target)
              input_var = Variable(batch_input)
              target_var = Variable(batch_target)
    
              plugin_data = [None, None]
    
              def closure():
                  batch_output = self.model(input_var)
                  loss = self.criterion(batch_output, target_var)
                  loss.backward()
                  if plugin_data[0] is None:
                      plugin_data[0] = batch_output.data
                      plugin_data[1] = loss.data
                  return loss
    
              self.optimizer.zero_grad()
              self.optimizer.step(closure)
    
          self.iterations += i

还有一些人喜欢模仿keras和scikit-learn的设计,设计一个fit接口。对读者来说,这些处理方式很难说哪个更好或更差,找到最适合自己的方法才是最好的。

BasicModule 的封装,可多可少。训练过程中的很多操作都可以移到BasicModule之中,比如get_optimizer方法用来获取优化器,比如train_step用来执行单歩训练。对于不同的模型,如果对应的优化器定义不一样,或者是训练方法不一样,可以复写这些函数自定义相应的方法,取决于自己的喜好和项目的实际需求。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

周陽讀書

周陽也想繼往聖之絕學呀~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值