深度学习之PyTorch实战(2)——神经网络模型搭建和参数优化

如果需要小编其他论文翻译,请移步小编的GitHub地址

  传送门:请点击我

  如果点击有误:https://github.com/LeBron-Jian/DeepLearningNote

  上一篇博客先搭建了基础环境,并熟悉了基础知识,本节基于此,再进行深一步的学习。

  接下来看看如何基于PyTorch深度学习框架用简单快捷的方式搭建出复杂的神经网络模型,同时让模型参数的优化方法趋于高效。如同使用PyTorch中的自动梯度方法一样,在搭建复杂的神经网络模型的时候,我们也可以使用PyTorch中已定义的类和方法,这些类和方法覆盖了神经网络中的线性变换、激活函数、卷积层、全连接层、池化层等常用神经网络结构的实现。在完成模型的搭建之后,我们还可以使用PyTorch提供的类型丰富的优化函数来完成对模型参数的优化,除此之外,还有很多防止模型在模型训练过程中发生过拟合的类。

知识储备——深度学习中的常见概念

1:批量

  批量,即Batch,是深度学习中的一个重要概念。批量通常是指两个不同的概念——如果对应的是模型训练方法,那么批量指的是将所有数据处理完以后一次性更新权重或者参数的估计,如果对应的是模型训练中的数据,那么对应的是一次输入供模型计算用的数据量。这两个概念有着紧密的关系。

  基于批量概念的模型训练通常按照如下步骤:

  (1) 初始化参数

  (2) 重复以下步骤:处理所有数据 ,更新参数

  和批量算法相对应的是递增算法,其步骤如下:

  (1) 初始化参数

  (2) 重复以下步骤:处理一个或者一组数据点,更新参数

  我们看到,这里的主要区别是批量算法一次处理所有的数据;而在递增算法中,每处理一个或者数个观测值就要更新一次参数。这里“处理”和“更新”二词根据算法的不同有不同的含义。在后向传播算法中,“处理”对应的具体操作就是在计算损失函数的梯度变化曲线。如果是批量算法,则计算平均或者总的损失函数的梯度变化曲线;而如果是递增算法,则计算损失函数仅在对应于该观测值或者数个观测值时的梯度变化曲线。“更新”则是从已有的参数值中减去梯度变化率和学习速率的乘积。

2:在线学习和离线学习

  在深度学习中,另外两个常见的概念是在线学习(Online Learning)和离线学习(Offline Learning)。在离线学习中,所有的数据都可以被反复获取,比如上面的批量学习就是离线学习的一种。而在在线学习中,每个观测值在处理以后会被遗弃,同时参数得到更新。在线学习永远是递增算法的一种,但是递增算法却既可以离线学习也可以在线学习。

  离线学习有如下几个优点。

?
1
2
3
4
5
6
7
8
9
10
1:对于任何固定个数的参数,目标函数都可以直接被计算出来,因此很容易验证模型
训练是否在朝着所需要的方向发展。
2:计算精度可以达到任意合理的程度。
3:可以使用各种不同的算法来避免出现局部最优的情况。
4:可以采用训练、验证、测试三分法对模型的普适度进行验证。
5:可以计算预测值及其置信区间。

  在线学习无法实现上述功能,因为数据并没有被存储,不能反复获取,因此对于任何固定的参数集,无法在训练集上计算损失函数,也无法在验证集上计算误差。这就造成在线算法一般来说比离线算法更加复杂和不稳定。但是离线递增算法并没有在线算法的问题,因此有必要理解在线学习和递增算法的区别。

3:偏移/阈值

  在深度学习中,采用sigmoid激活函数的隐藏层或者输出层的神经元通常在计算网络输入时加入一个偏移值,称为Bias。对于线性输出神经元,偏移项就是回归中的截距项。
  跟截距项的作用类似,偏移项可以被视为一个由特殊神经元引出的链接权重,这是因为偏移项通常链接到一个取固定单位值的偏移神经元。比如在一个多层感知器(MLP)神经网络中,某一个神经元的输入变量为N维,那么这个神经元在这个高维空间中根据参数画一个超平面,一边是正值,一边为负值。所使用的参数决定了这个超平面在输入空间中的相对位置。如果没有偏移项,这个超平面的位置就被限制住了,必须通过原点;如果多个神经元都需要其各自的超平面,那么就严重限制了模型的灵活性。这就好比一个没有截距项的回归模型,其斜率的估计值在大多数情况下会大大偏移最优估计值,因为生成的拟合曲线必须通过原点。因此,如果缺少偏移项,多层感知器的普适拟合能力就几乎不存在了。
  通常来说,每个隐藏层和输出层的神经元都有自己的偏移项。但是如果输入数据已经被等比例转换到一个有限值域中,比如[0,1]区间,那么第一个隐藏层的神经元设置了偏移项以后,后面任何层跟这些具备偏移项的神经元有链接的其他神经元就不需要再额外设置偏移项了。

