聊起初始化,大家應該都瞭解大名鼎鼎的Glorot初始化(也叫Xavier初始化),Kaiming初始化(也叫He初始化)。

0. 起因

之前調了一個模型,原作者是使用Tensorflow實現的,我在復現過程中使用了PyTorch,雖然已經儘可能注意二者的差異,但是效果始終差那麼點。後來想到,或許是因為二者層初始化不同所導致的(雖然最終證明不是……),在這個過程中,總結了一點有意義的內容,這裡和大家分享。

1. PyTorch初始化方法

首先我們來看一下PyTorch中初始化的方法,此處我們只關心平時最常使用到的3類操作:Linear,Conv,以及RNN。

1.1. Linear層初始化

假設一個全連接層,輸入channel C_	extrm{in} ,輸出channel C_	extrm{out} ,那麼它的weight的shape應該是 (C_	extrm{out}, C_	extrm{in}) ,而它的bias的shape,則應該為: (C_	extrm{out},)

根據PyTorch--Linear文檔,Linear層的weight被初始化為: U(-sqrt{k}, sqrt{k}) ,bias也被初始化為: U(-sqrt{k}, sqrt{k}) 。其中: k=frac{1}{C_	extrm{in}}

需要說明的是,原始的Glorot Initialization並不是採用這樣的方法。Glorot初始化,同時考慮 C_	extrm{in}C_	extrm{out} (也有的說是 	extrm{fan}\_	extrm{in}	extrm{fan}\_	extrm{out} )。weight被初始化為: U(-sqrt{k}, sqrt{k}) ,只不過,此處的 k 為: sqrt{k}=sqrt{frac{3}{(C_	extrm{in}+C_	extrm{out})color{red}{/2}}}=sqrt{frac{6}{C_	extrm{in}+C_	extrm{out}}}

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()

分佈圖如下:

1.2 Conv層

這裡僅以nn.Conv2d舉例,同樣,設輸入channel C_	extrm{in} ,輸出channel C_	extrm{out}。卷積層還多了一個kernel_size,這麼我們設為 ks (避免和上面的 k 衝突)。

那麼,在不考慮groups,dilation這些的情況下,我們卷積層的weight的shape應該是: (C_	extrm{out}, C_	extrm{in},ks,ks) ,bias的shape應該為: (C_	extrm{out},)

根據PyTorch--nn.Conv2d文檔,Conv2d層的weight被初始化為: U(-sqrt{k}, sqrt{k}) ,bias也被初始化為: U(-sqrt{k}, sqrt{k}) 。注意,卷積層要考慮到,kernel_size的影響了。所以: k=frac{1}{C_	extrm{in}cdot color{red}{ks^2}}

其實比較奇怪的是,在這兩個例子裏,我們的bias也用相同的方法進行了初始化。在我的印象中,bias要不然就是使用全0進行costant初始化,要不然就是直接不加bias,今天得虧是看了文檔,才知道在PyTorch裡面,bias是默認使用相同的方式進行初始化的。

import numpy as np
import matplotlib.pyplot as plt
import torch
import torch.nn as nn

# --------------------
# 1.2. PyTorch Conv2d
# --------------------
dummy_conv = nn.Conv2d(25, 64, 2)

layer = dummy_conv

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 Conv2d Initialization")
plt.show()

分佈圖如下:

1.3. RNN層

終於來到RNN層了,其實我的本意也就是看看兩個框架初始化是不是一樣,那快開始吧。

這邊我們使用GRUCell進行舉例,GRUCell接收一個input_size,一個hidden_size。因為他實際上是3個gate,每個gate需要綜合 h_{t-1}x_t 的信息。

其中, x_t 的shape是 (	extrm{input},)h_{t-1} 的shape是 (	extrm{hidden},) ,然後映射完的shape還是 (	extrm{hidden},)

所以它的weight實際上是3個,每個weight的shape是 (	extrm{hidden}, 	extrm{hidden}color{red}+	extrm{input}) 。但在這裡,PyTorch實際上是:1. 將3個weight合併了,那麼這個時候的shape就是: (color{red}{3*}	extrm{hidden}, 	extrm{hidden}+	extrm{input});2. 將 h_{t-1}x_t給拆開了,拆成一個 	extrm{weight}\_	extrm{ih} (給 x_t 用)和 	extrm{weight}\_	extrm{hh} (給h_{t-1}用)。bias同理。

