0x00 前言
在之前介绍的梯度下降法的步骤中,在每次更新参数时是需要计算所有样本的,通过对整个数据集的所有样本的计算来求解梯度的方向。这种计算方法被称为:批量梯度下降法BGD(Batch Gradient Descent)。但是这种方法在数据量很大时需要计算很久。
针对该缺点,有一种更好的方法:随机梯度下降法SGD(stochastic gradient descent),随机梯度下降是每次迭代使用一个样本来对参数进行更新。虽然不是每次迭代得到的损失函数都向着全局最优方向,但是大的整体的方向是向全局最优解的,最终的结果往往是在全局最优解附近。但是相比于批量梯度,这样的方法更快,我们也是可以接受的。下面就来学学随机梯度下降法吧!
0x01 理解随机梯度下降
1.1 随机取值的公式推导
批量梯度下降法,每一次计算过程,都要将样本中所有信息进行批量计算。但是显然如果样本量很大的话,计算梯度就比较耗时。基于这个问题,改进的方案就是随机梯度下降法。即每次迭代随机选取一个样本来对参数进行更新。使得训练速度加快。
下面我们从数学的角度来看:我们将原本的矩阵中根据样本数量求和这一操作去掉,同样也就不需要除以m了。
最终我们得到损失函数的进行求导,得到的结果为:
要注意,得到的向量是搜索方向,不是梯度方向,因此已经不是算是函数的梯度了。
1.2 随机下降与学习率的取值
其过程就是:每次随机取出一个i,得到一个向量,沿着这个随机产生的向量的方向进行搜索,不停的迭代,得到的损失函数的最小值。
随机梯度下降法的搜索过程如下图所示。如果是批量搜索,那么每次都是沿着一个方向前进,但是随机梯度下降法由于不能保证随机选择的方向是损失函数减小的方向,更不能保证一定是减小速度最快的方向,所以搜索路径就会呈现下图的态势。即随机梯度下降有着不可预知性。
但实验结论告诉我们,通过随机梯度下降法,依然能够达到最小值的附近(用精度换速度)。
随机梯度下降法的过程中,学习率的取值很重要,这是因为如果学习率一直取一个固定值,所以可能会导致点已经取到最小值附近了,但是固定的步长导致点的取值又跳去了这个点的范围。因此我们希望在随机梯度下降法中,学习率是逐渐递减的。
设计一个函数,使学习率随着下降循环次数的增加而减小。
我们想到最简单的表示方法是一个倒数的形式,不过这样也会有问题,如果循环次数比较小的时候,学习率下降的太快了,比如循环次数为1变为2,则学习率减少50%。因此我们可以将分子变为常数,并在分母上增加一个常数项来来缓解初始情况下,学习率变化太大的情况,且更灵活。
0x02 BGD&SGD效果比较
下面可以我们要对比一下,批量梯度下降算法和随机梯度下降算法的比较。我们主要观察运行时间和迭代次数这两个指标。
2.1 批量梯度下降法代码
import numpy as npimport matplotlib.pyplot as pltm = 100000x = np.random.normal(size=m)X = x.reshape(-1,1)y = 4. * x + 3. + np.random.normal(0, 3, size=m)def J(theta, X_b, y): try: return np.sum((y-X_b.dot(theta)) ** 2) / len(y) except: return float('inf')def dJ(theta, X_b, y): return X_b.T.dot(X_b.dot(theta) - y) * 2. / len(y)def gradient_descent(X_b, y, initial_theta, eta, n_iters=1e4, epsilon=1e-6): theta = initial_theta cur_iter = 0 while(cur_iter < n_iters): gradient = dJ(theta, X_b, y) last_theta = theta theta = theta - eta * gradient if (abs(J(theta, X_b, y) - J(last_theta, X_b, y)) < epsilon): break cur_iter += 1 return theta%%timeX_b = np.hstack([np.ones((len(X),1)),X])initial_theta = np.zeros(X_b.shape[1])eta = 0.01theta = gradient_descent(X_b, y, initial_theta, eta)theta"""CPU times: user 2.3 s, sys: 493 ms, total: 2.79 sWall time: 1.73 sarray([3.00328612, 4.00425536])"""
2.2 随机梯度下降
首先,计算损失函数的求导结果时,传递的不是整个矩阵和整个向量,而是其中一行X_b_i、其中的一个数值,因此表达式为X_b_i.T.dot(X_b_i.dot(theta) - y_i) * 2.
然后我们可以内置一个计算学习率的函数,即使用来缓解初始情况下,学习率变化太大的情况。
最后就是终止循环条件的差异了。在批量梯度下降的计算过程中,循环终止的条件有两个,第一个是循环次数达到上限;第二个是找到的损失函数减小的值不能在减小预设精度这么多了。对于批量梯度下降来说,循环终止的条件有两个,第一个是循环次数达到上限,第二个是找到的损失函数减小的值不能在减小预设精度这么多了
那么在随机梯度下降中,由于梯度下降的方向是随机的,所以损失函数不能保证一直减小,有可能是跳跃的,因此就不要第二个条件了,直接使用for循环判断迭代次数就好了。
# 传递的不是整个矩阵X_b,而是其中一行X_b_i;传递y其中的一个数值y_idef dJ_sgd(theta, X_b_i, y_i): return X_b_i.T.dot(X_b_i.dot(theta) - y_i) * 2.def sgd(X_b, y, initial_theta, n_iters): t0 = 5 t1 = 50 # def learning_rate(cur_iter): return t0 / (cur_iter + t1) theta = initial_theta for cur_iter in range(n_iters): # 随机找到一个样本(得到其索引) rand_i = np.random.randint(len(X_b)) gradient = dJ_sgd(theta, X_b[rand_i], y[rand_i]) theta = theta - learning_rate(cur_iter) * gradient return theta%%timeX_b = np.hstack([np.ones((len(X),1)),X])initial_theta = np.zeros(X_b.shape[1])theta = sgd(X_b, y, initial_theta, n_iters=len(X_b)//3)print(theta)"""输出:[2.9287233 4.05019715]CPU times: user 296 ms, sys: 6.49 ms, total: 302 msWall time: 300 ms"""
2.3 分析
通过简单的例子,我们可以看出,在批量梯度下降的过程中消耗时间1.73 s,而在随机梯度下降中,只使用300 ms。并且随机梯度下降中只考虑的三分一的样本量,且得到的结果一定达到了局部最小值的范围内。
0x03 代码实现
3.1 改进后的代码
在上一小节,为了比较批量梯度下降和随机梯度下降,我们进行了简单的实现,但实际上对于随机梯度下降法还是有一些问题的。
在之前,只是简单的只考虑的三分一的样本量,实际上我们已经考虑所有的样本量n次,然后在每次考虑样本量时采用随机的方式。
我们首先要了解:表示对所有样本的循环次数,所有样本的个数为m。因此我们在循环时,应该使用双重循环,即外层循环为每次循环所有样本,内层循环为在所有样本中进行随机选择,次数为样本数量。那么要注意:在计算学习率时,次数就变为
def fit_sgd(self, X_train, y_train, n_iters=50, t0=5, t1=50): """根据训练数据集X_train, y_train, 使用梯度下降法训练Linear Regression模型""" assert X_train.shape[0] == y_train.shape[0], \ "the size of X_train must be equal to the size of y_train" assert n_iters >= 1 def dJ_sgd(theta, X_b_i, y_i): return X_b_i * (X_b_i.dot(theta) - y_i) * 2. def sgd(X_b, y, initial_theta, n_iters=5, t0=5, t1=50): def learning_rate(t): return t0 / (t + t1) theta = initial_theta m = len(X_b) for i_iter in range(n_iters): # 将原本的数据随机打乱,然后再按顺序取值就相当于随机取值 indexes = np.random.permutation(m) X_b_new = X_b[indexes,:] y_new = y[indexes] for i in range(m): gradient = dJ_sgd(theta, X_b_new[i], y_new[i]) theta = theta - learning_rate(i_iter * m + i) * gradient return theta X_b = np.hstack([np.ones((len(X_train), 1)), X_train]) initial_theta = np.random.randn(X_b.shape[1]) self._theta = sgd(X_b, y_train, initial_theta, n_iters, t0, t1) self.intercept_ = self._theta[0] self.coef_ = self._theta[1:] return self
3.2 使用自己的SGD
import numpy as npfrom sklearn import datasetsboston = datasets.load_boston()X = boston.datay = boston.targetX = X[y < 50.0]y = y[y < 50.0]from myAlgorithm.LinearRegression import LinearRegressionfrom myAlgorithm.model_selection import train_test_splitX_train, X_test, y_train, y_test = train_test_split(X, y, seed=666)standardScaler = StandardScaler()standardScaler.fit(X_train)X_train_std = standardScaler.transform(X_train)X_test_std = standardScaler.transform(X_test)lin_reg1 = LinearRegression()lin_reg1.fit_sgd(X_train, y_train, n_iters=2)lin_reg1.score(X_test_std, y_test)"""输出:0.78651716204682975"""
通过增加的值,可以获得更好的结果:
lin_reg1.fit_sgd(X_train, y_train, n_iters=100)lin_reg1.score(X_test_std, y_test)"""输出:0.81294846132723497"""
3.3 sklearn中的SGD
from sklearn.linear_model import SGDRegressorsgd_reg = SGDRegressor() # 默认n_iter=5%time sgd_reg.fit(X_train_std, y_train)sgd_reg.score(X_test_std, y_test)
速度非常快!增加迭代次数,可以提升效果
sgd_reg = SGDRegressor(n_iter=100)%time sgd_reg.fit(X_train_std, y_train)sgd_reg.score(X_test_std, y_test)
sklearn中的算法实现,其实和我们的实现有很大的区别,进行了很多的优化。我们只是简单的实现了它的原理,感兴趣的同学可以去康康相关源代码。
0xFF总结
批量梯度下降法BGD(Batch Gradient Descent)。
- 优点:全局最优解;易于并行实现;
- 缺点:当样本数据很多时,计算量开销大,计算速度慢。
针对于上述缺点,其实有一种更好的方法:随机梯度下降法SGD(stochastic gradient descent),随机梯度下降是每次迭代使用一个样本来对参数进行更新。
- 优点:计算速度快;
- 缺点:收敛性能不好