【问题标题】:Why are gradients incorrect for categorical crossentropy?为什么分类交叉熵的梯度不正确?
【发布时间】:2020-01-17 19:34:01
【问题描述】:

在回答this question 之后,我在 tensorflow 2.0 中遇到了一些有趣但令人困惑的发现。 logits 的渐变对我来说看起来不正确。假设我们在这里有logitslabels

logits = tf.Variable([[0.8, 0.1, 0.1]], dtype=tf.float32)
labels = tf.constant([[1, 0, 0]],dtype=tf.float32)

with tf.GradientTape(persistent=True) as tape:
    loss = tf.reduce_sum(tf.keras.losses.categorical_crossentropy(labels, logits, 
                                                                  from_logits=False))
grads = tape.gradient(loss, logits)
print(grads)

由于logits已经是概率分布,所以我在损失函数中设置了from_logits=False

我认为 tensorflow 会使用loss=-\Sigma_i(p_i)\log(q_i) 来计算损失,如果我们在q_i 上推导,我们将得到-p_i/q_i 的导数。所以,预期的毕业生应该是[-1.25,0,0]。但是,张量流将返回 [-0.25,1,1]。

看了tf.categorical_crossentropy的源码后发现,即使我们设置了from_logits=False,它还是归一化了概率。这将改变最终的梯度表达式。具体来说,渐变将是-p_i/q_i+p_i/sum_j(q_j)。如果p_i=1sum_j(q_j)=1,最终的梯度将加一。这就是为什么梯度会是 -0.25,但是,我还没弄清楚为什么最后两个梯度会是 1。

为了证明所有梯度都增加了1/sum_j(q_j),我做了一个logits,它不是概率分布,仍然设置from_logits=False

logits = tf.Variable([[0.5, 0.1, 0.1]], dtype=tf.float32)
labels = tf.constant([[1, 0, 0]],dtype=tf.float32)

with tf.GradientTape(persistent=True) as tape:
    loss = tf.reduce_sum(tf.keras.losses.categorical_crossentropy(labels, logits,
                                                                  from_logits=False))
grads = tape.gradient(loss, logits)
print(grads)

tensorflow返回的grads是[-0.57142866,1.4285713,1.4285713 ],我觉得应该是[-2,0,0]

显示所有梯度都增加了1/(0.5+0.1+0.1)。对于p_i==1,渐变增加1/(0.5+0.1+0.1) 对我来说是有意义的。但是不明白为什么p_i==0,渐变还是增加了1/(0.5+0.1+0.1)

更新

感谢@OverLordGoldDragon 的善意提醒。标准化概率后,正确的梯度公式应该是-p_i/q_i+1/sum_j(q_j)。所以问题中的行为是预期的。

【问题讨论】:

  • 有趣的问题;还需要答案吗?
  • 是的,请!我还没弄明白。
  • 问题:-p_i/q_i+p_i/sum_j(q_j) 是您派生出来的,还是取自文本?问因为我实际上并不知道渐变的“公式” - 我有自己的推导,但它似乎与你的相矛盾
  • 你是对的!我又推导出来了,公式应该是-p_i/q_i+1/sum_j(q_j),也就是说每个渐变都会增加1/sum_j(q_j)。谢谢!我想当我问这个问题时,我错过了sum 而不是p 之一。

标签: python tensorflow deep-learning


【解决方案1】:

分类交叉熵很棘手,尤其是 w.r.t. one-hot 编码;在查看损失的计算方式时,假设一些预测在计算损失或梯度时被“抛弃”:

loss = f(labels * preds) = f([1, 0, 0] * preds)

为什么渐变不正确?上面可能暗示preds[1:] 无关紧要,但请注意这实际上不是preds - 它是preds_normalized,其中涉及preds 的单个元素。为了更好地了解正在发生的事情,Numpy backend 很有帮助;假设from_logits=False

losses = []
for label, pred in zip(labels, preds):
    pred_norm = pred / pred.sum(axis=-1, keepdims=True)
    losses.append(np.sum(label * -np.log(pred_norm), axis=-1, keepdims=False))

以上更完整的解释 - here。下面是我对梯度公式的推导,示例将其 Numpy 实现与tf.GradientTape 结果进行比较。要跳过繁琐的细节,请滚动到“主要想法”。


公式+推导:底部的正确性证明。

"""
grad = -y * sum(p_zeros) / (p_one * sum(pred)) + p_mask / sum(pred)

p_mask  = abs(y - 1)
p_zeros = p_mask * pred

y = label:      1D array of length N, one-hot 
p = prediction: 1D array of length N, float32 from 0 to 1
p_norm = normalized predictions
p_mask = prediction masks (see below)
"""

发生了什么? 从一个简单的例子开始,了解tf.GradientTape 在做什么:

w = tf.Variable([0.5, 0.1, 0.1])

with tf.GradientTape(persistent=True) as tape:
    f1 = w[0] + w[1]  # f = function
    f2 = w[0] / w[1]
    f3 = w[0] / (w[0] + w[1] + w[2])
print(tape.gradient(f1, w))  # [1.    1.  0.]
print(tape.gradient(f2, w))  # [10. -50.  0.]
print(tape.gradient(f3, w))  # [0.40816 -1.02040 -1.02040]

