PyTorch | 构建神经网络
一、神经网络核心组件
\qquad 神经网络看起来很复杂,节点很多,层数多,参数更多。但核心部分或组件不多,把这些组件确定后,这个神经网络基本就确定了。这些核心组件包括:
- 层:神经网络的基本结构,将输入张量转换为输出张量。
- 模型:层构成的网络。
- 损失函数:参数学习的目标函数,通过最小化损失函数来学习各种参数。
- 优化器:如何使损失函数最小,这就涉及优化器。
\qquad 当然这些核心组件不是独立的,它们之间,以及它们与神经网络其他组件之间有密切关系。为便于理解,我们可以把这些关键组件及相互关系用下图表示。
\qquad 多个层链接在一起构成一个模型或网络,输入数据通过这个模型转换为预测值,然后损失函数把预测值与真实值进行比较,得到损失值(损失值可以是距离、概率值等),该损失值用于衡量预测值与目标结果的匹配或相似程度,优化器利用损失值更新权重参数,从而使损失值越来越小。这是一个循环过程,当损失值达到一个阈值或循环次数到达值定次数,循环结束。
二、如何构建神经网络?
\qquad 本文采用nn
工具箱(一文学会 torch.nn 工具包),搭建了一个神经网络。虽然步骤较多,但关键就是选择网络层,构建网络,然后选择损失和优化器。在nn
工具箱中,可以直接引用的网络很多,有全连接层、卷积层、循环层、正则化层、激活层等。
2.1 构建网络层
\qquad 本文采用 torch.nn.Sequential 来构建网络层。
2.2 前向传播
\qquad 定义好每层后,最后还需要通过前向传播的方式把这些串起来。这就是涉及如何定义forward
函数的问题。forward
函数的任务需要把输入层、网络层、输出层链接起来,实现信息的前向传导。该函数的参数一般为输入数据,返回值为输出数据。
\qquad 在forward
函数中,有些层来自nn.Module
,也可以使用nn.functional
定义。来自nn.Module
的需要实例化,而使用nn.functional
定义的可以直接使用。
2.3 反向传播
\qquad 前向传播函数定义好以后,接下来就是梯度的反向传播(如何理解神经网络的前向传播与反向传播机制?)。深度学习中涉及很多函数,如果要自己手工实现反向传播,比较费时。好在 P y T o r c h PyTorch PyTorch 提供了自动反向传播的功能,使用 n n \pmb{nn} nnnnnn 工具箱,无须我们自己编写反向传播,直接让损失函数(loss
)调用backward()
即可。
\qquad 在反向传播过程中,优化器是一个重要角色。优化方法有很多,本文采用SGD
优化器。此外,我们还可以选择其他优化器。
2.4 训练模型
\qquad 层、模型、损失函数和优化器等都定义或创建好,接下来就是训练模型。训练模型时需要注意使模型处于训练模式,即调用model.train()
,调用model.train()
会把所有的module
设置为训练模式。如果是测试或验证阶段,需要使模型处于验证阶段,即调用model.eval()
,调用model.eval()
会把所有的training
属性设置为False
。
\qquad 缺省情况下梯度是累加的,需要手工把梯度初始化或清零,调用optimizer.zero_grad()
即可。训练过程中,正向传播生成网络的输出,计算输出和实际值之间的损失值。调用loss
、backward()
自动生成梯度,然后使用optimizer.step()
执行优化器,把梯度传播回每个网络。
\qquad 如果希望使用GPU
进行训练,需要把模型、训练数据、测试数据发送到GPU
上,即调用.to(device)
。
2.5 神经网络工具箱
\qquad PyTorch | 一文学会 torch.nn 工具包 - Containers(容器)
2.6 优化器
P y T o r c h \qquad PyTorch PyTorch 常用的优化方法都封装在torch.optim
里面,其设计很灵活,可以扩展为自定义的优化方法。所有的优化方法都是继承了基类optim.Optimizer
,并实现了自己的优化步骤。最常用的优化算法就是梯度下降法及其各种变种。
\qquad 本文使用的优化器是包含动量参数Momentum
的改良版随机梯度下降法(SGD
)。
使 用 优 化 器 的 一 般 步 骤 为 : 使用优化器的一般步骤为: 使用优化器的一般步骤为:
- 建立优化器实例
\qquad 导入optim
模块,实例化SGD
优化器,这里使用动量参数momentum
(该值一般在(0,1)
之间),是SGD
的改进版,效果一般比不使用动量规则的要好。import torch.optim as optim optimizer = optim.SGD(model.parameters(),lr=lr,momentum=momentum)
- 向前传播
\qquad 把输入数据传入神经网络Net
实例化对象model
中,自动执行forward
函数,得到out
输出值,然后用out
与标记label
计算损失值loss
。out = model(img) loss = criterion(out,label)
- 清空梯度
\qquad 缺省情况梯度是累加的,在梯度反向传播前,先需把梯度清零。optimizer.zero_grad()
- 反向传播
\qquad 基于损失值,把梯度进行反向传播。loss.backward()
- 更新参数参数
\qquad 基于当前梯度(存储在参数的.grad
属性中)更新参数。optimizer.step()
2.7 动态修改学习率参数
\qquad 修改参数的方式可以通过修改参数optimizer.param_groups
或新建optimizer
。新建optimizer
比较简单,optimizer
十分轻量级,所以开销很小。但是新的优化器会初始化动量等状态信息,这对于使用动量的优化器(带有momentum
参数的SGD
)可能会造成收敛中的震荡。所以,本文直接采用修改参数optimizer.param_groups
。
\qquad optimizer.param_groups
:长度为 1 1 1 的 l i s t list list,
\qquad optimizer.param_groups[0]
:长度为 6 6 6 的字典,包括权重参数、lr
、momentum
等参数。
三、实现神经网络实例
3.1 背景说明
\qquad 本章节利用神经网络完成对手写数字进行识别的实例,来说明如何借助 n n \pmb{nn} nnnnnn 工具箱来实现一个神经网络,并对神经网络有个直观了解。在这个基础上,后续我们将对 n n \pmb{nn} nnnnnn 的各模块进行详细介绍。实例环境:PyTorch 1.9.0
,数据集MNIST
。
主要步骤:
- 利用
PyTorch
内置函数mnist
下载数据。 - 利用
torchvision
对数据进行预处理,调用torch.utils
建立一个数据迭代器。 - 可视化源数据。
- 利用
nn
工具箱构建神经网络模型。 - 实例化模型,并定义损失函数及优化器。
- 训练模型。
- 可视化结果。
\qquad 神经网络的结构如下图所示。使用两个隐含层,每层激活函数为ReLU
,最后使用torch.max(out,1)
找出张量out
最大值对应索引作为预测值。
3.2 准备数据
3.2.1 导入必要的模块
# 导入必要的模块
import numpy as np
import torch
# 导入画图工具包
import matplotlib.pyplot as plt
%matplotlib inline
# 导入PyTorch内置的mnist数据
from torchvision.datasets import mnist
# 导入预处理模块
import torchvision.transforms as transforms
from torch.utils.data import DataLoader
# 导入nn及优化器
import torch.nn.functional as F
import torch.optim as optim
from torch import nn
3.2.2 定义一些超参数
# 定义一些超参数
train_batch_size = 64 # 一次训练所抓取的数据样本数量
test_batch_size = 128 # 一次测试所抓取的数据样本数量
learning_rate = 0.01 # 学习率
num_epoches = 20 # 迭代次数
lr = 0.01
# 不恰当的初始权值可能使得网络的损失函数在训练过程中陷入局部最小值,达不到全局最优的状态
momentum = 0.9 # momentum 动量能够在一定程度上解决这个问题
3.2.3 下载数据并对数据进行预处理
\qquad 点此详细了解 t r a n s f o r m s transforms transforms 函数。
\qquad MNIST
数据集包括 6 6 6 万张 28 × 28 28\times28 28×28 的训练样本, 1 1 1 万张测试样本,我们这里使用MNIST
来进行实战。
N o r m a l i z e ( [ 0.5 ] , [ 0.5 ] ) \qquad Normalize([0.5],[0.5]) Normalize([0.5],[0.5]) 对张量进行归一化,这里两个 0.5 0.5 0.5 分别表示对张量进行归一化的全局平均值和方差。因图像是灰色的只有一个通道,如果有多个通道,需要有多个数字,如三通道,应该是 N o r m a l i z e ( [ m 1 , m 2 , m 3 ] , [ n 1 , n 2 , n 3 ] ) Normalize([m1,m2,m3],[n1,n2,n3]) Normalize([m1,m2,m3],[n1,n2,n3])。
D a t a L o a d e r \qquad DataLoader DataLoader 是 P y t o r c h Pytorch Pytorch 中用来处理模型输入数据的一个工具类。借助 D a t a L o a d e r DataLoader DataLoader,可以方便地对输入数据进行操作。函数样式:torch.utils.data.DataLoader(dataset, batch_size=1, shuffle=False, num_workers=0)
。
dataset
:加载的数据集。batch_size
:每一批训练的数据量。shuffle
:是否将数据打乱。num_workers
:使用多线程加载的线程数, 0 0 0 代表不使用多线程。
# 定义预处理函数,这些预处理依次放在Compose函数中
transform = transforms.Compose([transforms.ToTensor(),transforms.Normalize([0.5],[0.5])])
# 下载数据,并对数据进行预处理
# root:指定了数据集要存放的路径
# transform:指定导入数据集时需要进行何种变换操作
# train:设置为True说明导入的是训练集,False则为测试集。
# download:如果为True,则从internet下载数据集并将其放在root目录中。如果数据集已下载,则不会再次下载。
train_dataset = mnist.MNIST(root='./data',transform=transform,train=True,download=True)
test_dataset = mnist.MNIST(root='./data',transform=transform,train=False,download=True)
# DataLoader是一个可迭代对象,可以像使用迭代器一样使用
train_loader = DataLoader(train_dataset,batch_size=train_batch_size,shuffle=True)
test_loader = DataLoader(test_dataset,batch_size=test_batch_size,shuffle=True)
3.3 可视化源数据
- f i g u r e figure figure 函数
# num:表示图形的编号或名称,数字代表编号,字符串表示名称。
# 如果没有提供该参数,则会创建新的图形,并且增加figure的计数数值;
# 如果提供该参数,并且具有此id的图形已经存在,则会将其激活并返回对其的引用,如果图形不存在,则会创建并返回它。
# figsize:用于设置画布的尺寸,宽度、高度,以英寸为单位(1英寸等于2.54厘米)。
# dpi:用于设置图形的分辨率。
# facecolor:用于设置画板的背景颜色。
# edgecolor:用于显示边框的颜色。
# frameon:表示是否显示边框。
# FigureClass:派生自matplotlib.figure.Figure的类,可以选择使用自定义的图形对象。
# clear:若设为True且改图形已经存在,则它会被清除。
matplotlib.pyplot.figure(num=None, figsize=None, dpi=None, facecolor=None, edgecolor=None, frameon=True, FigureClass=, clear=False, **kwargs)
- s u b p l o t subplot subplot 函数
# subplot函数将整个绘图区域等分为(nrows行*ncols列)个子区域
# 然后按照从左到右,从上到下的顺序对每个子区域进行编号,左上的子区域的编号为1
# 如果nrows,ncols和index这三个数都小于10的话,可以将它们缩写为一个整数,例:subplot(323)和subplot(3,2,3)是相同的
matplotlib.pyplot.subplot(nrows, ncols, index, **kwargs)
- i m s h o w imshow imshow 函数
# 使用imshow()函数可以非常容易地制作热力图,通过色差、亮度来展示数据的差异、易于理解
# X:要绘制的图像或数组
# cmap:颜色图谱
# interpolation:插值方法
imshow(X, cmap=None, norm=None, aspect=None,interpolation=None, alpha=None, vmin=None, vmax=None, origin=None,
extent=None, shape=None, filternorm=1,filterrad=4.0, imlim=None, resample=None, url=None, **kwargs)
- 可 视 化 源 数 据 代 码 : 可视化源数据代码: 可视化源数据代码:
# enumerate函数用于将一个可遍历的数据对象(如列表、元组或字符串)组合为一个索引序列,一般用在for循环
# enumerate(sequence, [start=0]) start表示下标起始位置
# 注:iter访问迭代对象时只返回元素,enumerate除了返回元素还会返回索引
# enumerate返回值有两个,一个是序号,一个是数据(包含训练数据和标签)
examples = enumerate(train_loader)
# 使用next每次只取一个元素,并且每取完一次,迭代器的指针会后移一位
# example_data中含有64个训练样本
# example_targets中含有64个训练样本的标签
batch_idx, (example_data, example_targets) = next(examples)
fig = plt.figure()
for i in range(12):
plt.subplot(3,4,i+1)
# tight_layout会自动调整子图参数,使之填充整个图像区域
plt.tight_layout()
plt.imshow(example_data[i][0],cmap='gray',interpolation='none')
plt.title("Ground Truth:{}".format(example_targets[i]))
# matplotlib.pyplot.xticks(ticks=None, labels=None, **kwargs)
# ticks:此参数是xtick位置的列表。如果将一个空列表作为参数传递,则它将删除所有xticks
# labels:此参数包含放置在给定刻度线位置的标签。
# **kwargs:此参数是文本属性,用于控制标签的外观。
plt.xticks([])
plt.yticks([])
输出:
3.4 构建模型
\qquad 数据预处理之后,我们开始构建网络,创建模型。
3.4.1 构建网络
1. class torch.nn.Linear(in_features, out_features, bias=True)
\qquad torch.nn.Linear
类用于设置网络中的全连接层,对输入数据做线性变换: y = x A T + b y=xA^T+b y=xAT+b。torch.nn.Linear
类接收的参数有三个,分别是:in_features
—每个输入样本的特征个数、out_features
—每个输出样本的特征个数和bias
—是否使用偏置,默认值为True
。torch.nn.Linear
类含有两个变量:weight
—可学习的权值,bias
—可学习的偏置。
\qquad 全连接层的输入与输出一般都设置为二维张量,形状通常为[batch_size, size]
,不同于卷积层要求输入输出是四维张量。in_features
指的是输入的二维张量的大小,即输入的[batch_size, in_features]
中的in_features
。out_features
指的是输出的二维张量的大小,即输出的[batch_size,output_size]
中的output_size
,当然,它也代表了该全连接层的神经元个数。从输入输出的张量的shape
角度来理解,相当于一个形状为[batch_size, in_features]
的输入张量变换成了形状为[batch_size, out_features]
的输出张量。
\qquad 由此可见torch.nn.Linear(in_features, out_features, bias=True)
网络的计算过程为: [ b a t c h _ s i z e , i n _ f e a t u r e s ] ∗ [ o u t _ f e a t u r e s , i n _ f e a t u r e s ] T = [ b a t c h _ s i z e , o u t _ f e a t u r e s ] [batch\_size, in\_features]*[out\_features, in\_features]^T=[batch\_size, out\_features] [batch_size,in_features]∗[out_features,in_features]T=[batch_size,out_features]其中,[out_features , in_features]
则是weight
的形状。相应的,bias
的形状为[out_features]
。
2. class torch.nn.BatchNorm1d
\qquad 先简单理解BatchNorm
就是在深度神经网络训练过程中使得每一层神经网络的输入保持相同分布的即可。
3. 代码
class Net(nn.Module):
# 使用Sequential构建网络,Sequential函数的功能是将网络的层组合到一起
def __init__(self, in_dim, n_hidden_1, n_hidden_2, out_dim):
super(Net, self).__init__()
self.layer1 = nn.Sequential(nn.Linear(in_dim,n_hidden_1),nn.BatchNorm1d(n_hidden_1))
self.layer2 = nn.Sequential(nn.Linear(n_hidden_1,n_hidden_2),nn.BatchNorm1d(n_hidden_2))
self.layer3 = nn.Sequential(nn.Linear(n_hidden_2,out_dim),nn.BatchNorm1d(out_dim))
def forward(self, x):
x = F.relu(self.layer1(x))
x = F.relu(self.layer2(x))
x = self.layer3(x)
return x
3.4.2 实例化网络
# 检测是否有可用的GPU,有则使用,否则使用CPU
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
# 实例化网络
model = Net(28*28,300,100,10)
model.to(device)
# 定义损失函数和优化器
criterion = nn.CrossEntropyLoss() # 交叉熵损失函数CrossEntropyLoss
optimizer = optim.SGD(model.parameters(),lr=lr,momentum=momentum)
3.5 训练模型
3.5.1 训练模型
# 开始训练
train_losses = []
train_acces = []
eval_losses = []
eval_acces = []
for epoch in range(num_epoches):
train_loss = 0
train_acc = 0
model.train() # 将模型调整为训练模式
# 动态修改参数学习率
if epoch % 5 == 0:
optimizer.param_groups[0]['lr'] *= 0.9
for img, label in train_loader:
img = img.to(device) # 加载到相应的设备中
label = label.to(device)
img = img.view(img.size(0),-1) # 变换矩阵维度:[64,1,28,28]=>[64,784]
# 前向传播
out = model(img)
loss = criterion(out,label)
# 反向传播
optimizer.zero_grad() # 将梯度初始化为零
loss.backward()
optimizer.step() # 更新所有的参数
# 记录误差
train_loss += loss.item() # item()用于在只包含一个元素的tensor中提取值
# 计算分类的准确率
_, pred = out.max(1) # 返回train_batch_size个预测值
num_correct = (pred == label).sum().item() # 返回预测正确的个数
acc = num_correct / img.shape[0]
train_acc += acc
# 将训练集的错误率和准确率添加到数组
train_losses.append(train_loss / len(train_loader))
train_acces.append(train_acc / len(train_loader))
# 在测试集上检验效果
eval_loss = 0
eval_acc = 0
model.eval() # 将模型调整为训练模式
for img, label in test_loader:
img = img.to(device) # 加载到相应的设备中
label = label.to(device)
img = img.view(img.size(0),-1) # 变换矩阵维度:[64,1,28,28]=>[64,784]
# 使用训练好的模型输出结果
out = model(img)
loss = criterion(out,label)
# 记录误差
eval_loss += loss.item() # item()用于在只包含一个元素的tensor中提取值
# 计算分类的准确率
_, pred = out.max(1) # 返回train_batch_size个预测值
num_correct = (pred == label).sum().item() # 返回预测正确的个数
acc = num_correct / img.shape[0]
eval_acc += acc
# 将测试集的错误率和准确率添加到数组
eval_losses.append(eval_loss / len(test_loader))
eval_acces.append(eval_acc / len(test_loader))
print("epoch:{},Train Loss:{:.4f},Train Acc:{:.4f},Test Loss:{:.4f},Test Acc:{:.4f}".
format(epoch,train_loss / len(train_loader),train_acc / len(train_loader),
eval_loss / len(test_loader),eval_acc / len(test_loader)))
输出:
3.5.2 可视化训练及测试损失值
plt.title("trainloss")
plt.plot(np.arange(len(train_losses)),train_losses)
plt.legend(["Train Loss"],loc="upper right")
输出: