梯度迭代类算法已成为目前各种领域的主流算法。各种现实中的问题分解抽象成机器可以处理的形式之后,基本都可归类为图像、自然语言处理、决策、时序、强化学习这几种类型,而当今解决这些问题的顶尖算法中,梯度迭代(梯度上升或梯度下降)都占据主流地位,比如决策类问题的比赛中,梯度下降决策树GBDT类算法是完全的主流,使用深度学习网络处理图片自然语言问题更毋庸置疑。
那么,梯度迭代算法究竟是什么?简单地说,就是代入数据,预测结果,如果结果偏大就调小参数,结果偏小就调大参数。举一个简单的例子,分为三个小问题:
问题一
假设父亲的智商影响儿子的智商,设父亲的智商为x,儿子的智商为y,y=wx,训练一个参数w学习二者之间的关系。目前有多个父子智商数据对,其中第一个数据:父亲智商x=100,儿子智商w=110,将w初值设为w=1.0;学习率设为0.00001,计算平均误差。
如下程序用于学习w。
import torch
import matplotlib.pyplot as plt
%matplotlib inline
x = torch.tensor([100.]) # 只做训练不求梯度
y = torch.tensor([110.])
w = torch.tensor([1.], requires_grad=True) # 模拟网络参数
lr = 0.00001 # 学习率
arr = []
for i in range(10):
pred = x * w
loss = (pred-y)*(pred-y) # 平方误差
loss.backward() # 计算梯度
print("real", y.item(), "pred", pred.item(), "loss", loss.item())
print("w.data", w.data.item(), "w.grad", w.grad.item())
w.data = w.data - lr * w.grad # 按学习率调参
w.grad.zero_() # 梯度清 0,否则梯度会不断累加
arr.append(loss.item())
plt.plot(arr)
程序中使用了torch的基本数据结构Tensor及其自动计算梯度的功能,模型、优化器、误差函数全部写代码实现。其误差等于实际的y值减预测值的平方,在第一次迭代中计算结果如下:
求误差函数对参数w的偏导数,用以调节w,具体使用链式法则:
然后使用梯度修改参数w,每次修改一个很小的步幅,即学习率。
每次w都变好一点,经过多次迭代,参数w逐渐逼近其真实值,误差也逐渐下降,如下图所示:
此时再代入值x=100,即可得到正确的预测y=110。上述是最简单的情况,为简化操作只训练了一个实例,如果有10000个实例代入训练,反复迭代20次,最终将通过微调的方法得到最为合理的参数w。
使用Pytorch提供的线性层、误差函数和优化器,功能与上面的程序一致,代码更加简单:
import torch
x = torch.tensor([100.]) # 只做训练不求梯度
y = torch.tensor([110.])
lr = 0.00001 # 学习率
model = torch.nn.Linear(1, 1, bias=False)
model.weight.data.fill_(1.0) # 初始化参数
optimizer = torch.optim.SGD(model.parameters(), lr=lr, momentum=0) # 优化器
lossfunc = torch.nn.MSELoss() # 平方根损失
for i in range(10):
pred = model(x)
loss = lossfunc(pred, y)
loss.backward() # 计算梯度
optimizer.step()
optimizer.zero_grad()
问题二
如果孩子的智商由父母双方决定,那么每一实例的x将提供父母双方的智商值x1,x2,学到的参数w也是两个:y=x1w1+x2w2,每次调节参数时误差函数分别对w1,w2求偏导。
(码字不易,转载请注明出处:谢彦的技术博客)
问题三
如果孩子的智商由父亲决定,父亲的智商由奶奶决定,此时网络的输入是奶奶的智商x,输出是孩子的智商y,父亲的智商成为中间变量。因此有:y=xw1w2,其中w1是奶奶对父亲的影响,w2是父亲对儿子的影响。(示例仅用于描述多层网络,实际上这样的双层网络与单层网络效果无异w3=w2*w1)。
import torch
x = torch.tensor([100.]) # 只做训练不求梯度
y = torch.tensor([110.])
lr = 0.00001 # 学习率
model1 = torch.nn.Linear(1, 1, bias=False)
model2 = torch.nn.Linear(1, 1, bias=False)
params = [{'params':model1.parameters()},
{'params':model2.parameters()}] # 同时优化两组参数
optimizer = torch.optim.SGD(params, lr=lr, momentum=0) # 优化器
lossfunc = torch.nn.MSELoss() # 平方根损失
for i in range(10):
pred = model2(model1(x))
loss = lossfunc(pred, y)
loss.backward() # 计算梯度
optimizer.step()
print("real", y.item(), "pred", pred.item(), "loss", loss.item())
print("w1.data", model1.weight.data.item(), "w1.grad", model1.weight.grad.item())
print("w2.data", model2.weight.data.item(), "w2.grad", model2.weight.grad.item())
optimizer.zero_grad()
(码字不易,转载请注明出处:谢彦的技术博客)
梯度爆炸和梯度消失
梯度爆炸和梯度消失指的是梯度太陡或者梯度太平,引发的调参问题,该问题由连乘引发,假设一个n层网络,每一次都有参数w,b,试想比较简单的情况b=0,于是有如下的前向传播:
当所有的W都大于1,且网络非常深时,最终的Y将非常大甚至越界,反之,当所有W都小于1,且网络非常深时,最终的Y将趋于0。如果网络的后几层的输入和输出都是0,则梯度也必然是“平”的,导致无法正常调参。
一般用下列方法缓解梯度爆炸和消失问题:
- 用归一化方法处理模型输入x。
- 控制模型初值参数w。
- 使用较小的学习率。
- 对模型中的数据流做一些限制。
- 使用残差网络,在前向传播过程中累加X值,使得W小于1时,Y也不再趋于0,从而解决梯度消失问题。
当模型参数无法如设想中的调整时,除上述方法还可以尝试:
- 冻结一些层,调整另一些层。
- 不能收敛时,或波动太大时,可考虑缩小学习率。
- 跟踪backward之后参数梯度的均值和方差,查看有无异常。
- 如果手动实现模型,需要验证梯度计算是否正确(计算双边误差)。
- 调试时需去掉Dropout,以保持稳定。
- 注意正则化项对代价函数的影响。
- W和B一般在训练过程中逐渐变大,有些问题可能只在W,B较大时才出现。
- 使用BatchNormal时,mini-batch size不能太小。