4.3.1 改进冷热法--梯度下降法
冷热法每次更新权重都要重新算一次,而且每次更新的权重改变虽然有一定的伸缩,但实际上更新权重完全没有依据,就是靠左右碰撞试错来搜索更新的路径。
那么能不能改进这种算法,让它学习的效率变好呢?
当然可以。要改进算法,首先需要看到算法的局限性。冷热法是能够发现局域最优解的,但局限在于以下两点:
- 每次更新权重的方向不确定。
- 每次更新权重的大小依赖于初始猜测。
如果我们能够打破以上的两个局限性,我们就找到了比冷热法更优秀的算法,而梯度下降法就是这样的更优秀的算法。
比如我们看如下的代码:
weight = 0.5
goal_pred = 0.8
input = 0.5
learning_rate = 0.8
for iteration in range(50):
pred = input * weight
error = (pred - goal_pred)**2
direction_and_amount = (pred - goal_pred) * input
weight -= learning_rate * direction_and_amount
print("Error:" + str(error) + " Prediction: " + str(pred))
上面的代码中,最重要的一句在for循环中的第三行,即:
direction_and_amount = (pred - goal_pred) * input
它是更新权重的最重要的依据。我们看到这一项完全不依赖于初始的设定值weight,它只和上一次的weight相关。而且下一步更新权重的正负号也完全确定,不需要探索两个方向。这说明这个算法的确解决了冷热法的两大局限。
尝试运行上面的代码,你会发现,在区区50步内,误差已经小到十亿分之一的量级。这说明上面的算法惊人地高效。
那么究竟这一项是如何想出来的呢?
人类不同于机器,从代码看学习的算法总是太过繁琐且不够直观。所以为了让我们看清楚上面的代码的内在含义。我们把上面的代码用函数的方法来表达出来。
实际上,我们目前遇到的学习问题无非是给定输入和输出,调整中间的函数,使得输入和输出匹配。这种问题事实上我们早就接触过了。
你可以先回忆一下函数的定义。函数是什么?
函数建立了定义域和值域之间的映射。它把定义域中的点映射到值域中的点。
这与我们需要学习的问题并无不同,我们就是在找这样的映射函数。所以我们把这个学习问题转换为一个代数问题:
给定x的值为0.5,y的值为0.8,求y对x的依赖关系。
你可能很快就能写出如下的解答:
的确,这是一个非常简单的一次函数求逆的过程,而且我们可以直接一步就得到精确解,误差为零。但是这样的可以严格求解的问题并不多见,绝大多数的问题实际上无非严格求解。在现实世界,我们不能期待0误差的结果。所以我们需要好好利用误差来调节我们的函数。我们现在假定不知道上面的精确解,只知道如下的待定的映射函数形式:
需要迭代求解的是权重。利用的给定值,我们写出误差函数:
要使得上面的误差最小,我们注意到误差函数实际上是一个关于的二次函数。也就是说,我们每次预测,其误差都可以在这个二次函数上找到一个点来对应。很自然地,如果我们每次预测都向这个二次函数的最低点走一步,那么最终我们会走到这个二次函数的底部。 也就达成了预测的目标。
答案呼之欲出了!我们需要做的就是沿着当前点的切线下降!
假如第一步我们猜测的y的值是:
那么第二步就是:
上式看上去很可怕,但是其实就是在说沿着当前点的切线下降。其中就是在这个预测点处的误差函数的斜率,或者叫导数。就是学习速率,也就是代码中的那个 learning_rate 。负号代表是下降的意思。
这个式子非常非常重要,请你仔细思考其背后的意义。这个章节中其它的所有东西你都可以含糊带过,但一定要理解这个式子。它贯穿了深度学习的始终。它就是梯度下降法的本质。