<jupyter_start><jupyter_text>Data Preprocessing<jupyter_code>ratings = pd.read_csv("/content/animelist.csv")
ratings.head(5)<jupyter_output><empty_output><jupyter_text>In the dataset, if a person hasn't left a rating to an anime, it was marked as a zero. Let's remove the zeros, as they will just introduce noise later on. Might as well remove the `watched_episodes`.<jupyter_code>ratings = ratings[ratings["rating"] > 0]
ratings = ratings[ratings["watching_status"] != 1]
ratings = ratings.drop(columns = ["watched_episodes"])
def transform(df):
    df.loc[df["rating"] <= 5, ["watching_status"]] = 0.0
    df.loc[df["watching_status"] == 2, ["watching_status"]] = 1.0
    df.loc[df["watching_status"] == 4, ["watching_status"]] = 0.0
    
    df.rename(columns = {'watching_status': 'interaction'}, inplace = True)
transform(ratings)
ratings.head()
ratings = ratings[ratings["interaction"] < 2]
ratings["interaction"].value_counts()
num_users = ratings["user_id"].nunique()
num_anime = ratings["anime_id"].nunique()

print(f"There are a total of {num_users} users found.")
print(f"There are a total of {num_anime} anime found.")

print("Max user id:", ratings["user_id"].max())
print("Max anime id:", ratings["anime_id"].max())<jupyter_output>There are a total of 310362 users found.
There are a total of 17136 anime found.
Max user id: 353404
Max anime id: 48456<jupyter_text>If we take a look at the anime and user ids, we can notice that some values there are missing. Let's make a two helper functions, for easy conversion between dataset ids and embedding ids.<jupyter_code>embId2user = sorted(ratings["user_id"].unique())
embId2anime = sorted(ratings["anime_id"].unique())
user2embId = {v: k for k, v in enumerate(embId2user)}
anime2embId = {v: k for k, v in enumerate(embId2anime)}<jupyter_output><empty_output><jupyter_text>Now, let's make the dataset. It will look like a quadruples of `(user_id, anime_embId, rating, interaction)`. We'll make 3 sets: `train`, `test` and `val`.<jupyter_code>np.random.seed(seed = 42)

def makeDataSet(df, split=0.95):
    n = df.to_numpy()
    
    n = np.random.permutation(n)

    x = n[:, :2]
    y = n[:, 2:]

    for i in range(x.shape[0]):
        x[i][0] = user2embId[x[i][0]]
        x[i][1] = anime2embId[x[i][1]]

    s1 = int(split * n.shape[0])
    s2 = s1 + int((1 - split) * n.shape[0] / 10)
    
    return (x[:s1], y[:s1], x[s1:s2], y[s1:s2], x[s2:], y[s2:])
x_train, y_train, x_test, y_test, x_val, y_val = makeDataSet(ratings)
y_train[:, 1].mean()<jupyter_output><empty_output><jupyter_text>Models<jupyter_code>print("Num GPUs Available: ", len(tf.config.list_physical_devices('GPU')))<jupyter_output>Num GPUs Available:  1<jupyter_text>Neural Network<jupyter_code>class HybridNeuralNetworkModel(tf.keras.Model):
    def __init__(self, num_users, num_items, embedding_dim):
        super(HybridNeuralNetworkModel, self).__init__()
        
        self.embedding_dim = embedding_dim
        
        self.user_embeddings = tf.keras.layers.Embedding(num_users, embedding_dim)
        self.item_embeddings = tf.keras.layers.Embedding(num_items, embedding_dim)

        self.dense1 = tf.keras.layers.Dense(64, activation='relu')
        self.dense2 = tf.keras.layers.Dense(1, activation='relu')
        
        self.dense3 = tf.keras.layers.Dense(1, activation='sigmoid')

        self.concat = tf.keras.layers.Concatenate()
        self.dropout = tf.keras.layers.Dropout(.5)

    def call(self, inputs, training = False):
        user_ids = inputs[:, 0]
        item_ids = inputs[:, 1]

        user_embedding = self.user_embeddings(user_ids)
        item_embedding = self.item_embeddings(item_ids)

        if training:
            user_embedding = self.dropout(user_embedding, training = training)
            item_embedding = self.dropout(item_embedding, training = training)

        user_embedding = tf.reshape(user_embedding, [-1, self.embedding_dim])
        item_embedding = tf.reshape(item_embedding, [-1, self.embedding_dim])

        conc = self.concat([user_embedding, item_embedding])
        x = self.dense1(conc)
        
        x1 = self.dense2(x)
        x2 = self.dense3(x)

        return x1, x2
hnn_model = HybridNeuralNetworkModel(num_users = num_users, 
                                     num_items = num_anime, 
                                     embedding_dim = 64)

hnn_model.compile(
    optimizer = tf.keras.optimizers.Adam(),
    loss = {
        "output_1": tf.keras.losses.MeanSquaredError(),
        "output_2": tf.keras.losses.BinaryCrossentropy()
    },
    loss_weights = {
        "output_1": 1.0,
        "output_2": 5.0
    },
    metrics = {
        "output_1": [
            tf.keras.metrics.RootMeanSquaredError("RMSE")
        ],
        "output_2": [
            tf.keras.metrics.BinaryAccuracy("Acc"),
            tf.keras.metrics.Precision(name = "P"),
            tf.keras.metrics.Recall(name = "R")
        ]
    },
    run_eagerly = True
)
callback = tf.keras.callbacks.EarlyStopping(monitor = 'loss', patience = 3)

history = hnn_model.fit(
    x = x_train, 
    y = {
        "output_1": y_train[:, 0],
        "output_2": y_train[:, 1]
    },
    batch_size = 64, 
    epochs = 100, 
    steps_per_epoch = 1000,
    callbacks = [callback],
    validation_data = (x_test, {
        "output_1": y_test[:, 0],
        "output_2": y_test[:, 1]
    }),
    validation_steps = 10,
    validation_batch_size = 64
)
hnn_model.save_weights(
    "/content/drive/Shareddrives/ML/Anime RecSys/Hybrid/NeuralNetworkModel/model", overwrite=True
)
hnn_model.load_weights(
    "/content/drive/Shareddrives/ML/Anime RecSys/Hybrid/NeuralNetworkModel/model"
)<jupyter_output><empty_output><jupyter_text>Comparing the models<jupyter_code>hnn_model.evaluate(x = x_val, y = [y_val[:, 0], y_val[:, 1]])<jupyter_output>83995/83995 [==============================] - 1674s 20ms/step - loss: 3.5091 - output_1_loss: 1.9694 - output_2_loss: 0.3079 - output_1_RMSE: 1.4034 - output_2_Acc: 0.8790 - output_2_P: 0.8931 - output_2_R: 0.9759
{'fraction_non_alphanumeric': 0.14439834024896264, 'fraction_numerical': 0.030871369294605808, 'mean_word_length': 3.5037369207772797, 'pattern_counts': {'":': 10, '<': 28, '<?xml version=': 0, '>': 27, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 2, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '7', 'dir': 'jupyter-structured-clean-dedup', 'id': '287033', 'license': 'MIT', 'n_tokens_mistral': 2286, 'n_tokens_neox': 2151, 'n_words': 427, 'path': 'User_Anime_Hybrid_Predictions.ipynb', 'repo_name': 'nomomon/anime-recommendations', 'size': '6025'}