Neural collaborative filtering

TL; DR

Rootlabs @ lunch session on Neural Collaborative Filtering

Sistemas de recomendação

🥅 Objetivo

O objetivo é gerar recomendações para um usuário. O sistema de recomendação perfeito recomendaria a um usuário um item que ele gostaria, e que a pessoa não chegaria ao item sozinha. Portanto, um modelo que recomenda os itens mais populares para todos os usuários não é considerado um bom sistema de recomendação - as recomendações não são personalizadas.

O resultado principal de um modelo seria, por exemplo, os 10 principais itens que um usuário pode gostar e ainda não viu.

📚 Dados

Os dados podem incluir informações sobre o usuário (ou seja: idade, sexo, informações sobre compras anteriores), mas deve incluir informações da interação entre usuários e itens - se um usuário comprou/gostou/avaliou um item. Essa interação costuma ser chamada de feedback. Existem dois tipos diferentes de feedback: explícito e implícito.

💬 Feedback explícito x implícito

Feedback explícito é a interação que o usuário fornece intencionalmente - classificações de 5 estrelas em um produto, a 👍 em um produto etc.

Feedback implícito ocorre sempre que temos uma "dica" das preferências do usuário. Comprou, viu um filme, etc. Recebemos um interesse entre o usuário e o item, mas não um feedback concreto - se o usuário realmente gostou do item (só porque eu vi o filme, não significa que gostei).

Como se poderia imaginar, o feedback explícito é mais informativo do que o implícito. Mas, na maioria dos casos, esses dados não estão prontamente disponíveis ou são muito escassos. No restante desta postagem, vamos nos concentrar em situações nas quais recebemos feedback implícito.

Ao trabalhar com feedback implícito, antes de dividir os dados em teste e treinamento, precisamos fazer a suposição de que toda interação observada é positiva e a ausência de interação é negativa. No exemplo, todos os filmes que assisti eu gostei e todos os filmes que não assisti eu não gostaria (não é realista, mas necessário para definir o problema).

🔪 Divisão de "test" e "train"

Depois de reunir todas as informações, temos todos os usuários, itens e interações. Podemos colocá-los em uma matriz - também conhecida como matriz de interação.

Ao construir o teste, uma ideia é manter uma interação entre um usuário e um item e tentar prever isso. Pela imagem acima, poderíamos impedir a entrada do casal ruivo Shrek para o teste.

Pode-se fazer isso amostrando uma interação aleatoriamente para cada usuário, mas é interessante ter em mente a taxa de teste do trem e a possibilidade de vazamento de dados. Pode ser melhor tentar dividir os dados usando os últimos d dias para teste e o restante para treinamento, ou algo mais próximo do caso de uso para o qual o modelo está sendo construído.

De certa forma, temos uma abordagem de aprendizado supervisionado, uma vez que estamos tentando prever dados rotulados. Mas, como mencionado antes, é anormal no sentido de que um rótulo "verdadeiro" no feedback implícito pode não corresponder ao usuário realmente ter gostado do item. A ruiva pode não ter gostado de Shrek, embora isso seja desconhecido para nós. Por outro lado, ela pode gostar de "Memento", mas não tinha conhecimento desse filme.

Mas isso é o melhor que podemos fazer com os dados que temos. Depois que os dados forem rotulados, as suposições feitas e tivermos uma divisão de teste e trem, podemos ver como modelar nossos problemas.

🤖 Abordagens

Existem duas abordagens principais quando se trata de sistemas de recomendação - content based filtering e collaborative filtering.

Content based filtering

A ideia é usar as informações dos usuários para fazer previsões sobre se ele gostará no futuro. Essas informações podem incluir informações pessoais e informações de compras anteriores. A única coisa a se considerar é que o recurso está disponível para a maioria dos usuários.

A abordagem pode ser resumida a qualquer configuração de aprendizado supervisionado - temos recursos (do usuário e do item) e um rótulo que queremos prever. A partir daí, podemos usar qualquer modelo para classificação - de regressão logística a redes neurais.

Se tivermos um feedback explícito, também poderíamos construir um usuário embedding que tenha recursos no mesmo espaço que os itens, e calcular a semelhança entre eles. Por exemplo, se um usuário definiu que gosta de filmes de ação e drama, podemos procurar filmes de ação e drama.

Mas a ideia geral é ter recomendações específicas para um único usuário - o que ele gosta, em oposição ao collaborative filtering.

