浅析梯度迭代算法

梯度迭代类算法已成为目前各种领域的主流算法。各种现实中的问题分解抽象成机器可以处理的形式之后,基本都可归类为图像、自然语言处理、决策、时序、强化学习这几种类型,而当今解决这些问题的顶尖算法中,梯度迭代(梯度上升或梯度下降)都占据主流地位,比如决策类问题的比赛中,梯度下降决策树 GBDT 类算法是完全的主流,使用深度学习网络处理图片自然语言问题更毋庸置疑。

那么,梯度迭代算法究竟是什么?简单地说,就是代入数据,预测结果,如果结果偏大就调小参数,结果偏小就调大参数。举一个简单的例子,分为三个小问题:

问题一

假设父亲的智商影响儿子的智商,设父亲的智商为x,儿子的智商为y,y=wx,训练一个参数 w 学习二者之间的关系。目前有多个父子智商数据对,其中第一个数据:父亲智商 x=100,儿子智商 w=110,将 w 初值设为 w=1.0;学习率设为 0.00001,计算平均误差。

如下程序用于学习 w。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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 提供的线性层、误差函数和优化器,功能与上面的程序一致,代码更加简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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 不能太小。