One possibility would be to go for hurdle-ish model with separate heads for classification and regression. The classification should be understood as whether products were sold (non-zero regression value) or not (regression value zero). The benefit is that it is possible to manually zero out the regression values, in case the classifier votes for the negative class. As an example, here's a two-head tf.keras
model that could be suitable for image regression.
from tensorflow.keras.layers import *
from tensorflow.keras.models import Model, Sequential
from tensorflow.keras.applications import ResNet50
...
input_shape = (256,256,3)
inputs = Input(input_shape)
# feature extration
base_model = ResNet50(
input_shape=input_shape,
weights='imagenet',
include_top=False,
)
base_model = base_model(inputs)
# bottleneck
bottleneck = GlobalAveragePooling2D()(base_model)
bottleneck = Dense(256, activation='relu')(bottleneck)
bottleneck = Dropout(0.25)(bottleneck)
# classification head
clf = Dense(32, activation='relu')(bottleneck)
clf = Dropout(0.5)(clf)
clf = Dense(1, use_bias=False)(clf)
clf_output = Activation("sigmoid", name="clf_head_output")(clf)
# regression head
regr = Dense(32, activation='relu')(bottleneck)
regr = Dropout(0.5)(regr)
regr = Dense(1, use_bias=False)(regr)
regr_output = Activation("relu", name="regr_head_output")(regr) #<- change activation according to your task
# build model
model = Model(inputs=[inputs], outputs=[regr_output, clf_output,])
Typically one would select mean_absolute_error
or mean_squared_error
as the regression output loss. The problem you may face in the case of ZIP (zero-inflated Poissonian) distribution is that the model's predictions might tend to be lower than the ground truth, especially for the highest regression values. If this is the case, then this is probably because the model gets most punished for predicting the high regression values wrong.
Standardizing the regression outputs is important, and one could try tricks such as boxcox, however, this might be difficult, keeping in mind that zero has a special role in the distribution. If there is some certain lower (L) and upper (U) bound for the regression task, a trick that has worked well for me is to normalize the outputs to the interval [0,1] and use angular regression instead. The idea is that then the original L and U would be identical with each other (the regression values lie on the unit circle) and the model gets equal punishment whether it tries to approach the high/rare values from "above" or "below". Here's also my implementation of angular loss for tf.keras
. I suggest using sigmoid
or clipped relu
(with max_value=1.
) with this approach.
import tensorflow as tf
import tensorflow.keras.backend as K
PI = tf.constant(math.pi)
def angular_l1(y_true, y_pred): #y_true and y_pred should be in the inteval [0,1]
y_true = y_true*2*PI #<- scale values to [0,2pi]
y_pred = y_pred*2*PI #<- scale values to [0,2pi]
return K.mean(K.abs(tf.math.atan2(K.sin(y_true - y_pred), K.cos(y_true - y_pred))))