持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第8天,点击查看活动详情
上一篇了解了线性回归、标准方程。我们知道Scikit-Learning的LinearRegression()就是一个线性回归。但是他是基于SVD算出伪逆,最后伪逆点乘预测值(标签)求出我们需要的。
但是不管是标准方程(时间复杂度为O()到O()还是SVD(时间复杂度是O()的计算量都很大,因此我现在要讲的是一种新的找的方式-梯度下降。
梯度下降
梯度下降是很常用的优化算法,能够在大范围的问题中找到最优解。梯度下降做的就是,不断迭代地调整参数,从而使得成本函数最小化。
图1 梯度下降(Gradient Descent)
让我们看图1,我们看到得每一个黑点就是一个,第一个值是随机的。我们的算法每一次迭代调整参数后,就会向着能使得成本函数最小的走去。
而每次怎么走多远(步长),就需要根据一个超参数-学习率。在设置学习率时,如果设置太小,下降得就会越慢,虽然最终我们还是能够得到最优解,但是需要时间会很长。
但是我们也要注意设置学习率不能太大,否则会遇见如图2的现象:
图2 学习率过高
迭代得到的不仅没有朝着最小值去,反而还在远离,也就是在发散。这样我们无论怎么迭代,最终都不会得到最优解了。
陷阱
除了学习率可能存在问题之外,还有就是我们的成本函数也会存在陷阱,如图3所示:
图3 陷阱
如果我们的成本函数不是那么完美的像一个碗的凸函数,这里的凸函数不要和高数的凸函数搞混了,他们的方向刚好是相反的,数学分析中以下凸为凸函数,这个问题之前还把我搞愣住了,后来我去查了一下才知道原来方向是不一样的- -!。
像如图三一样的成本函数存在局部最小值,也会导致我们的算法最终找错,或者如果随机点在右边开始,那么我们看到他有一段比较平坦的,这个会导致需要多次迭代,如果时间不够,也会导致我们找不到全局最小值。
幸运的是,我们用的MSE是一个凸函数(类似图2),只有全局最小值。
同时我们需要注意,例如现在又两个特征和,如果他们的取值范围比较接近(如进行标准化后的特征),那么我们能够更快的找到最优解,但是如果两个特征取值范围相差很大,会导致最终消耗的时间增加,这是为什么呢?我们先看一张图:
图4 有(左图)和没有(右图)特征缩放的梯度下降
我们这里以两个特征当坐标,中间的圆形就是我们的成本函数,圆心为最小值。左图的梯度下降中,我们发现它能够很快找到最小值,但是右边的似乎就不那么顺利,一开始也挺快的,但是到了相对平坦的后面部分时,却变得很慢。所以我们需要做好特征缩放。
批量梯度下降
让我们开始实现一下梯度下降吧。首先我们需要计算出每个关于的梯度,也就是他是怎么变小的。这里就需要成本函数对求偏导数。还记得我们的成本函数MSE吗?忘记了也没事,我重新再写一遍:
OK,接下来我们求出的偏导数一般会简写成 ):
成本函数的梯度向量(记作):
当我们得到了梯度向量后,我们就需要将其乘上学习率,最后减去前面的结果,就是下一步的:
^^!终于把烦人的公式了解完了,我们赶紧看看怎么用代码实现:
# 借用了上一篇的X和y,原谅我懒人一个
X = 2 * np.random.rand(100,1)
y = 4 + 3 * X + np.random.randn(100,1)
X_b = np.c_[np.ones((100,1)),X]
# 学习率
eta = 0.1
# 迭代次数
n_iterations = 1000
# 特征数
m=100
# 初始化随机的theta
theta = np.random.randn(2,1)
# 不断循环迭代
for iteration in range(n_iterations):
gradients = 2/m * X_b.T.dot(X_b.dot(theta)-y)
theta = theta - eta * gradients
theta # 输出 array([[3.81554623], [2.94310113]])
最后我们求出的结果还是不错的,这里需要注意的是,我们用了全部的训练集的数据,来计算每一步,这个就是我们实现的-批量梯度下降
我们再看看不同学习度的表现,如下图:
图5 各种学习度的梯度下降
这次我们能够很清楚的看到,如果学习度很小(左图),那么我们要找到最优解就会很慢,但是如果过大(右图),我们就会在训练中错过最优解。
随机梯度下降
我们实现了了批量梯度下降,也会发现一个速度慢的原因:每次计算梯度都需要全部的训练集。那有没有不需要全部数据集的实现方法呢,那必须有:就是接下来我们要看到的-随机梯度下降。它在每次计算梯度时,只需要随机取一个实例。
看到这个随机取一个计算,我们自然也能想到会有一个问题:那就是每次计算出的梯度会很不规则,大大小小,导致整个窜上窜下的。不过也正因为如此,我们的不容易被局部最小值困住,但他也永远不会走到全局最小值,而是不断在全局最小值附近旋转跳跃。因此最后算法的得出的值不是最优的,但也是足够好的。
我们知道,因为梯度计算的不规则性,最后我们需要让不要太随意窜来窜去,我们就需要降低学习度,让他能够一步一步走向最优解,我们把这个降低学习度的方法叫学习率调度。ok,上代码来:
m = len(X_b)
np.random.seed(42)
# 迭代次数
n_epochs = 50
# 学习率调度
t0, t1 = 5, 50
def learning_schedule(t):
return t0 / (t + t1)
# 随机初始化
theta = np.random.randn(2,1)
# 循环迭代
for epoch in range(n_epochs):
# 因为我们有m个实例,每次随机获取一个实例
# 其实在这边我们也发现了,如果按照这样随机来取,有些实例可能会一直取不到,有些实例被取到多次
# 所以尽可能先将数据集混洗一次
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
看到之后,是不是读者有了更加清晰的了解了。 当然我们也可以使用Scikit-learn提供的SGDRegressor()实现随机梯度下降的线性回归:
from sklearn.linear_model import SGDRegressor
sgd_reg = SGDRegressor(max_iter=1000,tol=1e-3,penalty=None, eta0=0.1)
# 注意这里需要通过ravel函数将y转成一维数组
sgd_reg.fit(X, y.ravel())
SGDRegressor的超参数含义:
max_iter迭代次数tol当theta低于设置的该值时,也可以停止训练penalty是否设置正则eta0开始的学习率
小批量梯度下降
我们发现了批量梯度下降和随机梯度下降的都有一丢丢的小问题,但是也有不少优点。我们可以集合他们的优点,削弱点他们的问题。小批量梯度下降就是不需要全部的训练集计算,也不仅仅基于某一个实例。这样的算法更加的稳定:
# 迭代次数
n_iterations = 50
# 批量大小,也就是每次取多小个实例
minibatch_size = 20
np.random.seed(42)
theta = np.random.randn(2,1) # random initialization
# 学习率调度
t0, t1 = 200, 1000
def learning_schedule(t):
return t0 / (t + t1)
t = 0
# 循环迭代
for epoch in range(n_iterations):
# 每次迭代,代将m个实例打乱,避免无法取到某些实例
shuffled_indices = np.random.permutation(m)
X_b_shuffled = X_b[shuffled_indices]
y_shuffled = y[shuffled_indices]
# 根据批量大小计算每次theta
for i in range(0, m, minibatch_size):
t += 1
# 从混洗的数据集中取值
xi = X_b_shuffled[i:i+minibatch_size]
yi = y_shuffled[i:i+minibatch_size]
gradients = 2/minibatch_size * xi.T.dot(xi.dot(theta) - yi)
# 学习度下降
eta = learning_schedule(t)
theta = theta - eta * gradients
最后我放一张图看看三个梯度下降的训练过程:
图6 训练过程
这下我们能够更加清楚的明白:
批量梯度下降很稳,就是看上去慢慢悠悠的随机梯度下降很快,但是就是有点调皮,一直在最优解附近乱逛小批量梯度下降结合了两者优点,弱化缺点