梯度下降法中学习率的观察

发布于:2025-04-11 ⋅ 阅读:(42) ⋅ 点赞:(0)

一、梯度下降法中学习率的观察

梯度下降法并不是一个机器学习的算法,它既不能解决回归问题,也不能解决分类问题。
那么它是什么呢?梯度下降时一种基于搜索的最优化的方法,它的目标是用于优化一个目标函数。
在机器学习中,梯度下降法的作用就是:最小化一个损失函数

1.1 梯度

梯度下降的梯度是什么?函数在某⼀点的梯度是这样⼀个向量,它的⽅向与取得最⼤⽅向导数的⽅向⼀致,⽽它的模为⽅向导数的最⼤值。在存在多个变量的函数中,梯度是⼀个向量,向量有⽅向,梯度的⽅向就指出了函数在给定点的上升最快的⽅向。这就意味着我们需要到达⼭底,那么在我们下⼭测量中,梯度就告诉我们下⼭的⽅向。梯度的⽅向是函数在给定点上升最快的⽅向,那么反⽅向就是函数在给定点下降最快的⽅向,所以我们只要沿着梯度的反⽅向⼀直⾛,就能⾛到局部的最低点!对于梯度的理解可能不太容易,我们需要搜索⼀些梯度与导数的资料,然后再去看⼀部分拟⽜顿法的资料来进⾏深刻的体会理解。

由于神经⽹络模型中有众多的参数,也称为权重参数(weight parameter)所以我们常常需要处理的是多元复合函数,要想知道某⼀个权重参数对损失函数的影响,那么就要求它的偏导数。因此对于权重参数,我们是可以确定向量的⽅向的,就是求它的导数值就可以了(所以说,要想理解梯度下降的数学原理必须要明⽩导数微分的概念)

权重参数
权重参数是模型中用于衡量输入量重要程度的数值参数。以线性模型为例,如简单线性回归模型 y = β 0 + β 1 x y = \beta_0 + \beta_1 x y=β0+β1x,其中 β 1 \beta_1 β1 就是权重参数(这里 x x x 是自变量),它表示自变量 x x x 对因变量 y y y 的影响程度和方向。在更复杂的神经网络模型中,权重参数存在于神经元之间的连接上,每个连接都有一个对应的权重值。

下面我们给出梯度下降的描述方法:

x n + 1 = x n − η ∂ f ( x , y ) ∂ x x_{n+1} = x_n - \eta \frac{\partial f(x, y)}{\partial x} xn+1=xnηxf(x,y)

在这解释一下式子,例如原函数为 z = f ( x , y ) = x 2 + y 2 z = f(x, y) = x^2 + y^2 z=f(x,y)=x2+y2,那么上述公式是一次迭代更新 x x x 求解的过程。 x n + 1 x_{n+1} xn+1 是更新后的 x x x x n x_n xn 是当前的 x x x ∂ f ( x , y ) ∂ x \frac{\partial f(x, y)}{\partial x} xf(x,y) 代表在 f ( x , y ) f(x, y) f(x,y) 函数中对 x x x 求偏导数, η \eta η 代表学习率。

1.2 学习率

学习率看起来很陌⽣,但结合着下⼭问题来解释就很好理解了。⽐如说,我们说要找⼀个合适的测量频率,保证我们下⼭⼜快,测量次数⼜少。那么这个学习率就是影响我们测量频率的因素。可以将其理解为梯度下降过程中的步⻓。
在我们训练模型的时候,学习率是⾥⾯很重要的⼀个超参数(Hyperparameters),它决定着我们的损失函数能否收敛到最⼩值,还有需要多⻓时间才能收敛到最⼩值。⼀个合适的学习率能够让我们的损失函数在合适的时间内收敛到局部最⼩值。

超参数(Hyperparameters)是指那些在训练过程开始之前就需要⼿动设置的参数,它们不能通过模型从数据中学习得
到,⽽是需要通过⼈为的经验、实验或者特定的搜索⽅法来进⾏选择和调整。

1.3 梯度下降法的模拟与可视化

在这里插入图片描述

上图描述的是一个损失函数函数不同参数取值的情况下(x轴),对应变化值(y轴)
对于损失函数,应该有一个最小值。对于最小化损失函数这样一个目标,实际上就是在上图所示的坐标系中,找到合适的参数,使得损失函数的取值最小
在这个例子中是在2维平面空间上的演示,所以参数只有1个(x轴的取值)。意味着每个参数,都对应着一个损失函数值。实际上模型的参数往往不止一个,所以这个图像描述的只是损失函数优化过程的一种简单表达。

