走向自动化:神经网络训练模型加入torch.nn和torch.optim

前言

本文将尝试在上一篇博客的训练模型的基础上加入torch.nn包以及torch.optim包来优化我的训练模型,使其在更少的训练次数里得到更好的优化结果。

一、torch.nn包

Pytorch中torch.nn包提供了很多实现神经网络的具体功能的类,具体本文就不展开讲了,以后肯定会涉及到,本文主要介绍在训练模型里用到的相关函数。
测试代码

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
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))

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.item()))
models.zero_grad()

loss.backward()

for param in models.parameters():
param.data -= param.grad.data * learning_rate

测试结果
1
2
3
4
5
6
7
8
9
10
Epoch:0, Loss:0.9800
Epoch:1000, Loss:0.9122
Epoch:2000, Loss:0.8534
Epoch:3000, Loss:0.8013
Epoch:4000, Loss:0.7544
Epoch:5000, Loss:0.7119
Epoch:6000, Loss:0.6727
Epoch:7000, Loss:0.6366
Epoch:8000, Loss:0.6031
Epoch:9000, Loss:0.5715

1.torch.nn.Sequential

Sequential类是一种序列容器,通过在容器中嵌套各种实现神经网络中具体功能的类,来完成神经网络模型的搭建。如果说上一篇博客提到的Variable类是将前向传播中的计算过程变成一张计算图,那么Sequential类就是将计算图封装在容器里,更为特殊的是,计算图的每一个步骤在容器里都有自己的名字,因此每一个步骤都可以当作模块单独拿出来调用。模块的命名方式可以是按照数字序列从零开始命名,也可以import一个OrderedDict包以有序字典的形式命名,这种方法的话对每个模块可以自己命名,有点类似于python列表和字典的区别。例如:

1
2
3
4
5
6
7
from collections import OrderedDict
models = torch.nn.Sequential(
OrderedDict([
("Line1", torch.nn.Linear(input_data, hidden_layer)),
("ReLU1", torch.nn.ReLU()),
("Line2", torch.nn.Linear(hidden_layer, output_data))])
)

2.torch.nn.Linear

torch.nn.Linear用于定义模型的线性层,即完成不同层之间的线性变换(如模型中输入层和隐藏层以及隐藏层和输出层的线性变换)。Linear有三个参数,分别是输入特征数,输出特征数以及是否使用偏置(默认为True)。Linear会自动生成权重参数和偏置(默认情况),因此在模型中不需要单独定义如w1w2之类的权重参数,并且Linear提供比原先自定义权重参数时使用的randn随机正太分布更好的参数初始化方法,让人放心~

3.torch.nn.ReLU

一看名字就是激活函数,先挖一个坑,以后一起讲,不讲可以打我,话放在这里了。

4.仨常见损失函数

torch.nn.MSELoss:均方误差函数
torch.nn.L1Loss:平均绝对误差函数
torch.nn.CrossEntropyLoss:交叉熵函数
仨在定义类的对象时都不需要传参,只要在使用实例时输入维度一样的参数即可计算(交叉熵函数要满足交叉熵的计算条件)

5.torch.nn.parameter

有点搞不明白,所以看了一下官方的docs:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
r"""A kind of Tensor that is to be considered a module parameter.

Parameters are :class:`~torch.Tensor` subclasses, that have a
very special property when used with :class:`Module` s - when they're
assigned as Module attributes they are automatically added to the list of
its parameters, and will appear e.g. in :meth:`~Module.parameters` iterator.
Assigning a Tensor doesn't have such effect. This is because one might
want to cache some temporary state, like last hidden state of the RNN, in
the model. If there was no such class as :class:`Parameter`, these
temporaries would get registered too.

Arguments:
data (Tensor): parameter tensor.
requires_grad (bool, optional): if the parameter requires gradient. See
:ref:`excluding-subgraphs` for more details. Default: `True`
"""

看着像是一个迭代器,也就是说在使用torch.nn包中的类进行神经网络的搭建之后,网络的参数都会保存在parameters()函数当中,访问models中的参数是对models.parameters()进行遍历完成的,然后才对每个遍历的参数进行更新。

6.???

我一开始在看代码的时候发现

1
models.zero_grad()

这个梯度置零的函数放的位置不太对,然后试着把它注释掉,结果一运行发现:
1
2
3
4
5
6
7
8
9
10
Epoch:0, Loss:1.0549
Epoch:1000, Loss:0.2391
Epoch:2000, Loss:0.2478
Epoch:3000, Loss:0.1410
Epoch:4000, Loss:0.1171
Epoch:5000, Loss:0.0837
Epoch:6000, Loss:0.0743
Epoch:7000, Loss:0.0634
Epoch:8000, Loss:0.0581
Epoch:9000, Loss:0.0590

误差降低了十倍?
这超出了我的知识范围……希望以后有能力解答这个问题……

二、torch.optim

我的训练模型到现在仍在自定义学习速率,这是因为输出层只有一个比较简单导致的。如果网络一复杂仍然自定义学习速率来进行权重参数的更新是非常不现实的,torch.optim包中正好提供了很多自动优化的类,下面介绍其中的Adam类。

测试代码

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
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))

epoch_n = 100
learning_rate = 1e-4
loss_fn = torch.nn.MSELoss()

optimizer = torch.optim.Adam(models.parameters(), lr=learning_rate)

for epoch in range(epoch_n):
y_pred = models(x)
loss = loss_fn(y_pred, y)
if epoch % 10 == 0:
print("Epoch:{}, Loss:{:.4f}".format(epoch, loss.item()))

optimizer.zero_grad()

loss.backward()

optimizer.step()

测试结果
1
2
3
4
5
6
7
8
9
10
Epoch:0, Loss:1.0926
Epoch:10, Loss:0.8934
Epoch:20, Loss:0.7386
Epoch:30, Loss:0.6166
Epoch:40, Loss:0.5177
Epoch:50, Loss:0.4355
Epoch:60, Loss:0.3654
Epoch:70, Loss:0.3050
Epoch:80, Loss:0.2525
Epoch:90, Loss:0.2068

仅仅100次就达到了比优化前10000次更好的效果

1.torch.optim.Adam

torch.optim.Adam类有两个参数,分别是需要被优化的参数和学习速率(默认为1e-2)。Adam的表现之所以这么优秀,是因为它可以做到使学习速率自适应调节,达到最好的速率。
加入优化算法,每次训练的梯度更新可以写成

1
optimizer.step()

2.???

注释掉梯度置零函数

1
optimizer.zero_grad()

好像训练效果又好了那么一点点……