使用 Keras 了解 WeightedKappaLoss

Shl*_*rtz 8 python keras tensorflow cohen-kappa ordinal-classification

我正在使用 Keras 尝试使用一系列事件来预测分数 (0-1) 的向量。

例如,X是由 3 个向量组成的序列,每个向量包含 6 个特征,而y是一个包含 3 个分数的向量:

X
[
  [1,2,3,4,5,6], <--- dummy data
  [1,2,3,4,5,6],
  [1,2,3,4,5,6]
]

y
[0.34 ,0.12 ,0.46] <--- dummy data
Run Code Online (Sandbox Code Playgroud)

我想将问题作为序数分类来解决,因此如果实际值是[0.5,0.5,0.5]预测值,[0.49,0.49,0.49]那么[0.3,0.3,0.3]. 我的原始解决方案是sigmoid在我的最后一层使用激活mse作为损失函数,因此每个输出神经元的输出范围在 0-1 之间:

def get_model(num_samples, num_features, output_size):
    opt = Adam()
    model = Sequential()
    
    model.add(LSTM(config['lstm_neurons'], activation=config['lstm_activation'], input_shape=(num_samples, num_features)))
    model.add(Dropout(config['dropout_rate']))

    for layer in config['dense_layers']:
      model.add(Dense(layer['neurons'], activation=layer['activation']))

    model.add(Dense(output_size, activation='sigmoid'))
    model.compile(loss='mse', optimizer=opt, metrics=['mae', 'mse'])

    return model
Run Code Online (Sandbox Code Playgroud)

我的目标是了解WeightedKappaLoss的用法并在我的实际数据上实现它。我创建了这个 Colab来摆弄这个想法。在 Colab 中,我的数据是一个序列形状(5000,3,3),我的目标形状(5000, 4)代表 4 个可能的类中的 1 个。

我希望模型理解它需要修剪 X 的浮点数以预测正确的 y 类:

[[3.49877793, 3.65873511, 3.20218196],
 [3.20258153, 3.7578669 , 3.83365481],
 [3.9579924 , 3.41765455, 3.89652426]], ----> y is 3 [0,0,1,0]

[[1.74290875, 1.41573056, 1.31195701],
 [1.89952004, 1.95459796, 1.93148095],
 [1.18668981, 1.98982041, 1.89025326]], ----> y is 1 [1,0,0,0]
Run Code Online (Sandbox Code Playgroud)

新型号代码:

def get_model(num_samples, num_features, output_size):
    opt = Adam(learning_rate=config['learning_rate'])
    model = Sequential()
    
    model.add(LSTM(config['lstm_neurons'], activation=config['lstm_activation'], input_shape=(num_samples, num_features)))
    model.add(Dropout(config['dropout_rate']))

    for layer in config['dense_layers']:
      model.add(Dense(layer['neurons'], activation=layer['activation']))

    model.add(Dense(output_size, activation='softmax'))
    model.compile(loss=tfa.losses.WeightedKappaLoss(num_classes=4), optimizer=opt, metrics=[tfa.metrics.CohenKappa(num_classes=4)])

    return model
Run Code Online (Sandbox Code Playgroud)

拟合模型时,我可以在 TensorBoard 上看到以下指标: 在此处输入图片说明在此处输入图片说明

我不确定以下几点,希望得到澄清:

  • 我用对了吗?
  • 在我最初的问题中,我预测了 3 个分数,而不是 Colab 示例,我只预测了 1 个。如果我使用 WeightedKappaLoss,是否意味着我需要将每个分数转换为向量100 个单热编码?
  • 有没有办法在原始浮点分数上使用 WeightedKappaLoss 而不转换为分类问题?

Mr.*_*ple 7

让我们的目标分开,两个子目标,我们走过的目的概念数学细节Weighted Kappa第一次,在那之后我们总结了几点要注意,当我们试图用WeightedKappaLoss在tensorflow

PS:如果只关心用法可以跳过理解部分


加权Kappa详解

由于加权卡帕可以看到的科恩kappa +权重,所以我们需要了解科恩kappa第一

科恩 kappa 的例子

假设我们有两个分类器(A 和 B)试图将 50 个陈述分为两类(真和假),它们在列联表中对这些陈述进行分类的方式:

         B
         True False
A True   20   5     25 statements A think is true
  False  10   15    25 statements A think is false
         30 statements B think is true
              20 statements B think is false
Run Code Online (Sandbox Code Playgroud)

现在假设我们想知道:A 和 B 的预测有多可靠?

我们可以做的只是简单地取 A 和 B 彼此一致的分类陈述的百分比,即观察到的一致的比例表示为Po,所以:

Po = (20 + 15) / 50 = 0.7
Run Code Online (Sandbox Code Playgroud)

但这是有问题的,因为 A 和 B 存在随机一致的概率,即期望概率一致的比例表示为Pe,如果我们使用观察到的百分比作为期望概率,则:

