PyTorch构建深度学习网络一般步骤
- 加 载 数 据 集 \color{red}加载数据集 加载数据集;
- 定 义 网 络 结 构 模 型 \color{red}定义网络结构模型 定义网络结构模型;
- 定 义 损 失 函 数 \color{red}定义损失函数 定义损失函数;
- 定 义 优 化 算 法 \color{red}定义优化算法 定义优化算法;
-
迭
代
训
练
\color{red}迭代训练
迭代训练;
在训练阶段主要分为四个部分。- 前向过程,计算输入到输出的结果。
- 由结果和labels计算损失函数的值。
- 后向过程,由损失计算各个变量的梯度。
- 优化器根据梯度进行参数的更新。
- 测 试 集 验 证 \color{red}测试集验证 测试集验证。
1. 配置库
#配置库
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
解释:
optim
是优化器模块,里面包括具体的优化算法有:SGD、Momentum、RMSProp、AdaGrad和Adam。其中Momentum是加速梯度下降算法,其他三个是改进学习率。常用的有:SGD和Adam。
Variable
是对tensor
的封装,用于放入计算图进行前向传播、反向传播和自动求导,是一个非常最重要的基本对象。包含三个重要属性:data
、grad
、creator
。其中:data
表示Tensor
的本身;grad
表示传播方向的梯度,creator
是创建这个Variable的Function引用
,该引用用于回溯整个创建链路。
如果是用户创建的Variable,则creator为None,同时这种Variable称为Leaf Variable,autograd只会给这种Variable分配梯度。
DataLoader
是Dataset
是一个包装类,用来将数据包装为Dataset
类,然后传入DataLoader
中,我们再使用DataLoader
这个类来更加快捷的对数据进行操作。具体来说,就是将数据集包装成特定的格式,便于处理和引用。
torchvision.transforms
是pytorch中的图像预处理模块,包含了很多种对图像数据进行变换的函数,这些都是在进行图像数据读入步骤中必不可少的。datasets
顾名思义,是一系列数据集,可以通过相应的命令加载诸如MNIST等的数据集。
2. 配置超参数
#配置参数
torch.manual_seed(1)
Learning_rate = 1e-2
Batch_Size = 16 # 没批处理的数据
Device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # 用CPU还是GPU
Epochs = 100 # 训练数据集的轮次
解释:
torch.manual_seed()
是设置随机数种子,保证程序数字的可重复性,便于测试等。batch_size
是批处理大小(批尺寸),即每次处理的数据量。 迭代次数 = 样本总数 批尺寸 \color{red}\text{迭代次数}=\frac{\text{样本总数}}{\text{批尺寸}} 迭代次数=批尺寸样本总数;在一定范围内,批尺寸越大,跑完一次全数据集(一个epoch)需要的迭代次数越少,处理数据速度也越快。但盲目增大会导致达到一定精度所需时间更大。learning_rate
为学习率。学习率越小,每次梯度下降的步伐越小,要达到目标精度所需训练次数也更多;但学习率过大会导致梯度下降不收敛,达不到学习的目的。num_epocher
是数据集训练次数,即跑几遍数据集。
3. 加载数据集
## 3.构建pipeline,对图像做处理,transform
pipeline = transforms.Compose([
transforms.ToTensor(), # 将图片转换成Tensor
transforms.Normalize((0.1307,), (0.3081, )) # 正则化,降低模型的复杂度
])
## 4.下载、加载数据
# 下载数据集
train_set = datasets.MNIST(root="data_Mnist", train=True, download=True, transform=pipeline)
test_set = datasets.MNIST(root="data_Mnist", train=False, download=True, transform=pipeline)
# 加载数据
train_loader = DataLoader(train_set, batch_size=Batch_Size, shuffle=True) # 加载图片,进行打乱
test_loader = DataLoader(test_set, batch_size=Batch_Size, shuffle=True)
解释:
- 加载datasets中的MNIST手写数字数据集,可以加载本地下载好的数据集。
root
为数据集存放的位置;train
表示是否是训练集;transform.ToTensor()
是将数据集数据归一化,将值为[0,255]
的Image转化为[0,1.0]
的Tensor数据,归一化能够提高梯度下降算法的收敛速度;downlaod
代表是否需要在线下载数据集,若本地没有则需要为True。
加载测试集,参数设置与训练集类似。
DataLoader
用于包装数据,并提供对数据的快捷处理,其中shuffle
参数为是否打乱顺序,训练集必须要打乱数据的次序,防止过拟合。
4. 定义卷积网络结构模型(重点)
##5. 构建网络模型
class Digit(nn.Module):
def __init__(self): # 定义自己的构造方法
super().__init__() # 继承构造方法
self.conv1 = nn.Conv2d(1, 10, 5) # 1:灰度图片的通道 10:输出通道 5:kernel padding默认为0,输出:10*26*26
self.conv2 = nn.Conv2d(10, 20, 3) # 10:输入通道 20:输出通道 3:Kernel padding默认为0,输出:20*24*24
self.fc1 = nn.Linear(20 * 10 * 10, 500) # 20*10*10:输入通道 500:输出通道
self.fc2 = nn.Linear(500, 10) # 500:输入通道 10:输出通道
def forward(self, x):
input_size = x.size(0) # batch_size
x = self.conv1(x) # 输入:batch*1*28*28, 输出:batch*10*24*24(28-5+1=24)
x = F.relu(x) # 激活函数shape不变
x = F.max_pool2d(x, 2, 2) # 输入:batch*10*24*24 输出:batch*10*12*12
x = self.conv2(x) # 输入:batch*10*12*12 输出:batch*20*10*10(12-3+1=10)
x = F.relu(x) #
x = x.view(input_size, -1) # view:拉平,-1 自动计算维度:20*10*10 = 2000
x = self.fc1(x) # 输入:batch*2000 输出:batch*500
x = F.relu(x) # 保持shape不变
x = self.fc2(x) # 输入: batch*500 输出:batch*10
output = F.log_softmax(x, dim=1) # 计算分类后每个数字的概率值
return output
解释:
-
nn.Moudle
是十分重要的类,包含网络各层的定义,以及forword函数的定义。 自 定 义 的 网 络 结 构 模 型 都 需 要 继 承 M o u d l e 类 \color{red}自定义的网络结构模型都需要继承Moudle类 自定义的网络结构模型都需要继承Moudle类。自 定 义 网 络 结 构 \color{blue}自定义网络结构 自定义网络结构:
- 需要继承
nn.Module
类,并实现forward
方法。 - 一般把网络中具有可学习参数的层放在构造函数
__init__()
中, - 不具有可学习参数的层(如ReLU)可放在构造函数中,也可不放在构造函数中(而在forward中使用nn.functional来代替)
- 只要在
nn.Module
的子类中定义了forward
函数,backward
函数就会被自动实现,而不需要像forword那样需要重新定义。
- 需要继承
-
super
类的作用是 继 承 \color{red}继承 继承,调用含super
的各个的基类__init__
函数。如果不使用
super
,就不会调用这些类的__init__
函数,除非显式声明。而且使用super
可以避免基类被重复调用。 -
nn.sequential
官方的解释是一个有序的容器,神经网络模块将按照在传入构造器的顺序依次被添加到计算图中执行,同时以神经网络模块为元素的有序字典也可以作为传入参数。在这里的作用是构造神经网络结构。 -
nn.Conv2d
是卷积神经网络的卷积层函数,其默认参数如下:nn.Conv2d(self, in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True))
程序中的参数解释:
-in_channels
: 输入数据通道数,RGB图片通道数为3,黑白图片的通道数1,MNIST数据集中是黑白图像,参数为1;
-out_channel
: 输出数据通道数,也称为卷积核数量,和提取的特征数量相关,这里取6;
-kennel_size
: 卷积核大小;这里取卷积核大小为3*3;
-stride
:步长,默认为1;
-padding
:填充数量,为保持输出尺寸不变,这里取1。
卷积的计算公式:
Input: ( N , C in , H in , W in ) \left(N, C_{\text {in }}, H_{\text {in }}, W_{\text {in }}\right) (N,Cin ,Hin ,Win )
Output: ( N , C out , H out , W out ) \left(N, C_{\text {out }}, H_{\text {out }}, W_{\text {out }}\right) (N,Cout ,Hout ,Wout ) where
H out = ⌊ H in + 2 × padding [ 0 ] − dilation [ 0 ] × ( k e r n e l s i z e [ 0 ] − 1 ) − 1 stride [ 0 ] + 1 ⌋ . W out = ⌊ W in + 2 × padding [ 1 ] − dilation [ 1 ] × ( k e r n e l s i z e [ 1 ] − 1 ) − 1 stride [ 1 ] + 1 ⌋ . \begin{aligned} H_{\text {out }} &=\left\lfloor\frac{H_{\text {in }}+2 \times \text { padding }[0]-\text { dilation }[0] \times(kernel\;size[0]-1)-1}{\text { stride }[0]}+1\right\rfloor.\\ W_{\text {out }} &=\left\lfloor\frac{W_{\text {in }}+2 \times \text { padding }[1]-\text { dilation }[1] \times(kernel\;size[1]-1)-1}{\text { stride }[1]}+1\right\rfloor. \end{aligned} Hout Wout =⌊ stride [0]Hin +2× padding [0]− dilation [0]×(kernelsize[0]−1)−1+1⌋.=⌊ stride [1]Win +2× padding [1]− dilation [1]×(kernelsize[1]−1)−1+1⌋.
-
nn.Relu
是激活函数,具有减少计算量,缓解过拟合的作用。 -
nn.MaxPool2d
是卷积神经网络的池化层,具有降采样,减少计算量的功能。这里采用的是最大池化层,卷积核大小为2*2
,步长为2
。池化层输出尺寸计算方法为:W
:图像宽,H
:图像高,D
:图像深度(通道数)F
:卷积核宽高,S
:步长
池化后输出图像大小:
W = ( W − F ) S + 1 H = ( H − F ) S + 1 W=\frac{(W-F)}{S}+1\\H=\frac{(H-F)}{S}+1 W=S(W−F)+1H=S(H−F)+1
输出的图像通道数(深度)不变。- 因此,程序中经过池化层后,图片尺寸为
6×14×14
,可见减少了计算量。
- 因此,程序中经过池化层后,图片尺寸为
-
经过两次卷积和池化操作后,图片最终输出尺寸为
16×5×5
的三维数组。接下来是 全 连 接 层 ( n n . l i n e r ) \color{red}全连接层(nn.liner) 全连接层(nn.liner)。之前是三维结构的特征数据,Liner层将之综合起来。- 全连接层的每一个结点都与上一层的所有结点相连,用来把前边提取到的特征综合起来。由于其全相连的特性,一般全连接层的参数也是最多的。
- 在程序中,由于前面传递过来了共
16×5×5
个节点,故其输入为400
;由于手写数字需要分为10
类,故最终输出节点数为10
。
-
forward
函数用于前向传播,将数据流前向传播。forward
函数定义后,反向传播函数backward()
会自动被定义,而不需要像前向传播一样显式定义,可直接调用。 -
model = Cnn(1,10) 将参数传入模型,并命名。
5.模型训练
## 6.定义优化器
model = Digit().to(Device)
optimizer = optim.Adam(model.parameters())
## 7.定义训练方法
def train_model(model, device, train_loader, optimizer, epoch):
# 模型训练
model.train()
for batch_index, (data, target) in enumerate(train_loader):
# 数据部署到device上去
data, target = data.to(device), target.to(device)
# 梯度初始化为0
optimizer.zero_grad()
# 训练后的结果
output = model(data)
# 计算损失
loss = F.cross_entropy(output, target)
# 找到最大的概率值,即最大的下标
pred = output.max(1, keepdim = True) # pred = output.argmax(dim=1)
# 反向传播
loss.backward()
# 参数优化
optimizer.step()
if batch_index % 3000 == 0:
print("Train Epoch:{} \t Loss: {:.6f}".format(epoch, loss.item()))
-
nn.CrossEntropyLoss
是交叉熵损失函数,用于计算损失。 -
Adam
函数是一个优化器,主要作用是优化我们的神经网络,使之在我们的训练过程中运算速度快起来,节省时间。model.parameters()是用于获取我们的神经网络参数。
-
enumerate()
是python的内置函数,用于 将 一 个 可 遍 历 的 数 据 对 象 ( 如 列 表 、 元 组 或 字 符 串 等 ) 组 合 为 一 个 索 引 序 列 \color{blue}将一个可遍历的数据对象(如列表、元组或字符串等)组合为一个索引序列 将一个可遍历的数据对象(如列表、元组或字符串等)组合为一个索引序列, 同 时 列 出 数 据 和 数 据 下 标 \color{blue}同时列出数据和数据下标 同时列出数据和数据下标。enumerate(a,start)
a是可迭代对象,start是计数起始数字,程序中是遍历训练集,从1开始计算。 -
Variable
在第一节中讲了,是对tensor
的封装,用于放入计算图进行前向传播、反向传播和自动求导,这里是将img和label两个变量进行封装。
out = model(img)是将数据传入模型,进行前向传播。 -
loss = criterion(out,label)
这里是将计算结果与标签比较,计算损失。 -
running_loss += loss.item() * label.size(0)
这里将损失累加,留作均值计算整个epoch的损失。在多维度时,loss为多个维度的均值,故需要先乘以维度数。 -
torch.max(a,1)
返回每一行中最大值的那个元素,且返回其索引(返回最大元素在这一行的列索引),这里的pred为返回的预测结果。 -
optimizer.zero_grad()
,清除掉前面累加的梯度,便于后面的反向传播。 -
loss.backward()
,反向传播。 -
optimizer.step()
,利用反向传播,更新网络模型内部的权值,从而使得模型的loss减小。
6.测试集中测试模型的识别率
def test_model(model, device, test_dataloader):
# 模型验证
model.eval()
# 正确率
correct = 0.0
# 测试损失
test_loss = 0.0
with torch.no_grad():#不会计算梯度,也不会进行方向传播
for data,target in test_loader:
# 部署到device上
data, target = data.to(device), target.to(device)
# 测试数据
output = model(data)
# 计算测试损失
test_loss += F.cross_entropy(output, target).item()
# 找到概率最大的下标
pred = output.max(1,keepdim=True)[1]#值,索引
#累计正确率
correct += pred.eq(target.view_as(pred)).sum().item()
test_loss /=len(test_loader.dataset)
print("Test -- Average Loss:{:.4f}, Accuracy:{:.3f}\n".format(test_loss, 100.0 * correct / len(test_loader.dataset)))
7. 调用
## 9. 调用 方法7/8
for epoch in range(1, Epochs +1):
train_model(model, Device, train_loader, optimizer, Epochs)
test_model(model, Device, test_loader)
8. 结果
Train Epoch:100 Loss: 0.004024
Train Epoch:100 Loss: 0.000000
Test -- Average Loss:0.0038, Accuracy:98.870
Train Epoch:100 Loss: 0.000000
Train Epoch:100 Loss: 0.010223
Test -- Average Loss:0.0037, Accuracy:98.920
Train Epoch:100 Loss: 0.000000
Train Epoch:100 Loss: 0.000252
Test -- Average Loss:0.0037, Accuracy:99.050
Train Epoch:100 Loss: 0.149225
Train Epoch:100 Loss: 0.000007
Test -- Average Loss:0.0047, Accuracy:99.110
整体代码
## 加载必要库
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
## 2. 定义超参数
Batch_Size = 16 #没批处理的数据
Device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # 用CPU还是GPU
Epochs = 100 #训练数据集的轮次
## 3. 构建pipeline,对图像做处理,transform
pipeline = transforms.Compose([
transforms.ToTensor(), # 将图片转换成Tensor
transforms.Normalize((0.1307,), (0.3081, )) # 正则化,降低模型的复杂度
])
## 4. 下载、加载数据
# 下载数据集
train_set = datasets.MNIST("data_Mnist",train = True, download=True, transform=pipeline)
test_set = datasets.MNIST("data_Mnist", train=False, download=True, transform=pipeline)
# 加载数据
train_loader = DataLoader(train_set, batch_size=Batch_Size, shuffle=True) # 加载图片,进行打乱
test_loader = DataLoader(test_set, batch_size=Batch_Size, shuffle=True)
##5. 构建网络模型
class Digit(nn.Module):
def __init__(self): # 定义自己的构造方法
super().__init__() # 继承构造方法
self.conv1 = nn.Conv2d(1, 10, 5) # 1:灰度图片的通道 10:输出通道 5:kernel
self.conv2 = nn.Conv2d(10, 20, 3) # 10:输入通道 20:输出通道 3:Kernel
self.fc1 = nn.Linear(20 * 10 * 10, 500) # 20*10*10:输入通道 500:输出通道
self.fc2 = nn.Linear(500, 10) # 500:输入通道 10:输出通道
def forward(self, x):
input_size = x.size(0) # batch_size
x = self.conv1(x) # 输入:batch*1*28*28, 输出:batch*10*24*24(28-5+1=24)
x = F.relu(x) # 激活函数shape不变
x = F.max_pool2d(x, 2, 2) # 输入:batch*10*24*24 输出:batch*10*12*12
x = self.conv2(x) # 输入:batch*10*12*12 输出:batch*20*10*10(12-3+1=10)
x = F.relu(x) #
x = x.view(input_size, -1) # view:拉平,-1 自动计算维度:20*10*10 = 2000
x = self.fc1(x) # 输入:batch*2000 输出:batch*500
x = F.relu(x) # 保持shape不变
x = self.fc2(x) # 输入: batch*500 输出:batch*10
output = F.log_softmax(x, dim=1) # 计算分类后每个数字的概率值
return output
## 6.定义优化器
model = Digit().to(Device)
optimizer = optim.Adam(model.parameters())
## 7.定义训练方法
def train_model(model, device, train_loader, optimizer, epoch):
# 模型训练
model.train()
for batch_index, (data, target) in enumerate(train_loader):
# 数据部署到device上去
data, target = data.to(device), target.to(device)
# 梯度初始化为0
optimizer.zero_grad()
# 训练后的结果
output = model(data)
# 计算损失
loss = F.cross_entropy(output, target)
# 找到最大的概率值,即最大的下标
pred = output.max(1, keepdim = True) # pred = output.argmax(dim=1)
# 反向传播
loss.backward()
# 参数优化
optimizer.step()
if batch_index % 3000 == 0:
print("Train Epoch:{} \t Loss: {:.6f}".format(epoch, loss.item()))
## 8.定义测试方法
def test_model(model, device, test_dataloader):
# 模型验证
model.eval()
# 正确率
correct = 0.0
# 测试损失
test_loss = 0.0
with torch.no_grad():#不会计算梯度,也不会进行方向传播
for data,target in test_loader:
# 部署到device上
data, target = data.to(device), target.to(device)
# 测试数据
output = model(data)
# 计算测试损失
test_loss += F.cross_entropy(output, target).item()
# 找到概率最大的下标
pred = output.max(1,keepdim=True)[1]#值,索引
#累计正确率
correct += pred.eq(target.view_as(pred)).sum().item()
test_loss /=len(test_loader.dataset)
print("Test -- Average Loss:{:.4f}, Accuracy:{:.3f}\n".format(test_loss, 100.0 * correct / len(test_loader.dataset)))
## 9. 调用 方法7/8
for epoch in range(1, Epochs +1):
train_model(model, Device, train_loader, optimizer, Epochs)
test_model(model, Device, test_loader)