Pytorch学习笔记2

书接上回,继续做笔记

神经网络的搭建

可以把以下代码当成个模板,有什么内容在里面填充即可

以下只涉及了前向反馈,是最简单的模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import torch
from torch import nn

# nn.Module:Base class for all neural network modules.
# 所有的模型都必须继承这个类
class Module(nn.Module):
def __init__(self):
super().__init__()

# 所有的模型都必须继承这个类
def forward(self,input):
# 把输入的加个1再返回出去
output = input+1
return output

# 初始化这个类
module = Module()
x = torch.tensor(1.0)
y = module(x)
print(y)

下面一一往框架里填充内容,让我们的神经网络丰富起来~

卷积层的使用

如何去理解卷积层

学过数字图像处理应该就很好理解这个概念,就是拿一个模板去遍历图像里的栅格,得到一个处理过的新图像。

更为详细的理解就不赘述了,在网上有很多很好的视频教程。

我们在官方文档的介绍里看到有很多种卷积层结构,1d即为一维卷积核,2d即为二维卷积核,我们以最常用的二维卷积核为代码实例

如何去使用卷积层

结合上文里学到的知识点做一次代码实操记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import torch
import torchvision
from torch import nn
from torch.nn import Conv2d
from torch.utils.data import DataLoader

# 引入数据集
dataset = torchvision.datasets.CIFAR10(root = './CIFAR10',train = False,transform = torchvision.transforms.ToTensor() ,download=False)

# 每次中数据集中取64张照片
dataloader = DataLoader(dataset=dataset ,batch_size = 64)

class Module(nn.Module):
def __init__(self):
super().__init__()
# 先在__init__定义好conv1之后才可以在后续中调用
# Conv2d参数含义往下看
self.conv1 = Conv2d(in_channels=3, out_channels=6, kernel_size = 3, stride=1, padding=1)

def forward(self,x):
x = self.conv1(x)
return x

module = Module()
writer = SummaryWriter('CIFAR10')

for data in dataloader:
imgs,targets = data
input = imgs
output = module(imgs)

# torch.size([64,3,32,32])
writer.add_images("Conv2d",input,1)

# torch.size([64,6,30,30]) -> [-1,3,30,30]
# add_images不支持加载6通道的Tensor,所以需要做一下reshape,reshape之后变成了[128,3,30,30]
# -1的意思是 这个位置的大小自动计算出
output = torch.reshape(output,(-1,3,32,32))
writer.add_images("Conv2d",output,2)

step += 1
writer.close()

Conv2d具体参数含义:

  • in_channels:输入通道数,一般照片都是3,在遥感影像里就要多注意
  • out_channels:卷积后的通道数
  • kernel_size:卷积核的长*宽,kernel_size = 3 即 3*3的卷积核
  • stride:卷积核每次移动的格数,默认都是1
  • padding:边缘填充格数,这决定了输入的图像尺寸大小,这个参数由卷积核的大小和输入图像尺寸决定,画图一看就很好理解,不要去背公式!

result:

这里明显可以看出,输入的图像是64张,输入变成了128张

卷积之后的图片也变得面目全非~这就算是一次特征变换吧

池化层

如何去理解池化层

池化层一般来说就是降采样。有很多种池化方式,最常见的是最大池化

以最大池化为例:将输入的图像划分为若干个矩形区域,对每个子区域输出最大值,如下图所示

从上面的例子可以看出,池化层的引入明显就是一种对于数据的压缩,为什么要引入池化层呢

  • 降维、去除冗余信息、对特征进行压缩、简化网络复杂度
  • 在一定程度上也控制了过拟合

如何去使用池化层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import torch
import torchvision
from torch import nn
from torch.nn import MaxPool2d
from torch.utils.data import DataLoader

dataset = torchvision.datasets.CIFAR10(root = './CIFAR10',train = False,transform = torchvision.transforms.ToTensor() ,download=False)
dataloader = DataLoader(dataset=dataset ,batch_size = 64)

class Module(nn.Module):
def __init__(self):
super().__init__()
# 在__init__定义好MaxPool2d
# 参数见下
self.maxP2d = MaxPool2d(kernel_size = 2, stride= 2, padding=1)

def forward(self,x):
x = self.maxP2d(x)
return x

module = Module()
writer = SummaryWriter('CIFAR10')

for data in dataloader:
imgs,targets = data
input = imgs
output = module(imgs)
# torch.size([64,3,32,32])
writer.add_images("maxpool",input,1)

# torch.size([64,3,16,16])
writer.add_images("maxpool",output,2)

writer.close()

MaxPool2d具体参数含义:基本和卷积层很像

  • kernel_size:层化核的长*宽,kernel_size =2 即 3*3的层化核
  • stride:层化核每次移动的格数
  • padding:边缘填充格数,这决定了输入的图像尺寸大小,这个参数由卷积核的大小和输入图像尺寸决定,画图一看就很好理解,不要去背公式!

