Use if/else logic in tensorflow to either add an element to one tensor or another

Viewed 64

I am building a custom loss function that needs to know whether the truth and the prediction have N pixels above a threshold. This is because the logic breaks if I supply an np.where() array which is empty. I can get around this issue by using try/else to return a 'flagged constant' in the case that the function fails on the empty set, but I'd like to do something different. Here is my current method.

def some_loss(cutoff=20, min_pix=10):
    def gen_loss(y_true, y_pred):
        trues = tf.map_fn(fn = lambda x: x, elems = y_true)
        preds = tf.map_fn(fn = lambda x: x, elems = y_pred)
        for idx in tf.range(tf.shape(y_true)[0]):
            # binarize both by cutoff
            true = y_true[idx]
            pred = y_pred[idx]
            true = tf.where(true < cutoff, 0.0, 1.0)
            pred = tf.where(pred < cutoff, 0.0, 1.0)
            # now I sum each to get the number of pixels above threshold
            n_true, n_pred = tf.reduce_sum(true), tf.reduce_sum(pred)
            # then I create a switch using tf.conditional
            switch = tf.cond(tf.logical_or(n_true < min_pix, n_pred < min_pix), lambda: tf.zeros_like(true), lambda: tf.ones_like(true))
            # this essentially allows me to turn off the loss if either condition is met
            # so I then run the function
            loss = get_loss(true, pred) # returns random constant if either is below threshold
            loss += tf.reduce_sum(tf.math.multiply(loss, switch))
        return loss
    return gen_loss

This may work, it compiles and trains a convolutional model. However, I don't like that there are random constants wandering about my loss function, and I'd rather only operate the function get_loss() if both true and pred meet the minimum conditions.

I'd prefer to make two tensors, one with samples not meeting the condition, the other with samples meeting the condition.


Separately, I've tried to use tf.conditional to test for each case and call a separate loss function in either case. The code is repeated below.

def avgMED(scaler, cutoff=20, min_N=30,c=3):
    def AVGmed(y_true, y_pred):
        const = tf.constant([c],tf.float32) # constant c, multiplied by MED (
        batch_size = tf.cast(tf.shape(y_true)[0], tf.float32)
        MSE = tf.reduce_mean(tf.square(y_true-y_pred))
        y_true = tf.reshape(y_true, shape=(tf.shape(y_true)[0], -1))
        y_pred = tf.reshape(y_pred, shape=(tf.shape(y_pred)[0], -1))
        loss, loss_med = tf.cast(0,dtype=tf.float32), tf.cast(0,dtype=tf.float32)
        # rescale
        y_true = y_true*scaler.scale_
        y_true = y_true+scaler.mean_
        y_pred = y_pred*scaler.scale_
        y_pred = y_pred+scaler.mean_
        trues = tf.map_fn(fn = lambda x: x, elems=y_true)
        preds = tf.map_fn(fn = lambda x: x, elems=y_pred)
        min_nonzero_pixels = tf.reduce_sum(tf.constant(min_N, dtype=tf.float32))
        for idx in tf.range(batch_size):
            idx = tf.cast(idx, tf.int32)
            true = trues[idx]
            pred = preds[idx]
            MSE = tf.reduce_mean(tfm.square(tfm.subtract(true,pred)))
            true = tf.where(true<cutoff,0.0,1.0)
            pred = tf.where(pred<cutoff,0.0,1.0)
            n_true = tf.reduce_sum(true)
            n_pred = tf.reduce_sum(pred)
            loss_TA = tf.cond(tf.logical_or(n_true < min_nonzero_pixels, n_pred < min_nonzero_pixels), get_zero(true,pred), get_MED(true,pred))
            loss_med += loss_TA.read(0)
            loss += loss_med + MSE # do we benefit from reducing across the batch dimension? we should be able to look at familiar batches and see the little increase due to the distance component
            tf.print(n_true,n_pred)
            tf.print(loss_med)
        return loss # this is essentially MSE given c ~ 0. Thus, this will show if there are some weird gradients flowing through that are preventing the model from learning
    return AVGmed 

def get_MED(A,B):
    # takes in binary tensors
    indices_A, indices_B = tf.where(A), tf.where(B)
    coordX_A_TA, coordY_A_TA = find_coord(indices_A) # finds x,y coordinates and returns tensor array
    coordX_B_TA, coordY_B_TA = find_coord(indices_B)
    mindists_AB_TA = find_min_distances(coordX_A_TA, coordY_A_TA, coordX_B_TA, coordY_B_TA)
    mindists_BA_TA = find_min_distances(coordX_B_TA, coordY_B_TA, coordX_A_TA, coordY_A_TA)
    # MED = mean error distance = 
    med_AB = tf.reduce_mean(mindists_AB_TA.read(0))
    med_BA = tf.reduce_mean(mindists_BA_TA.read(0))
    avg_med = tfm.divide(tfm.add(med_AB,med_BA),tf.constant(0.5))
    loss_TA = tf.TensorArray(dtype=tf.float32, size=0, dynamic_size=True)
    loss_TA.write(loss_TA.size(), avg_med)
    return loss_TA 

def get_zero(A,B):
    loss_TA = tf.TensorArray(dtype=tf.float32, size=0, dynamic_size=True)
    loss_TA.write(loss_TA.size(), 0)
    return loss_TA

However, with this framework I am now getting new errors about my generator not having enough data, which is absurd given the batch size I test with is 10 and 1 step_per_epoch on a train size of 100. Got a warning about not closing the TensorArray, which I expect happens whether the conditional is true or false. Inching closer to a solution but could use some guidance on how problematic my tensorflow logic is.

0 Answers
Related