上一节,我们写了很多代码,但是不知道你有没有注意,那些代码看起来跟PyTorch关系并不是很大啊,貌似很多都是Python原生代码?
如果你是这样的感觉,那我要告诉你,你感觉的没有错。前面主要在于机制的理解,我们实际上用手动的方式实现了一遍模型求解的过程,主要的改进就是使用了PyTorch里面的tensor数据结构,但是这还不够,PyTorch提供了很多强大的功能,当然不只是在处理tensor上面的,接下来我们就要看一下,PyTorch提供的方便我们运算的各种功能。
自动计算梯度
上次我们用手动求导计算梯度,可是你别忘了,那个包浆的温度计变换只需要2个参数,而如果有10亿个参数,那用手可是求导不过来啊。不要怕,PyTorch给出了自动求导机制。在PyTorch中,可以存储张量的生产路径,包括一个张量经过了何种计算,得到的结果有哪些,借助这个能力,对于我们用到的tensor,就可以找到它的爷爷tensor和它的爷爷的爷爷tensor,并且自动对这些操作求导,所有这些只需要你的一句“我需要autograd功能”。
我们来看一下实现方式,如果你已经把上一节的代码关了,没关系,这里我们从头写起,包括原始数据,紧接着是模型函数和loss函数,最后是给params初始化,这里唯一的区别就是,我们之前的初始化参数是这么写的: params = torch.tensor([1.0, 0.0]) 现在我们在tensor方法内部加上了一个参数requires_grad,并给它赋值True
代码语言:javascript复制%matplotlib inline
import numpy as np
import torch
torch.set_printoptions(edgeitems=2)
t_c = torch.tensor([0.5, 14.0, 15.0, 28.0, 11.0, 8.0,
3.0, -4.0, 6.0, 13.0, 21.0])
t_u = torch.tensor([35.7, 55.9, 58.2, 81.9, 56.3, 48.9,
33.9, 21.8, 48.4, 60.4, 68.4])
t_un = 0.1 * t_u
def model(t_u, w, b):
return w * t_u b
def loss_fn(t_p, t_c):
squared_diffs = (t_p - t_c)**2
return squared_diffs.mean()
#唯一改变
params = torch.tensor([1.0, 0.0], requires_grad=True)
加入这个requires_grad=True之后,意味着所有后续跟params相关的调用和操作记录都会被保留下来,任何一个经过params变换得到的新的tensor都可以追踪它的变换记录,如果它的变换函数是可微的,导数的值会被自动放进params的grad属性中。
让我们看一下代码
代码语言:javascript复制loss = loss_fn(model(t_u, *params), t_c)
loss.backward() #对loss进行反向传播
#输出params的梯度看看
params.grad
outs:tensor([4517.2969, 82.6000])
对于上面输出的grad有没有一丝熟悉的味道?这个结果跟我们之前第一次执行手动编写的grad函数时的结果是一样的,也就是说这里的自动方法跟我们前面手动编写的逻辑可以认为是一样的。
值得注意的是,我们实际的运算往往不是这么简单的,可能会涉及到若干个requires-grad为True的张量进行运算,在这种情况下,PyTorch会把整个计算图上的损失的导数,并把这些结果累加到grad属性中。
这里涉及到一个计算图的概念,大意是在PyTorch底层为tensor及运算构建了一个图关系,前面说到的关于反向传播也都是基于这个图上的存储关系进行的。这关系到PyTorch底层的运行逻辑,这里我们先不做太多的探讨,如果你对PyTorch的底层运行逻辑感兴趣可以进行深度的学习,否则,在这里我们还是先来看看它到底怎么去用的问题。
在调用backward()的时候,将会把导数累加在叶节点上,如果提前调用backward(),则会再次调用backward(),每个叶节点上的梯度将在上一次迭代中计算的梯度之上累加(求和),这会导致梯度计算的结果出错。
这里所谓的叶节点你可以认为就是我们最开始的那个tensor,在反向传播的路径里面它处于最末端,所以可以称为叶节点,当然在计算图上面,我们也可以把一些中间节点强行设置成叶节点,当然这就会使得梯度不再向下传导。
如果要防止这个问题发生,我们需要在每次迭代的时候手动的把梯度置为零。这看起来或多或少有点麻烦,为啥不自动在迭代的时候清零呢?按官方解释就是增加代码的灵活性,万一你需要不清零呢?所以这个事情我们先把它记住,不去探究深层次原因,将来如果遇到自然就能明白了。
代码语言:javascript复制if params.grad is not None:
params.grad.zero_()
这时候我们使用自动反向传播机制来改写我们之前的代码
代码语言:javascript复制with torch.no_grad的作用 在该模块下,所有计算得出的tensor的requires_grad都自动设置为False。
def training_loop(n_epochs, learning_rate, params, t_u, t_c):
for epoch in range(1, n_epochs 1):
if params.grad is not None:
params.grad.zero_()
t_p = model(t_u, *params)
loss = loss_fn(t_p, t_c)
loss.backward()
with torch.no_grad():
params -= learning_rate * params.grad
if epoch % 500 == 0:
print('Epoch %d, Loss %f' % (epoch, float(loss)))
return params
输出可以自己看一下,跟之前并没有什么区别,loss徘徊在2.9左右,不同的是我们让PyTorch自动的处理了梯度计算。
优化器
然后我们再来看另一个可以优化的地方。就是关于参数更新这块, params -= learning_rate * params.grad 我们这里采用的通过计算梯度,并按照梯度方向更新参数,这个计算称作梯度下降方法,而且是最原始的批量梯度下降方法。在每一个epoch,所有训练样本都会用于计算梯度,这个方案很稳妥,但是如果我们的样本很多的时候就不妙了,比如说计算一次就需要耗费大量的时间。
在PyTorch中提供了一个optim模块,里面收集了很多种优化方法
代码语言:javascript复制dir() 函数不带参数时,返回当前范围内的变量、方法和定义的类型列表;带参数时,返回参数的属性、方法列表。如果参数包含方法dir(),该方法将被调用。如果参数不包含dir(),该方法将最大限度地收集参数信息。
import torch.optim as optim
dir(optim)
outs:
['ASGD',
'Adadelta',
'Adagrad',
'Adam',
'AdamW',
'Adamax',
'LBFGS',
'NAdam',
'Optimizer',
'RAdam',
'RMSprop',
'Rprop',
'SGD',
'SparseAdam',
'__builtins__',
'__cached__',
'__doc__',
'__file__',
'__loader__',
'__name__',
'__package__',
'__path__',
'__spec__',
'_functional',
'_multi_tensor',
'lr_scheduler',
'swa_utils']
这些大写字母开头的就是优化算法,可以看到非常著名的adam,adamx等等。关于每个优化器都是怎么去优化的,这里就先不讨论了,我们先看优化器怎么用。
优化器接收参数tensor,读取他们的grad属性并对其执行更新的操作,然后再把接力棒交给模型。
接下来让我们使用优化器来实现梯度下降。我们使用了一个叫SGD的优化器,这个称为随机梯度下降,这个方法是每次计算只随机采用一个样本,大大降低了计算成本。
代码语言:javascript复制params = torch.tensor([1.0, 0.0], requires_grad=True)
learning_rate = 1e-5
optimizer = optim.SGD([params], lr=learning_rate)
t_p = model(t_u, *params)
loss = loss_fn(t_p, t_c)
loss.backward()
optimizer.step() #调用step()方法,就会更新params的值
params
outs:
tensor([ 9.5483e-01, -8.2600e-04], requires_grad=True)
正如我们前面所说,我们需要在执行反向传播前手动的把梯度归零,然后再次把训练跑起来。
代码语言:javascript复制def training_loop(n_epochs, optimizer, params, t_u, t_c):
for epoch in range(1, n_epochs 1):
t_p = model(t_u, *params)
loss = loss_fn(t_p, t_c)
optimizer.zero_grad() #手动调用梯度归零方法
loss.backward()
optimizer.step()
if epoch % 500 == 0:
print('Epoch %d, Loss %f' % (epoch, float(loss)))
return params
params = torch.tensor([1.0, 0.0], requires_grad=True)
learning_rate = 1e-2
optimizer = optim.SGD([params], lr=learning_rate)
training_loop(
n_epochs = 5000,
optimizer = optimizer,
params = params,
t_u = t_un,
t_c = t_c)
outs:
Epoch 500, Loss 7.860115
Epoch 1000, Loss 3.828538
Epoch 1500, Loss 3.092191
Epoch 2000, Loss 2.957698
Epoch 2500, Loss 2.933134
Epoch 3000, Loss 2.928648
Epoch 3500, Loss 2.927830
Epoch 4000, Loss 2.927679
Epoch 4500, Loss 2.927652
Epoch 5000, Loss 2.927647
tensor([ 5.3671, -17.3012], requires_grad=True)
这个地方你可以把优化器换成你喜欢的一个其他优化器来试试,当然你也可以去了解一下每个优化器都有什么特点,然后跑起来看看。