4:标准化数据

  在机器学习和深度学习中,常常会出现对数据标准化这个动作。那么什么是标准化数据呢?其实这里是用“标准化”这个词代替了几个类似的但又不同的动作。下面详细讲解三个常见的“标准化”数据处理动作。

?
1
2
3
4
5
6
7
8
9
10
11
(1)重放缩(Rescaling):通常指将一个向量加上或者减去一个常量,再乘
以或者除以一个常量。比如将华氏温度转换为摄氏温度就是一个重放缩的过程。
(2)规范化(Normalization):通常指将一个向量除以其范数,比如采用欧
式空间距离,则用向量的方差作为范数来规范化向量。在深度学习中,规范化通
常采用极差为范数,即将向量减去最小值,并除以其极差,从而使数值范围在0
到1之间。
(3)标准化(Standardization):通常指将一个向量移除其位置和规模的度
量。比如一个服从正态分布的向量,可以减去其均值,并除以其方差来标准化数
据,从而获得一个服从标准正态分布的向量。

  那么在深度学习中是否应该进行以上任何一种数据处理呢?答案是依照情况而定。一般来讲,如果激活函数的值域在0到1之间,那么规范化数据到[0,1]的值域区间是比较好的。另外一个考虑是规范化数据能使计算过程更加稳定,特别是在数据值域范围区别较大的时候,规范化数据总是相对稳健的一个选择。而且很多算法的初始值设定也是针对使规范化以后的数据更有效来设计的。

一:PyTorch之torch.nn

  PyTorch中的 torch.nn包提供了很多与实现神经网络中的具体功能相关的类,这些类涵盖了深度神经网络模型在搭建和参数优化过程中的常用内容,比如神经网络中的卷积层、池化层、全连接层这类层次构造的方法、防止过拟合的参数归一化方法、Dropout 方法,还有激活函数部分的线性激活函数、非线性激活函数相关的方法,等等。在学会使用PyTorch的 torch.nn进行神经网络模型的搭建和参数优化后,我们就会发现实现一个神经网络应用并没有我们想象中那么难。

1.1 导入包

  下面使用PyTorch的torch.nn包来简化我们之前的代码,开始部分的代码变化不大,如下所示:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#_*_coding:utf-8_*_
import torch
from torch.autograd import Variable
# 批量输入的数据量
batch_n = 100
# 通过隐藏层后输出的特征数
hidden_layer = 100
# 输入数据的特征个数
input_data = 1000
# 最后输出的分类结果数
output_data = 10
x = Variable(torch.randn(batch_n , input_data) , requires_grad = False)
y = Variable(torch.randn(batch_n , output_data) , requires_grad = False)

  和之前一样,这里首先导入必要的包、类并定义了4个变量,不过这里仅定义了输入和输出的变量,之前定义神经网络模型中的权重参数的代码被删减了,这和我们之后在代码中使用的torch.nn包中的类有关,因为这个类能够帮助我们自动生成和初始化对应维度的权重参数。

之前的代码如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#_*_coding:utf-8_*_
import torch
from torch.autograd import Variable
# 批量输入的数据量
batch_n = 100
# 通过隐藏层后输出的特征数
hidden_layer = 100
# 输入数据的特征个数
input_data = 1000
# 最后输出的分类结果数
output_data = 10
x = Variable(torch.randn(batch_n , input_data) , requires_grad = False)
y = Variable(torch.randn(batch_n , output_data) , requires_grad = False)
w1 = Variable(torch.randn(input_data,hidden_layer),requires_grad = True)
w2 = Variable(torch.randn(hidden_layer,output_data),requires_grad = True)

1.2 模型搭建

?
1
2
3
4
5
6
7
8
models = torch.nn.Sequential(
    # 首先通过其完成从输入层到隐藏层的线性变换
    torch.nn.Linear(input_data,hidden_layer),
    # 经过激活函数
    torch.nn.ReLU(),
    # 最后完成从隐藏层到输出层的线性变换
    torch.nn.Linear(hidden_layer,output_data)
)

  torch.nn.Sequential括号内的内容就是我们搭建的神经网络模型的具体结构,这里首先通过torch.nn.Linear(input_data, hidden_layer)完成从输入层到隐藏层的线性变换,然后经过激活函数及torch.nn.Linear(hidden_layer, output_data)完成从隐藏层到输出层的线性变换。下面分别对在以上代码中使用的torch.nn.Sequential、torch.nn.Linear和torch.nn.RelU这三个类进行详细介绍