图像池化之后很明显的变模糊了,因为像素格数发生了变化 [3,32,32] –> [3,16,16]

非线性激活

什么是非线性激活

就是引入激活函数,使得神经网络有拟合非线性函数的能力,这样就可以任意逼近任何非线性函数,从而应用到众多的非线性模型中。

常见的激活函数

先列着,以后深刻理解了各自的应用场景之后再来补充

Sigmoid激活函数

函数式:
导数式:

值域:(0,1)

ReLU激活函数

ReLU又叫“修正线性单元”,或者“线性整流函数”。

函数式: [公式]
导数式: [公式]
值域:[0,+∞)

激活函数的使用

就不重复写一大堆了

1
2
3
4
5
6
7
8
9
10
class Module(nn.Module):
def __init__(self):
super().__init__()
# 先声明好
self.ReLU = ReLU(False)
self.Sigmoid = Sigmoid()
# 再调用
def forward(self,x):
x = self.Sigmoid(x)
return x

这里我们用的是Sigmoid,看值域压缩到(0,1),看一下对比结果

可以发现压缩之后就仿佛蒙上了一层蒙版,雾里看图的感觉

image-20220317230130950

线性层

线性层又称为全连接层,其每个神经元与上一个层所有神经元相连,实现对前一层的线性组合或线性变换

我对线程层的理解就是最后的分类,不知道对不对

让我们简单看个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import torch
import torchvision
from torch import nn
from torch.nn import Linear
from torch.utils.data import DataLoader

dataset = torchvision.datasets.CIFAR10(root = './CIFAR10',train = False,transform = torchvision.transforms.ToTensor() ,download=False)
dataloader = DataLoader(dataset=dataset ,batch_size = 64,drop_last =True)

class Module(nn.Module):
def __init__(self):
super().__init__()
# 定义线性层
self.Linear = Linear(196608,10)

def forward(self,x):
x = self.Linear(x)
return x

module = Module()

for data in dataloader:
imgs,targets = data

# torch.size([64,3,32,32])
input = imgs
print(input.shape)

# torch.size([196608])
output = torch.flatten(imgs)
print(output.shape)

# torch.size([10])
output1 = module(output)
print(output1.shape)

writer.close()

Linear(196608,10) 第一个参数196608是由输入图像的特征所得到的

64,3,32,32 四个数相乘 = 196608,

为什么要 在module(output)之前 加一个flatten呢?就是对高维张量tensor做一个压缩,这样才能满足Linear的参数格式

Linear的用法很直接,最终打印的tensor.shape也和我们预想的一样

Sequential

Sequential就是一种简化表达

如果你的模型很复杂,有很多中间层,没有Sequential,写起来可能就是这样

