Hi i am developing a RS model. 
In this model the embedding of Gu select with component of the considered item to take into AND.
The model has an hyper parameter n_rules, which means the number of rules for each user. So if I have 2 rules, and 3 users and the dimension of embedding equal to 4, the users matrix embedding will be of dimension = (n_users, n_rules * dim_embeddings) = ( 3 , 4 * 2) . For each user the rules and then putted in DNF form, so that each user at the and it is represented by a single rule (that is the and-rules connected through OR operation).  
i'll explain better with an example with results, considering one user (u1) with two rules, each rules is of dim=4 and more items:
u1 = [1, 1, 0, 0, 0, 0, 1, 0]
i1 = [0.9, 1, 0.2, 0]
i2 = [0, 1, 1, 0]
I3 = [0, 0, 0, 1]

U1-i1:
Will give as results  1 for the first rule i.e.: [1, 1, 0, 0] choose in and the first two embedding of i [0.9, 1, 0.2, 0],    i.e = 0.9 AND 1 = 1 (almost) 0 for the second rule [0, 0, 1, 0],  will choose the third element of i[1, 1, 0.2, 0] , I.e. 0.2
The the two results are putted in DNF via an OR function (disjunction rule) 1 or 0 = 1. So the item will be recommended

And so on so forth. Considering the following code, how can I improve that ? I think that in this case the l2 regularization is not good. I have seen through experiments that the values tends to be too small, so the logic function does not work properly

The code is the following:  from abc import ABC
import torch
import numpy as np
import random


