A 60 Minute Blitz 学习笔记:Neural Networks

本文是 NEURAL NETWORKS 的学习笔记

torch.nn 模块可以用于构建神经网络。

nn 模块依赖 autograd 以定义模型,并对其求微分。 一个 nn.Module 中包含了多个层(layer),和一个 forward(input) 方法用于求前向传播的 output.

以下是一个分类手写数字(图上明明给的是字母…)的简单的前馈网络:

a network that classifies digit images

一个神经网络的典型训练过程如下:

  • 定义一个包含可学习参数(权重)的神经网络
  • 遍历输入数据集
  • 将输入数据传入网络处理
  • 计算损失(loss)(即输出和正确值之间的误差)
  • 反向传播梯度给网络的参数
  • 更新网络的参数(权重),一个简单的方法是:weight = weight = learning_rate * gradient

Define the network

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
44
45
46
47
48
49
50
51
52
53
import torch
import torch.nn as nn
import torch.nn.functional as F


class (nn.Module):

def __init__(self):

super().__init__()

# 输入图像是二值图,input channel 为 1
# 第一层隐藏层有 6 个节点,output channel 为 6
# 使用的卷积核大小为 3x3
self.conv1 = nn.Conv2d(1, 6, 3)
# 第三层隐藏层有 16 个节点,output channel 为 16
# 使用的卷积核大小为 3x3
# 注意第一、第三层之间还有一个池化层, 在下面定义
self.conv2 = nn.Conv2d(6, 16, 3)

# 3 个全连接层,做简单的仿射变换:y = Wx + b
# 第一层输入大小为 16x6x6, 输出大小为 120,后面两层见图
self.fc1 = nn.Linear(16 * 6 * 6, 120)
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 10)

def forward(self, x):
# 两个池化层都是最大池化,窗口为 2x2
# 池化层如果不显式指定 stride 那么步长默认等于窗口大小
x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2))
# 如果池化窗口是正方形的,则 size 参数可以只传入边长
x = F.max_pool2d(F.relu(self.conv2(x)), 2)

# 传入全连接层之前要先把输入 reshape 为 (batch_size, size) 的形式
# 其中 size 是单个输入展平成一维的长度
x = x.view(-1, self.num_flat_features(x))
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)

return x

def num_flat_features(self, x):
# 将输入 x 一维化为 (batch_size, size) 的形状
size = x.size()[1:] # 获取除了 batch_size 以外的其他维数大小
num_features = 1
for s in size:
num_features *= s
return num_features


net = Net()
print(net)
Net(
  (conv1): Conv2d(1, 6, kernel_size=(3, 3), stride=(1, 1))
  (conv2): Conv2d(6, 16, kernel_size=(3, 3), stride=(1, 1))
  (fc1): Linear(in_features=576, out_features=120, bias=True)
  (fc2): Linear(in_features=120, out_features=84, bias=True)
  (fc3): Linear(in_features=84, out_features=10, bias=True)
)

上面的网络结构中定义了 forward 函数,而 backward 会被 autograd 自动定义。在前馈阶段可以使用任意的张量运算。

注意开头图中的模型使用的卷积核是 5x5 而上述定义的网络的卷积核是 3x3 的,因此每一层的大小会有所不同
比如图中的模型在进入全连接层之前的 shape 是 16x5x5
而上述定义的模型在进入全连接层前的 shape 是 16x6x6

图中最后一层 gussian connection 实际上是全连接层 + MES Loss,现在一般被 Softmax 所替代。参考 Definition of a “Gaussian connection”

关于 nnFunctional 的说明:

nnFunctional 模块中有很多同名或是名称类似的方法,一个粗浅的解释是:nn 中的方法是 Functional 中同名方法的类封装。

nn 中的方法在调用之前需要先实例化,如 self.fc1 = nn.Linear(16 * 6 * 6, 120),然后调用 self.fc1(x). 该全连接层的参数不需要在每次调用时传入,而是由该全连接层对象保存。

Functional 中的方法需要每次传参调用,如 x = F.max_pool2d(F.relu(self.conv2(x)), 2),适用于一些无参或者不需要保存状态的层。

一般来说,具有学习参数的层使用 nn 模块的方法,而无参或无序学习参数的层使用 Functional 方法。

dropout 层建议使用 nn 模块的方法,因为 Functionaldropout 层不会在 model.eval() 时关闭。

参考:PyTorch 中,nn 与 nn.functional 有什么区别? - 有糖吃可好的回答 - 知乎

net.parameters() 保存了模型的可学习参数:

1
2
3
4
5
6
7
8
9
10
11
# 这里一共会有 10 层的参数,分别是:
# 1-2. 卷积层 1 的权重、bias
# 3-4. 卷积层 2 的权重、bias
# 5-6. 全连接层 1 的权重、bias
# 7-8. 全连接层 2 的权重、bias
# 9-10. 全连接层 3 的权重、bias

params = list(net.parameters())
print(len(params)) # params 的长度是有可学习参数的层数
for i in params:
print(i.size())
10
torch.Size([6, 1, 3, 3])
torch.Size([6])
torch.Size([16, 6, 3, 3])
torch.Size([16])
torch.Size([120, 576])
torch.Size([120])
torch.Size([84, 120])
torch.Size([84])
torch.Size([10, 84])
torch.Size([10])