Comparação:

  • 👍 Escala bem - usa apenas dados de um usuário
  • 👍 Posso recomendar interesses de nicho muito específicos - muito específicos para um usuário
  • 👎 O modelo é incapaz de explorar e expandir os interesses dos usuários (coisas novas que ele não manifestou interesse no passado)
  • 👎 Se o modelo for construído com recursos de engenharia manual, é necessário muito conhecimento de domínio e o modelo é tão bom quanto seus recursos

Collaborative filtering

Na collaborative filtering, não usamos recursos de usuário ou item. Nós nos concentramos inteiramente em nossa matriz de interação. A principal ideia de alto nível é descobrir o que um usuário vai gostar com base no que usuários semelhantes gostaram no passado.

Ideia principal com collaborative filtering.

Na imagem acima - o emoji de festa seria recomendado para caça-níqueis por ser semelhante ao demônio. Aqui, "usuários semelhantes" são aqueles que compraram os mesmos itens (ou semelhantes) no passado.

No collaborative filtering, olhamos apenas para todas as compras e todos os itens. Então o modelo não liga para o que são os itens e nem os usuários. Tentamos criar embeddings de usuário e item úteis com base nas interações entre usuários e itens - ou seja: a matriz de interação.

Essa é a ideia principal, mas vamos mergulhar no funcionamento desse modelo mais tarde.

Comparação:

  • 👍 Nenhum conhecimento necessário - não precisamos saber nada sobre o problema para o qual estamos modelando
  • 👍 Pode generalizar interesses e recomendar mais itens novos para um usuário
  • 👎 Não dimensiona bem - precisa de todas as interações de todos os itens e todos os usuários
  • 👎 Não incluímos quaisquer outros recursos específicos do usuário nem recursos específicos do item
  • 👎 Baseamos nosso modelo na matriz de interação - não podemos gerar previsões para usuários sem interação registrada (um novo usuário no varejo online, por exemplo); isso é conhecido como problema de inicialização a frio

Abordagem híbrida

É possível um híbrido entre as duas abordagens. E na maioria dos casos o que é implementado. Em uma visão mais geral, o que é feito é ter os dois modelos lado a lado e pesar suas previsões. Também podemos determinar esses pesos de maneira orientada por dados.

Isso também permite o fallback quando não há dados de interação para um usuário. Nesse caso, nos concentramos na abordagem "content based" até que os dados de interação estejam disponíveis.

Uma implementação muito popular e fácil de usar é o modelo LightFM, que é conhecido por alcançar resultados muito bons.

🤔 Avaliação

Como se poderia imaginar, técnicas de avaliação de aprendizagem supervisionada podem ser aplicadas aqui, incluindo curvas de sustentação, ganhos cumulativos, entre outros. No entanto, como se espera que forneçamos ao usuário um conjunto de recomendações e não nos importamos necessariamente com previsões de baixa classificação, outros métodos de avaliação que diferem das abordagens tradicionais se aplicam.

As métricas populares incluem precisão superior @k, precisão @k e recall @k. K é o número de itens que recomendamos a um usuário (no exemplo inicial, usei k = 10). A ideia é calcular as métricas olhando apenas para os K itens mais bem classificados. Tensorflow também oferece essas métricas como uma opção para precisão regular e métricas de recall.

Também é possível avaliar que gama de itens estão sendo recomendados - para evitar modelos em que apenas um subconjunto de todos os itens aparecem como recomendações. Ou personalização - evite que os itens mais populares acabem como recomendações. Recmetrics oferece algumas idéias para avaliação de motores de recomendação.

💁‍♂️ Collaborative filtering

⚠️ Agora vamos mergulhar na matemática ⚠️

Fatoração de matriz

Matematicamente falando, é uma ideia bastante direta - podemos fatorar a matriz de interação para obter os embeddings para os itens e usuários.

Semelhante à fatoração normal, estamos procurando as matrizes $U$ (para usuários) e $I$ (para itens) que, quando multiplicadas, geram a matriz de interação $R$ (para avaliações), ou

$$U \cdot I = R$$

Observe que $\cdot$ denota multiplicação de matrizes e que as matrizes $U$ e $I$ podem variar em suas dimensões dependendo do tamanho de seu vetor de incorporação.

https://developers.google.com/machine-learning/recommendation/images/2Dmatrix.svg

Exemplo de matriz de interação e matrizes de usuário (à esquerda) e item (em cima). O tamanho de incorporação neste exemplo é 2. Imagem de Google Developers.

ML

Agora que sabemos o que queremos, como podemos implementar a solução?

