對簡單梯度下降方法的分析總結,有關步長,梯度精度和迭代次數
我們對一組數據進行簡單函數擬合時,會用到一種基礎方法即梯度下降法
基本原理
- 現在我們有一組數據
- 這些數據之間的關系為
他們之間是函數關系Z(x, y)
-
現在我們要從現有的這n組數據中進行分析,最終找到一組符合這組數據的w1, w2, b,一開始我們並不清楚這三個參數
-
首先可以初始化這三個參量都為0,並由此根據現有的x,y值算出我們對於z的預測\(A_i\),設\(\theta\)為向量(w1, w2)
-
之后對方差函數
\[J(\theta) = \frac{1}{n}\sum_{i = 1}^n [z_i - (w_1 * x_i + w_2 * y_i + b)]^2 \]求梯度
其梯度為\[\theta' = (\frac{\partial J}{\partial w_1}, \frac{\partial J}{\partial w_2}) \]有關偏導數的定義較為簡潔,可自行從網上搜索
事實上梯度所指的方向就是J函數上升最快的方向
而我們的目標是讓方差J函數盡量小,所以我們需要將\(\theta\)向梯度的反方向\(-\theta'\)變化- 當然我們知道,這里的梯度僅僅是(w1, w2)處的梯度,w變化后梯度也會變化,所以我們需要讓w變化一點點,這“一點點”我們用步長“st”表示,即
\[\theta = \theta - \theta' * st \]
st由我們自行定義
- 對於b, 我們這樣進行擬合:
\[b = b + \frac{1}{n}\sum_{i = 1}^n [z_i - (w_1 * x_i + w_2 * y_i + b)] \]這樣可以使b能夠趨於樣本中心
- 然后重新執行前面的步驟,最終當方差小於某個值或者達到一定次數時,迭代結束,獲得一組符合條件精度的w1, w2, b,完成直線擬合
-
-
示例代碼
-
其中初始參數w1,w2,b被設定為0.5,0.5,5,存放在wn.txt中
-
十組數據存放於11.csv中,csv文件的x為“math”列,y為“English”列,z為“all”列,按照
\[all = 0.3 * math + 0.7 * English + 20 \]生成(即目標w1 == 0.3,w2 == 0.7, b == 20),如下圖:
- python代碼如下:
-
import pandas as pd
import numpy as np
def initww(): # 初始化w,b
aa = np.zeros(3, dtype=float)
with open("wn.txt", "r") as f:
co = 0
cc = f.readlines()
for line in cc:
aa[co] = float(line)
co += 1
return aa
def initcsv(csv): # 初始化x, y
aa = np.zeros((len(csv["math"]), 2), dtype=float)
co = 0
for maths in csv["math"]:
aa[co][0] = float(maths)
co += 1
co = 0
for en in csv["English"]:
aa[co][1] = float(en)
co += 1
return aa
def initreals(csv): # 初始化z
aa = np.zeros(len(csv["all"]), dtype=float)
co = 0
for alls in csv["all"]:
aa[co] = float(alls)
co += 1
return aa
def cost(ws, grade, truth): # 方差函數
pred = np.sum(grade * ws[:-1], axis=1) + ws[-1]
return np.sum((truth - pred) ** 2) / pred.size
def gradient(ws, grade, truth, de): # 計算梯度,傳入w,b,x,y,z以及現在的方差de
grad = np.zeros(len(ws), dtype=float)
for i in range(len(ws) - 1): # 計算每一分量上的偏導數
neww = ws.copy()
neww[i] += 0.000001
grad[i] = (cost(neww, grade, truth) - de) / 0.000001 # 這里選取dx為0.000001,可以根據需要調整
return grad
def gd(ws, grade, truth, de, st): # 梯度下降,傳入w,b,x,y,z以及現在的方差de, 步長st
rate = gradient(ws, grade, truth, de)
ws[:-1] -= rate[:-1] * st # 調整w
pred = np.sum(grade * ws[:-1], axis=1) + ws[-1]
ws[-1] += np.sum(truth - pred) / pred.size # 調整b
return ws
if __name__ == "__main__":
data = pd.read_csv("11.csv") # 讀取數據並存入array數組中
ww = initww() # ww[:-1]是w1,w2,ww[2]是b
score = initcsv(data) # 十組x,y數據
reals = initreals(data) # 十個z值
d = 0.0
for i in range(100): # 迭代一百次
d = cost(ww, score, reals)
print(ww[0], " ", ww[1], " ", ww[2], " ", "cost:" + str(d)) # 每次輸出w與b和方差
ww = gd(ww, score, reals, d, 0.001) # 梯度下降入口,步長設定為0.001
對於步長,dx和迭代次數的分析
- 上述代碼解決當前問題是可以的,不過在一開始,我的步長,dx與迭代次數分別是0.01,0.01和15
- 盡管有了理論的支持,在實踐中還是會遇到各種各樣的錯誤現象,而這其中很多都表現為結果發散
如下圖
可以看出發散嚴重
這種現象表現為發散至很大的一個數,而且發散是加速的
這種主要是步長選擇不合理,每一步調整之后w的位置距離最優解差的更遠了
- 我們這里w是0.1的數量級,選取0.01的步長對於它以及當前的方差函數來說,太大了,將步長改為0.001后表現如下
可以看到cost一開始在減小,但之后卻在11.4左右趨於穩定,其他數據也是在一個數附近趨於穩定,當然我們能看到這些數據是和答案相差較多的
這種“死胡同”現象在這里是因為所求梯度不准確所致,我們知道和求導一樣,dx選取越趨近於零,梯度越准確,我們這里選擇的0.01可以說是“太大了”
- 將dx改為0.001后,結果如下
可以看出,cost降到了0.16,並且其他的參數也與答案很接近,但是這個結果對我們的要求來說可能還不夠,我們需要一個更精確的結果
這里可以看出w,b的變化還可以進行下去,我們需要增加迭代次數
- 將迭代次數增加到100,結果如下
我們確信再往下應該不會有太多改變了,但是結果仍然有一定差距
不過以上的操作說明減小步長與dx,增加迭代次數確實可以提高預測的精度/曲線的擬合度
但是減小步長的同時我們必須增加迭代次數,由於迭代次數受限限制於當前計算機的算力,步長不可以無限減小,所以為了提高精度,我們應該主要減小dx
- 將dx改為0.000001,得到如下結果
可以看出,結果可以說是基本正確了,這樣我們的函數就擬合成功了
總結發現
-
1.步長選取不在於多小,而是在現有條件上選擇最能滿足當前問題的步長,步長減小時盡管精度增加,更不易出現發散錯誤,但是迭代次數要增加(當然不一定同倍數增加)
-
2.可以說迭代次數決定了我們在現有步長,dx的條件下能達到多小的誤差,在迭代次數增加到一定程度后,這個誤差受制於其他因素,不會再減小
-
3.而dx越小,梯度越准確,可以想到,越接近最優解(誤差函數的極小值處),梯度越小,而梯度精度就越重要,所以,越小的dx決定了這個程序最終能達到多精確的擬合效果
-
補充,簡單考慮時,我們可以按上面的程序里一樣,直接用偏導數定義去求梯度,但是這樣一定要選擇一個足夠小的dx才可以。我們對已知解析式的誤差函數一般直接使用它的偏導函數來求梯度,這樣就避免了這個問題,所以上面的程序完全可以依此改寫,當然,如果誤差函數不易得偏導函數,使用定義也是一種方法