1.2.1 torch.nn.Sequential

  torch.nn.Sequential类是torch.nn中的一种序列容器,通过在容器中嵌套各种实现神经网络中具体功能相关的类,来完成对神经网络模型的搭建,最主要的是,参数会按照我们定义好的序列自动传递下去。我们可以将嵌套在容器中的各个部分看作各种不同的模块,这些模块可以自由组合。模块的加入一般有两种方式,一种是在以上代码中使用的直接嵌套,另一种是以orderdict有序字典的方式进行传入,这两种方式的唯一区别是,使用后者搭建的模型的每个模块都有我们自定义的名字,而前者默认使用从零开始的数字序列作为每个模块的名字。下面通过示例来直观地看一下使用这两种方式搭建的模型之间的区别。

  首先,使用直接嵌套搭建的模型代码如下:

?
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
#_*_coding:utf-8_*_
import torch
from torch.autograd import Variable
# 批量输入的数据量
batch_n = 100
# 通过隐藏层后输出的特征数
hidden_layer = 100
# 输入数据的特征个数
input_data = 1000
# 最后输出的分类结果数
output_data = 10
x = Variable(torch.randn(batch_n , input_data) , requires_grad = False)
y = Variable(torch.randn(batch_n , output_data) , requires_grad = False)
models = torch.nn.Sequential(
    # 首先通过其完成从输入层到隐藏层的线性变换
    torch.nn.Linear(input_data,hidden_layer),
    # 经过激活函数
    torch.nn.ReLU(),
    # 最后完成从隐藏层到输出层的线性变换
    torch.nn.Linear(hidden_layer,output_data)
)
print(models)

  这里对模型的结构进行打印输出,结果如下:

?
1
2
3
4
5
Sequential(
  (0): Linear(in_features=1000, out_features=100, bias=True)
  (1): ReLU()
  (2): Linear(in_features=100, out_features=10, bias=True)
)

  使用orderdict有序字典进行传入来搭建的模型代码如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#_*_coding:utf-8_*_
import torch
from torch.autograd import Variable
from collections import OrderedDict
# 批量输入的数据量
batch_n = 100
# 通过隐藏层后输出的特征数
hidden_layer = 100
# 输入数据的特征个数
input_data = 1000
# 最后输出的分类结果数
output_data = 10
models = torch.nn.Sequential(OrderedDict([
    ('Linel',torch.nn.Linear(input_data,hidden_layer)),
    ('ReLU1',torch.nn.ReLU()),
    ('Line2',torch.nn.Linear(hidden_layer,output_data))
])
)
print(models)

  这里对该模型的结构进行打印输出,结果如下:

?
1
2
3
4
5
Sequential(
  (Linel): Linear(in_features=1000, out_features=100, bias=True)
  (ReLU1): ReLU()
  (Line2): Linear(in_features=100, out_features=10, bias=True)
)

   通过对这两种方式进行比较,我们会发现,对模块使用自定义的名称可让我们更便捷地找到模型中相应的模块并进行操作。

1.2.2 torch.nn.Linear

  torch.nn.Linear类用于定义模型的线性层,即完成前面提到的不同的层之间的线性变换。torch.nn.Linear类接收的参数有三个,分别是输入特征数、输出特征数和是否使用偏置,设置是否使用偏置的参数是一个布尔值,默认为True,即使用偏置。在实际使用的过程中,我们只需将输入的特征数和输出的特征数传递给torch.nn.Linear类,就会自动生成对应维度的权重参数和偏置,对于生成的权重参数和偏置,我们的模型默认使用了一种比之前的简单随机方式更好的参数初始化方法。
  根据我们搭建模型的输入、输出和层次结构需求,它的输入是在一个批次中包含100个特征数为1000的数据,最后得到100个特征数为10的输出数据,中间需要经过两次线性变换,所以要使用两个线性层,两个线性层的代码分别是torch.nn.Linear(input_data,hidden_layer)和torch.nn.Linear(hidden_layer, output_data)。可看到,其代替了之前使用矩阵乘法方式的实现,代码更精炼、简洁。

1.2.3 torch.nn.RelU

  torch.nn.ReLU类属于非线性激活分类,在定义时默认不需要传入参数。当然,在 torch.nn包中还有许多非线性激活函数类可供选择,比如之前讲到的PReLU、LeakyReLU、Tanh、Sigmoid、Softmax等。

  在掌握torch.nn.Sequential、torch.nn.Linear和torch.nn.RelU的使用方法后,快速搭建更复杂的多层神经网络模型变为可能,而且在整个模型的搭建过程中不需要对在模型中使用到的权重参数和偏置进行任何定义和初始化说明,因为参数已经完成了自动生成。

1.3 优化模型

?
1
2
3
epoch_n = 10000
learning_rate = 1e-4
loss_fn = torch.nn.MSELoss()

  前两句代码和之前的代码没有多大区别,只是单纯地增加了学习速率和训练次数,学习速率现在是0.0001,训练次数增加到了10000次,这样做是为了让最终得到的结果更好。不过计算损失函数的代码发生了改变,现在使用的是在torch.nn包中已经定义好的均方误差函数类torch.nn.MSELoss来计算损失值,而之前的代码是根据损失函数的计算公式来编写的。

  下面简单介绍在torch.nn包中常用的损失函数的具体用法,如下所述:

