通过实例学习 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')
99 880.579689608281
199 854.0555627713447
299 835.0649579500803
399 821.369172352883
499 811.4200797717988
599 804.1424592009078
699 798.7835679283971
799 794.8123966679943
899 791.8516135436988
999 789.6312092332105
1099 787.9566727255892
1199 786.6869765834631
1299 785.7192368208233
1399 784.9779487653068
1499 784.4073829648856
1599 783.9661786053822
1699 783.6234753797376
1799 783.3561293941161
1899 783.146697690572
1999 782.9819710274254
Result: y = -0.05099192206877935 + 5.075189949472816 x + 0.004669889031269278 x^2 + 0.028076619049389115 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')
99 883.4296875
199 846.044921875
299 823.0543823242188
399 808.742919921875
499 799.6961059570312
599 793.8943481445312
699 790.1231079101562
799 787.6404418945312
899 785.9865112304688
999 784.872314453125
1099 784.113525390625
1199 783.5916748046875
1299 783.2293090820312
1399 782.9754028320312
1499 782.7958984375
1599 782.6681518554688
1699 782.5762939453125
1799 782.509765625
1899 782.4613647460938
1999 782.4259033203125
Result: y = 0.015941178426146507 + 2.6255147457122803 x + -0.0018874391680583358 x^2 + 0.028076613321900368 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')
99 943.6873168945312
199 903.8673095703125
299 873.939453125
399 851.4177856445312
499 834.4534301757812
599 821.6660766601562
699 812.0220336914062
799 804.74560546875
899 799.2537841796875
999 795.108154296875
1099 791.9780883789062
1199 789.6143798828125
1299 787.8292846679688
1399 786.4810791015625
1499 785.4628295898438
1599 784.693603515625
1699 784.1126708984375
1799 783.6737060546875
1899 783.34228515625
1999 783.091796875
Result: y = 0.032735299319028854 + 0.6361034512519836 x + -0.005412467289716005 x^2 + 0.028076613321900368 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)')
99 209.95834350585938
199 144.66018676757812
299 100.70249938964844
399 71.03519439697266
499 50.97850799560547
599 37.403133392333984
699 28.206867218017578
799 21.973188400268555
899 17.7457275390625
999 14.877889633178711
1099 12.931766510009766
1199 11.610918045043945
1299 10.714258193969727
1399 10.10548210144043
1499 9.692106246948242
1599 9.411375045776367
1699 9.220745086669922
1799 9.091285705566406
1899 9.003361701965332
1999 8.943639755249023
Result: y = -5.423830273798558e-09 + -2.208526849746704 * P3(1.3320399228078372e-09 + 0.2554861009120941 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')
99 672.3364868164062
199 448.7064208984375
299 300.4986572265625
399 202.26097106933594
499 137.13494873046875
599 93.9523696899414
699 65.31460571289062
799 46.318885803222656
899 33.71611785888672
999 25.35308074951172
1099 19.802162170410156
1199 16.116811752319336
1299 13.669382095336914
1399 12.043644905090332
1499 10.963366508483887
1599 10.245325088500977
1699 9.7678804397583
1799 9.450323104858398
1899 9.23902416229248
1999 9.098373413085938
Result: y = -0.006202241405844688 + 0.8414672017097473 x + 0.0010699888225644827 x^2 + -0.09115784615278244 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')
99 1610.013671875
199 844.4555053710938
299 525.6090698242188
399 334.3529968261719
499 213.2096405029297
599 135.09706115722656
699 83.0303955078125
799 48.23293685913086
899 26.453231811523438
999 14.763155937194824
1099 10.078741073608398
1199 9.046186447143555
1299 8.941937446594238
1399 8.894529342651367
1499 8.899309158325195
1599 8.912175178527832
1699 8.913202285766602
1799 8.911144256591797
1899 8.92212200164795
1999 8.92563247680664
Result: y = -0.0005531301139853895 + 0.8562383651733398 x + -0.0005647626821883023 x^2 + -0.0938328206539154 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()}')
99 62.18498229980469
199 58.930118560791016
299 55.8530387878418
399 52.94403076171875
499 50.19393539428711
599 47.59403991699219
699 45.136138916015625
799 42.812469482421875
899 40.61569595336914
999 38.538875579833984
1099 36.575462341308594
1199 34.71923828125
1299 32.964359283447266
1399 31.305295944213867
1499 29.736791610717773
1599 28.2539005279541
1699 26.85195541381836
1799 25.52651023864746
1899 24.273393630981445
1999 23.08867073059082
Result: y = -1.397053837776184 + -0.8716074824333191 x + 0.35939672589302063 x^2 + -0.23259805142879486 x^3

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()}')
1999 31.825870513916016
3999 30.36163330078125
5999 28.165559768676758
7999 4.4947919845581055
9999 25.11688804626465
11999 4.422863960266113
13999 22.777265548706055
15999 21.440027236938477
17999 20.374134063720703
19999 19.437679290771484
21999 18.513486862182617
23999 17.685436248779297
25999 16.829214096069336
27999 16.081615447998047
29999 15.38708782196045
Result: y = 0.5208832621574402 + -2.605482578277588 x + 0.06938754767179489 x^2 + 0.6473004221916199 x^3 + -0.033068109303712845 x^4 ? + -0.033068109303712845 x^5 ?