我正在尝试为每个类别添加各种简单的指标。tf.keras.metrics.TruePositives, tf.keras.metrics.Precision ...
当最后一个Dense
层为两个或更多时,这会导致下面的崩溃。
InvalidArgumentError: 不兼容的形状: [2,128] 与 [2,64] [[{{node metrics_12/fp/LogicalAnd}}]]
如果我仅使用accuracy
作为指标,它就能工作。我很确定我遗漏了一些基本的东西。因为我在TensorFlow和深度学习方面只是一个业余爱好者。我做错了什么?我如何为每个类别获取指标(主要是真/假阳性/阴性)?(示例代码只有0,1类别,在实际应用中还有更多类别)
Colab链接: https://colab.research.google.com/drive/1aAz1pfN6ttBp8nU6rZgo8OA_Hwdseyg8
#%%from typing import List, Set, Dict, Tuple, Optional, Anyimport numpy as npimport tensorflow as tffrom tensorflow.keras.models import Sequentialfrom tensorflow.keras.layers import Dense, Dropout, BatchNormalization, CuDNNLSTM, LSTM, Flatten#%%# 为演示目的创建随机训练值。## train_x 是[# [# [0.3, 0.54 ... 0.8],# [0.4, 0.6 ... 0.55],# ...# ],# [# [0.3, 0.54 ... 0.8],# [0.4, 0.6 ... 0.55],# ...# ],# ...# ]## train_y 是train_x序列的对应分类,总是0或1# [0, 1, 0, 1, 0, ... 0]SAMPLES_CNT = 1000train_x = np.random.rand(SAMPLES_CNT,5,4)train_y = np.vectorize(lambda x: int(round(x)))(np.random.rand(SAMPLES_CNT))val_x = np.random.rand(int(SAMPLES_CNT * 0.1),5,4)val_y = np.vectorize(lambda x: int(round(x)))(np.random.rand(int(SAMPLES_CNT * 0.1)))#%%shape = Tuple[int, int]model = Sequential()model.add(LSTM(32,input_shape=train_x.shape[1:], return_sequences=True, stateful=False))model.add(Dropout(0.2))model.add(BatchNormalization())model.add(LSTM(32,input_shape=train_x.shape[1:], return_sequences=False, stateful=False))model.add(Dropout(0.2))model.add(BatchNormalization())model.add(Dense(16, activation="relu"))model.add(Dropout(0.2))model.add(Dense(2, activation="softmax"))metrics = [ 'accuracy', tf.keras.metrics.TruePositives(name='tp'), tf.keras.metrics.FalsePositives(thresholds=[0.5, 0.5], name='fp'), tf.keras.metrics.TrueNegatives(name='tn'), tf.keras.metrics.FalseNegatives(name='fn'), tf.keras.metrics.Precision(name='precision'), tf.keras.metrics.Recall(name='recall'), tf.keras.metrics.AUC(name='auc'),]model.compile( optimizer=tf.keras.optimizers.Adam(lr=0.001, decay=1e-6), loss='sparse_categorical_crossentropy', metrics=metrics)fit = model.fit( train_x, train_y, batch_size=64, epochs=2, validation_data=(val_x, val_y), shuffle=False,)for i, val in enumerate(model.metrics_names): print(model.metrics_names[i], fit.history[val][:1])
回答:
但是,如果你确实想知道如何在多类别问题中做到这一点,那么我们需要创建自定义指标。
首先,我必须说,我认为在分类问题中使用这些指标并没有太大意义(在多个类别中只有一个正确类别 – 'softmax'
+ 'categorical_crossentropy'
)。这种问题不是“二元”的,因此没有“阳性”和“阴性”,而是多个类别中的一个正确类别。
如果你把它看作是各个类别,并将每个类别视为一个二元类别,你会得到类似这样的结果:
- 每次模型正确分类一个类别时,意味着1个TP + 4个TN(注意有多少真阴性,因为有不止两种结果)
- 每次模型错误分类一个类别时,意味着1个FP + 1个FN + 3个TN
如果你将这些数字加起来,它们实际上并没有太大意义。(或许我错过了某种专门用于计算分类问题的这些指标的方法… 然而,你可以使用下面的所有内容来处理这个问题,了解上述内容)。
另一方面,你可以为多个二元类别获得良好的指标(其中每个类别独立于其他类别,并且可以有多个正确类别 – 'sigmoid'
+ 'binary_crossentropy'
)。
在这种情况下,你可以遵循两种方法:
- 按类别获取指标
- 以某种方式平均所有类别的指标(你可以在sklearn文档中看到一些平均类型的示例)
按类别指标
这些对应于sklearn文档中的'binary'
平均模式。
方案1:
将每个类别作为一个独立的模型输出,在编译时,为每个输出设置所有这些指标。TensorFlow将单独查看每个输出并无问题地计算所有内容。
方案2:
这应该作为每个类别的独立指标来完成。因此,我们可以为此创建一个考虑类别索引的包装器。
我将提供一些示例。请注意,这些示例都不能是“稀疏”的,因为可能有多个正确类别,因此,在这种情况下,地面true
数据将具有形状(samples, classes)
,就像预测的pred
值一样。
对于每个指标,你可以创建一个像这样的包装器:
#类别获取器def get_class(true, pred, index): #获取类别 true = true[:, index] pred = pred[:, index] #四舍五入pred - 你可以选择不同的阈值 pred = K.cast(K.greater(pred, 0.5), K.floatx()) return true, pred#类别包装器def some_metric_per_class(class_index): def the_actual_metric(true, pred): true, pred = get_class(class_index) return calculations return the_actual_metric
这样的包装器可以像这样使用:
metrics = [some_metric_per_class(i) for i in range(n_classes)]metrics += [some_other_metric_per_class(i) for i in range(n_classes)]model.compile(metrics = metrics, ...)
这里
现在,每个以下指标应该有自己的包装器(我在这里没有写出来,以避免不必要的重复):
def TP(true, pred): true, pred = get_class(class_index) return K.sum(true * pred)def FP(true, pred): true, pred = get_class(class_index) return K.sum(pred * (1 - true))def TN(true, pred): true, pred = get_class(class_index) return K.sum((1-true) * (1-pred))def FN(true, pred): true, pred = get_class(class_index) return K.sum((1-pred) * true)def precision(true, pred): true, pred = get_class(class_index) TP = K.sum(true * pred) TP_and_FP = K.sum(pred) return K.switch(K.equal(TP_and_FP, 0), 1, TP / TP_and_FP)def recall(true, pred): true, pred = get_class(class_index) TP = K.sum(true * pred) TP_and_FN = K.sum(true) return K.switch(K.equal(TP_and_FN, 0), 1, TP / TP_and_FN)def AUC(true, pred): true, pred = get_class(class_index) #我们希望严格的1D数组 - 不能有(batch, 1)这样的情况 true= K.flatten(true) pred = K.flatten(pred) #此批次中元素的总数 totalCount = K.shape(true)[0] #按降序排序预测值 values, indices = tf.nn.top_k(pred, k = totalCount) #根据上述预测排序地面真实值 sortedTrue = K.gather(true, indices) #获取已排序的负元素 negatives = 1 - sortedTrue #每阈值的真阳性计数 TPCurve = K.cumsum(sortedTrue) #曲线下面积 auc = K.sum(TPCurve * negatives) #将结果标准化在0和1之间 totalCount = K.cast(totalCount, K.floatx()) positiveCount = K.sum(true) negativeCount = totalCount - positiveCount totalArea = positiveCount * negativeCount return auc / totalArea
重要:精确度、召回率和AUC不会是精确值,因为Keras按批次计算指标,然后平均每个批次的结果。
平均指标
这些可能只在“精确度”和“召回率”上有意义。所以我只为这两个指标做这个处理。
这里不需要包装器,或者不需要单独的输出。true
和pred
数据与之前的示例中一样,形状为(samples, classes)
。
在这里,我们使用相同的计算,但现在我们将所有类别保持在一起,并决定如何平均它们。
def base_metrics(true, pred): #四舍五入pred - 你可以选择不同的阈值 pred = K.cast(K.greater(pred, 0.5), K.floatx()) TP = K.sum(true * pred, axis=0) TP_and_FP = K.sum(pred, axis=0) TP_and_FN = K.sum(true, axis=0) return TP, TP_and_FP, TP_and_FNdef precision_micro(true, pred): TP, TP_FP, TP_FN = base_metrics(true, pred) TP = K.sum(TP) TP_FP = K.sum(TP_FP) return K.switch(K.equal(TP_FP, 0), 1, TP / TP_FP)def precision_macro(true, pred): TP, TP_FP, TP_FN = base_metrics(true, pred) precision = K.switch(K.equal(TP_FP, 0), 1, TP / TP_FP) return K.mean(precision)
你可以对recall_micro
和recall_macro
做同样的事情,但使用TP_FN
而不是TP_FP
。