Pe = (probability statement A think is true) * (probability statement B think is true) +
     (probability statement A think is false) * (probability statement B think is false) 
   = (25 / 50) * (30 / 50) + 
     (25 / 50) * (20 / 50)
   = 0.5
Run Code Online (Sandbox Code Playgroud)

Cohen 的 kappa 系数表示K包含PoPe为我们提供关于预测 A 和 B 的可靠性的更可靠的预测:

在此处输入图片说明

K = (Po - Pe) / (1 - Pe) = 1 - (1 - Po) / (1 - Pe) = 1 - (1 - 0.7) / (1 - 0.5) = 0.4
Run Code Online (Sandbox Code Playgroud)

我们可以看到 A 和 B 越是一致(Po更高),他们因偶然而一致的越少(Pe更低),Cohen 的 kappa越“认为”结果是可靠的

现在假设 A 是陈述的标签(基本事实),然后K告诉我们 B 的预测有多可靠,即在考虑随机机会时,预测与标签的一致程度

Cohen 的 kappa 的权重

我们用m类正式定义列联表:

                                    classifier 2
                       class.1  class.2  class... class.k  Sum over row
               class.1   n11      n12      ...      n1k      n1+  
               class.2   n21      n22      ...      n2k      n2+  
classifier 1   class...  ...      ...      ...      ...      ...  
               class.k   nk1      nk2      ...      nkk      nk+  
       Sum over column   n+1      n+2      ...      n+k      N   # total sum of all table cells
Run Code Online (Sandbox Code Playgroud)

表格单元格包含交叉分类类别的计数,分别表示为niji,j分别用于行和列索引

考虑那些k序类是分开的两个分类类别,如分离1, 0为五类1, 0.75, 0.5, 0.25, 0其中有一个平稳有序的过渡,我们不能说这些类是除第一和最后一类独立的,例如very good, good, normal, bad, very bad,在very goodgood不独立,good应该更接近bad比到very bad

由于相邻的类是相互依赖的,那么为了计算与协议相关的数量,我们需要定义这个依赖项,即权重表示为Wij,它分配给列联表中的每个单元格,权重的值(在 [0, 1] 范围内)取决于关于两个班级的接近程度

现在让我们来看看PoPe公式加权卡帕

在此处输入图片说明

PoPe公式科恩kappa

在此处输入图片说明

我们可以看到,PoPe式中的科恩kappa是式中的特殊情况下的加权的Kappa,其中weight = 1分配给所有对角单元和重量= 0别处,当我们计算K使用(科恩kappa系数)PoPe加权卡伯我们也考虑相邻类之间的依赖关系考虑到

下面介绍两种常用的称重系统:

  1. 线性权重:

在此处输入图片说明

  1. 二次权重:

在此处输入图片说明

其中,|i-j|是类之间的距离,是类k的数量

加权 Kappa 损失

这种损失用于我们之前提到的一个分类器是标签的情况,这种损失的目的是使模型(另一个分类器)的预测尽可能可靠,即鼓励模型使更多的预测与标签一致,同时减少考虑相邻类之间的依赖性时的随机猜测

加权 Kappa 损失的公式由下式给出:

在此处输入图片说明

它只是采用负Cohen's kappa 系数的公式并去掉常数-1然后对其应用自然对数,其中dij = |i-j|对于线性权重dij = (|i-j|)^2对于二次权重

下面是用tensroflow编写的Weighted Kappa Loss的源码,可以看到它只是实现了上面的Weighted Kappa Loss公式:

import warnings
from typing import Optional

import tensorflow as tf
from typeguard import typechecked

from tensorflow_addons.utils.types import Number

