在 Tensorflow/Keras 中 Dice Loss 的正确实现

Correct Implementation of Dice Loss in Tensorflow / Keras

我一直在尝试使用 Region Based: Dice Loss but there have been a lot of variations on the internet to a varying degree that I could not find two identical implementations. The problem is that all of these produce varying results. Below are the implementations that I found. Some uses smoothing factor which the authors in this paper 进行试验,调用了 epsilon,有些人在分子和分母中都使用了它,一个实现使用了 Gamma 等等。

有人可以帮我正确实施吗?

import tensorflow as tf
import tensorflow.keras.backend as K
import numpy as np

def dice_loss1(y_true, y_pred, smooth=1e-6):
    '''
    https://www.kaggle.com/code/bigironsphere/loss-function-library-keras-pytorch/notebook
    '''
    y_pred = tf.convert_to_tensor(y_pred)
    y_true = tf.cast(y_true, y_pred.dtype)
    smooth = tf.cast(smooth, y_pred.dtype)
    
    y_pred = K.flatten(y_pred)
    y_true = K.flatten(y_true)
    
    intersection = K.sum(K.dot(y_true, y_pred))    
    dice_coef = (2*intersection + smooth) / (K.sum(y_true) + K.sum(y_pred) + smooth)
    dice_loss = 1-dice_coef
    return dice_loss
    

def dice_loss2(y_true, y_pred, smooth=1e-6): # Only Smooth
    """
    https://gist.github.com/wassname/7793e2058c5c9dacb5212c0ac0b18a8a
    """
    y_pred = tf.convert_to_tensor(y_pred)
    y_true = tf.cast(y_true, y_pred.dtype)
    smooth = tf.cast(smooth, y_pred.dtype)
    
    intersection = K.sum(K.abs(y_true * y_pred), axis=-1)
    dice_coef  = (2. * intersection + smooth) / (K.sum(K.square(y_true),-1) + K.sum(K.square(y_pred),-1) + smooth)
    return 1- dice_coef


def dice_loss3(y_true, y_pred): # No gamma, no smooth
    '''
    https://lars76.github.io/2018/09/27/loss-functions-for-segmentation.html
    '''
    y_pred = tf.convert_to_tensor(y_pred)
    y_true = tf.cast(y_true, y_pred.dtype)
    
    y_pred = tf.math.sigmoid(y_pred)
    numerator = 2 * tf.reduce_sum(y_true * y_pred)
    denominator = tf.reduce_sum(y_true + y_pred)

    return 1 - numerator / denominator


def dice_loss4(y_true, y_pred, smooth=1e-6, gama=1): # Gama + Smooth is used
    '''
    https://dev.to/_aadidev/3-common-loss-functions-for-image-segmentation-545o
    '''
    y_pred = tf.convert_to_tensor(y_pred)
    y_true = tf.cast(y_true, y_pred.dtype)
    smooth = tf.cast(smooth, y_pred.dtype)
    gama = tf.cast(gama, y_pred.dtype)

    nominator = 2 * tf.reduce_sum(tf.multiply(y_pred, y_true)) + smooth
    denominator = tf.reduce_sum(y_pred ** gama) + tf.reduce_sum(y_true ** gama) + smooth

    result = 1 - tf.divide(nominator, denominator)
    return result

y_true = np.array([[0,0,1,0],
                   [0,0,1,0],
                   [0,0,1.,0.]])

y_pred = np.array([[0,0,0.9,0],
                   [0,0,0.1,0],
                   [1,1,0.1,1.]])

# print(dice_loss1(y_true, y_pred)) # Gives you error in K.dot()
print(dice_loss2(y_true, y_pred))
print(dice_loss3(y_true, y_pred)) # provides array of values
print(dice_loss4(y_true, y_pred))

我使用了 brain tumor segmentation 的骰子损失的变体。我用于此类结果的骰子系数的实现是:

def dice_coef(y_true, y_pred, smooth=100):        
    y_true_f = K.flatten(y_true)
    y_pred_f = K.flatten(y_pred)
    intersection = K.sum(y_true_f * y_pred_f)
    dice = (2. * intersection + smooth) / (K.sum(y_true_f) + K.sum(y_pred_f) + smooth)
    return dice

为了让它成为一个损失,需要把它做成我们想要最小化的函数。这可以通过将其设为负数来实现:

def dice_coef_loss(y_true, y_pred):
    return -dice_coef(y_true, y_pred)

或从 1 中减去:

def dice_coef_loss(y_true, y_pred):
    return 1 - dice_coef(y_true, y_pred)

或应用一些其他函数然后求反 - 例如,取负对数(这可以平滑梯度):

def dice_coef_loss(y_true, y_pred):
    return -K.log(dice_coef(y_true, y_pred))

变量 smooth 代表您在其他具有各种名称(smoothingepsilon 等)的实现中的观察结果。为清楚起见,此平滑变量的存在是为了处理地面实况具有非常少的白色(或没有)白色像素的情况(假设白色像素属于 class 或对象的边界,具体取决于您的实现)。

如果smooth设置得太低,当ground truth只有很少到0个白色像素而预测图像有一些non-zero个白色像素时,模型会受到更严重的惩罚。将 smooth 设置得更高意味着如果在 ground truth 具有 none 时预测图像具有少量白色像素,则损失值将更低。不过,根据模型需要的激进程度,较低的值可能更好。

这是一个说明性的例子:

import numpy as np
import tensorflow as tf
from tensorflow.keras import backend as K


def dice_coef(y_true, y_pred, smooth):
    y_true_f = K.flatten(y_true)
    y_pred_f = K.flatten(y_pred)
    intersection = K.sum(y_true_f * y_pred_f)
    dice = (2. * intersection + smooth) / (K.sum(y_true_f) + K.sum(y_pred_f) + smooth)
    return dice


def dice_coef_loss(y_true, y_pred, smooth):
    return 1 - dice_coef(y_true, y_pred, smooth)


if __name__ == '__main__':
    smooth = 10e-6
    y_pred = np.zeros((1, 128, 128))
    # one pixel is set to 1
    y_pred[0, 0, 0] = 1
    y_pred = tf.convert_to_tensor(y_pred, dtype=tf.float32)
    y_true = tf.zeros((1, 128, 128), dtype=tf.float32)
    print(dice_coef(y_true, y_pred, smooth=smooth))
    print(dice_coef_loss(y_true, y_pred, smooth=smooth))

将打印出:

tf.Tensor(9.9999e-06, shape=(), dtype=float32)
tf.Tensor(0.99999, shape=(), dtype=float32)

但是如果 smooth 设置为 100:

tf.Tensor(0.990099, shape=(), dtype=float32)
tf.Tensor(0.009900987, shape=(), dtype=float32)

显示损失减少到 0.009 而不是 0.99。

为了完整起见,如果您有多个分割通道(B X W X H X K,其中 B 是批量大小,WH 是图像的尺寸,和 K 是不同的细分通道),同样的概念适用,但可以按如下方式实现:

def dice_coef_multilabel(y_true, y_pred, M, smooth):
    dice = 0
    for index in range(M):
        dice += dice_coef(y_true[:,:,:,index], y_pred[:,:,:,index], smooth)
    return dice

dice_coef一样,可以通过求反或减法转化为损失函数。 smooth 也可以按频道进行调整,如果您提供列表或其他序列(例如 smooth_list):

def dice_coef_multilabel(y_true, y_pred, M, smooth_list):
    dice = 0
    for index in range(M):
        dice += dice_coef(y_true[:,:,:,index], y_pred[:,:,:,index], smooth_list[index])
    return dice