1.3.1 torch.nn.MSELoss

  torch.nn.MSELoss类使用均方误差函数对损失值进行计算,在定义类的对象时不用传入任何参数,但在使用实例时需要输入两个维度一样的参数方可进行计算。示例如下

?
1
2
3
4
5
6
7
8
import torch
from torch.autograd import Variable
loss_f = torch.nn.MSELoss()
x = Variable(torch.randn(100,100))
y = Variable(torch.randn(100,100))
loss = loss_f(x,y)
print(loss)

  以上代码首先通过随机方式生成了两个维度都是(100,100)的参数,然后使用均方误差函数来计算两组参数的损失值,打印输出的结果如下:

?
1
tensor(2.0121)

1.3.2 torch.nn.L1Loss

  torch.nn.L1Loss类使用平均绝对误差函数对损失值进行计算,同样,在定义类的对象时不用传入任何参数,但在使用实例时需要输入两个维度一样的参数进行计算。示例如下:

?
1
2
3
4
5
6
7
8
import torch
from torch.autograd import Variable
loss_f = torch.nn.L1Loss()
x = Variable(torch.randn(100,100))
y = Variable(torch.randn(100,100))
loss = loss_f(x,y)
print(loss)

  以上代码也是通过随机方式生成了两个维度都是(100,100)的参数,然后使用平均绝对误差函数来计算两组参数的损失值,打印输出的结果如下:

?
1
tensor(1.1294)

1.3.3 torch.nn.CrossEntropyLoss

torch.nn.CrossEntropyLoss类用于计算交叉熵,在定义类的对象时不用传入任何参数,在使用实例时需要输入两个满足交叉熵的计算条件的参数,代码如下:

?
1
2
3
4
5
6
7
8
import torch
from torch.autograd import Variable
loss_f = torch.nn.CrossEntropyLoss()
x = Variable(torch.randn(3,5))
y = Variable(torch.LongTensor(3).random_(5))
loss = loss_f(x,y)
print(loss)

   这里生成的第1组参数是一个随机参数,维度为(3,5);第2组参数是3个范围为0~4的随机数字。计算这两组参数的损失值,打印输出的结果如下

?
1
tensor(1.6983)

  在学会使用PyTorch中的优化函数之后,我们就可以对自己建立的神经网络模型进行训练并对参数进行优化了,代码如下:

?
1
2
3
4
5
6
7
8
9
10
11
for epoch in range(epoch_n):
    y_pred = models(x)
    loss = loss_fn(y_pred,y)
    if epoch%1000 == 0:
        print('Epoch:{},Loss:{:.4f}'.format(epoch,loss.data[0]))
    models.zero_grad()
    loss.backward()
    for param in models.parameters():
        param.data -= param.grad.data*learning_rate

   以上代码中的绝大部分和之前训练和优化部分的代码是一样的,但是参数梯度更新的方式发生了改变。因为使用了不同的模型搭建方法,所以访问模型中的全部参数是通过对“models.parameters()”进行遍历完成的,然后才对每个遍历的参数进行梯度更新。其打印输入结果的方式是每完成1000次训练,就打印输出当前的loss值.

1.4 结果及分析

?
1
2
3
4
5
6
7
8
9
10
11
12
Epoch:0,Loss:1.0140
Epoch:1000,Loss:0.9409
Epoch:2000,Loss:0.8776
Epoch:3000,Loss:0.8216
Epoch:4000,Loss:0.7716
Epoch:5000,Loss:0.7263
Epoch:6000,Loss:0.6850
Epoch:7000,Loss:0.6468
Epoch:8000,Loss:0.6109
Epoch:9000,Loss:0.5773
Process finished with exit code 0

  从结果可以看出,参数的优化效果比较理想,loss值被控制在相对较小的范围之内,这和我们增强了训练次数有很大关系。

完整代码如下:

+ View Code?
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
#_*_coding:utf-8_*_
import torch
from torch.autograd import Variable
# 批量输入的数据量
batch_n = 100
# 通过隐藏层后输出的特征数
hidden_layer = 100
# 输入数据的特征个数
input_data = 1000
# 最后输出的分类结果数
output_data = 10
x = Variable(torch.randn(batch_n , input_data) , requires_grad = False)
y = Variable(torch.randn(batch_n , output_data) , requires_grad = False)
models = torch.nn.Sequential(
    # 首先通过其完成从输入层到隐藏层的线性变换
    torch.nn.Linear(input_data,hidden_layer),
    # 经过激活函数
    torch.nn.ReLU(),
    # 最后完成从隐藏层到输出层的线性变换
    torch.nn.Linear(hidden_layer,output_data)
)
# print(models)
epoch_n = 10000
learning_rate = 1e-4
loss_fn = torch.nn.MSELoss()
for epoch in range(epoch_n):
    y_pred = models(x)
    loss = loss_fn(y_pred,y)
    if epoch%1000 == 0:
        print('Epoch:{},Loss:{:.4f}'.format(epoch,loss.data[0]))
    models.zero_grad()
    loss.backward()
    for param in models.parameters():
        param.data -= param.grad.data*learning_rate

