Мультикласс sparse_categorical_crossentropy TruePositives metri c Несовместимые формы: [2 128] против [2,64] - PullRequest
2 голосов
/ 20 февраля 2020

Я пытаюсь добавить различные простые метрики для каждого класса. tf.keras.metrics.TruePositives, tf.keras.metrics.Precision ... Это приводит к cra sh ниже, когда последний Dense слой равен двум или более.

InvalidArgumentError: Incompatible shapes: [2,128] vs. [2,64]
     [[{{node metrics_12/fp/LogicalAnd}}]]

Это работает, если я использую только accuracy в качестве метри c. Я почти уверен, что упускаю что-то фундаментальное. Поскольку я всего лишь дилетант, когда дело доходит до TensorFlow и Deep Learning. Что я делаю неправильно? Как получить метрики для каждого класса (в основном, истинные / ложные положительные / отрицательные стороны)? (пример кода содержит только 0,1 класса, в реальном приложении их больше)

Ссылка на Colab: https://colab.research.google.com/drive/1aAz1pfN6ttBp8nU6rZgo8OA_Hwdseyg8

#%%

from typing import List, Set, Dict, Tuple, Optional, Any
import numpy as np
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Dropout, BatchNormalization, CuDNNLSTM, LSTM, Flatten

#%%

# Create random training values for demo purposes.
#
# train_x is [
#   [
#        [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 is corresponding classification of train_x sequences, always 0 or 1
# [0, 1, 0, 1, 0, ... 0]

SAMPLES_CNT = 1000

train_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])

Ответы [ 2 ]

2 голосов
/ 27 февраля 2020

Но если вы хотите знать, как это сделать для задач мультикласса, то нам нужно создать собственные метрики.

Во-первых, я должен сказать, что считаю, что нет смысла иметь эти метрики в категориальной задаче (среди многих только один правильный класс - 'softmax' + 'categorical_crossentropy'). Такая проблема не является «бинарной», так что на самом деле нет «положительной» и «отрицательной», а одна правильная среди многих.

Если вы рассматриваете его как отдельные классы и рассматриваете каждый из них как двоичный класс, вы получите что-то вроде:

  • Каждый раз, когда модель получает правильный класс, это означает 1 TP + 4 TN (обратите внимание, сколько истинных негативов есть, потому что есть больше, чем просто два результата)
  • Каждый раз, когда модель получает неправильный класс, это означает 1 FP + 1 FN + 3 TN

Если сложить эти цифры, они просто не имеют особого смысла. (Возможно, мне не хватает какого-то специального метода для их вычисления для категориальной задачи ... и все же вы можете использовать все, что ниже, для этого, зная выше).

Теперь, с другой стороны, Вы можете получить хорошие метрики для нескольких двоичных классов (где каждый класс независим от других, и более одного класса может быть правильным - 'sigmoid' + 'binary_crossentropy').

В этом случае вы можете следовать двум подходам:

  • Получить метрики "для класса"
  • Как-то усреднить метрики всех классов (вы можете увидеть некоторые типы средних значений здесь в документации sklearn )

Метрики для класса

Они соответствуют среднему режиму 'binary' в документации sklearn.

Альтернатива 1:

У каждого класса в качестве отдельного выхода модели при компиляции установите все эти метрики для каждого из выходов. Tensorflow будет видеть каждый вывод в отдельности и вычислять все без проблем.

Альтернатива 2:

Это должно быть сделано в виде отдельного показателя c для каждого класса. Таким образом, мы можем создать оболочку для этого, учитывая индекс класса.

Я приведу несколько примеров. Обратите внимание, что ни один из них не может быть «разреженным», поскольку более одного класса может быть правильным, поэтому в этом случае наземные данные true будут иметь форму (samples, classes), как и прогнозируемые значения pred.

Для каждого метри c вы создаете обертку, подобную этой:

#class getter
def get_class(true, pred, index):
    #get the class
    true = true[:, index]
    pred = pred[:, index]

    #round pred - you can choose different thresholds
    pred = K.cast(K.greater(pred, 0.5), K.floatx())

    return true, pred

#class wrapper
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)

     #We want strictly 1D arrays - cannot have (batch, 1), for instance
    true= K.flatten(true)
    pred = K.flatten(pred)

        #total number of elements in this batch
    totalCount = K.shape(true)[0]

        #sorting the prediction values in descending order
    values, indices = tf.nn.top_k(pred, k = totalCount)   
        #sorting the ground truth values based on the predictions above         
    sortedTrue = K.gather(true, indices)

        #getting the ground negative elements (already sorted above)
    negatives = 1 - sortedTrue

        #the true positive count per threshold
    TPCurve = K.cumsum(sortedTrue)

        #area under the curve
    auc = K.sum(TPCurve * negatives)

       #normalizing the result between 0 and 1
    totalCount = K.cast(totalCount, K.floatx())
    positiveCount = K.sum(true)
    negativeCount = totalCount - positiveCount
    totalArea = positiveCount * negativeCount
    return  auc / totalArea

Объяснение о AU C

Важно : точность, отзыв и AU C не будут точными значениями, поскольку Keras рассчитывает показатели по партиям, а затем усредняет результаты для каждого пакета.

Усредненные метрики

Они, вероятно, имеют смысл только с "точностью" и "отзывом". Так что я делаю это для этих двоих.

Здесь нет необходимости ни в обертках, ни в отдельных выходах. Данные true и pred такие же, как в предыдущих примерах, с формой (samples, classes).

Здесь мы используем одни и те же вычисления, но теперь мы держим все классы вместе и решаем, как их усреднить.

def base_metrics(true, pred):
    #round pred - you can choose different thresholds
    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_FN

def 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.

1 голос
/ 20 февраля 2020

Некоторые из этих метрик должны работать только с одним классом.

Вам нужно Dense(1, activation='sigmoid') и 'binary_crossentropy'.

Обратите внимание, что эти две проблемы абсолютно одинаковы:

  • Dense(1, activation='sigmoid'), с loss='binary_crossentropy'
  • Dense(2, activation='softmax'), с loss='sparse_categorical_crossentropy'
...