所以,結論是,在GRUCell層中,我們有兩個參數: 	extrm{weight}\_	extrm{ih} 的shape應該為: (3*	extrm{hidden}, 	extrm{input}) ,而	extrm{weight}\_	extrm{hh} 的shape應該為 (3*	extrm{hidden}, 	extrm{hidden})

根據PyTorch--nn.GRUCell文檔,GRUCell層的weight仍然被初始化為: U(-sqrt{k}, sqrt{k})。此處的 k=frac{1}{	extrm{hidden_size}}

# --------------------
# 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初始化了,同時看 	extrm{fan}\_	extrm{in}	extrm{fan}\_	extrm{out}

2. Tensorflow初始化

撒花,終於到2了!儘管今年已經是2019年,但tensorflow的文檔還是一言難盡,讀者經常需要dive into source code才能知道你到底想要幹嘛。與之相對應,PyTorch的文檔就友好很多。怎麼說呢,我感覺讀者,一方面,其實不想知道太底層的東西,另一方面,拜託您別封裝的那麼死,我們不是要的不是fit一下就完的東西。

儘管tf1.x已經日趨式微,不過這邊我們還是用的是tf1.x版本進行實驗。

2.0. tf.get_variable

上面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初始化!因為我們可以指定任意維張量,而只有二維纔有	extrm{fan}\_	extrm{in}	extrm{fan}\_	extrm{out}這個概念,所以我們還得再試試。

例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()

我們還是特意卡了一下 U(-0.1, 0.1) ,因為240+360正好是600。

例2. tf.get_variable,三維情況,新增的維度在前

剛才我們指定的維度是 [240, 360] ,然後我們現在看一下如果變數變成三維,會怎麼樣。

我先盲猜一把,當維度超過2的時候,可能是類似1.2裡面將其他維度視作了kernel_size。所以一邊是	extrm{fan}\_	extrm{in}	extrm{fan}\_	extrm{out}的加法。另一邊是kernel_size們的乘法。

我們先走一小步,看一下shape為: [100, 240, 360] 的情況,這裡我是卡了一個 U(-0.01, 0.01)

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=[100, 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 3D initialization")
plt.show()

嚯,好傢夥,還真讓咱們給蒙對了。還真就是真樣的。再看一下,如果把100放在最後,我們申請的shape是 [240, 360, 100] ,會怎麼樣呢?那 sqrt{k}=sqrt{frac{6}{240	imes(360+100)}}=0.0074

完全吻合!

為了保險,我們再試一個,申請一個shape為[5, 2, 2, 5, 240, 360]的變數,還是湊 U(-0.01, 0.01)的分佈,看一下結果:

好了,我覺得現在可以總結一下了:

  1. Tensorflow中,默認使用的就是Glorot Uniform進行初始化;
  2. 當申請變數dim=2時,默認兩個維度為 	extrm{fan}\_	extrm{in}	extrm{fan}\_	extrm{out}U(-sqrt{k}, sqrt{k}) ,其中 sqrt{k}=sqrt{frac{6}{	extrm{fan}\_	extrm{in}+	extrm{fan}\_	extrm{out}}}
  3. 當申請變數dim>2時,默認最後兩個維度為 	extrm{fan}\_	extrm{in}	extrm{fan}\_	extrm{out}。前面的所有維度為kernel_size,仍使用 ks 指代,我們假設有 m 個: ks[1],...,ks[m] 。那麼變數分佈仍為: U(-sqrt{k}, sqrt{k}) ,此時: sqrt{k}=sqrt{frac{6}{prod_{i=1}^{m}ks[i]cdot(	extrm{fan}\_	extrm{in}+	extrm{fan}\_	extrm{out})}}

其實還想寫一下,不過已經發現沒必要了,因為在Tensorflow裡面,所有變數的申請都使用tf.get_variable這個API,換言之,Tensorflow不會像PyTorch一樣根據不同的層,採取不同的初始化策略。所以你只要知道變數的shape,那麼按照上面的法則,你就知道它遵循一個什麼分佈了。

寫的匆忙,有問題還請指出。


推薦閱讀:
相關文章