這是最近的第二篇,目前只寫了交叉熵,以後有時間會加上別的。

交叉熵(CrossEntropy)

  • nn.CrossEntropyLoss
  • nn.BCELoss
  • nn.BCEWithLogitsLoss

n_classes > 1

這應該是與分類相關最常見的一個損失函数,在目標檢測中對每個框的類別分数,和語義分割中對每個像素點的類別置信度訓練時,都可以使用這個損失函数。語義分割中的每一個像素就等效於目標檢測中的一個框,和圖像分類中的一整張圖。

在 PyTorch 中,交叉熵在 nn.CrossEntropyLoss,它就是將 nn.LogSoftmaxnn.NLLLoss 兩個合在一起。

對於 LogSoftmax,也就是在 softmax 的值上求了一个對数:

其中 j 是所有的類別,總數為 n_classes。

但是使用 LogSoftmax 比分成兩步数值上會更穩定:由於指數的存在,當輸入值 xi 過小時,在 softmax 時可能向下溢出而变為 0(在 log 之後就會是無限大);過大時,指数計算可能會向上溢出。而計算 softmax 的 log 時可對分子分母同時除以(最)大的指数值:

這樣就避免了上述問題,同時可以看到計算速度會加快不少。對輸出的每個值都(在類別通道上)完成上述計算之後,NLLLoss (negative log likelihood loss) 就是對每一個 box,取出其 GT 對應的通道上的 LogSoftmax(xi) 值,取相反数,最後所有取出的值相加。

class NLLLoss() 在構造時有三個參数,一個是 weight,表示對某一個 class,取出來值取相反数後,乘上一個權重;另一個是 ignore_index,表示真實標籤中需要忽略的值,一般圖像中邊界和無法辨別的區域會用一個負值標記;reduction="mean" 表示對所有 dimension 求解之後,求和並除以 batch * prod(dimension),而 "sum" 表示求和後直接輸出,"none" 表示直接輸出数組 shape = (batch, d1, ..., dk)

下面測試一下,假設是圖像分類 batch = 3, n_classes = 4,那麼 model output logits 應是一個 (3, 4) 的 tensor,而 GT 應該是 (3,) tensor(其他 task 只是有更多 dimension N C d1 ... dk

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
prediction_logtis = torch.rand(3, 4)  # (batch, C)
>>> tensor(
[[0.2870, 0.2232, 0.6423, 0.6035],
[0.4180, 0.9434, 0.6034, 0.5521],
[0.0026, 0.4993, 0.7166, 0.2258]]
)

log_sm = F.log_softmax(prediction_logtis, dim=-1)
>>> tensor(
[[-1.5554, -1.6192, -1.2001, -1.2389],
[-1.6171, -1.0916, -1.4317, -1.4830],
[-1.7810, -1.2842, -1.0669, -1.5578]]
)

math.log(math.exp(0.2870) / (math.exp(0.2870)+math.exp(0.2232)+math.exp(0.6423)+math.exp(0.6035)))
>>> -1.5554288144704078

target = torch.tensor([0, 2, 1]) # (batch, )
nll_loss = torch.nn.NLLLoss(reduction="mean")
nll_loss(log_sm, target)
>>> tensor(1.4238)

手動計算一下 loss:(1.5554 + 1.4317 + 1.2842) / 3 = 1.4238

這種是 label 為 long int 的形式,在 tensorflow 中為 v1: sparse_softmax_cross_entropy_with_logits v2: tf.keras.losses.SparseCategoricalCrossentropy

1
2
3
4
5
6
7
8
9
10
11
tf_pred = prediction_logtis.detach().numpy()
tf_target = target.detach().numpy()

# tfv1
tf.compat.v1.nn.sparse_softmax_cross_entropy_with_logits(labels=tf_target, logits=tf_pred)
>>> <tf.Tensor: shape=(3,), dtype=float32, numpy=array([1.5553646, 1.4316716, 1.2842305], dtype=float32)>

# tfv2
scce = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)
scce(tf_target, tf_pred)
>>> <tf.Tensor: shape=(), dtype=float32, numpy=1.4237556>

此外 tf 還允許一種 one hot 形式的 label,v1: softmax_cross_entropy_with_logits_v2 v2: tf.keras.losses.CategoricalCrossentropy,例如上面的 label 可以改為

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# torch
target_onehot = [
[1, 0, 0, 0],
[0, 0, 1, 0],
[0, 1, 0, 0],
] # (minibatch, C)
nll_loss_onehot = -(torch.tensor(target_onehot) * log_sm).sum() / prediction.shape[0]
>>> tensor(1.4238)

# tfv1
tf_nll_loss_onehot = tf.compat.v1.nn.softmax_cross_entropy_with_logits_v2(target_onehot, tf_pred)
>>> <tf.Tensor: shape=(3,), dtype=float32, numpy=array([1.5553646, 1.4316716, 1.2842305], dtype=float32)>

tf.math.reduce_mean(tf_nll_loss_onehot)
>>> <tf.Tensor: shape=(), dtype=float32, numpy=1.4237556>

# tfv2
tf_cce = tf.keras.losses.CategoricalCrossentropy(from_logits=True)
tf_cce(target_onehot, tf_pred)
>>> <tf.Tensor: shape=(), dtype=float32, numpy=1.4237556>

對於 one-hot 的形式,還可以改為使用 soft label,表示其概率上不獨立,即 label:0 -> [1, 0, 0] -> [0.9, 0.09, 0.01],tensorflow 可以直接使用,pytorch 需要自定義一個,即上述手動的乘法。我發現 mmcls/soft_cross_entropy 有一個現成的自定義 CrossEntropyLoss 實現,和上面一回事。


對於 nn.CrossEntropyLoss,如前所述,就是把兩步合在了一起,所以它的公式如下:

它的參数也與 NLLLoss 相同,這裡以一個 Batch Size = 2, classes = 3, size = (3, 3) 的輸出為例,即語義分割任務,看看兩者是不是相等的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
prediction = torch.rand(2, 3, 3, 3)
# 語義分割 GT 需要 NHW,int64 格式,shape = (B, d1, d2)
target = torch.tensor([
[[0,2,1],[0,2,1],[0,2,1]],
[[0,2,1],[0,2,1],[0,2,1]]
])

log_sm = F.log_softmax(prediction, dim=1)
nlll = nn.NLLLoss(reduction="sum")
print(nlll(log_sm, target) / 18)
>>> tensor(1.1208)

cel = nn.CrossEntropyLoss(reduction="mean")
print(cel(prediction, target))
>>> tensor(1.1208)

可以看到二者結果完全相同。

n_class = 1

交叉熵還有一個二元(Binary)形式的 nn.BCELoss,它要求 GT 只能是 01 兩個值,比如 Faster R-CNN 中的 RPN 模塊的訓練就可以使用它,只有兩類後,公式簡化如下:

可以使用 torch.nn.functional.binary_cross_entropy,與 sklearn.metrics.log_loss 完全相同。

sigmoid/softmax in binary

The sigmoid function is used for the two-class logistic regression, whereas the softmax function is used for the multiclass logistic regression.

對於 sigmoid,最後輸出一個值,輸入是 x,參數是 θ,那麼 sigmoid 的輸出為

p(y = 0|x) = 1 - p(y = 1|x),所以

對於 softmax,最後的輸出是兩個值,輸入是 x,兩個輸出的參數分別是 θ_n,那麼 softmax 的輸出為

其中 ,二分類的情況下,softmax 退化為 sigmoid。nn.BCEWithLogitsLoss 將 sigmoid 激活和 BCELoss 結合,同樣更數值穩定。