É aqui que o aprendizado de máquina entra em ação. Aproximamos essas matrizes $U$ e $I$ com base em exemplos. Começamos com valores aleatórios para $U$ e $I$, produzindo um conjunto aleatório de números para $R_{pred}$, a matriz de interação aproximada. Podemos comparar $R_ {actual}$ e $R_ {pred}$ usando qualquer função de perda (pode pensar em algo como entropia cruzada binária , mas somando para cada elemento na matriz) e iterar usando qualquer algoritmo de otimização (pode pensar em "gradient descent"), alterando $U$ e $I$ incrementalmente para chegar mais perto de $R_ {real}$.

Depois de muitas iterações, devemos ter algo semelhante a

https://developers.google.com/machine-learning/recommendation/images/Matrixfactor.svg

A matriz de interação real à esquerda ($R_ {real}$) e a matriz aproximada, ou prevista, dadas as matrizes treinadas $U$ e $I$ ($R_ {pred}$). Imagem de Google Developers.

Depois de muitas iterações, a matriz converge na aproximação das interações reais e podemos produzir os itens com as pontuações de predição mais altas (que ele ainda não gostou). Voltando para a mulher ruiva, a modelo recomendaria "The Dark Knight Rises".

Existem outras alternativas alternativas como para perdas e algoritmos de otimização, que podem convergir mais rapidamente, mas podem ser mais caros computacionalmente, como os mínimos quadrados alternados ponderados (WALS). Os Google Developers oferecem uma comparação geral entre o "gradient descent" e o WALS. Continuaremos com a "gradient descent" assim que falaremos sobre redes neurais.

Isso faz sentido?

Por que isso faz sentido com o que descrevemos acima?

Para tornar as coisas mais simples, digamos que os encaixes dos itens sejam fixos. É mais fácil ver que, se dois usuários com comportamento muito semelhante, eles acabariam com embeddings semelhantes e, portanto, um item apreciado por um usuário seria recomendado ao outro.

🧠 Neural collaborative filtering

Parte "Neural"

A principal adição que temos nesta parte é incluir uma rede neural com esses embeddings que criamos. Isso não é diferente do processo de criação de embeddings de palavras com redes neurais. Nós aproximamos os embeddings aleatórios inicializados com base nos rótulos, usando retropropagação. É interessante adicionar funções não lineares às camadas da rede, uma vez que o "collaborative filtering" "vanilla" já cuida das combinações lineares entre os embeddings de usuário e item (no produto matriz).

Https://lh6.googleusercontent.com/kioqr4bQsvJdfvl0YJwnnyNyLG3f55ChHzkYt9cRMQYHttemX9-5vK3-005tMBZdWCSkfkSt0hQjcNxDrV46wqOLRWCLQWyuxoNfX9dSYEF7JBWXIqH1oMcQlmAy6mtMyF1KuiZ2jjs

Rede neural para "collaborative filtering", com base no usuário e embeddings de itens. Na camada de entrada, $u$ e $i$ são uma codificação ativa que representa o índice desse usuário. Imagem do artigo Neural Collaborative Filtering.

Pode-se notar que as representações aqui serão diferentes daquelas encontradas no "collaborative filtering" "vanilla". A ideia não é "substituir", mas "adicionar". Podemos realizar a fatoração da matriz como uma rede neural, concatenar as camadas finais e passá-las por uma camada final que produzirá as previsões. Todos juntos, vai parecer

Https://lh4.googleusercontent.com/_UeIXyWB5RiO-wapO60ZpeB2f9MjGmXRnJk2hCUHtXdAuGyHtbTRgO8RmYCix4bWt4Snh8LUEBgiuexUR_VmsdQjo9aH4i35G3YiI8lBcyMYbNuOs1po_xjlfhudWDpnJ5O4RsKj3mw

Arquitetura neural colaborativa. Imagem do artigo Neural Collaborative Filtering.

Esses dois modelos diferentes juntos formam a arquitetura geral. Eles podem, no entanto, ser treinados separadamente e ajustados no final (aprendizagem por transferência). Isso pode acelerar o tempo de treinamento e produzir melhores resultados.

💪 Ganhos

A questão é: a complexidade adicional vale a pena?

Ao comparar experimentalmente os resultados usando o conjunto de dados MovieLens, os resultados entre o LightFM e o neural collaborative filtering foram semelhantes (sinta-se à vontade para dar uma olhada na demo!), com um custo computacional consideravelmente maior para redes neurais.

