Logo

郎哥编程

理解梯度下降算法

2021-03-11 53

梯度下降算法是不断通过迭代找到目标函数的最小值。假设目标函数可导,若寻找该函数的最小值,可以给定函数一个初始值t,计算函数在t处的导数,t与导数和学习率乘积的差作为函数的下一个输入值,依次类推,不断迭代,直至找到函数的最小值或函数收敛到一个最小值。

要理解梯度算法,最好的方法就是给出案例,下面先看一元函数的案例。

01.PNG

函数的导函数为:

02.PNG

要找到函数f(x)的最小值,需要给f(x)一个初始值,让f(x)从初始值开始迭代,每次迭代都要减小f(x)的值。设f(x)的初始值t = 2.1, f(x)的自变量x如何变化,才能让f(x)的值逐步减小呢?

03.png

观察上图发现,f(x)的最小值是0,当前点的位置为(2.1,4.41)。设当前位置为P(2.1,4.41),若让P点沿曲线向下运动(逐步减小自变量x的值),可到达函数的最小值;若让P点沿曲线向上运动(逐步增加自变量x的值),函数的值会趋向于无穷大。

问题是在寻找函数最小值的迭代过程中,程序如何判断x是增加还是减小呢?因为f(x)是连续函数,其单调性与导数的符号有着密切的联系,若函数在P点可导,当δ足够小时,若导数大于0,函数在以P点为中心的δ邻域内单调增加;若导数小于0时,函数在以P点为中心的δ邻域内单调减少。因此可以利用函数在P点的导数来确定自变量x在P点的变化方向。

因此导数对于最小化一个函数很有用,因为它让程序知道如何更改x来略微改善y,更改x的公式为:

04.PNG

上述公式使用当前x值减去f’(x)与a的乘积。

a是学习率,a的值由人工来设置,主要用来调整x变化的大小。找到一个合适的a对算法非常关键:若a设置过小,每次迭代x变化非常微小,算法需要花费较多的时间来找到函数的最小值;若a设置过大,每次迭代x变化较大,算法有可能找不到最小值,因为在迭代过程中算法可能会跨过函数的最小值。

f’(x)确定了x在迭代过程中的变化方向。若f’(x)大于0,a与f’(x)的乘积为正数,f(x)在x的δ邻域内单调增加,需要减小x的值; 若f’(x)小于0,a与f’(x)的乘积为负数,f(x)在x的δ邻域内单调减小,需要增加x的值。

当f’(x)等于0时,导数无法提供x的变化方向,此时f(x)的函数值不一定是全局极值点(函数的最小值或最大值)。f’(x)=0的点称为函数的临界点(也称为驻点),这些临界点可能是局部极值(函数局部最小值或最大值)。若函数存在多个局部极值点,算法可能找不到全局极值点,在这种情况下,即使算法找到的解不是函数的最小值,但只要对于目标函数显著低的值,就可以接受这样的解。

小结一下,上述算法通过学习率来控制x的变化大小,利用导数来确定x的变化方向,让x不断迭代以最小化某个函数,这种算法称为梯度下降算法。

例1  f(x)=x^2梯度下降算法示例

 函数:

05.PNG

导函数:

06.PNG

x的初始化值为x(0) = 2.1,学习率a = 0.2,迭代过程为:

第一次迭代:

07.png

第二次迭代:

08.png

经过多次迭代后,f(x^2)函数逐步收敛到最小值。

案例代码见课程资源(unit2/case05.py)

import numpy
from sympy import diff
from sympy import symbols
# 定义f(x)=x^2函数
def fun_x2(x):
    return x ** 2
 
# 求函数的导数
def fun_dy(x,value):
    m = diff(fun_x2(x),x)
    return m.subs(x,value)
 
# 程序入口
if __name__ == '__main__':
   
    x = symbols("x")
    # 定义学习率
    a = 0.2
    # 定义函数初始值  
    x_value = 2.1
    # 定义函数收敛的极小值
    min_value = 1E-5
    while True:
        x_value = x_value - a * fun_dy(x,x_value)
        y_value = fun_x2(x_value)
        print(x_value,y_value)
        # 函数收敛到min_value  
        if y_value < min_value:
           break

下面来看二元函数的案例。

函数:

09.PNG

对于二元及多元函数,我们需要用到偏导数的概念,偏导数表示函数一个变量发生变化时(其它变量作为常数),函数值的变化情况。对于二元函数f(x,y)来说,它有两个偏导数,分别是函数对变量x的偏导数和对变量y的偏导数。

10.PNG

f(x,y)的偏导数构成一个向量,记为:

11.PNG

梯度严谨的定义是相对一个向量求导的导数,向量的每个分量是多元函数关于每个变量的偏导数,此时多元函数的临界点是梯度中所有分量都为零的点。

对多元函数运用梯度下降算法求极小值,用到的基本运算是向量运算,与一元函数的运算有所不同。

例2  f(x)=x^2+y^2梯度下降算法示例

函数:

12.PNG

函数的梯度为:

13.PNG

f(x,y)的初始化值为f(2,3),即:

14.PNG

学习率a = 0.1,迭代过程为:

