直接动手,在代码中理解深度学习的原理。复杂的代码看不懂,那就从简单的代码开始看起。
举个例子,如果你对梯度下降没有彻底弄懂,先直接写几行代码,跑一个简单的例子。
import torch
w = torch.tensor([0.5,0.5],requires_grad=True)
loss = w[0]**2 + 2*w[1]**2
opti = torch.optim.SGD([w],lr=0.01)
loss.backward()
print("w的梯度:",w.grad)
print("w:",w)
opti.step()
print("w:",w)
result:
w的梯度: tensor([1., 2.])
w: tensor([0.5000, 0.5000], requires_grad=True)
w: tensor([0.4900, 0.4800], requires_grad=True)
w是参数,在实际项目中几百万个参数太复杂了,那我直接指定两个参数行不行?
loss是简单的损失函数:
梯度可以直接口算:
梯度更新公式: $w=w-learning\_rate*\frac{dloss}{dw}\\=[0.5,0.5]-0.01*[1,2]\\=[0.49,0.48]$
对照代码的结果,发现结果是一致的,是不是可以加深理解梯度下降法?
多改动代码中的一些参数,多试试不同的例子,可以更深入地理解原理。
改动一:多更新一次参数,会怎样?
import torch
w = torch.tensor([0.5,0.5],requires_grad=True)
loss = w[0]**2 + 2*w[1]**2
opti = torch.optim.SGD([w],lr=0.01)
loss.backward()
print("w的梯度:",w.grad)
print("w:",w)
opti.step()
print("w:",w)
opti.step()
print("w:",w)
result:
w的梯度: tensor([1., 2.])
w: tensor([0.5000, 0.5000], requires_grad=True)
w: tensor([0.4900, 0.4800], requires_grad=True)
w: tensor([0.4800, 0.4600], requires_grad=True)
可以看到,opti.step()的作用只是机械地再做一次梯度更新。
实际训练中,是不是这样一步一步地更新参数?并不是。更新完参数后,需要用新的参数跑下一批数据,然后得到新的loss,算出新的梯度。
于是应该做一些改动:更新完参数后,让梯度清零,然后重新做梯度计算。
import torch
w = torch.tensor([0.5,0.5],requires_grad=True)
loss = w[0].clone()**2 + 2*w[1].clone()**2
opti = torch.optim.SGD([w],lr=0.01)
loss.backward(retain_graph=True)
print("w的梯度:",w.grad)
print("w:",w)
opti.step()
print("w:",w)
opti.zero_grad()
loss = w[0].clone()**2 + 2*w[1].clone()**2
loss.backward()
print("w的梯度:",w.grad)
opti.step()
print("w:",w)
result:
w的梯度: tensor([1., 2.])
w: tensor([0.5000, 0.5000], requires_grad=True)
w: tensor([0.4900, 0.4800], requires_grad=True)
w的梯度: tensor([0.9800, 1.9200])
w: tensor([0.4802, 0.4608], requires_grad=True)
由于w已经变成[0.49,0.48]
那么 $w=w-learning\_rate*\frac{dloss}{dw}\\=[0.49,0.48]-0.01*[0.98,1.92]\\=[0.4802,0.4608]$
可以和代码对得上。
这里做了两次梯度更新。如果要做很多次呢?自然而然就会用到循环了。
改动二:看看SGD还有哪些参数可以调整的?比如权值衰减。
import torch
w = torch.tensor([0.5,0.5],requires_grad=True)
loss = w[0].clone()**2 + 2*w[1].clone()**2
opti = torch.optim.SGD([w],lr=0.01,weight_decay=1)
loss.backward(retain_graph=True)
print("w的梯度:",w.grad)
print("w:",w)
opti.step()
print("w:",w)
opti.step()
print("w:",w)
result:
w的梯度: tensor([1., 2.])
w: tensor([0.5000, 0.5000], requires_grad=True)
w: tensor([0.4850, 0.4750], requires_grad=True)
w: tensor([0.4702, 0.4502], requires_grad=True)
如果引入权值衰减,相当于损失函数增加了参数向量的二范数的平方,即
$loss=loss+\frac{\lambda}{2}||w||^2\\=w_{1}^{2}+2*w_{2}^{2}+\frac{\lambda}{2}(w_{1}^{2}+w_{2}^{2})$
代码中weight_decay=1,即
$w=w-learning\_rate*\frac{dloss}{dw}\\=[0.5,0.5]-0.01*[1.5,2.5]\\=[0.485,0.475]$
可以和代码结果对上。
注意,打印梯度的时候,你会发现w的梯度没有把权值衰减考虑进去。因为权值衰减是优化器要考虑的事情,而反向传播(求梯度)不关优化器的事情。权值衰减是在优化器阶段做的。
改动三:看看SGD还有哪些参数可以调整的?比如momentum。
import torch
w = torch.tensor([0.5,0.5],requires_grad=True)
loss = w[0].clone()**2 + 2*w[1].clone()**2
opti = torch.optim.SGD([w],lr=0.01,momentum=0.1)
loss.backward(retain_graph=True)
print("w的梯度:",w.grad)
print("w:",w)
opti.step()
print("w:",w)
opti.step()
print("w:",w)
result:
w的梯度: tensor([1., 2.])
w: tensor([0.5000, 0.5000], requires_grad=True)
w: tensor([0.4900, 0.4800], requires_grad=True)
w: tensor([0.4790, 0.4580], requires_grad=True)
momentum方法引入了动量的概念。每次梯度更新的量不是learning_rate*grad,而是动量。
的初始值为0。在代码中gamma=0.1
在第一次更新中,
$w=w-v_1\\=[0.5,0.5]-[0.01,0.02]\\=[0.49,0.48]$
到目前为止和不带动量的SGD的结果一致。
第二次更新,
$v_{2}=0.1*v_1+0.01*[1,2]\\=[0.001,0.002]+[0.01,0.02]\\=[0.011,0.022]$
$w=w-v_2\\=[0.49,0.48]-[0.011,0.022]\\=[0.479,0.458]$
和代码的结果一致。
改动四:除了SGD,我能不能使用其他优化器?比如adagrad?
SGD对于每一个参数都使用同一个学习率。adagrad则对每一个参数使用不同的学习率:梯度越大,学习率越小;梯度越小,学习率越大。
import torch
w = torch.tensor([0.5,0.5],requires_grad=True)
loss = w[0].clone()**2 + 2*w[1].clone()**2
opti = torch.optim.Adagrad([w],lr=0.01,lr_decay=0, weight_decay=0, initial_accumulator_value=0)
loss.backward(retain_graph=True)
print("w的梯度:",w.grad)
print("w:",w)
opti.step()
print("w:",w)
opti.zero_grad()
loss = w[0].clone()**2 + 2*w[1].clone()**2
loss.backward(retain_graph=True)
print("w的梯度:",w.grad)
opti.step()
print("w:",w)
output:
w的梯度: tensor([1., 2.])
w: tensor([0.5000, 0.5000], requires_grad=True)
w: tensor([0.4900, 0.4900], requires_grad=True)
w的梯度: tensor([0.9800, 1.9600])
w: tensor([0.4830, 0.4830], requires_grad=True)
adagrad会维护一个和w同维度的tensor ,t代表步数,默认
首先看第一步( 符号指的是对应元素的点乘):
$s_1=s_0+grad\odot grad\\=s_0+[1,2]\odot[1,2]\\=[1,4]$
$w=w-\frac{learning\_rate}{\sqrt{s_1}}\odot grad\\=[0.5,0.5]-\frac{0.01}{[1,2]}\odot[1,2]\\=[0.49,0.49]$
再来看第二步:
$s_2=s_1+grad\odot grad\\=s_1+[0.98,1.96]\odot[0.98,1.96]\\=[1,4]+[0.9604,3.8416]\\=[1.9604,7.8416]$
$w=w-\frac{learning\_rate}{\sqrt{s_2}}\odot grad\\=[0.49,0.49]-\frac{0.01}{[\sqrt{1.9604},\sqrt{7.8416}]}\odot[0.98,1.96]\\=[0.483,0.483]$
笔算结果和代码一致。adagrad本质上是对学习率做了一个修正: 。从adagrad的公式可以看出,某个参数的梯度越大,其对应的学习率越小。在本例子中, 比 的梯度大,对应的学习率更小,大和小相抵消后, 和 更新的步长刚好相同。
而且可以从公式中看出,adagrad算法需要维护一个和w形状一致的张量 。也就是说,引入adagrad后,显存会至少增加一倍参数量的级别。
改动五:既然之前尝试了SGD/Adagrad,那么我干脆把几个常见的优化器都尝试一遍,比如RMSprop
import torch
w = torch.tensor([0.5,0.5],requires_grad=True)
loss = w[0].clone()**2 + 2*w[1].clone()**2
opti = torch.optim.RMSprop([w],lr=0.01,alpha=0.5, eps=0,momentum=0,weight_decay=0,)
loss.backward(retain_graph=True)
print("w的梯度:",w.grad)
print("w:",w)
opti.step()
print("w:",w)
opti.zero_grad()
loss = w[0].clone()**2 + 2*w[1].clone()**2
loss.backward(retain_graph=True)
print("w的梯度:",w.grad)
opti.step()
print("w:",w)
output:
w的梯度: tensor([1., 2.])
w: tensor([0.5000, 0.5000], requires_grad=True)
w: tensor([0.4859, 0.4859], requires_grad=True)
w的梯度: tensor([0.9717, 1.9434])
w: tensor([0.4744, 0.4744], requires_grad=True)
RMSprop是基于adagrad的改进,adagrad累加梯度平方的公式是
RMSprop的移动平均公式是 ,也就是说,多了一个可调节的超参数。
首先看第一步: $s_1=0.5s_0+0.5\cdot grad\odot grad\\=0.5s_0+0.5\cdot[1,2]\odot[1,2]\\=[0.5,2]$
$w=w-\frac{learning\_rate}{\sqrt{s_1}}\odot grad\\=[0.5,0.5]-\frac{0.01}{[\frac{1}{\sqrt{2}},\sqrt{2}]}\odot[1,2]\\=[0.4859,0.4859]$
再来看第二步:
$s_2=0.5s_1+0.5\cdot grad\odot grad\\=0.5s_1+0.5\cdot[0.9717,1.9434]\odot[0.9717,1.9434]\\=[0.25,1]+[0.4721,1.8884]\\=[0.7221,2.8884]$
$w=w-\frac{learning\_rate}{\sqrt{s_2}}\odot grad\\=[0.4859,0.4859]-\frac{0.01}{[\sqrt{0.7221},\sqrt{2.8884}]}\odot[0.9717,1.9434]\\=[0.474465,0.474465]$
笔算结果和代码结果有万分之一的误差,可能是由于笔算多次四舍五入的差异导致。
如果令参数alpha=0,在这个例子里,梯度更新公式就变成 。它仍然根据梯度决定方向:梯度为正就减,为负就加,只是步长不再随梯度大小变化。
改动六:目前已经试了momentum和RMSprop,而adam结合了以上两种优化思想,接下来试试adam
adam的梯度更新公式如下:
$m_t=\beta_1m_{t-1}+(1-\beta_1)g_t\\v_t=\beta_2v_{t-1}+(1-\beta_2)g_{t}^{2}\\w=w-learning\_rate\cdot\frac{\frac{m_t}{1-\beta_1^t}}{\sqrt{\frac{v_t}{1-\beta_2^t}}}$
import torch
w = torch.tensor([0.5,0.5],requires_grad=True)
loss = w[0].clone()**2 + 2*w[1].clone()**2
opti = torch.optim.Adam([w],lr=0.01,eps=0,betas=(0.5,0))
loss.backward(retain_graph=True)
print("w的梯度:",w.grad)
print("w:",w)
opti.step()
print("w:",w)
opti.zero_grad()
loss = w[0].clone()**2 + 2*w[1].clone()**2
loss.backward(retain_graph=True)
print("w的梯度:",w.grad)
opti.step()
print("w:",w)
output:
w的梯度: tensor([1., 2.])
w: tensor([0.5000, 0.5000], requires_grad=True)
w: tensor([0.4900, 0.4900], requires_grad=True)
w的梯度: tensor([0.9800, 1.9600])
w: tensor([0.4799, 0.4799], requires_grad=True)
首先看第一步:
$w=w-0.01\cdot\frac{\frac{[0.5,1]}{0.5}}{[1,2]}\\=[0.5,0.5]-[0.01,0.01]\\=[0.49,0.49]$
然后看第二步:
$w=w-0.01\cdot\frac{\frac{[0.74,1.48]}{0.75}}{[0.98,1.96]}\\=[0.49,0.49]-0.01\cdot\frac{\frac{[0.74,1.48]}{0.75}}{[0.98,1.96]}\\=[0.4799,0.4799]$
注:Adam的weight_decay是起作用的。比如刚开始w1=0.5,原来的梯度是1;如果weight_decay=1,优化器用的梯度就变成1.5。只是这个例子比较特殊,又设置了eps=0,梯度放大之后,Adam公式里的分子、分母也跟着放大,最后抵消了,所以看起来参数更新没变。AdamW则是在更新参数时,额外减去learning_rate*weight_decay*w。
改动七:试试结合学习率调整策略来看梯度下降的结果。
pytorch中学习率调整策略分为三种:
(1)基于既定规则,比如等间隔调整(step)、指数衰减调整(exponential)等
(2)基于监控目标,自适应调整,比如ReduceLROnPlateau
(3)自定义调整
下面的代码以自定义调整为例,自定义了lambda函数,调整后的学习率等于初始学习率乘以epoch轮数。注意本例子中学习率调整的时机在梯度更新之前。
import torch
w = torch.tensor([0.5,0.5],requires_grad=True)
opti = torch.optim.Adam([w],lr=0.01,eps=0,betas=(0.5,0))
scheduler = torch.optim.lr_scheduler.LambdaLR(opti, lr_lambda=lambda x:x)
for epoch in range(2):
loss = w[0].clone()**2 + 2*w[1].clone()**2
loss.backward(retain_graph=True)
print("\n===当前轮数:",epoch)
print("w的梯度:",w.grad)
print("梯度更新前的w:",w)
scheduler.step()
print("当前学习率:",scheduler.get_lr()[0])
opti.step()
print("梯度更新后的w:",w)
opti.zero_grad()
output:
===当前轮数: 1
w的梯度: tensor([1., 2.])
梯度更新前的w: tensor([0.5000, 0.5000], requires_grad=True)
当前学习率: 0.01
梯度更新后的w: tensor([0.4900, 0.4900], requires_grad=True)
===当前轮数: 2
w的梯度: tensor([0.9800, 1.9600])
梯度更新前的w: tensor([0.4900, 0.4900], requires_grad=True)
当前学习率: 0.02
梯度更新后的w: tensor([0.4699, 0.4699], requires_grad=True)
第一轮的学习率=0.01*1,不变,因此结果和【改动六】一致。
第二轮的学习率=0.01*2=0.02,因此第二轮中,
$w=w-0.02\cdot\frac{\frac{[0.74,1.48]}{0.75}}{[0.98,1.96]}\\=[0.49,0.49]-0.02\cdot\frac{\frac{[0.74,1.48]}{0.75}}{[0.98,1.96]}\\=[0.4699,0.4699]$
和代码结果一致。
用简单的例子去跑代码,多调整一些不同的参数。然后对比笔算结果和代码结果是否一致。这种方法既可以加深对深度学习基础知识的理解,也可以更快入门pytorch代码。