回答后this question ,我在 tensorflow 2.0 中遇到了一些有趣但令人困惑的发现。 logits
的梯度对我来说看起来不正确。假设我们有 logits
和 labels
这里。
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=1
和 sum_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_one
是 pred
哪里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] == .9
和 pred[2] == .2
反之亦然,p_norm
完全一样。结语 :为简单起见,派生公式适用于一维情况,可能不适用于 N 维
labels
和 preds
张量,但可以很容易地推广。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/