大白話5分鐘帶你走進人工智慧-第十一節梯度下降之手動實現梯度下降和隨機梯度下降的程式碼(6)

LHBlog發表於2019-04-15

                                                        第十一節梯度下降之手動實現梯度下降和隨機梯度下降的程式碼(6)

我們回憶一下,之前我們們講什麼了?梯度下降,那麼梯度下降是一種什麼演算法呢?函式最優化演算法。那麼它做的是給我一個函式,拿到這個函式之後,我可以求這個函式的導函式,或者叫可以求這個函式的梯度。導函式是一個數兒,梯度是一組數,求出來梯度之後怎麼用?把你瞎蒙出來的這組θ值,減去α乘以梯度向量,是不是就得到了新的θ,那麼往復這麼迭代下去的,是不是越來越小,越來越小,最後達到我們的最優解的數值解? 你拿到數值解之後,我們實際上就得到了我們的一組最好的θ。

回到我們繼續學習的場景來說,我們想要找到一組能夠使損失函式最小的θ,那麼我原來是通過解析解方式能夠直接把這θ求出來,但是求的過程太慢了,有可能當你引數太多的時候,所以我們通過梯度下降法可以得到我們損失函式最小那一刻對應的W值,也就是完成了一個我們這種引數型模型的訓練。其實這個東西雖然我們們只講了一個線性迴歸,但是邏輯迴歸svm,嶺迴歸,學了之後,你本質就會發現它是不同的損失函式,還是同樣使用函式最優化的方法達到最低值,因為都是引數型模型,只要它有損失函式,最終你就能通過梯度下降的方式找到令損失函式最小的一組解,這組解就是你訓練完了,想要拿到手,用來預測未來,比如放到拍人更美晶片裡面的模型。哪怕深度神經網路也是這樣。

在講梯度下降背後的數學原理之前,我們上午只是從直覺上來講,梯度為負的時候應該加一點數,梯度為正的時候應該減一點數,而且梯度越大證明我應該越多加一點數,只是這麼來解釋一下梯度下降,那麼它背後實際上是有它的理論所在,為什麼要直接把梯度拿過來直接乘上一個數,就能達到一個比較快的收斂的這麼一個結果,它有它的理論所在的。

在講這個之前我們還是先來到程式碼,我們手工的實現一個batch_gradient_descent。批量梯度下降,還記得批量梯度下降和Stochastic_ gradient_descent什麼關係嗎?一個是隨機梯度下降,一個是批量梯度下降,那麼隨機跟批量差在哪了?就是計算負梯度的時候,按理說應該用到所有資料,通過所有的資料各自算出一個結果,然後求平均值.現在我們們改成了直接抽選一條資料,算出結果就直接當做負梯度來用了,,這樣會更快一點,這是一個妥協。理論向實際的妥協,那麼我們先看看實現批量梯度下降來解決。

import numpy as np
#固定隨機種子
np.random.seed(1)
#建立模擬訓練集
X = 2 * np.random.rand(10000, 1)
y = 4 + 3 * X + np.random.randn(10000, 1)
X_b = np.c_[np.ones((10000, 1)), X]
# print(X_b)

learning_rate = 0.1
n_iterations = 500
#有100條樣本
m = 10000

#初始化θ
theta = np.random.randn(2, 1)
count = 0


for iteration in range(n_iterations):
    count += 1
    gradients = 1/m * X_b.T.dot(X_b.dot(theta)-y)
    theta = theta - learning_rate * gradients

print(count)
print(theta)
X = 2 * np.random.rand(10000, 1)
y = 4 + 3 * X + np.random.randn(10000, 1)

上這兩行程式碼仍然是生成一百個X,模擬出對應了一百個Y,這個程式碼裡邊我們不掉現成的包,不像在這用了一個sklearn了,我們不用sklearn的話,還有什麼包幫你好心的生成出一個截距來,是不是沒有了?所以我們是不是還是要手工的,在X這裡面拼出一個全為1的向量作為X0,現在X_b是一個什麼形狀呢?100行2列,第一列是什麼?是不是全是1,第二列是什麼?隨機生成的數。我們解釋下上面程式碼:

                                                  learning_rate = 0.1
                                                   n_iterations = 500

我們做梯度下降的時候,是不是有一個α?我們命名它為學習率,拿一個變數給它接住,learning_rate=0.1。那麼iterations什麼意思? 迭代是吧,n_iterations就是說我最大迭代次數是1萬次,通常這個超引數也是有的,因為在你如果萬一你的學習率設的不好,這個程式是不是就變成死迴圈了?它一直在走,如果你不設一個終止條件的話,你這個東西一訓練你有可能就再也停不下來了。所以通常都會有一個最大迭代次數的。當走了1萬次之後沒收斂,也給我停在這,最後給我報個警告說並沒有收斂,說明你這個東西有點問題。你應該增加點最大次數,或者你調一調你的學習率是不是太小了或者太大了,導致它還沒走到或者說走過了震盪震盪出去了,你需要再去調整。M是這個資料的數量,這一會再說它是幹嘛的。

                                                 theta = np.random.randn(2, 1)
                                                 count = 0