二:PyTorch之torch.optim

  到目前为止,代码中的神经网络权重的参数优化和更新还没有实现自动化,并且目前使用的优化方法都有固定的学习速率,所以优化函数相对简单,如果我们自己实现一些高级的参数优化算法,则优化函数部分的代码会变得较为复杂。在PyTorch的torch.optim包中提供了非常多的可实现参数自动优化的类,比如SGD、AdaGrad、RMSProp、Adam等,这些类都可以被直接调用,使用起来也非常方便。

2.1 优化模型

  我们使用自动化的优化函数实现方法对之前的代码进行替换,新的代码如下:

?
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
#_*_coding:utf-8_*_
import torch
from torch.autograd import Variable
# 批量输入的数据量
batch_n = 100
# 通过隐藏层后输出的特征数
hidden_layer = 100
# 输入数据的特征个数
input_data = 1000
# 最后输出的分类结果数
output_data = 10
x = Variable(torch.randn(batch_n , input_data) , requires_grad = False)
y = Variable(torch.randn(batch_n , output_data) , requires_grad = False)
models = torch.nn.Sequential(
    # 首先通过其完成从输入层到隐藏层的线性变换
    torch.nn.Linear(input_data,hidden_layer),
    # 经过激活函数
    torch.nn.ReLU(),
    # 最后完成从隐藏层到输出层的线性变换
    torch.nn.Linear(hidden_layer,output_data)
)
# print(models)
epoch_n = 20
learning_rate = 1e-4
loss_fn = torch.nn.MSELoss()
optimzer = torch.optim.Adam(models.parameters(),lr = learning_rate)

  这里使用了torch.optim包中的torch.optim.Adam类作为我们的模型参数的优化函数,在torch.optim.Adam类中输入的是被优化的参数和学习速率的初始值,如果没有输入学习速率的初始值,那么默认使用0.001这个值。因为我们需要优化的是模型中的全部参数,所以传递给torch.optim.Adam类的参数是models.parameters。另外,Adam优化函数还有一个强大的功能,就是可以对梯度更新使用到的学习速率进行自适应调节,所以最后得到的结果自然会比之前的代码更理想。

2.2 训练模型

进行模型训练的代码如下:

?
1
2
3
4
5
6
7
8
9
10
11
#进行模型训练
for epoch in range(epoch_n):
    y_pred = models(x)
    loss = loss_fn(y_pred,y)
    print('Epoch:{}, Loss:{:.4f}'.format(epoch, loss.data[0]))
    optimzer.zero_grad()
    loss.backward()
    #进行梯度更新
    optimzer.step()

在以上代码中有几处代码和之前的训练代码不同,这是因为我们引入了优化算法,所以通过直接调用optimzer.zero_grad来完成对模型参数梯度的归零;并且在以上代码中增加了optimzer.step,它的主要功能是使用计算得到的梯度值对各个节点的参数进行梯度更新。

2.3 打印结果

  这里只进行20次训练并打印每轮训练的loss值,结果如下:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Epoch:0, Loss:1.1289
Epoch:1, Loss:1.1073
Epoch:2, Loss:1.0862
Epoch:3, Loss:1.0656
Epoch:4, Loss:1.0455
Epoch:5, Loss:1.0258
Epoch:6, Loss:1.0065
Epoch:7, Loss:0.9877
Epoch:8, Loss:0.9692
Epoch:9, Loss:0.9513
Epoch:10, Loss:0.9338
Epoch:11, Loss:0.9166
Epoch:12, Loss:0.8997
Epoch:13, Loss:0.8833
Epoch:14, Loss:0.8672
Epoch:15, Loss:0.8514
Epoch:16, Loss:0.8360
Epoch:17, Loss:0.8209
Epoch:18, Loss:0.8061
Epoch:19, Loss:0.7917
Process finished with exit code 0

  在看到这个结果后我们会很惊讶,因为使用torch.optim.Adam类进行参数优化 后仅仅进行了20次训练,得到的loss值就已经远远低于之前进行10000次优化训练的 结果。所以,如果对torch.optim中的优化算法类使用得当,就更能帮助我们优化好 模型中的参数。

2.4 完整的代码如下:

