模拟实现梯度下降法
(在notebook中)
既然我们要模拟实现梯度下降法,那么首先,我们就得确认一下我们的损失函数取什么
我们先设置一下这个曲线,将其设置成-1到6的范围,进行141的均分
plot_x = np.linspace(-1,6,141)
plot_x
其中内容为
我们取这样一个曲线(二次曲线),假设其为我们的损失函数
plot_y = (plot_x-2.5)**2-1
这样获取到这两个数值以后,我们就可以很容易的绘制出这个图像
plt.plot(plot_x,plot_y)
图像如下
我们开始实现这个梯度下降法
首先很重要的是每一次我们都要计算这个损失函数所对应的导数,我们将这个函数设为dJ,对应为theta,我们这里的求导就是链式求导的过程中的一环,即2*(theta-2.5)
def dJ(theta):
return 2*(theta-2.5)
在过程中,我们还需要确定theta所对应的损失函数是多少,需要知道以后我们才能计算出当前这一点已经到达了多少,我们设为J,直接带入损失函数的公式就可以了
def J(theta):
return (theta-2.5)**2-1
那么就开始梯度下降法的过程了,梯度下降法很容易的将0这个点作为初始点,然后再进行循环,在每一轮循环的过程中,我们要先计算这个当前的点的对应的梯度,将其设为gradient,每次求出导数以后,theta都向导数的负方向进行移动,每次的步长我们设为eta(学习率)
然后我们要判断这个新的theta是否到了最小值的点,即导数为0的时候,但是由于编程的各种情况以及精度和误差,可能并达到不了0的情况
这样如果想要判断是否达到最小值的话,只用把每一次求到的新的theta与上一个theta进行比较且比起小,当两者的差距小于epsilon(特别小的值),那么我们就认为其到达了最小值,这样我们就退出循环,那么我们就需要保存一下上次的theta,再求出新的theta,然后再使用梯度下降法的方式前进就可以了
eta = 0.1
epsilon = 1e-8
theta = 0.0
while True:
gradient = dJ(theta)
last_theta = theta
theta = theta - eta * gradient
if(abs(J(theta) - J(last_theta)) < epsilon):
break
在退出循环以后我们打印出来看一下结果
print(theta)
print(J(theta))
结果如下
从结果可以验证出是成功的
然后我们看一下这个过程的情况,我们创建出一个数组用来记录theta的信息,在初始化的时候里面只放入一个数组,每次执行以后我们都将theta添加进数组中
theta = 0.0
theta_history = [theta]
while True:
gradient = dJ(theta)
last_theta = theta
theta = theta - eta * gradient
theta_history.append(theta)
if(abs(J(theta) - J(last_theta)) < epsilon):
break
那么我们就来看一下这个theta_history,如果说我们想要绘制出这个曲线,那我们就要先把损失函数的曲线给绘制出来(横轴去plot_x,纵轴对plot_x中的每一个值都取一个J)
然后再准备绘制一下theta_history,在横轴我们就取这个theta_history,对应的纵轴则是对整个数组中的每一个theta进行J,颜色设置成红色(为啥我的感觉不是点出来的,看上去就是线,应该会因为梯度的变化而进行稠密的变化)
plt.plot(plot_x,J(plot_x))
plt.plot(np.array(theta_history),J(np.array(theta_history)),color='r')
图像如下
我们可以用len(theta_history)看一下数组长度(46为45次的查找加上1个初始值)
为了方便使用上面的代码以及更好的更改eta值,对其进行封装,将过程设置成gradient_descent函数,epsilon默认为1e-8,同时将绘制图像的过程也封装成plot_theta_history函数
def gradient_descent(initial_theta,eta,epsilon=1e-8):
theta = initial_theta
theta_history.append(initial_theta)
while True:
gradient = dJ(theta)
last_theta = theta
theta = theta - eta * gradient
theta_history.append(theta)
if(abs(J(theta) - J(last_theta)) < epsilon):
break
def plot_theta_history():
plt.plot(plot_x, J(plot_x))plt.plot(np.array(theta_history),J(np.array(theta_history)),color='r')
现在我们使试一下使用不同的eta值得到的会有什么不同,这里我们使eta为0.01,设置theta_history数组为空,设置初始值为0,然后绘制出这个过程
eta = 0.01
theta_history = []
gradient_descent(0.,eta)
plot_theta_history()
输出如下
其中用len(theta_history)可以看到数组的长度为424
当然设置成0.001也是可以的,由于我这个图像完全看不出来这个点的稠密情况,只能是看看数组长度来发现这个情况了,可以看到长度为3682
这是我们的学习率一直降低的情况,相反的,如果我们的学习率相对的大一些,将其设为0.8
eta = 0.8
theta_history = []
gradient_descent(0.,eta)
plot_theta_history()
我们通过图像可以发现,虽然其在左右边不断地跳跃,但是由于我们的eta还是小的(没有到达eta的极限值),实际上损失函数值还是在减小的,通过这个曲折的过程,我们还是可以得到这个最小值
这就说明了,只要不超过某个值,实际上只要使能让损失函数下降,不管是不是在一边,都没有关系,那么我们设其为1.1试验一下
eta = 1.1
theta_history = []
gradient_descent(0.,eta)
运行后我们发现直接是报错了,原因是eta太大了,由于不断地增加上升损失函数,使编译器直接报错了
解决方法也是有的,我们将J改变一下,对其进行一下异常检测即可,成功返回,异常就返回一个inf
def J(theta):
try:
return (theta-2.5)**2-1.
except:
return float('inf')
这样改进以后就不会出现异常,但是会使得这个程序会陷入一个死循环,会使得先前的while True中的if中断变成无穷减无穷,使其永远无法触发这个情况
eta = 1.1
theta_history = []
gradient_descent(0.,eta)
强制中断运行以后
所以为了避免这种情况,我们要对先前的函数进行修改,我们对循环的最大值设置一下,将n_iters设置成10000(1e4),在其中我们设置一个变量 i_iter,将其初始为0 ,用来记录当前是第几次循环,这样我们就需要将while的条件修改为 i_iter < n_iters,如果没有可以跳出循环的theta,我们使i_iter += 1,这样到了一万次循环就会停止循环
def gradient_descent(initial_theta,eta,n_iters = 1e4,epsilon=1e-8):
theta = initial_theta
theta_history.append(initial_theta)
i_iter = 0
while i_iter < n_iters:
gradient = dJ(theta)
last_theta = theta
theta = theta - eta * gradient
theta_history.append(theta)
if(abs(J(theta) - J(last_theta)) < epsilon):
break
i_iter += 1
那么我们再次调用eta为1.1这个代码,经过修改以后,这一次我们是可以执行完的,其中的数组长度为10001
我们再用theta_history[-1]看一下其中的最后一个值
可以发现为nan,这就是eta取值太大导致的
那么我们可以试试将其可视化出来,为了可视化,我们让循环只有十次
eta = 1.1
theta_history = []
gradient_descent(0.,eta,n_iters=10)
plot_theta_history()
最后图像为
可以发现,这个损失函数越来越大,虽然说在eta为0.8是降低的,在eta为1.1的时候就是增大的情况是发生的,但是不能说eta的极限就简单的是个1.0
实际上eta的取值极限是根据损失函数在theta这个点上的相应的导数值,而不是说有一个固定的标准,普遍来讲,设置成0.01是比较稳的,发生异常的时候就可以试试绘制出曲线的方法来看看是不是因为学习率过大导致的问题
(歇了大半天,卑微准打工人)