1
2
3
4
5
6
7
8
9
10
11
12
class Module(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = Conv2d(in_channels = 3,out_channels= 32,kernel_size= 5,padding=2)
self.maxpool1 = MaxPool2d(2)
self.conv2 = Conv2d(in_channels = 32,out_channels= 32,kernel_size= 5,padding=2)
self.maxpool2 = MaxPool2d(2)
self.conv3 = Conv2d(in_channels = 32,out_channels= 64,kernel_size= 5,padding=2)
self.maxpool3 = MaxPool2d(2)
self.flatten = Flatten()
self.Linear1 = Linear(1024,64)
self.Linear2 = Linear(64,10)

有了Sequential,写起来就是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Module(nn.Module):
def __init__(self):
super().__init__()

self.model1 = Sequential(
Conv2d(in_channels = 3,out_channels= 32,kernel_size= 5,padding=2),
MaxPool2d(2),
Conv2d(in_channels = 32,out_channels= 32,kernel_size= 5,padding=2),
MaxPool2d(2),
Conv2d(in_channels = 32,out_channels= 64,kernel_size= 5,padding=2) ,
MaxPool2d(2),
Flatten(),
Linear(1024,64),
Linear(64,10)
)

代码的简洁明了很重要

损失函数 & 反向传播

概念介绍

神经网络的训练过程一般来说就是 在已经x和y(又称标签)的训练集中,输入一个值(x),通过一连串的网络结构(前向反馈)之后得到 预测的一个值(y’),计算y‘和y的差距(损失函数),将这个差距反馈给网络(反向传播),以此来更新网络中的参数,以上的过程被称为一个epoch;然后我们再传入一次参数,一直往复,直至我们的损失函数小到让我们接受。

损失函数就是用一个函数来衡量我们预测的值和实际的值的差距是多少

不同的损失函数,有着不同的计算方式,适用于不同的场景,常见的有最小二乘、交叉熵、平均绝对值误差

image-20220318110120972

官方文档给出了很多损失函数,以后再去慢慢探究其中的差异

反向传播是现在众多深度学习训练方法的基础,通过计算神经网络中损失函数对各参数的梯度,配合优化方法更新参数,从而降低损失函数。

介绍一下主要的优化器:

SGD,随机梯度下降

SGDM,SGD with momentum,加入动量机制的SGD

Adagrad,自适应梯度下降。利用迭代次数和累计梯度,对学习率进行自动衰减

学习率除以前t-1迭代的梯度平方和,没有考虑迭代衰减

RMSProp,在Adagrad的基础上加入了对于迭代衰减的考虑,学习率除以前t-1迭代的梯度的加权平方和。与当前迭代越近的梯度,对当前的影响应该越大。

Adam,SGDM和RMSProp的结合,基本解决了梯度下降的一系列问题,比如随机小样本、自适应学习率、容易卡在梯度较小点等问题。

(具体比较在之后补充)

代码实操

先以交叉熵SGD作为代码实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 定义要用什么损失函数
loss = nn.CrossEntropyLoss()

# 定义要用什么反向传播优化器
optimizer = optim.SGD(params = model.parameters(), lr=0.01, momentum=0.9)

# 数据训练一轮是不够的,需要多轮训练,即多个epoch
for epoch in range(10):

for data in dataloader:
imgs,targets = data
outputs = module(imgs)
result_loss = loss(outputs,targets)
# 每次优化前都对模型中的参数梯度做清零
optim.zero_grad()
# 求出每个节点的梯度
result_loss.backward()
# 对模型参数进行调优
optim.step()

optim.SGD具体参数介绍:

  • params:优化器要对哪个模型的参数做优化,这里填的是model.parameters()
  • lr:leanring rate,学习率,学习率的设置影响了模型训练的速率和好坏,需要根据实际情况做多次测试进行调整
  • momentum:动量大小,

pytorch里的SGD方法实际上就涵盖了SGD和SGDM,区别就在于是否设置momentum

设置了momentum,就是SGDM

不设置momentum,就是SGD

模型的使用及修改

官方文档里内置了很多有名的深度学习模型,当我们想直接使用的时候需用在model下调用即可

以vgg16模型为例,如何使用呢

以上简单两行代码就可以了,其中pretrained表示是否下载训练好的模型
pretrained (bool): If True, returns a model pre-trained on ImageNet
当我们打印这个模型的时候会发现,最后的输出是1000,即这个模型可以分类出1000个类别

当我们将这个模型用在自己的数据集,肯定要做一些修改,

比如我们的数据集类别只有10个类,那如何修改呢

1
2
3
4
5
6
7
8
9
10
import torchvision

vgg16_true = torchvision.models.vgg16(pretrained = True)
vgg16_false = torchvision.models.vgg16(pretrained = False)

# 在模型最下面添加一个线性层
vgg16_true.classifier.add_module("add_linear",nn.Linear(1000,10))

# 把模型最下面的一个线性层做修改
vgg16_false.classifier[6] = nn.Linear(4096,10)

网络模型的保存与读取

模型的保存和读取是一起的,各有两种方式

1
2
3
4
5
6
7
vgg16_false = torchvision.models.vgg16(pretrained = False)

# 保存方式1,可以保存下来模型的结构+模型里的参数
torch.save(obj = vgg16_false, f = "vgg16_method1.pth")
# 加载方式1
model = torch.load("vgg16_method1.pth")
print(model)
1
2
3
4
5
6
7
vgg16_true = torchvision.models.vgg16(pretrained = True)

# 保存方式2,只保存 模型的参数(推荐)
torch.save(vgg16_true.state_dict,"vgg16_method2.pth")
# 加载方式2
vgg16_true.load_state_dict(torch.load("vgg16_method2.pth"))

如何使用GPU训练

GPU是深度学习必不可少的一块内容,可以提高训练速度。

使用GPU训练,其他的地方都不用改动

只需要改动三块内容,分别是网络模型、数据和损失函数

方式一

1
2
3
4
# 网络模型
model = Module()
if torch.cuda.is_available:
model = model.cuda()
1
2
3
4
# 数据
if torch.cuda.is_available:
imgs = imgs.cuda()
targets = targets.cuda()
1
2
3
4
# 损失函数
loss_fn = nn.CrossEntropyLoss()
if torch.cuda.is_available:
loss_fn = loss_fn.cuda()

方式二

1
2
3
4
5
6
7
8
9
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# 网络模型
model.todevice(device)
# 数据
imgs.todevice(device)
targets.todevice(device)
# 损失函数
loss_fn.todevice(device)
1
2
!nvidia -smi 
这个命令查看本机的GPU配置和使用情况

到这里,pytorch主要模块的介绍就结束了。

把这些组装起来,就是一个完整简单的深度学习框架。

下篇笔记中,做一个完整的实例介绍。

参考文章