1
$\begingroup$

I'm having trouble with making my validation metrics go down for the binary_crossentropy and go up for the F1-score and AUC. I've tried tuning my hyper parameters such as the number of latent features of the model (8), the learning rate (0.0075), the lambda in the regularization term (1.2), the the drop out rate (0.4), and the batch size (16384), which have seemingly maximum values you can give to where you can ensure the prevention of overfitting, but to no avail my validation F1-Score and AUC always stalls at around 47%, 52% at its highest if I increase my epochs to 500.

It even got to the point that a higher batch size gave my RAM problems since I only use my mere CPU in this ML task (Because I've no graphics card unfortunately).

Here is my model architecture which uses an embedding layer initially then essentially takes the output of this embedding layer and feeds it into two phases so to speak, one that will flatten the output of the embedding layer and one which concatenates the output of the embedding layer and feeds it into a fully connected network.

class DFM(tf.keras.Model):
    def __init__(self, n_users, n_items, emb_dim=32, layers_dims=[16, 16, 16], lambda_=1, keep_prob=1, regularization="L2"):
        """
        Implements the DFM (Deep Factorization Machine) architecture
        """
        super(DFM, self).__init__()
        # number of unique users and items
        self.n_users = n_users
        self.n_items = n_items

        # hyperparams
        self.lambda_ = lambda_
        self.drop_prob = 1 - keep_prob
        self.emb_dim = emb_dim
        self.layers_dims = layers_dims

        # regularization to use
        self.regularization = L2 if regularization == "L2" else L1

        # number of layers of DNN
        self.num_layers = len(layers_dims)

        # initialize embedding and embedding bias layers
        self.user_emb_layer = tf.keras.layers.Embedding(n_users, emb_dim, embeddings_regularizer=self.regularization(lambda_), name='user_embedding')
        self.item_emb_layer = tf.keras.layers.Embedding(n_items, emb_dim, embeddings_regularizer=self.regularization(lambda_), name='item_embedding')

        self.user_emb_bias_layer = tf.keras.layers.Embedding(n_users, 1, embeddings_initializer='zeros', name='user_embedding_bias')
        self.item_emb_bias_layer = tf.keras.layers.Embedding(n_items, 1, embeddings_initializer='zeros', name='item_embedding_bias')

        # initialize dot product layer and add layer for
        # embedding vectors and bias scalars respectively
        self.dot_layer = tf.keras.layers.Dot(axes=(2, 1))
        self.add_layer = tf.keras.layers.Add()

        # initialize flatten layer to flatten sum of the dot product
        # of user_emb & item_emb, user_emb_bias, and  item_emb_bias
        self.flatten_fact_matrix_layer = tf.keras.layers.Flatten()
        
        # initialize concat layer as input to DNN
        self.concat_layer = tf.keras.layers.Concatenate(axis=2)
        self.flatten_concat_emb_layer = tf.keras.layers.Flatten()

        # initialize dense and activation layers of DNN
        self.dense_layers, self.act_layers, self.dropout_layers = self.init_dense_act_drop_layers()

        # initialize last layer of DNN to dense with no activation
        self.last_dense_layer = tf.keras.layers.Dense(units=1, activation='linear', kernel_regularizer=self.regularization(lambda_))

        
        self.add_layer = tf.keras.layers.Add()

        # output layer will just be a sigmoid activation layer
        self.out_layer = tf.keras.layers.Activation(activation=tf.nn.sigmoid)
        

    def call(self, inputs, **kwargs):
        # catch inputs first since Model will be taking in a 2 rows of data
        # the user_id_input which is m x 1 and item_id_input which is m x 1
        # since one example would be one user and one item
        user_id_input = inputs[0]
        item_id_input = inputs[1]

        # DEFINE FORWARD PROPAGATION

        # once user_id_input is passed dimensionality goes from m x 1
        # to m x 1 x emb_dim
        user_emb = self.user_emb_layer(user_id_input)
        item_emb = self.item_emb_layer(item_id_input)

        user_emb_bias = self.user_emb_bias_layer(user_id_input)
        item_emb_bias = self.item_emb_bias_layer(item_id_input)

        # calculate the dot product of the user_emb and item_emb vectors
        user_item_dot = self.dot_layer([user_emb, tf.transpose(item_emb, perm=[0, 2, 1])])
        fact_matrix = self.add_layer([user_item_dot, user_emb_bias, item_emb_bias])
        fact_matrix_flat = self.flatten_fact_matrix_layer(fact_matrix)

        # concatenate the user_emb and item_emb vectors
        # then feed to fully connected deep neural net
        A = self.concat_layer([user_emb, item_emb])
        flat_A = self.flatten_concat_emb_layer(A)

        # forward propagate through deep neural network according to number of layers
        for l in range(self.num_layers):
            # pass concatenated user_embedding and item embedding 
            # to dense layer to calculate Z at layer l
            Z = self.dense_layers[l](flat_A)

            # activate output Z layer by passing to relu activation layer
            flat_A = self.act_layers[l](Z)

            if kwargs['training'] == True:
                flat_A = self.dropout_layers[l](flat_A)

        # pass second to the last layer to a linear layer
        A_last = self.last_dense_layer(flat_A)

        # add the output to the flattened factorized matrix
        sum_ = self.add_layer([A_last, fact_matrix_flat])

        # pass the sum of last dense layer and the flattened 
        # factorized matrix to a sigmoid activation function
        out = self.out_layer(sum_)

        return out

    def init_dense_act_drop_layers(self):
        """
        
        """
        dense_layers = []
        act_layers = []
        dropout_layers = []

        layers_dims = self.layers_dims
        for layer_dim in layers_dims:
            dense_layers.append(tf.keras.layers.Dense(units=layer_dim, kernel_regularizer=self.regularization(self.lambda_)))
            act_layers.append(tf.keras.layers.Activation(activation=tf.nn.relu))

            # drop 1 - keep_prob percent of the neurons e.g. keep_prob
            # is 0.2 so drop 1 - 0.2 or 0.8/80% of the neurons at each 
            # activation layer
            dropout_layers.append(tf.keras.layers.Dropout(rate=self.drop_prob))


        return dense_layers, act_layers, dropout_layers

And here is my recommendation dataset which consists of 12033 unique users, 4228 items. Below is a snippet of the two data splits I've used one for training and validation respectively. Originally this dataset had explicit ratings from 1 to 5 but because I've framed it instead as users either having positively interacted with an item or have not interacted with an item. Where the positively interacted items meant that the user has rated the item from 4 to 5 which I labeled 1 and the items not interacted by a user I labeled as 0. Basically for each positive interaction by a user I labeled 1 and sampled unrated items by that user (the same as the number of positive interactions), which I labeled as 0.

raw_ratings.csv

,user_id,item_id,rating
0,2406,253,4
1,10316,1435,2
2,564,941,5
3,6311,1443,4
4,11737,762,2
5,10730,895,4
6,7627,2874,1
7,10602,729,4
8,3805,1605,3
9,8860,1649,5
10,9998,2934,1
...

train.csv

,user_id,item_id,interaction
0,9098,1550,0
1,169,387,1
2,5355,2961,1
3,6475,1518,1
4,10990,3224,1
5,5046,630,0
6,6801,2124,1
7,5039,2996,0
8,2231,2008,0
9,8980,1419,0
10,9423,4051,1
...

cross.csv

,user_id,item_id,interaction
0,8716,3544,1
1,3308,565,0
2,1018,3907,0
3,8814,3239,0
4,619,2014,0
5,4048,3752,1
6,1591,946,1
7,3594,2986,1
8,9485,209,0
...

I should note that

And here is a snippet the train_model.py file

from tensorflow.keras.losses import BinaryCrossentropy as bce_loss
from tensorflow.keras.metrics import (BinaryAccuracy, 
    Precision,
    Recall,
    AUC,
    BinaryCrossentropy as bce_metric, 
)

model = DFM(
            n_users=n_users, 
            n_items=n_items, 
            emb_dim=8,
            layers_dims=[16, 16, 16],
            lambda_=1.2, 
            keep_prob=0.8, 
            regularization="L2")
    }

    model.compile(
        optimizer=Adam(learning_rate=0.0075),
        loss=bce_loss(),
        metrics=[bce_metric(), BinaryAccuracy(), Precision(), Recall(), AUC(), f1_m]
    )

    # train model
    history = model.fit(
        [train_data['user_id'], train_data['item_id']],
        train_data['interaction'],
        batch_size=16384,
        epochs=500,
        validation_data=([cross_data['user_id'], cross_data['item_id']], cross_data['interaction']),
    )

