Learning PyTorch with examples
来自这里。
本教程通过自包含的示例来介绍PyTorch的基本概念。
PyTorch的核心是两个主要功能:
- 可在GPU上运行的,类似于numpy的多维tensor
- 自动区分构建的和训练的神经网络
我们将使用全连接ReLU网络作为示例。网络中包含单个隐藏层,通过最小化网络输出与真实输出之间的欧氏距离,用梯度下降训练来拟合随机数据。
Tensor
Warm-up: numpy
在介绍PyTorch之前,我们先用numpy来实现网络。
Numpy提供了一个多维数组对象和很多操作数组的函数。Numpy是一个常用的科学计算框架,它对计算图形、深度学习或梯度一无所知。但是,通过使用numpy操作来手动实现网络的前向和后向传递,我们可以轻松地使用numpy将两层网络适配到随机数据:
|
#!/usr/bin/env python3 |
|
|
|
import numpy as np |
|
|
|
# N是批量大小,D_in 是输入维度,H 是隐藏维度, D_out 是输出维度 |
|
N, D_in, H, D_out = 64, 1000, 100, 10 |
|
|
|
# 随机创建输入输出数据 |
|
x = np.random.randn(N, D_in) |
|
y = np.random.randn(N, D_out) |
|
|
|
# 随机初始化权重 |
|
w1 = np.random.randn(D_in, H) |
|
w2 = np.random.randn(H, D_out) |
|
|
|
learning_rate = 1e-6 |
|
for i in range(500): |
|
|
|
# 前向传递:计算预测y |
|
h = x.dot(w1) |
|
h_relu = np.maximum(h, 0) |
|
y_pred = h_relu.dot(w2) |
|
|
|
# 计算并输出 loss |
|
loss = np.square(y_pred-y).sum() |
|
print(i, loss) |
|
|
|
# 反向传递,计算相对于loss的w1和w2的梯度 |
|
grad_y_pred = 2.0*(y_pred-y) |
|
grad_w2 = h_relu.T.dot(grad_y_pred) |
|
grad_h_relu = grad_y_pred.dot(w2.T) |
|
grad_h = grad_h_relu.copy() |
|
grad_h[h < 0] = 0 |
|
grad_w1 = x.T.dot(grad_h) |
|
|
|
# 更新权重 |
|
w1 -= learning_rate*grad_w1 |
|
w2 -= learning_rate*grad_w2 |
PyTorch: Tensor
Numpy是一个非常好的框架,但它不能使用GPU加速它的数值计算。在现代深度神经网络中,GPU通常提供50倍甚至更高的加速效果,所以很不幸,对现代深度神经学习来说,numpy是不够的。
这里我们引入最基本的PyT概念:Tensor。PyTorch张量在概念上与numpy相同:张量是一个多维数组,同时PyTorch提供了很多操作张量的函数。此外,张量可以跟踪计算图形和梯度,这些同样是科学计算的常用工具。
不像numpy,PyTorch张量可以使用GPU加速它的数据计算。为了在GPU上运行PyTorch张量,你只需要将它转换成新的数据类型。
现在我们使用PyTorch张量将两层网络适配到随机数据。像numpy例子那样,我们需要手动实现网络的前向和后向传递:
|
import torch |
|
|
|
dtype = torch.float |
|
device = torch.device('cpu') |
|
# device = torch.device('cuda:0') # 若在GPU上执行,取消注释 |
|
|
|
# N是批量大小,D_in 是输入维度,H 是隐藏维度, D_out 是输出维度 |
|
N, D_in, H, D_out = 64, 1000, 100, 10 |
|
|
|
# 随机创建输入输出数据 |
|
x = torch.randn(N, D_in, device=device, dtype=dtype) |
|
y = torch.randn(N, D_out, device=device, dtype=dtype) |
|
|
|
# 随机初始化权重 |
|
w1 = torch.randn(D_in, H, device=device, dtype=dtype) |
|
w2 = torch.randn(H, D_out, device=device, dtype=dtype) |
|
|
|
learning_rate = 1e-6 |
|
for i in range(500): |
|
# 前向传递:计算预测y |
|
h = x.mm(w1) |
|
h_relu = h.clamp(min=0) |
|
y_pred = h_relu.mm(w2) |
|
|
|
# 计算并输出 loss |
|
loss = (y_pred-y).pow(2).sum().item() |
|
print(i, loss) |
|
|
|
# 反向传递,计算相对于loss的w1和w2的梯度 |
|
grad_y_pred = 2.0*(y_pred-y) |
|
grad_w2 = h_relu.t().mm(grad_y_pred) |
|
grad_h_relu = grad_y_pred.mm(w2.t()) |
|
grad_h = grad_h_relu.clone() |
|
grad_h[h < 0] = 0 |
|
grad_w1 = x.t().mm(grad_h) |
|
|
|
# 更新权重 |
|
w1 -= learning_rate*grad_w1 |
|
w2 -= learning_rate*grad_w2 |
Autograd
PyTorch:Tensor and autograd
在上面的例子中,我们必须手动实现我们神经网络的前向和反向传播。手动实现反向传播对于只有两层的网络来说并不是什么难事,但对于大型复杂的网络来说会变得非常繁琐。
谢天谢地,在神经网络中我们可以使用自动分化(automatic differentiation)来自动计算反向传播。在PyTorch中的autograd包提供了完整的功能。在使用autograd
时,网络的前向传递会定义一个计算图,图中的节点是张量,边界是从输入张量产生输出张量的函数。提供这个图的反馈,你可以轻松计算梯度。
这听起来很复杂,在实践中使用起来相当简单。每个张量表示计算图中的一个节点。如果x
是一个x.requires_grad=True
的张量,那么x.grad
就是另一个包含x
对某个标量的梯度的另一个张量。
现在我们使用PyTorch的Tensor和autograd来实现我们的两层网络,我们不再需要手动实现网络中反向传递。
|
import torch |
|
|
|
dtype = torch.float |
|
device = torch.device('cpu') |
|
# device = torch.device('cuda:0') # 若在GPU上执行,取消注释 |
|
|
|
# N是批量大小,D_in 是输入维度,H 是隐藏维度, D_out 是输出维度 |
|
N, D_in, H, D_out = 64, 1000, 100, 10 |
|
|
|
# 随机创建输入输出数据 |
|
x = torch.randn(N, D_in, device=device, dtype=dtype) |
|
y = torch.randn(N, D_out, device=device, dtype=dtype) |
|
|
|
# 随机初始化权重 |
|
w1 = torch.randn(D_in, H, device=device, dtype=dtype, requires_grad=True) |
|
w2 = torch.randn(H, D_out, device=device, dtype=dtype, requires_grad=True) |
|
|
|
learning_rate = 1e-6 |
|
for i in range(500): |
|
# 前向传递:使用Tensor的操作来计算预测y,这些操作跟使用Tensor计算前向传递的操作很相似, |
|
# 但是我们不需要保留中间变量的引用,因为我们不用手动执行后向传递。 |
|
y_pred = x.mm(w1).clamp(min=0).mm(w2) |
|
|
|
# 计算并输出 loss |
|
loss = (y_pred-y).pow(2).sum() |
|
print(i, loss.item()) |
|
|
|
# 反向传递,计算相对于loss的w1和w2的梯度 |
|
loss.backward() |
|
|
|
# 更新权重 |
|
with torch.no_grad(): |
|
w1 -= learning_rate * w1.grad |
|
w2 -= learning_rate * w2.grad |
|
|
|
w1.grad.zero_() |
|
w2.grad.zero_() |
PyTorch:定义新的autograd函数
实际上,每个原始的autograd运算符实际上是两个作用于张量的函数。forward函数从输入张量计算出输出张量,backward函数接收输出张量相对于某个标量值的梯度,并计算输入张量相对于该标量值的梯度。
在PyTorch中,通过定义torch.autograd.Function
的子类和实现其中的forward
和backward
我们可以很容易地定义我们自己的autograd操作。然后,我们可以通过构造一个实例并像函数一样调用它,传递包含输入数据的张量来使用新的autograd操作符。
在这个例子中,我们定义了自定义的autograd函数来执行ReLU非线性计算,并使用它来实现我们的两层网络:
|
import torch |
|
|
|
class MyReLU(torch.autograd.Function): |
|
|
|
|
|
def forward(ctx,input): |
|
''' |
|
在正向传递中,我们收到一个包含输入和返回的张量 |
|
包含输出的张量。ctx是一个可以使用的上下文对象 |
|
隐藏信息以备向后计算。在向后传递过程中你可以 |
|
使用ctx.save_for_backward方法任意缓存使用的对象。 |
|
''' |
|
ctx.save_for_backward(input) |
|
return input.clamp(min=0) |
|
|
|
|
|
def backward(ctx,grad_output): |
|
''' |
|
在后向传递中,我们得到一个关于输出的包含损失梯度的张量,我们需要计算关于输入的损失梯度。 |
|
''' |
|
input, = ctx.saved_tensors |
|
grad_input = grad_output.clone() |
|
grad_input[input < 0] = 0 |
|
return grad_input |
|
|
|
dtype = torch.float |
|
device = torch.device('cpu') |
|
# device = torch.device('cuda:0') # 若在GPU上执行,取消注释 |
|
|
|
# N是批量大小,D_in 是输入维度,H 是隐藏维度, D_out 是输出维度 |
|
N, D_in, H, D_out = 64, 1000, 100, 10 |
|
|
|
# 随机创建输入输出数据 |
|
x = torch.randn(N, D_in, device=device, dtype=dtype) |
|
y = torch.randn(N, D_out, device=device, dtype=dtype) |
|
|
|
# 随机初始化权重 |
|
w1 = torch.randn(D_in, H, device=device, dtype=dtype, requires_grad=True) |
|
w2 = torch.randn(H, D_out, device=device, dtype=dtype, requires_grad=True) |
|
|
|
learning_rate = 1e-6 |
|
for i in range(500): |
|
# 使用我们自定义的函数 |
|
relu = MyReLU.apply |
|
|
|
# 前向传递:使用Tensor的操作来计算预测y,这些操作跟使用Tensor计算前向传递的操作很相似, |
|
# 但是我们不需要保留中间变量的引用,因为我们不用手动执行后向传递。 |
|
y_pred = relu(x.mm(w1)).mm(w2) |
|
|
|
# 计算并输出 loss |
|
loss = (y_pred-y).pow(2).sum() |
|
print(i, loss.item()) |
|
|
|
# 反向传递,计算相对于loss的w1和w2的梯度 |
|
loss.backward() |
|
|
|
# 更新权重 |
|
with torch.no_grad(): |
|
w1 -= learning_rate * w1.grad |
|
w2 -= learning_rate * w2.grad |
|
|
|
w1.grad.zero_() |
|
w2.grad.zero_() |
TensorFlow: 静态图
PyTorch的autograd跟TensorFlow很相似:在两个框架中我们都定义计算图,并且利用自动分化计算梯度。它们两最大的不同之处在于TensorFlow的计算图是静态,而PyTorch使用动态计算图。
在TensorFlow中,一旦我们定义了计算图,那么它将被一遍遍地执行,尽管可能会输入不同的数据到图中。在PyTorch中,每次前向传递都将定义一个新的计算图。
静态图很好,因为可以预先优化图。例如,为了提高效率,框架可能决定融合一些图形操作,或者提出一种策略,将图形分布到许多gpu或许多机器上。如果您反复重用相同的图,那么这种潜在的昂贵的预先优化可以在反复运行相同的图时进行分摊。
静态和动态图不同之处在与控制流(control flow)。对于某些模型,我们可能希望对每个数据点执行不同的计算。例如,对于每个数据点,可以按不同的时间步长展开一个递归网络,这种展开可以实现为一个循环。对于静态图,循环结构需要成为图的一部分;因此,TensorFlow提供了tf.scan
等操作符扫描是否将循环嵌入到图中。对于动态图,情况更简单:因为我们为每个示例动态构建图,所以我们可以使用常规命令式流控制来执行每个输入不同的计算。
与上面的PyTorch autograd例子相反,这里我们使用TensorFlow来安装一个简单的两层网络:
|
import tensorflow as tf |
|
import numpy as np |
|
|
|
# 首先定义计算图 |
|
|
|
# N是批量大小,D_in 是输入维度,H 是隐藏维度, D_out 是输出维度 |
|
N, D_in, H, D_out = 64, 1000, 100, 10 |
|
|
|
# 为输入和目标数据创建占位符;它们在执行时会被真实数据填充 |
|
x = tf.placeholder(tf.float32, shape=(None, D_in)) |
|
y = tf.placeholder(tf.float32, shape=(None, D_out)) |
|
|
|
# 随机初始化权重 |
|
w1 = tf.Variable(tf.random_normal((D_in, H))) |
|
w2 = tf.Variable(tf.random_normal((H, D_out))) |
|
|
|
# 前向传递:通过TensorFlow张量上的操作计算预测的y值 |
|
# 注意,这段代码并不执行任何数值操作,它仅仅是设置计算图以便我们之后执行 |
|
h = tf.matmul(x, w1) |
|
h_relu = tf.maximum(h, tf.zeros(1)) |
|
y_pred = tf.matmul(h_relu, w2) |
|
|
|
# 通过TensorFlow张量上的操作计算损失 |
|
loss = tf.reduce_sum((y-y_pred)**2.0) |
|
|
|
# 计算loss关于w1和w2的梯度 |
|
grad_w1, grad_w2 = tf.gradients(loss, [w1, w2]) |
|
|
|
# 使用梯度更新权重。实际上在更新权重时,我们需要在执行图的过程中评估新的权重new_w1和new_w2。 |
|
# 注意,在TensorFlow中更新权重值得操作是计算图的一部分,而在PyTorch中,这些操作发生在计算图之外。 |
|
learning_rate = 1e-6 |
|
new_w1 = w1.assign(w1 - learning_rate * grad_w1) |
|
new_w2 = w2.assign(w2 - learning_rate * grad_w2) |
|
|
|
# 现在,我们已经构建玩计算图,那么我们输入一个TensorFlow回话来实际执行图 |
|
with tf.Session() as sess: |
|
# 执行图之前需要先初始化变量w1和w2 |
|
sess.run(tf.global_variables_initializer()) |
|
|
|
# 创建numpy数组来存储输入x和目标y的真实数据 |
|
x_value = np.random.randn(N, D_in) |
|
y_value = np.random.randn(N, D_out) |
|
|
|
for _ in range(500): |
|
# 重复执行多次。它每次执行时,我们使用参数`feed_dict`将x_value赋值给x,y_value赋值给y。 |
|
# 我们每次执行计算图时,我们需要计算loss、new_w1和new_w2的值,这些值得张量将以numpy数组的形式返回 |
|
loss_value, _, _ = sess.run([loss, new_w1, new_w2], feed_dict={ |
|
x: x_value, y: y_value}) |
|
print(loss_value) |
nn 模块
PyTorch: nn
计算图和autograd
是定义复杂运算和自动求导的非常强大的典范,然而对于大型的神经网络,原始的autograd
是远远不够。
在构建神经网络时,我们经常考虑将计算分层进行,其中一些计算具有可学习的参数,这些参数将在学习过程中得到优化。
在TensorFlow中,Keras、TensorFlow-Slim和TFLearn等包提供了对原始计算图的高级抽象,这些抽象对构建神经网络有很大帮助。
在PyTorch中,nn
包提供了同样的服务。nn
包中定义了一系列的Modules,可以看做是神经网的层。模型接受输入张量并计算输出张量,同时像张量包含可学习参数一样保留内部状态。nn
包中同样定义了一系列的损失函数以便训练神经网。
在接下来的例子里使用nn
包来实现我们的两层网络:
|
import torch |
|
|
|
# N是批量大小,D_in 是输入维度,H 是隐藏维度, D_out 是输出维度 |
|
N, D_in, H, D_out = 64, 1000, 100, 10 |
|
|
|
# 随机创建输入输出数据 |
|
x = torch.randn(N, D_in) |
|
y = torch.randn(N, D_out) |
|
|
|
# 使用nn包将我们的模型定义为一系列的层。nn.Sequential是一个模型,它包含其它模型并按顺序使用它们来生成输出。 |
|
# 每次模型使用线性函数从输入中计算输出,并保留权重和偏置的张量。 |
|
model = torch.nn.Sequential( |
|
torch.nn.Linear(D_in,H), |
|
torch.nn.ReLU(), |
|
torch.nn.Linear(H,D_out), |
|
) |
|
|
|
# nn包中也包含了常用的损失函数定义,这里我们使用Mean Squared Error(MSE,均方误差)作为我们的损失函数 |
|
loss_fn = torch.nn.MSELoss(reduction='sum') |
|
|
|
learning_rate = 1e-4 |
|
for i in range(500): |
|
# 前向传递:使用模型计算关于x的预测y |
|
y_pred = model(x) |
|
|
|
# 计算损失 |
|
loss = loss_fn(y_pred,y) |
|
print(i,loss.item()) |
|
|
|
# 反向传递前零化梯度 |
|
model.zero_grad() |
|
|
|
# 反向传递 |
|
loss.backward() |
|
|
|
# 更新权重 |
|
with torch.no_grad(): |
|
for param in model.parameters(): |
|
param -= learning_rate * param.grad |
PyTorch: optim(优化)
到目前为止,我们通过手动改变带有可学习参数的张量的方式(使用torch.no_grad()
或者.data
的方式避免在autograd过程中追踪历史记录)来更新我们模型的权重。对于像随机梯度下降这样的简单优化算法,这并不是一个巨大的负担,但在实践中,我们经常使用更复杂的优化器(如AdaGrad、RMSProp、Adam等)来训练神经网络。
在PyTorch中,optim
包抽象了优化算法的概念,并提供了常用优化算法的实现。
在接下来的例子中,我们想之前一样使用nn
包定义我们的模型,但是我们使用optim
包提供的Adam算法来优化我们的模型:
|
import torch |
|
|
|
# N是批量大小,D_in 是输入维度,H 是隐藏维度, D_out 是输出维度 |
|
N, D_in, H, D_out = 64, 1000, 100, 10 |
|
|
|
# 随机创建输入输出数据 |
|
x = torch.randn(N, D_in) |
|
y = torch.randn(N, D_out) |
|
|
|
# 使用nn包将我们的模型定义为一系列的层。nn.Sequential是一个模型,它包含其它模型并按顺序使用它们来生成输出。 |
|
# 每次模型使用线性函数从输入中计算输出,并保留权重和偏置的张量。 |
|
model = torch.nn.Sequential( |
|
torch.nn.Linear(D_in,H), |
|
torch.nn.ReLU(), |
|
torch.nn.Linear(H,D_out), |
|
) |
|
|
|
# nn包中也包含了常用的损失函数定义,这里我们使用Mean Squared Error(MSE,均方误差)作为我们的损失函数 |
|
loss_fn = torch.nn.MSELoss(reduction='sum') |
|
|
|
# 学习率和优化算法 |
|
learning_rate = 1e-4 |
|
optimizer = torch.optim.Adam(model.parameters(),lr=learning_rate) |
|
|
|
for i in range(500): |
|
# 前向传递:使用模型计算关于x的预测y |
|
y_pred = model(x) |
|
|
|
# 计算损失 |
|
loss = loss_fn(y_pred,y) |
|
print(i,loss.item()) |
|
|
|
# 反向传递前零化梯度 |
|
optimizer.zero_grad() |
|
|
|
# 反向传递 |
|
loss.backward() |
|
|
|
# 调用优化算法的setp()函数来更新参数 |
|
optimizer.step() |
PyTorch: 自定义nn模型
有时候你可能需要比现有的一系列模型更复杂的特殊模型,这是你可以通过将nn.Module
子类化的方式定义你自己的模型,并定义forward
函数来接受输入张量并使用其他模块或张量上的其他autograd操作来生成输出张量。
接下来我们使用自定义模型子类来实现我们的两次网络:
|
import torch |
|
|
|
class TwoLayerNet(torch.nn.Module): |
|
def __init__(self,D_in,H,D_out): |
|
super(TwoLayerNet,self).__init__() |
|
self.linear1 = torch.nn.Linear(D_in,H) |
|
self.linear2 = torch.nn.Linear(H,D_out) |
|
|
|
def forward(self,x): |
|
h_relu = self.linear1(x).clamp(min=0) |
|
y_pred = self.linear2(h_relu) |
|
return y_pred |
|
|
|
# N是批量大小,D_in 是输入维度,H 是隐藏维度, D_out 是输出维度 |
|
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) |
|
|
|
# 损失函数和优化函数 |
|
criterion = torch.nn.MSELoss(reduction='sum') |
|
optimizer = torch.optim.SGD(model.parameters(),lr=1e-4) |
|
|
|
for i in range(500): |
|
# 前向传递:使用模型计算关于x的预测y |
|
y_pred = model(x) |
|
|
|
# 计算损失 |
|
loss = criterion(y_pred,y) |
|
print(i,loss.item()) |
|
|
|
# 反向传递前零化梯度 |
|
optimizer.zero_grad() |
|
|
|
# 反向传递 |
|
loss.backward() |
|
|
|
# 调用优化算法的setp()函数来更新参数 |
|
optimizer.step() |
PyTorch: 控制流 + 权重共享
作为动态图和共享权重的一个例子,我们实现一个分成奇怪的模型:一个全连接ReLU网络,每个前向传递从1到4之间选择一个随机数并且使用那么多隐藏层,重复多次使用相同的权重计算最深的隐藏层。
对于这个模型,我们可以使用普通的Python流控制来实现循环,并且我们可以通过在定义正向传递时多次重用同一个模型来实现最内层之间的权重共享。
通过子类化的方式我们可以轻松实现这个模型:
|
import random |
|
import torch |
|
|
|
|
|
class DynamicNet(torch.nn.Module): |
|
def __init__(self, D_in, H, D_out): |
|
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): |
|
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是批量大小,D_in 是输入维度,H 是隐藏维度, D_out 是输出维度 |
|
N, D_in, H, D_out = 64, 1000, 100, 10 |
|
|
|
# 随机创建输入输出数据 |
|
x = torch.randn(N, D_in) |
|
y = torch.randn(N, D_out) |
|
|
|
# 实例化自定义模型 |
|
model = DynamicNet(D_in, H, D_out) |
|
|
|
# 损失函数和优化函数 |
|
criterion = torch.nn.MSELoss(reduction='sum') |
|
optimizer = torch.optim.SGD(model.parameters(), lr=1e-4, momentum=0.9) |
|
|
|
for i in range(500): |
|
# 前向传递:使用模型计算关于x的预测y |
|
y_pred = model(x) |
|
|
|
# 计算损失 |
|
loss = criterion(y_pred, y) |
|
print(i, loss.item()) |
|
|
|
# 反向传递前零化梯度 |
|
optimizer.zero_grad() |
|
|
|
# 反向传递 |
|
loss.backward() |
|
|
|
# 调用优化算法的setp()函数来更新参数 |
|
optimizer.step() |