这篇博客瞄准的是 pytorch 官方教程中 Learning PyTorch
章节的 Learning PyTorch with Examples
部分。
完整网盘链接: https://pan.baidu.com/s/1L9PVZ-KRDGVER-AJnXOvlQ?pwd=aa2m 提取码: aa2m
主要由以下四个部分组成:
- Tensors
- Autograd
- nn Module
- Examples
PyTorch 的核心功能有两个:
- 可以在 GPU 上运行的n维Tensor;
- 用于构建和训练神经网络的自动微分;
这里将使用三阶多项式拟合 y = s i n ( x ) y=sin(x) y=sin(x) 作为运行示例。网络有四个参数,并将通过梯度下降进行训练,以通过最小化网络输出和真实输出之间的欧几里得距离来拟合随机数据。
Tensors
Numpy 梯度下降拟合三阶多项式
Numpy 提供了一个 n 维数组对象,以及许多用于操作这些数组的函数。Numpy 是一个通用的科学计算框架,通过pytorch可以轻松地使用 numpy 来拟合三阶多项式到正弦函数,方法是使用 numpy 操作手动实现通过网络的前向和后向传递:
导入依赖包
import numpy as np
import math
定义一个随机的输入 x x x 与输出 y y y
x = np.linspace(-math.pi, math.pi, 2000)
y = np.sin(x)
随机初始化模型的权重
a = np.random.randn()
b = np.random.randn()
c = np.random.randn()
d = np.random.randn()
手动模拟神经网络MSE梯度下降调参
learning_rate = 1e-6
for t in range(2000):
y_pred = a + (b*x) + (c*x**2) + (d*x**3) # y=a+bx+cx^2+dx^3
loss = np.square(y_pred-y).sum()
if t % 100 == 99:
print(f"[{t+1}/2000]: loss={loss}")
# 计算梯度
grad_y_pred = 2.0 * (y_pred - y)
grad_a = grad_y_pred.sum()
grad_b = (grad_y_pred * x).sum()
grad_c = (grad_y_pred * x**2).sum()
grad_d = (grad_y_pred * x**3).sum()
# 更新权重
a -= learning_rate * grad_a
b -= learning_rate * grad_b
c -= learning_rate * grad_c
d -= learning_rate * grad_d
print(f'Result: y = {a} + {b} x + {c} x^2 + {d} x^3')
这里在loss时使用的是MSE:
L o s s = ∑ ( y p r e d − y ) 2 Loss=\sum(y_{pred}-y)^{2} Loss=∑(ypred−y)2
因此损失函数的梯度为:
∂ L ∂ y p r e d = ∂ ( y p r e d − y ) 2 ∂ y p r e d = 2 ( y p r e d − y ) \frac{\partial{L}}{\partial{y_{pred}}}=\frac{\partial{(y_{pred}-y)^{2}}}{\partial{y_pred}}=2(y_{pred}-y) ∂ypred∂L=∂ypred∂(ypred−y)2=2(ypred−y)
由链式求导法则得所有参数的梯度为
∂ L ∂ a = ∑ 2 ( y p r e d − y ) ∂ L ∂ b = ∑ 2 ( y p r e d − y ) x ∂ L ∂ c = ∑ 2 ( y p r e d − y ) x 2 ∂ L ∂ d = ∑ 2 ( y p r e d − y ) x 3 \begin{align} \frac{\partial{L}}{\partial{a}} &= \sum{2(y_{pred}-y)} \\ \frac{\partial{L}}{\partial{b}} &= \sum{2(y_{pred}-y)x} \\ \frac{\partial{L}}{\partial{c}} &= \sum{2(y_{pred}-y)x^{2}} \\ \frac{\partial{L}}{\partial{d}} &= \sum{2(y_{pred}-y)x^{3}} \end{align} ∂a∂L∂b∂L∂c∂L∂d∂L=∑2(ypred−y)=∑2(ypred−y)x=∑2(ypred−y)x2=∑2(ypred−y)x3
那么参数更新使用梯度下降:
θ = θ − α ∂ L ∂ θ \theta=\theta-\alpha\frac{\partial{L}}{\partial{\theta}} θ=θ−α∂θ∂L
pytorch 梯度下降拟合三阶多项式
Numpy 无法利用 GPU 来加速其数值计算。对于现代深度神经网络而言,GPU 通常可以提供 50 倍或更高的加速。PyTorch 概念Tensor与numpy 数组相同,是一个 n 维数组。PyTorch 提供了许多用于操作这些张量的函数。与 numpy 不同的是,PyTorch Tensor可以利用 GPU 来加速其数值计算。这里使用 Tensor 将三阶多项式拟合为正弦函数。与上面的 numpy 示例类似,需要手动实现通过网络的前向和后向传递。
导入依赖包
import torch
import math
定义一个随机的输入 x 与输出 y
dtype = torch.float
device = torch.device('cuda:0')
x = torch.linspace(-math.pi, math.pi, 2000, device=device, dtype=dtype)
y = torch.sin(x)
随机初始化模型的权重
a = torch.randn((), device=device, dtype=dtype)
b = torch.randn((), device=device, dtype=dtype)
c = torch.randn((), device=device, dtype=dtype)
d = torch.randn((), device=device, dtype=dtype)
手动模拟神经网络MSE梯度下降调参
learning_rate = 1e-6
for t in range(2000):
y_pred = a + (b*x) + (c*x**2) + (d*x**3) # y=a+bx+cx^2+dx^3
loss = (y_pred-y).pow(2).sum().item()
if t % 200 == 99:
print(f"[{t+1}/2000]: loss={loss}")
# 计算梯度
grad_y_pred = 2.0 * (y_pred - y)
grad_a = grad_y_pred.sum()
grad_b = (grad_y_pred * x).sum()
grad_c = (grad_y_pred * x ** 2).sum()
grad_d = (grad_y_pred * x ** 3).sum()
# 更新权重
a -= learning_rate * grad_a
b -= learning_rate * grad_b
c -= learning_rate * grad_c
d -= learning_rate * grad_d
print(f'Result: y = {a.item()} + {b.item()} x + {c.item()} x^2 + {d.item()} x^3')
可以看到代码基本与 Numpy 的一样,但如果你真的运行后会发现这个结果是秒出的,而 Numpy 的结果是一条一条打印的。
Autograd
Tensor & Autograd
在上面的例子中,必须手动实现神经网络的前向和后向传递。pytorch 提供了自动微分机来自动计算神经网络中的反向传播,在PyTorch 的 autograd
包中。使用 autograd
时,网络的前向传递将定义一个计算图,图中的节点是Tensor,边是从输入Tensor产生输出Tensor的函数,通过此图反向传播以轻松计算梯度。每个Tensor代表计算图中的一个节点。如果 x x x 是具有 x.requires_grad=True
的Tensor,则 x.grad
是另一个Tensor,保存了 x
相对于某个标量值的梯度。
这里使用 PyTorch Tensors
和 autograd
来实现用三阶多项式示例拟合正弦波,这部分内容和上面的非常相似,只不过用pytorch的特性代替了一些原本需要手算的部分。
导入依赖的库:
import torch
import math
查看当前可用的计算加速设备,这里使用了 torch.set_defaule_device()
后面就不用将每个变量手动移动了
dtype = torch.float
device = torch.accelerator.current_accelerator().type if torch.accelerator.is_available() else 'cpu'
print(f"Device {device}")
torch.set_default_device(device)
定义需要被拟合的sin函数
x = torch.linspace(-math.pi, math.pi, 2000, dtype=dtype)
y = torch.sin(x)
定义模型的权重
a = torch.randn((), dtype=dtype, requires_grad=True)
b = torch.randn((), dtype=dtype, requires_grad=True)
c = torch.randn((), dtype=dtype, requires_grad=True)
d = torch.randn((), dtype=dtype, requires_grad=True)
定义训练流程
learning_rate = 1e-6
for t in range(2000):
y_pred = a + (b*x) + (c*x**2) + (d*x**3) # y=a+bx+cx^2+dx^3
loss = (y_pred-y).pow(2).sum() # 此处loss天然是一个Tensor,求loss和
if t % 100 == 99:
print(f"[{t+1}/2000]: loss={loss}")
# 反向传播
loss.backward()
# 模拟optimizer梯度下降动作
with torch.no_grad(): # 在进行梯度下降时要关闭梯度计算,避免出现混乱
a -= learning_rate * a.grad
b -= learning_rate * b.grad
c -= learning_rate * c.grad
d -= learning_rate * d.grad
# 在完成一次梯度计算后将梯度清空
a.grad = None
b.grad = None
c.grad = None
d.grad = None
print(f'Result: y = {a.item()} + {b.item()} x + {c.item()} x^2 + {d.item()} x^3')
Defining new autograd functions
在底层每个原始自动求导运算符实际上是两个对Tensor进行操作的函数:
- 前向函数根据输入Tensor计算输出Tensor;
- 后向函数接收输出Tensor相对于某个标量值的梯度,并计算输入Tensor相对于同一标量值的梯度;
在 PyTorch 中,可以通过定义 torch.autograd.Function
的子类并实现前向和后向函数来定义自动求导运算符,然后通过构造一个实例并像函数一样调用它来使用的新自动求导运算符,传递包含输入数据的Tensor。
下面这个例子以拟合 y = a + b P 3 ( c + d x ) , P 3 = 1 / 2 ( 5 x 3 − 3 x ) y=a+bP_{3}(c+dx), P_{3}=1/2(5x^{3}-3x) y=a+bP3(c+dx),P3=1/2(5x3−3x) 函数为例。
导入依赖库
import torch
import math
定义拟合函数中 P3 的部分
class LegendrePolynomial3(torch.autograd.Function):
@staticmethod
def forward(ctx, input):
ctx.save_for_backward(input)
return 0.5 * (5*input**3 - 3*input)
@staticmethod
def backward(ctx, grad_output):
input, = ctx.saved_tensors
return grad_output * 1.5 * (5*input**2 - 1) # 求导后的函数
查看计算加速设备
dtype = torch.float
device = torch.device('cuda:0')
定义sin变量
x = torch.linspace(-math.pi, math.pi, 2000, dtype=dtype)
y = torch.sin(x)
定义权重
a = torch.full((), 0.0, device=device, dtype=dtype, requires_grad=True)
b = torch.full((), -1.0, device=device, dtype=dtype, requires_grad=True)
c = torch.full((), 0.0, device=device, dtype=dtype, requires_grad=True)
d = torch.full((), 0.3, device=device, dtype=dtype, requires_grad=True)
模拟梯度下降
learning_rate = 5e-6
for t in range(2000):
P3 = LegendrePolynomial3.apply
y_pred = a + b*P3(c+d*x)
loss = (y_pred-y).pow(2).sum()
if t % 100 == 99:
print(f"[{t+1}/2000]: loss={loss.item()}")
loss.backward()
with torch.no_grad():
a -= learning_rate * a.grad
b -= learning_rate * b.grad
c -= learning_rate * c.grad
d -= learning_rate * d.grad
a.grad = None
b.grad = None
c.grad = None
d.grad = None
print(f'Result: y = {a.item()} + {b.item()} * P3({c.item()} + {d.item()} x)')
nn.module
PyTorch: nn
计算图和自动求导是定义复杂运算符和自动求导的非常强大的范例;然而对于大型神经网络来说,原始的自动求导可能有点太低级了。在构建神经网络时,经常考虑将计算安排到Layer中,其中一些Layer具有可学习的参数,这些参数将在学习过程中进行优化。
在 TensorFlow 中,Keras、TensorFlow-Slim 和 TFLearn 等包提供了对原始计算图的更高级别的抽象。在 PyTorch 中,nn
包也用于同样的目的。nn
包定义了一组模块,它们大致相当于神经网络层。模块接收输入Tensor并计算输出Tensor,但也可能保存内部状态,例如包含可学习参数的Tensor,nn
包还定义了一组常用的损失函数。
在这个例子中使用 nn 包来实现多项式模型网络:
导入依赖库
import torch
import math
定义真值
dtype = torch.float
x = torch.linspace(-math.pi, math.pi, 2000, dtype=dtype)
y = torch.sin(x)
定义输入tensor
p = torch.tensor([1,2,3])
xx = x.unsqueeze(-1).pow(p)
定义模型
model = torch.nn.Sequential(
torch.nn.Linear(3,1),
torch.nn.Flatten(0, 1)
)
定义损失函数
loss_fn = torch.nn.MSELoss(reduction='sum')
执行拟合过程
learning_rate = 1e-6
for t in range(2000):
y_pred = model(xx)
loss = loss_fn(y_pred, x)
if t % 100 == 99:
print(f"[{t+1}/2000]: loss={loss.item()}")
# 清空模型中参数的梯度
model.zero_grad()
# 梯度下降反向传播
loss.backward()
with torch.no_grad():
for param in model.parameters():
param -= learning_rate * param.grad
模型拟合结果
linear_layer = model[0]
print(f'Result: y = {linear_layer.bias.item()} + {linear_layer.weight[:, 0].item()} x + {linear_layer.weight[:, 1].item()} x^2 + {linear_layer.weight[:, 2].item()} x^3')
PyTorch.optim
这对于简单的优化算法(如随机梯度下降)来说并不是一个巨大的负担,但在实践中,经常需要使用更复杂的优化器(如 AdaGrad、RMSProp、Adam 等)来训练神经网络。PyTorch 中的 optim
包抽象了优化算法的概念,并提供了常用优化算法的实现。
在此示例中,将使用 nn
包来定义模型,使用 optim
包提供的 RMSprop
算法来优化模型。
导入依赖库
import torch
import math
定义真值
x = torch.linspace(-math.pi, math.pi, 2000, dtype=torch.float)
y = torch.sin(x)
定义输入tensor
p = torch.tensor([1,2,3])
xx = x.unsqueeze(-1).pow(p)
定义模型 & 损失函数 & 优化器
learning_rate = 1e-3
model = torch.nn.Sequential(
torch.nn.Linear(3,1),
torch.nn.Flatten(0, 1)
)
loss_fn = torch.nn.MSELoss(reduction='sum')
optimizer = torch.optim.RMSprop(model.parameters(), lr=learning_rate)
执行拟合过程
for t in range(2000):
y_pred = model(xx)
loss = loss_fn(y_pred, y)
if t % 100 == 99:
print(f"[{t+1}/2000]: loss={loss.item()}")
# 优化器清空梯度
optimizer.zero_grad()
# loss反向传播
loss.backward()
# 优化器执行参数调整
optimizer.step()
模型拟合结果
linear_layer = model[0]
print(f'Result: y = {linear_layer.bias.item()} + {linear_layer.weight[:, 0].item()} x + {linear_layer.weight[:, 1].item()} x^2 + {linear_layer.weight[:, 2].item()} x^3')
PyTorch: Custom nn Modules
有时需要指定比现有模块序列更复杂的模型,可以通过子类化 nn.Module
并定义一个前向来定义自己的模块,该前向接收输入Tensor并使用其他模块或Tensor上的其他自动求导操作生成输出Tensor。
在此示例中,将三阶多项式实现为自定义模块子类:
导入依赖库
import torch
import math
定义三阶多项式
class Polynomial3(torch.nn.Module):
def __init__(self):
super().__init__()
self.a = torch.nn.Parameter(torch.randn(()))
self.b = torch.nn.Parameter(torch.randn(()))
self.c = torch.nn.Parameter(torch.randn(()))
self.d = torch.nn.Parameter(torch.randn(()))
def forward(self, x):
return self.a + (self.b*x) + (self.c*x**2) + (self.d*x**3) # y= a + bx + cx^2 + dx^3
def string(self):
return f'y = {self.a.item()} + {self.b.item()} x + {self.c.item()} x^2 + {self.d.item()} x^3'
定义真值
x = torch.linspace(-math.pi, math.pi, 2000, dtype=torch.float)
y = torch.sin(x)
定义模型 & 损失函数 & 优化器
model = Polynomial3()
criterion = torch.nn.MSELoss(reduction='sum')
optimizer = torch.optim.SGD(model.parameters(), lr=1e-6)
执行拟合
for t in range(2000):
y_pred = model(x)
loss = criterion(y_pred, y)
if t % 100 == 99:
print(f"[{t+1}/2000]: loss={loss.item()}")
# 优化
optimizer.zero_grad()
loss.backward()
optimizer.step()
查看拟合结果
print(f"Fitness Result: {model.string()}")
PyTorch: Control Flow + Weight Sharing
下面这个例子是动态图和权重共享的一个例子,实现了一个三阶+五阶多项式,在每次前向传递时选择一个 3 到 5 之间的随机数并使用对应的阶数,多次重复使用相同的权重来计算四阶和五阶。
对于这个模型可以使用普通的 Python 流控制来实现循环,通过在定义前向传递时多次重复使用相同的参数来实现权重共享。
导入依赖库
import random
import torch
import math
定义多项式
class DynamicNet(torch.nn.Module):
def __init__(self):
super().__init__()
self.a = torch.nn.Parameter(torch.randn(()))
self.b = torch.nn.Parameter(torch.randn(()))
self.c = torch.nn.Parameter(torch.randn(()))
self.d = torch.nn.Parameter(torch.randn(()))
self.e = torch.nn.Parameter(torch.randn(()))
def forward(self, x):
y = self.a + (self.b*x) + (self.c*x**2) + (self.d*x**3)
for exp in range(4, random.randint(4,6)):
y = y + (self.e * x ** exp)
return y
def string(self):
return f'y = {self.a.item()} + {self.b.item()} x + {self.c.item()} x^2 + {self.d.item()} x^3 + {self.e.item()} x^4 ? + {self.e.item()} x^5 ?'
定义真值
x = torch.linspace(-math.pi, math.pi, 2000, dtype=torch.float)
y = torch.sin(x)
定义模型等
model = DynamicNet()
criterion = torch.nn.MSELoss(reduction='sum')
optimizer = torch.optim.SGD(model.parameters(), lr=1e-8, momentum=0.9)
执行拟合
for t in range(30000):
y_pred = model(x)
loss = criterion(y_pred, y)
if t % 3000 == 2999:
print(f"[{t+1}/2000]: loss={loss.item()}")
optimizer.zero_grad()
loss.backward()
optimizer.step()
print(f"Fitness Result: {model.string()}")