1. 程式人生 > 實用技巧 >【DLPytorch】Optimizer(一)

【DLPytorch】Optimizer(一)

前言

損失函式通常被稱作優化問題的目標函式(objective function),是一個基於訓練資料集的損失函式,優化的目標在於降低訓練誤差。依據慣例,優化演算法通常只考慮最小化目標函式。其實,任何最大化問題都可以很容易地轉化為最小化問題,只需令目標函式的相反數為新的目標函式即可。

為了求得最小化目標函式的數值解,我們將通過優化演算法有限次迭代模型引數來儘可能降低損失函式的值。但優化在深度學習中有很多挑戰。其中的兩個挑戰,即區域性最小值和鞍點。

區域性最小值:深度學習模型的目標函式可能有若干區域性最優值。當一個優化問題的數值解在區域性最優解附近時,由於目標函式有關解的梯度接近或變成零,最終迭代求得的數值解可能只令目標函式區域性最小化而非全域性最小化。

def f(x):
    return x * np.cos(np.pi * x)


x = np.arange(-1.0, 2.0, 0.1)
y = f(x)
fig = plt.plot(x, y)

plt.annotate('local minimum', xy=(-0.3, -0.25), xytext=(-0.77, -1.0), arrowprops=dict(arrowstyle='->'))
plt.annotate('global minimun', xy=(1.1, -0.95), xytext=(0.6, 0.8), arrowprops=dict(arrowstyle='->'))

plt.xlabel('x')
plt.ylabel('f(x)')

plt.show()

鞍點:在圖的鞍點位置,目標函式在\(x\)軸方向上是區域性最小值,但在\(y\)軸方向上是區域性最大值。

x, y = np.mgrid[-1: 1: 31j, -1: 1: 31j]
# np.mgrid[ 第1維,第2維 ,第3維 , …] 

z = x**2 - y**2

fig = plt.figure()
ax = fig.add_subplot(111, projection = '3d')
ax.plot_wireframe(x, y, z, **{'rstride': 2, 'cstride': 2})
ax.plot([0], [0], [0], 'rx')
# 設定xyz的刻度
ticks = [-1,  0, 1]
plt.xticks(ticks)
plt.yticks(ticks)

ax.set_zticks(ticks)
plt.xlabel('x')
plt.ylabel('y')
plt.show()

假設一個函式的輸入為\(k\)維向量,輸出為標量,那麼它的海森矩陣(Hessian matrix)有\(k\)個特徵值。該函式在梯度為0的位置上可能是區域性最小值、區域性最大值或者鞍點。

  • 當函式的海森矩陣在梯度為零的位置上的特徵值全為正時,該函式得到區域性最小值。
  • 當函式的海森矩陣在梯度為零的位置上的特徵值全為負時,該函式得到區域性最大值。
  • 當函式的海森矩陣在梯度為零的位置上的特徵值有正有負時,該函式得到鞍點。

隨機矩陣理論告訴我們,對於一個大的高斯隨機矩陣來說,任一特徵值是正或者是負的概率都是0.5 [1]。那麼,以上第一種情況的概率為 \(0.5^k\)。由於深度學習模型引數通常都是高維的(\(k\)很大),目標函式的鞍點通常比區域性最小值更常見。

在深度學習中,雖然找到目標函式的全域性最優解很難,但這並非必要。通過學習各種的優化演算法,幫助我們解決上述的兩類常見的問題。

梯度下降和隨機梯度下降

梯度下降

通過\(x \leftarrow x - \eta f'(x)\)來迭代\(x\),函式\(f(x)\)的值可能會降低。因此在梯度下降中,我們先選取一個初始值\(x\)和常數\(\eta > 0\),然後不斷通過上式來迭代\(x\),直到達到停止條件,例如\(f'(x)^2\)的值已足夠小或迭代次數已達到某個值。其實就是每次的梯度都會指引x越來越靠近最值。

以目標函式\(f(x)=x^2\)為例來看一看梯度下降是如何工作的,使用\(x=10\)作為初始值,並設\(\eta=0.2\)。使用梯度下降對\(x\)迭代10次,可見最終\(x\)的值較接近最優解。

def gd(eta):
    x = 10
    results = [x]
    for i in range(10):
        x -= eta * 2 * x
        results.append(x)
    print("after epoch 10, x: ", x) # epoch 10, x: 0.06046617599999997
    return results


def show_trace(res):
    f_line = np.arange(-10, 10, 0.1)
    fig = plt.figure()
    ax = fig.add_subplot(111)
    ax.plot(f_line, [x * x for x in f_line])
    ax.plot(res, [x * x for x in res], '-o')
    plt.xlabel('x')
    plt.ylabel('y')
    plt.show()


res = gd(0.2)
show_trace(res)

學習率

上述梯度下降演算法中的正數\(\eta\)通常叫作學習率。這是一個超引數,需要人工設定。如果使用過小的學習率,會導致\(x\)更新緩慢從而需要更多的迭代才能得到較好的解。

當學習率過小時,最終\(x\)的值依然與最優解存在較大偏差。

