機器學習-梯度下降法的詳細推導與代碼實現

計算

對於線性迴歸,梯度下降法的目標就是找到一個足夠好的向量θ\theta,使代價函數J(θ)=i=1m(y^yi)2J(\theta) = \sum_{i=1}^{m}(\hat{y}-y_{i})^{2}取得最小值。線性迴歸的代價函數是關於θ\theta的多元函數。如下:
J(θ)=i=1m(y^yi)2=i=1m(θx(i)yi)2 J(\theta) = \sum_{i=1}^{m}(\hat{y}-y^{i})^{2} = \sum_{i=1}^{m}(\theta x^{(i)}-y^{i})^{2}
J(θ)=i=1m(yiθ0θ1x1(i)θ2x2(i)...θnxn(i))2 J(\theta) = \sum_{i=1}^{m}(y^{i} - \theta_{0} - \theta_{1}x^{(i)}_1 - \theta_{2}x^{(i)}_2 - ... - \theta_{n}x^{(i)}_n )^{2}

對代價函數J(θ)J(\theta)的每一個θ\theta分量求偏導數,
J(θ)θi=[J(θ)θ1J(θ)θ2J(θ)θn]=2[i=1m(y(i)Xb(i)θ)(1)i=1m(y(i)Xb(i)θ)(X1(i))i=1m(y(i)Xb(i)θ)(Xn(i))]=2[i=1m(Xb(i)θy(i))i=1m(Xb(i)θy(i))(X1(i))i=1m(Xb(i)θy(i))(Xn(i))] \frac{\partial J(\theta)}{\partial \theta_i}= \begin{bmatrix} \frac{\partial J(\theta)}{\partial \theta_1} \\ \frac{\partial J(\theta)}{\partial \theta_2} \\ \vdots \\ \frac{\partial J(\theta)}{\partial \theta_n} \end{bmatrix} = 2 \begin{bmatrix} \sum_{i=1}^{m}(y^{(i)} - X^{(i)}_b\theta) (-1) \\ \sum_{i=1}^{m}(y^{(i)} - X^{(i)}_b\theta) (-X^{(i)}_1) \\ \vdots \\ \sum_{i=1}^{m}(y^{(i)} - X^{(i)}_b\theta) (-X^{(i)}_n) \end{bmatrix} = 2 \begin{bmatrix} \sum_{i=1}^{m}(X^{(i)}_b\theta -y^{(i)}) \\ \sum_{i=1}^{m}(X^{(i)}_b\theta -y^{(i)}) (X^{(i)}_1) \\ \vdots \\ \sum_{i=1}^{m}(X^{(i)}_b\theta - y^{(i)}) (X^{(i)}_n) \end{bmatrix}
這裏將J()除以m可以縮小梯度的值:
J(θ)=1mi=1m(y^yi)2 J(\theta) = \frac{1}{m}\sum_{i=1}^{m}(\hat{y}-y^{i})^{2}
此時梯度爲:
J(θ)θi=2m[i=1m(Xb(i)θy(i))i=1m(Xb(i)θy(i))(X1(i))i=1m(Xb(i)θy(i))(Xn(i))] \frac{\partial J(\theta)}{\partial \theta_i}= \frac{2}{m} \begin{bmatrix} \sum_{i=1}^{m}(X^{(i)}_b\theta -y^{(i)}) \\ \sum_{i=1}^{m}(X^{(i)}_b\theta -y^{(i)}) (X^{(i)}_1) \\ \vdots \\ \sum_{i=1}^{m}(X^{(i)}_b\theta - y^{(i)}) (X^{(i)}_n) \end{bmatrix}

  • 此處也可以除以2m,因爲可以方便求導數,不過,對結果沒有什麼影響,可以隨意
  • 這裏的J(θ)J(\theta)分爲m個部分,因爲有m個點。計算梯度的時候需要把m個點的座標代進去關於θ\theta的表達式。這是梯度的標準求法。這個公式,是完整的梯度公式。即,在每個θ\theta方向求導數。得到的梯度向量就是下降最快的方向。
  • 隨機梯度下降法就是簡化了這個J(θ)J(\theta),不需要再把m個點都代入進去,而是隨機地抽取一個點,所以計算速度大爲提高。
  • 小批量梯度下降法就是中和了這兩個算法,每次取batch個點代進去,計算梯度。BGD的梯度最後除以了m,這裏的MBGD也可以除以batch。其實除和不除,都能計算出來,因爲除的這個batch,在迭代的時候,會和α\alpha相乘,只不過會對α\alpha的取值有所影響。

