批梯度下降的主要问题是,它使用整个训练集来计算每一步的梯度,这使得在训练集很大时非常慢。在另一个的极端情况下,随机梯度下降在每一步的梯度计算中,只会选取训练集中随机一个样本,并只根据该单个样本计算梯度。显然,这使得算法更快,因为它在每次迭代中只有很少的数据被操作。它还使得在大型训练集上进行训练成为可能,因为在每次迭代中只有一个样本需要在内存中(SGD【即随机梯度下降】可以被实现为一个核外算法)。
另一方面,由于它的随机性特点,这个算法比批量梯度下降的规则要少得多:代价函数将会上下波动,而不是平缓地下降到最低点。随着时间的推移,它将会非常接近最小值,但是一旦它到达那里,它就会继续反弹,永远不会稳定下来(见图4-9)。因此,一旦算法停止,最终的参数值是不错的,但却不是最优的。
图4 - 9 随机梯度下降法
当代价函数非常不规则时(如图4-6所示),这实际上可以帮助算法从局部极小值中跳出来,所以随机梯度下降比批梯度下降有更好的机会获得全局的最低点。
因此,随机性是摆脱局部最优解的一个很好的方式。但糟糕的是,这意味着算法可能永远无法找到真正的最优解。解决这个困境的方法是,逐渐的降低提低下降的学习速率。这些步骤开始时取较大的学习速率(这有助于快速的处理,并避免得到局部极小值),然后慢慢的调低学习速率的值,使得该算法维持在全局最小值处。这个过程被称为模拟退火算法【simulated annealing】,因为它类似于冶金过程中的退火过程,熔融金属慢慢冷却下来。在每次迭代中决定学习速率的函数称为学习计划【 learning schedule】。如果学习速度降低得太快,你可能会被困在一个局部最小值,或者甚至在最小值的一半处被冻结。如果学习速率降低得太慢,你可能会在很长一段时间内跳过最低点(就是步子迈的太大,跨过最低点了),如果你过早停止训练,你就会得到一个次优的解决方案。
这段代码使用一个简单的学习计划【 learning schedule】实现了随机梯度下降:
n_epochs = 50
t0, t1 = 5, 50 # learning schedule hyperparameters
def learning_schedule(t):
return t0 / (t + t1)
theta = np.random.randn(2,1) # random initialization
for epoch in range(n_epochs):
for i in range(m):
random_index = np.random.randint(m)
xi = X_b[random_index:random_index+1]
yi = y[random_index:random_index+1]
gradients = 2 * xi.T.dot(xi.dot(theta) - yi)
eta = learning_schedule(epoch * m + i)
theta = theta - eta * gradients
按照惯例,我们迭代了m次;每一轮都被称为新纪元[epoch]。虽然批量梯度下降代码在整个培训集中重复了1000次,但是这段代码只经过了50次,并且得到了一个相当好的解决方案:
>>> theta
array([[ 4.21076011],
[ 2.74856079]])
图4-10显示了训练的前10个步子(注意每一步是多么不规则)。
图4-10 随机梯度下降前10步
请注意,由于样本是随机挑选的,所以有些样本可能会被多次选中,而另一些则可能根本不会被选中。如果你想确保算法在每个新纪元[epoch]都经历了每一个样本,那么另一种方法就是洗牌,然后通过实例进行实例,然后再洗牌,以此类推。然而,这样通常是收敛得更慢。
要使用Scikit-Learn,以SGD进行线性回归,您可以使用SGDRegreuse类,它默认为优化平方误差代价函数。
下面的代码运行50个周期/新纪元[epoch],从0.1(eta0=0.1)的学习速率开始,使用默认的学习计划(不同于前面的学习计划【 learning schedule】),并且它不使用任何正则化(penalty=None ;关于这一点有更多细节需要自己学习):
from sklearn.linear_model import SGDRegressor
sgd_reg = SGDRegressor(n_iter=50, penalty=None, eta0=0.1)
sgd_reg.fit(X, y.ravel())
再一次,你找到了一个非常接近于正规方程所得到的的解决方案:
>>> sgd_reg.intercept_, sgd_reg.coef_
(array([ 4.18380366]), array([ 2.74205299]))