python - 为什么分类交叉熵的梯度不正确?

标签 python tensorflow deep-learning

回答后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]。但是,tensorflow 将返回 [-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,不是prob分布,设置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) .所以问题中的行为是预期的。

最佳答案

分类交叉熵很棘手,尤其是 w.r.t.一键编码;在查看如何计算损失时,假设某些预测在计算损失或梯度时被“抛弃”,就会出现问题:
loss = f(labels * preds) = f([1, 0, 0] * preds)
为什么梯度不正确? 以上可能提示preds[1:]没关系,但请注意,这实际上不是预测 - 它是 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将输入张量的每个元素与其微分视为一个变量。考虑到这一点,通过基本 tf 实现分类交叉熵就足够了。函数然后手工推导它的导数,看看它们是否一致。这是我在底部代码中所做的,上面链接的答案中更好地解释了损失。

公式说明 :
f3以上是最有见地的,因为它实际上是pred_norm ;我们现在需要的只是添加一个自然对数,并处理两种不同的情况: y==1 的 grads | ,并为 y==0 ;使用方便的 Wolf,可以快速计算导数。向分母添加更多变量,我们可以看到以下模式:
  • d(loss)/d(p_one) = p_zeros / (p_one * sum(pred))
  • d(loss)/d(p_non_one) = -1 / sum(pred)

  • 哪里p_onepred哪里label == 1 , p_non_one是其他任何 pred元素,和 p_zeros都是pred元素除外 p_one .底部的代码只是一个简单的实现,使用紧凑的语法。

    说明示例 :

    假设 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_one , 和 grad_zeros .

    主要思想 :可以理解,这是很多细节,所以这里的主要思想是:label 的每个元素和 pred影响 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
    

    关于python - 为什么分类交叉熵的梯度不正确?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/57965732/

    相关文章:

    python - Numpy:从N个离散概率分布中的每一个中随机选择1个样本

    python - 从 Pandas 数据框转换为 TensorFlow 张量对象

    python - Fasttext .vec 和 .bin 文件之间的区别

    python - 用 pandas 读取 Excel XML .xls 文件

    javascript - Python:setExtreme 后从 Highcharts 读取数据

    python - 如何指定一系列 unicode 字符

    Tensorflow 未在 jupyter notebook 的 GPU 上运行

    python - 方法对象在神经网络代码中不可下标

    machine-learning - 神经网络模型不学习?

    deep-learning - torch.utils.data.random_split() 没有分割数据