# 梯度下降
import numpy as np 
import matplotlib.pyplot as plt # 画图
#创建等差数列x,代表模型的参数取值,共150个
plot_x = np.linspace(-1, 6, 150) # 生成-1到6之间的150个点
print(plot_x) # 生成的点
#通过二次方程来模拟一个损失函数的计算,plot_y代表损失函数的值
plot_y = (plot_x - 2.5) ** 2 - 1 # 生成对应的y值
plt.plot(plot_x, plot_y) # 画出图像
plt.show() # 显示图像 
[-1.         -0.95302013 -0.90604027 -0.8590604  -0.81208054 -0.76510067
 -0.71812081 -0.67114094 -0.62416107 -0.57718121 -0.53020134 -0.48322148
 -0.43624161 -0.38926174 -0.34228188 -0.29530201 -0.24832215 -0.20134228
 -0.15436242 -0.10738255 -0.06040268 -0.01342282  0.03355705  0.08053691
  0.12751678  0.17449664  0.22147651  0.26845638  0.31543624  0.36241611
  0.40939597  0.45637584  0.5033557   0.55033557  0.59731544  0.6442953
  0.69127517  0.73825503  0.7852349   0.83221477  0.87919463  0.9261745
  0.97315436  1.02013423  1.06711409  1.11409396  1.16107383  1.20805369
  1.25503356  1.30201342  1.34899329  1.39597315  1.44295302  1.48993289
  1.53691275  1.58389262  1.63087248  1.67785235  1.72483221  1.77181208
  1.81879195  1.86577181  1.91275168  1.95973154  2.00671141  2.05369128
  2.10067114  2.14765101  2.19463087  2.24161074  2.2885906   2.33557047
  2.38255034  2.4295302   2.47651007  2.52348993  2.5704698   2.61744966
  2.66442953  2.7114094   2.75838926  2.80536913  2.85234899  2.89932886
  2.94630872  2.99328859  3.04026846  3.08724832  3.13422819  3.18120805
  3.22818792  3.27516779  3.32214765  3.36912752  3.41610738  3.46308725
  3.51006711  3.55704698  3.60402685  3.65100671  3.69798658  3.74496644
  3.79194631  3.83892617  3.88590604  3.93288591  3.97986577  4.02684564
  4.0738255   4.12080537  4.16778523  4.2147651   4.26174497  4.30872483
  4.3557047   4.40268456  4.44966443  4.4966443   4.54362416  4.59060403
  4.63758389  4.68456376  4.73154362  4.77852349  4.82550336  4.87248322
  4.91946309  4.96644295  5.01342282  5.06040268  5.10738255  5.15436242
  5.20134228  5.24832215  5.29530201  5.34228188  5.38926174  5.43624161
  5.48322148  5.53020134  5.57718121  5.62416107  5.67114094  5.71812081
  5.76510067  5.81208054  5.8590604   5.90604027  5.95302013  6.        ]

在这里插入图片描述

⾸先定义⼀个函数,来计算损失函数对应的导数。⽬标就是计算参数$ \theta 的导数还需要定义⼀个函数来计算 的导数 还需要定义⼀个函数来计算 的导数还需要定义个函数来计算 \theta $值对应的损失函数

# 计算导数
def dervate(theta):  #dervate是导数
    return 2 * (theta - 2.5)

# 计算损失函数
def loss(theta):  #loss是损失函数
        return (theta - 2.5) ** 2 - 1

计算梯度值

#以0作为teata的初始值
theta = 0.0 #参数初始值
eta = 0.1 #学习率(学习率过大会有溢出)
epsilon = 1e-8 #精度

while True: 
    gradient = dervate(theta) #计算当前teata对应点的梯度(导数)

    last_theta = theta #更新teata前,先累计保存上一次的teata值

    theta = theta - eta * gradient #更新参数值,向导数的负方向移动一步,步长使用eta(学习率)来控制

    # 理论上theta最小值为0是最佳,但实际情况下,theta很难达到刚好等于0的情况
    # 所以我们设置一个精度最小值epsilon,来表示我们需要theta达到的最小值目标 
    # 如果当前theta与上一次的theta差值小于epsilon,则认为已经达到最小值
    # 满足的话就停止循环
    # abs是绝对值函数

    if abs(loss(theta) - loss(last_theta)) < epsilon: #中断循环条件(如果连续损失函数值变化很小,就停止)
        break  


