29

I'm fitting a train_generator and by means of a custom callback I want to compute custom metrics on my validation_generator. How can I access params validation_steps and validation_data within a custom callback? It’s not in self.params, can’t find it in self.model either. Here's what I'd like to do. Any different approach'd be welcomed.

model.fit_generator(generator=train_generator,
                    steps_per_epoch=steps_per_epoch,
                    epochs=epochs,
                    validation_data=validation_generator,
                    validation_steps=validation_steps,
                    callbacks=[CustomMetrics()])


class CustomMetrics(keras.callbacks.Callback):

    def on_epoch_end(self, batch, logs={}):        
        for i in validation_steps:
             # features, labels = next(validation_data)
             # compute custom metric: f(features, labels) 
        return

keras: 2.1.1

Update

I managed to pass my validation data to a custom callback's constructor. However, this results in an annoying "The kernel appears to have died. It will restart automatically." message. I doubt if this is the right way to do it. Any suggestion?

class CustomMetrics(keras.callbacks.Callback):

    def __init__(self, validation_generator, validation_steps):
        self.validation_generator = validation_generator
        self.validation_steps = validation_steps


    def on_epoch_end(self, batch, logs={}):

        self.scores = {
            'recall_score': [],
            'precision_score': [],
            'f1_score': []
        }

        for batch_index in range(self.validation_steps):
            features, y_true = next(self.validation_generator)            
            y_pred = np.asarray(self.model.predict(features))
            y_pred = y_pred.round().astype(int) 
            self.scores['recall_score'].append(recall_score(y_true[:,0], y_pred[:,0]))
            self.scores['precision_score'].append(precision_score(y_true[:,0], y_pred[:,0]))
            self.scores['f1_score'].append(f1_score(y_true[:,0], y_pred[:,0]))
        return

metrics = CustomMetrics(validation_generator, validation_steps)

model.fit_generator(generator=train_generator,
                    steps_per_epoch=steps_per_epoch,
                    epochs=epochs,
                    validation_data=validation_generator,
                    validation_steps=validation_steps,
                    shuffle=True,
                    callbacks=[metrics],
                    verbose=1)