+ View Code?
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
43
#_*_coding:utf-8_*_
import torch
from torch.autograd import Variable
# 批量输入的数据量
batch_n = 100
# 通过隐藏层后输出的特征数
hidden_layer = 100
# 输入数据的特征个数
input_data = 1000
# 最后输出的分类结果数
output_data = 10
x = Variable(torch.randn(batch_n , input_data) , requires_grad = False)
y = Variable(torch.randn(batch_n , output_data) , requires_grad = False)
models = torch.nn.Sequential(
    # 首先通过其完成从输入层到隐藏层的线性变换
    torch.nn.Linear(input_data,hidden_layer),
    # 经过激活函数
    torch.nn.ReLU(),
    # 最后完成从隐藏层到输出层的线性变换
    torch.nn.Linear(hidden_layer,output_data)
)
# print(models)
epoch_n = 20
learning_rate = 1e-4
loss_fn = torch.nn.MSELoss()
optimzer = torch.optim.Adam(models.parameters(),lr = learning_rate)
#进行模型训练
for epoch in range(epoch_n):
    y_pred = models(x)
    loss = loss_fn(y_pred,y)
    print('Epoch:{}, Loss:{:.4f}'.format(epoch, loss.data[0]))
    optimzer.zero_grad()
    loss.backward()
    #进行梯度更新
    optimzer.step()

三:torch和torchvision

  在PyTorch中有两个核心的包,分别是torch和torchvision。我们之前已经接触了torch包的一部分内容,比如使用了torch.nn中的线性层加激活函数配合torch.optim完成了神经网络模型的搭建和模型参数的优化,并使用了 torch.autograd实现自动梯度的功能,接下来会介绍如何使用torch.nn中的类来搭建卷积神经网络。
  torchvision包的主要功能是实现数据的处理、导入和预览等,所以如果需要对计算机视觉的相关问题进行处理,就可以借用在torchvision包中提供的大量的类来完成相应的工作。下面可以看看都导入了什么包

?
1
2
3
4
5
6
import torch
# torchvision包的主要功能是实现数据的处理,导入和预览等
import torchvision
from torchvision import datasets
from torchvision import transforms
from torch.autograd import Variable

3.1 PyTorch中的torch.transforms

  在前面讲到过,在torch.transforms中提供了丰富的类对载入的数据进行变换,现在让我们看看如何进行变换。我们知道,在计算机视觉中处理的数据集有很大一部分是图片类型的,而在PyTorch中实际进行计算的是Tensor数据类型的变量,所以我们首先需要解决的是数据类型转换的问题,如果获取的数据是格式或者大小不一的图片,则还需要进行归一化和大小缩放等操作,庆幸的是,这些方法在torch.transforms中都能找到。
  在torch.transforms中有大量的数据变换类,其中有很大一部分可以用于实现数据增强(Data Argumentation)。若在我们需要解决的问题上能够参与到模型训练中的图片数据非常有限,则这时就要通过对有限的图片数据进行各种变换,来生成新的训练集了,这些变换可以是缩小或者放大图片的大小、对图片进行水平或者垂直翻转等,都是数据增强的方法。不过在手写数字识别的问题上可以不使用数据增强的方法,因为可用于模型训练的数据已经足够了。对数据进行载入及有相应变化的代码如下:

?
1
2
3
transform = transforms.Compose([transforms.ToTensor(),
                                transforms.Normalize(mean=[0.5,0.5,0.5],
                                std=[0.5,0.5,0.5])])

  我们可以将上面代码中的torchvision.transforms.Compose类看作是一种容器,它能够同时对多种数据变换进行组合。传入的参数是一个列表,列表中的元素就是对载入的数据进行的各种变换操作。

  在以上的代码中,在torchvision.transforms.Compose类中只是用了一个类型的转换变化transfroms.ToTensor和一个数据标准化变换transforms.Normalize。这里使用的是标准化变换也叫标准差变换法,这种方法需要使用原始数据的均值(Mean)和标准差(Standard Deviation)来进行数据的标准化,在经过标准化变换之后,数据全部符合均值为0,标准差为1的标准正态分布,计算公式入选:

  不过这里我们偷了一个懒,均值和标准差的值并非来自原始数据的,而是自行定义了一个,不过仍然能够达到我们的目的。

  下面看看在torchvision.transforms中常用的数据变换操作。

3.1.1  torchvision.transforms.Resize

  用于对载入的图片数据按我们需求的大小进行缩放。传递给这个类的参数可以是一个整型数据,也可以是一个类似于(h ,w )的序列,其中,h 代表高度,w 代表宽度,但是如果使用的是一个整型数据,那么表示缩放的宽度和高度都是这个整型数据的值。

3.1.2  torchvision.transforms.Scale

  用于对载入的图片数据按我们需求的大小进行缩放,用法和torchvision.transforms.Resize类似。

3.1.3  torchvision.transforms.CenterCrop

  用于对载入的图片以图片中心为参考点,按我们需要的大小进行裁剪。传递给这个类的参数可以是一个整型数据,也可以是一个类似于(h ,w )的序列。

