本教程通过自包含的示例介绍PyTorch的基本概念。
在它的核心部分,PyTorch提供了两个主要的功能:
- N维的张量,与numpy相似但可运行在GPU上。
- 用于搭建和训练神经网络的自动求导。
我们将会使用全连接ReLU神经网络作为我们的运行实例。这个神经网络会有一个隐藏层,并使用梯度下降方法来缩小神经网络输出与实际结果间的差距以适应随机数据。
张量
热身:numpy
在介绍PyTorch之前,我们将会首先使用numpy来搭建神经网络。
numpy提供了一个n维的数组对象,和许多操纵这些数组的操作。numpy是一个通用的科学计算框架,它并不了解任何关于计算图、深度学习或梯度的事。然后,通过使用numpy操作来手动地实现神经网络的前向和后向传播,我们可以轻易地使用numpy来使一个两层地神经网络适应随机数据。
import numpy as np
## N为batch size,D_in和D_out分别为输入和输出的维度,H为隐藏层维度
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 t in range(500):
## 前向传播计算结果
## dot()是numpy中的函数,可以通过numpy调用,也可以使用numpy对象调用,a.dot(b)和np.dot(a,b)效果相同
## 其运算逻辑即为矩阵乘法
## 更详细的见 https://www.cnblogs.com/luhuan/p/7925790.html
h = x.dot(w1)
h_relu = np.maximum(h,0)
y_pred = h_relu.dot(w2)
## 使用欧几里得距离计算误差
loss = np.square(y_pred - y).sum()
print(t,loss)
## 后向传播计算导数
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)
## copy()为深拷贝,不同于切片,它得到的值与它拷贝的值是相互独立的
## 更详细的见 https://blog.csdn.net/u010099080/article/details/59111207
grad_h = grad_h_relu.copy()
## 如果h此处值小于0,则导数值为0
grad_h[h < 0] = 0
grad_w1 = x.T.dot(grad_h)
## 更新权重
w1 -= learning_rate * grad_w1
w2 -= learning_rate * grad_w2
其结果为:
0 23480234.619771272
1 16969738.413497925
2 15794528.769508518
3 17800053.3587495
4 21691014.607606158
5 25685196.165032614
6 27002350.701414548
7 23740289.828860078
8 16890862.310665496
9 9995764.701150808
…
210 0.20815566911905675
211 0.196827249830064
212 0.18612682108672252
213 0.17601956345333744
214 0.1664764263447795
215 0.1574584872475641
216 0.14893863082689898
217 0.14089080595288167
218 0.13328530666242536
219 0.1260991455982713
…
490 1.4986059732653066e-07
491 1.4285739130150745e-07
492 1.361822103046696e-07
493 1.2981939899622808e-07
494 1.2375452885499174e-07
495 1.1797410575954792e-07
496 1.1246412001096617e-07
497 1.0721229144142987e-07
498 1.0220615485656917e-07
499 9.74352670082558e-08
PyTorch:Tensor(张量)
numpy是一个很好的框架,但是它无法利用GPU来加速数值计算。对现代的深度神经网络来说,GPU往往能提供50倍以上的速度加成,所以很不幸的,numpy无法满足现代深度学习的要求。
这里我们介绍最基本的PyTorch概念:Tensor。PyTorch张量在概念上和numpy数组相同:张量是一个n维的数组,PyTorch还提供了很多操作张量的函数。张量还能在幕后记录计算图和梯度,它们作为通用工具,对科学计算也是很有用的。
但是,不同于numpy,PyTorch张量可以利用GPU来加速它们的数值计算。为了让PyTorch张量运行在GPU上,你只需要简单地将其转换成新的数据类型即可。
这里我们使用PyTorch张量来让双层神经网络适应随机数据。就像上面的numpy实例一样,我们需要手动地实现神经网络的前向和后向传播。
import torch
dtype = torch.float
device = torch.device("cuda:0")
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 t in range(500):
## 类似dot(),将两个矩阵相乘
## 更详细的见 https://blog.csdn.net/Real_Brilliant/article/details/85756477
h = x.mm(w1)
## clamp()将张量中的每个元素都夹紧到min和max之间
## 更详细的见 https://blog.csdn.net/u013230189/article/details/82627375
h_relu = h.clamp(min=0)
y_pred = h_relu.mm(w2)
## 当张量只有一个值时,item()将其转换为标量,否则报错
## 更详细的见 https://blog.csdn.net/zz2230633069/article/details/83092376
loss = (y - y_pred).pow(2).sum().item()
print(t," --- ",loss)
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
自动求导
PyTorch:张量和自动求导
在上面的例子里,我们手动实现了我们神经网络的前向和后向传播。手动试试后向传播对双层的小网络来说并不是一个大问题,但是对大的复杂网络来说,这可能会很快变得毛茸茸的(hairy)。
幸运的是,我们可以使用自动求导来使得神经网络得后向传播变得自动化。PyTorch的autograd包提供了这项功能。当使用自动求导时,你的神经网络的前向传播会定义一个计算图,图中的节点均为张量,而边则是利用输入张量产生输出张量的函数。通过该图进行反向传播可以轻松地计算梯度。
这听上去很复杂,但实践起来却很简单。每个张量代表了计算图中的一个节点。如果x是一个x.requires_grad=True的张量,那么x.grad是另一个保存x关于某些标量值的梯度的张量。
这里我们使用PyTorch张量和自动求导来实现我们的双层神经网络,我们现在不需要再手动地实现神经网络的后向传播了:
import torch
dtype = torch.float
device = torch.device("cuda:0")
N,D_in,H,D_out = 64,1000,100,10
x = torch.randn(N,D_in,dtype=dtype,device=device)
y = torch.randn(N,D_out,dtype=dtype,device=device)
w1 = torch.randn(D_in,H,dtype=dtype,device=device,requires_grad=True)
w2 = torch.randn(H,D_out,dtype=dtype,device=device,requires_grad=True)
learning_rate = 1e-6
for t in range(500):
y_pred = x.mm(w1).clamp(min=0).mm(w2)
loss = (y_pred - y).pow(2).sum()
print(t," --- ",loss.item())
loss.backward()
## no_grad()使得此时进行关于w1、w2这样requires_grad=True的张量的操作时,并不记录梯度
## 目的应该是为了减少运算量
## 更详细的见 https://blog.csdn.net/g11d111/article/details/80840310
with torch.no_grad():
w1 -= learning_rate * w1.grad
w2 -= learning_rate * w2.grad
## grad.zero_()将会清空此时的梯度值。这是因为使用backward()会累积梯度,
## 这对当前的神经网络来说是个问题。更详细的见
## https://discuss.pytorch.org/t/why-do-we-need-to-set-the-gradients-manually-to-zero-in-pytorch/4903/11
w1.grad.zero_()
w2.grad.zero_()
PyTorch:定义自动求导函数
在引擎盖下,每个原始的自动求导操作器实际上是两个对张量进行操作的函数。forward函数通过输入张量计算输出张量。backward函数接收输出张量关于一些标量值的梯度值,并计算输入张量关于那些标量的梯度。
在PyTorch里,我们可以通过定义一个torch.autograd.Function的子类来简单地定义我们自己的自动求导器,进而实现forward和backward函数。然后我们可以通过构建一个实例来使用我们的自动求导器,我们可以像调用函数一样调用它,并将包含输入数据的张量传递给它。
在这个例子中,我们定义了自己的自动求导函数来执行ReLU非线性函数,并使用它来实现我们的双层网络。
import torch
class MyReLU(torch.autograd.Function):
## @staticmethod表明接下来定义的是一个静态方法,该方法可被类和实例调用
## 更详细的见 https://www.cnblogs.com/Meanwey/p/9788713.html
@staticmethod
def forward(ctx,input):
## ctx.save_for_backward()保存一些会在后向传播中用到的值
## 更详细的见 https://blog.csdn.net/u012436149/article/details/78829329
ctx.save_for_backward(input)
return input.clamp(min=0)
@staticmethod
def backward(ctx,grad_output):
## 将ctx保存的值赋给backward()中的变量
input = ctx.saved_tensors
grad_input = grad_output.clone()
grad_input.clamp(min=0)
return grad_input
dtype = torch.float
device = torch.device("cuda:0")
N,D_in,H,D_out = 64,1000,100,10
x = torch.randn(N,D_in,dtype=dtype,device=device)
y = torch.randn(N,D_out,dtype=dtype,device=device)
w1 = torch.randn(D_in,H,dtype=dtype,device=device,requires_grad=True)
w2 = torch.randn(H,D_out,dtype=dtype,device=device,requires_grad=True)
learning_rate = 1e-6
for t in range(500):
relu = MyReLU.apply
y_pred = relu(x.mm(w1)).mm(w2)
loss = (y_pred - y).pow(2).sum()
print(t," --- ",loss.item())
loss.backward()
## no_grad()使得此时进行关于w1、w2这样requires_grad=True的张量的操作时,并不记录梯度
## 目的应该是为了减少运算量
## 更详细的见 https://blog.csdn.net/g11d111/article/details/80840310
with torch.no_grad():
w1 -= learning_rate * w1.grad
w2 -= learning_rate * w2.grad
## grad.zero_()将会清空此时的梯度值。这是因为使用backward()会累积梯度,
## 这对当前的神经网络来说是个问题。更详细的见
## https://discuss.pytorch.org/t/why-do-we-need-to-set-the-gradients-manually-to-zero-in-pytorch/4903/11
w1.grad.zero_()
w2.grad.zero_()
(注:关于自定义求导函数,我的理解是,可以把它理解为一个嵌套自动求导。每次前向和后向传播到了自定义的自动求导函数这时,就带入相应的前向或后向函数。其好处在于,对于一些神经网络,我们对其的某些特殊步骤的求导有着特殊的要求,就可以使用自定义求导函数来修改求导方式。)
TensorFlow:静态图
PyTorch的自动求导看上去有一点像TensorFlow:在两个框架上,我们都定义计算图,且使用自动求导来计算梯度。这两个框架最大的区别是TensorFlow的计算图是静态的,而PyTorch使用动态计算图。
在TensorFlow,我们只定义一次计算图,接着一遍又一遍的输入可能不同的数据,但都执行着相同的图。在PyTorch中,每次前向传播都定义了一个新的计算图。
静态图的好处在于,你可以预先优化图像。比如说,一个框架可能决定为了高效来融合一些图的操作,或者提出一种在多个GPU或机器上分发图形的策略。如果你反复使用相同的图,那么这个可能代价高昂的前期优化可以被摊销,因为相同的图会反复运行。
静态图和动态图的一个区别是控制流程。对有些模型,我们可能希望对每个数据点进行不同的计算。比如说,对于每个数据点,可以针对不同数量的时间步长展开循环网络,这种展开可作为寻欢实现。使用静态图形,循环结构必须是图形的一部分。为此TensorFlow提供了诸如tf.scan这样的操作器,来将循环嵌入图中。而对于动态图来说,事情就变得简单了:因为我们为每个示例都动态地构建了计算图,我们可以使用常规的命令流控制来执行每个输入都不同的计算。
与上面的Pytorch自动求导的示例相比,这里我们使用TensorFlow来拟合一个简单的双层网络:
import tensorflow as tf
import numpy as np
N,D_in,H,D_out = 64,1000,100,10
## placeholder()为一个占位符节点,可以理解为形参,定义了接收的数据的参数
## 更详细的见 https://blog.csdn.net/slq1023/article/details/81277913
x = tf.placeholder(tf.float32,shape=(None,D_in))
y = tf.placeholder(tf.float32,shape=(None,D_out))
## 定义前向传播的op,直到计算出loss
## op在执行sess.run()前并不运行,可以将其理解为类似函数的一种东西
## Variable()为tensorflow的变量节点,创建时使用初始值来初始化,在运行过程中该值不变
## 更详细的见 https://www.cnblogs.com/Vulpers/p/7809276.html
w1 = tf.Variable(tf.random_normal((D_in,H)))
w2 = tf.Variable(tf.random_normal((H,D_out)))
## 矩阵相乘,更详细的见 https://blog.csdn.net/flyfish1986/article/details/79141763/
h = tf.matmul(x,w1)
##前向传播来计算y的预测值,它同样不进行计算
## zeros()创建一个所有元素都置为0的张量,其中第一个参数为返回张量的大小
## 更详细的见 https://www.w3cschool.cn/tensorflow_python/tensorflow_python-3fj22okr.html
h_relu = tf.maximum(h,tf.zeros(1))
y_pred = tf.matmul(h_relu,w2)
## reduce_sum()为tensor内部的求和函数
## 更详细的见 https://www.cnblogs.com/pied/p/8259089.html
loss = tf.reduce_sum((y - y_pred) ** 2.0)
## 定义求导op
## gradients()分别求第一个参数对第二个参数的梯度
## 更详细的见 https://www.cnblogs.com/lyc-seu/p/8566538.html
grad_w1,grad_w2 = tf.gradients(loss,[w1,w2])
learning_rate = 1e-6
## 定义w1和w2的更新op
## 更详细的见 https://blog.csdn.net/cyj68815923/article/details/81558685
new_w1 = w1.assign(w1 - learning_rate * grad_w1)
new_w2 = w2.assign(w2 - learning_rate * grad_w2)
## 开始执行计算图
with tf.Session() as sess:
## 初始化模型的参数
## 更详细的见 https://blog.csdn.net/u012436149/article/details/78291545
sess.run(tf.global_variables_initializer())
## 给定输入值
x_value = np.random.randn(N,D_in)
y_value = np.random.randn(N,D_out)
## feed_dict指定输入张量,按照第一个参数的顺序依次执行op,即完成了前向和后向传播
## 虽然这里只提到了loss、new_w1和new_w2三个op,但因为它们如果想得到结果,就必须
## 运行其它op,所以实际上相当于将所有op作为了参数
for _ in range(500):
loss_value = sess.run([loss,new_w1,new_w2],feed_dict={x:x_value,y:y_value})
print(loss_value)
nn 模块
PyTorch:nn
计算图和自动求导是定义复杂操作器和自动求导的有力模范。然后对大型的神经网络来说,原始的自动求导可能就有点不适用了。
当建立一个神经网络时,我们经常想要把计算安排到层里,其中一些有着科学系的参数,这些参数将在学习期间进行优化。
在TensorFlow中,像Keras、TensorFlow-Slim和TFLearn这类的包提供了原始计算图的高级抽象,这能够有效地构建神经网络。
在PyTorch中,nn包提供了同样的功能。nn包定义了一组模块,它们大致相当于神经网络层。模块接收输入张量并计算输出张量,但也可以保留内部状态,比如包含可学习参数的张量。nn包还定义了一系列有用的损失函数,它们是训练神经网络时常用的有用损失函数。
在本例中,我们使用nn包来实现我们的双层神经网络:
import torch
N,D_in,H,D_out = 64,1000,100,10
x = torch.randn(N,D_in)
y = torch.randn(N,D_out)
## 一个有序的容器,被传入其中的数据将依序进行处理
## 更详细的见 https://blog.csdn.net/dss_dssssd/article/details/82980222
model = torch.nn.Sequential(
torch.nn.Linear(D_in,H),
torch.nn.ReLU(),
torch.nn.Linear(H,D_out),
)
## 指定应用于输出的缩减,其中sum表示输出将被求和
## 更详细的见 https://pytorch.org/docs/stable/nn.html
loss_fn = torch.nn.MSELoss(reduction='sum')
learning_rate = 1e-6
for t in range(500):
y_pred = model(x)
loss = loss_fn(y_pred,y)
print(t,loss.item())
model.zero_grad()
loss.backward()
with torch.no_grad():
## parameters()获得一个model中所有权重的列表
for param in model.parameters():
param -= learning_rate * param.grad
Pytorch:optim
到目前为止,我们手动更新了拥有可学习参数的张量的权重(使用torch.no_grad()或.data来避免追踪自动求导的历史)。对于像随机梯度下降这样简单的优化算法来说,这不算是很大的负担,但是在实践中我们常常使用AdaGrad、RMSProp、Adam等复杂的优化算法来训练神经网络。
PyTorch的optim包抽象了优化算法的思想,并提供了常用优化算法的实现。
在本例中,我们将像之前一样使用nn包来定义我们的模型,但是我们将会使用optim包提供的Adam优化算法来优化模型:
import torch
N,D_in,H,D_out = 64,1000,100,10
x = torch.randn(N,D_in)
y = torch.randn(N,D_out)
model = torch.nn.Sequential(
torch.nn.Linear(D_in,H),
torch.nn.ReLU(),
torch.nn.Linear(H,D_out)
)
loss_fn = torch.nn.MSELoss(reduction='sum')
learning_rate = 1e-4
## 定义权重的更新方法,Adam为其中一个种方法,其第一个参数为待更新的权重
## 更详细的见 https://blog.csdn.net/gdymind/article/details/82708920
optimizer = torch.optim.Adam(model.parameters(),lr=learning_rate)
for t in range(500):
y_pred = model(x)
loss = loss_fn(y_pred,y)
print(t," --- ",loss.item())
optimizer.zero_grad()
loss.backward()
## 每次执行backward()后,需要执行一次step()来更新权重
optimizer.step()
PyTorch:自定义nn模块
有时候你想要指定比现有的模块更复杂的模块,在这些情况下,你可以通过定义nn.Module的子类和定义使用其它模块或张量上的自动求导操作来接收输入张量,产生输出张量的前向函数来定义你自己的模块。
在本例中,我们将我们的双层网络实现为自定义的Module子类:
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 = 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 t in range(500):
y_pred = model(x)
loss = criterion(y_pred,y)
print(t," --- ",loss.item())
optimizer.zero_grad()
loss.backward()
optimizer.step()
PyTorch:控制流 + 权值共享
作为动态图和权值共享的例子,我们实现一个非常奇怪的模型:一个全连接的ReLU网络,其每一次前向传播都选择一个1到4间的随机数字,并使用那么多的隐藏层,重复使用相同的权重来计算最里面的隐藏层。
对于这个模型,我们可以使用普通的Python流控制来实现循环,我们能够通过在定义前向传播时简单地多次复用同样的Module来实现最里面的隐藏层的权值共享。
我们可以简单的把这个模型作为Module的子类来实现:
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 = 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 t in range(500):
y_pred = model(x)
loss = criterion(y_pred,y)
print(t," -- ",loss.item())
optimizer.zero_grad()
loss.backward()
optimizer.step()