Mas, por outro lado, obtemos mais flexibilidade com o modelo. Além do mais, podemos integrar facilmente um modelo de content based filtering (usando redes neurais) para formar um sistema de recomendação híbrido no Tensorflow. E a complexidade dos problemas que podem ser resolvidos também aumenta - problemas complexos de PNL ou de série temporal podem ser integrados. Poderíamos, por exemplo, alavancar modelos pré-treinados como o BERT e integrá-los neste sistema híbrido de recomendação.

💻 Demo

Caso esteja rodando o notebook não esqueça de fazer o download das bibliotecas e inicie o tensorboard no background:

No terminal

pip install tensorflow lightfm pandas
tensorboard --logdir 2020-09-11-neural_collaborative_filter/logs

ou no notebook

!pip install tensorflow lightfm pandas
%load_ext tensorboard
!tensorboard --logdir 2020-09-11-neural_collaborative_filter/logs &

Os dados

Interaction matrix:
[[5 3 4 3 3 5 4 0 5 3]
 [4 0 0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0 0 0]
 [4 0 0 0 0 0 0 4 4 0]
 [0 0 0 5 0 0 5 5 5 4]
 [0 0 0 0 0 0 3 0 0 0]
 [0 0 0 0 0 0 4 0 0 0]
 [4 0 0 4 0 0 0 0 4 0]]

# collapse
for dataset in ["test", "train"]:
    data[dataset] = (data[dataset].toarray() > 0).astype("int8")

# Make the ratings binary
print("Interaction matrix:")
print(data["train"][:10, :10])

print("\nRatings:")
unique_ratings = np.unique(data["train"])
print(unique_ratings)
Interaction matrix:
[[1 1 1 1 1 1 1 0 1 1]
 [1 0 0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0 0 0]
 [0 0 0 0 0 0 0 0 0 0]
 [1 0 0 0 0 0 0 1 1 0]
 [0 0 0 1 0 0 1 1 1 1]
 [0 0 0 0 0 0 1 0 0 0]
 [0 0 0 0 0 0 1 0 0 0]
 [1 0 0 1 0 0 0 0 1 0]]

Ratings:
[0 1]
from typing import List


def wide_to_long(wide: np.array, possible_ratings: List[int]) -> np.array:
    """Go from wide table to long.
    :param wide: wide array with user-item interactions
    :param possible_ratings: list of possible ratings that we may have."""

    def _get_ratings(arr: np.array, rating: int) -> np.array:
        """Generate long array for the rating provided
        :param arr: wide array with user-item interactions
        :param rating: the rating that we are interested"""
        idx = np.where(arr == rating)
        return np.vstack(
            (idx[0], idx[1], np.ones(idx[0].size, dtype="int8") * rating)
        ).T

    long_arrays = []
    for r in possible_ratings:
        long_arrays.append(_get_ratings(wide, r))

    return np.vstack(long_arrays)
long_train = wide_to_long(data["train"], unique_ratings)
df_train = pd.DataFrame(long_train, columns=["user_id", "item_id", "interaction"])
All interactions:
user_id item_id interaction
0 0 7 0
1 0 10 0
2 0 19 0
3 0 20 0
4 0 26 0
Only positive interactions:
user_id item_id interaction
1511499 0 0 1
1511500 0 1 1
1511501 0 2 1
1511502 0 3 1
1511503 0 4 1

O modelo (Neural Collaborative Filtering)

import tensorflow.keras as keras
from tensorflow.keras.layers import (
    Concatenate,
    Dense,
    Embedding,
    Flatten,
    Input,
    Multiply,
)
from tensorflow.keras.models import Model
from tensorflow.keras.regularizers import l2