print(theta) #打印更新后的参数值,可以看到参数值向最小值方向移动了(多次打印)
print(loss(theta)) #打印损失函数值,随着参数值的更新,损失函数值也在不断减小
2.499891109642585
-0.99999998814289

通过结果可以看出,当theta取2.5时候,损失函数的最⼩值正好对应着截距-1

1.4 学习率对梯度的影响

为了能够计算不同学习率下,theta的每一步变更值。我们对代码进行一些补充

theta = 0.0 
eta = 0.1 
epsilon = 1e-8 

#添加一个记录每一步theta变更的list
theta_history = [theta]

while True: #设置最大循环次数
    gradient = dervate(theta) 
    last_theta = theta 
    theta = theta - eta * gradient 
    # 记录每一步theta变更 
    theta_history.append(theta) 

    if abs(loss(theta) - loss(last_theta)) < epsilon: 
        break 

绘制theta每一步的变更


plt.plot(plot_x, plot_y) #画出图像
plt.plot(np.array(theta_history), loss(np.array(theta_history)), color='r', marker='+') #画出参数值的变化
plt.show() #显示图像

在这里插入图片描述

可以看到theta在下降过程中,每⼀步都在逐渐变⼩(逐渐逼近),直到满⾜⼩于epsilon的条件。
查看⼀下theta_history的⻓度

len(theta_history) #打印theta_history的长度
46

我们通过梯度下降法,经过了45(减去初始值)次查找,最终找到了theta的最⼩值

为了更⽅便的调试eta(学习率)的值,我们对代码进⾏进⼀步的封装。把梯度下降⽅法
和绘图分别封装为两个函数

theta_history = [] 

def gradient_descent(initial_theta, eta, epsilon=1e-8):
    theta = initial_theta
    theta_history.append(initial_theta) # 记录初始值

    while True:
        gradient = dervate(theta) 
        last_theta = theta 
        theta = theta - eta * gradient 
        # 记录每一步theta变更 
        theta_history.append(theta) 

        if abs(loss(theta) - loss(last_theta)) < epsilon: 
            break 
        
def plot_theta_history():
    plt.plot(plot_x, plot_y) #画出图像
    plt.plot(np.array(theta_history), loss(np.array(theta_history)), color='r', marker='+') #画出参数值的变化
    plt.show() #显示图像

尝试更小的eta值

eta = 0.01
theta_history = []
gradient_descent(0.0, eta) #调用函数
plot_theta_history() #画出图像

在这里插入图片描述

theta下降的路径中,每一步的长度更短了,步数也更多了

len(theta_history) #打印theta_history的长度
424

这次经过了423步才得到theta的最⼩值
eta就是梯度下降中我们常常谈到的学习率learn rate(lr)。学习率的⼤⼩直接影响到梯 度更新的步数。很明显,学习率越⼩,theta下降的步⻓越⼩。所需要的步数(计算次 数)也就越多。

尝试更小的学习率

eta = 0.001
theta_history = []
gradient_descent(0.0, eta) #调用函数
plot_theta_history() #画出图像

在这里插入图片描述

步长更加密集了

len(theta_history) #打印theta_history的长度
3682

共经过了3281步
那么学习率调大一些是不是更好呢?

eta = 0.8
theta_history = []
gradient_descent(0.0, eta) #调用函数
plot_theta_history() #画出图像

在这里插入图片描述

theta 变成了从曲线的左侧跳到了右侧,由于eta还是够⼩。最终我们还是计算得到了theta 的最⼩值
如果更大一些的学习率,会是什么效果呢?

eta = 1.1
theta_history = []
gradient_descent(0.0, eta) #调用函数
plot_theta_history() #画出图像
---------------------------------------------------------------------------

OverflowError                             Traceback (most recent call last)

Cell In[19], line 3
      1 eta = 1.1
      2 theta_history = []
----> 3 gradient_descent(0.0, eta) #调用函数
      4 plot_theta_history() #画出图像


Cell In[11], line 14, in gradient_descent(initial_theta, eta, epsilon)
     11 # 记录每一步theta变更 
     12 theta_history.append(theta) 
