神经网络是使用torch.nn
包构造的.
nn
使用autograd定义模型和对模型做微分. 一个nn.module
对象包含layers, 一个返回输出的forward(input)
方法.
例如, 看一个分类数字图片的网络:
convnet
这是简单的前馈(feed-forward)网络. 它获取输入, 将它送给几个layer一层接一层的处理, 最后给出输出.
一个典型的神经网络训练过程如下:
weight = weight - learning_rate * gradient
定义这个网络:
import torch
import torch.nn as nn
import torch.nn.functional as F
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
# 1 input image channel, 6 output channels, 3x3 square convolution
# kernel
self.conv1 = nn.Conv2d(1, 6, 3)
self.conv2 = nn.Conv2d(6, 16, 3)
# an affine operation: y = Wx + b
self.fc1 = nn.Linear(16 * 6 * 6, 120) # 6*6 from image dimension
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 10)
def forward(self, x):
# Max pooling over a (2, 2) window
x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2))
# If the size is a square you can only specify a single number
x = F.max_pool2d(F.relu(self.conv2(x)), 2)
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):
size = x.size()[1:] # all dimensions except the batch dimension
num_features = 1
for s in size:
num_features *= s
return num_features
net = Net()
print(net)
Out:
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
时自动定义. 你可以在forward
函数中使用任意的Tensor
操作.
可训练的参数由net.parameters()
返回:
params = list(net.parameters())
print(len(params))
print(params[0].size()) # conv1's .weight
Out:
10
torch.Size([6, 1, 3, 3])
用一个随机的32x32输入试一下.
注意:
这个网络(LeNet)的期望输入size是32x32. 如果想要在MNIST数据集上使用这个网络, 需要将图像resize到32x32.
input = torch.randn(1, 1, 32, 32)
out = net(input)
print(out)
Out:
tensor([[-0.0029, 0.0154, 0.0369, 0.0380, -0.0812, -0.0062, -0.0510, -0.0086,
0.0619, 0.0218]], grad_fn=<AddmmBackward>)
将所有参数的梯度buffer清0, 用随机梯度来反向传播:
net.zero_grad()
out.backward(torch.randn(1, 10))
注意
torch.nn
只支持mini-batches. 整个torch.nn
包只支持样本的mini-batch的输入, 而不是单个的样本.
例如,nn.Conv2d
会接受4D Tensor, size是 nSamples x nChannels x Height x Width.
如果你有一个样本, 使用input.unsqueeze(0)
加入一个假的维度.
往下进行前, 先简单回顾一下.
回顾:
torch.Tensor
- 一个多维度的 array, 支持autograd操作, 例如backward(). 也能保存tensor的梯度.nn.Module
- 神经网络模块. 封装参数, 可以将参数移动到GPU, 导出, 载入等.nn.Parameter
- 一种Tensor, 作为属性赋给一个Module时, 会自动登记为训练参数.autograd.Function
-autograd
操作的forward
和backward
定义的实现. 每一个Tensor
操作会创建至少一个Function
节点. 节点连接到创建Tensor
的函数并记录运算历史.现在,我们学习了:
backward
还剩下:
loss函数 将(output,target) 作为输入, 计算一个值, 表征output距离target有多远的估计.
nn
包里有多种loss函数. 一个简单的loss是nn.MSELoss
计算output和target间的均方误差.
例如:
output = net(input)
target = torch.randn(10) # a dummy target, for example
target = target.view(1, -1) # make it the same shape as output
criterion = nn.MSELoss()
loss = criterion(output, target)
print(loss)
Out:
tensor(0.4715, grad_fn=<MseLossBackward>)
如果你使用loss的.grad_fn
属性, 在backward的方向上跟踪loss, 你会发现一个类似这样的一个计算图:
input -> conv2d -> relu -> maxpool2d -> conv2d -> relu -> maxpool2d
-> view -> linear -> relu -> linear -> relu -> linear
-> MSELoss
-> loss
所以, 当你调用loss.backward()
时, 整个图都对loss
求差分, 图中所有requires_grad=True
的Tensor
都会累加梯度.
为了清晰, 来跟踪几步backward
:
print(loss.grad_fn) # MSELoss
print(loss.grad_fn.next_functions[0][0]) # Linear
print(loss.grad_fn.next_functions[0][0].next_functions[0][0]) # ReLU
Out:
<MseLossBackward object at 0x7fa7a4719080>
<AddmmBackward object at 0x7fa7a47193c8>
<AccumulateGrad object at 0x7fa7a47193c8>
为了将误差反向传播, 我们只需要调用 loss.backward()
. 然而需要先清零存在的梯度, 否则新梯度会累加到原来的梯度上.
现在我们调用loss.backward()
, 看看conv1的bias的梯度变化.
net.zero_grad() # zeroes the gradient buffers of all parameters
print('conv1.bias.grad before backward')
print(net.conv1.bias.grad)
loss.backward()
print('conv1.bias.grad after backward')
print(net.conv1.bias.grad)
Out:
conv1.bias.grad before backward
tensor([0., 0., 0., 0., 0., 0.])
conv1.bias.grad after backward
tensor([-0.0005, 0.0012, -0.0029, 0.0029, -0.0079, 0.0023])
现在我们知道了如何使用loss函数.
稍后阅读:
nn
包有很多modules和loss函数. 全部列表见这里.
只剩下了:
最简单的更新方法是随机梯度下降法(Stochastic Gradient Descent , SGD):
weight = weight - learning_rate * gradient
用简单的Python代码实现:
learning_rate = 0.01
for f in net.parameters():
f.data.sub_(f.grad.data * learning_rate)
然而, 还有其他的更新方法: SGD, Nesterov-SGD, Adam, RMSProp等等. 我们构建了一个小包: torch.optim
来实现所有这些方法. 使用很简单:
import torch.optim as optim
# create your optimizer
optimizer = optim.SGD(net.parameters(), lr=0.01)
# in your training loop:
optimizer.zero_grad() # zero the gradient buffers
output = net(input)
loss = criterion(output, target)
loss.backward()
optimizer.step() # Does the update
注意:
注意梯度buffer是如何用optimizer.zero_grad()
手动清零的. 如反向传播部分所讲的, 这是因为梯度是累加的.
Total running time of the script: ( 0 minutes 3.761 seconds)
在上述LeNet网络定义后, 可以观察net.parameters()的组织方式:
params = list(net.parameters())
print(len(params))
for i in range(len(params)):
print(params[i].size())
Out:
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])
可以看出, 10个Tensor矩阵分别代表了各层的kernel和bias参数.