def create_ncf(
    number_of_users: int,
    number_of_items: int,
    latent_dim_mf: int = 4,
    latent_dim_mlp: int = 32,
    reg_mf: int = 0,
    reg_mlp: int = 0.01,
    dense_layers: List[int] = [8, 4],
    reg_layers: List[int] = [0.01, 0.01],
    activation_dense: str = "relu",
) -> keras.Model:

    # input layer
    user = Input(shape=(), dtype="int32", name="user_id")
    item = Input(shape=(), dtype="int32", name="item_id")

    # embedding layers
    mf_user_embedding = Embedding(
        input_dim=number_of_users,
        output_dim=latent_dim_mf,
        name="mf_user_embedding",
        embeddings_initializer="RandomNormal",
        embeddings_regularizer=l2(reg_mf),
        input_length=1,
    )
    mf_item_embedding = Embedding(
        input_dim=number_of_items,
        output_dim=latent_dim_mf,
        name="mf_item_embedding",
        embeddings_initializer="RandomNormal",
        embeddings_regularizer=l2(reg_mf),
        input_length=1,
    )

    mlp_user_embedding = Embedding(
        input_dim=number_of_users,
        output_dim=latent_dim_mlp,
        name="mlp_user_embedding",
        embeddings_initializer="RandomNormal",
        embeddings_regularizer=l2(reg_mlp),
        input_length=1,
    )
    mlp_item_embedding = Embedding(
        input_dim=number_of_items,
        output_dim=latent_dim_mlp,
        name="mlp_item_embedding",
        embeddings_initializer="RandomNormal",
        embeddings_regularizer=l2(reg_mlp),
        input_length=1,
    )

    # MF vector
    mf_user_latent = Flatten()(mf_user_embedding(user))
    mf_item_latent = Flatten()(mf_item_embedding(item))
    mf_cat_latent = Multiply()([mf_user_latent, mf_item_latent])

    # MLP vector
    mlp_user_latent = Flatten()(mlp_user_embedding(user))
    mlp_item_latent = Flatten()(mlp_item_embedding(item))
    mlp_cat_latent = Concatenate()([mlp_user_latent, mlp_item_latent])

    mlp_vector = mlp_cat_latent

    # build dense layers for model
    for i in range(len(dense_layers)):
        layer = Dense(
            dense_layers[i],
            activity_regularizer=l2(reg_layers[i]),
            activation=activation_dense,
            name="layer%d" % i,
        )
        mlp_vector = layer(mlp_vector)

    predict_layer = Concatenate()([mf_cat_latent, mlp_vector])

    result = Dense(
        1, activation="sigmoid", kernel_initializer="lecun_uniform", name="interaction"
    )

    output = result(predict_layer)

    model = Model(
        inputs=[user, item],
        outputs=[output],
    )

    return model

# collapse
from tensorflow.keras.optimizers import Adam

n_users, n_items = data["train"].shape
ncf_model = create_ncf(n_users, n_items)

ncf_model.compile(
    optimizer=Adam(),
    loss="binary_crossentropy",
    metrics=[
        tf.keras.metrics.TruePositives(name="tp"),
        tf.keras.metrics.FalsePositives(name="fp"),
        tf.keras.metrics.TrueNegatives(name="tn"),
        tf.keras.metrics.FalseNegatives(name="fn"),
        tf.keras.metrics.BinaryAccuracy(name="accuracy"),
        tf.keras.metrics.Precision(name="precision"),
        tf.keras.metrics.Recall(name="recall"),
        tf.keras.metrics.AUC(name="auc"),
    ],
)
ncf_model._name = "neural_collaborative_filtering"
ncf_model.summary()
Model: "neural_collaborative_filtering"
__________________________________________________________________________________________________
Layer (type)                    Output Shape         Param #     Connected to                     
==================================================================================================
user_id (InputLayer)            [(None,)]            0                                            
__________________________________________________________________________________________________
item_id (InputLayer)            [(None,)]            0                                            
__________________________________________________________________________________________________
mlp_user_embedding (Embedding)  (None, 32)           30176       user_id[0][0]                    
__________________________________________________________________________________________________
mlp_item_embedding (Embedding)  (None, 32)           53824       item_id[0][0]                    
__________________________________________________________________________________________________
flatten_2 (Flatten)             (None, 32)           0           mlp_user_embedding[0][0]         
__________________________________________________________________________________________________
flatten_3 (Flatten)             (None, 32)           0           mlp_item_embedding[0][0]         
__________________________________________________________________________________________________
mf_user_embedding (Embedding)   (None, 4)            3772        user_id[0][0]                    
__________________________________________________________________________________________________
mf_item_embedding (Embedding)   (None, 4)            6728        item_id[0][0]                    
__________________________________________________________________________________________________
concatenate (Concatenate)       (None, 64)           0           flatten_2[0][0]                  
                                                                 flatten_3[0][0]                  
__________________________________________________________________________________________________
flatten (Flatten)               (None, 4)            0           mf_user_embedding[0][0]          
__________________________________________________________________________________________________
flatten_1 (Flatten)             (None, 4)            0           mf_item_embedding[0][0]          
__________________________________________________________________________________________________
layer0 (Dense)                  (None, 8)            520         concatenate[0][0]                
__________________________________________________________________________________________________
multiply (Multiply)             (None, 4)            0           flatten[0][0]                    
                                                                 flatten_1[0][0]                  
