机器学习的最终目标就是获得泛化能力,而什么是泛化能力呢,就是指处理未被观察过的数据(非训练数据)的能力。
一、为何要引入损失函数
损失函数就是评估一个学习器“恶劣程度”的指标——即描述了当前学习器对训练数据在多大程度上不拟合、不一致。所谓让机器通过“学习”数据信息从而产生一个学习器的过程,就是去寻找最优参数,使得损失函数达到极小甚至最小的过程。
而这一过程如何实现?——答案就是求导,而导数就是所谓的“梯度”。
举个例子:假设
y
k
y_k
yk 表示一个学习器的预测结果,
t
k
t_k
tk 表示该数据的实际值,假设以均方误差为损失函数,那么损失函数可以表示为:
E
=
1
2
∑
k
=
1
n
(
y
k
−
t
k
)
2
E=\frac{1}{2}\sum^{n}_{k=1}(y_k-t_k)^2
E=21k=1∑n(yk−tk)2
而
y
k
y_k
yk是由训练数据和模型参数
β
,
α
\beta,\alpha
β,α共同决定的,可以表示为
y
k
=
f
(
x
;
β
,
α
)
y_k = f(x;\beta,\alpha)
yk=f(x;β,α)
因此,损失函数可以表示为
L
(
x
;
β
,
α
)
=
1
2
∑
k
=
1
n
(
f
(
x
;
β
,
α
)
−
t
k
)
2
L(x;\beta,\alpha)=\frac{1}{2}\sum^{n}_{k=1}( f(x;\beta,\alpha)-t_k)^2
L(x;β,α)=21k=1∑n(f(x;β,α)−tk)2
训练的任务就是要找到最优的
β
,
α
\beta,\alpha
β,α的值,使得
L
(
x
;
β
,
α
)
L(x;\beta,\alpha)
L(x;β,α)达到极小或最小,数学上如何求极小值/最小值?答案就是求导。
那么求导得到的“导数”(即梯度)是什么意义呢?它表示“如果稍稍改变权重参数的值,损失函数的值会如何变化”。如果导数的值为负,那么通过使模型参数正向变化,可以减小损失函数的值;如果导数的值为正,那么通过使模型参数负向变化,可以减小损失函数的值。而当导数为0时,无论模型的参数往什么方向变化,损失函数的值都不会改变。
那么问题来了,机器学习的最终目的是获得泛化性,也就是要提高预测或识别的精度,为什么不以精度为学习的目标,反而整一个损失函数为目标函数呢?
试想这样一个场景,假设有100条训练数据(测试数据也行~),如果有目前的预测精度为0.32,那么表示当前有32条数据预测正确。这时,如果我们微调模型参数,会有什么结果呢?
(1)调节的幅度过于微小,预测精度没有任何改善,还是0.32.
(2)预测精度有所改善,本来能预测对32个,现在预测对了33个,精度变为了0.33.
显然,精度对微小的参数变化基本上没有什么反应,即便有反应,它的值也是不连续的、突然的变化。就好比,当前精度是0.32,我们没法通过调参使其达到0.325;而如果当前损失函数是0.32,我们可以让其达到值域内的任意值,0.325, 0.3256,0.32567…
总结一下就是——精度是不连续的函数,而损失函数是连续型的函数;如果用精度最为目标,绝大多数地方的导数都将为0,参数将无法更新。
二、梯度和学习率
上文已经提及,梯度就是对损失函数求导的导数值,它表示“如果稍稍改变权重参数的值,损失函数的值会如何变化”。我们想要损失函数往最小的方向走,那么梯度的方向就是各点处函数值减小最多的方向。注意,梯度的方向并不一定指向最小值,但在每一点处,沿着梯度可以最大限度地减小损失函数的值。
像这样,通过不断沿着梯度方向前进,逐渐减小函数值的过程,就是梯度法。梯度法是解决机器学习最优化问题的常见方法。我们通过公式来描述梯度法,参数的迭代过程可以描述为
β
=
β
−
η
∂
f
∂
β
\beta=\beta-\eta\frac{\partial{f}}{\partial{\beta}}
β=β−η∂β∂f
α
=
α
−
η
∂
f
∂
α
\alpha=\alpha-\eta\frac{\partial{f}}{\partial{\alpha}}
α=α−η∂α∂f
其中,
η
\eta
η表示每次迭代的更新量,被称为学习率。它决定在一次学习中应该学习多少,以及在多大程度上更新参数。学习率是一个超参数(不能通过数据训练得到,而需要人工设定的参数),一般这个值过大或过小都无法抵达一个“好的位置”。
三、用Python简单实现一个梯度下降算法
【例】使用梯度法求解
f
(
x
0
,
x
1
)
=
x
0
2
+
x
1
2
f(x_0, x_1)=x_0^2+x_1^2
f(x0,x1)=x02+x12的最小值。
【求解】
step 1、首先定义一个求梯度的函数
import numpy as np
def gradient(f,x):
h = 1e-4 #h是求导时自变量的差值,设为很小的常数即可
grad = np.zeros_like(x) #
for idx in range(x.size):
tmp_val = x[idx]
x[idx] = tmp_val + h
fxh1 = f(x) #计算f(x+h)
x[idx] = tmp_val - h
fxh2 = f(x) #计算f(x-h)
grad[idx] = (fxh1 - fxh2)/(2*h)
x[idx] = tmp_val #还原
return grad
step 2、定义一个函数用于实现梯度下降的过程
def descent(f, init_x, lr = 0.01, step_num = 100):
x = init_x
for i in range(step_num): #step_num为迭代次数
grad = gradient(f, x) #求梯度
x -= lr*grad #每次迭代用学习率乘以梯度
return x
step 3、定义待求解函数并初始化输出 x 0 x_0 x0和 x 1 x_1 x1的最小值
def func(x):
return x[0]**2 + x[1]**2
init_x = np.array([-3.0, 4.0])
descent(func, init_x = init_x, lr = 0.1, step_num = 100)
最终的输出结果为
array([-6.11110793e-10, 8.14814391e-10])
最终结果非常接近于0,这是正确的。这里可以做一个实验,如果学习率过大或过小,都不会得到接近于0的结果:
print(descent(func, init_x = init_x, lr = 10, step_num = 100))
print(descent(func, init_x = init_x, lr = 1e-10, step_num = 100))
输出结果如下,可见学习率过大的话,最终会发散成一个很大的值;而学习率过小的话,基本上没怎么更新就结束了。
[-2.58983747e+13 -1.29524862e+12]
[-2.99999994 3.99999992]
本文参考了《深度学习——基于python的理论与实践》
欢迎指正!