PyTorch 实例Ⅰ(全连接神经网络)

★ 利用多层全连接神经网络实现MNIST手写数字分类

前提:GPU版PyTorch已安装

查看方法:

import torch
print(torch.__version__) # 查看Pytorch版本
# 1.7.1
print(torch.cuda.is_available()) # 验证GPU版是否可用
# True

Part 1/2

定义的神经网络:单独的一个脚本文件,供后期调用 net.py

  • 注意:在Jupyter notebook中,建立.py文件的方法,可以先建立一个.txt文件,编辑完成后,改后缀为.py即可。

# 在PyTorch里面可以很简单地定义三层全连接神经网络
class simpleNet(nn.Module):
# 输入的维度,第一层网络的神经元个数、第二层网络神经元个数、以及第三层网络(输出层)神经元的个数
def __init__(self, in_dim, n_hidden_1, n_hidden_2, out_dim):
super(simpleNet, self).__init__()
self.layer1 = nn.Linear(in_dim, n_hidden_1)
self.layer2 = nn.Linear(n_hidden_1, n_hidden_2)
self.layer3 = nn.Linear(n_hidden_2, out_dim)
def forward(self, x):
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
return x

Part 2/2

添加激活函数(增加网络的非线性,只需在每层网络的输出部分增加激活函数就可以了)

  • 激活函数就是神经元内部的激活处理,最后将结果输出变成第二层网络的输入

  • 常用的激活函数:

    1. Sigmoid(会造成梯度消失;输出不是以0为均值)

    2. Tanh(仍然会造成梯度消失;输出已变成了0均值)

    3. ReLU(Rectified Linear Unit, 可以极大地加速随机梯度下降法的收敛速度;因为它是线性的, 且不存在梯度消失的问题;相比Sigmoid & Tanh, ReLU计算方法更加简单)(训练的时候很脆弱,比如一个很大的梯度,经过ReLU激活函数,更新参数之后,会使得这个神经元不会对任何数据有激活现象;本质上是一个不可逆的过程)

    4. Leaky ReLU(ReLU激活函数的变式,主要是为了修复ReLU激活函数中训练比较脆弱的这个缺点)

    5. Maxout(既有ReLU激活函数的优点,同时也避免了ReLU激活函数训练脆弱的缺点;But,它加倍了模型的参数,导致了模型的存储变大)

  • 这里只需要在每层网络的输出部分添加激活函数就可以了,用到nn.Sequential(), 这个函数是将网络的层组合到一起,比如将nn.Linear()和nn.ReLU()组合到一起作为self.layer

  • 最后一层输出层不能添加激活函数,因为输出的结果表示的是实际的得分

class Activation_Net(nn.Module):
def __init__(self, in_dim, n_hidden_1, n_hidden_2, out_dim):
super(NeuralNetwork, self).__init__()
# 将网络的层组合到一起,比如将nn.Linear() 和 nn.ReLU()组合到一起作为self.layer
self.layer1 = nn.Sequential( nn.Linear(in_dim, n_hidden_1), nn.ReLU(True) )
self.layer2 = nn.Sequential( nn.Linear(n_hidden_1, n_hidden_2), nn.ReLU(True) )
# 注意:最后一层输出层不能添加激活函数,因为输出的结果表示的是实际的得分
self.layer3 = nn.Sequential( nn.Linear(n_hidden_2, out_dim) )
def forward(self, x):
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
return x

添加批标准化(加快收敛速度的方法)

  • 【数据的预处理 和 参数的初始化】

  • 数据的预处理(PCA和白噪声在卷积网络中基本不使用,因为卷积网络可以自动学习如何提取这些特征而不需要人工再去对其进行干预)

    • 中心化(√)

    • 标准化(√)

    • PCA(主成分分析):对数据进行去相关性

    • 白噪声

  • 参数的预处理(在进入网络之前) – 权重初始化

    • 全0初始化

    • 随机初始化

    • 稀疏初始化

    • 初始化偏执

    • 批标准化(核心想法:标准化这个过程是可微的,减少了很多不合理初始化的问题;通常批标准化应用在全连接层后面、非线性层前面;实际中批标准化已变成了神经网络中的一个标准技术,特别是在卷积神经网络中,它对于很坏的初始化有很强的鲁棒性,同时还可以加快网络的收敛速度)

  • 使用nn.Sequential()将nn.BatchNorm1d()组合到网络层中

  • 注意:批标准化一般放在全连接层的后面、非线性层(激活函数)的前面