接下來θ,我們上來梯度下降,是不是要先蒙出兩個θ來,於是我生成兩個隨機數,np.random.randn(2, 1)這個的意思是生成一個兩行一列的-1到+1之間的正態分佈的隨機數。接下來我就進入for迴圈:

                                                for iteration in range(n_iterations):
                                                     count += 1
                                                     gradients = 1/m * X_b.T.dot(X_b.dot(theta)-y)
                                                    theta = theta - learning_rate * gradients

在python2中,如果你輸入range(10000),會得到一個列表,從0一直到9999,就實實在在的是一個列表,你把這列表擱進去,進行for迴圈,會得到什麼?第一次迴圈的時候,這次此時的iteration等於0,第二次等於1,因為列表中的每一個元素要賦值到這個變數裡面去。那麼在python3裡面range就不再直接給你實實在在的生成一個列表了,而生成一個python裡面獨一無二的型別叫Generator,生成器。生成器是一個什麼?你只是想借用這個東西去迴圈意思,迴圈一次,你有必要先放一個列表,在那佔你的記憶體空間,沒有必要?實際上它是一個懶載入的這麼一個東西,你每次迭代第一次返回0第二次返回1,每次迭代就給你返回一個數,生成器本質它就變成一個函式了,你第一次呼叫它返回零,第二次掉他返回一,第三次呼叫它返回二,它裡邊記錄了自己當前到哪了,並且生成規則,這樣就沒有必要去佔用你的記憶體空間了,這個是range通常是用來做for迴圈用的,就為了有一個序號。 還有另一種高階的用法是enumerate,假如你用一個enumerate(list),它會給你返回兩個數,第一個是索引號,第二個是list中的本身的元素。在這用逗號可以把這兩個變數分別複製給你指定的兩個變數名。

for i,a in enumerate(list):
  print(i,a)
  那麼i在此時實際上就是li1裡面的第一個元素的索引號是零,第二個元素就是本身是什麼什麼,這個是一個很方便的技巧,能夠幫你在for迴圈體內部既需要索引號來計算它的位置,又需要這個資料本身的時候可以直接用enumerate一次性的把它取出來,很簡單的一個技巧。
                gradients = 1/m * X_b.T.dot(X_b.dot(theta)-y)
            theta = theta - learning_rate * gradients

那麼在這我們只需要n_iterations給他做個計數器就好了,所以我在這兒只用它。那麼我們看這count預設是零,他是一個計數器,每次迴圈會自己加1,+=大家都應該能看懂,自己自加1,那麼此時的gradients實際上就是X_b的轉置。乘以它,再乘以1/m。gradients = 1/m * X_b.T.dot(X_b.dot(theta)-y)。這一步裡面有沒有加和?是批量梯度下降帶加和的版本,還是隨機梯度下降,不帶加和的版本?因為X_B是個矩陣,X_b^{\mathrm{T}} \cdot(X_b \bullet \theta-\mathrm{Y})實際上你看X_b是不是一個矩陣,Y是一個向量,我們需要看Y是個行向量還是列向量, X是一百行一列的,Y是一百行一列的,它是一個列向量。然後用最開始的X轉置去乘以列向量,實際上矩陣乘以一個向量的時候,本身就拿第一行乘以第一列加第二行乘以第一列,本身就把加和融在矩陣乘法裡面去了。所以實際上1/m * X_b.T.dot(X_b.dot(theta)-y)這個東西本身就已經是帶加和的了,而且如前面所說是不是一定要加一個M分之一?接下來實現梯度下降是不是非常簡單,拿上一代的θ減去learnrate,乘以你計算出來的梯度就完事了! 也就是

                                           theta = theta - learning_rate * gradients

我們看梯度下降,雖然講了半天感覺很複雜,其實四行程式碼結束了,那麼在執行完了這1萬次迭代之後,我們把θ給列印出來,看看結果,我沒有實現那個tolerance,沒有判定。假如這樣我每一步都讓它列印θ。

                                          for iteration in range(n_iterations):
                                                     count += 1
                                                     gradients = 1/m * X_b.T.dot(X_b.dot(theta)-y)
                                                     theta = theta - learning_rate * gradients
                                                     if count%20==0:
                                                             print(theta)

