Tensors
Warm-up: numpy
对于numpy来说, 它对计算图, 深度学习, 梯度等等概念几乎是不知道的, 但是, 如果我们了解简单神经网络的具体结构, 那么我们就可以很轻易的用numpy来实现这个简单网络, 对此, 我们通常需要自己来实现前向计算和反向计算的逻辑, 下面我们来实现一个具有两层隐藏层的简单网络:
1 | import numpy as np |
在执行上述代码以后, w1
和w2
的值会是的预测出来的pred_y
与y
之间的平方损失越来越小.
PyTorch: Tensors
用PyTorch实现一个简单的神经网络
在神经网络的实现中, 较麻烦的是梯度的计算过程, 下面利用PyTorch的自动求导来实现一个简单的神经网络(两层隐藏层)
1 | import torch |
Autograd
自定义一个具有自动求导功能的PyTorch函数
上面的例子是使用手动的方式求梯度的, 当模型参数变多时, 这样的方式显然很不方便. 不过, 借助PyTorch的autograd
模块, 可以方便的求取任意参数的导数.
在使用PyTorch的自动推导模块autograd
时, 前向传播过程会被定义成一个计算图, 图中的节点是Tensors, 图中的边是一些函数, 用于根据 input Tensors 来生成 output Tensors. 比如当 x
是一个Tensor, 并且拥有属性x.requires_grad=True
, 那么x.grad
就是另一个Tensor, 它持有loss相对于x
的梯度.
1 | import torch |
Defining new autograd functions
在PyTorch中, 每一个具有自动求导功能的operator都由两个作用在Tensors上的函数实现, 分别是用于计算输出的 前向函数 , 以及用于计算梯度的 反向函数. 因此, 我们在可以在PyTorch中通过继承父类torch.autograd.Function
, 并实现其中的forward
和 backward
函数来定义自己的自定义autograd functions
1 | import torch |
Static Graphs
PyTorch采用动态计算图, 而TensorFlow采用静态计算图
静态计算图: 只对计算图定义一次, 而后会多次执行这个计算图.
好处:
- 可以预先对计算图进行优化, 融合一些计算图上的操作, 并且方便在分布式多GPU或多机的训练中优化模型
动态计算图: 每执行一次都会重新定义一张计算图.
- 控制流就像Python一样, 更容易被人接受, 可以方便的使用for, if等语句来动态的定义计算图, 并且调试起来较为方便.
nn Module
nn
对于大型网络模型来说, 直接使用autograd
有些太过底层(too low-level). 为此在搭建神经网络时, 我们经常会将计算放置到 layers
上 , 这些 layers
中的可学习参数会在训练中就行更新. 在TF中, Keras, TF-Slim等提高了封装性更高的高层API, 在PyTorch中, nn
包可以提供这些功能. 在nn
包中, 定义了一系列的 Modules , 可以类比为神经网络中的不同层. 一个 Module 会接受一组 input Tensors, 并计算出对应的 output Tensors, 同时会持有一些内部状态(如可学习的权重参数). 在nn
包中还定义了一系列有用的 loss functins 可供使用. 下面尝试用 nn
包来实现上面的两层网络:
1 | import |
optim
可以看到, 上面在更新参数时, 我们仍采取的是手动更新的方式, 对于简单的优化算法来说, 这并不是什么难事, 但是如果我们希望使用更加复杂的优化算法如AdaGrad, Adam时, 采用 optim
包提供的高层API可以方便的使用这些优化算法.1
2
3
4
5
6
7
8optimizer = torch.optim.Adam(model.parameters(), lr=1e-4)
for t in range(500):
y_pred = model(x)
loss = loss_fn(y_pred, y)
optimizer.zero_grad() # 已经将待优化参数model.parameters()传给优化器了
loss.backward()
optimizer.step() # 执行一次参数优化操作(是不是很简单?)
Custom nn Modules
有时候, 我们需要定义一些相比于序列化模型更加复杂的模型, 此时, 我们可以通过继承nn.Module
,同时定义forward
前向计算函数来自定义一个 Module. 下面我们就用这种方式来自定义一个具有两层网络的 Module.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
class TwoLayerNet(torch.nn.Module):
def __init__(self, D_in, H, D_out):
# 通常我们将具有参数的层写在__init__函数中, 将不具有参数的ops写在forward中
self.linear1 = torch.nn.Linear(D_in, H)
self.linear2 = torch.nn.Linear(H, D_out)
def forward(self, input):
h_relu = self.linear1(input).clamp(min=0)
y_pred = self.linear2(h_relu)
return y_pred
N, D_in, H, D_out = 64, 1000, 100, 10
x = torch.randn(N, D_in)
y = torch.randn(N, D_out)
model = TwoLayerNet(D_in, H, D_out)
loss_fn = torch.nn.MSELoss(reduction="sum")
optim = torch.optim.SGD(model.parameters(), lr=1e-4)
for t in range(500):
y_pred = model(x)
loss = loss_fn(y_pred, y)
optim.zero_grad()
loss.backward()
optim.step()
Control Flow + Weight Sharing
为了更好的演示动态图和权重共享的特点, 我们会在下面实现一个非常奇怪的模型: 一个全连接的ReLU网络, 中间会随机的使用1~4层隐藏层, 并且重复利用相同的权重来计算最深层的隐藏层输出.
在PyTorch中, 我们可以通过for循环来实现这种动态模型, 并且通过重复调用同一个Module就可以很容易使用多层之间的参数共享, 如下所示: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
49import random
import torch
class DynamicNet(torch.nn.Module):
def __init__(self, D_in, H, D_out):
# 实现三个 nn.Linear 实例, 意味着在模型中只有 三个 nn.Linear 的参数
super(DynamicNet, self).__init__()
self.input_linear = torch.nn.Linear(D_in, H)
self.middle_linear = torch.nn.Linear(H, H)
self.output_linear = torch.nn.Linear(H, D_out)
def forward(self, x):
"""
在PyTorch中, 我们可以通过for循环来随机的选择中间层的层数, 使得每一次
执行forward函数时, 都具有不同的中间层层数. 而这些中间层都来自于同一个Module实例, 因而具有共享的权重参数.
"""
h_relu = self.input_linear(x).clamp(min=0)
for _ in range(random.randint(0,3)):
h_relu = self.middle_linear(h_relu).clamp(min=0)
y_pred = self.output_linear(h_relu)
return y_pred;
# N is batch size; D_in is input dimension;
# H is hidden dimension; D_out is output dimension.
N, D_in, H, D_out = 64, 1000, 100, 10
# Create random Tensors to hold inputs and outputs
x = torch.randn(N, D_in)
y = torch.randn(N, D_out)
# Construct our model by instantiating the class defined above
model = DynamicNet(D_in, H, D_out)
# Construct our loss function and an Optimizer. Training this strange model with
# vanilla stochastic gradient descent is tough, so we use momentum
criterion = torch.nn.MSELoss(reduction='sum')
optimizer = torch.optim.SGD(model.parameters(), lr=1e-4, momentum=0.9)
for t in range(500):
# Forward pass: Compute predicted y by passing x to the model
y_pred = model(x)
# Compute and print loss
loss = criterion(y_pred, y)
print(t, loss.item())
# Zero gradients, perform a backward pass, and update the weights.
optimizer.zero_grad()
loss.backward()
optimizer.step()