在AI学习笔记(八)深度学习与神经网络、推理与训练中,介绍了神经网络的正向传播及反向传播,此处不再赘述,本文的重点在于如何用pytorch工具实现正向传播与反向传播的过程。
Pytorch的有两个核心特征,其一是能创建如Numpy形式的张量,但不同在于用pytorch创建的可以在GPU上运行,在速度上占优势;其二是自动微分工具。本文将使用带有激活函数ReLU的全连接网络作为示例,设置该网络有一个单一的隐藏层,并将使用梯度下降训练,通过最小化网络输出和真实结果的欧几里得距离,来拟合随机生成的数据。
为了对神经网络由更清楚的认识与理解,在介绍PyTorch之前,这里首先使用numpy实现手动神经网络的训练过程。Numpy提供了一个n为数组的对象,以及许多用于操作这些数组的函数。Numpy是用于科学计算的通用框架;它对计算图、深度学习和梯度一无所知。然而,仍然可以使用其手动实现简单网络的前向和后向传播来拟合随机数据。代码如下:
#!/usr/bin/env torch
# -*- coding:utf-8 -*-
# @Time : 2021/2/4, 22:26
# @Author: Lee
# @File : tensor_concerned.py
"""
Numpy提供了一个n维数组对象,以及许多用于操作这些数组的函数。
Numpy是用于科学计算的通用框架,它对计算图、深度学习和梯度一
无所知。但是,却可以使用它来手动实现网络的前向和反向传播,来
拟合随机数据。
"""
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-y_pred).sum()
if (i+1) % 50 == 0:
print(i, 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)
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
运行结果如下:
49 16802.202250139962
99 872.9062055307663
149 85.25050942196341
199 10.546343598962737
249 1.4718805065661518
299 0.2206409147141588
349 0.034622724629688996
399 0.005601707830267057
449 0.0009259105956584462
499 0.00015546025406697002
Numpy是一个很棒的框架,但它不能利用GPU来加速其数值计算。对于现代深度神经网络,GPU通常提供50倍甚至更高的加速,所以,numpy不能满足当代深度学习的需求。
PyTorch中的tensor在概念上与numpy的array相同,tensor是一个n维数组,PyTorch提供了许多函数用于操作这些张量。任何希望使用Numpy执行的计算也可以用PyTorch的tensor来完成,可以认为他们是科学计算的通用工具。要在GPU上运行Tensor,在构造张量使用device参数把tensor建立在GPU上。
在这里,使用tensor将随机数据上训练一个两层的网络。和前面的Numpy的例子类似,我们使用PyTorch的tensor,手动在网络中实现前向传播和反向传播。具体代码如下:
#!/usr/bin/env torch
# -*- coding:utf-8 -*-
# @Time : 2021/2/5, 20:40
# @Author: Lee
# @File : tensor_conerned_torch.py
import torch
dtype = torch.float
device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
# 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) # (64, 1000)
y = torch.randn(N, D_out, device=device, dtype=dtype) # (64, 10)
# 随机化初始权重
w1 = torch.randn(D_in, H, device=device, dtype=dtype) # (1000, 100)
w2 = torch.randn(H, D_out, device=device, dtype=dtype) # (100, 10)
learning_rate = 1e-6
for t in range(500):
# 向前传递:计算预测y
h = x.mm(w1) # (64, 100)
h_relu = h.clamp(min=0) # (64, 100) 将h中小于0的部分都取0
"""
torch.clamp(input, min, max, out=None)
将输入input张量每个元素的范围限制到区间 [min,max],返回结果到一个新张量。
input (Tensor) – 输入张量
min (Number) – 限制范围下限
max (Number) – 限制范围上限
out (Tensor, optional) – 输出张量
"""
y_pred = h_relu.mm(w2) # (64, 10) 矩阵乘法
# 计算和打印损失
loss = (y_pred - y).pow(2).sum().item() # .item取值
if (t+1) % 50 == 0: # 每隔50次打印一次loss
print(t, loss)
# Backprop计算w1和w2对于损耗的梯度
grad_y_pred = 2.0 * (y_pred - y) # (64, 10)
grad_w2 = h_relu.t().mm(grad_y_pred) # (100, 10)
grad_h_relu = grad_y_pred.mm(w2.t()) # (64, 100)
grad_h = grad_h_relu.clone()
grad_h[h < 0] = 0
grad_w1 = x.t().mm(grad_h) # (1000, 100)
# 使用梯度下降更新权值
w1 -= learning_rate * grad_w1 # (1000, 100)
w2 -= learning_rate * grad_w2 # (100, 10)
运行结果如下:
49 11731.8369140625
99 359.118408203125
149 18.55643081665039
199 1.158257007598877
249 0.07949955016374588
299 0.005998088046908379
349 0.0006813545478507876
399 0.00015579612227156758
449 5.9364447224652395e-05
499 3.018024654011242e-05
由于是拟合随机数据,与Numpy结果不一致属于正常现象。但相同的在于,拟合数据与真实数据的偏差随迭代次数的增加越来越小。
上面的例子中,需要手动实现神经网络的前向和后向传播。手动实现方向传递对于小型网络来说可能可以实现,但是对于实际工程中需要用到的大型网络来说很快就会变得非常繁琐。
但是可以使用自动微分工具来自动计算神经网络的反向传播。PyTorch中autograd包含了这个功能。当使用autograd时,网络前向传播将定义一个计算图;图中的节点是tensor,边是函数,这些函数是输出tensor到tensor的映射。这张计算图使得在网络中反向传播时梯度的计算十分简单。
这听起来很复杂,在实践中使用起来非常简单。如果我们计算某些的tensor的梯度,我们只需要在建立这个tensor时加入这么一句:requires_grad=True。这个tensor上的任何PyTorch的操作都将构造一个计算图,从而允许我们稍后在图中执行反向传播。如果这个tensor x的requires_grad=True,那么反向传播之后x.grad将会是另一个张量,其中x关于某个标量值的梯度。
有时可能希望防止Pytorch在requires_grad=True的张量执行某些操作时构建计算图。例如,在训练神经网络是,我们通常不希望通过权重更新步骤反向传播。在这种情况下,我们可以使用torch.no_grad()上下文管理器来防止构造计算图。
下面是使用PytTorch的tensor和autograd来实现两层神经网络,不再需要手动执行网络的方向传播。
#!/usr/bin/env torch
# -*- coding:utf-8 -*-
# @Time : 2021/2/5, 21:29
# @Author: Lee
# @File : tensor_auto_grad.py
import torch
dtype = torch.float
# 如果检测到GPU则在GPU设备上执行,否则在cpu上执行
device = torch.device('cuda:0' if torch.cuda.is_available() else 'cpu')
# N是批量大小;D_in是输入维度,H是隐藏的维度,D_out是输出维度
N, D_in, H, D_out = 64, 1000, 100, 10
# 创建随机输入和输出的数据
# 设置requires_grad=False(默认),不需要计算梯度
# 在方向传播过程中不需要对这些张量求梯度
x = torch.randn(N, D_in, device=device, dtype=dtype) # (64, 1000)
y = torch.randn(N, D_out, device=device, dtype=dtype) # (64, 10)
# 随机化初始权重
# 设置requires_grad=True表示需要计算梯度
# 在方向传播过程中需要对这些张量求梯度
w1 = torch.randn(D_in, H, device=device, dtype=dtype, requires_grad=True) # (1000, 100)
w2 = torch.randn(H, D_out, device=device, dtype=dtype, requires_grad=True) # (100, 10)
learning_rate = 1e-6
for t in range(500):
"""
前向传播:使用tensors上的操作计算预测值y_pred
由于w1和w2有requires_grad=True,涉及这些张量的操作将让PyTorch构建计算图
从而允许自动计算梯度。由于我们不再手工实现传播传播,所以不需要保留中间值的引用
"""
y_pred = x.mm(w1).clamp(min=0).mm(w2)
"""
使用tensors上的操作计算和打印loss
loss是一个形状为()的张量,loss.item得到这个张量对应数值
"""
loss = (y_pred - y).pow(2).sum()
if (t+1) % 50 == 0:
print("第", t, "次:loss.item() = ", loss.item(), " loss = ", loss)
"""
使用autograd计算方向传播,这个调用将计算loss对所有requires_grad=True的tensor求梯度
这次调用之后,w1.grad和w2.grad分别是loss对w1,w2的梯度张量
"""
loss.backward()
"""
使用梯度下降更新权重,对于这一步,我们只想对w1和w2的值进行原地改变,不想为更新阶段构建计算图
所以我们使用torch.no_grad()上下文管理器放置PyTorch更新构建计算图
"""
with torch.no_grad():
w1 -= learning_rate * w1.grad
w2 -= learning_rate * w2.grad
# 反向传播手动将梯度设置为0
w1.grad.zero_()
w2.grad.zero_()
运行结果如下:
第 49 次:loss.item() = 16584.423828125 loss = tensor(16584.4238, grad_fn=)
第 99 次:loss.item() = 463.129150390625 loss = tensor(463.1292, grad_fn=)
第 149 次:loss.item() = 19.573333740234375 loss = tensor(19.5733, grad_fn=)
第 199 次:loss.item() = 1.0451782941818237 loss = tensor(1.0452, grad_fn=)
第 249 次:loss.item() = 0.06843310594558716 loss = tensor(0.0684, grad_fn=)
第 299 次:loss.item() = 0.005556420423090458 loss = tensor(0.0056, grad_fn=)
第 349 次:loss.item() = 0.0007366308709606528 loss = tensor(0.0007, grad_fn=)
第 399 次:loss.item() = 0.00018612074200063944 loss = tensor(0.0002, grad_fn=)
第 449 次:loss.item() = 7.222769636427984e-05 loss = tensor(7.2228e-05, grad_fn=)
第 499 次:loss.item() = 3.7307498132577166e-05 loss = tensor(3.7307e-05, grad_fn=)
在底层,每一个原始的自动求导运算实际上是两个在Tensor上运行的函数。其中,forward函数计算从输入Tensors获得的输出Tensors。而backward函数接受输出Tensors对于某个标量值的梯度,并且计算输入Tensors相对于该相同标量值的梯度。
在PyTorch中,可以通过torch.autograd.Function的子类并实现farward和backward函数,来定义自己的自动求导运算。之后我们就可以使用这个新的自动梯度运算符了。然后,可以通过构造一个实例并像调用函数一样,传入包含输入数据的tensor调用它,这样来使用新的自动求导运算。
这个例子中,自定义一个自动求导函数来展示ReLU的非线性,并用它实现我们的两层网络:
#!/usr/bin/env torch
# -*- coding:utf-8 -*-
# @Time : 2021/2/5, 22:46
# @Author: Lee
# @File : customize_gradfunc.py
import torch
class MyReLU(torch.autograd.Function):
"""
我们可以通过建立torch.autograd的子类来实现我们自定义的autograd函数,
并完成张量的正向传播
"""
@staticmethod
def forward(ctx, x):
"""
在正向传播中,我们接收到一个上下文对象和一个包含输入的张量;
我们必须返回一个包含输出的张量
并且我们可以使用上下文对象来缓存对象,以便在反向传播中使用
"""
ctx.save_for_backward(x)
return x.clamp(min=0)
@staticmethod
def backward(ctx, grad_output):
"""
在反向传播中,我们接受到上下文对象和一个张量,
其包含了相对于正向传播错承重产生的输出的损失的梯度。
我们可以从上下文对象中检索缓存的数据,
并且必须计算并返回与正向传播的输入相关的损失的梯度
"""
x, = ctx.saved_tensors
grad_x = grad_output.clone()
grad_x[x < 0] = 0
return grad_x
dtype = torch.float
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
# N是批量大小;D_in是输入维度,H是隐藏的维度,D_out是输出维度
N, D_in, H, D_out = 64, 1000, 100, 10
# 创建随机输入和输出的数据
# 设置requires_grad=False(默认),不需要计算梯度
# 在方向传播过程中不需要对这些张量求梯度
x = torch.randn(N, D_in, device=device, dtype=dtype) # (64, 1000)
y = torch.randn(N, D_out, device=device, dtype=dtype) # (64, 10)
# 随机化初始权重
# 设置requires_grad=True表示需要计算梯度
# 在方向传播过程中需要对这些张量求梯度
w1 = torch.randn(D_in, H, device=device, dtype=dtype, requires_grad=True) # (1000, 100)
w2 = torch.randn(H, D_out, device=device, dtype=dtype, requires_grad=True) # (100, 10)
learning_rate = 1e-6
for t in range(500):
"""
正向传播:使用张量上的操作来计算输出值y
通过调用MyReLU.apply函数来使用自定义的ReLU
"""
y_pred = MyReLU.apply(x.mm(w1)).mm(w2)
# 计算并输出loss
loss = (y_pred - y).pow(2).sum()
if (t+1) % 50 == 0:
print("第", t, "次:loss.item() = ", loss.item(), " loss = ", loss)
# 使用autograd计算反向传播过程
loss.backward()
with torch.no_grad():
# 用梯度下降更新权重
w1 -= learning_rate * w1.grad
w2 -= learning_rate * w2.grad
# 在反向传播之后手动清零梯度
w1.grad.zero_()
w2.grad.zero_()
运行结果如下:
第 49 次:loss.item() = 17667.228515625 loss = tensor(17667.2285, grad_fn=)
第 99 次:loss.item() = 771.8282470703125 loss = tensor(771.8282, grad_fn=)
第 149 次:loss.item() = 56.19752502441406 loss = tensor(56.1975, grad_fn=)
第 199 次:loss.item() = 4.765769958496094 loss = tensor(4.7658, grad_fn=)
第 249 次:loss.item() = 0.43421220779418945 loss = tensor(0.4342, grad_fn=)
第 299 次:loss.item() = 0.041442546993494034 loss = tensor(0.0414, grad_fn=)
第 349 次:loss.item() = 0.004321477375924587 loss = tensor(0.0043, grad_fn=)
第 399 次:loss.item() = 0.000666604726575315 loss = tensor(0.0007, grad_fn=)
第 449 次:loss.item() = 0.0001781438768375665 loss = tensor(0.0002, grad_fn=)
第 499 次:loss.item() = 7.088546408340335e-05 loss = tensor(7.0885e-05, grad_fn=)