3.1.4  torchvision.transforms.RandomCrop

  用于对载入的图片按我们需要的大小进行随机裁剪。传递给这个类的参数可以是一个整型数据,也可以是一个类似于(h ,w )的序列。

3.1.5  torchvision.transforms.RandomHorizontalFlip

  用于对载入的图片按随机概率进行水平翻转。我们可以通过传递给这个类的参数自定义随机概率,如果没有定义,则使用默认的概率值0.5。

3.1.6  torchvision.transforms.RandomVerticalFlip

  用于对载入的图片按随机概率进行垂直翻转。我们可以通过传递给这个类的参数自定义随机概率,如果没有定义,则使用默认的概率值0.5。

3.1.7  torchvision.transforms.ToTensor

  用于对载入的图片数据进行类型转换,将之前构成PIL图片的数据转换成Tensor数据类型的变量,让PyTorch能够对其进行计算和处理。

3.1.8  torchvision.transforms.ToPILImage

  用于将Tensor变量的数据转换成PIL图片数据,主要是为了方便图片内容的显示

3.2 PyTorch中的torch.nn

  神经网络的典型处理如下所示:

  • 1,定义可学习参数的网络结构(堆叠各层和层的设计)
  • 2,数据集输入
  • 3,对输入进行处理(由定义的网络层进行处理),主要体现在网络的前向传播
  • 4,计算loss,由Loss层计算
  • 5,反向传播求梯度
  • 6,根据梯度改变参数值,最简单的实现方式(SGD)为:

weight = weight - learning_rate * gradient

  首先我们看一个卷积神经网络模型搭建的代码:

?
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
#模型搭建和参数优化
# 在顺利完成数据装载后,我们可以开始编写卷积神经网络模型的搭建和参数优化的代码
#卷积层使用torch.nn.Conv2d类来搭建
# 激活层使用torch.nn.ReLU 类方法来搭建
# 池化层使用torch.nn.MaxPool2d类方法来搭建
# 全连接层使用 torch.nn.Linear 类方法来搭建
class Model(torch.nn.Module):
    def __init__(self):
        super(Model,self).__init__()
        self.conv1 = torch.nn.Sequential(
            torch.nn.Conv2d(1,64,kernel_size=3,stride=1,padding=1),
            torch.nn.ReLU(),
            torch.nn.Conv2d(64,128,kernel_size=3,stride=1,padding=1),
            torch.nn.ReLU(),
            torch.nn.MaxPool2d(stride=2,kernel_size=2))
        self.dense = torch.nn.Sequential(
            torch.nn.Linear(14*14*128,1024),
            torch.nn.ReLU(),
            torch.nn.Dropout(p = 0.5),
            torch.nn.Linear(1024,10)
        )
    def forward(self, x):
        x = self.conv1(x)
        x = x.view(-1,14*14*128)
        x = self.dense(x)
        return x

  上面我们选择搭建了一个在结构层次上有所简化的卷积神经网络模型,在结构上使用了两个卷积层:一个最大池化层和两个全连接层,这里对具体的使用方法进行补充说明。

3.2.1 torch.nn.Conv2d

  用于搭建卷积神经网络的卷积层,主要的输入参数有输入通道数、输出通道数、卷积核大小、卷积核移动步长和Paddingde值。其中,输入通道数的数据类型是整型,用于确定输入数据的层数;输出通道数的数据类型也是整型,用于确定输出数据的层数;卷积核大小的数据类型是整型,用于确定卷积核的大小;卷积核移动步长的数据类型是整型,用于确定卷积核每次滑动的步长;Paddingde 的数据类型是整型,值为0时表示不进行边界像素的填充,如果值大于0,那么增加数字所对应的边界像素层数。

3.2.2  torch.nn.MaxPool2d

  用于实现卷积神经网络中的最大池化层,主要的输入参数是池化窗口大小、池化窗口移动步长和Padding的值。同样,池化窗口大小的数据类型是整型,用于确定池化窗口的大小。池化窗口步长的数据类型也是整型,用于确定池化窗口每次移动的步长。Padding的值和在torch.nn.Conv2d中定义的Paddingde值的用法和意义是一样的。

3.2.3  torch.nn.Dropout

  torch.nn.Dropout类用于防止卷积神经网络在训练的过程中发生过拟合,其工作原理简单来说就是在模型训练的过程中,以一定的随机概率将卷积神经网络模型的部分参数归零,以达到减少相邻两层神经连接的目的。下图显示了 Dropout方法的效果。

  在上图中的打叉的神经节点就是被随机抽中并丢弃的神经连接,正是因为选取的方式的随机性,所以在模型的每轮训练中选择丢弃的神经连接也是不同的,这样做是为了让我们最后训练出来的模型对各部分的权重参数不产生过度依赖,从而防止过拟合,对于torch.nn.Dropout类,我们可以对随机概率值的大小进行设置,如果不足任何设置,我们就使用默认的概率值0.5。

  最后说一下代码中前向传播forward函数中的内容,首先经过self.conv1进行卷积处理,然后进行x.view(-1,14*14*128),对参数实现扁平化,因为之后紧接着的就是全连接层,所以如果不进行扁平化,则全连接层的实际输出的参数维度和其定义输入的维度将不匹配,程序将会报错,最后通过self.dense定义的全连接层进行最后的分类。

