聊起初始化,大家應該都瞭解大名鼎鼎的Glorot初始化(也叫Xavier初始化),Kaiming初始化(也叫He初始化)。
之前調了一個模型,原作者是使用Tensorflow實現的,我在復現過程中使用了PyTorch,雖然已經儘可能注意二者的差異,但是效果始終差那麼點。後來想到,或許是因為二者層初始化不同所導致的(雖然最終證明不是……),在這個過程中,總結了一點有意義的內容,這裡和大家分享。
首先我們來看一下PyTorch中初始化的方法,此處我們只關心平時最常使用到的3類操作:Linear,Conv,以及RNN。
假設一個全連接層,輸入channel ,輸出channel ,那麼它的weight的shape應該是 ,而它的bias的shape,則應該為: 。
根據PyTorch--Linear文檔,Linear層的weight被初始化為: ,bias也被初始化為: 。其中: 。
需要說明的是,原始的Glorot Initialization並不是採用這樣的方法。Glorot初始化,同時考慮 和 (也有的說是 、 )。weight被初始化為: ,只不過,此處的 為: 。
import numpy as np import matplotlib.pyplot as plt import torch import torch.nn as nn
# ============================================================ # Check PyTorch Initialization (conv2d / linear / lstm). # ============================================================ # -------------------- # 1.1. PyTorch Linear # -------------------- dummy_linear = nn.Linear(100, 250) layer = dummy_linear
layer_w = layer.weight # Should be U(-0.1, 0.1) layer_w = layer_w.detach() layer_w_np = layer_w.numpy() layer_w_np = np.reshape(layer_w_np, [-1]) print(layer_w_np.shape)
fig, ax = plt.subplots() ax.hist(layer_w_np, bins=10) ax.set_title("PyTorch Linear Initialization") plt.show()
分佈圖如下:
這裡僅以nn.Conv2d舉例,同樣,設輸入channel ,輸出channel 。卷積層還多了一個kernel_size,這麼我們設為 (避免和上面的 衝突)。
那麼,在不考慮groups,dilation這些的情況下,我們卷積層的weight的shape應該是: ,bias的shape應該為:
根據PyTorch--nn.Conv2d文檔,Conv2d層的weight被初始化為: ,bias也被初始化為: 。注意,卷積層要考慮到,kernel_size的影響了。所以: 。
其實比較奇怪的是,在這兩個例子裏,我們的bias也用相同的方法進行了初始化。在我的印象中,bias要不然就是使用全0進行costant初始化,要不然就是直接不加bias,今天得虧是看了文檔,才知道在PyTorch裡面,bias是默認使用相同的方式進行初始化的。
# -------------------- # 1.2. PyTorch Conv2d # -------------------- dummy_conv = nn.Conv2d(25, 64, 2)
layer = dummy_conv
fig, ax = plt.subplots() ax.hist(layer_w_np, bins=10) ax.set_title("PyTorch Conv2d Initialization") plt.show()
終於來到RNN層了,其實我的本意也就是看看兩個框架初始化是不是一樣,那快開始吧。
這邊我們使用GRUCell進行舉例,GRUCell接收一個input_size,一個hidden_size。因為他實際上是3個gate,每個gate需要綜合 和 的信息。
其中, 的shape是 , 的shape是 ,然後映射完的shape還是 。
所以它的weight實際上是3個,每個weight的shape是 。但在這裡,PyTorch實際上是:1. 將3個weight合併了,那麼這個時候的shape就是: ;2. 將 和 給拆開了,拆成一個 (給 用)和 (給用)。bias同理。
所以,結論是,在GRUCell層中,我們有兩個參數: 的shape應該為: ,而 的shape應該為 。
根據PyTorch--nn.GRUCell文檔,GRUCell層的weight仍然被初始化為: 。此處的 。
# -------------------- # 1.3. PyTorch GRUCell # -------------------- dummy_gru_cell = nn.GRUCell(input_size=50, hidden_size=100)
layer = dummy_gru_cell
layer_w = layer.weight_hh # Should be U(-0.1, 0.1) layer_w = layer_w.detach() layer_w_np = layer_w.numpy() layer_w_np = np.reshape(layer_w_np, [-1]) print(layer_w_np.shape)
fig, ax = plt.subplots() ax.hist(layer_w_np, bins=10) ax.set_title("PyTorch GRUCell Initialization") plt.show()
PyTorch有一套自己的初始化方法,這個東西不完全是Glorot初始化,我們就管它叫類Glorot初始化吧,嗯,類Glorot_uniform初始化。
然後上面幾個代碼也再簡單不過,我只是卡了一下,總是讓它的weight分佈是一個從-0.1到0.1的一個均勻分佈。
我還要再強調一下,就是torch.nn.init實際上是有Glorot初始化的(API是xavier_uniform_,一個東西),但是在這個裡面就還真的是正經八百Glorot初始化了,同時看 , 。
撒花,終於到2了!儘管今年已經是2019年,但tensorflow的文檔還是一言難盡,讀者經常需要dive into source code才能知道你到底想要幹嘛。與之相對應,PyTorch的文檔就友好很多。怎麼說呢,我感覺讀者,一方面,其實不想知道太底層的東西,另一方面,拜託您別封裝的那麼死,我們不是要的不是fit一下就完的東西。
儘管tf1.x已經日趨式微,不過這邊我們還是用的是tf1.x版本進行實驗。
上面PyTorch是沒有這一節的,不過考慮到Tensorflow裡面所有的layer的變數聲明,實際上都使用的是tf.get_variable這個API,我們有必要做一個簡單的查看。
換句話講,你個tf.get_variable搞明白了,後續那些層,甚至不用再看。
看一下源碼,反正文檔是指不上了,在variable_scope.py裡面,給了下面一句話:
If initializer is `None` (the default), the default initializer passed in the variable scope will be used. If that one is `None` too, a `glorot_uniform_initializer` will be used. The initializer can also be a Tensor, in which case the variable is initialized to this value and shape.
也就是說,他其實是用的Glorot Uniform初始化!因為我們可以指定任意維張量,而只有二維纔有 , 這個概念,所以我們還得再試試。
例1. tf.get_variable,二維情況。
import os import numpy as np import matplotlib.pyplot as plt import tensorflow as tf
# ============================================================ # Check Tensorflow Initialization (conv2d / linear / lstm). # ============================================================ print(tf.__version__) os.environ[CUDA_VISIBLE_DEVICES] = 7
# -------------------- # 2.0. PyTorch GRUCell # -------------------- w_2d = tf.get_variable(w_2d, shape=[240, 360]) init = tf.global_variables_initializer()
# -------------------- # Executation # -------------------- config = tf.ConfigProto() config.gpu_options.allow_growth = True with tf.Session(config=config) as sess: sess.run(init) w_2d_eval = sess.run(w_2d)
print(w_2d_eval.shape) layer_w_np = np.reshape(w_2d_eval, [-1]) print(layer_w_np.shape)
fig, ax = plt.subplots() ax.hist(layer_w_np, bins=30) ax.set_title("Tensorflow get_variable 2D initialization") plt.show()
我們還是特意卡了一下 ,因為240+360正好是600。
例2. tf.get_variable,三維情況,新增的維度在前。
剛才我們指定的維度是 ,然後我們現在看一下如果變數變成三維,會怎麼樣。
我先盲猜一把,當維度超過2的時候,可能是類似1.2裡面將其他維度視作了kernel_size。所以一邊是 , 的加法。另一邊是kernel_size們的乘法。
我們先走一小步,看一下shape為: 的情況,這裡我是卡了一個
# -------------------- # 2.0. PyTorch GRUCell # -------------------- w_2d = tf.get_variable(w_2d, shape=[100, 240, 360]) init = tf.global_variables_initializer()
fig, ax = plt.subplots() ax.hist(layer_w_np, bins=30) ax.set_title("Tensorflow get_variable 3D initialization") plt.show()
嚯,好傢夥,還真讓咱們給蒙對了。還真就是真樣的。再看一下,如果把100放在最後,我們申請的shape是 ,會怎麼樣呢?那
完全吻合!
為了保險,我們再試一個,申請一個shape為的變數,還是湊 的分佈,看一下結果:
好了,我覺得現在可以總結一下了:
其實還想寫一下,不過已經發現沒必要了,因為在Tensorflow裡面,所有變數的申請都使用tf.get_variable這個API,換言之,Tensorflow不會像PyTorch一樣根據不同的層,採取不同的初始化策略。所以你只要知道變數的shape,那麼按照上面的法則,你就知道它遵循一個什麼分佈了。
寫的匆忙,有問題還請指出。