class RBRSModel(torch.nn.Module, ABC):
    def __init__(self,
                 num_users: int,
                 num_items: int,
                 learning_rate: float,
                 embed_k: int,
                 l_w: int,
                 random_seed: int,
                 n_rules: int,
                 name="RBRS",
                 **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.embed_k = embed_k  # Embedding dimension for each rule
        self.learning_rate = learning_rate
        self.l_w = l_w
        self.n_rules = n_rules  # Number of conjunction rules
        self.epsilon = 1e-12  # Small constant to prevent log(0)

        # Initialize embeddings for users and items
        # For users, we have n_rules different embeddings (one for each rule)
        self.Gu = torch.nn.Embedding(self.num_users, self.n_rules * self.embed_k)
        torch.nn.init.trunc_normal_(self.Gu.weight, mean=0.5, std=1.0, a=0.0, b=1.0)
        # torch.nn.init.xavier_uniform_(self.Gu.weight)
        self.Gu.to(self.device)

        # Item embeddings
        self.Gi = torch.nn.Embedding(self.num_items, self.embed_k)
        torch.nn.init.trunc_normal_(self.Gi.weight, mean=0.5, std=1.0, a=0.0, b=1.0)
        # torch.nn.init.xavier_uniform_(self.Gi.weight)
        self.Gi.to(self.device)

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

    def conjunction_rule(self, u: torch.Tensor, i: torch.Tensor) -> torch.Tensor:
        # Compute the conjunction (AND) function
        # u and i are tensors of shape [batch_size, embed_k]
        # Return tensor of shape [batch_size], i.e. u.shape[0] == i.shape[0]
        expr = 1 - u * (1 - i) + self.epsilon
        log_expr = torch.log(expr)
        sum_log_expr = torch.sum(log_expr, dim=1)
        res = -1.0 / (-1.0 + sum_log_expr)
        return res

    def disjunction_rule(self, and_rules: torch.Tensor) -> torch.Tensor:
        # Compute the disjunction (OR) function
        # and_rules is a tensor of shape [batch_size, n_rules], i.e. and_rules.shape[0] == batch_size, and_rules.shape[1] == n_rules
        expr = 1 - and_rules + self.epsilon
        log_expr = torch.log(expr)
        sum_log_expr = torch.sum(log_expr, dim=1)
        res = 1 - (-1.0 / (-1.0 + sum_log_expr))
        return res

    def forward(self, inputs, **kwargs):
        users, items = inputs
        batch_size = users.shape[0]

        # Get user embeddings and reshape for rules
        gu = self.Gu.weight[users]
        gu = gu.view(batch_size, self.n_rules, self.embed_k)  # 3 dim: 1st_dim = user_id, 2nd_dim = rule_no, 3d_dim = embedding of user-rule
        gu = torch.clamp(gu, 0.0, 1.0)  # Ensure embeddings are in [0,1]

        # Get item embeddings
        gamma_i = self.Gi.weight[items]  # Shape: [batch_size, embed_k]
        gamma_i = torch.clamp(gamma_i, 0.0, 1.0)

        # Compute conjunctions for each rule
        and_scores = []
        for r in range(self.n_rules):
            gamma_u_r = gu[:, r, :]  # User embedding for rule r
            score_and = self.conjunction_rule(u=gamma_u_r, i=gamma_i)  # Shape: [batch_size]
            and_scores.append(score_and.unsqueeze(1))  # Shape: [batch_size, 1]

        # Concatenate and compute disjunction
        and_scores_tensor = torch.cat(and_scores, dim=1)  # Shape: [batch_size, n_rules]
        xui = self.disjunction_rule(and_scores_tensor)  # Shape: [batch_size]

        return xui, gu, gamma_i

    def predict(self, start, stop, **kwargs):
        # Prediction for all items for users in the range [start, stop)
        gu = self.Gu.weight[start:stop].to(self.device)  # Shape: [num_users, n_rules * embed_k]
        gu = gu.view(gu.shape[0], self.n_rules, self.embed_k)
        gu = torch.clamp(gu, 0.0, 1.0)

        gamma_i = self.Gi.weight.to(self.device)  # Shape: [num_items, embed_k]
        gamma_i = torch.clamp(gamma_i, 0.0, 1.0)

        # Compute scores for each user and item
        scores = []
        for r in range(self.n_rules):
            gu_r = gu[:, r, :]  # [num_users, embed_k]
            # Broadcasting over items
            expr = 1 - gu_r.unsqueeze(1) * (1 - gamma_i.unsqueeze(0)) + self.epsilon  # [num_users, num_items, embed_k]
            log_expr = torch.log(expr)
            sum_log_expr = torch.sum(log_expr, dim=2)
            and_score = -1.0 / (-1.0 + sum_log_expr)  # [num_users, num_items]
            scores.append(and_score.unsqueeze(0))  # [1, num_users, num_items]

        # Combine scores using disjunction
        and_scores_tensor = torch.cat(scores, dim=0)  # [n_rules, num_users, num_items]
        expr = 1 - and_scores_tensor + self.epsilon  # [n_rules, num_users, num_items]
        log_expr = torch.log(expr)
        sum_log_expr = torch.sum(log_expr, dim=0)  # Sum over rules
        final_scores = 1 - (-1.0 / (-1.0 + sum_log_expr))  # [num_users, num_items]

        return final_scores

    def train_step(self, batch):
        user, pos, neg = batch

        # Positive items
        xui_pos, gu_pos, gamma_i_pos = self.forward(inputs=(user[:,0], pos[:, 0]))
        # Negative items
        xui_neg, _, gamma_i_neg = self.forward(inputs=(user[:, 0], neg[:, 0]))

        # BPR Loss
        # loss = -torch.mean(torch.nn.functional.logsigmoid(xui_pos - xui_neg))

        # BPR Loss V2
        # Compute the difference between positive and negative scores
        diff_scores = xui_pos - xui_neg  # Shape: (batch_size,)
        # Compute the BPR loss using the numerically stable softplus function
        loss = torch.nn.functional.softplus(-diff_scores).mean()

        # Regularization Loss -- Non va bene - sicuro da cambiare
        reg_loss = self.l_w * (
                gu_pos.norm(2).pow(2) +
                gamma_i_pos.norm(2).pow(2) +
                gamma_i_neg.norm(2).pow(2)
        ) / user.shape[0]

        loss += reg_loss

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

        return loss.item()

    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)