Task06-pytorch实现简易神经网络

PyTorch是美国互联网巨头Facebook在深度学习框架Torch的基础上使用Python重写的一个全新的深度学习框架,它更像NumPy的替代产物,不仅继承了NumPy的众多优点,还支持GPUs计算,在计算效率上要比NumPy有更明显的优势;不仅如此,PyTorch还有许多高级功能,比如拥有丰富的API,可以快速完成深度神经网络模型的搭建和训练。所以 PyTorch一经发布,便受到了众多开发人员和科研人员的追捧和喜爱,成为AI从业者的重要工具之一。

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

张量Tensor

Tensorflow中数据的核心单元就是Tensor。张量包含了一个数据集合,这个数据集合就是原始值变形而来的,它可以是一个任何维度的数据。tensor的rank就是其维度。

Rank本意是矩阵的秩,不过Tensor Rank和Matrix Rank的意义不太一样,这里就还叫Rank。Tensor Rank的意义看起来更像是维度,比如Rank =1就是向量,Rank=2 就是矩阵了,Rank = 0 就是一个值。

1 搭建一个简易神经网络

1.1 导入包

代码的开始处是相关包的导入:

1
2
3
4
5
6
import torch

batch_n = 100
hidden_layer = 100
input_data = 1000
output_data = 10

首先通过import torch 导入必要的包,然后定义4个整型变量,其中:batch_n是在一个批次中输入数据的数量,值是100,这意味着在一个批次中输入100个数据,同时,每个数据包含的数据特征有input_data个,因为input_data的值是1000,所以每个数据的特征就是1000个,hidden_layer用于定义经过隐藏层后保留的数据特征的个数,这里有100个,因为的模型只考虑一层隐藏层,所以在代码中仅仅定义了一个隐藏层的参数;output_data是输出的数据,值是10, 可以将输出的数据看作一个分类结果值得数量,个数10表示 最后要得到10个分类结果值。

一个批次的数据从输入到输出的完整过程是:先输入100个具有1000个特征的数据,经过隐藏层后变成100个具有100个特征的数据,再经过输出层后输出100个具有10个分类结果值的数据,在得到输出结果之后计算损失并进行后向传播,这样一次模型的训练就完成了,然后训练这个流程就可以完成指定次数的训练,并达到优化模型参数的目的。

1.2 初始化权重

1
2
3
4
5
x = torch.randn(batch_n,input_data)
y = torch.randn(batch_n,output_data)

w1 = torch.randn(input_data,hidden_layer)
w2 = torch.randn(hidden_layer,output_data)

在以上的代码中定义的从输入层到隐藏层,从隐藏层到输出层对应的权重参数,同在之前说到的过程中使用的参数维度是一致的,由于 现在并没有好的权重参数的初始化方法,尽管这并不是一个好主意,可以看到,在代码中定义的输入层维度为(100,1000),输出层维度为(100,10),同时,从输入层到隐藏层的权重参数维度为(1000,100),从隐藏层到输出层的权重参数维度为(100,10),这里可能会好奇权重参数的维度是如何定义下来的,其实,只要把整个过程看作矩阵连续的乘法运算,九自然能够明白了,在代码中 的真实值y也是通过随机的方式生成的,所以一开始在使用损失函数计算损失值时得到的结果会较大。

1.3 定义训练次数和学习效率

在定义好输入,输出和权重参数值之后,就可以开始训练模型和优化权重参数了,在此之前,还需要明确训练的总次数和学习效率,代码如下:

1
2
epoch_n = 20
learning_rate = 1e-6

由于接下来会使用梯度下降的方法来优化神经网络的参数,所以必须定义后向传播的次数和梯度下降使用的学习效率。在以上代码中使用了epoch_n定义训练的次数,epoch_n的值为20,所以 需要通过循环的方式让程序进行20次训练,来完成对初始化权重参数的优化和调整。在优化的过程中使用的学习效率learning_rate的值为1e-6,表示0.000001,接下来对模型进行正式训练并对参数进行优化。

1.3 梯度下降优化神经网络的参数

下面代码通过最外层的一个大循环来保证 的模型可以进行20次训练,循环内的是神经网络模型具体的前向传播和后向传播代码。参数的优化和更新使用梯度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
for epoch in range(epoch_n):
h1 = x.mm(w1) # 100*1000
h1 = h1.clamp(min=0)
y_pred = h1.mm(w2) # 100*10
# print(y_pred)

loss = (y_pred - y).pow(2).sum()# 采用误差的平方来衡量损失
print("Epoch:{} , Loss:{:.4f}".format(epoch, loss))