class Batch_Net(nn.Module):
def __init__(self, in_dim, n_hidden_1, n_hidden_2, out_dim):
super(Batch_Net, self).__init__()
# 注意:批标准化一般放在全连接层的后面、非线性层(激活函数)
self.layer1 = nn.Sequential( nn.Linear(in_dim, n_hidden_1), nn.BatchNorm1d(n_hidden_1), nn.ReLU(True) )
self.layer2 = nn.Sequential( nn.Linear(n_hidden_1, n_hidden_2), nn.BatchNorm1d(n_hidden_2), nn.ReLU(True) )
self.layer3 = nn.Sequential( nn.Linear(n_hidden_2, out_dim) )
def forward(self, x):
x = self.layer1(x)
x = self.layer2(x)
x = self.layer3(x)
return x
import torch
from torch import nn, optim
from torch.autograd import Variable
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
import net
batch_size = 64
learning_rate = 1e-2
num_epoches = 20
# transforms.Compose() 将各种预处理操作组合到一起,
# transforms.Normalize([0.5], [0.5]) 表示减去0.5再除以0.5,这样图片转化到了-1~1 之间
# 注意:因为图片是灰度图,所以只有一个通道,
# 如果是彩色的图片,有3通道,那么用transfomrs.Normalize([a, b, c], [d, e, f])来表示每个通道对应的均值和方差
data_tf = transforms.Compose(
[transforms.ToTensor(),
transforms.Normalize([0.5], [0.5])])

读取数据集

  • 通过PyTorch的内置函数 torchvision.datasets.MNIST导入数据集,传入数据预处理

  • 接着使用torch.utils.data.DataLoader建立一个数据迭代器,传入数据集和batch_size

train_dataset = datasets.MNIST(root = './data', train = True, transform = data_tf, download = False)
test_dataset = datasets.MNIST(root = './data', train = False, transform = data_tf)
train_loader = DataLoader(train_dataset, batch_size = batch_size, shuffle = True) # shuffle = True来表示每次迭代数据的时候是否将数据打乱
test_loader = DataLoader(test_dataset, batch_size = batch_size, shuffle = False)
# 数据集解释和查看方法 https://blog.csdn.net/qq_41185868/article/details/79094752

导入网络、定义损失函数和优化方法

  • net.simpleNet 是定义的简单三层网络,里面的参数是28x28, 300, 100, 10

  • 其中输入的维度是28x28,因为输入图片大小是28x28;

  • 然后定义两个隐藏层分别是300和100;

  • 最后输出的结果必须是10, 因为这是一个分类问题,一共有0~9这10个数字,所以是10分类

防止过拟合:

  • 正则化(L2正则化-在损失函数中增加权重的二范数量级, L1正则化-在损失函数中增加权重的1范数)

  • Dropout(核心思想:在训练网络的时候依概率P保留每个神经元,也就是说每次训练的时候有些神经元会被设置为0)


关于torch.optim(优化)

在机器学习或者深度学习中,我们需要通过修改参数使得损失函数最小化(或最大化),优化算法就是一种调整模型参数更新的策略。

  • 优化算法分为两大类:

  • 一阶优化算法(使用各个参数的梯度值来更新参数,最常用的一阶优化算法是梯度下降。所谓的梯度就是导数的多变量表达式,函数的梯度形成了一个向量场,同时也是一个方向,这个方向上方向导数最大,且等于梯度。梯度下降的功能是通过寻找最小值,控制方差,更新模型参数,最终使模型收敛。)

  • 二阶优化算法(使用了二阶导数*也叫做Hessian方法—,来最小化或最大化损失函数,主要基于牛顿法,但是由于二阶导数的计算成本很高,所以这种方法并没有广泛使用。)