以下是求梯度的函數,很簡單,但是我總是記不住,這裏作詳細的介紹:

        def dJ(theta, X_b, y):
            res = np.empty(len(theta))
            res[0] = np.sum(X_b.dot(theta) - y)
            for i in range(1, len(theta)):
                res[i] = (X_b.dot(theta) - y).dot(X_b[:, i])
            return res * 2 / len(X_b)

首先上面矩陣中的每個元素其實也需要通過矩陣運算得出
i=1m(Xb(i)θy(i))(X1(i))=(X(1)θy(1))X1(1)+(X(2)θy(2))X1(2)+...+(X(m)θy(m))X1(m) \sum_{i=1}^{m}(X^{(i)}_b\theta -y^{(i)}) (X^{(i)}_1) = (X^{(1)}\theta - y^{(1)})X^{(1)}_1 + (X^{(2)}\theta - y^{(2)})X^{(2)}_1 + ... + (X^{(m)}\theta - y^{(m)})X^{(m)}_1
上面的式子可以用用兩個向量的內積表示爲,此處是內積,不是矩陣乘法:
[θ0+X1(1)θ1+X2(1)θ2+Xn(1)θn...y(1)θ0+X1(2)θ1+X2(2)θ2+Xn(2)θn...y(2)θ0+X1(m)θ1+X2(m)θ2+Xn(m)θn...y(m)][X1(1)X1(2)X1(m)] \begin{bmatrix} \theta_0 + X^{(1)}_1\theta_1 + X^{(1)}_2\theta_2 + X^{(1)}_n\theta_n ... - y^{(1)} \\ \theta_0 + X^{(2)}_1\theta_1 + X^{(2)}_2\theta_2 + X^{(2)}_n\theta_n ... - y^{(2)} \\ \vdots \\ \theta_0 + X^{(m)}_1\theta_1 + X^{(m)}_2\theta_2 + X^{(m)}_n\theta_n ... - y^{(m)} \end{bmatrix} \cdot \begin{bmatrix} X^{(1)}_1 \\ X^{(2)}_1 \\ \vdots \\ X^{(m)}_1 \end{bmatrix}
上面的式子又等價於:
([1X1(1)X2(1)Xn(1)1X1(2)X2(2)Xn(2)1X1(m)X2(m)Xn(m)][θ0θ1θn][y(1)y(2)y(m)])[X1(1)X1(2)X1(m)] (\begin{bmatrix} 1 & X^{(1)}_{1} & X^{(1)}_{2} & \cdots & X^{(1)}_{n} \\ 1 & X^{(2)}_{1} & X^{(2)}_{2} & \cdots & X^{(2)}_{n} \\ \vdots & \vdots & \ddots & \vdots \\ 1 & X^{(m)}_{1} & X^{(m)}_{2} & \cdots & X^{(m)}_{n} \end{bmatrix} \cdot \begin{bmatrix} \theta_0 \\ \theta_1 \\ \vdots \\ \theta_n \end{bmatrix} - \begin{bmatrix} y^{(1)} \\ y^{(2)} \\ \vdots \\ y^{(m)} \end{bmatrix})\cdot \begin{bmatrix} X^{(1)}_1 \\ X^{(2)}_1 \\ \vdots \\ X^{(m)}_1 \end{bmatrix}

最終可以表示爲,X[:,i]X[:,i]是python中的切片的寫法:
(Xbθy)X[:,i] (X_b \theta - y)* X[:,i]

因此,除了第一行以外,每一行的偏導數都可以表示爲 (Xbθy)X[:,i](X_b \theta - y)* X[:,i],上面的代碼中的循環,就是對除了第一行以外的所有行,每一行都進行一次計算。這裏dot()是點乘,向量點乘之後就自動求和了。所以不需要再次求和。

            for i in range(1, len(theta)):
                res[i] = (X_b.dot(theta) - y).dot(X_b[:, i])

所以其實是兩種情況:

(Xbθy)(X_b \theta - y) 第一行
(Xbθy)X[:,i](X_b \theta - y)* X[:,i] 之後的所有行

實現

思路:先準備數據集X和y ==> 使用交叉驗證選出訓練樣本 ==> 迴歸
迴歸的過程: 將轉換爲X_b(就是加上X0) ==> 設置初始化 θ\theta ==> 進行梯度下降(計算各個方向的偏導數-> 循環計算theta = theta - eta * dj)

