PS:要轉載請註明出處,本人版權所有。

PS: 這個只是基於《我自己》的理解,

如果和你的原則及想法相沖突,請諒解,勿噴。

前置說明

  本文作為本人csdn blog的主站的備份。(BlogID=109)

環境說明
  • Windows 10
  • VSCode
  • Python 3.8.10
  • Pytorch 1.8.1
  • Cuda 10.2

前言


  如果有計算機背景的相關童鞋,都應該知道數值計算中的上溢和下溢的問題。關於計算機中的數值表示,在我的《數與計算機 (編碼、原碼、反碼、補碼、移碼、IEEE 754、定點數、浮點數)》 (https://blog.csdn.net/u011728480/article/details/100277582) 一文中有比較好的介紹。計算機中的數值表示,相對於實數數軸來說是離散且有限的,意思就是計算機中的能表示的數有最大值和最小值以及最小單位,特別是浮點數表示,有興趣的可以看看上文。

  其實很好理解,深度學習裡面具有大量的乘法加法,一不小心你就會遇見上溢和下溢的問題,因此我們一不小心就會遇見NAN和INF的問題(NAN和INF詳見上文提到的文章)。此外,由於一些特殊的情況,可能會導致我們的引數的偏導數接近於0,讓我們的模型收斂的非常的慢。因此我們可能需要從模型的初始化以及相關的模型構造方面來好好的討論一下我們在訓練過程中可能出現的問題。

  一般來說,我們訓練的時候都非常的關注我們的損失函式,如果損失函式值異常,會導致相關的偏導數出現接近於0或者接近於無限大,那麼就會直接導致模型訓練及其困難。此外,我們的權重引數也會參與網路計算,按照上述的描述,權重引數的初始值也可能導致損失函式的值異常。因此大佬們也引入了另外一種常見的初始化方式Xavier,比較具有普適性。下面我們簡單的驗證一下我們訓練過程中出現梯度接近於0和接近於無限大的情況,這裡也就是說的梯度消失和梯度爆炸問題。同時也簡單說明引數初始化相關的問題。

梯度消失(gradient vanishing)


  在深度學習中有一個啟用層叫做Sigmoid層,其定義如下是:\(Sigmoid(x)=1/(1+\exp(-x))\),如果我們的模型裡面接入了這種啟用函式,很容易構造出梯度消失的情況,下面我們看一下其導數和函式值相對於X的相關關係。

  程式碼如下:

import torch
import numpy as np
import matplotlib.pyplot as plt fig, ax = plt.subplots()
xdata, ydata = [[], []], [[], []]
line0, = ax.plot([], [], 'r-', label='sigmoid')
line1, = ax.plot([], [], 'b-', label='gradient-sigmoid') def init_and_show(xlim_min, xlim_max, ylim_min, ylim_max):
ax.set_xlabel('x')
ax.set_ylabel('sigmoid(x)')
ax.set_title('sigmoid/gradient-sigmoid')
ax.set_xlim(xlim_min, xlim_max)
ax.set_ylim(ylim_min, ylim_max)
ax.legend([line0, line1], ('sigmoid', 'gradient-sigmoid')) line0.set_data(xdata[0], ydata[0])
line1.set_data(xdata[1], ydata[1]) plt.show() def sigmoid_test():
x = np.arange(-10.0, 10.0, 0.1) x = torch.tensor(x, dtype=torch.float, requires_grad=True) sig_fun = torch.nn.Sigmoid() y = sig_fun(x) y.backward(torch.ones_like(y)) xdata[0] = x.detach().numpy()
xdata[1] = x.detach().numpy()
ydata[0] = y.detach().numpy()
ydata[1] = x.grad.detach().numpy() init_and_show(-10.0, 10.0, 0, 1) def multi_mat_dot():
M = np.random.normal(size=(4, 4))
print('⼀個矩陣\n', M)
for i in range(10000):
M = np.dot(M, np.random.normal(size=(4, 4)))
print('乘以100個矩陣後\n', M) if __name__ == '__main__':
sigmoid_test()

  結果圖如下

  我們可以從圖中看到,當x小於-5和大於+5的時候,其導數的值接近於0,導致bp的時候,引數更新小,模型收斂的特別的慢。

梯度爆炸(gradient exploding)


  現在我們假設我們有一個模型,其有N個線性層構成,定義輸入為X,標籤為Y,模型為 \(M(X) = X*W_1 .... W_{n-2}*W_{n-1}*W_n\),損失函式為\(L(X) = M(X) - Y = X*W_1 .... W_{n-2}*W_{n-1}*W_n - Y\),求W1關於損失函式的偏導數\(\frac{dL(X)}{dW_1} = X*W_2 .... W_{n-2}*W_{n-1}*W_n\)。從這裡我們可以看到W2到Wn與輸入的X的乘積構成了W1的偏導數。

  下面我們簡單的構造一個矩陣,然後讓他計算100次乘法。程式碼如下:

import torch
import numpy as np
import matplotlib.pyplot as plt fig, ax = plt.subplots()
xdata, ydata = [[], []], [[], []]
line0, = ax.plot([], [], 'r-', label='sigmoid')
line1, = ax.plot([], [], 'b-', label='gradient-sigmoid') def init_and_show(xlim_min, xlim_max, ylim_min, ylim_max):
ax.set_xlabel('x')
ax.set_ylabel('sigmoid(x)')
ax.set_title('sigmoid/gradient-sigmoid')
ax.set_xlim(xlim_min, xlim_max)
ax.set_ylim(ylim_min, ylim_max)
ax.legend([line0, line1], ('sigmoid', 'gradient-sigmoid')) line0.set_data(xdata[0], ydata[0])
line1.set_data(xdata[1], ydata[1]) plt.show() def sigmoid_test():
x = np.arange(-10.0, 10.0, 0.1) x = torch.tensor(x, dtype=torch.float, requires_grad=True) sig_fun = torch.nn.Sigmoid() y = sig_fun(x) y.backward(torch.ones_like(y)) xdata[0] = x.detach().numpy()
xdata[1] = x.detach().numpy()
ydata[0] = y.detach().numpy()
ydata[1] = x.grad.detach().numpy() init_and_show(-10.0, 10.0, 0, 1) def multi_mat_dot():
M = np.random.normal(size=(4, 4))
print('⼀個矩陣\n', M)
for i in range(100):
M = np.dot(M, np.random.normal(size=(4, 4)))
print('乘以100個矩陣後\n', M) if __name__ == '__main__':
multi_mat_dot()

  他計算100次乘法後結果如下:

  我們可以看到,經過100次乘法後,其值已經非常大(小)了指數都是到了25了。這個時候算出來的損失非常大的,這個時候梯度也非常大,很容易導致訓練異常。

引數初始化之Xavier


  文首我們提到,我們之前的引數初始化都是基於期望為0,方差為一個指定值初始化的,這裡面的指定值是隨個人定義的,這個可能會給我們的訓練過程帶來困擾。

  但是我們可以從以下的角度來看待這個事情,我們的權重引數W是一個期望為0,方差為\(\delta^2\)的特定分佈。我們的輸入特徵X是一個期望為0,方差為\(\lambda^2\)的特定分佈(注意這裡不僅僅是正態分佈)。我們假設我們的模型是線性模型,那麼其輸出為:\(O_i = \sum\limits_{j=1}^{n}W_{ij}X_{j}\),\(O_i\)是代表第i層的輸出。這個時候,我們求出\(O_i\)的期望是:\(E(O_i) = \sum\limits_{j=1}^{n}E(W_{ij}X_{j}) = \sum\limits_{j=1}^{n}E(W_{ij})E(X_{j}) = 0\),其方差為:\(Variance(O_i) = E(O_i^2) - (E(O_i))^2 = \sum\limits_{j=1}^{n}E(W_{ij}^2X_{j}^2) - 0 = \sum\limits_{j=1}^{n}E(W_{ij}^2)E(X_{j}^2) = n*\delta^2*\lambda^2\)。我們現在假設如果要\(O_i\)的方差等於X的方差,那麼\(n*\delta^2 = 1\)才能夠滿足要求。現在我們考慮BP的時候,也需要\(n_{out}*\delta^2 = 1\)才能夠保證方差不會變,至少從數值穩定性來說,我們應該保證方差儘量穩定,不應該放大。我們同時考慮n和\(n_{out}\),那麼我們可以認為當\(1/2*(n+n_{out})*\delta^2 = 1\)時,我們保證了輸出O的方差在約定範圍內,儘量保證了其數值的穩定性,這就是Xavier方法的核心內容。

  初始化方法有很多,但是Xavier方法有較大的普適性。對於某些模型,特定的初始化方法有奇效。

後記


  到本文結束,其實我們可以訓練一些簡單的模型了,但是本文所介紹的3個概念會一直伴隨著我們以後的學習過程,如果訓練出現了INF,NAN這些特殊的值,基本我們就需要往這方面去想和解決問題。

參考文獻


打賞、訂閱、收藏、丟香蕉、硬幣,請關注公眾號(攻城獅的搬磚之路)

PS: 請尊重原創,不喜勿噴。

PS: 要轉載請註明出處,本人版權所有。

PS: 有問題請留言,看到後我會第一時間回覆。