我們看看θ隨著更新,它很早你發現是不是都已經收斂了其實?你看她通過多少次收斂的,上來是3.27,3.52,下次慢慢在變化變化,每一步都走的還比較穩健,到4.00,4.04,

是不是越走越慢了,你發現。你看第一部時候從3.27到3.51,到後來4.04,4.07是不是越走越慢了?4.10,4.12,為什麼會越走越慢,學過梯度下降,你們現在是不是應該知道了?因為越接近谷底,它的梯度值怎麼樣?越大還是越小? 越小,它自然就越走越慢,越小走的越慢。那麼最後到4.18,收斂了再也不動,但是我們迴圈是不是還在往下一直繼續,只不過每次加的梯度都是怎麼樣?零。

再有一個問題,剛才我的資料集裡面並沒有做歸一化,那麼實際上它需要做最大最小值歸一化嗎?本質上不太需要,因為它只有一個W,只有一個W的時候自然做歸一化是無所謂的,如果你有多個W的情況下,你對每一個X在預先處理之前做都需要做歸一化,其實也是兩行程式碼的事。我們加入最大最小值歸一化的方式:

                                                 X_b[:,1]=X_b[:,1]-X_b[:,1].mean()

X_b[:,1]這個冒號什麼意思?這個冒號就是一個索引方式,我要取所有的行,我就打一個冒號:,你取所有的行的第一列,就寫個1,X_b[:,1]其實是一個一維陣列,就是那一堆一百個隨機數,那麼X_b[:,1].mean()加一個.mean,你可以看到它的平均是多少?0.95。 如果你用X_b[:,1]=X_b[:,1]-X_b[:,1].mean(),會自動的幫我們對位相減,每一個數都減減去它。然後我們此時在看我們剪完了之後的結果,是不是就變成有正有負的了?

此時在做梯度下降,按理說速度應該更快一些,我們看剛才我們迭代次數是多少,取決於你初始化的點,如果你初始化的點好的話是沒有區別的,如果初始化的點不好的話就有區別。我們執行一下,你看一下,我期待它會有變化。需要多少次?331.剛才有多少次?500多次,現在只需要300多次,就證明剛才隨機那個點,實際上對於能不能正負一起優化,是不是有實際的敏感度的,你現在做了一個均值歸一化,實際上發生了什麼問題? 迭代次數變得更好了,更少了,就更快地達到了收斂的值。但是你發現它W1和W0也變了,

肯定會變,因為你的整個數值全都變了,但變了沒關係。怎麼樣才能繼續用起來?你這變過之後的W,對你新預測的資料拿回來之後先做同樣的歸一化,你再去預測結果也是正確的。

   有了批量梯度下降的程式碼,我們再看隨機梯度下降的話,就簡單多了。先上程式碼:

import numpy as np
from sklearn.linear_model import SGDRegressor

#固定隨機種子
np.random.seed(1)
#生成訓練集
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]
print(X_b)

n_epochs = 1000#迭代次數
t0, t1 = 5, 50

m = 100

#設定可變學習率
def learning_schedule(t):
    return t0 / (t + t1)

#初始化θ
theta = np.random.randn(2, 1)


#隨機梯度下降
learning_rate = 0.1
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)
        learning_rate = learning_schedule(epoch*m + i)#隨著迭代次數增加,學習率逐漸減小。
        theta = theta - learning_rate * gradients

print(theta)

SGDRegressor

解釋下:np.random.seed(1),確定隨機種子,這是不是萬年不變的老三樣,沒有什麼變化,然後我們還是n_iterations,總共迭代一千次。n_epochs = 1000。為什麼是雙重for迴圈?我細緻的給大家說,首先我是不是隨機梯度下降,我要有一個隨機,我要隨機選出一條資料來,我在這一部分

                                               random_index = np.random.randint(m)
                                               xi = X_b[random_index:random_index+1]
                                               yi = y[random_index:random_index+1]

都是在隨機的選X和Y,randint(m)代表從0到99隨機選出一個數字來作為index,作為索引,選出來之後,我的X是不是要從X裡邊把這個隨機位的索引給取出來,所以我取出來X的索引。那麼Y是不是為了隨機取出來索引,這兩個xi = X_b[random_index:random_index+1], yi = y[random_index:random_index+1]就是把對應的那一條X和對應的Y給搞出來。那麼梯度就通過那一個X乘以了一個Y,  gradients = 2*xi.T.dot(xi.dot(theta)-yi)得到了單個計算出來的梯度,你說這個表示式怎麼沒變,表示式是不用變,只不過原來的X矩陣是一百行兩列,現在X矩陣變成一行兩列,你表示式是不用變的,一行自動指出來一個數就不再是一個向量了,那麼此時用learning_rate,我原來是不是定死了就是0.1,而現在我的learning_rate變成了learning_schedule返回的一個結果,我看我定義的learning_schedule是什麼?

                                                     def learning_schedule(t):
                                                        return t0 / (t + t1)

