通过实例学习 PyTorch

通过范例学习 PyTorch

本博文通过几个独立的例子介绍了 PyTorch 的基础概念。

其核心,PyTorch 提供了两个主要的特征:

  • 一个 n-维张量(n-dimensional Tensor),类似 NumPy 但是可以运行在 GPU 设备上
  • 构建和训练神经网络,可自动求微分

我们将使用三阶多项式去拟合 y=sin(x) 的问题作为我们的例子。神经网络将会有 4 个参数,并且将使用梯度下降通过最小化(minimizing)网络输出和真实输出的欧氏距离(Euclidean distance)去拟合随机数据。

Tensors

热身:NumPy

在介绍 PyTorch 之前,我们首先使用 NumPy 实现一个神经网络。

NumPy 提供了一种 n-维数组(n-dimensional)数组对象,和许多操纵这些数组的函数。NumPy 对于科学计算是一个充满活力的框架;它不需要了解任何关于计算图(computation graphs)、深度学习或梯度。但是,我们可以轻松地使用 NumPy 操作手动实现网络的前向传播和反向传播,拟合一个三阶多项式到 sine 函数。

import numpy as np
import math

# 创建随机输入和输出的数据
x = np.linspace(-math.pi, math.pi, 2000)
y = np.sin(x)

# 随机初始化权重(weight)
a = np.random.randn()
b = np.random.randn()
c = np.random.randn()
d = np.random.randn()

learning_rate = 1e-6
for t in range(2000):
    # 前向传播:计算预测的 y
    # y = a + bx + cx^2 + dx^3
    y_pred = a + b * c + c * x ** 2 + d * x ** 3
    
    # 计算打印损失值(loss)
    loss = np.square(y_pred - y).sum()
    if t % 100 == 99:
        print(t, loss)
    
    # 反向传播计算 a, b, c, d 关于 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()
    
    # 更新权重参数(weight)
    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')

PyTorch: Tensors

尽管 NumPy 是一个非常棒的框架,但是它不可以利用 GPU 去加速数值计算。对于现代深度神经网络,GPU 通过提供了 50倍或以上 的加速,不幸地是 NumPy 对于深度学习是还不够的。

这里我们介绍了 PyTorch 最基础的概念:Tensor 。一个 PyTorch Tensor 从概念上与 NumPy 数组是完全相同的:Tensor 是一个 n 维数组(n-dimensional array),并且 PyTorch 提供了许多处理这些 Tensor 的函数。幕后,Tensor 记录了一个计算图(computation graph)和梯度,并且这些对于科学计算也是一个非常实用并充满活力的工具。

不像 NumPy,PyTorch Tensor 可以利用 GPU 加速数值计算。为了将 PyTorch Tensor 运行在 GPU 上,你只需要简单地指定正确的设备。

这里我们使用 PyTorch Tensor 去拟合一个三阶多项式到 sine 函数。就像上面 NumPy 的例子,我们需要手动实现网络的前向传播和反向传播。

import torch
import math

dtype = torch.float
device = torch.device("cpu")
# 下面这条注释,可以使用 GPU
# device = torch.device("cuda:0")

# 创建随机输入输出
x = torch.linspace(-math.pi, math.pi, 2000, device=device, dtype=dtype)
y = torch.sin(x)

# 随机初始化权重(weight)
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)

learnign_rate = 1e-6
for t in range(2000):
    # 前向传播:计算预测的 y
    y_pred = a + b * c + c * x ** 2 + d * x ** 3
    
    # 计算和输出损失值(loss)
    loss = (y_pred - y).pow(2).sum().item()
    if t % 100 == 99:
        print(t, loss)
        
    # 反向传播计算 a, b, c, d 关于损失值(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()
    
    # 使用梯度下降更新权重(weight)
    a -= learnign_rate * grad_a
    b -= learnign_rate * grad_b
    c -= learnign_rate * grad_c
    d -= learnign_rate * grad_d
    
print(f'Result: y = {a.item()} + {b.item()} x + {c.item()} x^2 + {d.item()} x^3')

自动求导

PyTorch: Tensor and autograd

在上面的例子中,我们必须手动实现神经网络的前向传播和反向传播。

对于一个小的两层网络手动实现反向传播并没有什么大不了的,但是对于更大更复杂的网络是一件非常可怕的事情。