class LinearRegression2:
    def __init__(self):
        self.coef_ = None        # 表示參數,theta_[1:]
        self.intercept_ = None   # 表示截距 ==>theta[0]
        self._thera = None       # 表示完整的theta==> theta[:]

    def fit_gd(self, X_train, y_train, eta=0.01, n_iters=1e4):
        """使用梯度下降法尋找最小的代價函數"""
        # 格式化X和theta,加上x0 和 theta0
        X_b = np.hstack([np.ones((len(X_train), 1)), X_train])
        initial_theta = np.zeros(X_b.shape[1])

        # 調用循環的梯度下降
        self._thera = self.gradient_descent(X_b, y_train, initial_theta, eta=eta, n_iters=n_iters)
        self.intercept_ = self._thera[0]
        self.coef_ = self._thera[1:]
        return self

    def gradient_descent(self, X_b, y, initial_theta, eta, n_iters=1e4, epsilon=1e-8):
        theta = initial_theta

        i = 0
        while i < n_iters:
            i += 1
            lastTheta = theta       # 記錄上一個參數向量
            dj = self.dj(theta, X_b, y)
            theta = theta - eta * dj
            if np.absolute(self.J(lastTheta, X_b, y) - self.J(theta, X_b, y)) < epsilon:
                break
        return theta

    def dj(self, theta, X_b, y):
        """計算代價函數的偏導數"""
        # res 是 長度爲len的一維數組
        res = np.zeros((len(theta)))        ##### zeros(5) == zeros((5,)) != zeros((5,1))
        res[0] = np.sum(X_b.dot(theta) - y)     # 需要將向量求和。沒有辦法,這裏只能手動分開求解。因爲這裏X是從列方向分割,沒有X0
        for i in range(1, len(theta)):
            res[i] = (X_b.dot(theta) - y).dot(X_b[:, i])
        return res * 2 / len(X_b)

    
    def J(self, theta, X_b, y):
        """計算代價函數"""
        return np.sum((y - X_b.dot(theta)) ** 2) / len(y)

調用效果如下:

    np.random.seed(666)
    x = 2 * np.random.random(size=100)
    y = x * 3. + 4. + np.random.normal(size=100)
    X = x.reshape(-1, 1)
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_ratio=0.2, seed=666)

    line = LinearRegression2()
    line.fit_gd(X_train, y_train)

    ###
    4.085675667692203
[   2.97732994]

使用向量實現

博客沒法顯示全公式。。就用圖片湊合吧.XbθyX_b\theta -y是列向量

即,最後的公式爲:
J(θ)=2mXbT(Xbθy) J(\theta) = \frac{2}{m}X^T_b \cdot (X_b\theta-y)
使用python的numpy實現:

        def dJ(theta, X_b, y):
            return X_b.T.dot(X_b.dot(theta) - y) * 2. / len(y)

其中,XbX_b是在原始數據的每一列前面加了一列後的矩陣,y是訓練數據中給定的y_trainy\_train

小結

  • 不管是用向量計算還是用手動計算。都需要先把訓練數據XX轉換爲XbX_b
  • 一般來說,XX都是ndarray類型的二維數組,使用hstack([ist1, list2])函數可以將兩個數組攤在一起。將XX轉換爲XbX_b的代碼爲:

    X_b = np.hstack([np.ones((len(X_train), 1)), X_train])

  • 大體思路就是:
    1. 調用fit()方法擬合,該方法中產生XbX_b,並初始化θ\theta。最後使調用梯度下降方法gradient_descent()來找到最優的θ\theta
    2. gradient_descent() 循環調用dj()計算對θ\theta的偏導數,並每一次都對θ\theta的值進行更新。
    3. gradient_descent() 中會調用J()來判斷梯度的增量是否已經足夠小。

數據標準化

在這裏使用波士頓房價數據測試,發現一些很有意思的東西,如下。爲什麼會這樣?,因爲數據沒有歸一化,在數據集中存在一下特別大和特別小的數字,導致在計算梯度時,可能會導致梯度的跨度太大,而無法收斂。也有可能時計算式出現了過大的數inf。

    linear.fit_gd(X, y, n_iters=1e4, eta=0.001)
    print(linear.coef_)

    ###
    [nan nan nan nan nan nan nan nan nan nan nan nan nan]

