机器学习笔记(十二):随机梯度下降

    技术2024-12-16  16

    凌云时刻 · 技术

    导读:这篇笔记主要介绍梯度下降法,梯度下降不是机器学习专属的算法,它是一种基于搜索的最优化方法,也就是通过不断的搜索然后找到损失函数的最小值。像上篇笔记中使用正规方程解实现多元线性回归,基于   这个模型我们可以推导出   的数学解,但是很多模型是推导不出数学解的,所以就需要梯度下降法来搜索出最优解。

    作者 | 计缘

    来源 | 凌云时刻(微信号:linuxpk)

    随机梯度下降法

    在实际的运用中,训练数据的量级往往都很大,我们目前实现梯度下降法是需要让每一个样本数据都参与梯度计算的,称为批量梯度下降,所以当样本数据量很大的时候,计算梯度的速度就会很慢,所以这一节我们来看看改进这个情况的随机梯度下降法。

    以一个山谷的俯视示意图为例,先来看看批量梯度下降法对   值的查找轨迹:

    可以看到呈现出的轨迹是平滑下降的,每一个   值都比上一个   值小,最终找到使损失函数达到极小值的   。这是因为每一个   都会对所有样本进行了计算,为了增加拟合效率,对每一个   ,我们不对所有样本进行计算,而是每次只取一个样本进行计算,取多次来找到最终的   值,这就是随机梯度下降的基本思想。

    那么我们再来看看随机梯度下降法的   查找轨迹:

    因为每次只随机取一个样本进行计算,所以可以看到随机梯度下降法查找θθ的轨迹是也是随机的,很多时候搜索下一个θθ的路径并不是最短路径,并且下一个θθ的损失函数值比上一个θθ的损失函数值还要大,但是最终依然会找到使损失函数达到极小值的θθ。

    我们来看看梯度公式:

       

    按照随机梯度下降的思路,每次只取一个样本进行计算,也就是i每次只取一个值:

       

    向量化后可得:

       

    注意上面这个公式并不是梯度公式,而是搜‍‍索   的‍‍某一个方向,批量梯度下降是不放过任何一个搜‍‍索   的‍‍方向,而随机梯度下降是每次选择一个方向进行搜索。另外需要注意的是随机梯度下降法中的步长(学习率)并不是固定不变的,不然就无法找补随机路径的不足了,所以随机梯度下降法中的步长是根据搜索次数的增加而降低的,也就是逐渐递减的。那么学习率的公式我们首先想到的就是取搜索次数的倒数:

     

    但是如果搜索次数很小或者很大的时候都会出现问题,前者会导致学习率下降的幅度过大,而后者会导致学习率下降的幅度过小,所以我们将分母加上一个常数,分子也用一个常数,这样就能保证学习率的递减幅度在一个较为平滑的状态:

       

    a和b其实就是随机梯度下降法的两个超参数了,不过这里我们不对这两个超参数进行搜索,我们使用最佳实践就好,一般取a为5,b为50。

    另外一个关键的超参数是搜索次数,假设取样本数据量的1/3作为搜索次数,因为每次是随机取一个样本数据,那势必会有一部分样本数据取不到,从而不会计算,并且取到的样本数据里也会有重复的数据,这样虽然收敛时间减少了,但是准确率却会打折扣,那么为了训练数据的准确性,我们希望每次搜索到的样本数据不重复,并且能所有的样本数据都希望在某一方向进行计算,所以将搜索次数的概念转变一下,既为随机搜索样本数据,且不重复,且所有样本数据都被搜索到一遍的轮数。这样一来,这个轮数可以很小,一般搜索4轮5轮既可。

     实现随机梯度下降法

    我们先模拟10万条样本数据,然后用批量梯度下降法训练一次,看看拟合时间:

    import numpy as np # 模拟10万条样本数据 m = 100000 # 随机生成10万个数 x = np.random.random(size=m) # 转换成10万行1列的矩阵 X = x.reshape(-1, 1) # 拟定一个线性方程,计算y值,系数为4,截距为3,并且加上一个随机噪音值 y = x * 4 + 3 + np.random.normal(0, 3, size=m) # 先使用批量梯度下降法 from myML.LinearRegression import LinearRegression lr = LinearRegression() %%time lr.fit_gd(X, y) # 结果 CPU times: user 6.59 s, sys: 496 ms, total: 7.08 s Wall time: 5.19 s # 截距 lr.intercept_ # 结果 2.9982721096748928 # 系数 lr.coef_ # 结果 array([ 3.99485476])

    我们看到使用批量梯度下降法计算出的截距和系数和拟定的线性方程中的截距和系数是差不多的,并且拟合时间用了5.19秒。

    下面我们直接在PyCharm中封装随机梯度下降的方法,在LinearRegression类中增加fit_sgd方法:

     

    # 使用随机梯度下降法,根据训练数据集X_train,y_train训练LinearRegression模型 def fit_sgd(self, X_train, y_train, n_iters=5, a=5, b=50): assert X_train.shape[0] == y_train.shape[0], \ "特征数据矩阵的行数要等于样本结果数据的行数" assert n_iters >= 1, \ "至少要搜索一轮" # 定义theta查找方向的函数,这里不是全量的X_b矩阵了,而是X_b矩阵中的一行数据, # 既其中的的一个样本数据,对应的y值也只有一个 def dL_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): # 定义学习率公式 def eta(iters): return a / (iters + b) theta = initial_theta # 样本数量 m = len(X_b) # 第一层循环是循环轮数 for i_inter in range(n_iters): # 在每一轮,随机生成一个乱序数组,个数为m indexs = np.random.permutation(m) # 打乱样本数据 X_b_new = X_b[indexs] y_new = y[indexs] # 第二层循环便利所有为乱序的样本数据,既保证样本数据能被随机的,全部的计算到 for i in range(m): # 每次用一个随机样本数据计算theta搜索方向 gradient = dL_sgd(theta, X_b_new[i], y_new[i]) # 计算下一个theta theta = theta - eta(i_inter * m + i) * gradient return theta # 构建X_b X_b = np.hstack([np.ones((len(X_train), 1)), X_train]) # 初始化theta向量为元素全为0的向量 initial_theta = np.zeros(X_b.shape[1]) self._theta = sgd(X_b, y_train, initial_theta, n_iters) self.intercept_ = self._theta[0] self.coef_ = self._theta[1:]

    然后在Jupyter Notebook中使用封装好的随机梯度下降法来训练刚才的数据:

     

    lr1 = LinearRegression() # 这里我们使用默认的5次搜索轮数,轮数越少,拟合时间越短 %%time lr1.fit_sgd(X, y) # 结果 CPU times: user 967 ms, sys: 6.31 ms, total: 973 ms Wall time: 971 ms # 截距 lr1.intercept_ # 结果 2.9746308939605761 # 系数 lr1.coef_ # 结果 array([ 3.96594805])

    可以看到最终的结果和批量梯度下降法训练出的是差不多的,但是拟合时间只用了971毫秒。

    Scikit Learn中的随机梯度下降法

    这一节我们用真实的波士顿房价数据,使用Scikit Learn中的随机梯度下降进行训练看看:

    import numpy as np from sklearn import datasets boston = datasets.load_boston() X = boston.data y = boston.target X = X[y < 50.0] y = y[y < 50.0] from myML.modelSelection import train_test_split X_train, y_train, X_test, y_test = train_test_split(X, y, seed=123) from sklearn.preprocessing import StandardScaler standard_scalar = StandardScaler() standard_scalar.fit(X_train) X_train_standard = standard_scalar.transform(X_train) X_test_standard = standard_scalar.transform(X_test) from sklearn.linear_model import SGDRegressor sgd_reg = SGDRegressor(n_iter=150) %time sgd_reg.fit(X_train_standard, y_train) sgd_reg.score(X_test_standard, y_test) # 结果 CPU times: user 8.59 ms, sys: 2.26 ms, total: 10.8 ms Wall time: 8.06 ms 0.80003192122081712

    再来看看使用我们自己封装的随机梯度下降训练波士顿房价数据:

     

    from myML.LinearRegression import LinearRegression lr = LinearRegression() %%time lr.fit_gd(X_train_standard, y_train, n_iters=150) # 结果 CPU times: user 256 ms, sys: 3.43 ms, total: 259 ms Wall time: 257 ms lr1.score(X_test_standard, y_test) # 结果 0.80028998868733348

    可以看到在同样的搜索轮数,相同的评分值情况下,Scikit Learn中的随机梯度下降法的收敛时间远远小于我们自己实现的随机梯度下降法,这是因为Scikit Learn中实现的时候使用了大量优化的算法,我们只是使用了最核心的思想进行封装,这点需要大家知晓。

    关于梯度的调试

     

    从字面意思就不难看出梯度下降法中梯度的重要性,在线性回归问题中,我们尚且能推导出梯度的公式,但在一些复杂的情况下,推导梯度的公式非常不容易,所以我们推导出的梯度公式是否合理,是否是正确的,就需要有一个方法来验证。这一小节就给大家介绍一个梯度调试的方法。

    拿一维场景来说,A点的梯度也就是导数是它切线M的斜率。然后我们在直线M的右侧再画出一条平行与直线M的直线N,直线N的斜率与直线M的斜率近乎相等,此时直线N与曲线有两个相交点B和点C,这两个点分别在A点的负方向,也就是下上方,和在A点的正方向,也就是在下方,此时直线M也称为曲线的割线:

    ‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

    在第一篇笔记中讲过直线的斜率定义,既对于两个已知点‍‍‍‍‍‍‍‍   和   ‍‍‍‍‍‍‍‍‍‍,如果‍‍   不等   ,‍‍则经过这两点直线的斜率为   。我们假设点B和点C相距点A为   ,所以直线N的斜率为:‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍‍

       

    通过这个公式就可以模拟计算出某一点的导数。对于高维的梯度也是同样的道理。

       

       

    ‍‍‍‍‍‍‍‍‍‍高维情况中,   和梯度如上所示,如果我们要模拟计算‍‍   的‍‍梯度,首先得‍‍出   上方‍‍和下方的   :‍‍‍‍‍‍‍‍‍‍

       

    ‍‍

     

    那么模拟出的   导数为:   

    以此类推,可以模拟计算出所有   的导数,从而模拟计算出梯度。虽然该方法与损失函数形态无关,但是对每个   导数的模拟计算都需要将两组   向量代入损失函数进行计算,时间复杂度还是非常高的,但是有一个优势是可以忽略损失函数形态。所以我们一般用这种方式在一开始投入一些时间算出梯度,因为这个梯度是基于斜率的数学定理计算出的,所以它可以认为是一个合理的梯度,然后将它作为标准来验证通过梯度下降法计算出的梯度的正确性。

     实现梯度的调试方法

    我们在PyCharm中对我们封装好的梯度下降做一下改动,首先在LinearRegression类中增加一个名为dL_debug()的方法:

    def dL_debug(theta, X_b, y, epsilon=0.01): # 开辟大小与theta向量一致的向量空间 result = np.empty(len(theta)) # 便利theta向量中的每一个theta for i in range(len(theta)): # 复制一份theta向量 theta_1 = theta.copy() # 将第i个theta加上一个距离,既求该theta正方向的theta theta_1[i] += epsilon # 在复制一份theta向量 theta_2 = theta.copy() # 将第i个theta减去同样的距离,既求该theta负方向的theta theta_2[i] -= epsilon # 求出这两个点连线的斜率,既模拟该theta的导数 result[i] = (L(theta_1, X_b, y) - L(theta_2, X_b, y)) / (2 * epsilon) return result

    然后对fit_gd()方法增加一个参数is_debug,默认值为False,然后对gradient_descent()进行修改,让其当is_debug为True时走Debug的求梯度的方法,反之走梯度公式的方法:

      

    # 实现批量梯度下降法 def gradient_descent(X_b, y, initial_theta, eta, difference=1e-8): theta = initial_theta i_iter = 0 while i_iter < n_iters: # 当is_debug为True时走debug的求梯度的方法,反之走梯度公式的方法 if is_debug: gradient = dL_debug(theta, X_b, y) else: gradient = dL(theta, X_b, y) last_theta = theta theta = theta - eta * gradient if (abs(L(theta, X_b, y) - L(last_theta, X_b, y)) < difference): break i_iter += 1 return theta

    下面我们使用波士顿房价的数据验证一下:

     

    import numpy as np from sklearn import datasets boston = datasets.load_boston() X = boston.data y = boston.target X = X[y < 50.0] y = y[y < 50.0] from myML.modelSelection import train_test_split X_train, y_train, X_test, y_test = train_test_split(X, y, seed=123) from sklearn.preprocessing import StandardScaler standard_scalar = StandardScaler() standard_scalar.fit(X_train) X_train_standard = standard_scalar.transform(X_train) X_test_standard = standard_scalar.transform(X_test) from myML.LinearRegression import LinearRegression lr = LinearRegression() # 使用debug方式训练 %time lr.fit_gd(X_train_standard, y_train, is_debug=True) # 结果 CPU times: user 2.55 s, sys: 11.4 ms, total: 2.57 s Wall time: 2.57 s lr.intercept_ # 结果 21.629336734693936 lr.coef_ # 结果 array([-0.9525182 , 0.55252408, -0.30736822, -0.03926274, -1.37014814, 2.61387294, -0.82461734, -2.36441751, 2.02340617, -2.17890468, -1.76883751, 0.7438223 , -2.25694241]) # 用梯度公式方式训练 % time lr.fit_gd(X_train_standard, y_train) # 结果 CPU times: user 241 ms, sys: 4.12 ms, total: 245 ms Wall time: 242 ms lr.intercept_ # 结果 21.629336734693936 lr.coef_ # 结果 array([-0.9525182 , 0.55252408, -0.30736822, -0.03926274, -1.37014814, 2.61387294, -0.82461734, -2.36441751, 2.02340617, -2.17890468, -1.76883751, 0.7438223 , -2.25694241])

    可以看到我们使用Debug方式训练数据时用了2.57秒,不过求了准确合理的梯度。然后用梯度公式法训练了数据,耗时242毫米,但结果相同,说明梯度计算的没有问题。

      

     

    END

    往期精彩文章回顾

    机器学习笔记(十一):优化梯度公式

    机器学习笔记(十):梯度下降

    机器学习笔记(九):多元线性回归

    机器学习笔记(八):线性回归算法的评测标准

    机器学习笔记(七):线性回归

    机器学习笔记(六):数据归一化

    机器学习笔记(五):超参数

    机器学习笔记(四):kNN算法

    机器学习笔记(三):NumPy、Matplotlib、kNN算法

    机器学习笔记(二):矩阵、环境搭建、NumPy

    长按扫描二维码关注凌云时刻

    每日收获前沿技术与科技洞见

    Processed: 0.018, SQL: 9