torch.optim是一个实现各种优化算法的包,大多数常见的算法都能够直接通过这个包来调用,比如随机梯度下降,以及添加动量的随机梯度下降(SGD),自适应学习率等。

  • 梯度下降法

  • 梯度下降法的变式:

    • 随机梯度下降法(SGD,梯度下降法的一个小变形,每次使用一批batch数据进行梯度的计算,而不是计算全部数据的梯度-可以避免运算时间特别长、增加随机性,容易跳出局部极小点)

    • 增加动量(Momentum,参数更新不仅仅基于当前的梯度,也基于之前的梯度)& 动量的另一个变形(Nesterov, 计算经过动量更新之后的位置的梯度)

    • 自适应学习率(Adagrad,缺点是在某些情况下一直递减的学习率并不好,会造成学习过早停止)

    • 自适应学习率的改进方法(RMSprop,通过一个衰减率将所有的梯度平方变小,使用了一种滑动平均的方式,越靠前面的梯度对自适应的学习率影响越小,这样就能更加有效地避免Adagrad学习率一直递减太多的问题,能够更快地收敛。)

model = net.simpleNet(28 * 28, 300, 100, 10)
if torch.cuda.is_available():
model = model.cuda()
criterion = nn.CrossEntropyLoss() # 损失函数定义为分类问题中最常见的损失函数交叉熵
optimizer = optim.SGD(model.parameters(), lr = learning_rate) # 使用随机梯度下降(SGD)来优化损失函数

开始训练网络

  • Variable(变量):神经网络计算图里特有的一个概念,Variable提供了自动求导的功能;

    • 神经网络在做运算的时候需要先构造一个计算图谱,然后在里面进行前向传播和反向传播

    • Variable和Tensor本质上没有区别,不过Variable会被放入一个计算图中,然后进行前向传播,反向传播,自动求导

  • Variable是在 torch.autograd.Variable中,tensor -> Variable 方法,例如tensor a 变成 Variable,只需要Variable(a)即可。

epoch = 0
for data in train_loader:
img, label = data
img = img.view(img.size(0), -1)
if torch.cuda.is_available():
img = img.cuda()
label = label.cuda()
else:
img = Variable(img)
label = Variable(label)
out = model(img)
loss = criterion(out, label)
print_loss = loss.data.item()
optimizer.zero_grad()
loss.backward()
optimizer.step()
epoch += 1
if epoch % 100 == 0:
print('epoch: {}, loss: {:.4}'.format(epoch, loss.data.item()))

运行结果:

1.epoch: 100, loss: 1.596
2.epoch: 200, loss: 1.015
3.epoch: 300, loss: 0.7385
4.epoch: 400, loss: 0.5793
5.epoch: 500, loss: 0.6007
6.epoch: 600, loss: 0.4507
7.epoch: 700, loss: 0.5527
8.epoch: 800, loss: 0.3461
9.epoch: 900, loss: 0.5305

测试网络 Code Version 1.0 - From Website

model.eval()
eval_loss = 0
eval_acc = 0
for data in test_loader:
img, label = data
img = img.view(img.size(0), -1)
if torch.cuda.is_available():
img = img.cuda()
label = label.cuda()
else:
img = Variable(img)
label = Variable(label)
out = model(img)
loss = criterion(out, label)
eval_loss += loss.data.item() * label.size(0)
_, pred = torch.max(out, 1)
num_correct = (pred == label).sum()
eval_acc += num_correct.item()
print('Test Loss: {:.6f}, Acc: {:.6f}'.format(
eval_loss / (len(test_dataset)),
eval_acc / (len(test_dataset))
))

测试网络 Code Version 2.0 - From Book

model.eval()
eval_loss = 0
eval_acc = 0
for data in test_loader:
img, label = data
img = img.view(img.size(0), -1)
if torch.cuda.is_available():
# t
# volatile=True表示前向传播时不会保留缓存,因为对于测试集,不需要做反向传播,所以可以在前向传播时释放掉内存,节约内存空间
img = Variable(img, volatile=True).cuda()
label = Variable(label, volatile=True).cuda() # t
else:
img = Variable(img, volatile=True) # t
label = Variable(label, volatile=True) # t
out = model(img)
loss = criterion(out, label)
eval_loss += loss.data[0]*label.size(0) # t
_, pred = torch.max(out, 1)
num_correct = (pred == label).sum()
eval_acc += num_correct.data[0] # t
print('Test Loss: {:.6f}, Acc: {:.6f}'.format(
eval_loss / (len(test_dataset)),
eval_acc / (len(test_dataset))
))

运行结果:

1.Test Loss: 0.379588, Acc: 0.892300