gray_y_pred = 2 * (y_pred - y)#误差的平方偏导后*2
gray_w2 = h1.t().mm(gray_y_pred)

grad_h = gray_y_pred.clone()
grad_h = grad_h.mm(w2.t())
grad_h.clamp_(min=0)
grad_w1 = x.t().mm(grad_h)

w1 -= learning_rate * grad_w1
w2 -= learning_rate * gray_w2

以上代码通过最外层的一个大循环来保证 的模型可以进行20层训练,循环内的是神经网络模型具体的前向传播和后向传播代码,参数的优化和更新使用梯度下降来完成。在这个神经网络的前向传播中,通过两个连续的矩阵乘法计算出预测结果,在计算的过程中还对矩阵乘积的结果使用clamp方法进行裁剪,将小于零的值全部重新赋值于0,这就像加上了一个ReLU激活函数的功能。

前向传播得到的预测结果通过 y_pred来表示,在得到了预测值后就可以使用预测值和真实值来计算误差值了。 用loss来表示误差值,对误差值的计算使用了均方误差函数。之后的代码部分就是通过实现后向传播来对权重参数进行优化了,为了计算方便, 的代码实现使用的是每个节点的链式求导结果,在通过计算之后,就能够得到每个权重参数对应的梯度分别是grad_w1和grad_w2。在得到参数的梯度值之后,按照之前定义好的学习速率对w1和w2的权重参数进行更新,在代码中每次训练时, 都会对loss的值进行打印输出,以方便看到整个优化过程的效果,所以最后会有20个loss值被打印显示。

采用误差的平方来衡量损失,因此损失函数为:

loss=(ay) loss =(a-y)

Loss=loss2=(ay)2Loss=loss^{2}=(a-y)^{2}

根据链式法则:

z1=xw1 z_1 =x*w_1

z2=z1w2z_2=z_1*w_2

dL(ay)dw2=dLdz2dz2dw2=2(ay)z1{dL(a-y)}{dw_2}= \frac {dL}{dz_2}* \frac{dz_2}{dw_2}=2*(a-y)*z_1

dL(ay)dw1=dLdz2dz2dw2dw2dw1=2(ay)w2x\frac{dL(a-y)}{dw_1} = \frac{dL}{dz_2}* \frac{dz_2}{dw_2}* \frac{dw_2}{dw_1}=2*(a-y)*w_2*x


2 自动梯度

在上面基于PyTorch深度学习框架搭建了一个简易神经网络模型,并通过在代码中使用前向传播和后向传播实现了对这个模型的训练和对权重参数的额优化,不过该模型在结构上很简单,而且神经网络的代码也不复杂。 在实践中搭建的网络模型都是层次更深的神经网络模型,即深度神经网络模型,结构会有所变化,代码也会更复杂。对于深度的神经网络模型的前向传播使用简单的代码就能实现,但是很难实现涉及该模型中后向传播梯度计算部分的代码,其中最苦难的就是对模型计算逻辑的梳理。

在PyTorch中提供了一种非常方便的方法,可以帮助 实现对模型中后向传播梯度的自动计算,避免了“重复造轮子”,这就是接下来要学习的torch.autograd包,通过torch.autograd包,可以使模型参数自动计算在优化过程中需要用到的梯度值,在很大程度上帮助降低了实现后向传播代码的复杂度。

torch.autograd和Variable

torch.autograd包的主要功能是完成神经网络后向传播中的链式求导,手动实现链式求导的代码会给 造成很大的困扰,而torch.autograd包中丰富的类减少了这些不必要的麻烦。

实现自动梯度功能的过程大概分为以下几步:

  • 1 通过输入的Tensor数据类型的变量在神经网络的前向传播过程中生成一张计算图

  • 2 根据这个计算图和输出结果准确计算出每个参数需要更新的梯度

  • 3 通过完成后向传播完成对参数梯度的更新

在实践中完成自动梯度需要用到torch.autograd包中的Variable类对 定义的Tensor数据类型变量进行封装,在封装后,计算图中的各个节点就是一个variable 对象,这样才能应用自动梯度的功能。autograd package是PyTorch中所有神经网络的核心。先了解一些基本知识,然后开始训练第一个神经网络。autograd package提供了Tensors上所有运算的自动求导功能。它是一个按运行定义(define-by-run)的框架,这意味着反向传播是依据代码运行情况而定义的,并且每一个单次迭代都可能不相同。

