zih*_*hao 5 python deep-learning tensorflow
在回答这个问题之后,我在 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)
Run Code Online (Sandbox Code Playgroud)
由于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,它不是概率分布,并设置为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)
Run Code Online (Sandbox Code Playgroud)
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)
。所以问题中的行为是预期的。
分类交叉熵是很棘手的,特别是对于 one-hot 编码;问题的产生是因为在查看损失的计算方式时,假设某些预测在计算损失或梯度时被“抛弃”:
loss = f(labels * preds) = f([1, 0, 0] * preds)
为什么梯度不正确?上面可能表明这preds[1:]
并不重要,但请注意,这实际上不是preds - 它是preds_normalized
,它涉及 的单个元素preds
。为了更好地了解正在发生的事情,Numpy 后端很有帮助;假设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))
Run Code Online (Sandbox Code Playgroud)
上面的更完整的解释 -这里。下面是我对梯度公式的推导,并通过示例将其 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)
"""
Run Code Online (Sandbox Code Playgroud)
发生了什么?从一个简单的例子开始,了解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]
Run Code Online (Sandbox Code Playgroud)
让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
"""
Run Code Online (Sandbox Code Playgroud)
换句话说,tf.GradientTape
将输入张量的每个元素视为一个变量。考虑到这一点,通过初等函数实现分类交叉熵就足够了tf
,然后手动导出它的导数,看看它们是否一致。这就是我在底部代码中所做的,上面链接的答案中更好地解释了损失。
公式解释:
f3
以上是最有洞察力的,因为它实际上是pred_norm
;我们现在需要做的就是添加一个自然对数,并处理两种不同的情况: 的梯度y==1
和 的梯度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
Run Code Online (Sandbox Code Playgroud)
根据上面的内容,我们可以看到梯度有效地分为两个计算:grad_one
、 和grad_zeros
。
主要思想:可以理解,这有很多细节,所以主要思想如下:和的每个元素都会影响,并且损失是使用, not计算的,并且标准化步骤是反向传播。我们可以运行一些视觉效果来确认这一点: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)
Run Code Online (Sandbox Code Playgroud)
虽只是不同logits[2]
,grads[1]
但变化却一模一样。从上面的解释很清楚grad_zeros
,但更直观的是,分类交叉熵并不关心零标签预测单独“有多错误”,只关心集体-因为它只是半直接计算来自pred[0]
(ie 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()
Run Code Online (Sandbox Code Playgroud)
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
Run Code Online (Sandbox Code Playgroud)
归档时间: |
|
查看次数: |
453 次 |
最近记录: |