w = [w1, w2, w3]。那么:

"""
grad = [df1/dw1, df1/dw2, df1/dw3]

grad1 = [d(w1 + w2)/w1, d(w1 + w2)/w2, d(w1 + w2)/w3] = [1, 1, 0]
grad2 = [d(w1 / w2)/w1, d(w1 / w2)/w2, d(w1 + w2)/w3] = [1/w2, -w1/w2^2, 0] = [10, -50, 0]
grad3 = [(w1 + w2)/K, - w2/K, -w3/K] = [0.40816 -1.02040 -1.02040] -- K = (w1 + w2 + w3)^2
"""

换句话说,tf.GradientTape 将输入张量的 每个元素 视为一个变量。记住这一点,通过基本的tffunctions 实现分类交叉熵就足够了,然后手动推导它的导数,看看它们是否一致。这是我在底部代码中所做的,上面链接的答案中更好地解释了损失。


公式说明

上面的f3是最有见地的,因为它实际上是pred_norm;我们现在只需要添加一个自然日志,并处理两个不同的情况:y==1y==0 的毕业生;使用方便的 Wolf,可以在瞬间计算导数。给分母添加更多变量,我们可以看到如下模式:

其中p_onepred,其中label == 1p_non_one 是任何其他pred 元素,而p_zeros 是除p_one 之外的所有pred 元素。底部的代码只是简单的实现,使用紧凑的语法。


说明示例

假设label = [1, 0, 0]; pred = [.5, .1, .1]。下面是numpy_gradient,一步一步来:

p_mask == [0, 1, 1]  # effectively `label` "inverted", to exclude `p_one`
p_one  == .5         # pred where `label` == 1

## grad_zeros
p_mask / np.sum(pred) == [0, 1, 1] / (.5 + .1 + .1) = [0, 1/.7, 1/.7]

## grad_one
p_one  * np.sum(pred) == .5 * (.5 + .1 + .1) = .5 * .7 = .35
p_mask * pred         == [0, 1, 1] * [.5, .1, .1] = [0, .1, .1]
np.sum(p_mask * pred) == .2
label * np.sum(p_mask * pred) == .2 * [1, 0, 0] = [.2, 0, 0]

label * np.sum(p_mask * pred) / (p_one * np.sum(pred)) 
== [.2, 0, 0] / .35 = 0.57142854

如上所述,我们可以看到梯度有效地分为两个计算:grad_onegrad_zeros


主要思想:可以理解,这是很多细节,所以这里是主要思想:labelpred 的每个元素都会影响 grad,并且损失是使用pred_norm 计算的,而不是pred,并且标准化步骤是反向传播的。我们可以运行一点视觉来确认这一点:

labels = tf.constant([[1, 0, 0]],dtype=tf.float32)
grads = []
for i in np.linspace(0, 1, 100):
    logits = tf.Variable([[0.5, 0.1, i]], dtype=tf.float32)
    with tf.GradientTape(persistent=True) as tape:
        loss = tf.keras.losses.categorical_crossentropy(
              labels, logits, from_logits=False)  
    grads.append(tape.gradient(loss, logits))
grads = np.vstack(grads)
plt.plot(grads)

即使只有logits[2] 不同,grads[1] 也不同完全相同。上面的grad_zeros 解释很清楚,但更直观地说,分类交叉熵不关心零标签预测“有多么错误”,只是集体 - 因为它仅半直接计算来自pred[0](即pred[0] / sum(pred))的损失,它被所有其他pred标准化。所以无论pred[1] == .9pred[2] == .2还是相反,p_norm都是完全一样的。


结语:为简单起见,派生公式用于一维情况,可能不适用于 N 维 labelspreds 张量,但可以轻松推广。


Numpy 与 tf.GradientTape

def numpy_gradient(label, pred):
    p_mask = np.abs(label - 1)
    p_one  = pred[np.where(label==1)[0][0]]
    return p_mask / np.sum(pred) \
           - label * np.sum(p_mask * pred) / (p_one * np.sum(pred))

def gtape_gradient(label, pred):
    pred  = tf.Variable(pred)
    label = tf.Variable(label)

    with tf.GradientTape() as tape:
        loss = - tf.math.log(tf.reduce_sum(label * pred) / tf.reduce_sum(pred))
    return tape.gradient(loss, pred).numpy()
label = np.array([1.,   0., 0. ])
pred  = np.array([0.5, 0.1, 0.1])
print(numpy_gradient(label, pred))
print(gtape_gradient(label, pred))

# [-0.57142854  1.4285713   1.4285713 ]  <-- 100% agreement
# [-0.57142866  1.4285713   1.4285713 ]  <-- 100% agreement

【讨论】:

  • 如果一开始没有看源代码,我不会期望 pred 的规范化。干得好!
  • @zihaozhihao 是的,无论如何,输入都是以一种或另一种方式规范化的,除非阅读源代码,否则它是抽象的——我发现 Numpy 后端比原始tf 代码更容易理解;如有疑问,Numpy。好问题。
猜你喜欢
  • 2020-01-13
  • 1970-01-01
  • 1970-01-01
  • 2020-01-03
  • 2019-07-13
  • 2021-03-22
  • 2021-05-29
  • 2021-07-17
  • 2021-03-19
相关资源
最近更新 更多