只要將學習係數設定的足夠小,就不會報錯。

linear.fit_gd(X, y, n_iters=1e4, eta=0.0000001)

使用sklearn的StandardScaler歸一化數據。注意歸一化需要將所有的X都歸一化,包括用來訓練的x_train和用來測試的x_test

    from sklearn.preprocessing import StandardScaler

    standardScaler = StandardScaler()
    standardScaler.fit(X)                       # 導入數據
    X_standard = standardScaler.transform(X)    # 轉換數據

所以,到目前爲止,要預測一個波士頓房價數據的完整過程是這樣的:

    from sklearn import datasets

    # 加載數據集
    X = datasets.load_boston().data
    y = datasets.load_boston().target

    # 剔除噪音
    X = X[y < 50]
    y = y[y < 50]

    # 數據歸一化處理
    from sklearn.preprocessing import StandardScaler

    standardScaler = StandardScaler()
    standardScaler.fit(X)
    X_standard = standardScaler.transform(X)

    # 交叉驗證
    X_train_standard, X_test_standard, y_train, y_test = train_test_split(X_standard, y, test_ratio=0.2, seed=666)

    # 迴歸,擬合
    linear = LinearRegression2()
    linear.fit_gd(X_train_standard, y_train, n_iters=3e5)

    # 使用score計算擬合的效果
    print(linear.score(X_test_standard, y_test))

隨機梯度下降法

原本的梯度下降法:
J(θ)θi=2m[i=1m(Xb(i)θy(i))i=1m(Xb(i)θy(i))(X1(i))i=1m(Xb(i)θy(i))(Xn(i))] \frac{\partial J(\theta)}{\partial \theta_i}= \frac{2}{m} \begin{bmatrix} \sum_{i=1}^{m}(X^{(i)}_b\theta -y^{(i)}) \\ \sum_{i=1}^{m}(X^{(i)}_b\theta -y^{(i)}) (X^{(i)}_1) \\ \vdots \\ \sum_{i=1}^{m}(X^{(i)}_b\theta - y^{(i)}) (X^{(i)}_n) \end{bmatrix}

隨機梯度下降法:
 2. ((Xb(i)θy(i))X0(i)(Xb(i)θy(i))X1(i)(Xb(i)θy(i))X2(i)(Xb(i)θy(i))Xn(i))=2(Xb(i))T(Xb(i)θy(i)) \text { 2. }\left(\begin{array}{c}{\left(X_{b}^{(i)} \theta-y^{(i)}\right) \cdot X_{0}^{(i)}} \\ {\left(X_{b}^{(i)} \theta-y^{(i)}\right) \cdot X_{1}^{(i)}} \\ {\left(X_{b}^{(i)} \theta-y^{(i)}\right) \cdot X_{2}^{(i)}} \\ {\cdots} \\ {\left(X_{b}^{(i)} \theta-y^{(i)}\right) \cdot X_{n}^{(i)}}\end{array}\right)=2 \cdot\left(X_{b}^{(i)}\right)^{T} \cdot\left(X_{b}^{(i)} \theta-y^{(i)}\right)

原本的計算偏導數的函數用這個替代。裏只有一個樣本哦,所有的誤差和X都是一個同一個樣本的。而原來的函數,每計算一次偏導數都要對n個特徵,每個循環m次,共mn次計算。計算量確實是大大提高,但是,不能保證每次都找到最好的梯度。使用隨機產生的梯度。相比較原來的函數,去掉了步長。
學習率:
η=t0iiters+t1 \eta=\frac{t_{0}}{i_{-} i t e r s+t_{1}}

  • 去掉了參數中的步長
  • n_iters 表示遍歷所有樣本的輪數
  • 學習率需要不斷縮小,因爲,算出來的梯度不能保證是越來越小的(因爲隨機)
