logits:未经过normalize(未经过激活函数处理)的原始预测值,例如一个mlp将特征映射到num_target_class维的输出tensor就是logits。
probs:probabilities的简写,例如logits经过sigmoid函数,就变成了分布在0-1之间的概率值probs。
Binary Cross-Entropy Loss,简称为BCE loss,即二元交叉熵损失。
二元交叉熵损失是一种用于二分类问题的损失函数。它衡量的是模型预测的概率分布与真实标签的概率分布之间的差异。在二分类问题中,每个样本的标签只有两种可能的状态,通常表示为 0(负类)和 1(正类)。
其公式为:
L
B
C
E
=
−
1
N
∑
i
=
1
N
[
y
i
log
(
p
i
)
+
(
1
−
y
i
)
log
(
1
−
p
i
)
]
L_{BCE}=-\frac{1}{N} \sum_{i=1}^N\left[y_i \log \left(p_i\right)+\left(1-y_i\right) \log \left(1-p_i\right)\right]
LBCE=−N1i=1∑N[yilog(pi)+(1−yi)log(1−pi)]
其中:
当真实标签 y i = 1 y_i=1 yi=1 时,损失函数的第一部分 y i log ( p i ) y_i \log \left(p_i\right) yilog(pi) 起作用,第二部分为 0 。此时, 如果预测概率 p i p_i pi 接近 1 (接近真实标签 y i = 1 y_i=1 yi=1), 那么 log ( p i ) \log \left(p_i\right) log(pi) 接近 0 , 损失较小;如果 p i p_i pi 接近 0 (即模型预测错误),那么 log ( p i ) \log \left(p_i\right) log(pi) 会变得成绝对值很大的负数,导致取反后loss很大。
当真实标签 y i = 0 y_i=0 yi=0 时,损失函数的第二部分 ( 1 − y i ) log ( 1 − p i ) \left(1-y_i\right) \log \left(1-p_i\right) (1−yi)log(1−pi)起作用,第一部分为 0。此时,预测概率 p i p_i pi越接近于 0,整体loss越小。
import torch
import torch.nn.functional as F
def manual_binary_cross_entropy_with_logits(logits, targets):
# 使用 Sigmoid 函数将 logits 转换为概率
probs = torch.sigmoid(logits)
# 计算二元交叉熵损失
loss = - torch.mean(targets * torch.log(probs) + (1 - targets) * torch.log(1 - probs))
return loss
# logits和targets可以是任意shape的tensor,只要两者shape相同即可
logits = torch.tensor([0.2, -0.4, 1.2, 0.8])
targets = torch.tensor([0., 1., 1., 0.])
assert logits.shape == targets.shape
# 使用 PyTorch 的 F.binary_cross_entropy_with_logits 函数计算损失
loss_pytorch = F.binary_cross_entropy_with_logits(logits, targets)
# 使用手动实现的函数计算损失
loss_manual = manual_binary_cross_entropy_with_logits(logits, targets)
print(f'Loss (PyTorch): {loss_pytorch.item()}')
print(f'Loss (Manual): {loss_manual.item()}')
F.binary_cross_entropy的输入是probs
F.binary_cross_entropy_with_logits的输入是logits
Dice Loss 来自文章《V-Net: Fully Convolutional Neural Networks for Volumetric Medical Image Segmentation》,它能够有效地处理图像分割任务中正负样本不平衡问题。例如,当一张图片中真实分割区域占比很小,即gt_label里的正样本像素很少。这时候如果用BCE Loss,图片里的每个像素都要去进行二分类,而绝大部分的像素都是负样本,所以会存在正负样本不平衡的问题。
介绍Dice Loss前,需要先了解Dice系数。
Dice系数是一种用于评估两个集合的相似性的度量函数,取值范围在0到1之间,取值越大表示越相似。定义如下:
dice coefficient
=
2
×
∣
X
∩
Y
∣
∣
X
∣
+
∣
Y
∣
\text { dice coefficient }=\frac{2 \times|X \cap Y|}{|X|+|Y|}
dice coefficient =∣X∣+∣Y∣2×∣X∩Y∣
其中 X X X 和 Y Y Y 分别是两个样本集合, X ∩ Y X \cap Y X∩Y 表示它们交集的大小, ∣ X ∣ |X| ∣X∣ 和 ∣ Y ∣ |Y| ∣Y∣分别表示 X X X 和 Y Y Y中的样本个数。由于分子乘上了 2,Dice 系数的值在 0 到 1 之间,值越大表示相似度越高。
而对于Dice Loss,我们的训练目的是让两个样本越来越相似,所以当
X
X
X 和
Y
Y
Y越相似,loss值应该越小,而Dice系数的值在 0 到 1 之间,我们可以直接用1减去Dice系数来作为Dice Loss,这样就能达成两个样本越相似loss值越小的目的:
L
d
i
c
e
=
1
−
dice coefficient
L_{dice}= 1 - \text { dice coefficient }
Ldice=1− dice coefficient
在图像分割任务中,Dice 系数计算预测分割区域和真实分割区域之间的重叠度。上述公式中的
X
X
X 和
Y
Y
Y则分别代表预测分割区域和真实分割区域。
I
=
∑
1
N
p
i
y
i
U
=
∑
1
N
(
p
i
+
y
i
)
=
∑
1
N
p
i
+
∑
1
N
y
i
其中:
Dice Loss 为 Dice 系数的补数,即 1 减去 Dice 系数。在实际应用中,为了避免除以零的情况,通常会在分子和分母中添加一个小的平滑项
ϵ
\epsilon
ϵ。
L
dice
=
1
−
2
I
+
ε
U
+
ε
L_{\text {dice }}=1-\frac{2 I+\varepsilon}{U+\varepsilon}
Ldice =1−U+ε2I+ε
在许多实现中,将预测值 logits 和目标 targets 在计算Dice Loss的过程中除以一个缩放因子(如 1000 或 10000),通常是为了数值稳定性。当处理大型图像或大量数据时,像素点的总数可能非常大,这意味着求和操作可能产生非常大的值。这可能导致计算过程中出现数值不稳定性,尤其是在梯度下降和反向传播过程中。通过引入一个缩放因子,可以将这些值缩小到一个更合理的范围内,从而减少数值不稳定性的风险。
import torch
import torch.nn.functional as F
def dice_loss(
logits: torch.Tensor, # shape为[bs, h, w]
targets: torch.Tensor, # shape为[bs, h, w]
scale=1000,
eps=1e-6,
):
probs = logits.sigmoid()
# 只对h,w这两维求和,保留bs这一维,这样每个sample都可以得到一个dice loss
numerator = 2 * (probs / scale * targets).sum(dim=(-2, -1))
denominator = (probs / scale).sum(dim=(-2, -1)) + (targets / scale).sum(dim=(-2, -1))
loss = 1 - (numerator + eps) / (denominator + eps)
return loss
# 随机生成shape为(4, 480, 640)的预测值 logits 和目标 targets
# targets要么是 0(非分割区域),要么是 1(分割区域)
logits = torch.randn(4, 480, 640)
targets = torch.randint(0, 2, (4, 480, 640))
loss = dice_loss(logits, targets) # shape为[bs, ]
print(f"Dics Loss: {loss}")