3.2.4  pytorch中 前向传播函数forward的使用与解释

  从上面代码可以看到,不论是在定义网络结构还是定义网络层的操作(Op),均需要定义 forward 函数,下面学习。

  首先看 forward的使用流程,以一个 Module为例:

  • 1,调用 module 的 call  方法
  • 2,module 的 call 里面调用 module 的 forward 方法
  • 3,forward里面如果碰到Module的子类,回到第一步,如果碰到Function的子类,继续往下
  • 4,调用 Function的call方法
  • 5,Function的call方法调用了 Function的forward方法
  • 6,Function的forward返回值
  • 7,module的forward返回值
  • 8,在module的 call进行 forward_hook 操作,然后返回值

  上述中调用module 的 call 方法是指 nn.Module 的 __call__ 方法的类可以当做函数调用,具体参考Python的面向对象编程。也就是说 ,当把定义的网络模型 model 当做函数调用的时候就自动调用定义的网络模型 forward方法。

  nn.Module 的 __call__ 方法部分源码如下:

?
1
2
3
4
5
6
7
def __call__(self, *input, **kwargs):
   result = self.forward(*input, **kwargs)
   for hook in self._forward_hooks.values():
       #将注册的hook拿出来用
       hook_result = hook(self, input, result)
   ...
   return result

  可以看到,当执行 model(x) 的时候,底层自动调用 forward 方法计算结果。

  下面举例说明:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Module(nn.Module):
    def __init__(self):
        super(Module, self).__init__()
        # ......
        
    def forward(self, x):
        # ......
        return x
data = .....  #输入数据
# 实例化一个对象
module = Module()
# 前向传播
module(data) 
# 而不是使用下面的
# module.forward(data)  

  实际上 module(data) 是等价于  module.forward(data)。等价的原因是因为 pyhon class中的 __call__ 和 __init__方法。

?
1
2
3
4
5
6
7
8
class A():
    def __call__(self):
        print('i can be called like a function')
a = A()
a()
# output
# i can be called like a function

  __call__ 里调用其他的函数

?
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
class A():
    def __call__(self, param):
        print('i can called like a function')
        print('传入参数的类型是:{}   值为: {}'.format(type(param), param))
        res = self.forward(param)
        return res
    def forward(self, input_):
        print('forward 函数被调用了')
        print('in  forward, 传入参数类型是:{}  值为: {}'.format(type(input_), input_))
        return input_
a = A()
input_param = a('i')
print('对象a传入的参数是:', input_param)
'''
output:
        i can called like a function
        传入参数的类型是:<class 'str'>   值为: i
        forward 函数被调用了
        in  forward, 传入参数类型是:<class 'str'>  值为: i
        对象a传入的参数是: i
'''

3.2.5  pytorch中 x = x.view(x.size(0), -1) 的理解

  这句话一般出现在model类的forward函数中,具体位置一般都是在调用分类器之前(可以参考之前的代码),分类器是一个简单的 nn.Linear() 结构,输入输出都是维度为1的值, x = x.view(x.size(0), -1) 这句话的出现就是为了将前面多维度的 tensor 展平成一维。

  下面写一个简单的例子,我们根据这个解析:

?
1
2
3
4
5
6
7
8
9
10
class NET(nn.Module):
    def __init__(self,batch_size):
        super(NET,self).__init__()
        self.conv = nn.Conv2d(outchannels=3,in_channels=64,kernel_size=3,stride=1)
        self.fc = nn.Linear(64*batch_size,10)
  
    def forward(self,x):
        x = self.conv(x)
        x = x.view(x.size(0), -1) 
        out = self.fc(x)

  上面是个简单的网络结构,包含一个卷积层和一个分类层。forward()函数中,input首先经过卷积层,此时的输出 x 是包含 batchsize 维度为4的tensor,即(batchsize, channels, x, y),x.size(0) 指 batchsize 的值。 x = x.view(x.size(0), -1) 简化为  x = x.view(batchsize, -1)。

  view() 函数的功能跟 reshape类似,用来转换size大小。x = x.view(batchsize, -1) 中的 batchsize 指转换后有几行,而 -1 指在不告诉函数有多少列的情况下,根据原tensor数据和batchsize自动分配列数。

参考地址:https://blog.csdn.net/u011501388/article/details/84062483

(0)

相关推荐