__________________________________________________________________________________________________
layer1 (Dense)                  (None, 4)            36          layer0[0][0]                     
__________________________________________________________________________________________________
concatenate_1 (Concatenate)     (None, 8)            0           multiply[0][0]                   
                                                                 layer1[0][0]                     
__________________________________________________________________________________________________
interaction (Dense)             (None, 1)            9           concatenate_1[0][0]              
==================================================================================================
Total params: 95,065
Trainable params: 95,065
Non-trainable params: 0
__________________________________________________________________________________________________
def make_tf_dataset(
    df: pd.DataFrame,
    targets: List[str],
    val_split: float = 0.1,
    batch_size: int = 512,
    seed=42,
):
    """Make TensorFlow dataset from Pandas DataFrame.
    :param df: input DataFrame - only contains features and target(s)
    :param targets: list of columns names corresponding to targets
    :param val_split: fraction of the data that should be used for validation
    :param batch_size: batch size for training
    :param seed: random seed for shuffling data - set to `None` to not shuffle"""

    n_val = round(df.shape[0] * val_split)
    if seed:
        # shuffle all the rows
        x = df.sample(frac=1, random_state=seed).to_dict("series")
    else:
        x = df.to_dict("series")
    y = dict()
    for t in targets:
        y[t] = x.pop(t)
    ds = tf.data.Dataset.from_tensor_slices((x, y))

    ds_val = ds.take(n_val).batch(batch_size)
    ds_train = ds.skip(n_val).batch(batch_size)
    return ds_train, ds_val
# create train and validation datasets
ds_train, ds_val = make_tf_dataset(df_train, ["interaction"])
%%time
# define logs and callbacks
logdir = os.path.join("logs", datetime.datetime.now().strftime("%Y%m%d-%H%M%S"))
tensorboard_callback = tf.keras.callbacks.TensorBoard(logdir, histogram_freq=1)
early_stopping_callback = tf.keras.callbacks.EarlyStopping(
    monitor="val_loss", patience=0
)

train_hist = ncf_model.fit(
    ds_train,
    validation_data=ds_val,
    epochs=N_EPOCHS,
    callbacks=[tensorboard_callback, early_stopping_callback],
    verbose=1,
)
Epoch 1/10
/usr/local/anaconda3/envs/inteligencia-superficial/lib/python3.7/site-packages/tensorflow/python/framework/indexed_slices.py:432: UserWarning: Converting sparse IndexedSlices to a dense Tensor of unknown shape. This may consume a large amount of memory.
  "Converting sparse IndexedSlices to a dense Tensor of unknown shape. "
   1/2789 [..............................] - ETA: 0s - loss: 2.7761 - tp: 1.0000 - fp: 32.0000 - tn: 459.0000 - fn: 20.0000 - accuracy: 0.8984 - precision: 0.0303 - recall: 0.0476 - auc: 0.4325WARNING:tensorflow:From /usr/local/anaconda3/envs/inteligencia-superficial/lib/python3.7/site-packages/tensorflow/python/ops/summary_ops_v2.py:1277: stop (from tensorflow.python.eager.profiler) is deprecated and will be removed after 2020-07-01.
