The following is the code of a recommender system:

Can you find why it is not working well ?
The idea behind is that the model works in a fuzzy logic.

user_intent is a matrix of rules, each row in user_intent.T is a rule.
Each rule is used as selector to construct an AND function.
For example, if selector = [1, 0, 0, 1], it means only the first and last elements of the "selected" array will be included in the operation.
    Given selected = [1, 1, 1, 0], applying the selector results in: 1 AND 0.

The rules weight, SHOULD give for each user-rule, a score for that user wrt that rules.
rules_weight = torch.softmax(u_embeddings @ user_intent, dim=1)


The prediction score of the items, is calculated first indecently from the user, and only wrt the user_intent by the following function:
all_scores = self.and_scores(selected=gi)
all_scores contain for each item , the scare wrt all the possible rules.

Then for the final prediction this score are putted in OR. The or function works like AND function, a selector and a selected.
In this case the selectors are the rules_weight, and the selected the scores of each item-rule.

The code is the following, but it is not working properly why ?

from abc import ABC

import torch_sparse
from torch_geometric.nn import LGConv
import torch
import torch_geometric
import numpy as np
import random
import torch.nn as nn
import torch.nn.functional as F

class RBIGCFModel(torch.nn.Module, ABC):
    def __init__(self,
                 num_users: int,
                 num_items: int,
                 nint: int,
                 n_layers: int,
                 embed_k: int,
                 learning_rate: int,
                 l_2: float,
                 l_c: float,
                 k: int,
                 t: int,
                 adj: torch_sparse.SparseTensor,
                 random_seed: int,
                 name="RBIGCF",
                 **kwargs
                 ):
        super().__init__()

        # set seed
        random.seed(random_seed)
        np.random.seed(random_seed)
        torch.manual_seed(random_seed)
        torch.cuda.manual_seed(random_seed)
        torch.cuda.manual_seed_all(random_seed)
        torch.backends.cudnn.deterministic = True

        self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

        self.num_users = num_users
        self.num_items = num_items
        self.nint = nint
        self.n_layers = n_layers
        self.embed_k = embed_k
        self.learning_rate = learning_rate
        self.l_2 = l_2
        self.l_c = l_c
        self.k = k
        self.t = t
        self.adj = adj
        self.normalize = True

        #------------------ Embedding and parameters initialization

        #------- User and Item Embedding: U = (num_user, dim_embedding) ; I = (num_item, dim_embedding)
        self.user_emb = nn.Embedding(self.num_users, self.embed_k)
        self.item_emb = nn.Embedding(self.num_items, self.embed_k)
        # torch.nn.init.xavier_normal_(self.user_emb.weight)
        # torch.nn.init.xavier_normal_(self.item_emb.weight)
        torch.nn.init.trunc_normal_(self.user_emb.weight, mean=0.5, std=1.0, a=0.0, b=1.0)
        torch.nn.init.trunc_normal_(self.item_emb.weight, mean=0.5, std=1.0, a=0.0, b=1.0)

        #------- User and Item Intent initialization
        # if using the sigmoid, it is best to initialize embedding such that sigmoid(emb) = 0.5
        # user_intent =  (dim_embedding, num_intent)
        self.user_intent = nn.Parameter(
            nn.init.xavier_normal_(torch.empty((self.embed_k, self.nint))), requires_grad=True
        )
        # item_intent =  (dim_embedding, num_intent)
        self.item_intent = nn.Parameter(
            nn.init.xavier_normal_(torch.empty((self.embed_k, self.nint))), requires_grad=True
        )



        # ------- GNN  initialization
        propagation_network_list = []
        for _ in range(self.n_layers):
            propagation_network_list.append((LGConv(normalize=self.normalize), 'x, edge_index -> x'))
        self.propagation_network = torch_geometric.nn.Sequential('x, edge_index', propagation_network_list)
        self.propagation_network.to(self.device)
        self.softplus = torch.nn.Softplus()

        # ------- Optimizer
        self.optimizer = torch.optim.Adam(self.parameters(), lr=self.learning_rate)

    def disjunction(self, selector: torch.Tensor, selected:torch.Tensor, epsilon: float = 1e-40) -> torch.Tensor:
        '''
        Args:
        selector (torch.Tensor): Tensor of shape [batch_size, embed_k], representing user embeddings.
        selected (torch.Tensor): Tensor of shape [batch_size, embed_k], representing item embeddings.
        epsilon (float, optional): Small constant to prevent log(0). Defaults to 1e-12.

    Returns:
        torch.Tensor: Tensor of shape [batch_size], containing the conjunction scores.

    Flows:
        The selector is used to determine which values from the "selected" array should be considered in an AND operation.
        For example, if selector = [1, 0, 0, 1], it means only the first and last elements of the "selected" array will be included in the operation.
        Given selected = [1, 1, 1, 0], applying the selector results in: 1 OR 0.
        '''

        expr = torch.log(1 - selector * selected + epsilon)
        log_sum = torch.sum( expr, dim=1)
        return 1 - (-1 / (-1 + log_sum))

    def and_scores(self, selected: torch.Tensor, epsilon: float = 1e-40) -> torch.Tensor:
        """
        Compute the (OR) function for each pair of selector and selected.

        Args:
            selector (torch.Tensor): Tensor of shape [n_selector, embed_k], representing weight to construct and rules (user_intent)
            selected (torch.Tensor): Tensor of shape [n_selected, embed_k], representing item embeddings to which apply these rules i.e:items
            epsilon (float, optional): Small constant to prevent log(0). Defaults to 1e-40.

        Returns:
            torch.Tensor: Tensor of shape [n_selected, n_selector], containing the conjunction scores.
        """
        # Ensure dimensions are compatible for broadcasting
        # selector: [n_selector, embed_k]
        # selected: [n_selected, embed_k]

        # Add dimensions to broadcast the tensors over each other
        user_intent = torch.clamp(self.user_intent, 0, 1)
        selected = torch.clamp(selected, 0, 1)
        selector_expanded = user_intent.unsqueeze(0)  # [1, n_selector, embed_k]
        selected_expanded = selected.unsqueeze(1)  # [n_selected, 1, embed_k]

        # Perform the AND operation element-wise
        expr = 1 - selector_expanded * (1 - selected_expanded) + epsilon  # [n_selected, n_selector, embed_k]

        # Logarithm of the terms
        log_expr = torch.log(expr)

        # Sum over the embedding dimension
        sum_log_expr = torch.sum(log_expr, dim=2)  # [n_selected, n_selector]

        # Compute the final conjunction score for each selector-selected pair
        res = -1.0 / (-1.0 + sum_log_expr)  # [n_selected, n_selector]
        return res


    def conjunction(self, selector: torch.Tensor, selected: torch.Tensor, epsilon: float = 1e-40) -> torch.Tensor:
        """
        Compute the conjunction (AND) function.

        Args:
            selector (torch.Tensor): Tensor of shape [batch_size, embed_k], representing user embeddings.
            selected (torch.Tensor): Tensor of shape [batch_size, embed_k], representing item embeddings.
            epsilon (float, optional): Small constant to prevent log(0). Defaults to 1e-12.

        Returns:
            torch.Tensor: Tensor of shape [batch_size], containing the conjunction scores.
        """
        # Compute the element-wise terms
        selected = torch.clamp(selected, 0, 1)
        selector = torch.clam(selector, 0, 1)
        expr = 1 - selector * (1 - selected) + epsilon  # epsilon prevents log(0)
        # Take the logarithm of each term
        log_expr = torch.log(expr)
        # Sum over the embedding dimensions
        sum_log_expr = torch.sum(log_expr, dim=1)
        # Compute the final AND function value
        res = -1.0 / (-1.0 + sum_log_expr)
        return res


    def propagate_embeddings(self, evaluate=False):
        ego_embeddings = torch.cat((self.user_emb.weight.to(self.device), self.item_emb.weight.to(self.device)), 0)
        all_embeddings = [ego_embeddings]

        # GNN propagation
        for layer in range(0, self.n_layers):
            if evaluate:
                self.propagation_network.eval()
                with torch.no_grad():
                    all_embeddings += [list(
                        self.propagation_network.children()
                    )[layer](all_embeddings[layer].to(self.device), self.adj.to(self.device))]
            else:
                all_embeddings += [list(
                    self.propagation_network.children()
                )[layer](all_embeddings[layer].to(self.device), self.adj.to(self.device))]

        if evaluate:
            self.propagation_network.train()

        # ----------------- User and Item GNN embeddings
        all_embeddings = torch.mean(torch.stack(all_embeddings, 0), dim=0)
        # all_embeddings = sum([all_embeddings[k] * self.alpha[k] for k in range(len(all_embeddings))])
        u_embeddings, i_embeddings = torch.split(all_embeddings, [self.num_users, self.num_items], 0)
        u_embeddings, i_embeddings = torch.clamp(u_embeddings, 0, 1), torch.clamp(i_embeddings, 0, 1)

        item_intent = torch.clamp(self.item_intent, 0, 1)
        user_intent = torch.clamp(self.user_intent, 0, 1)

        # Rules OR weight by attention
        rules_weight = torch.softmax(u_embeddings @ user_intent, dim=1)  # (num_users, n_rules)

        # ----------------- Bilateral Intent -- item
        i_int_embeddings = torch.softmax(i_embeddings @ item_intent, dim=1) @ item_intent.T

        # ----------------- Reparameterization
        # noise = torch.randn_like(i_embeddings)
        i_rep = i_embeddings + i_int_embeddings # * noise ---i after reparametrization

        # ---------------- Final emb
        user_embedding, item_embedding =  u_embeddings, torch.clamp(i_rep, 0, 1)



        return rules_weight, item_embedding

    def forward(self, inputs, **kwargs):
        '''
                gru = rules_weight for users:  batch x n_rules - so for each user in the batch we have the rule weight
                gri = i-emb: batch x n_rules: each line represent the score for the specific item wrt all the rules
                return score: batch_size * 1 (each element = score u-i)
                '''
        gru, gri = inputs
        return self.disjunction(gru, gri)
    def predict(self, gru, gri, **kwargs):
        """
            Compute the disjunction score for all combinations of gru and gri.

            Args:
                gru (torch.Tensor): Tensor of shape [N, dim_emb], representing rules weight for users.
                gri (torch.Tensor): Tensor of shape [M, dim_emb], representing score for specific items.

            Returns:
                torch.Tensor: Tensor of shape [N, M], containing the disjunction scores for each combination.
            """
        # Expand dimensions to make the tensors broadcastable
        gri = self.and_scores(selected=gri)
        gru_expanded = gru.unsqueeze(1)  # Shape [N, 1, dim_emb]
        gri_expanded = gri.unsqueeze(0)  # Shape [1, M, dim_emb]

        # Apply the disjunction function for all combinations of gru and gri
        expr = torch.log(1 - gru_expanded * gri_expanded + 1e-40)  # Shape [N, M, dim_emb]

        # Sum along the embedding dimension (dim_emb)
        log_sum = torch.sum(expr, dim=2)  # Shape [N, M]

        # Compute the final disjunction score
        res = 1 - (-1 / (-1 + log_sum))  # Shape [N, M]

        return res


    def rule_constrastive(self):
        cl = 0.0
        # item_intent = F.sigmoid(self.item_intent)
        user_intent = torch.clamp(self.user_intent, 0, 1)
        pos_score = torch.exp(torch.sum(user_intent.T * user_intent.T, dim=1) / self.t)
        neg_score = torch.sum(torch.exp(torch.mm(user_intent.T, user_intent) / self.t), axis=1)
        cl += torch.sum(-torch.log(pos_score / (neg_score + 1e-8) + 1e-8))
        cl /= pos_score.shape[0]
        return cl



    def train_step(self, batch):

        # retrieve  embeddings
        # gr = rules_weight for each user = (n_user, n_rules) --> for each - ui: rules weight
        #
        gr, gi = self.propagate_embeddings()

        # user_ids, positive_items_ids, negative_items_ids
        user_t, pos_t, neg_t = batch
        user, pos, neg = user_t[:, 0], pos_t[:, 0], neg_t[:, 0]

        all_scores = self.and_scores(selected=gi)  # shape = (n_items , n_rules)

        # ELBO Loss
        xu_pos = self.forward(inputs=(gr[user], all_scores[pos]))  # positive score
        xu_neg = self.forward(inputs=(gr[user], all_scores[neg]))  # negative score
        # difference = torch.clamp(xu_pos - xu_neg, -80.0, 1e8)
        # loss = torch.sum(self.softplus(-difference))
        # loss = torch.mean(F.softplus(xu_neg - xu_pos))
        diff_score = xu_pos - xu_neg
        loss = torch.nn.functional.softplus(-diff_score).mean()


        # Sigmoid Regularization: Encourage raw embeddings to stay around values that map to ((0, 1)) appropriately.
        # loss += self.sigmoid_reg * torch.mean(raw_embeddings ** 2)

        # L2 Regularization Loss
        # l2_reg_loss = self.l_2 * (self.user_emb.weight[user].norm(2).pow(2) +
        #                        self.item_emb.weight[pos].norm(2).pow(2) +
        #                        self.item_emb.weight[neg].norm(2).pow(2)) / float(batch[0].shape[0])
        # loss += l2_reg_loss

        # L1 Regularization Loss
        reg_loss = self.l_2 * ( self.user_emb.weight[user].norm(1) +
                                self.item_emb.weight[pos].norm(1) +
                                self.item_emb.weight[neg].norm(1)) / user.shape[0]
        loss += reg_loss

        # # L2 Intent Reg. Loss
        # l2_intent_reg_loss = self.l_2 * (self.user_intent.norm(2).pow(2) +
        #                               self.item_intent.norm(2).pow(2)) / float(self.nint)
        # loss += l2_intent_reg_loss

        # graph contrastive loss
        # gcl_loss = self.l_c * self.cal_ssl_loss(user, pos, full_emb, int_emb)
        contrastive_loss = self.l_c * self.rule_constrastive()
        loss += contrastive_loss



        self.optimizer.zero_grad()
        loss.backward()
        self.optimizer.step()

        return loss.detach().cpu().numpy()

    def get_top_k(self, preds, train_mask, k=100):
        return torch.topk(torch.where(torch.tensor(train_mask).to(self.device), preds.to(self.device),
                                      torch.tensor(-np.inf).to(self.device)), k=k, sorted=True)