autograd.Variable 是这个package的中心类。它打包了一个Tensor,并且支持几乎所有运算。一旦你完成了你的计算,可以调用.backward(),所有梯度就可以自动计算。
  你可以使用.data属性来访问原始tensor。相对于变量的梯度值可以被积累到.grad中。
  这里还有一个类对于自动梯度的执行是很重要的:Function(函数)
  变量和函数是相互关联的,并且建立一个非循环图。每一个变量有一个.grad_fn属性,它可以引用一个创建了变量的函数(除了那些用户创建的变量——他们的grad_fn是空的)。
  如果想要计算导数,可以调用Variable上的.backward()。如果变量是标量(只有一个元素),你不需要为backward()确定任何参数。但是,如果它有多个元素,你需要确定grad_output参数(这是一个具有匹配形状的tensor)。

如果已经按照如上的方式完成了相关操作,则在选中了计算图中的某个节点时,这个节点必定是一个Variable对象,用X表示 选中的节点,那么X.data代表Tensor数据类型 的变量,X.grad也是一个Variable对象,不过他代表的是X的梯度,在想访问梯度值的时候需要X.grad.data

下面通过一个自动剃度的实例来看看如何使用torch.autograd.Variable类和torch.autograd包, 同样搭建一个二层结构的神经网络模型,这有利于 之前搭建的简易神经网络模型的训练和优化过程进行对比,重新实现。

2.1 导入包

代码的开始处是相关包的导入,但是在代码中增加一行,from torch.autograd import Variable,之前定义的不变:

1
2
3
4
5
6
7
8
9
10
11
import torch
from torch.autograd import Variable

# 批量输入的数据量
batch_n = 100
# 通过隐藏层后输出的特征数
hidden_layer = 100
# 输入数据的特征个数
input_data = 1000
# 最后输出的分类结果数
output_data = 10

其中:batch_n是在一个批次中输入数据的数量,值是100,这意味着 在一个批次中输入100个数据,同时,每个数据包含的数据特征有input_data个,因为input_data的值是1000,所以每个数据的特征就是1000个,hidden_layer用于定义经过隐藏层后保留的数据特征的个数,这里有100个,因为 的模型只考虑一层隐藏层,所以在代码中仅仅定义了一个隐藏层的参数;output_data是输出的数据,值是10, 可以将输出的数据看作一个分类结果值得数量,个数10表示 最后要得到10个分类结果值。

2.2 初始化权重

1
2
3
4
5
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)

“Variable(torch.randn(batch_n, input_data), requires_grad = False)”这段代码就是之前讲到的用 Variable类对 Tensor数据类型变量进行封装的操作。在以上代码中还使用了一个requires_grad参数,这个参数的赋值类型是布尔型,如果requires_grad的值是False,那么表示该变量在进行自动梯度计算的过程中不会保留梯度值。 将输入的数据x和输出的数据y的requires_grad参数均设置为False,这是因为这两个变量并不是 的模型需要优化的参数,而两个权重w1和w2的requires_grad参数的值为True

2.3 定义训练次数和学习效率

在定义好输入,输出和权重参数值之后,就可以开始训练模型和优化权重参数了,在此之前, 还需要明确训练的总次数和学习效率,代码如下:

1
2
epoch_n = 20
learning_rate = 1e-6

和之前一样,在以上代码中使用了epoch_n定义训练的次数,epoch_n的值为20,所以 需要通过循环的方式让程序进行20次训练,来完成对初始化权重参数的优化和调整。在优化的过程中使用的学习效率learning_rate的值为1e-6,表示0.000001,接下来对模型进行正式训练并对参数进行优化。

2.4 新的模型训练和参数优化

下面代码通过最外层的一个大循环来保证 的模型可以进行20次训练,循环内的是神经网络模型具体的前向传播和后向传播代码。参数的优化和更新使用梯度

1
2
3
4
5
6
7
8
9
10
11
12
for epoch in range(epoch_n):

y_pred = x.mm(w1).clamp(min= 0 ).mm(w2)
loss = (y_pred - y).pow(2).sum()
print("Epoch:{} , Loss:{:.4f}".format(epoch, loss.data))

loss.backward()
w1.data -= learning_rate * w1.grad.data
w2.data -= learning_rate * w2.grad.data

w1.grad.data.zero_()
w2.grad.data.zero_()

和之前的代码相比,当前的代码更简洁了,之前代码中的后向传播计算部分变成了新代码中的 loss.backward(),这个函数的功能在于让模型根据计算图自动计算每个节点的梯度值并根据需求进行保留,有了这一步, 的权重参数 w1.data和 w2.data就可以直接使用在自动梯度过程中求得的梯度值w1.data.grad和w2.data.grad,并结合学习速率来对现有的参数进行更新、优化了。在代码的最后还要将本次计算得到的各个参数节点的梯度值通过grad.data.zero_()全部置零,如果不置零,则计算的梯度值会被一直累加,这样就会影响到后续的计算。同样,在整个模型的训练和优化过程中,每个循环都加入了打印loss值的操作,所以最后会得到20个loss值的打印输出。