幸亏的是,我们可以使用 自动微分(automatic differentiation 自动化神经网络的反向传播的计算。在 PyTorch 的 autograd 包正好提供了这个功能。当使用 autograd 时,神经网络的前向传播将定义一个 计算图(Computational graph ,图中的结点(nodes)将会是一个 Tensor,每条边(edges)将会是从一个输入 Tensor 产生输出 Tensor 的函数。反向传播通过计算图让我们轻松地计算梯度。

这听起来有些复杂,但是实际上是相当简单的。每一个 Tensor 代表了计算图中的结点。如果 x 是一个 Tensor,它就有 x.requires_grad=True 然后 x.grad 就是另一个张量,其持有 x 关于某个标量值的梯度。

这里我们使用 PyTorch Tensor 和 autograd 实现使用一个三阶多项式去拟合 sine 曲线的例子;现在,我们不再需要手动地实现网络的反向传播。

import torch
import math

dtype = torch.float
device = torch.device("cpu")
# 下面这条注释,可以使用 GPU
# device = torch.device("cuda:0")

# 创建 Tensor
# 默认情况下,requires_grad=False 表示我们不需要计算关于这些 Tensor 的梯度
x = torch.linspace(-math.pi, math.pi, 2000, device=device, dtype=dtype)
y = torch.sin(x)

# 创建随机权重(weight) Tensor
# 对于一个三阶多项式,我们需要 4 个参数
# 设置 requires_grad=True 表明我们在反向传播的时候想要计算关于这些 Tensor 的梯度
a = torch.randn((), device=device, dtype=dtype, requires_grad=True)
b = torch.randn((), device=device, dtype=dtype, requires_grad=True)
c = torch.randn((), device=device, dtype=dtype, requires_grad=True)
d = torch.randn((), device=device, dtype=dtype, requires_grad=True)

learnign_rate = 1e-6
for t in range(2000):
    # 使用 Tensor 上的运算前向传播计算预测的 y
    y_pred = a + b * c + c * x ** 2 + d * x ** 3
    
    # 使用 Tensor 上的操作计算并打印损失值(loss)
    # 现在,loss 是一个 Tensor,shape 是 (1,)
    # loss.item() 得到 loss 持有的标量值
    loss = (y_pred - y).pow(2).sum()
    if t % 100 == 99:
        print(t, loss.item())
        
    # 使用 autograd 计算反向传播。下面这个调用将会计算 loss 关于所有 requires_grad=True 的 Tensor 的梯度。
    # 在此之后,调用 a.grad, b.grad, c.grad, d.grad 将得到 a, b, c, d 关于 loss 的梯度
    loss.backward()
    
    # 使用梯度下降手动更新权重。使用 torch.no_grad() 包起来。
    # 因为这些权重(weight)都有 requires_grad=True 但是在 autograd 中,我们不需要跟踪这些操作。
    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')

PyTorchL 定义一个新的 autograd 函数

在底层,每一个原始自动求导(autograd)运算符实际上是两个操作在 Tensor 上的函数。前向传播(forward 函数计算出从输入 Tensor 到输出 Tensor。反向传播(backward 函数收到输出 Tensor 关于某个标量值的梯度,并且计算输入 Tensor 关于那些相同标量值的梯度。

在 PyTorch 中我们可以轻松地定义我们自己的自动求导运算符,一个 torch.autograd.Function 的子类并且实现 forwardbackward 函数。然后我们可以使用我们新定义的自动求导运算符,构建一个类实例(instance)然后像函数样调用它,传入输入数据的 Tensor。

在这个例子中,我们定义我们的模型为 \(y=a+b P_3(c+dx)\) 而不是 \(y=a+bx+cx^2+dx^3\),其中 \(P_3(x)=\frac{1}{2}\left(5x^3-3x\right)\) 是一个 3 次(degreeLegendre 多项式。对于计算 \(P_3\) 的前向传播和反向传播,我们写下自定义的自动求导函数,并用它实现我们的模型。

import torch
import math

class LegendrePolynomial3(torch.autograd.Function):
    """
    我们可以通过继承 torch.autograd.Function 实现我们自定义自动求导函数,
    并实现操作在 Tensor 上的前向传播和反向传播。
    """
    
    @staticmethod
    def forward(ctx, input):
        """
        在前向传播中,我们接受一个包含输入的 Tensor 并返回包含输出的 Tensor。
        ctx 是一个上下文(context)对象,用来存放反向传播计算时用到的信息。
        你可以使用 ctx.save_for_backward 方法缓存任意对象以供反向传播使用。
        """
        ctx.save_for_backward(input)
        return 0.5 * (5 * input ** 3 - 3 * input)
    
    @staticmethod
    def backward(ctx, grad_output):
        """
        在反向传播中,我们接收一个张量,其持有 loss 关于输出的梯度,
        并且我们需要计算 loss 关于输入的梯度。
        """
        input, = ctx.saved_tensors
        return grad_output * 1.5 * (5 * input ** 2 - 1)
    
dtype = torch.float
device = torch.device("cpu")
# device = torch.device("cuda:0")  # 取消注释可以使用 GPU

# 创建输入输出 Tensor。
# 默认情况下,requires_grad=False 表明我们在反向传播时不需要计算这些 Tensor 的梯度。
x = torch.linspace(-math.pi, math.pi, 2000, device=device, dtype=dtype)
y = torch.sin(x)

# 创建随机权重(weight)Tensor。在这个例子,我们需要 4 个权重:
# y = a + b * P3(c + d * x) 为保证收敛,这些权重需要被初始化成离正确结果不太远。
# 设置 requires_grad=True 表明我们在反向传播期间想要计算关于这些 Tensor 的梯度。
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):
    # 为了应用(apply)我们的函数,我们使用 Function.apply
    # 并将这个函数取个别名 P3
    P3 = LegendrePolynomial3.apply
    
    # 前向传播:使用操作(operations)计算预测的 y。
    # 我们使用我们自定义的 autograd 操作计算 P3。
    y_pred = a + b * P3(c + d * x)
    
    # 计算并输出损失值(loss)
    loss = (y_pred - y).pow(2).sum()
    if t % 100 == 99:
        print(t, loss.item())
        
    # 使用 autograd 计算反向传播。
    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

对于定义一个复杂的运算符和自动微分,计算图和 autograd 是一个非常强大的范例(paradigm),但是对于一个很大的神经网络来说,原生的(raw)autograd 就有一点低级(low-level)了。

当构建神经网络的时候,我们经常考虑将这些计算安排整理到一个 层(layers 中,在学习期间,一些 可学习参数(learnable parameters 将会被优化。

在 TensorFlow 中,像 Keras, TensorFlow-SlimTFLearn 包在原生计算图之上提供了更高阶的抽象,这非常有益于构建神经网络。

在 PyTorch 中,nn 包服务于相同的目的。nn 包定义了一套 Modules,大致上等价于神经网络中的层(layers)。一个 Module 接受输入 Tensor 并计算输出 Tensor,也许也会持有内部的状态(state),比如包含可学习的参数的(learnable parameters)Tensor。当训练神经网络时,nn 包也定了一套损失函数(loss function)。

在这个例子中,我们使用 nn 包实现我们的多项式模型。

import torch
import math

# 创建输入输出 Tensor
x = torch.linspace(-math.pi, math.pi, 2000)
y = torch.sin(x)

# 在这个例子中,输出 y 是一个(x,x^2,x^3)的线性函数,所以
# 我们可以考虑它是一个线性(linear layer)神经网络层。
# 让我们准备 Tensor(x,x^2,x^3)
p = torch.tensor([1, 2, 3])
xx = x.unsqueeze(-1).pow(p)

# 在上面的代码中,x.unsqueeze(-1) 有着 (2000,1)的形状(shape),并且 p 有着 (3,)的形状
# 对于这个例子,广播语义(broadcasting semantics),得到一个 (2000,3)的 Tensor。

# 使用 nn 包定义我们一系列的层的模型。nn.Sequential 是一个 Module,其包含其它 Modules
# 并按顺序应用它们产生输出。线性 Modules 从输入使用一个线性函数计算输出
# 并在内部持有模型的 weight 和 bias 的 Tensor。Flatten layer 展开线性层的输出到
# 一个匹配 y 的形状(shape)的一维(1D)的 Tensor。
model = torch.nn.Sequential(
    torch.nn.Linear(3, 1),
    torch.nn.Flatten(0, 1)
)

# nn 包同样包含流行的损失函数(loss function)的定义;在这个例子中,
# 我们将使用均方误差(Mean Square Error——MSE)作为我们的损失函数。
loss_fn = torch.nn.MSELoss(reduction='sum')

learning_rate = 1e-6
for t in range(2000):
    # 前向传播:通过传入 x 到 model 计算预测的 y。Module 对象重写了
    # __call__ 函数,所我们可以就像调用函数一样调用他们。当你这么做的时候
    # 传入输入 Tensor 到 Module 然后它计算产生输出的 Tensor。
    y_pred = model(xx)
    
    # 计算并打印损失值(loss)。我们传入 y 的预测值和真实值的 Tensor,
    # 之后 loss function 返回损失值(loss)的 Tensor。
    loss = loss_fn(y_pred, y)
    if t % 100 == 99:
        print(t, loss.item())
        
    # 运行反向传播之前清零一下梯度
    model.zero_grad()
    # 反向传播:计算 loss 关于所有 model 的可学习参数的梯度。从底层上来说,
    # 每一个 requires_grad=True 的 Module 的参数(parameters)都被存储在一个 Tensor 中,
    # 所以下面这个调用将计算 model 中所有可学习参数的的梯度。
    loss.backward()
    
    # 使用梯度下降更新权重。每一个参数都是一个 Tensor,
    # 所以我们就像之前一样得到它的梯度。
    with torch.no_grad():
        for param in model.parameters():
            param -= learning_rate * param.grad
    
# 你也可以就像得到列表(list)的第一个元素一样,得到 model 的第一层
linear_layer = model[0]

# 对于 linear layer,它的参数被存储为 weight 和 bias。
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

到目前为止,我们通过 torch.no_grad() 手动更改持有可学习参数的 Tensor 来更新了我们模型的权重。这对于一些简单的优化算法,比如随机梯度下降(stochastic gradient descent),并不是一个太大的负担,但是实际上,我们经常使用更复杂的优化器(Optimizer),比如 AdaGradRMSpropAdam等,训练神经网络。

PyTorch 里的 optim 包抽象了一个优化算法的思想,并且提供了常用的优化算法的实现。

在这个例子,我们依旧使用 nn 包定义我们的模型,但是我们将使用 optim 包提供的 RMSprop 算法优化模型。

import torch
import math


# 创建输入输出的 Tensor。
x = torch.linspace(-math.pi, math.pi, 2000)
y = torch.sin(x)

# 准备输入的 Tensor(x,x^2,x^3)。
p = torch.tensor([1, 2, 3])
xx = x.unsqueeze(-1).pow(p)

# 使用 nn 包定义我们的模型和损失函数。
model = torch.nn.Sequential(
    torch.nn.Linear(3, 1),
    torch.nn.Flatten(0, 1)
)
loss_fn = torch.nn.MSELoss(reduction='sum')

# 使用 optim 包定义一个将会为我们更新模型的权重(weight)的优化器(Optimizer)。
# 这里我们将使用 RMSprop,optim 包包含了许多其它优化算法。
# RMSprop 构造器的第一个参数是告诉优化器哪些 Tensor 应该被更新。
learning_rate = 1e-3
optimizer = torch.optim.RMSprop(model.parameters(), lr=learning_rate)
for t in range(2000):
    # 前向传播:传入 x 到 model 计算预测的 y
    y_pred = model(xx)
    
    # 计算并打印损失值(loss)
    loss = loss_fn(y_pred, y)
    if t % 100 == 99:
        print(t, loss.item())
    
    # 在反向传播之前,需要使用优化器(optimizer)对象清零所有将被更新的变量的梯度
    # (模型的可学习参数(learnable weight))。这是因为在默认情况下,不论何时 .backward() 被调用时,
    # 梯度会被累积在缓冲区(换言之,不会被覆盖)。可以通过 torch.autograd.backward 的官方文档查看更多细节。
    optimizer.zero_grad()
    
    # 反向传播:计算 loss 关于模型参数的梯度。
    loss.backward()
    
    # 调用优化器上的 step 函数,更新它的参数。
    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:定制 nn Modules

某些时候,你想要指定的模型比 Modules 存在的顺序(sequence)模型还要复杂,在这种情况下,你可以通过继承 nn.Module 的子类并且定义一个 forward 函数,这个函数使用其它 Modules 或者其它 autograd 操作符,接收输入 Tensor 并计算输出 Tensor。

在这个例子中,我们实现我们的三阶多项式作为定制的 Module 的子类。

import torch
import math

class Polynomial3(torch.nn.Module):
    def __init__(self):
        """
        在这个构造器,我们实例化四个参数并赋它们为成员 parameters。
        """
        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):
        """
        在前向传播函数,我们接收一个输入数据的 Tensor,并且我们必须返回输出数据的 Tensor。
        我们可以使用定义在构造器的 Modules 以及任意的操作在 Tensor 上的运算符。
        """
        return self.a + self.b * x + self.c * x ** 2 + self.d * x ** 3
    
    def string(self):
        """
        就像 Python 中的任意一个类一样,你也可以随便定义任何的方法(method)在 PyTorch Modules中。
        """
        return f'y = {self.a.item()} + {self.b.item()} x + {self.c.item()} x^2 + {self.d.item()} x^3'
    
# 创建输入输出的 Tensor。
x = torch.linspace(-math.pi, math.pi, 2000)
y = torch.sin(x)

# 实例化上面定义的类,构造我们的模型。
model = Polynomial3()

# 构造我们的损失函数(loss function)和一个优化器(Optimizer)。在 SGD 构造器里
# 调用 model.parameters(),构造器将包含 nn.Linear Module 的可学习的参数(learnable parameters)
# 其是模型(model)的成员变量。
criterion = torch.nn.MSELoss(reduce='sum')
optimizer = torch.optim.SGD(model.parameters(), lr=1e-6)
for t in range(2000):
    # 前向传播:传入 x 到模型计算预测的 y
    y_pred = model(x)
    
    # 计算并打印损失值(loss)
    loss = criterion(y_pred, y)
    if t % 100 == 99:
        print(t, loss.item())
        
    # 清零梯度,执行反向传播,更新参数。
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()
    
print(f'Result: {model.string()}')

PyTorch:控制流 + 权重(参数)共享

作为一个动态图和权重共享的例子,我们实现一个非常强大的模型:一个 3-5 阶的多项式,在前向传播时选择一个 3-5 之间的随机数,并且使用许多阶,多次重复使用相同的权重计算第四阶和第五阶。

对于这个模型,我们可以使用典型的 Python 控制流实现循环,并且当定义前向传播时,我们可以简单地复用相同的参数多次实现权重共享。

我们可以继承 Module 类轻松地实现这个模型。

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):
        """
        对于模型的前向传播,我们随机选择 4 或 5 并复用参数 e 计算这些阶的贡献(contribution)。
        因为每一次前向传播都构建了一个动态的计算图,当定义模型的前向传播时,
        我们可以使用常规的 Python 控制流操作符,像循环或条件语句。
        这里我们也看到了当定义一个计算图时重复使用相同的参数多次是相当安全。
        """
        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):
        """
        就像 Python 中的任意一个类一样,你也可以随便定义任何的方法(method)在 PyTorch Modules中。
        """
        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 ?'
    
# 创建持有输入输出的 Tensor。
x = torch.linspace(-math.pi, math.pi, 2000)
y = torch.sin(x)

# 通过实例化上面定义的类,构造我们的模型。
model = DynamicNet()

# 构造我们的损失函数(loss fcuntion)和一个优化器(Optimizer)。使用毫无特色的
# 随机梯度下降(stochastic gradient descent)训练这个强大的模型是艰难的,
# 所以我们使用动量(momentum)。
criterion = torch.nn.MSELoss(reduce='sum')
optimizer = torch.optim.SGD(model.parameters(), lr=1e-8, momentum=0.9)
for t in range(30000):
    # 前向传播:传入 x 到 model 计算预测的 y。
    y_pred = model(x)
    
    # 计算并打印损失值(loss)
    loss = criterion(y_pred, y)
    if t % 2000 == 1999:
        print(t, loss.item())
        
    # 清零梯度、执行反向传播、更新参数
    optimizer.zero_grad()
    loss.backward()
    optimizer.step()

print(f'Result: {model.string()}')
发表评论

评论已关闭。

相关文章