如果使用過大的學習率,\(\left|\eta f'(x)\right|\)可能會過大從而使前面提到的一階泰勒展開公式不再成立:這時我們無法保證迭代\(x\)會降低\(f(x)\)的值。

因此,使用適當的學習率,沿著梯度反方向更新自變數可能降低目標函式值。梯度下降重複這一更新過程直到得到滿足要求的解。當學習率過大或過小都有問題。一個合適的學習率通常是需要通過多次實驗找到的。

多維度梯度下降

對每一個維度都要進行梯度下降,比如目標函式\(f(\boldsymbol{x})=x_1^2+2x_2^2\)。那麼,梯度\(\nabla f(\boldsymbol{x}) = [2x_1, 4x_2]^\top\)

def gd_2d(x1, x2):
    return (x1 - eta * 2 * x1, x2 - eta * 4 * x2)

還需要畫等高線來觀察:

隨機梯度下降

如果使用梯度下降,每次自變數迭代的計算開銷為\(\mathcal{O}(n)\),它隨著\(n\)線性增長。因此,當訓練資料樣本數很大時,梯度下降每次迭代的計算開銷很高。

隨機梯度下降(stochastic gradient descent,SGD)減少了每次迭代的計算開銷。在隨機梯度下降的每次迭代中,我們隨機均勻取樣的一個樣本索引\(i\in\{1,\ldots,n\}\),並計算梯度\(\nabla f_i(\boldsymbol{x})\)來迭代\(\boldsymbol{x}\)

\[\boldsymbol{x} \leftarrow \boldsymbol{x} - \eta \nabla f_i(\boldsymbol{x}). \]

這裡\(\eta\)同樣是學習率。可以看到每次迭代的計算開銷從梯度下降的\(\mathcal{O}(n)\)降到了常數\(\mathcal{O}(1)\)

我們通過在梯度中新增均值為0的隨機噪聲來模擬隨機梯度下降,以此來比較它與梯度下降的區別。

def sgd_2d(x1, x2, s1, s2):
    return (x1 - eta * (2 * x1 + np.random.normal(0.1)),
            x2 - eta * (4 * x2 + np.random.normal(0.1)), 0, 0)

show_trace_2d(f_2d, train_2d(sgd_2d))

所以,當訓練資料集的樣本較多時,梯度下降每次迭代的計算開銷較大,隨機梯度下降通常更受青睞。

問題補充

1. Matplotlib中的annotate(註解)的用法

連結:https://blog.csdn.net/leaf_zizi/article/details/82886755

Axes.annotate(s, xy, *args, **kwargs)
params:
  s:註釋文字的內容
	xy:被註釋的座標點,二維元組形如(x,y)
	xytext:註釋文字的座標點,也是二維元組,預設與xy相同
	arrowprops:箭頭的樣式,dict(字典)型資料,如果該屬性非空,則會在註釋文字和被註釋點之間畫一個箭頭。如果不設定'arrowstyle' 關鍵字,則允許包含以下關鍵字:
  
示例:
plt.annotate('local minimum', xy=(-0.3, -0.25), xytext=(-0.77, -1.0), arrowprops=dict(arrowstyle='->'))

2. np.mgrid()用法

連結:https://www.cnblogs.com/wanghui-garcia/p/10763103.html

# 功能:返回多維結構,常見的如2D圖形,3D圖形
np.mgrid[ 第1維,第2維 ,第3維 , …] 

# 第n維的書寫形式為:
a:b:c # c表示步長,為實數表示間隔;該為長度為[a,b),左開右閉

-- 
a:b:cj # cj表示步長,為複數表示點數;該長度為[a,b],左閉右閉
    
    
# 舉例說明:
# 生成1D陣列:
a=np.mgrid[-4:4:3j] # 在[-4,4]區間內取3個值
# 返回array([-4.,  0.,  4.])
 
# 生成個2D矩陣:
a = np.mgrid[[1:3:3j, 4:5:2j]]
# 生成的是3*2的矩陣

x, y = np.mgrid[1:3:3j, 4:5:2j]

"""
x
array([[1., 1.],
       [2., 2.],
       [3., 3.]])
"""

3. Python zip() 函式

zip() 函式用於將可迭代的物件作為引數,將物件中對應的元素打包成一個個元組,然後返回由這些元組組成的列表。

如果各個迭代器的元素個數不一致,則返回列表長度與最短的物件相同,利用 * 號操作符,可以將元組解壓為列表。

zip([iterable, ...])
params:
	iterabl -- 一個或多個迭代器;
return:
	返回元組列表

>>>a = [1,2,3]
>>> b = [4,5,6]
>>> c = [4,5,6,7,8]
>>> zipped = zip(a,b)     # 打包為元組的列表
[(1, 4), (2, 5), (3, 6)]
>>> zip(a,c)              # 元素個數與最短的列表一致
[(1, 4), (2, 5), (3, 6)]
>>> zip(*zipped)          # 與 zip 相反,*zipped 可理解為解壓,返回二維矩陣式
[(1, 2, 3), (4, 5, 6)]

zip(a,b):將a和b中的元素對應組合成元組。結果:(1,4),(2,5),(3,6)。而要想看到這個結果,需用*zip()函式

*zip(a,b):先將a和b中的元素對應組合成元組形成結果,再將這個結果解壓,即 * 表示解壓。但不輸出,需藉助print打印出來:

4. plt.contour的用法

連結:https://blog.csdn.net/qq_42505705/article/details/88771942

contour([X, Y,] Z, [levels], *kwargs) # 繪製等高線

params:
  - X,Y 座標
  - Z :繪製輪廓的高度值。
  
# 示例
plt.contour(x1, x2, f(x1, x2), colors='#1f77b4')