2.5 自定义传播函数

其实除了可以采用自动梯度方法, 还可以通过构建一个继承了torch.nn.Module的新类,来完成对前向传播函数和后向传播函数的重写。在这个新类中, 使用forward作为前向传播函数的关键字,使用backward作为后向传播函数的关键字。下面介绍如何使用自定义传播函数的方法,来调整之前具备自动梯度功能的简易神经网络模型。整个代码的开始部分如下:

1
2
3
4
5
6
7
8
9
10
11
12
#_*_coding:utf-8_*_
import torch
from torch.autograd import Variable

# 批量输入的数据量
batch_n = 100
# 通过隐藏层后输出的特征数
hidden_layer = 100
# 输入数据的特征个数
input_data = 1000
# 最后输出的分类结果数
output_data = 10

和之前的代码一样,在代码的开始部分同样是导入必要的包、类,并定义需要用到的4 个变量。下面看看新的代码部分是如何定义 的前向传播 forward 函数和后向传播backward函数的:

1
2
3
4
5
6
7
8
9
10
11
12
class Model(torch.nn.Module):
def __init__(self):
super(Model,self).__init__()

def forward(self,input,w1,w2):
x = torch.mm(input,w1)
x = torch.clamp(x,min=0)
x = torch.mm(x,w2)
return x

def backward(self):
pass

以上代码展示了一个比较常用的Python类的构造方式:首先通过class Model(torch.nn.Module)完成了类继承的操作,之后分别是类的初始化,以及forward函数和backward函数。forward函数实现了模型的前向传播中的矩阵运算,backward实现了模型的后向传播中的自动梯度计算,后向传播如果没有特别的需求,则在一般情况下不用进行调整。在定义好类之后, 就可以对其进行调用了,代码如下:

1
model = Model()

这一系列操作相当于完成了对简易神经网络的搭建,然后就只剩下对模型进行训练和对参数进行优化的部分了,代码如下:

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
#_*_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)

# 训练次数设置为20
epoch_n = 20
# 将学习效率设置为0.000001
learning_rate = 1e-6

for epoch in range(epoch_n):

y_pred = x.mm(w1).clamp(min= 0 ).mm(w2)
loss = (y_pred - y).pow(2).sum()
print("Epoch:{} , Loss:{:.4f}".format(epoch, loss.data))

loss.backward()
w1.data -= learning_rate * w1.grad.data
w2.data -= learning_rate * w2.grad.data

w1.grad.data.zero_()
w2.grad.data.zero_()

这里,变量的赋值、训练次数和学习速率的定义,以及模型训练和参数优化使用的代码,和在 6.2.1节中使用的代码没有太大的差异,不同的是, 的模型通过“y_pred =model(x, w1, w2)”来完成对模型预测值的输出,并且整个训练部分的代码被简化了。在20次训练后,20个loss值的打印输出如下:

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
#_*_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)

# 训练次数设置为20
epoch_n = 20
# 将学习效率设置为0.000001
learning_rate = 1e-6

for epoch in range(epoch_n):

y_pred = x.mm(w1).clamp(min= 0 ).mm(w2)
loss = (y_pred - y).pow(2).sum()
print("Epoch:{} , Loss:{:.4f}".format(epoch, loss.data))

loss.backward()
w1.data -= learning_rate * w1.grad.data
w2.data -= learning_rate * w2.grad.data

w1.grad.data.zero_()
w2.grad.data.zero_()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Epoch:0 , Loss:42383984.0000
Epoch:1 , Loss:63978384.0000
Epoch:2 , Loss:211757952.0000
Epoch:3 , Loss:577945792.0000
Epoch:4 , Loss:296412704.0000
Epoch:5 , Loss:7378756.5000
Epoch:6 , Loss:4554581.0000
Epoch:7 , Loss:3130111.5000
Epoch:8 , Loss:2335843.0000
Epoch:9 , Loss:1857806.3750
Epoch:10 , Loss:1549259.2500
Epoch:11 , Loss:1336432.5000
Epoch:12 , Loss:1180022.3750
Epoch:13 , Loss:1058830.1250
Epoch:14 , Loss:960660.6875
Epoch:15 , Loss:878386.6250
Epoch:16 , Loss:807673.3125
Epoch:17 , Loss:745840.7500
Epoch:18 , Loss:691052.0000
Epoch:19 , Loss:642167.3125