class StochasticDescent:
    """隨機梯度下降法"""

    def __init__(self):
        self.coef_ = None       # 表示參數,theta_[1:]
        self.intercept_ = None  # 表示截距 ==>theta[0]
        self._thera = None      # 表示完整的theta==> theta[:]

    def fit_SGD(self, X_train, y_train, eta=0.01, n_iters=1):
        """使用梯度下降法尋找最小的代價函數"""
        # 格式化X和theta,加上x0 和 theta0
        X_b = np.hstack([np.ones((len(X_train), 1)), X_train])
        initial_theta = np.zeros(X_b.shape[1])

        # 調用循環的梯度下降
        self._thera = self.sgd(X_b, y_train, initial_theta, n_iters, 5, 50)
        self.intercept_ = self._thera[0]
        self.coef_ = self._thera[1:]
        return self

    def sgd(self, X_b, y_train, initial_theta, n_iters, t0=5, t1=50):
        """隨機梯度下降, niters是輪數"""

        def get_study_rate(i_iters):
            """把學習率和迭代次數聯繫起來"""
            return t0 / (t1 + i_iters)

        def dJ_sgd(X_b_i, theta, y):
            """計算隨機一個元素的梯度"""
            return 2 * X_b_i.T.dot(X_b_i.dot(theta) - y)

        m = len(X_b)
        theta = initial_theta
        for n in range(int(n_iters)):
            indexes = np.random.permutation(m)
            X_b_new = X_b[indexes, :]
            y_new = y[indexes]
            for i in range(m):
                sgd = dJ_sgd(X_b_new[i], theta, y_new[i])
                theta = theta - get_study_rate(m * n + i) * sgd
        return theta

sklearn 中的隨機梯度下降

SGDRegressor位於線性模型下,SGDRegressor(max_iter=50)可以傳入參數 max_iter指定迭代的次數。

    from sklearn.linear_model import SGDRegressor

    sgd_reg = SGDRegressor(max_iter=50)
    sgd_reg.fit(X, y)

調試梯度下降法

對每個θ\theta分量,都要微積分,最後,用算出來的微積分向量,作爲梯度。

  • 計算量巨大,調試成功後,要關閉
  • 分子的兩個順序不能錯,我把順序搞錯了,結果導數算反了。。。

dJdθ=J(θ+ε)J(θε)2ε \frac{d J}{d \theta}=\frac{J(\theta+\varepsilon)-J(\theta-\varepsilon)}{2 \varepsilon}

關鍵代碼在這:

    def dj_debug(self, X_b, theta, y, epsilon=0.001):
        res = np.zeros((len(theta), ))
        for i in range(len(theta)):
            theta_1 = theta.copy()
            theta_2 = theta.copy()
            theta_1[i] += epsilon
            theta_2[i] -= epsilon
            res[i] = (self.J(theta_1, X_b, y) - self.J(theta_2, X_b, y)) / (2 * epsilon)
        return res

完整代碼:

class aDebugGradient:
    """隨機梯度下降法"""

    def __init__(self):
        self.coef_ = None       # 表示參數,theta_[1:]
        self.intercept_ = None  # 表示截距 ==>theta[0]
        self._thera = None      # 表示完整的theta==> theta[:]

    def fit_debug(self, X_train, y_train, eta=0.01, n_iters=1e4):
        """使用梯度下降法尋找最小的代價函數"""
        # 格式化X和theta,加上x0 和 theta0
        X_b = np.hstack([np.ones((len(X_train), 1)), X_train])
        initial_theta = np.zeros(X_b.shape[1])

        # 調用循環的梯度下降
        self._thera = self.gradient_Descent(self.dj_debug, X_b, y_train, initial_theta, eta, n_iters)
        self.intercept_ = self._thera[0]
        self.coef_ = self._thera[1:]
        return self

    def J(self, theta, X_b, y):
        return np.sum((y - X_b.dot(theta)) ** 2) / len(y)

    def dj_debug(self, X_b, theta, y, epsilon=0.001):
        res = np.zeros((len(theta), ))
        for i in range(len(theta)):
            theta_1 = theta.copy()
            theta_2 = theta.copy()
            theta_1[i] += epsilon
            theta_2[i] -= epsilon
            res[i] = (self.J(theta_1, X_b, y) - self.J(theta_2, X_b, y)) / (2 * epsilon)
        return res

    def dJ_math(self, X_b, theta, y):
        return X_b.T.dot(X_b.dot(theta) - y) * 2. / len(y)

    def gradient_Descent(self, dj, X_b, y_train, initial_theta, eta, n_iters,  epsilon=1e-8):
        """梯度下降"""
        theta = initial_theta
        for i in range(int(n_iters)):
            gradient = dj(X_b, theta, y_train)      # 計算得到梯度
            last_theta = theta
            theta = theta - eta * gradient
            if np.absolute(self.J(theta, X_b, y_train) - self.J(last_theta, X_b, y_train)) < epsilon:
                break
        return theta

最後,,,這垃圾公式真是煩死人!!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章