梯度下降算法是不断通过迭代找到目标函数的最小值。假设目标函数可导,若寻找该函数的最小值,可以给定函数一个初始值t,计算函数在t处的导数,t与导数和学习率乘积的差作为函数的下一个输入值,依次类推,不断迭代,直至找到函数的最小值或函数收敛到一个最小值。
要理解梯度算法,最好的方法就是给出案例,下面先看一元函数的案例。
函数的导函数为:
要找到函数f(x)的最小值,需要给f(x)一个初始值,让f(x)从初始值开始迭代,每次迭代都要减小f(x)的值。设f(x)的初始值t = 2.1, f(x)的自变量x如何变化,才能让f(x)的值逐步减小呢?

观察上图发现,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的公式为:
上述公式使用当前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梯度下降算法示例
函数:
导函数:
x的初始化值为x(0) = 2.1,学习率a = 0.2,迭代过程为:
第一次迭代:

第二次迭代:

经过多次迭代后,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下面来看二元函数的案例。
函数:
对于二元及多元函数,我们需要用到偏导数的概念,偏导数表示函数一个变量发生变化时(其它变量作为常数),函数值的变化情况。对于二元函数f(x,y)来说,它有两个偏导数,分别是函数对变量x的偏导数和对变量y的偏导数。
f(x,y)的偏导数构成一个向量,记为:
梯度严谨的定义是相对一个向量求导的导数,向量的每个分量是多元函数关于每个变量的偏导数,此时多元函数的临界点是梯度中所有分量都为零的点。
对多元函数运用梯度下降算法求极小值,用到的基本运算是向量运算,与一元函数的运算有所不同。
例2 f(x)=x^2+y^2梯度下降算法示例
函数:
函数的梯度为:
f(x,y)的初始化值为f(2,3),即:
学习率a = 0.1,迭代过程为:
第一次迭代:

第二次迭代:

经过多次迭代后,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小时,测量一次刀具的厚度,得到下面一组实验数据:
设时间为t,刀具厚度为为y,根据上面的数据建立时间t和刀具厚度y之间的函数关系y=f(t),利用梯度下降算法对数据进行线性回归预测。
绘制刀具磨损实验数据的散点图。

观察刀具磨损实验数据的散点图,每个数据点的连线大致接近于一条直线,可以认为y=f(t)是线性函数。
预测函数(也称为假设函数)为:
为了找出预测函数的系数a和b,我们还需要确定一个目标函数(也称为代价函数、损失函数),目标函数是对系数a和b进行优化,让预测函数预测的y值与真实的y值更接近。我们选用预测值与真实值的均方误差作为目标函数,目的是调整系数a和b,让预测值与真实值的均方误差最小。
目标函数为:
观察目标函数,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
预测函数为:
绘制预测函数效果图,直观上了解一下预测函数是否合适,使用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()预测效果图如下:

刀具磨损实验数据(7,24.8)并没有用于算法训练,将其用于预测函数的测试数据,预测数值为:
真实数值与预测数值的偏差为:0.419。
下面使用矩阵进行梯度计算。
将上面的数据整理为矩阵:
预测函数定义为:
预测函数添加x(0)主要是为了矩阵计算方便,X为样本矩阵,w为参数向量,w(0)、w(1)为x(0)和x(1)的系数(也称为参数或权重),y为真实样本值的向量。
使用矩阵后,目标函数描述为:
目标函数的分母为2*m,分母乘以2纯粹是为了计算方便,目标函数求导时2会被约去。
目标函数的导函数为:
例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
预测函数为:
预测效果图如下:

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