谁偷走了我的梯度?模型参数初始化和损失函数之间的小坑

 

这篇博客记录了我在工作中的遇到的一个小坑,主要是关于:为什么我的深度模型训练不起来?loss为啥不降?指标为啥不变?到底是谁偷走了我的梯度?

这篇博客记录了我在工作中的遇到的一个小坑,主要是关于:为什么我的深度模型训练不起来?loss为啥不降?指标为啥不变?到底是谁偷走了我的梯度?

其实这个坑在 debug 之后回想一下细节,不得不感慨自己有些愚钝,又或者说还是自己的基础不够扎实,脑子里缺乏一些条件反射。当然,这也和我在 coding 前没有仔细思考有关系。

问题

Anyway,我遇到的问题是训练开始后模型的 loss 不降,对应的 AUC 指标也不变始终为 0.5,并且如下图 1,查看模型的各层参数也都不变为,权值恒为 0。 关于我的任务简单描述下:训练数据是两个视频(video_a,video_b)目标是判断两个视频的相似性,通过 DNN 层后对两个 video 的表示后计算内积结果,并作为交叉熵损失函数的 logits。

我使用的特征都是一些统计特征包含 categorical 和 real-value 特征,使用的类别特征和实值特征全部都经过了 embedding 处理,我的模型就是个简单的三层全连接 DNN,模型的 embedding 层全 0 初始化,DNN 参数层随机初始化,DNN 的 bias 全 0 初始化。可以保证数据集、模型的结构设计和训练过程无误。那么,为什么我的模型训练不起来呢?

loss-and-metrics

Fig. 1. 模型训练的 loss 和 auc 变化情况

探寻原因

可能经验丰富的人和脑子灵敏的人(是的,我就是脑子不灵敏的那个)已经知道了我的问题,那么针对那些暂时没想通的人,我们来看下一个 easy 案例,针对我的 case,我简化了一个 tiny 模型,输入数据只有一个 pair 对(video_a, video_b),代码里实际上是 video_a 和 video_b 经过 embedding 后的值。与此同时,我的模型也精简为一个简单的单层全连接,bias 初始化为 0,那么我运行如下的代码会发生什么呢?想想 ana 会怎么做

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import tensorflow as tf
import numpy as np

a = tf.Variable(initial_value=[[0.,0.,0.,0.,0.,0.,0.,0.]])
b = tf.Variable(initial_value=[[0.,0.,0.,0.,0.,0.,0.,0.]])

label = tf.constant([[0.]])

W = tf.Variable(initial_value=np.random.rand(8,4), dtype=tf.float32)
B = tf.Variable(initial_value=np.zeros((1,4)), dtype=tf.float32)

vec_a = tf.matmul(a, W) + B
vec_b = tf.matmul(b, W) + B

cos = tf.multiply(vec_a, vec_b)

loss = tf.reduce_mean(
          tf.nn.sigmoid_cross_entropy_with_logits(
              labels=label,
              logits=tf.reduce_sum(cos, keep_dims=True)
          )
      )

with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())
    print("cos: ", sess.run(cos))
    print("loss: ", sess.run(loss))
    print("gradient of loss/cos: ", sess.run(tf.gradients(loss, cos)))
    print("gradient of cos/W: ", sess.run(tf.gradients(cos, W)))
    print("gradient of loss/a: ", sess.run(tf.gradients(cos, a)))

不卖关子了,上述代码是无法正常训练的,下面的是输出结果。

1
2
3
4
5
6
7
8
9
10
11
12
cos:  [[0. 0. 0. 0.]]
loss:  0.6931472
gradient of loss/cos:  [array([[0.5, 0.5, 0.5, 0.5]], dtype=float32)]
gradient of cos/W:  [array([[0., 0., 0., 0.],
       [0., 0., 0., 0.],
       [0., 0., 0., 0.],
       [0., 0., 0., 0.],
       [0., 0., 0., 0.],
       [0., 0., 0., 0.],
       [0., 0., 0., 0.],
       [0., 0., 0., 0.]], dtype=float32)]
gradient of cos/a:  [array([[0., 0., 0., 0., 0., 0., 0., 0.]], dtype=float32)]

