¿Cómo definir una métrica de rendimiento personalizada en Keras?

11

Intenté definir una función métrica personalizada (F1-Score) en Keras (backend de Tensorflow) de acuerdo con lo siguiente:

def f1_score(tags, predicted):

    tags = set(tags)
    predicted = set(predicted)

    tp = len(tags & predicted)
    fp = len(predicted) - tp 
    fn = len(tags) - tp

    if tp>0:
        precision=float(tp)/(tp+fp)
        recall=float(tp)/(tp+fn)
        return 2*((precision*recall)/(precision+recall))
    else:
        return 0

Hasta ahora, todo bien, pero cuando trato de aplicarlo en la compilación del modelo:

model1.compile(loss="binary_crossentropy", optimizer=Adam(), metrics=[f1_score])

da error:

TypeError                                 Traceback (most recent call last)
<ipython-input-85-4eca4def003f> in <module>()
      5 model1.add(Dense(output_dim=10, activation="sigmoid"))
      6 
----> 7 model1.compile(loss="binary_crossentropy", optimizer=Adam(), metrics=[f1_score])
      8 
      9 h=model1.fit(X_train, Y_train, batch_size=500, nb_epoch=5, verbose=True, validation_split=0.1)

/home/buda/anaconda2/lib/python2.7/site-packages/keras/models.pyc in compile(self, optimizer, loss, metrics, sample_weight_mode, **kwargs)
    522                            metrics=metrics,
    523                            sample_weight_mode=sample_weight_mode,
--> 524                            **kwargs)
    525         self.optimizer = self.model.optimizer
    526         self.loss = self.model.loss

/home/buda/anaconda2/lib/python2.7/site-packages/keras/engine/training.pyc in compile(self, optimizer, loss, metrics, loss_weights, sample_weight_mode, **kwargs)
    664                 else:
    665                     metric_fn = metrics_module.get(metric)
--> 666                     self.metrics_tensors.append(metric_fn(y_true, y_pred))
    667                     if len(self.output_names) == 1:
    668                         self.metrics_names.append(metric_fn.__name__)

<ipython-input-84-b8a5752b6d55> in f1_score(tags, predicted)
      4     #tf.convert_to_tensor(img.eval())
      5 
----> 6     tags = set(tags)
      7     predicted = set(predicted)
      8 

/home/buda/anaconda2/lib/python2.7/site-packages/tensorflow/python/framework/ops.pyc in __iter__(self)
    493       TypeError: when invoked.
    494     """
--> 495     raise TypeError("'Tensor' object is not iterable.")
    496 
    497   def __bool__(self):

TypeError: 'Tensor' object is not iterable.

¿Cuál es el problema aquí? ¿El hecho de que mis entradas de función f1_score no son matrices de Tensorflow? Si es así, ¿dónde / cómo puedo convertirlos correctamente?

Hendrik
fuente
Hmm, el mensaje de error implica que estás obteniendo objetos tensoriales. ¡Quizás necesites la evaluación después de todo! Si es así, es probable que su error se use evalcuando quiera decireval()
Neil Slater

Respuestas:

16

Tienes que usar las funciones de backend de Keras . Desafortunadamente, no son compatibles con el &operador, por lo que debe crear una solución alternativa: generamos matrices de la dimensión batch_size x 3, donde (por ejemplo, para verdadero positivo) la primera columna es el vector de verdad fundamental, el segundo la predicción real y el tercero es tipo de columna de ayuda de etiquetas, que contiene en el caso de las verdaderas positivas solamente. Luego verificamos qué instancias son instancias positivas, se predicen como positivas y la etiqueta auxiliar también es positiva. Esos son los verdaderos positivos.

Podemos hacer este análogo con falsos positivos, falsos negativos y verdaderos negativos con algunos cálculos inversos de las etiquetas.

Su métrica f1 puede tener el siguiente aspecto:

def f1_score(y_true, y_pred):
    """
    f1 score

    :param y_true:
    :param y_pred:
    :return:
    """
    tp_3d = K.concatenate(
        [
            K.cast(y_true, 'bool'),
            K.cast(K.round(y_pred), 'bool'),
            K.cast(K.ones_like(y_pred), 'bool')
        ], axis=1
    )

    fp_3d = K.concatenate(
        [
            K.cast(K.abs(y_true - K.ones_like(y_true)), 'bool'),
            K.cast(K.round(y_pred), 'bool'),
            K.cast(K.ones_like(y_pred), 'bool')
        ], axis=1
    )

    fn_3d = K.concatenate(
        [
            K.cast(y_true, 'bool'),
            K.cast(K.abs(K.round(y_pred) - K.ones_like(y_pred)), 'bool'),
            K.cast(K.ones_like(y_pred), 'bool')
        ], axis=1
    )

    tp = K.sum(K.cast(K.all(tp_3d, axis=1), 'int32'))
    fp = K.sum(K.cast(K.all(fp_3d, axis=1), 'int32'))
    fn = K.sum(K.cast(K.all(fn_3d, axis=1), 'int32'))

    precision = tp / (tp + fp)
    recall = tp / (tp + fn)
    return 2 * ((precision * recall) / (precision + recall))

Dado que la calculadora de back-end de Keras devuelve nan para la división por cero, no necesitamos la declaración if-else para la declaración de devolución.

Editar: He encontrado una idea bastante buena para una implementación exacta. El problema con nuestro primer enfoque es que solo es "aproximado", ya que se calcula por lotes y se promedia posteriormente. También se podría calcular esto después de cada época con el keras.callbacks. Encuentre la idea aquí: https://github.com/fchollet/keras/issues/5794

Un ejemplo de implementación sería:

import keras
import numpy as np
import sklearn.metrics as sklm


class Metrics(keras.callbacks.Callback):
    def on_train_begin(self, logs={}):
        self.confusion = []
        self.precision = []
        self.recall = []
        self.f1s = []
        self.kappa = []
        self.auc = []

    def on_epoch_end(self, epoch, logs={}):
        score = np.asarray(self.model.predict(self.validation_data[0]))
        predict = np.round(np.asarray(self.model.predict(self.validation_data[0])))
        targ = self.validation_data[1]

        self.auc.append(sklm.roc_auc_score(targ, score))
        self.confusion.append(sklm.confusion_matrix(targ, predict))
        self.precision.append(sklm.precision_score(targ, predict))
        self.recall.append(sklm.recall_score(targ, predict))
        self.f1s.append(sklm.f1_score(targ, predict))
        self.kappa.append(sklm.cohen_kappa_score(targ, predict))

        return

Para hacer que la red llame a esta función, simplemente agréguela a sus devoluciones de llamada como

metrics = Metrics()
model.fit(
    train_instances.x,
    train_instances.y,
    batch_size,
    epochs,
    verbose=2,
    callbacks=[metrics],
    validation_data=(valid_instances.x, valid_instances.y),
)

Entonces simplemente puede acceder a los miembros de la metricsvariable.

pexmar
fuente
3
Gracias, esto ya ha sido realmente útil. ¿Sabes cómo incorporar las métricas personalizadas en una devolución de llamada de tensorboard para que puedan ser monitoreadas durante el entrenamiento?
N.Kaiser