梯度下降的Python代码实现
注:以下内容使用的是python3.10版本,jumpyter,使用了d2l(动手深度学习)的包,但该包对推导公式并无影响,对数据集生成以及测试会有一定影响。
%matplotlib inlineimport randomimport torchfrom d2l import torch as d2l以下是数据集的生成 w是我们自己定义的一个权重 b同样是我们自己定义的一个偏置
然后用定义的函数生成了num_examples个样本,其中: X是均值为0,标准差为1的正态分布,len(w)可以看作权重的个数,一共有num_example个,组成了一个矩阵,作为特征数据集 matmul则是矩阵乘法,将X乘以权重并加上偏差,得到标签数据集 之后则是添加噪声
def synthetic_data(w,b,num_examples): X=torch.normal(0,1,(num_examples,len(w))) y=torch.matmul(X,w)+b y+=torch.normal(0,0.01,y.shape) return X,y.reshape(-1,1)
true_w=torch.tensor([2,-3.4])true_b=4.2features,labels=synthetic_data(true_w,true_b,1000)可以画图得到数据的分布情况
d2l.set_figsize()d2l.plt.scatter(features[:,1].detach().numpy(),labels.detach().numpy(),1)这里是随机打乱下标,然后以10个为一组,返回对应的X(特征)以及y(标签)
def data_iter(batch_size,features,labels): num_examples=len(features) indices=list(range(num_examples)) random.shuffle(indices) for i in range(0,num_examples,batch_size): batch_indices=torch.tensor(indices[i:min(i+batch_size,num_examples)]) yield features[batch_indices],labels[indices]
batch_size=10
for X,y in data_iter(batch_size,features,labels): print(X,'\_n',y) break下面到了我真正想讲的东西,也就是数学推导 首先是随机一个w和b,这里的w和b是什么并不重要,至于为什么往下面看。
w=torch.normal(0,0.01,size=(2,1),requires_grad=True)b=torch.zeros(1,requires_grad=True)在数学中我们可以这样表示:
定义线性回归公式:
def linreg(X,w,b): return torch.matmul(X,w)+b看着很复杂,但是在数学中就会清晰明了:
一元一次方程嘛,线性回归,为什么是线性的呢?就是因为成比例。
然后是平方损失函数:
def square_loss(y_hat,y): return (y_hat-y.reshape(y_hat.shape))**2/2同样的,在数学上:
下面到了难点:
首先no_grad()是停止记录计算图。避免梯度的累积,因为梯度下降是多次的,上一次的梯度下降是不应该影响下一次的梯度下降。
举个很简单的例子,在山顶想要到山谷的梯度是很陡的,但山间到山谷的梯度就会比较平缓一些了,第一次梯度下降可能是从山顶到了山谷,如果累计梯度的话,那么下一次下降的方向就是山顶的最速下降方向+山间的最速下降方向合起来的向量了。
那么接下来讲一个让我比较困惑的点,也就是优化算法:
def sgd(params,lr,batch_size): with torch.no_grad(): for param in params: param-=lr*param.grad/batch_size param.grad.zero_()让我们首先确定这里代码所代表的数学含义:
对应代码中的 param -= …。
代表学习率(代码中的 lr),控制每次参数更新的步长。
对应代码中的 param.grad,即损失函数对当前参数计算出的梯度(求导结果)。
代表小批量样本的数量(代码中的 batch_size)。在这里除以 batch_size 是因为前面在计算损失函数时没有求均值(只是简单求和了当前批次所有样本的损失),所以在这里对梯度求均值,确保更新步长不会因为批量大小的变化而剧烈波动。
param.grad.zero_() 是每次更新完参数后,必须将现有的梯度清零,否则 PyTorch 默认会把下一次计算的梯度和这次的累加起来,导致计算错误。
现在我们都在推导公式了,自然要深入一点思考,首当其中的就是w到底是怎么算出来的,这也是这个公式的核心,对吧?
在机器学习中我们训练的时候,数据是有训练集和测试集的,训练集包含了特征和标签,也就是说,我们是知道真实的y值的,对吧?
模型预测(线性方程):
损失函数(平方损失):
那么,损失函数自然是算得出的,对吧?
看着毫无关联,我们该怎么算呢?
注意到是和相关的,L又和相关,那么想对w求偏导,我们根据链式法则,可以转换为:
对 求导:
对 求导:
现在,把外层和内层乘起来,我们就得到了最终的梯度:
关于权重 的梯度:
关于偏置 的梯度:
算这个有什么用呢?
我们回归基础,偏导是什么?
不动其他参数,对某个参数微调,对函数的改变量,对吧?
那么,,就是改变,对的改变,对的改变。
需要注意的是,并不是只代表了一个数,而是一个向量,并且这个向量是的参数(带入可知),也就是说对求偏导,其实是求梯度。
梯度又是什么呢?代表着在当前点的最快上升路径,反之则是下降最快的路径。
我们可以来一点几何上的直观想象,不是说某个点指向山顶,其上升速度一定是最快的,只需要顺着下降即可。
山的例子其实在我看来是一个很有问题的例子,会让用这个模型想象的人,不自觉的认为切线方向一定是上升速率最快的方向。
因为你如果观看过曲线最速下降的图的指向,就会发现绝大部分情况下,这个方向不是指向山顶的,而是指向一个莫名其妙的位置,虽然有向下的趋势,但很明显不符合人的常识。
顺着山爬或者顺着山下才应该最快的,为什么不是这样?
首先需要注意的是,如果要用山的模型,至少应该把二维转换为三维。
绝大部分给你举的例子,都是一个用y=x^2的函数伪装的山。
我更推荐你用一个三维的图像去看,这是我给出的一个会造成梯度震荡的例子:
然后,我们不应该用一个全局的眼光去看。
梯度从来指向的不是全局最高的位置,而是一个极小范围内,距离下一个等高线的最速位置。
我们可以设想一个二维映一维的图像,二维中有无数个圆圈(不一定是标准圆),每个圆圈都代表了一个高度,如果圆圈是标准的,那么梯度下降确实沿着最速方向(垂直于切线的方向),但如果圆是椭圆…
椭圆的垂直于切线的方向会指向圆心吗?
你可以现在去画一个图,或者在脑内设想一下,甚至是问ai,答案都是很明显的不会。
至于为什么垂直切线的方向是梯度的方向,这涉及到了一个数学推导:
先想象一下,沿着等高线走,高度的变化是怎么样的?
显然等高线之所以是等高线,就是因为高度相同
假设这座山的高度由函数 决定。你在山上的位置是 。
朝着某个极其微小的方向迈出一步,这一步的位移向量我们叫它 。
在迈出这一步后,高度的变化量(数学上叫全微分 )是由两部分叠加而成的:X方向的坡度乘以X方向的步长,加上Y方向的坡度乘以Y方向的步长。用公式写出来就是:
我们可以看作两个向量的点乘
第一个向量是 —— 这正是梯度的定义 !
第二个向量是 —— 这是迈出那一步的方向 !
高度变化便可以写作:
如果这迈出的这一步 ,是刚好顺着等高线的切线方向走的,那么高度是绝对不会变的,也就是高度变化量 。把 代入上面的公式:
这可以证明,梯度垂直于等高线切线,显然又是数学的小巧思…
不过我们知道了这个后,加上前面的举例,就可以得到一个比较直观的结论了,那就是梯度是指向局部最高点的。
这也就是为什么很多时候你看的梯度明明显然存在一个最短的路径,但就是反反复复跳的原因。
(扯的有点偏了,但确实对理解这个会有很大的帮助)
理解了意义后,又是怎么改变的呢?
梯度下降,下降的是谁的梯度呢?
让我们拉回来,再看一遍公式:
理解上面的内容后,我们就不难发现梯度下降其实很类似于贪心算法,一直在寻找局部最优解,这里的显然就是w,学习率其实就是沿着梯度走了多少,每个点的梯度显然不一样,这就是更新的梯度,而我们的核心目的就是找到一个,使得L的损失最小。
我们这里在重新整理一下思路:
的梯度:
梯度代表上升最快的方向,反之则是下降最快的方向,是关于的函数,在一个固定的点,梯度上升代表误差变大,梯度下降代表误差减小。
我们可以看作坐标,梯度则是L(损失)的上升方向,为了使损失下降,我们要往反向去走一个位移,然后更新的位置,这就是这个公式的意义。
然后w变小,那么梯度模型预测的会更精准,导致梯度变小,也就是越走需要调整的位移越小.
如果是在几何图像上,你可以画n个椭圆,然后看看沿着切线方向,走一走试试看。
当不再更新的时候,就代表走到最低点。
还有一个值得注意的点是有人应该会疑惑,梯度是求函数的偏导,难道不会存在一个值越小,梯度越大的函数吗?例如梯度是-lnx这样的
这个就不过多解释了,平方损失嘛…自己想想应该就明白了,看公式推导其实也看得出来,除非预测值越来越偏离原值,不然梯度都是逐渐减小的。
lr=0.03num_epochs=3net=linregloss=squared_lossfor epoch in range(num_epochs): for X,y in data_iter(batch_size,features,labels): l=loss(net(X,w,b),y) l.sum().backward() sgd([w,b],lr,batch_size) with torch.no_grad(): train_1=loss(net(features,w,b),labels) print(f'epoch{epoch+1},loss {train_1.mean():f}')这就是最后的实现啦。
部分信息可能已经过时