Instructions for updating:
use `tf.profiler.experimental.stop` instead.
WARNING:tensorflow:Callbacks method `on_train_batch_end` is slow compared to the batch time (batch time: 0.0031s vs `on_train_batch_end` time: 0.0254s). Check your callbacks.
2789/2789 [==============================] - 9s 3ms/step - loss: 0.2318 - tp: 1691.0000 - fp: 789.0000 - tn: 1359625.0000 - fn: 65408.0000 - accuracy: 0.9536 - precision: 0.6819 - recall: 0.0252 - auc: 0.8033 - val_loss: 0.1408 - val_tp: 902.0000 - val_fp: 416.0000 - val_tn: 150669.0000 - val_fn: 6626.0000 - val_accuracy: 0.9556 - val_precision: 0.6844 - val_recall: 0.1198 - val_auc: 0.9020
Epoch 2/10
2789/2789 [==============================] - 8s 3ms/step - loss: 0.1279 - tp: 11668.0000 - fp: 6340.0000 - tn: 1354074.0000 - fn: 55431.0000 - accuracy: 0.9567 - precision: 0.6479 - recall: 0.1739 - auc: 0.9164 - val_loss: 0.1236 - val_tp: 1532.0000 - val_fp: 854.0000 - val_tn: 150231.0000 - val_fn: 5996.0000 - val_accuracy: 0.9568 - val_precision: 0.6421 - val_recall: 0.2035 - val_auc: 0.9195
Epoch 3/10
2789/2789 [==============================] - 8s 3ms/step - loss: 0.1191 - tp: 13715.0000 - fp: 7758.0000 - tn: 1352656.0000 - fn: 53384.0000 - accuracy: 0.9572 - precision: 0.6387 - recall: 0.2044 - auc: 0.9254 - val_loss: 0.1198 - val_tp: 1587.0000 - val_fp: 835.0000 - val_tn: 150250.0000 - val_fn: 5941.0000 - val_accuracy: 0.9573 - val_precision: 0.6552 - val_recall: 0.2108 - val_auc: 0.9232
Epoch 4/10
2789/2789 [==============================] - 8s 3ms/step - loss: 0.1148 - tp: 14333.0000 - fp: 7576.0000 - tn: 1352838.0000 - fn: 52766.0000 - accuracy: 0.9577 - precision: 0.6542 - recall: 0.2136 - auc: 0.9293 - val_loss: 0.1160 - val_tp: 1610.0000 - val_fp: 797.0000 - val_tn: 150288.0000 - val_fn: 5918.0000 - val_accuracy: 0.9577 - val_precision: 0.6689 - val_recall: 0.2139 - val_auc: 0.9267
Epoch 5/10
2789/2789 [==============================] - 8s 3ms/step - loss: 0.1114 - tp: 15531.0000 - fp: 7649.0000 - tn: 1352765.0000 - fn: 51568.0000 - accuracy: 0.9585 - precision: 0.6700 - recall: 0.2315 - auc: 0.9335 - val_loss: 0.1138 - val_tp: 1777.0000 - val_fp: 877.0000 - val_tn: 150208.0000 - val_fn: 5751.0000 - val_accuracy: 0.9582 - val_precision: 0.6696 - val_recall: 0.2361 - val_auc: 0.9294
Epoch 6/10
2789/2789 [==============================] - 8s 3ms/step - loss: 0.1088 - tp: 16978.0000 - fp: 8344.0000 - tn: 1352070.0000 - fn: 50121.0000 - accuracy: 0.9590 - precision: 0.6705 - recall: 0.2530 - auc: 0.9373 - val_loss: 0.1120 - val_tp: 1927.0000 - val_fp: 975.0000 - val_tn: 150110.0000 - val_fn: 5601.0000 - val_accuracy: 0.9585 - val_precision: 0.6640 - val_recall: 0.2560 - val_auc: 0.9317
Epoch 7/10
2789/2789 [==============================] - 8s 3ms/step - loss: 0.1069 - tp: 18235.0000 - fp: 9057.0000 - tn: 1351357.0000 - fn: 48864.0000 - accuracy: 0.9594 - precision: 0.6681 - recall: 0.2718 - auc: 0.9401 - val_loss: 0.1108 - val_tp: 2033.0000 - val_fp: 1031.0000 - val_tn: 150054.0000 - val_fn: 5495.0000 - val_accuracy: 0.9589 - val_precision: 0.6635 - val_recall: 0.2701 - val_auc: 0.9338
Epoch 8/10
2789/2789 [==============================] - 8s 3ms/step - loss: 0.1055 - tp: 19127.0000 - fp: 9621.0000 - tn: 1350793.0000 - fn: 47972.0000 - accuracy: 0.9597 - precision: 0.6653 - recall: 0.2851 - auc: 0.9421 - val_loss: 0.1100 - val_tp: 2113.0000 - val_fp: 1069.0000 - val_tn: 150016.0000 - val_fn: 5415.0000 - val_accuracy: 0.9591 - val_precision: 0.6640 - val_recall: 0.2807 - val_auc: 0.9350
Epoch 9/10
2789/2789 [==============================] - 8s 3ms/step - loss: 0.1046 - tp: 19749.0000 - fp: 9984.0000 - tn: 1350430.0000 - fn: 47350.0000 - accuracy: 0.9598 - precision: 0.6642 - recall: 0.2943 - auc: 0.9435 - val_loss: 0.1094 - val_tp: 2154.0000 - val_fp: 1107.0000 - val_tn: 149978.0000 - val_fn: 5374.0000 - val_accuracy: 0.9591 - val_precision: 0.6605 - val_recall: 0.2861 - val_auc: 0.9357
Epoch 10/10
2789/2789 [==============================] - 8s 3ms/step - loss: 0.1040 - tp: 20082.0000 - fp: 10168.0000 - tn: 1350246.0000 - fn: 47017.0000 - accuracy: 0.9599 - precision: 0.6639 - recall: 0.2993 - auc: 0.9445 - val_loss: 0.1090 - val_tp: 2191.0000 - val_fp: 1126.0000 - val_tn: 149959.0000 - val_fn: 5337.0000 - val_accuracy: 0.9593 - val_precision: 0.6605 - val_recall: 0.2910 - val_auc: 0.9364
CPU times: user 2min 29s, sys: 43.4 s, total: 3min 12s
Wall time: 1min 20s
long_test = wide_to_long(data["train"], unique_ratings)
df_test = pd.DataFrame(long_test, columns=["user_id", "item_id", "interaction"])
ds_test, _ = make_tf_dataset(df_test, ["interaction"], val_split=0, seed=None)
%%time
ncf_predictions = ncf_model.predict(ds_test)
df_test["ncf_predictions"] = ncf_predictions
CPU times: user 3.81 s, sys: 210 ms, total: 4.02 s
Wall time: 3.69 s
user_id item_id interaction ncf_predictions
0 0 7 0 0.523643
1 0 10 0 0.719504
2 0 19 0 0.100669
3 0 20 0 0.123813
4 0 26 0 0.102480