第一次迭代:

15.png

第二次迭代:

16.png

经过多次迭代后,f(x^2+y^2)函数逐步收敛到最小值。

案例代码见课程资源(unit2/case06.py)

import numpy as np
from sympy import diff
from sympy import symbols
# 定义f(x,y)=x^2+y^2函数
def fun_xy(x,y):
    return x ** 2 + y ** 2
 
# 求函数的导数
def fun_dy(x,y,xvalue,yvalue):
    dx = diff(fun_xy(x,y),x)
    dy = diff(fun_xy(x,y),y)
    x1 = dx.subs(x,xvalue)
    y1 = dy.subs(y,yvalue)
    return np.array([x1,y1])
 
# 程序入口
if __name__ == '__main__':
   
    x = symbols("x")
    y = symbols("y")
    # 定义学习率
    a = 0.1
    # 定义函数初始值
    v0 = np.array([1,3])
    # 定义函数收敛的极小值
    min_value = 1E-5
    # print(fun_dy(x,y,1,3))
    while True:
        v0 = v0 - a*fun_dy(x,y,v0[0],v0[1])
        y_value = fun_xy(v0[0],v0[1])
        print(y_value)
        # 函数收敛到min_value  
        if y_value < min_value:
           break

下面是一组测试刀具磨损速度的数据, 测试过程为每隔1小时,测量一次刀具的厚度,得到下面一组实验数据:

17.PNG

设时间为t,刀具厚度为为y,根据上面的数据建立时间t和刀具厚度y之间的函数关系y=f(t),利用梯度下降算法对数据进行线性回归预测。

绘制刀具磨损实验数据的散点图。

18.png

观察刀具磨损实验数据的散点图,每个数据点的连线大致接近于一条直线,可以认为y=f(t)是线性函数。

预测函数(也称为假设函数)为:

19.PNG

为了找出预测函数的系数a和b,我们还需要确定一个目标函数(也称为代价函数、损失函数),目标函数是对系数a和b进行优化,让预测函数预测的y值与真实的y值更接近。我们选用预测值与真实值的均方误差作为目标函数,目的是调整系数a和b,让预测值与真实值的均方误差最小。

目标函数为:

20.PNG

观察目标函数,t和y都是已知量,变量是a和b,该函数是二元函数二次函数,求这个函数的最小值。

可以分别求变量a和b的偏导函数,令偏导数为0,求导后解方程组可以解得a和b的值。偏导数为0是函数的驻点,但不一定是函数的最小值,不一定能找到最优解。使用梯度下降算法虽然也不一定能找到最优解,但至少能找到我们可以接受的解。

梯度下降算法的具体过程为:

1、 初始化目标函数变量a和b的值;

2、 设定可接受解的最小值mse_min和学习率alpha;

3、 迭代变量a和b的值;

(1)计算当前nowmse,若nowmse<mse_min,退出迭代;

(2)计算样本数据预测值与真实值的偏差;

(3)计算变量a和b的下一个迭代值;

例3  使用梯度下降算法确定刀具磨损数据的预测函数

案例代码见课程资源(unit2/case07.py)

from sympy import diff
from sympy import symbols
import numpy as np
import matplotlib.pyplot as plt
 
 
# 定义计算偏导的函数
def func(data,a,b):
    exp = ""
    for i,item in enumerate(data[0]):
       exp += "(" + str(data[0][i]) + "-" + "(" + str(data[1][i]) + "*a+b)) ** 2"
       if i != len(data[0]) - 1:
           exp += "+"
    return eval(exp)
 
# 计算均方误差函数
def mse(sum_quadratic,m):
   
    return sum_quadratic / m
 
# 程序入口
if __name__ == '__main__':
 
    # 训练数据集
    data = [[27,26.8,26.5,26.3,26.1,25.7,25.3],[0,1,2,3,4,5,6]]
   
    # 符号化a和b
    a = symbols("a")
    b = symbols("b")
    # 学习率
    alpha = 0.01
    # 预测函数a和b系数的初始值
    nowa = 0.1
    nowb = 0.1
    # 设定均方误差可接受值
    mse_min = 0.01
    # 定义计算均方误差对象
    mse_evalf = func(data,a,b)
    # 控制循环次数,防止程序陷入死循环
    count = 0
    while True:
        # 计算当前预测值与真实值均方误差  
        nowmse = mse_evalf.evalf(subs={a:nowa,b:nowb})/len(data[0])
        # 若当前均方误差小于可接受均方误差,退出循环
        if nowmse < mse_min:
            print(count)
            print(nowa)
            print(nowb)
            print(nowmse)
            break
        if count > 200:
            print(count)
            break
        count = count + 1
        # 计算目标函数变量a的偏导函数
        da = diff(func(data,a,b),a)
        # 计算nowa的当前值
        nowa = nowa - alpha *  da.subs({a:nowa,b:nowb})
        # 计算目标函数变量b的偏导函数
        db = diff(func(data,a,b),b)
        # 计算nowb的当前值
        nowb = nowb - alpha *  db.subs({a:nowa,b:nowb})

运行程序,输出结果如下:

迭代次数:119

