深度学习中的优化

听着挺高端的是不,还听着挺难的,确实,单单来讲里边的数学确实有点子麻烦,但是我们有了一些计算机工具,我们就不需要从数学推导角度去深度的理解了。

img

优化与凸优化

无论如何我们都是无法避开讲一些数学的,(虽然俺的数学是个二把刀)我们尽力少用一些抽象的数学表达来描述清楚,但也会有一些朴素且不严谨(对于理解是无伤大雅的)

优化

首先优化是什么呢?为了变得更好而采取的动作。优化实际上和规划是很像的(其实优化是规划的子集),(有一说一现在高考连线性规划都不学了)都有某个目标函数和某些约束模型,最终得到最佳的参数组合。举个例子,我们知道找一个函数的最值我们需要用导数来寻找,但是可不可以通过某种搜索的方法穷举出最值呢?就拿一个二次函数来讲:

右上角的点为随机选择的初始点,我们希望找一个下坡路从而找到一个低点(是不是有点贪心的感觉,所以他也会有贪心出现的错误),然后逐渐逼近最低点,用数学的方式表达: \[ argmin \ x^2 \] 没有约束条件,用到的求解方法表现为: \[ x_{t}=x_{t-1}-\eta \ \omega \] 其中各种符号具体是什么意思,稍后我们在解释,我们先来看另一种情况:

我们依旧采用原来的算法,但是会发现:诶好像这个不是最小值,这也就是我们刚才提到的贪心的弊端,他没有全局视野,就很容易搜索到局部(极值)而不是最值,这也是我们接下来要面对的挑战之一。

回到深度学习上来,深度学习中的优化主要体现在使模型的loss函数的值尽可能小,从而训练得到尽可能完美的参数。同时我们会认识到不是所有的机器学习都涉及优化,比如一些有解析解的机器学习方法我们就不需要进行优化。

凸优化

至于凸优化我们就迅速略过,凸优化涉及的数学知识就有点难理解了,挑两个重点公式和概念提一嘴:

凸集:对于一个集合\(X\),任意一个\(a,b\in X\),都有\(\lambda a+(1-\lambda)b \in X\),其中\(\lambda \in [0,1]\),那么称集合\(X\)为凸的。

用人话来说的话就是一个集合中两点的连线扔在集合中,为什么是这样表示呢,我们设想一个点集,那么\(a,b\)最佳的表达方式其实一个向量对吧,是的,当我们进入深度学习后要习惯以向量视角看问题,那么我们对上面的公式进行整型: \[ \lambda a+(1-\lambda)b=\lambda(a-b)+b \] 这样我们发现这刚好是一条线段上的所有点

凸函数:这里我们就按照高数中的凸函数进行理解即可 \[ f(\lambda x+(1-\lambda)x^{'}) \leq \lambda f(x)+(1-\lambda)f(x^{'}) \] p.s.这里还有个小故事,大家感兴趣可以去查一下,国内和国外的数学教材凸凹定义其实是反的

性质

性质大多是比较重要的,我们就记一下就好了(就别证明了罢)

  • 凸优化中局部极小值即为全局极小值
  • 凸优化下的约束可以转化为拉格拉日乘子法
  • 也可以采用罚函数法或者投影法

梯度下降

数学概念

先回忆一些数学概念,梯度,一种向量,是各个轴上的偏导数,函数沿着梯度方向变化率最大。

然后我们来回到一元函数,这很有有益于理解:

\[ x_{t}=x_{t-1}-\eta \ f^{'}(x) \] 现在我们可以解释之前的公式了,其中\(\eta\)被称为学习率,表现为梯度下降的步长,而这个一阶导数提供了梯度方向,这里可以自己分类讨论一下(当其大于0?当其小于0?)就能体会到这个数学机器是如何工作的了。

代码示例

虽然之前是通过numpy手搓过梯度下降的方法的,但是现在pytorch提供了更多的方便。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
from torch.autograd import Variable
import torch

# 定义二次函数 f(x) = x^2
def quadratic_function(x):
return x**2

# 初始值
x = torch.tensor(10.0, requires_grad=True)

# 定义学习率和迭代次数
learning_rate = 0.1
iterations = 1000

# 梯度下降优化过程
for i in range(iterations):
# 计算二次函数的值
y = quadratic_function(x)

# 使用自动微分计算梯度
y.backward()

# 更新参数
with torch.no_grad():
x -= learning_rate * x.grad

# 重置梯度
x.grad.zero_()

# 输出最小值
print("最小值 x =", x.item())
print("最小值 f(x) =", quadratic_function(x).item())

image-20240225101148445

ok,问题我们之前大部分已经提过了,比如什么局部最优啊,现在我们要抛出一个新问题,学习率能瞎写吗?答案是也许能,但不好,所以我们要研究怎么搞好学习率。下面是一个瞎写学习率的情况(初始点为10):

学习率

再研究学习率之前,我们要先提一嘴多维的梯度下降,其实公式也没啥: \[ x_{t}=x_{t-1}-\eta \nabla f(x) \] 梯度一般由一个雅克比矩阵给出,其为一个向量。

Hessian矩阵\(H\),我想应该不需要介绍了,这里我们不去管什么泰勒展开的推导,我们仅仅记住结论:

牛顿法:最优的梯度下降(吗?),\(x_{t}=x_{t-1}-H^{-1} \nabla f(x)\),显然是有问题的,存储Hessian矩阵可能要花费大量内存,在凸优化上牛顿法非常好用

预处理:\(x_{t}=x_{t-1}-\eta \ diag(H)^{-1} \nabla f(x)\),相当于在每个方向(变量)上选择了不同的学习率

随机梯度下降

数学理论

这个就是我们常用的SGD了,其核心也就是一个用抽样来估计总体,从而降低运算所需的时间,假设使用梯度下降法,样本记做\(x_i\),那么计算总的loss为: \[ Loss(X)=\frac{1}{n}\sum_{i=1}^{n}{Loss(x_i)} \] 其中我们需要计算梯度: \[ \nabla Loss(X)=\frac{1}{n}\sum_{i=1}^{n}{\nabla Loss(x_i)} \] 于是我们有了一个朴实无华的想法,那我直接少算几个不就行了()你别说,你还真别说,还真是这么搞得。

随机梯度下降就是均匀随机采样一个\(x_i\),然后直接算一个$Loss(x_i) \(,然后扔进梯度下降公式。\)$ x_{t+1}=x_t- Loss(x_i) \[ 那这么搞是否有点问题呢?只算一个有没有可能算的不准呢?当然有可能,但是在统计学上其实问题不大: \] iLoss(x_i)= {i=1}^{n}{Loss(x_i)}=Loss(X) $$

代码示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
import torch
import numpy as np
import matplotlib.pyplot as plt

# 定义函数 f(x1, x2) = x1^2 + 2 * x2^2
def quadratic_function(x1, x2):
return x1 ** 2 + 2 * x2 ** 2

# 初始化参数
x1 = torch.tensor(-3.0, requires_grad=True)
x2 = torch.tensor(-6.0, requires_grad=True)

# 定义学习率和迭代次数
learning_rate = 0.1
iterations = 10

# 记录优化过程中的点
x1_history = [x1.item()]
x2_history = [x2.item()]

# 随机梯度下降优化过程
for i in range(iterations):
# 计算函数的值
y = quadratic_function(x1, x2)

# 使用自动微分计算梯度
y.backward()

# 更新参数,在梯度上加上随机波动
with torch.no_grad():
x1 -= learning_rate * (x1.grad+torch.normal(0.0, 1, (1,)).item())
x2 -= learning_rate * (x2.grad+torch.normal(0.0, 1, (1,)).item())

# 重置梯度
x1.grad.zero_()
x2.grad.zero_()

# 记录参数
x1_history.append(x1.item())
x2_history.append(x2.item())

# 生成网格点
x1_grid, x2_grid = np.meshgrid(np.linspace(-5, 1, 100), np.linspace(-6, 1, 100))
z = quadratic_function(torch.tensor(x1_grid), torch.tensor(x2_grid))

# 绘制等高线图
plt.figure(figsize=(8, 6))
plt.contourf(x1_grid, x2_grid, z, levels=20, cmap='viridis')
plt.colorbar(label='Function Value')
plt.xlabel('x1')
plt.ylabel('x2')
plt.title('Contour Plot of Quadratic Function')

# 绘制优化过程中的点
plt.plot(x1_history, x2_history, marker='o', color='red', label='Optimization Path')
plt.legend()
plt.grid(True)
plt.show()

动态学习率

这块直接摆一下公式,俺表示这就是炼丹的玄学部分了:

  • $(t)=i    if  t_i <t <t{t+1} $分段常数
  • \(\eta (t)=\eta_0 \cdot e^{-\lambda t}\)指数衰减
  • \(\eta (t)=\eta_0 \cdot (\beta t+1)^{-\alpha}\)多项式衰减

总结

喵的写太多了,到这里基本上就可以入门了,也有一定的数学基础了,但是我不认为去仔细研究数学的一些严谨的收敛证明是有用的。后边就要靠自己去看一看啦,比如什么Ada,Adam,小批量随机梯度下降之类的。

参考链接

11. 优化算法 — 动手学深度学习 2.0.0 documentation (d2l.ai)