注:仅仅是学习记录笔记,搬运了学习课程的ppt内容,本意不是抄袭!望大家不要误解!纯属学习记录笔记!!!!!!
一、AutoGrad的回溯机制与动态计算图
1.可微分性相关属性
requires_grad属性:可微分性
x = torch.tensor(1., requires_grad=True)
print(x)
grad_fn属性:存储Tensor微分函数
y = x ** 2
print(y)
print(y.grad_fn)
grad_fn其实是存储了Tensor的微分函数,或者说grad_fn存储了可微分张量在进行计算的过程中函数关系,此处x到y其实就是进行了幂运算。
这里值得主要的是,y不仅和x存在幂运算关系(y = x**2),更重要的是,**y本身还是一个有x张量计算得出的一个张量。**而对于一个可微分张量生成的张量,也是可微分的。
print(y.requires_grad)
也就是相比于x,y不仅同样拥有张量的取值,并且同样可微,还额外存储了x到y的函数计算信息。我们再尝试围绕y创建新的函数关系,z = y + 1
z = y + 1
print(z)
print(z.requires_grad)
print(z.grad_fn)
不难发现,z也同时存储了张量计算数值、z是可微的,并且z还存储了和y的计算关系(add)。据此我们可以知道,在PyTorch的张量计算过程中,如果我们设置初始张量是可微的,则在计算过程中,每一个由原张量计算得出的新张量都是可微的,并且还会保存此前一步的函数关系,这也就是所谓的回溯机制。而根据这个回溯机制,我们就能非常清楚掌握张量的每一步计算,并据此绘制张量计算图。
2.张量计算图
借助回溯机制,我们就能将张量的复杂计算过程抽象为一张图(Graph),例如此前我们定义的x、y、z三个张量,三者的计算关系就可以由下图进行表示。
计算图的定义
上图就是用于记录可微分张量计算关系的张量计算图,图由节点和有向边构成,其中节点表示张量,边表示函数计算关系,方向则表示实际运算方向,张量计算图本质是有向无环图。
节点类型
在张量计算图中,虽然每个节点都表示可微分张量,但节点和节点之间却略有不同。就像在前例中,y和z保存了函数计算关系,但x没有,而在实际计算关系中,我们不难发现z是所有计算的终点,因此,虽然x、y、z都是节点,但每个节点却并不一样。此处我们可以将节点分为三类,分别是: a):叶节点,也就是初始输入的可微分张量,前例中x就是叶节点; b):输出节点,也就是最后计算得出的张量,前例中z就是输出节点; c):中间节点,在一张计算图中,除了叶节点和输出节点,其他都是中间节点,前例中y就是中间节点。 当然,在一张计算图中,可以有多个叶节点和中间节点,但大多数情况下,只有一个输出节点,若存在多个输出结果,我们也往往会将其保存在一个张量中。
3.计算图的动态性
,PyTorch的计算图是动态计算图,会根据可微分张量的计算过程自动生成,并且伴随着新张量或运算的加入不断更新,这使得PyTorch的计算图更加灵活高效,并且更加易于构建,相比于先构件图后执行计算的部分框架(如老版本的TensorFlow),动态图也更加适用于面向对象编程。
二、反向传播与梯度计算
我们曾使用autograd.grad进行函数某一点的导数值得计算,其实,除了使用函数以外,我们还有另一种方法,也能进行导数运算:反向传播。当然,此时导数运算结果我们也可以有另一种解读:计算梯度结果。
首先,对于某一个可微分张量的导数值(梯度值),存储在grad属性中。
print(x.grad)
在最初,x.grad属性是空值,不会返回任何结果,我们虽然已经构建了x、y、z三者之间的函数关系,x也有具体取值,但要计算x点导数,还需要进行具体的求导运算,也就是执行所谓的反向传播。所谓反向传播,我们可以简单理解为,在此前记录的函数关系基础上,反向传播函数关系,进而求得叶节点的导数值。在必要时求导,这也是节省计算资源和存储空间的必要规定。
print(z)
print(z.grad_fn)
print(z.backward())
print(x)
print(x.grad)
注意,在默认情况下,在一张计算图上执行反向传播,只能计算一次,再次调用backward方法将报错。 当然,在y上也能执行反向传播
x = torch.tensor(1., requires_grad=True)
y = x ** 2
z = y + 1
print(y.backward())
print(x.grad)
无论何时,我们只能计算叶节点的导数值
print(y.grad)
会报错,对于中间节点来说,无法求其导数。
至此,我们就了解了反向传播的基本概念和使用方法: (1)反向传播的本质:函数关系的反向传播(不是反函数); (2)反向传播的执行条件:拥有函数关系的可微分张量(计算图中除了叶节点的其他节点); (3)反向传播的函数作用:计算叶节点的导数/微分/梯度运算结果;
1.反向传播运算注意事项
中间节点反向传播和输出节点反向传播区别
尽管中间节点也可进行反向传播,但很多时候由于存在复合函数关系,中间节点反向传播的计算结果和输出节点反向传播输出结果并不相同。
x = torch.tensor(1.,requires_grad = True)
y = x ** 2
z = y ** 2
print(z.backward())
print(x.grad)
x = torch.tensor(1.,requires_grad = True)
y = x ** 2
z = y ** 2
print(y.backward())
print(x.grad)
中间节点的梯度保存
默认情况下,在反向传播过程中,中间节点并不会保存梯度 若想保存中间节点的梯度,我们可以使用retain_grad()方法
import torch
x = torch.tensor(1.,requires_grad = True)
y = x ** 2
y.retain_grad()
z = y ** 2
z.backward()
y
y.grad
x.grad
但是很奇怪,这个retain_grad()只能在jupyter notebook里面使用,在pycharm里面使用无效
2.阻止计算图追踪
在默认情况下,只要初始张量是可微分张量,系统就会自动追踪其相关运算,并保存在计算图关系中,我们也可通过grad_fn来查看记录的函数关系,但在特殊的情况下,我们并不希望可微张量从创建到运算结果输出都被记录,此时就可以使用一些方法来阻止部分运算被记录。
with torch.no_grad():阻止计算图记录
x = torch.tensor(1.,requires_grad = True)
y = x ** 2
with torch.no_grad():
z = y ** 2
print(z)
print(z.requires_grad)
print(y)
with相当于是一个上下文管理器,with torch.no_grad()内部代码都“屏蔽”了计算图的追踪记录
.detach()方法:创建一个不可导的相同张量
x = torch.tensor(1., requires_grad=True)
y = x ** 2
y1 = y.detach()
z = y1 ** 2
print(y)
print(y1)
print(z)
可以看出来,创建了y1的不可微分张量之后,不管是y1还是y1的衍生函数,都是不可微分张量
4.识别叶节点
由于叶节点较为特殊,如果需要识别在一个计算图中某张量是否是叶节点,可以使用is_leaf属性查看对应张量是否是叶节点。 is_leaf
print(x.is_leaf)
print(y.is_leaf)
但is_leaf方法也有容易混淆的地方,对于任何一个新创建的张量,无论是否可导、是否加入计算图,都是可以是叶节点,这些节点距离真正的叶节点,只差一个requires_grad属性调整。
print(torch.tensor([1]).is_leaf)
x = torch.tensor(1., requires_grad=True)
y = x ** 2
y1 = y.detach()
print(y1.is_leaf)
三、梯度下降基本思想
1.最小二乘法的局限与优化
在所有的优化算法中最小二乘法虽然高效并且结果精确,但也有不完美的地方,核心就在于最小二乘法的使用条件较为苛刻,要求特征张量的交叉乘积结果必须是满秩矩阵,才能进行求解。而在实际情况中,很多数据的特征张量并不能满足条件,此时就无法使用最小二乘法进行求解。
最小二乘法结果: 当最小二乘法失效的情况时,其实往往也就代表原目标函数没有最优解或最优解不唯一。针对这个问题,解决的方案是:在原矩阵方程中加入倍数的单位矩阵。 当然,上式修改后求得的结果就不再是全域最小值,而是一个接近最小值的点。鉴于许多目标函数本身也并不存在最小值或者唯一最小值,在优化的过程中略有偏差也是可以接受的。当然,伴随着深度学习的逐渐深入,我们会发现,最小值并不唯一存在才是目标函数的常态。基于此情况,很多根据等式形变得到的精确的求解析解的优化方法(如最小二乘)就无法适用,此时我们需要寻找一种更加通用的,能够高效、快速逼近目标函数优化目标的最优化方法。在机器学习领域,最通用的求解目标函数的最优化方法就是著名的梯度下降算法。
值得一提的是,我们通常指的梯度下降算法,并不是某一个算法,而是某一类依照梯度下降基本理论基础展开的算法簇,包括梯度下降算法、随机梯度下降算法、小批量梯度下降算法等等。接下来,我们就从最简单的梯度下降入手,讲解梯度下降的核心思想和一般使用方法。
2.梯度下降核心思想
梯度下降的基本思想其实并不复杂,其核心就是希望能够通过数学意义上的迭代运算,从一个随机点出发,一步步逼近最优解。
例如,在此前求解简单线性回归方程的过程中,我们曾查看SSE的三维函数图像如下: 而梯度下降,作为最优化算法,核心目标也是找到或者逼近最小值点,而其基本过程则: 在目标函数上随机找到一个初始点; 通过迭代运算,一步步逼近最小值点; 数学意义上的迭代运算,指的是上一次计算的结果作为下一次运算的初始条件带入运算
3.梯度下降的方向与步长
3.1 导数与梯度
函数上某一点的导数值的几何含义就是函数在该点上切线的斜率。例如y=x**2中,x在1点的导数就是函数在1点的切线的斜率。
from matplotlib import pyplot as plt
x = np.arange(-10, 10, 0.1)
y = x ** 2
z = 2 * x - 1
plt.plot(x, y, '-')
plt.plot(x, z, 'r-')
plt.plot(1, 1, 'bo')
plt.show()
当然,该点导数值的另外一个解释就是该点的梯度,梯度的值(grad)和导数相同,而梯度的概念可以视为导数概念的延申,只不过梯度更侧重方向的概念,也就是从梯度的角度解读导数值,就代表着当前这个点的可以使得y值增加最快的移动方向。
梯度:梯度本身是一个代表方向的矢量,代表某一函数在该点处沿着梯度方向变化时,变化率最大。当然,梯度的正方向代表函数值增长最快的方向,梯度的负方向表示函数减少最快的方向。
x = torch.tensor(1., requires_grad=True)
y = x ** 2
y.backward()
print(x.grad)
3.2 梯度与方向
根据lesson5可知,我们有目标函数如下所示:
fig = plt.figure()
ax = plt.axes(projection='3d')
ax.plot_surface(a, b, SSE, cmap='rainbow')
ax.contour(a, b, SSE, zdir='z', offset=0, cmap="rainbow")
plt.show()
此时a、b是在实数域上取值。假设二者初始值为0,也就是初始随机点为原点。对于(0,0)点,有梯度计算如下:
a = torch.tensor(0., requires_grad=True)
print(a)
b = torch.tensor(0., requires_grad=True)
print(b)
sse = torch.pow((2 - a - b), 2) + torch.pow((4 - 3 * a -b), 2)
print(sse)
sse.backward()
print(a.grad, b.grad)
也就是原点和(-28,-12)这个点之间连成直线的方向,就是能够使得sse变化最快的方向,并且朝向(-28,-12)方向就是使得sse增加最快的方向,反方向则是令sse减少最快的方向。
x = np.arange(-30, 30, 0.1)
y = (12/28) * x
plt.plot(x, y, '-')
plt.plot(0, 0, 'ro')
plt.plot(-28, -12, 'ro')
plt.show()
Point:这里有关于方向的两点讨论
1、方向没有大小,虽然这是个显而易见的观点,但我们当我们说朝着(-28,-12)方向移动,只是说沿着直线移动,并非一步移动到(-28,-12)上; 2、方向跟随梯度,随时在发生变化。值得注意的是,一旦点发生移动,梯度就会随之发生变化,也就是说,哪怕是沿着让sse变化最快的方向移动,一旦“沿着方向”移动了一小步,这个方向就不再是最优方向了。
当然,逆梯度值的方向变化是使得sse变小的最快方向,我们尝试移动“一小步”。一步移动到(28,12)是没有意义的,梯度各分量数值的绝对值本身也没有距离这个层面的数学含义。由于a和b的取值要按照(28,12)等比例变化,因此我们不妨采用如下方法进行移动:
print(sse)
a = torch.tensor(0.28, requires_grad=True)
b = torch.tensor(0.12, requires_grad=True)
s1 = (2-a-b) ** 2 + (4-3*a-b) ** 2
print(s1)
s1.backward()
print(a.grad, b.grad)
print(0.28+0.214)
print(0.12+0.0928)
new_a = 0.494,new_b = 0.2128,这两个值将作为新的梯度进行更新
a = torch.tensor(0.49944, requires_grad=True)
b = torch.tensor(0.2128, requires_grad=True)
s2 = (2-a-b) ** 2 + (4-3*a-b) ** 2
print(s2)
s2.backward()
print(a.grad, b.grad)
接下来,新的梯度值分别为0.4994+0.0116.3088, 0.2128+0.017.1533
四、梯度下降的数学表示
1.梯度下降的代数表示
2.再次理解步长
如果损失函数是凸函数,并且全域最小值存在,则步长可以表示当前点和最小值点之间距离的比例关系。但总的来说,对于步长的设置,我们有如下初步结论: (1)步长太短:会极大的影响迭代收敛的时间,整体计算效率会非常低; (2)步长太长:容易跳过最优解,导致结果震荡。
手动尝试实现一轮迭代
weights = torch.zeros(2, 1, requires_grad=True)
X = torch.tensor([[1., 1], [3, 1]], requires_grad=True)
y = torch.tensor([2., 4], requires_grad=True).reshape(2, 1)
eps = torch.tensor(0.01, requires_grad=True)
print(eps)
grad = torch.mm(X.t(), (torch.mm(X, weights) - y))/2
print(grad)
'''
tensor([[-7.],
[-3.]], grad_fn=<DivBackward0>)
注意对比代数方程计算结果,初始梯度为(-28,-12),此处相差4,也就是2m,m是样本个数。
k=0
for k in range(3):
grad = torch.mm(X.t(), (torch.mm(X, weights) - y))/2
weights = weights - eps * grad
k += 1
print(weights)
'''
tensor([[0.2563],
[0.1102]], grad_fn=<SubBackward0>)
'''
编写函数进行迭代运算
def graddescent(X, y, eps=torch.tensor(0.01, requires_grad=True), numIt=1000):
m, n = X.shape
weights = torch.zeros(n, 1)
for k in range(numIt):
grad = torch.mm(X.t(), (torch.mm(X, weights) - y))/2
weights = weights - eps * grad
return weights
X = torch.tensor([[1.,1],[3, 1]], requires_grad = True)
y = torch.tensor([2.,4], requires_grad=True).reshape(2,1)
print(graddescent(X, y))
'''
tensor([[1.0372],
[0.9102]], grad_fn=<SubBackward0>)
'''
new_weight = graddescent(X, y, numIt=10000)
print(new_weight)
'''
tensor([[1.0000],
[1.0000]], grad_fn=<SubBackward0>)
'''
sse = torch.mm((torch.mm(X, weights)-y).t(), torch.mm(X, weights)-y)
print(sse)
|