定義了兩個超引數,分別是t0和t1,你丟進一個t來,你看t越大,return這個值會怎麼樣? 越小,那麼我們看t總共能到多少?是不是epoch*m+i, epoch從哪來的?是不是從n_epochs來的,也就是上來迴圈第一次的時候它是多少?0,此時你看return結果這算算是多少,是不是就是零?那麼你看此時的t是零,此時的learning_schedule返回的是一個多大的數,是不是0.1?也就是第一次執行的時候學習率是多少?0.1。當我內層迴圈第101次執行的時候此時epoch等於多少? 等於1。epoch*m+i越來越大,那麼此時的學習率learning_schedule是上升了還是下降了?變大了還是變小了? 變小了一點,也就是說隨著迭代的加深,epoch是不是越來越大?傳到這裡邊數也越來越大,學習率是越來越小的,所以這個也是梯度下降的一種變種。它會把學習率隨著迭代的次數推進,讓學習率越來越小,這樣保證你就可以設定一個初始的時候比較大的學習率,這樣你學習率萬一設大了,它也不會一直震盪越遠,因為隨著迭代越多,梯度越來越小。在我們sklearn 裡面的SGDRegressor函式是有相關的超引數可以設定的。

    def __init__(self, loss="squared_loss", penalty="l2", alpha=0.0001,
                 l1_ratio=0.15, fit_intercept=True, max_iter=None, tol=None,
                 shuffle=True, verbose=0, epsilon=DEFAULT_EPSILON,
                 random_state=None, learning_rate="invscaling", eta0=0.01,
                 power_t=0.25, warm_start=False, average=False, n_iter=None):
        super(SGDRegressor, self).__init__(loss=loss, penalty=penalty,
                                           alpha=alpha, l1_ratio=l1_ratio,
                                           fit_intercept=fit_intercept,
                                           max_iter=max_iter, tol=tol,
                                           shuffle=shuffle,
                                           verbose=verbose,
                                           epsilon=epsilon,
                                           random_state=random_state,
                                           learning_rate=learning_rate,
                                           eta0=eta0, power_t=power_t,
                                           warm_start=warm_start,
                                           average=average, n_iter=n_iter)

loss="squared_loss",這個是什麼? 就是說SGDRegressor是一個通用的機器學習的方式,你可以自己告訴他我的損失函式是什麼,你甭管損失函式是什麼,我給你通過SG的方向一直下降得到一個結果,你要把MSE傳給我,你得到就是一個線性迴歸的結果,你要把一個mse加L2正則的損失函式給我,我得到的就是一個嶺迴歸的結果,就損失函式不同,你的演算法其實就改變了,那麼在SGD它是不捆綁演算法本身的,你給我什麼損失函式執行出來什麼樣,我就是一個幫你下降做計算題的機器。那麼預設的就是squared_loss,alpha實際上是指的l1跟l2中間的一個超引數,l1_ratio也一樣,這兩個超引數是依附在penalty之上的一個超引數,我們們講完了正則化之後,你就明它什麼意思了。

fit_intercept=True,這個什麼意思,截距是不是要幫你搞出來。max_iter=None,什麼意思?最大迭代次數,它沒設。tol是什麼意思,收斂次數。shuffle=True,shuffle什麼意思?shuffle實際上把資料亂序。你上來不是給了我一堆X嗎?我幫你先洗個牌亂一下序再進行訓練,對於我們們這種演算法來說是否亂序不影響最終計算結果。random_state=None就是隨機種子.learning_rate="invscaling", learning_rate是學習率,那麼看learning_rate都有哪些可以選擇的地方?constant什麼意思?常數,那麼此時的eta學習率通常也用eta表示就等於eta0,後邊是不是還有一個eta0,如果你在這寫一個learning_rate=constant,後邊eta0賦一個值,實際上就是一個定值的學習率。然後它有兩種變種的,一個叫optimal,優化的,用1.0/(alpha*(t+t0))。另外一個是什麼?T的power T次方,就是T的N次方,這種方式叫invscaling。它們都是差不多,它們都是想讓這學習率越往下走越小,這麼一種可變學習率的調整方式,那麼預設是使用invscaling倒置縮放的這種方式來做的,也就是說實際上我們現在就瞭解到,在sklearn中並沒有使用這種定值的學習率,預設的實現裡面,並不是使用固定的學習率來做梯度下降的,而是使用這種可變學習率來做的。

聊了這麼多梯度下降的邏輯和過程,有沒有對其底層原理感興趣,所以下一節我們將講解梯度下降的底層原理。

相關文章