神经网络

convnet

卷积网

这是一个简单的前馈网络。 它获取输入,将其一层又一层地馈入,然后最终给出输出。

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

  • 定义具有一些可学习参数(或权重)的神经网络
  • 遍历输入数据集
  • 通过网络处理输入
  • 计算损失(输出正确的距离有多远)
  • 将梯度传播回网络参数
  • 通常使用简单的更新规则来更新网络的权重: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
30
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个输入图像通道,6个输出通道,5x5平方卷积
# kernel 内核
self.conv1 = nn.Conv2d(1, 6, 5)
self.conv2 = nn.Conv2d(6, 16, 5)
# an affine operation: y = Wx + b
self.fc1 = nn.Linear(16 * 5 * 5, 120) # 图像尺寸为5*5
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 10)

def forward(self, x):
# (2, 2)窗口上的最大池化
x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2))
# 如果大小是正方形,则可以指定一个数字
x = F.max_pool2d(F.relu(self.conv2(x)), 2)
# 压平除批量尺寸外的所有尺寸
x = torch.flatten(x, 1)
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x

net = Net()
print(net)

super() 是用来解决多重继承问题的,直接用类名调用父类方法在使用单继承的时候没问题,但是如果使用多继承,会涉及到查找顺序(MRO)、重复调用(钻石继承)等种种问题。

output:

1
2
3
4
5
6
7
Net(
(conv1): Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1))
(conv2): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1))
(fc1): Linear(in_features=400, 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函数,就可以使用autograd自动定义backward函数(计算梯度)。

模型的可学习参数由net.parameters()返回

1
2
3
params = list(net.parameters())
print(len(params))
print(params[0].size()) # conv1's .weight

output:

1
2
10
torch.Size([6, 1, 5, 5])

让我们尝试一个32x32随机输入。

注意:该网络的预期输入大小(LeNet)为32x32。 要在 MNIST 数据集 上使用此网络,请将图像从数据集中调整为32x32

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
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个输入图像通道,6个输出通道,5x5平方卷积
# kernel 内核
self.conv1 = nn.Conv2d(1, 6, 5)
self.conv2 = nn.Conv2d(6, 16, 5)
# an affine operation: y = Wx + b
self.fc1 = nn.Linear(16 * 5 * 5, 120) # 图像尺寸为5*5
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 10)

def forward(self, x):
# (2, 2)窗口上的最大池化
x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2))
# 如果大小是正方形,则可以指定一个数字
x = F.max_pool2d(F.relu(self.conv2(x)), 2)
# 压平除批量尺寸外的所有尺寸
x = torch.flatten(x, 1)
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x

net = Net()
################################### 与上文相同 ##############################
input = torch.randn(1, 1, 32, 32)
out = net(input)
print(out)

output:

1
2
tensor([[ 0.0265,  0.0280,  0.0228,  0.1131,  0.0270, -0.1227,  0.0928,  0.0028,
0.0389, -0.0410]], grad_fn=<AddmmBackward>)

使用随机梯度将所有参数和反向传播的梯度缓冲区归零:

1
2
net.zero_grad()
out.backward(torch.randn(1, 10))

torch.nn仅支持小批量。 整个torch.nn包仅支持作为微型样本而不是单个样本的输入。

  • torch.Tensor-一个多维数组,支持诸如backward()的自动微分操作。 同样,保持相对于张量的梯度。
  • nn.Module-神经网络模块。 封装参数的便捷方法,并带有将其移动到 GPU,导出,加载等的帮助器。
  • nn.Parameter-一种张量,即将其分配为Module的属性时,自动注册为参数。
  • autograd.Function-实现自动微分操作的正向和反向定义。 每个Tensor操作都会创建至少一个Function节点,该节点连接到创建Tensor的函数,并且编码其历史记录。

损失函数

损失函数采用一对(输出,目标)输入,并计算一个值,该值估计输出与目标之间的距离。

nn包下有几种不同的损失函数。 一个简单的损失是:nn.MSELoss,它计算输入和目标之间的均方误差。

1
2
3
4
5
6
7
output = net(input)
target = torch.randn(10) # 一个虚拟目标
target = target.view(1, -1) # 使它与输出相同的形状
criterion = nn.MSELoss()

loss = criterion(output, target)
print(loss)

output:

1
tensor(1.1649, grad_fn=<MseLossBackward0>)

现在,如果使用.grad_fn属性向后跟随loss,您将看到一个计算图,如下所示:

1
2
3
4
input -> conv2d -> relu -> maxpool2d -> conv2d -> relu -> maxpool2d
-> view -> linear -> relu -> linear -> relu -> linear
-> MSELoss
-> loss

当我们调用loss.backward()时,整个图将被微分。图中具有requires_grad=True的所有张量将随梯度累积其.grad张量

让我们向后走几步:

1
2
3
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

output:

1
2
3
<MseLossBackward0 object at 0x7f71283dd048>
<AddmmBackward0 object at 0x7f71283dd7f0>
<AccumulateGrad object at 0x7f71283dd7f0>

反向传播

要反向传播误差,我们要做的只是对loss.backward()。 不过,需要清除现有的梯度,否则梯度将累积到现有的梯度中。

现在,我们将其称为loss.backward(),然后看一下向后前后conv1的偏差梯度。

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)

output:

1
2
3
4
conv1.bias.grad before backward
tensor([0., 0., 0., 0., 0., 0.])
conv1.bias.grad after backward
tensor([ 0.0188, 0.0172, -0.0044, -0.0141, -0.0058, -0.0013])

更新权重

实践中使用的最简单的更新规则是 随机梯度下降(SGD)

weight = weight - learning_rate * gradient

下一次的权重 = 本次的权重值 - 学习率 * 梯度(误差)

实现

1
2
3
learning_rate = 0.01 							# 学习率0.01
for f in net.parameters(): # 遍历与调整
f.data.sub_(f.grad.data * learning_rate) # 更新权重

在使用神经网络时,您希望使用各种不同的更新规则,如 SGD,Nesterov-SGD,Adam,RMSProp 等……

为实现此目的,我们构建了一个小包装:torch.optim,可实现所有这些方法。

1
2
3
4
5
6
7
8
9
10
11
import torch.optim as optim		# 引入更新规则包

# 创建您的优化器
optimizer = optim.SGD(net.parameters(), lr=0.01)

# 在你的训练循环中:
optimizer.zero_grad() # 将梯度缓冲区归零
output = net(input)
loss = criterion(output, target)
loss.backward()
optimizer.step() # 更新

使用optimizer.zero_grad()将梯度缓冲区手动设置为零,这是因为反向传播部分中所述累积了梯度。