聊起初始化,大家应该都了解大名鼎鼎的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,那么按照上面的法则,你就知道它遵循一个什么分布了。

写的匆忙,有问题还请指出。


推荐阅读:
相关文章