---> 14 if abs(loss(theta) - loss(last_theta)) < epsilon: 
     15     break


Cell In[6], line 7, in loss(theta)
      6 def loss(theta):  #loss是损失函数
----> 7         return (theta - 2.5) ** 2 - 1


OverflowError: (34, 'Result too large')

直接报错Result too large,结果值太大
为什么会产生这样的效果呢?
原因是eta太⼤所导致的,theta在每次更新也会变得越来越⼤。梯度⾮但没有收敛,反⽽在向着反⽅向狂奔。直到结果值最终超出了计算机所能容纳的最⼤值,错误类型OverflowError(计算溢出错误)


为了避免计算值的移出,我们改造损失值计算方法


# 计算损失函数
def loss(theta):  #loss是损失函数
    try:
        return (theta - 2.5) ** 2 - 1
    except:
        return float('inf') #如果计算出错,返回无穷大

给梯度更新方法添加一个最大循环次数

def gradient_descent(initial_theta, eta, n_iters=1e4, epsilon=1e-8):
    theta = initial_theta
    i_iter = 0 #初始循环次数
    theta_history.append(initial_theta) # 记录初始值

    while i_iter < n_iters: #小于最大循环次数
        gradient = dervate(theta) 
        last_theta = theta 
        theta = theta - eta * gradient 
        # 记录每一步theta变更 
        theta_history.append(theta) 

        if abs(loss(theta) - loss(last_theta)) < epsilon: 
            break 
        i_iter += 1
eta = 1.1
theta_history = []
gradient_descent(0.0, eta) #调用函数

此时执行没有报错了

len(theta_history) #打印theta_history的长度
10001

达到最大循环次数后,梯度计算就停止了
为了观察eta = 1.1时,theta到底是如何更新的。我们指定一个有限的梯度更新次数(10次)

eta = 1.1
theta_history = []
gradient_descent(0.0, eta,n_iters=10) #调用函数
plot_theta_history() #画出图像

在这里插入图片描述

可以看到theta的值从曲线出发,逐渐向外。最终越变越⼤

总结

学习率的最佳取值

学习率 η \eta η 的取值,是不是1就是极限值呢?

实际上 η \eta η 的取值是和损失函数相关的,或者说是和 θ \theta θ 的导数相关的。所以没有一个固定标准。

所以,学习率对于梯度下降法来说也是一个超参数,也需要网络搜索来寻找。
保险的方法,是把 eta 先设置为 0.01 0.01 0.01,然后逐渐寻找它的最佳取值。大多数函数,都是可以胜任的。

如果出现了参数值过大,也可以用上面的方法绘制图形来查看


梯度更新

在得到了逻辑回归的损失函数后,我们就可以使用梯度下降法寻找损失函数极小值。直白的说,就是要找出,当 θ \theta θ 取什么值时,损失函数可以到达极小值。

要求出梯度,我们要对 J ( θ ) J(\theta) J(θ) 对于 θ j \theta_j θj 的偏导数 ∂ J ( θ ) ∂ θ j \frac{\partial J(\theta)}{\partial \theta_j} θjJ(θ)。求偏导的意义在于,描述函数在某一点的变化率。梯度越小,说明变化率越小,趋近于 0 0 0 时,就说明参数已经收敛了。


θ \theta θ 的偏导

∂ ∂ θ j J ( θ ) = 1 m ∑ i m ( 1 1 + e − θ T x i − y i ) x i j = 1 m ∑ i m ( y ^ i − y i ) x i j \frac{\partial}{\partial \theta_j} J(\theta) = \frac{1}{m} \sum_i^m \left( \frac{1}{1 + e^{-\theta^T x_i}} - y_i \right)x_{ij} = \frac{1}{m} \sum_i^m (\hat{y}_i - y_i)x_{ij} θjJ(θ)=m1im(1+eθTxi1yi)xij=m1im(y^iyi)xij

delta_theta = np.dot((y_hat - y), X) / m

b i a s bias bias 的偏导

∂ ∂ b i a s J ( b i a s ) = 1 m ∑ i m ( y ^ i − y i ) \frac{\partial}{\partial bias} J(bias) = \frac{1}{m} \sum_i^m (\hat{y}_i - y_i) biasJ(bias)=m1im(y^iyi)

delta_b = np.mean(y_hat - y)

实际运算中,我们带入的是向量 X X X。所以最终的梯度会在求和后取均值。