至于原因嘛,看了输出结果想必就很清楚了,模型的梯度在 cos 之后就消失了,为什么会消失?这是因为:

\[cos = vec_a * vec_b = (a * W + B)(b * W + B) = ab \cdot W^2 + (aB + bB) \cdot W + B^2\] \[\frac{\partial{cos}}{\mathrm{d}W} = 2abW + aB + bB\] \[\frac{\partial{cos}}{\mathrm{d}a} = b\cdot W^2 + BW\]

而由于 embedding 全 0 初始化,所以 $a$ 和 $b$ 初始值都为 0,那么无论原始数据是什么,对于 W 来说梯度都为 0,同样可以看出对于 embedding $a$ 来说,其梯度在 $B$ 初始化为 0 的时候同样为 0,因此 embedding $a$ 的梯度也为 0,那么模型自然是始终无变化,loss 和 AUC 也自然不变。如果 $B$ 初始化的时候不为 0,我们随机初始化 $B$ 结果如下,自然也就可以正常训练了(当然,第一次计算 $W$ 的梯度肯定也还是 0)。

1
2
3
4
5
6
7
8
9
10
11
12
cos:  [[0.8982575  0.20215534 0.39968604 0.5663532 ]]
loss:  2.1856866
gradient of loss/cos:  [array([[0.88759947, 0.88759947, 0.88759947, 0.88759947]], dtype=float32)]
gradient of cos/W:  [array([[0., 0., 0., 0.],
       [0., 0., 0., 0.],
       [0., 0., 0., 0.],
       [0., 0., 0., 0.],
       [0., 0., 0., 0.],
       [0., 0., 0., 0.],
       [0., 0., 0., 0.],
       [0., 0., 0., 0.]], dtype=float32)]
gradient of cos/a:  [array([[1.4191248, 1.7045877, 1.1545454, 1.0757841, 1.7959626, 1.9317265, 1.3685277, 1.88685  ]], dtype=float32)]

发散一下

是不是感觉挺 “easy” 的 = =。但是你不去静下心的想,很容易忽略这个问题,你可能要问为什么会全 0 初始化,这是因为我们的模型的特征是不断迭代的,为了防止新加入的特征对模型影响过大,我们会对新的 embedding 特征进行全 0 初始化,但是当你 train from new 的时候如果忽略掉了这个初始化的细节,emmmm,结果就是像我一样在这为了个愚蠢的 bug 写 blog。(逃

留个课后小作业,如果我用 video 组成三元组并使用 triplet_loss 来训练模型,一个简化的代码如下,仍然使用 embedding 全 0 初始化,结果会如何?以及为什么会出现这个结果?思考清楚你就会发现掌握原理还是重要的,不能当个调包侠啊!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import tensorflow as tf
import numpy as np

a = tf.Variable(initial_value=[[0.,0.,0.,0.,0.,0.,0.,0.]])
b = tf.Variable(initial_value=[[0.,0.,0.,0.,0.,0.,0.,0.]])
c = tf.Variable(initial_value=[[0.,0.,0.,0.,0.,0.,0.,0.]])

label = tf.constant([[1.]])

W = tf.Variable(initial_value=np.random.rand(8,4), dtype=tf.float32)
B = tf.Variable(initial_value=np.zeros((1,4)), dtype=tf.float32)

vec_a = tf.matmul(a, W) + B
vec_b = tf.matmul(b, W) + B
vec_c = tf.matmul(c, W) + B

dist_rp = tf.square(vec_a - vec_b)
dist_rn = tf.square(vec_a - vec_c)

loss = tf.reduce_mean(
          tf.maximum(0.0, 0.314 + dist_rp - dist_rn)
      )

with tf.Session() as sess:
    sess.run(tf.global_variables_initializer())
    print("dist_rp: ", sess.run(dist_rp))
    print("loss: ", sess.run(loss))
    print("gradient of loss/dist_rp: ", sess.run(tf.gradients(loss, dist_rp)))
    print("gradient of loss/W: ", sess.run(tf.gradients(loss, W)))
    print("gradient of loss/a: ", sess.run(tf.gradients(loss, a)))