Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Implementing custom loss function in keras with condition

I need some help with keras loss function. I have been implementing custom loss function on keras with Tensorflow backend.

I have implemented the custom loss function in numpy but it would be great if it could be translated into keras loss function. The loss function takes dataframe and series of user id. The Euclidean distance for same user_id are positive and negative if the user_id are different. The function returns summed up scalar distance of the dataframe.

def custom_loss_numpy (encodings, user_id):
# user_id: a pandas series of users
# encodings: a pandas dataframe of encodings

    batch_dist = 0

    for i in range(len(user_id)):
         first_row = encodings.iloc[i,:].values
         first_user = user_id[i]

         for j in range(i+1, len(user_id)):
              second_user = user_id[j]
              second_row = encodings.iloc[j,:].values

        # compute distance: if the users are same then Euclidean distance is positive otherwise negative.
            if first_user == second_user:
                tmp_dist = np.linalg.norm(first_row - second_row)
            else:
                tmp_dist = -np.linalg.norm(first_row - second_row)

            batch_dist += tmp_dist

    return batch_dist

I have tried to implement into keras loss function. I extracted numpy array from y_true and y_pred tensor objects.

def custom_loss_keras(y_true, y_pred):
    # session of my program
    sess = tf_session.TF_Session().get()

    with sess.as_default():
        array_pred = y_pred.eval()
        print(array_pred)

But I get the following error.

tensorflow.python.framework.errors_impl.InvalidArgumentError: You must feed a value for placeholder tensor 'dense_1_input' with dtype float and shape [?,102]
 [[Node: dense_1_input = Placeholder[dtype=DT_FLOAT, shape=[?,102], _device="/job:localhost/replica:0/task:0/device:CPU:0"]()]]

Any kind of help would be really appreciated.

like image 500
Black Mask Avatar asked Jun 29 '18 11:06

Black Mask


2 Answers

First of all, it is not possible to "extract numpy array from y_true and y_pred" in Keras loss functions. You have to operate the tensors with Keras backend functions (or TF functions) to calculate the loss.

In other words, it would be better to think about a "vectorized" way to calculate the loss, without using if-else and loops.

Your loss function can be computed in the following steps:

  1. Generate a matrix of pairwise Euclidean distances, between all pairs of vectors in encodings.
  2. Generate a matrix I whose element I_ij is 1 if user_i == user_j, and -1 if user_i != user_j.
  3. Element-wise multiply the two matrices, and sum up the elements to get the final loss.

Here's an implementation:

def custom_loss_keras(user_id, encodings):
    # calculate pairwise Euclidean distance matrix
    pairwise_diff = K.expand_dims(encodings, 0) - K.expand_dims(encodings, 1)
    pairwise_squared_distance = K.sum(K.square(pairwise_diff), axis=-1)

    # add a small number before taking K.sqrt for numerical safety
    # (K.sqrt(0) sometimes becomes nan)
    pairwise_distance = K.sqrt(pairwise_squared_distance + K.epsilon())

    # this will be a pairwise matrix of True and False, with shape (batch_size, batch_size)
    pairwise_equal = K.equal(K.expand_dims(user_id, 0), K.expand_dims(user_id, 1))

    # convert True and False to 1 and -1
    pos_neg = K.cast(pairwise_equal, K.floatx()) * 2 - 1

    # divide by 2 to match the output of `custom_loss_numpy`, but it's not really necessary
    return K.sum(pairwise_distance * pos_neg, axis=-1) / 2

I've assumed that user_id are integers in the code above. The trick here is to use K.expand_dims for implementing pairwise operations. It's probably a bit difficult to understand at a first glance, but it's quite useful.

It should give about the same loss value as custom_loss_numpy (there will be a little bit difference because of K.epsilon()):

encodings = np.random.rand(32, 10)
user_id = np.random.randint(10, size=32)

print(K.eval(custom_loss_keras(K.variable(user_id), K.variable(encodings))).sum())
-478.4245

print(custom_loss_numpy(pd.DataFrame(encodings), pd.Series(user_id)))
-478.42953553795815

I've made a mistake in the loss function.

When this function is used in training, since Keras automatically changes y_true to be at least 2D, the argument user_id is no longer a 1D tensor. The shape of it will be (batch_size, 1).

In order to use this function, the extra axis must be removed:

def custom_loss_keras(user_id, encodings):
    pairwise_diff = K.expand_dims(encodings, 0) - K.expand_dims(encodings, 1)
    pairwise_squared_distance = K.sum(K.square(pairwise_diff), axis=-1)
    pairwise_distance = K.sqrt(pairwise_squared_distance + K.epsilon())

    user_id = K.squeeze(user_id, axis=1)  # remove the axis added by Keras
    pairwise_equal = K.equal(K.expand_dims(user_id, 0), K.expand_dims(user_id, 1))

    pos_neg = K.cast(pairwise_equal, K.floatx()) * 2 - 1
    return K.sum(pairwise_distance * pos_neg, axis=-1) / 2
like image 141
Yu-Yang Avatar answered Oct 01 '22 10:10

Yu-Yang


There are two steps in implementing a parameterized custom loss function in Keras. First, writing a method for the coefficient/metric. Second, writing a wrapper function to format things the way Keras needs them to be.

  1. It's actually quite a bit cleaner to use the Keras backend instead of tensorflow directly for simple custom loss functions like DICE. Here's an example of the coefficient implemented that way:

    import keras.backend as K
    def dice_coef(y_true, y_pred, smooth, thresh):
        y_pred = y_pred > thresh
        y_true_f = K.flatten(y_true)
        y_pred_f = K.flatten(y_pred)
        intersection = K.sum(y_true_f * y_pred_f)
        return (2. * intersection + smooth) / (K.sum(y_true_f) + K.sum(y_pred_f) + smooth)
    
  1. Now for the tricky part. Keras loss functions must only take (y_true, y_pred) as parameters. So we need a separate function that returns another function:

    def dice_loss(smooth, thresh):
        def dice(y_true, y_pred)
            return -dice_coef(y_true, y_pred, smooth, thresh)
        return dice
    

Finally, you can use it as follows in Keras compile:

# build model 
model = my_model()
# get the loss function
model_dice = dice_loss(smooth=1e-5, thresh=0.5)
# compile model
model.compile(loss=model_dice)
like image 32
PyMatFlow Avatar answered Oct 01 '22 08:10

PyMatFlow