# collapse
data["ncf_predictions"] = df_test.pivot(
    index="user_id", columns="item_id", values="ncf_predictions"
).values
print("Neural collaborative filtering predictions")
print(data["ncf_predictions"][:10, :4])
Neural collaborative filtering predictions
[[7.7809501e-01 3.4897393e-01 2.3736593e-01 7.5093412e-01]
 [1.5352371e-01 1.8476248e-03 2.3163706e-02 3.6399364e-03]
 [4.6624422e-02 4.7096610e-04 1.2840241e-02 1.1576419e-04]
 [8.5962385e-02 1.4925003e-03 6.1967373e-03 5.1632524e-04]
 [5.8516884e-01 2.8336483e-01 7.5634271e-02 3.0715367e-01]
 [4.0988737e-01 2.2669524e-02 1.0599941e-02 4.0282601e-01]
 [6.0177052e-01 6.6075641e-01 7.8367621e-02 8.1673837e-01]
 [4.9012059e-01 8.9323461e-02 6.3689947e-03 6.7939401e-02]
 [1.5069479e-01 1.3713539e-03 2.8979778e-04 2.2239387e-03]
 [5.0181168e-01 6.9155514e-02 3.4887791e-02 4.8452517e-01]]
precision_ncf = tf.keras.metrics.Precision(top_k=TOP_K)
recall_ncf = tf.keras.metrics.Recall(top_k=TOP_K)

precision_ncf.update_state(data["test"], data["ncf_predictions"])
recall_ncf.update_state(data["test"], data["ncf_predictions"])
print(
    f"At K = {TOP_K}, we have a precision of {precision_ncf.result().numpy():.5f}",
    f"and a recall of {recall_ncf.result().numpy():.5f}",
)
At K = 5, we have a precision of 0.10859 and a recall of 0.06487
%%time
# LightFM model
def norm(x: float) -> float:
    """Normalize vector"""
    return (x - np.min(x)) / np.ptp(x)


lightfm_model = LightFM(loss="warp")
lightfm_model.fit(sparse.coo_matrix(data["train"]), epochs=N_EPOCHS)

lightfm_predictions = lightfm_model.predict(
    df_test["user_id"].values, df_test["item_id"].values
)
df_test["lightfm_predictions"] = lightfm_predictions
wide_predictions = df_test.pivot(
    index="user_id", columns="item_id", values="lightfm_predictions"
).values
data["lightfm_predictions"] = norm(wide_predictions)

# compute the metrics
precision_lightfm = tf.keras.metrics.Precision(top_k=TOP_K)
recall_lightfm = tf.keras.metrics.Recall(top_k=TOP_K)
precision_lightfm.update_state(data["test"], data["lightfm_predictions"])
recall_lightfm.update_state(data["test"], data["lightfm_predictions"])
print(
    f"At K = {TOP_K}, we have a precision of {precision_lightfm.result().numpy():.5f}",
    "and a recall of {recall_lightfm.result().numpy():.5f}",
)
At K = 5, we have a precision of 0.10541 and a recall of 0.06297
CPU times: user 1.01 s, sys: 235 ms, total: 1.25 s
Wall time: 858 ms