I have a CNN binary classifier with one-hot-encoded labels that I've written using Keras and it's just not training to the metric I want to encourage. My data is very imbalanced (91% class 0, 9% class 1) and, no matter what I do, it always favors accuracy in the majority class. I've played with class weights. I've tried creating balanced test and train files. It just seems that it's harder for my CNN to find patterns in the minority class, so it always returns improvements in the majority class. What I'd like to do is create a custom loss that allows me to define a score that rewards the minority class's true positives (TP) and penalizes its false positives (FP). Something like the following:
def minority_score(y_true, y_pred):
max_value_true = K.argmax(y_true, -1)
max_value_pred = K.argmax(y_pred, -1)
FP = np.logical_and(K.eval(max_value_pred) == 1, K.eval(max_value_true) == 0)
TP = np.logical_and(K.eval(max_value_pred) == 1, K.eval(max_value_true) == 1)
score = (TP *3) - FP # punish each FP with a -1 and reward each TP with a +3
return score # invert if using as loss function
model = build_model()
model.compile(loss=minority_score,
optimizer=keras.optimizers.Adam(lr=0.0001),
metrics=[minority_score, metrics.categorical_accuracy])
I've tried various things but, as my labels are one-hot-encoded, I always run into problems decoding them. y_pred
and y_true
are tensors in the above example. In that example, I get this error:
InvalidArgumentError (see above for traceback): You must feed a value for placeholder tensor 'conv2d_1_input' with dtype float and shape [?,28,28,1]
Here's the relevant code all put together. Thank you.
def minority_score(y_true, y_pred):
max_value_true = K.argmax(y_true, -1)
max_value_pred = K.argmax(y_pred, -1)
# below line errors out
FP = np.logical_and(K.eval(max_value_pred) == 1, K.eval(max_value_true) == 0)
TP = np.logical_and(K.eval(max_value_pred) == 1, K.eval(max_value_true) == 1)
score = (TP *3) - FP # punish each FP with a -1 and reward each TP with a +3
return score
class Modeler:
def build_model(self):
"""
this method only builds the scaffolding
weights should be set and/or loaded
outside of it
"""
model = Sequential()
# layerset 1
model.add(Conv2D(
filters=32,
kernel_size=[3, 3],
strides=(1, 1),
input_shape=(28, 28, 1),
padding='same'
))
model.add(BatchNormalization())
model.add(Activation("relu"))
model.add(MaxPooling2D(pool_size=(2, 2), strides=(2, 2)))
model.add(Dropout(0.2))
# layerset 2
model.add(Conv2D(
filters=64,
kernel_size=[3, 3],
padding='same'
))
model.add(BatchNormalization())
model.add(Activation("relu"))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.2))
# layerset 3
model.add(Conv2D(
filters=128,
kernel_size=[3, 3],
padding='same'
))
model.add(BatchNormalization())
model.add(Activation("relu"))
model.add(MaxPooling2D(pool_size=(2, 2)))
model.add(Dropout(0.25))
model.add(Flatten())
model.add(Dense(2048, activation='relu'))
model.add(Dense(self.num_classes, activation='softmax'))
return model
def compile_model(self, model, learning_rate):
model.compile(loss=minority_score,
optimizer=keras.optimizers.Adam(lr=learning_rate),
metrics=[minority_score, metrics.categorical_accuracy)
return model
def one_hot_encode_labels(self, data):
return keras.utils.to_categorical(data, self.num_classes)
modeler = modeler.build_model()
model = modeler.compile_model()
# load model weights from checkpoint here
# load and shape data here
# create class weight dictionary here
model.fit(x_train, y_train,
batch_size=batch_size,
epochs=num_epochs,
verbose=1,
validation_data=(x_test, y_test),
callbacks=callbacks_list,
class_weight=weight_dict)
```
tf.py_function
to allow one to use numpy operations. Please keep in mind that tensor operations include automatic auto-differentiation support. python numpy operations do not. So if the function affects gradient computations you would need to tell tensorflow how to compute the gradients. $\endgroup$