使用一个随机的输入来测试这个模型(注意这个模型的输入图片形状是 32x32,如果要将这个模型用于 MNIST 数据集,需要先 resize 图片大小)。

1
2
3
input = torch.randn(1, 1, 32, 32) # (batch_size, channel, height, width) 
out = net(input)
print(out)
tensor([[ 0.0182, -0.0672, -0.0943,  0.0062, -0.1144, -0.0432, -0.0099,  0.1065,
         -0.1105,  0.0359]], grad_fn=<AddmmBackward>)

在反向传播之前要先调用 net.zero_grad() 把模型原有的梯度清零,否则会有梯度累积的问题。参考:Why do we need to call zero_grad() in PyTorch? Why do we need to explicitly call zero_grad()?

1
2
net.zero_grad() # 将原有的梯度清零
out.backward(torch.randn(1, 10)) # 执行反向传播(这一步只计算梯度,不更新权重)

在更进一步之前,先回顾一下先前学过的类:

  • torch.Tensor - 一个支持自动梯度计算(如 backward())的多维数组,而且会保存自身的梯度
  • nn.Module - 神经网络模块。
  • nn.Parameter - Tensor 的子类。当一个 nn.Parameter 的实例被定义为一个 Module 的属性时,它会被注册到该 Module 实例的 .parameters() 中,而只有在 .parameter() 迭代器中的参数才是可训练参数。参考 torch.nn.parametersThe purpose of introducing nn.Parameter in pytorch
  • autograd.Function - 实现一个自动求导的操作的前向和反向传播的定义。每个 Tensor 的运算都会创建至少一个 Function 结点,该 Function 结点与对应的运算所创建Tensor 关联,并编码它的运算记录。

至此,我们已经学习了:

  • 如何定义一个神经网络
  • 处理输入数据和调用反向传播

剩余的内容:

  • loss 的计算
  • 更新网络的权重

Loss Function

一个损失函数(Loss Function)使用 (output, target) 作为输入,并计算一个用于评估输出值和真实值差异的值。

nn 模块中有很多不同的损失函数。一个简单的 loss 是 nn.MESLoss,即均方误差 Mean Square Error.

1
2
3
4
5
6
7
8
output = net(input)
target = torch.randn(10) # 随便设置一个 target
target = target.view(1, -1) # 让 target 和 output 有一样的 shape
# shape 的第二维长度是 10, 因为是 10 类分类
criterion = nn.MSELoss() # 实例化 MSELoss

loss = criterion(output, target) # 计算 loss
print(loss)
tensor(1.1486, grad_fn=<MseLossBackward>)

按照反向传播的方向追踪 loss, 使用其 .grad_fn 属性,可以得到以下的计算图:

input -> conv2d -> relu -> maxpool2d -> conv2d -> relu -> maxpool2d
  -> view -> linear -> relu -> linear -> relu -> linear
  -> MSELoss
  -> loss

loss.backward() 被调用时,整张图就会对该 loss 求微分,而其中 requires_grad=True 的 tensor 会在其 .grad 属性中累加梯度。

下面看几个 .grad_fn 的示例:

1
2
3
print(loss.grad_fn) # loss 是被 MSELoss 创建的
print(loss.grad_fn.next_functions[0][0]) # Linear
print(loss.grad_fn.next_functions[0][0].next_functions[0][0]) # ReLu
<MseLossBackward object at 0x7fc805de5da0>
<AddmmBackward object at 0x7fc805d6c0b8>
<AccumulateGrad object at 0x7fc861e461d0>

Backprop

调用 loss.backward() 可以将误差反向传播给输入。如果不事先清空现有的梯度,则求得的梯度会被累积。

以卷积层 1 的 bias 的梯度为例,看看其执行反向传播前后的梯度:

1
2
3
4
5
6
7
8
9
net.zero_grad() # 清空原有的梯度值

print('conv1.bias.grad before backward')
print(net.conv1.bias.grad)

loss.backward()

print('conv1.bias.grad after backward')
print(net.conv1.bias.grad)
conv1.bias.grad before backward
tensor([0., 0., 0., 0., 0., 0.])
conv1.bias.grad after backward
tensor([ 0.0028, -0.0147, -0.0008, -0.0015,  0.0013, -0.0033])

Update the weights

一个简单的更新权重的方法是使用随机梯度下降(SGD):

weight = weight - learning_rate * gradient
1
2
3
4
# 使用 python 语句的实现
learning_rate = 0.01
for f in net.parameters():
f.data.sub_(f.grad. data * learning_rate)

torch.optim 模块中提供了许多种更新权重的方法的实现:SGD、Nesterov-SGM、Adam、RMSProp 等。以 SGD 为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
import torch.optim as optim

# 实例化 optimizer
# 第一个参数是要优化的参数,这里是整个网络中的所有参数
# 第二个参数是学习率
optimizer = optim.SGD(net.parameters(), lr=0.01)

# 在训练过程中执行:
optimizer.zero_grad() # 清空原有梯度
output = net(input)
loss = criterion(output, target)
loss.backward() # 反向传播(计算梯度)
optimizer.step() # 更新权重

参考资料: