17

I am working on a medical dataset where I am trying to have as less false negatives as possible. A prediction of "disease when actually no disease" is okay for me but a prediction "no disease when actually a disease" is not. That is, I am okay with FP but not FN.

After doing some research, I found out ways like Keeping higher learning rate for one class, using class weights,ensemble learning with specificity/sensitivity etc.

I achieved the near desired result using class weights like class_weight = {0 : 0.3,1: 0.7} and then calling the model.fit(class_weights=class_weight). This gave me very low FN but a pretty high FP. I am trying to reduce FP as much as possible keeping FN very low.

I am struggling to write a custom loss function using Keras which will help me to penalize the false negatives. Thanks for the help.

Swapnil B.
  • 729
  • 1
  • 8
  • 23
  • [Here](https://datascience.stackexchange.com/questions/33587/keras-custom-loss-function-as-true-negatives-by-true-negatives-plus-false-posit) you can find how to write a custom Keras loss function for specificity. – Matti Feb 06 '19 at 06:04

1 Answers1

21

I'll briefly introduce the concepts we're trying to tackle.

Recall

From all that were positive, how many did our model predict as positive?

All that were positive = positive

What our model said were positive = said positive

recall

Since recall is inversely proportional to FN, improving it decreases FN.

Specificity

From all that were negative, how many did our model predict as negative?

All that were negative = negative

What our model said were negative = said negative

specificity

Since specificity is inversely proportional to FP, improving it decreases FP.

In your next searches, or whatever classification-related activity you perform, knowing these is going to give you an extra edge in communication and understanding.


A Solution

So. These two concepts, as you mas have figured out already, are opposites. This means that increasing one is likely to decrease the other.

Since you want priority on recall, but don't want to loose too much in specificity, you can combine both of those and attribute weights. Following what's clearly explained in this answer:

import numpy as np
import keras.backend as K

def binary_recall_specificity(y_true, y_pred, recall_weight, spec_weight):

    TN = np.logical_and(K.eval(y_true) == 0, K.eval(y_pred) == 0)
    TP = np.logical_and(K.eval(y_true) == 1, K.eval(y_pred) == 1)

    FP = np.logical_and(K.eval(y_true) == 0, K.eval(y_pred) == 1)
    FN = np.logical_and(K.eval(y_true) == 1, K.eval(y_pred) == 0)

    # Converted as Keras Tensors
    TN = K.sum(K.variable(TN))
    FP = K.sum(K.variable(FP))

    specificity = TN / (TN + FP + K.epsilon())
    recall = TP / (TP + FN + K.epsilon())

    return 1.0 - (recall_weight*recall + spec_weight*specificity)

Notice recall_weight and spec_weight? They're weights we're attributing to each of the metrics. For distribution convention, they should always add to 1.0¹, e.g. recall_weight=0.9, specificity_weight=0.1. The intention here is for you to see what proportion best suits your needs.

But Keras' loss functions must only receive (y_true, y_pred) as arguments, so let's define a wrapper:

# Our custom loss' wrapper
def custom_loss(recall_weight, spec_weight):

    def recall_spec_loss(y_true, y_pred):
        return binary_recall_specificity(y_true, y_pred, recall_weight, spec_weight)

    # Returns the (y_true, y_pred) loss function
    return recall_spec_loss

And onto using it, we'd have

# Build model, add layers, etc
model = my_model
# Getting our loss function for specific weights
loss = custom_loss(recall_weight=0.9, spec_weight=0.1)
# Compiling the model with such loss
model.compile(loss=loss)

¹ The weights, added, must total 1.0, because in case both recall=1.0 and specificity=1.0 (the perfect score), the formula

loss1

Shall give us, for example,

loss2

Clearly, if we got the perfect score, we'd want our loss to equal 0.

Julio Cezar Silva
  • 2,148
  • 1
  • 21
  • 30
  • 2
    Hi @julio, I tried to implement your solution, but it returns me the next error: `InvalidArgumentError (see above for traceback): You must feed a value for placeholder tensor 'dense_1_target' with dtype float and shape [?,?]`. Any idea of how to fix it? The first thing I changed in your code was to replace np.logical_and to tf.logical_and, because it only works with tensors. But I don't know how to change the other error... Thank you in advance! – Cristina V Nov 28 '19 at 08:55
  • 1
    @CristinaV Were you able to replicate it in TensorFlow 2 ? I am also getting the same error. – Aman Dalmia Jul 26 '21 at 09:24
  • Tried your solution but got an error ValueError: No gradients provided for any variable: – SchwarzbrotMitHummus Aug 25 '21 at 12:52
  • I don't see how this can be used in training, given that the function `binary_recall_specificity` is not differentiable, and breaks backpropagation. – Andrey Kuehlkamp Sep 14 '21 at 17:06
  • This function did not work for me at first because I got an error "AttributeError: module 'tensorflow' has no attribute 'enable_eager_execution'". I was able to fix that by setting 'run_eagerly=True' in the compiler: model.compile(, run_eagerly=True). However as pointed out by @AndreyKuehlkamp, this is not differentiable and broke the backpropagation during training. I dont think this is a useable loss function as is. – Shep Bryan Nov 17 '21 at 18:02
  • You can use the reparameterization trick to make this differentiable. – William Yolland Jul 26 '22 at 01:52