Here are the metric values during training:

Epoch 1/500
23/23 [==============================] - 3s 40ms/step - loss: 56.4068 - binary_crossentropy: 0.6943 - binary_accuracy: 0.4825 - precision: 0.4786 - recall: 0.4096 - auc: 0.4737 - f1_m: 0.4370 - val_loss: 18.8620 - val_binary_crossentropy: 0.6956 - val_binary_accuracy: 0.4633 - val_precision: 0.4693 - val_recall: 0.5232 - val_auc: 0.4496 - val_f1_m: 0.4946
Epoch 2/500
23/23 [==============================] - 0s 18ms/step - loss: 9.8139 - binary_crossentropy: 0.6910 - binary_accuracy: 0.5353 - precision: 0.5337 - recall: 0.5481 - auc: 0.5491 - f1_m: 0.5390 - val_loss: 3.4573 - val_binary_crossentropy: 0.6981 - val_binary_accuracy: 0.4553 - val_precision: 0.4506 - val_recall: 0.3852 - val_auc: 0.4372 - val_f1_m: 0.4153
Epoch 3/500
23/23 [==============================] - 0s 18ms/step - loss: 1.8671 - binary_crossentropy: 0.6892 - binary_accuracy: 0.5446 - precision: 0.5447 - recall: 0.5350 - auc: 0.5624 - f1_m: 0.5390 - val_loss: 0.9702 - val_binary_crossentropy: 0.7005 - val_binary_accuracy: 0.4521 - val_precision: 0.4541 - val_recall: 0.4492 - val_auc: 0.4346 - val_f1_m: 0.4513
...
23/23 [==============================] - 0s 18ms/step - loss: 0.6900 - binary_crossentropy: 0.6831 - binary_accuracy: 0.5553 - precision: 0.5549 - recall: 0.5519 - auc: 0.5789 - f1_m: 0.5535 - val_loss: 0.7400 - val_binary_crossentropy: 0.7332 - val_binary_accuracy: 0.4396 - val_precision: 0.4419 - val_recall: 0.4398 - val_auc: 0.4145 - val_f1_m: 0.4404
Epoch 498/500
23/23 [==============================] - 0s 18ms/step - loss: 0.6900 - binary_crossentropy: 0.6831 - binary_accuracy: 0.5555 - precision: 0.5548 - recall: 0.5545 - auc: 0.5790 - f1_m: 0.5545 - val_loss: 0.7400 - val_binary_crossentropy: 0.7332 - val_binary_accuracy: 0.4392 - val_precision: 0.4417 - val_recall: 0.4406 - val_auc: 0.4145 - val_f1_m: 0.4408
Epoch 499/500
23/23 [==============================] - 0s 18ms/step - loss: 0.6898 - binary_crossentropy: 0.6831 - binary_accuracy: 0.5556 - precision: 0.5549 - recall: 0.5549 - auc: 0.5790 - f1_m: 0.5548 - val_loss: 0.7398 - val_binary_crossentropy: 0.7331 - val_binary_accuracy: 0.4396 - val_precision: 0.4424 - val_recall: 0.4439 - val_auc: 0.4145 - val_f1_m: 0.4427
Epoch 500/500
23/23 [==============================] - 0s 18ms/step - loss: 0.6898 - binary_crossentropy: 0.6831 - binary_accuracy: 0.5554 - precision: 0.5549 - recall: 0.5536 - auc: 0.5789 - f1_m: 0.5542 - val_loss: 0.7397 - val_binary_crossentropy: 0.7332 - val_binary_accuracy: 0.4392 - val_precision: 0.4404 - val_recall: 0.4293 - val_auc: 0.4145 - val_f1_m: 0.4344

And finally the graph after 500 epochs enter image description here

So my question Could it be that my architecture is too complex or is my dataset the problem? Am I missing something I should know about training recommender systems? And what importance does the dataset of a recommender system play here?

I'm really in need of help since I've been stuck for basically two weeks now because of this any help would be appreciated

$\endgroup$

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.