w00dy
  • 748
  • 1
  • 6
  • 23
  • I don't think there is a good alternative. If you look at the code for [_fit_loop](https://github.com/keras-team/keras/blob/eac78b859beb31cafa65a3edb4eaa888d3b6c2e6/keras/engine/training.py#L1037) in keras, it doesn't really pass validation_steps and validation_data to the callback. – sumitgouthaman Jan 06 '18 at 20:21
  • what about using next(validation_generatro) on ( on batch begin) , is that will be better than your way? I mean , I don't know in this case if next(val_generator) will take the next iteration or it always begin randomly from beginning and it will never cover all the validation data. – W. Sam Jun 26 '18 at 07:42
  • If you look at the Keras TensorBoard Callback there seems to be a way of getting validation data from the model, but I can't find where it happens in the code: https://github.com/tensorflow/tensorflow/blob/r1.14/tensorflow/python/keras/callbacks_v1.py – markemus Jul 09 '19 at 18:23
  • I provide a possible answer here: https://stackoverflow.com/a/59697739/880783 – bers Jan 11 '20 at 19:16
  • Does this answer your question? [Create keras callback to save model predictions and targets for each batch during training](https://stackoverflow.com/questions/47079111/create-keras-callback-to-save-model-predictions-and-targets-for-each-batch-durin) – bers Jan 11 '20 at 19:17
  • Hi all... thanks for responding to this question. It's been a while, I cannot replicate the problem above today but I'm happy to see a fervent discussion here – w00dy Dec 22 '20 at 17:07

4 Answers4

8

You can iterate directly over self.validation_data to aggregate all the validation data at the end of each epoch. If you want to calculate precision, recall and F1 across the complete validation dataset:

# Validation metrics callback: validation precision, recall and F1
# Some of the code was adapted from https://medium.com/@thongonary/how-to-compute-f1-score-for-each-epoch-in-keras-a1acd17715a2
class Metrics(callbacks.Callback):

    def on_train_begin(self, logs={}):
        self.val_f1s = []
        self.val_recalls = []
        self.val_precisions = []

    def on_epoch_end(self, epoch, logs):
        # 5.4.1 For each validation batch
        for batch_index in range(0, len(self.validation_data)):
            # 5.4.1.1 Get the batch target values
            temp_targ = self.validation_data[batch_index][1]
            # 5.4.1.2 Get the batch prediction values
            temp_predict = (np.asarray(self.model.predict(
                                self.validation_data[batch_index][0]))).round()
            # 5.4.1.3 Append them to the corresponding output objects
            if(batch_index == 0):
                val_targ = temp_targ
                val_predict = temp_predict
            else:
                val_targ = np.vstack((val_targ, temp_targ))
                val_predict = np.vstack((val_predict, temp_predict))

        val_f1 = round(f1_score(val_targ, val_predict), 4)
        val_recall = round(recall_score(val_targ, val_predict), 4)
        val_precis = round(precision_score(val_targ, val_predict), 4)

        self.val_f1s.append(val_f1)
        self.val_recalls.append(val_recall)
        self.val_precisions.append(val_precis)

        # Add custom metrics to the logs, so that we can use them with
        # EarlyStop and csvLogger callbacks
        logs["val_f1"] = val_f1
        logs["val_recall"] = val_recall
        logs["val_precis"] = val_precis

        print("— val_f1: {} — val_precis: {} — val_recall {}".format(
                 val_f1, val_precis, val_recall))
        return

valid_metrics = Metrics()

Then you can add valid_metrics to the callback argument:

your_model.fit_generator(..., callbacks = [valid_metrics])

Be sure to put it at the beginning of the callbacks in case you want other callbacks to use these measures.

Verdant89
  • 101
  • 1
  • 3
  • 4
    Is there a way of using the prediction results from the validation data, rather than calculating them again? – Eduardo Pignatelli Apr 30 '19 at 13:48
  • 3
    what is the prerequisite to access self.validation in the `def on_epoch_end(self, batch, logs)` ? I always run into an `AttributeError: 'Metrics' object has no attribute 'validation_data'` – vanessaxenia Sep 30 '19 at 09:39
  • 1
    @vanessaxenia You need to pass validation_data in the Metrics class as a parameter. – Timbus Calin Oct 09 '19 at 08:28
  • 1
    Your `batch_index` is actually a direct index into the data, so it yields one training example at a time. You need to do slicing to get the full batch. Also, more critically `self.validation_data` is just a list of 4 elements, and this answer doesn't work at all. – information_interchange Oct 12 '19 at 18:29
1

I was locking for solution for the same problem, then I find yours and another solution in the accepted answer here. If the second solution work, I think it will be better than iterating thorough all validation again at " on epoch end"

The idea is to save the target and pred placeholders in variables and update the variables through custom callback at " on batch end"

W. Sam
  • 818
  • 1
  • 7
  • 21
1

Here's how:

from sklearn.metrics import r2_score

class MetricsCallback(keras.callbacks.Callback):
    def on_epoch_end(self, epoch, logs=None):
        if epoch:
            print(self.validation_data[0])
            x_test = self.validation_data[0]
            y_test = self.validation_data[1]
            predictions = self.model.predict(x_test)
            print('r2:', r2_score(prediction, y_test).round(2))

model.fit( ..., callbacks=[MetricsCallback()])

Reference

Keras 2.2.4

B Seven
  • 44,484
  • 66
  • 240
  • 385
  • 3
    As far as your reference on github says, self.validation data is None, and this issue is not solved yet. – Vadym B. Feb 28 '19 at 16:08
  • 3
    @VadymB. - That's because `Unfortunately, since moving from fit to flow_from_directory and fit_generator, this has erred because self.validation_data is None.` I'm using `fit`. – B Seven Feb 28 '19 at 18:41
0

Verdant89 made a few mistake and did not implement all functions. The code below should work.

class Metrics(callbacks.Callback):

def on_train_begin(self, logs={}):
    self.val_f1s = []
    self.val_recalls = []
    self.val_precisions = []

def on_epoch_end(self, epoch, logs):
    # 5.4.1 For each validation batch
    for batch_index in range(0, len(self.validation_data[0])):
        # 5.4.1.1 Get the batch target values
        temp_target = self.validation_data[1][batch_index]
        # 5.4.1.2 Get the batch prediction values
        temp_predict = (np.asarray(self.model.predict(np.expand_dims(
                            self.validation_data[0][batch_index],axis=0)))).round()
        # 5.4.1.3 Append them to the corresponding output objects
        if batch_index == 0:
            val_target = temp_target
            val_predict = temp_predict
        else:
            val_target = np.vstack((val_target, temp_target))
            val_predict = np.vstack((val_predict, temp_predict))

    tp, tn, fp, fn = self.compute_tptnfpfn(val_target, val_predict)
    val_f1 = round(self.compute_f1(tp, tn, fp, fn), 4)
    val_recall = round(self.compute_recall(tp, tn, fp, fn), 4)
    val_precis = round(self.compute_precision(tp, tn, fp, fn), 4)

    self.val_f1s.append(val_f1)
    self.val_recalls.append(val_recall)
    self.val_precisions.append(val_precis)

    # Add custom metrics to the logs, so that we can use them with
    # EarlyStop and csvLogger callbacks
    logs["val_f1"] = val_f1
    logs["val_recall"] = val_recall
    logs["val_precis"] = val_precis

    print("— val_f1: {} — val_precis: {} — val_recall {}".format(
             val_f1, val_precis, val_recall))
    return

def compute_tptnfpfn(self,val_target,val_predict):
    # cast to boolean
    val_target = val_target.astype('bool')
    val_predict = val_predict.astype('bool')

    tp = np.count_nonzero(val_target * val_predict)
    tn = np.count_nonzero(~val_target * ~val_predict)
    fp = np.count_nonzero(~val_target * val_predict)
    fn = np.count_nonzero(val_target * ~val_predict)

    return tp, tn, fp, fn

def compute_f1(self,tp, tn, fp, fn):
    f1 = tp*1. / (tp + 0.5*(fp+fn) + sys.float_info.epsilon)
    return f1

def compute_recall(self,tp, tn, fp, fn):
    recall = tp*1. / (tp + fn + sys.float_info.epsilon)
    return recall

def compute_precision(self,tp, tn, fp, fn):
    precision = tp*1. / (tp + fp + sys.float_info.epsilon)
    return precision