均方误差(MSE):0.00982528611983628

预测函数系数a:-0.247498707068441

预测函数截距b:26.9516377429767

预测函数为:

21.PNG

绘制预测函数效果图,直观上了解一下预测函数是否合适,使用matplotlib绘制训练数据和测试数据的散点图,同时绘制预测函数的直线方程。

例4  绘制预测模型效果图

案例代码见课程资源(unit2/case08.py)

import numpy as np
import matplotlib.pyplot as plt
 
# 定义绘图函数
def plotter(ax, data1, data2, param_dict,type):
    if type == 0:
       ax.scatter(data1,data2,**param_dict)
    else:
       ax.plot(data1, data2, **param_dict)
 
# 绘制散点图
def draw_scatter(ax,x,y):
    plotter(ax,x,y,{'color': 'b','label':'刀具磨损数据散点图'},0)
 
# 获取预测模型直线方程数据
def get_line_data(x):
   y = -0.2475 * x + 26.9516
   return (x,y)
 
# 程序入口
if __name__ == '__main__':
    # 创建1个子轴
    fig, ax = plt.subplots(1,1)
 
    # 设置图例中文显示
    plt.rcParams['font.sans-serif'] = ['SimHei']
    # 训练数据集
    data = [[27,26.8,26.5,26.3,26.1,25.7,25.3],[0,1,2,3,4,5,6]]
    # 绘制刀具磨损数据散点图
    draw_scatter(ax,np.array(data[1]),np.array(data[0]))
   
    # 获取预测模型直线方程数据
    x1,y1 = get_line_data(np.array(data[1]))
    plotter(ax,x1,y1,{'color': 'm','label':'预测模型直线方程'},1)
    plt.show()

预测效果图如下:

22.png

刀具磨损实验数据(7,24.8)并没有用于算法训练,将其用于预测函数的测试数据,预测数值为:

26.PNG

真实数值与预测数值的偏差为:0.419。

下面使用矩阵进行梯度计算。

将上面的数据整理为矩阵:

27.PNG

预测函数定义为:

28.PNG

预测函数添加x(0)主要是为了矩阵计算方便,X为样本矩阵,w为参数向量,w(0)、w(1)为x(0)和x(1)的系数(也称为参数或权重),y为真实样本值的向量。

使用矩阵后,目标函数描述为:

29.PNG

目标函数的分母为2*m,分母乘以2纯粹是为了计算方便,目标函数求导时2会被约去。

目标函数的导函数为:

30.PNG

例5  使用矩阵进行梯度计算

案例代码见课程资源(unit2/case09.py)

import numpy as np
 
 
# 定义计算梯度的函数
def gradientfun(x,w,y):
    diff = np.dot(x,w)-y
    return (1/m) * np.dot(x.transpose(), diff)
 
# 计算均方误差函数
def mse(x,w,y):
   
    diff = np.dot(x,w) - y 
    return (1/(2*m)) * np.dot(diff.transpose(), diff)
 
 
# 程序入口
if __name__ == '__main__':
 
    # 定义矩阵X
    X = np.array([[1,1,1,1,1,1,1],[0,1,2,3,4,5,6]])
    # 转置矩阵X
    X = X.transpose()
    # 定义梯度向量w
    # 初始化梯度向量w
    w = np.array([0.1,0.1])
    # 转置矩阵w(行向量为1行矩阵)
    w = w.transpose()
    # 定义向量y
    y = np.array([27,26.8,26.5,26.3,26.1,25.7,25.3])
    # 转置矩阵y
    y = y.transpose()
 
    # 定义样本数量
    m = 7
    # 学习率
    alpha = 0.01
    # 设定均方误差可接受值
    mse_min = 0.01
    # 计算梯度
    gradient = gradientfun(X,w,y)
    count = 0
    while True:
        # 计算当前预测值与真实值均方误差  
        nowmse = mse(X,w,y)
        print(nowmse)
        # 若当前均方误差小于可接受均方误差,退出循环
        if nowmse < mse_min:
            print("迭代次数:" + str(count))
            print("均方误差(MSE):" + str(nowmse))
            print("预测函数系数a:" + str(w[1]))
            print("预测函数截距b:" + str(w[0]))
            break
        if count > 2000:
            print(count)
            break
        count = count + 1
        # 迭代梯度向量
        w = w - alpha * gradient
        # 计算梯度
        gradient = gradientfun(X,w,y)

运行程序,输出结果如下:

迭代次数:1635

均方误差(MSE):0.00999402061562005

预测函数系数a:-0.2240944373480152

预测函数截距b:26.852217719030463

预测函数为:

31.PNG

预测效果图如下:

32.png

刀具磨损实验数据(7,24.8)并没有用于算法训练,将其用于预测函数的测试数据,预测数值为:

33.PNG

真实数值与预测数值的偏差为:0.48。

代码在线纠错(通义千问 qwen-max)

支持粘贴多个代码文件,提交后由阿里云通义千问自动分析代码漏洞、语法错误、逻辑问题并给出修改建议。
您已解锁 AI 代码纠错功能,可正常使用!

评论区

登录 后发表评论
暂无评论