3

I am trying to create a custom macro for recall = (recall of class1 + recall of class2)/2. I came up with the following code but I am not sure how to calculate the true positive of class 0.

def unweightedRecall():
    def recall(y_true, y_pred):
        # recall of class 1
        true_positives1 = K.sum(K.round(K.clip(y_pred * y_true, 0, 1)))
        possible_positives1 = K.sum(K.round(K.clip(y_true, 0, 1)))
        recall1 = true_positives1 / (possible_positives1 + K.epsilon())

        # --- get true positive of class 0 in true_positives0 here ---
        # Also, is there a cleaner way to get possible_positives0
        possible_positives0 = K.int_shape(y_true)[0] - possible_positives1
        recall0 = true_positives0 / (possible_positives0 + K.epsilon())
        return (recall0 + recall1)/2
    return recall

It seems I will have to use Keras.backend.equal(x, y), but how do i create a tensor with shape K.int_shape(y_true)[0] and all values, say x?


Edit 1

Based on Marcin's comments, I wanted to create a custom metric based on callback in keras. While browsing issues in Keras, I came across the following code for f1 metric:

class Metrics(keras.callbacks.Callback):
    def on_epoch_end(self, batch, logs={}):
        predict = np.asarray(self.model.predict(self.validation_data[0]))
        targ = self.validation_data[1]
        self.f1s=f1(targ, predict)
        return
metrics = Metrics()
model.fit(X_train, y_train, epochs=epochs, batch_size=batch_size, validation_data=[X_test,y_test], 
       verbose=1, callbacks=[metrics])

But how is the callback returning the accuracy? I wanted to implement unweighted recall = (recall class1 + recall class2)/2. I can think of the following code but would appreciate any help to complete it

from sklearn.metrics import recall_score
class Metrics(keras.callbacks.Callback):
    def on_epoch_end(self, batch, logs={}):
        predict = np.asarray(self.model.predict(self.validation_data[0]))
        targ = self.validation_data[1]
        # --- what to store the result in?? ---
        self.XXXX=recall_score(targ, predict, average='macro')
        # we really dont need to return anything ??
        return
metrics = Metrics()
model.fit(X_train, y_train, epochs=epochs, batch_size=batch_size, validation_data=[X_test,y_test], 
       verbose=1, callbacks=[metrics])

Edit 2: model:

def createModelHelper(numNeurons=40, optimizer='adam'):
    inputLayer = Input(shape=(data.shape[1],))
    denseLayer1 = Dense(numNeurons)(inputLayer)
    outputLayer = Dense(1, activation='sigmoid')(denseLayer1)
    model = Model(input=inputLayer, output=outputLayer)
    model.compile(loss=unweightedRecall, optimizer=optimizer)
    return model
Benjamin Breton
  • 1,388
  • 1
  • 13
  • 42
Aditya
  • 5,509
  • 4
  • 31
  • 51
  • 1
    There is a problem with computing `precision` and `recall` using `keras.metrics` and `keras.losses` `API. Remember - that the final value of loss or metric is a mean across every batch - but for `precision` and `recall` - a mean across the batches is not equal to the final metric value. I advise you to use `keras.callbacks` in order to compute appropriate values. – Marcin Możejko Feb 12 '18 at 09:34
  • Thanks for that info! any pointer for the (custom) callback would be really appreciated! If it is easy to implement callback for (recall class1 + recall class2)/2, I would highly appreciate that in answer :) – Aditya Feb 12 '18 at 09:58

1 Answers1

4

keras version (with the mean problem).

Are your two classes actually only one dimension output (0 or 1)?

If so:

def recall(y_true, y_pred):
    # recall of class 1

    #do not use "round" here if you're going to use this as a loss function
    true_positives = K.sum(K.round(y_pred) * y_true)
    possible_positives = K.sum(y_true)
    return true_positives / (possible_positives + K.epsilon())


def unweightedRecall(y_true, y_pred):
    return (recall(y_true,y_pred) + recall(1-y_true,1-y_pred))/2.

Now, if your two classes are actually a 2-element output:

def unweightedRecall(y_true, y_pred):
    return (recall(y_true[:,0],y_pred[:,0]) + recall(y_true[:,1],y_pred[:,1]))/2.

Callback version:

For the callback, you can use a LambdaCallback, and you manually print or store the results:

myCallBack = LambdaCallback(on_epoch_end=unweightedRecall)
stored_metrics = []

def unweightedRecall(epoch,logs):
    predict = model.predict(self.validation_data[0])
    targ = self.validation_data[1]

    result = (recall(targ,predict) + recall(1-targ,1-predict))/2. 
    print("recall for epoch " + str(epoch) + ": " + str(result))
    stored_metrics.append(result)

Where recall is a function using np instead of K. And epsilon = np.finfo(float).eps or epsilon = np.finfo(np.float32).eps)

Daniel Möller
  • 84,878
  • 18
  • 192
  • 214
  • Thanks a lot for that awesome answer!! How can the callback version be used for `metrics` parameter in model.compile? I am still not sure how the `metrics` parameter will associate the result to the metric. – Aditya Feb 15 '18 at 18:11
  • The callback will not be a metric, that's why we created the `stored_metrics` list. – Daniel Möller Feb 15 '18 at 18:56
  • In the backprop stage, I want to use 'unweighted recall' to update the weights. How can I achieve that using the callback function? According to my understanding, I can use the first method to achieve this by updating the `metrics` to use `unweightedRecall`. – Aditya Feb 16 '18 at 01:02
  • 1
    You must use a "loss" function. `loss = unweightedRecall` -- But this will only work with the keras function, not with the callback version. And you will have the problem mentioned by Marcin. But this doesn't seem to be a relevant problem. After all, almost all applications are divided in batches and treated as such. – Daniel Möller Feb 16 '18 at 01:55
  • thanks for the correction! that was really helpful :) – Aditya Feb 16 '18 at 01:57
  • On using the loss function I get `ValueError: An operation has 'None' for gradient`. I see `y_pred` is ``, but I doubt shape is an issue here as I still get the error if I do `y_pred = y_pred[:,0]` in `unweightedRecall`. The y_pred is output of sigmoid. – Aditya Feb 16 '18 at 10:05
  • The model woks fine if I use `binary_crossentropy` loss instead. I have updated the question to include the model. – Aditya Feb 16 '18 at 18:05
  • Hmmm, that's true.... it's the "round" function. It's not differentiable. Maybe you can use just `y_pred`. You won't get an exact recall, but it will work. – Daniel Möller Feb 16 '18 at 18:22