class WeightedKappaLoss(tf.keras.losses.Loss):
    @typechecked
    def __init__(
        self,
        num_classes: int,
        weightage: Optional[str] = "quadratic",
        name: Optional[str] = "cohen_kappa_loss",
        epsilon: Optional[Number] = 1e-6,
        dtype: Optional[tf.DType] = tf.float32,
        reduction: str = tf.keras.losses.Reduction.NONE,
    ):
        super().__init__(name=name, reduction=reduction)
        warnings.warn(
            "The data type for `WeightedKappaLoss` defaults to "
            "`tf.keras.backend.floatx()`."
            "The argument `dtype` will be removed in Addons `0.12`.",
            DeprecationWarning,
        )
        if weightage not in ("linear", "quadratic"):
            raise ValueError("Unknown kappa weighting type.")

        self.weightage = weightage
        self.num_classes = num_classes
        self.epsilon = epsilon or tf.keras.backend.epsilon()
        label_vec = tf.range(num_classes, dtype=tf.keras.backend.floatx())
        self.row_label_vec = tf.reshape(label_vec, [1, num_classes])
        self.col_label_vec = tf.reshape(label_vec, [num_classes, 1])
        col_mat = tf.tile(self.col_label_vec, [1, num_classes])
        row_mat = tf.tile(self.row_label_vec, [num_classes, 1])
        if weightage == "linear":
            self.weight_mat = tf.abs(col_mat - row_mat)
        else:
            self.weight_mat = (col_mat - row_mat) ** 2

    def call(self, y_true, y_pred):
        y_true = tf.cast(y_true, dtype=self.col_label_vec.dtype)
        y_pred = tf.cast(y_pred, dtype=self.weight_mat.dtype)
        batch_size = tf.shape(y_true)[0]
        cat_labels = tf.matmul(y_true, self.col_label_vec)
        cat_label_mat = tf.tile(cat_labels, [1, self.num_classes])
        row_label_mat = tf.tile(self.row_label_vec, [batch_size, 1])
        if self.weightage == "linear":
            weight = tf.abs(cat_label_mat - row_label_mat)
        else:
            weight = (cat_label_mat - row_label_mat) ** 2
        numerator = tf.reduce_sum(weight * y_pred)
        label_dist = tf.reduce_sum(y_true, axis=0, keepdims=True)
        pred_dist = tf.reduce_sum(y_pred, axis=0, keepdims=True)
        w_pred_dist = tf.matmul(self.weight_mat, pred_dist, transpose_b=True)
        denominator = tf.reduce_sum(tf.matmul(label_dist, w_pred_dist))
        denominator /= tf.cast(batch_size, dtype=denominator.dtype)
        loss = tf.math.divide_no_nan(numerator, denominator)
        return tf.math.log(loss + self.epsilon)

    def get_config(self):
        config = {
            "num_classes": self.num_classes,
            "weightage": self.weightage,
            "epsilon": self.epsilon,
        }
        base_config = super().get_config()
        return {**base_config, **config}
Run Code Online (Sandbox Code Playgroud)

加权 Kappa 损失的使用

每当我们可以将我们的问题形成为序数分类问题时,我们都可以使用加权 Kappa 损失,即类形成平滑有序的过渡,相邻的类是相互依赖的,就像用对某些东西进行排名一样,模型的输出应该像结果very good, good, normal, bad, very badSoftmax

我们不能用加权卡帕损失,当我们试图预测分数的矢量(0-1),即使它们可以总结到1,由于权重向量的每一个元素都是不同的,这个损失不问如何通过不同的减法是价值,但是通过乘法询问数字是多少,例如:

import tensorflow as tf
from tensorflow_addons.losses import WeightedKappaLoss

y_true = tf.constant([[0.1, 0.2, 0.6, 0.1], [0.1, 0.5, 0.3, 0.1],
                      [0.8, 0.05, 0.05, 0.1], [0.01, 0.09, 0.1, 0.8]])
y_pred_0 = tf.constant([[0.1, 0.2, 0.6, 0.1], [0.1, 0.5, 0.3, 0.1],
                      [0.8, 0.05, 0.05, 0.1], [0.01, 0.09, 0.1, 0.8]])
y_pred_1 = tf.constant([[0.0, 0.1, 0.9, 0.0], [0.1, 0.5, 0.3, 0.1],
                      [0.8, 0.05, 0.05, 0.1], [0.01, 0.09, 0.1, 0.8]])

kappa_loss = WeightedKappaLoss(weightage='linear', num_classes=4)
loss_0 = kappa_loss(y_true, y_pred_0)
loss_1 = kappa_loss(y_true, y_pred_1)
print('Loss_0: {}, loss_1: {}'.format(loss_0.numpy(), loss_1.numpy()))
Run Code Online (Sandbox Code Playgroud)

输出:

# y_pred_0 equal to y_true yet loss_1 is smaller than loss_0
Loss_0: -0.7053321599960327, loss_1: -0.8015820980072021
Run Code Online (Sandbox Code Playgroud)

您在Colab 中的代码在Ordinal Classification Problems的上下文中正常工作,因为您形成的函数X->Y非常简单(X 的整数是 Y 索引 + 1),因此模型可以相当快速和准确地学习它,正如我们所看到的K(Cohen 的kappa 系数)高达1.0和加权 Kappa 损失低于-13.0(实际上通常是我们可以预期的最小)

总之,您可以使用加权 Kappa 损失,除非您可以将您的问题形成为具有单热方式标签的序数分类问题,如果您可以并尝试解决 LTR(学习排名)问题,那么您可以查看本教程实现 ListNet本教程 tensorflow_ranking以获得更好的结果,否则你不应该使用加权 Kappa 损失,如果你只能将你的问题形成为回归问题,那么你应该像你原来的解决方案一样做


参考:

维基百科上的科恩河童

R 中的加权 Kappa:对于两个序数变量

tensroflow-addons 中 WeightedKappaLoss 的源代码

tfa.losses.WeightedKappaLoss 的文档

分类变量、有序变量和数值变量之间的区别