From a141bd39dc770c07cf48a18d3ecb8ecb55d9cdf2 Mon Sep 17 00:00:00 2001 From: liyuan97 <33855278+liyuan97@users.noreply.github.com> Date: Mon, 31 Oct 2022 10:40:40 +0800 Subject: [PATCH 1/2] Add comirec Model. (#83) * Add comirec Model. --- README.md | 6 + deepmatch/models/__init__.py | 1 + deepmatch/models/comirec.py | 188 +++++++++++++++++++++++ docs/pics/comirec.jpg | Bin 0 -> 102540 bytes docs/source/Features.md | 7 + docs/source/Models.rst | 2 +- docs/source/deepmatch.models.comirec.rst | 7 + docs/source/deepmatch.models.rst | 1 + tests/models/COMIREC_test.py | 33 ++++ 9 files changed, 244 insertions(+), 1 deletion(-) create mode 100755 deepmatch/models/comirec.py create mode 100644 docs/pics/comirec.jpg create mode 100644 docs/source/deepmatch.models.comirec.rst create mode 100644 tests/models/COMIREC_test.py diff --git a/README.md b/README.md index a02cb8c..b6451bd 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ Let's [**Get Started!**](https://deepmatch.readthedocs.io/en/latest/Quick-Start. | NCF | [WWW 2017][Neural Collaborative Filtering](https://arxiv.org/abs/1708.05031) | | SDM | [CIKM 2019][SDM: Sequential Deep Matching Model for Online Large-scale Recommender System](https://arxiv.org/abs/1909.00385) | | MIND | [CIKM 2019][Multi-interest network with dynamic routing for recommendation at Tmall](https://arxiv.org/pdf/1904.08030) | +| COMIREC | [KDD 2020][Controllable Multi-Interest Framework for Recommendation](https://arxiv.org/pdf/2005.09347.pdf) | ## Contributors([welcome to join us!](./CONTRIBUTING.md)) @@ -60,6 +61,11 @@ Let's [**Get Started!**](https://deepmatch.readthedocs.io/en/latest/Quick-Start. LeoCai

ByteDance

​ + + ​ pic
+ ​ Li Yuan +

Tencent

​ + ​ pic
Yang Jieyu diff --git a/deepmatch/models/__init__.py b/deepmatch/models/__init__.py index d31db22..d4edec2 100644 --- a/deepmatch/models/__init__.py +++ b/deepmatch/models/__init__.py @@ -4,3 +4,4 @@ from .ncf import NCF from .mind import MIND from .sdm import SDM +from .comirec import ComiRec \ No newline at end of file diff --git a/deepmatch/models/comirec.py b/deepmatch/models/comirec.py new file mode 100755 index 0000000..07420b5 --- /dev/null +++ b/deepmatch/models/comirec.py @@ -0,0 +1,188 @@ +""" +Author: + Li Yuan, lysysu@qq.com + +Reference: +Yukuo Cen, Jianwei Zhang, Xu Zou, et al. Controllable Multi-Interest Framework for Recommendation//Accepted to KDD 2020 +""" + +import tensorflow as tf +from deepctr.feature_column import SparseFeat, VarLenSparseFeat, DenseFeat, \ + embedding_lookup, varlen_embedding_lookup, get_varlen_pooling_list, get_dense_input, build_input_features +from deepctr.layers import DNN, PositionEncoding +from deepctr.layers.utils import NoMask, combined_dnn_input, add_func +from tensorflow.python.keras.layers import Concatenate, Lambda +from tensorflow.python.keras.models import Model +from ..inputs import create_embedding_matrix +from ..layers.core import CapsuleLayer, PoolingLayer, LabelAwareAttention, SampledSoftmaxLayer, EmbeddingIndex +from ..layers.interaction import SoftmaxWeightedSum +from ..utils import get_item_embedding + + +def tile_user_otherfeat(user_other_feature, interest_num): + return tf.tile(tf.expand_dims(user_other_feature, -2), [1, interest_num, 1]) + + +def tile_user_his_mask(hist_len, seq_max_len, interest_num): + return tf.tile(tf.sequence_mask(hist_len, seq_max_len), [1, interest_num, 1]) + + +def softmax_Weighted_Sum(input): + history_emb_add_pos, mask, attn = input[0], input[1], input[2] + attn = tf.transpose(attn, [0, 2, 1]) + pad = tf.ones_like(mask, dtype=tf.float32) * (-2 ** 32 + 1) + attn = tf.where(mask, attn, pad) # [batch_size, seq_len, num_interests] + attn = tf.nn.softmax(attn) # [batch_size, seq_len, num_interests] + high_capsule = tf.matmul(attn, history_emb_add_pos) + return high_capsule + + +def ComiRec(user_feature_columns, item_feature_columns, interest_num=2, p=100, interest_extractor='sa', add_pos=False, + user_dnn_hidden_units=(64, 32), dnn_activation='relu', dnn_use_bn=False, l2_reg_dnn=0, + l2_reg_embedding=1e-6, + dnn_dropout=0, output_activation='linear', sampler_config=None, seed=1024): + """Instantiates the ComiRec Model architecture. + + :param user_feature_columns: An iterable containing user's features used by the model. + :param item_feature_columns: An iterable containing item's features used by the model. + :param num_sampled: int, the number of classes to randomly sample per batch. + :param interest_num: int, the max size of user interest embedding + :param p: float,the parameter for adjusting the attention distribution in LabelAwareAttention. + :param interest_extractor: string, type of a multi-interest extraction module, 'sa' means self-attentive and 'dr' means dynamic routing + :param add_pos: bool. Whether use positional encoding layer + :param dnn_use_bn: bool. Whether use BatchNormalization before activation or not in deep net + :param user_dnn_hidden_units: list,list of positive integer or empty list, the layer number and units in each layer of user tower + :param dnn_activation: Activation function to use in deep net + :param dnn_use_bn: bool. Whether use BatchNormalization before activation or not in deep net + :param l2_reg_dnn: L2 regularizer strength applied to DNN + :param l2_reg_embedding: float. L2 regularizer strength applied to embedding vector + :param dnn_dropout: float in [0,1), the probability we will drop out a given DNN coordinate. + :param output_activation: Activation function to use in output layer + :param sampler_config: negative sample config. + :param seed: integer ,to use as random seed. + :return: A Keras model instance. + + """ + + if len(item_feature_columns) > 1: + raise ValueError("Now ComiRec only support 1 item feature like item_id") + if interest_extractor.lower() not in ['dr', 'sa']: + raise ValueError("Now ComiRec only support dr and sa two interest_extractor") + item_feature_column = item_feature_columns[0] + item_feature_name = item_feature_column.name + item_vocabulary_size = item_feature_columns[0].vocabulary_size + item_embedding_dim = item_feature_columns[0].embedding_dim + if user_dnn_hidden_units[-1] != item_embedding_dim: + user_dnn_hidden_units = tuple(list(user_dnn_hidden_units) + [item_embedding_dim]) + # item_index = Input(tensor=tf.constant([list(range(item_vocabulary_size))])) + + history_feature_list = [item_feature_name] + + features = build_input_features(user_feature_columns) + sparse_feature_columns = list( + filter(lambda x: isinstance(x, SparseFeat), user_feature_columns)) if user_feature_columns else [] + dense_feature_columns = list( + filter(lambda x: isinstance(x, DenseFeat), user_feature_columns)) if user_feature_columns else [] + varlen_sparse_feature_columns = list( + filter(lambda x: isinstance(x, VarLenSparseFeat), user_feature_columns)) if user_feature_columns else [] + history_feature_columns = [] + sparse_varlen_feature_columns = [] + history_fc_names = list(map(lambda x: "hist_" + x, history_feature_list)) + for fc in varlen_sparse_feature_columns: + feature_name = fc.name + if feature_name in history_fc_names: + history_feature_columns.append(fc) + else: + sparse_varlen_feature_columns.append(fc) + seq_max_len = history_feature_columns[0].maxlen + inputs_list = list(features.values()) + + embedding_matrix_dict = create_embedding_matrix(user_feature_columns + item_feature_columns, l2_reg_embedding, + seed=seed, prefix="") + + item_features = build_input_features(item_feature_columns) + + query_emb_list = embedding_lookup(embedding_matrix_dict, item_features, item_feature_columns, + history_feature_list, + history_feature_list, to_list=True) + keys_emb_list = embedding_lookup(embedding_matrix_dict, features, history_feature_columns, history_fc_names, + history_fc_names, to_list=True) + dnn_input_emb_list = embedding_lookup(embedding_matrix_dict, features, sparse_feature_columns, + mask_feat_list=history_feature_list, to_list=True) + dense_value_list = get_dense_input(features, dense_feature_columns) + + sequence_embed_dict = varlen_embedding_lookup(embedding_matrix_dict, features, sparse_varlen_feature_columns) + sequence_embed_list = get_varlen_pooling_list(sequence_embed_dict, features, sparse_varlen_feature_columns, + to_list=True) + + dnn_input_emb_list += sequence_embed_list + + # keys_emb = concat_func(keys_emb_list, mask=True) + # query_emb = concat_func(query_emb_list, mask=True) + + history_emb = PoolingLayer()(NoMask()(keys_emb_list)) # [None, max_len, emb_dim] + target_emb = PoolingLayer()(NoMask()(query_emb_list)) + + # target_emb_size = target_emb.get_shape()[-1].value + # max_len = history_emb.get_shape()[1].value + hist_len = features['hist_len'] + + high_capsule = None + if interest_extractor.lower() == 'dr': + high_capsule = CapsuleLayer(input_units=item_embedding_dim, + out_units=item_embedding_dim, max_len=seq_max_len, + k_max=interest_num)((history_emb, hist_len)) + elif interest_extractor.lower() == 'sa': + history_emb_add_pos = history_emb + if add_pos: + position_embedding = PositionEncoding()(history_emb) + history_emb_add_pos = add_func([history_emb_add_pos, position_embedding]) # [None, max_len, emb_dim] + + attn = DNN((item_embedding_dim * 4, interest_num), activation='tanh', l2_reg=l2_reg_dnn, + dropout_rate=dnn_dropout, use_bn=dnn_use_bn, output_activation=None, seed=seed, + name="user_dnn_attn")(history_emb_add_pos) + mask = Lambda(tile_user_his_mask, arguments={'interest_num': interest_num, + 'seq_max_len': seq_max_len})( + hist_len) # [None, interest_num, max_len] + # high_capsule = SoftmaxWeightedSum(dropout_rate=0, future_binding=False, + # seed=seed)([attn, history_emb_add_pos, mask]) + high_capsule = Lambda(softmax_Weighted_Sum)((history_emb_add_pos, mask, attn)) + + print("high_capsule", + high_capsule) # Tensor("softmax_weighted_sum/MatMul:0", shape=(None, 2, 32), dtype=float32) Tensor("capsule_layer/Reshape_1:0", shape=(None, 2, 32), dtype=float32) + if len(dnn_input_emb_list) > 0 or len(dense_value_list) > 0: + user_other_feature = combined_dnn_input(dnn_input_emb_list, dense_value_list) + other_feature_tile = Lambda(tile_user_otherfeat, arguments={'interest_num': interest_num})(user_other_feature) + print("other_feature_tile", other_feature_tile, "NoMask", NoMask()(other_feature_tile)) + user_deep_input = Concatenate()([NoMask()(other_feature_tile), high_capsule]) + else: + user_deep_input = high_capsule + + user_embeddings = DNN(user_dnn_hidden_units, dnn_activation, l2_reg_dnn, + dnn_dropout, dnn_use_bn, output_activation=output_activation, seed=seed, + name="user_dnn")( + user_deep_input) + + item_inputs_list = list(item_features.values()) + + item_embedding_matrix = embedding_matrix_dict[item_feature_name] + + item_index = EmbeddingIndex(list(range(item_vocabulary_size)))(item_features[item_feature_name]) + + item_embedding_weight = NoMask()(item_embedding_matrix(item_index)) + + pooling_item_embedding_weight = PoolingLayer()([item_embedding_weight]) + + user_embedding_final = LabelAwareAttention(k_max=interest_num, pow_p=p)((user_embeddings, target_emb)) + output = SampledSoftmaxLayer(sampler_config._asdict())( + [pooling_item_embedding_weight, user_embedding_final, item_features[item_feature_name]]) + model = Model(inputs=inputs_list + item_inputs_list, outputs=output) + + model.__setattr__("user_input", inputs_list) + model.__setattr__("user_embedding", user_embeddings) + + model.__setattr__("item_input", item_inputs_list) + model.__setattr__("item_embedding", + get_item_embedding(pooling_item_embedding_weight, item_features[item_feature_name])) + + return model diff --git a/docs/pics/comirec.jpg b/docs/pics/comirec.jpg new file mode 100644 index 0000000000000000000000000000000000000000..925022436be016f2762c82d3b1ffc1ac6e2df9c1 GIT binary patch literal 102540 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 SDM MIND - + COMIREC \ No newline at end of file diff --git a/docs/source/deepmatch.models.comirec.rst b/docs/source/deepmatch.models.comirec.rst new file mode 100644 index 0000000..42f3b18 --- /dev/null +++ b/docs/source/deepmatch.models.comirec.rst @@ -0,0 +1,7 @@ +deepmatch.models.comirec module +============================ + +.. automodule:: deepmatch.models.comirec + :members: + :no-undoc-members: + :no-show-inheritance: diff --git a/docs/source/deepmatch.models.rst b/docs/source/deepmatch.models.rst index fb069ed..a0b7fed 100644 --- a/docs/source/deepmatch.models.rst +++ b/docs/source/deepmatch.models.rst @@ -9,6 +9,7 @@ Submodules deepmatch.models.dssm deepmatch.models.fm deepmatch.models.mind + deepmatch.models.comirec deepmatch.models.ncf deepmatch.models.sdm deepmatch.models.youtubednn diff --git a/tests/models/COMIREC_test.py b/tests/models/COMIREC_test.py new file mode 100644 index 0000000..f43b71e --- /dev/null +++ b/tests/models/COMIREC_test.py @@ -0,0 +1,33 @@ +import pytest +import tensorflow as tf +from deepmatch.models import ComiRec +from deepmatch.utils import sampledsoftmaxloss, NegativeSampler +from tensorflow.python.keras import backend as K + +from ..utils import check_model, get_xy_fd + + +@pytest.mark.parametrize( + 'interest_num,p,interest_extractor,add_pos', + [(2, 1, 'sa',True), (1, 100, 'dr',False), (3, 50, 'dr', True), + ] +) +def test_COMIREC(interest_num, p, interest_extractor,add_pos): + model_name = "COMIREC" + + x, y, user_feature_columns, item_feature_columns = get_xy_fd(False) + + if tf.__version__ >= '2.0.0': + tf.compat.v1.disable_eager_execution() + else: + K.set_learning_phase(True) + sampler_config = NegativeSampler(sampler='uniform', num_sampled=2, item_name='item') + model = ComiRec(user_feature_columns, item_feature_columns, p=p, interest_num=interest_num, interest_extractor=interest_extractor, + add_pos=add_pos,sampler_config=sampler_config) + + model.compile('adam', sampledsoftmaxloss) + check_model(model, model_name, x, y) + + +if __name__ == "__main__": + pass From b681eb82495dbe1c36e2eff9d91b7bc79517281e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E6=A2=A6?= Date: Mon, 31 Oct 2022 23:25:41 +0800 Subject: [PATCH 2/2] update evaluation logic (#90) update evaluation logic --- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- .github/ISSUE_TEMPLATE/question.md | 2 +- .github/workflows/ci.yml | 21 +- .gitignore | 1 + deepmatch/__init__.py | 2 +- deepmatch/models/comirec.py | 38 +- deepmatch/models/mind.py | 4 +- deepmatch/models/sdm.py | 1 - deepmatch/models/youtubednn.py | 1 - docs/source/History.md | 1 + docs/source/conf.py | 2 +- docs/source/index.rst | 4 +- examples/colab_MovieLen1M_ComiRec.ipynb | 449 ++++++++++++++++++++++++ examples/run_sdm.py | 47 ++- examples/run_youtubednn.py | 47 ++- setup.py | 7 +- tests/models/COMIREC_test.py | 13 +- tests/models/SDM_test.py | 1 + 18 files changed, 569 insertions(+), 74 deletions(-) create mode 100644 examples/colab_MovieLen1M_ComiRec.ipynb diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index c902713..003a9bd 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -20,7 +20,7 @@ Steps to reproduce the behavior: **Operating environment(运行环境):** - python version [e.g. 3.6, 3.7, 3.8] - tensorflow version [e.g. 1.9.0, 1.14.0, 2.5.0] - - deepmatch version [e.g. 0.3.0,] + - deepmatch version [e.g. 0.3.1,] **Additional context** Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index b306341..681b91a 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -17,4 +17,4 @@ Add any other context about the problem here. **Operating environment(运行环境):** - python version [e.g. 3.6, 3.7, 3.8] - tensorflow version [e.g. 1.9.0, 1.14.0, 2.5.0] - - deepmatch version [e.g. 0.3.0,] + - deepmatch version [e.g. 0.3.1,] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1cc83f4..030c477 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: timeout-minutes: 120 strategy: matrix: - python-version: [3.6,3.7,3.8] + python-version: [3.6,3.7,3.8,3.9,3.10.7] tf-version: [1.9.0,1.14.0,2.5.0] exclude: @@ -57,12 +57,28 @@ jobs: tf-version: 2.8.0 - python-version: 3.6 tf-version: 2.9.0 + - python-version: 3.6 + tf-version: 2.10.0 - python-version: 3.9 tf-version: 1.4.0 + - python-version: 3.9 + tf-version: 1.9.0 - python-version: 3.9 tf-version: 1.15.0 - python-version: 3.9 - tf-version: 2.2.0 + tf-version: 1.14.0 + - python-version: 3.10.7 + tf-version: 1.4.0 + - python-version: 3.10.7 + tf-version: 1.9.0 + - python-version: 3.10.7 + tf-version: 1.15.0 + - python-version: 3.10.7 + tf-version: 1.14.0 + - python-version: 3.10.7 + tf-version: 2.5.0 + - python-version: 3.10.7 + tf-version: 2.6.0 steps: @@ -75,6 +91,7 @@ jobs: - name: Install dependencies run: | + sudo apt update && sudo apt install -y pkg-config libhdf5-dev pip3 install -q tensorflow==${{ matrix.tf-version }} pip install -q protobuf==3.19.0 pip install -q requests diff --git a/.gitignore b/.gitignore index b6e4761..e4a1406 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.idea # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/deepmatch/__init__.py b/deepmatch/__init__.py index 5852002..ec7afeb 100644 --- a/deepmatch/__init__.py +++ b/deepmatch/__init__.py @@ -1,4 +1,4 @@ from .utils import check_version -__version__ = '0.3.0' +__version__ = '0.3.1' check_version(__version__) diff --git a/deepmatch/models/comirec.py b/deepmatch/models/comirec.py index 07420b5..220bbcc 100755 --- a/deepmatch/models/comirec.py +++ b/deepmatch/models/comirec.py @@ -13,18 +13,20 @@ from deepctr.layers.utils import NoMask, combined_dnn_input, add_func from tensorflow.python.keras.layers import Concatenate, Lambda from tensorflow.python.keras.models import Model + from ..inputs import create_embedding_matrix -from ..layers.core import CapsuleLayer, PoolingLayer, LabelAwareAttention, SampledSoftmaxLayer, EmbeddingIndex +from ..layers.core import CapsuleLayer, PoolingLayer, MaskUserEmbedding, LabelAwareAttention, SampledSoftmaxLayer, \ + EmbeddingIndex from ..layers.interaction import SoftmaxWeightedSum from ..utils import get_item_embedding -def tile_user_otherfeat(user_other_feature, interest_num): - return tf.tile(tf.expand_dims(user_other_feature, -2), [1, interest_num, 1]) +def tile_user_otherfeat(user_other_feature, k_max): + return tf.tile(tf.expand_dims(user_other_feature, -2), [1, k_max, 1]) -def tile_user_his_mask(hist_len, seq_max_len, interest_num): - return tf.tile(tf.sequence_mask(hist_len, seq_max_len), [1, interest_num, 1]) +def tile_user_his_mask(hist_len, seq_max_len, k_max): + return tf.tile(tf.sequence_mask(hist_len, seq_max_len), [1, k_max, 1]) def softmax_Weighted_Sum(input): @@ -37,7 +39,8 @@ def softmax_Weighted_Sum(input): return high_capsule -def ComiRec(user_feature_columns, item_feature_columns, interest_num=2, p=100, interest_extractor='sa', add_pos=False, +def ComiRec(user_feature_columns, item_feature_columns, k_max=2, p=100, interest_extractor='sa', + add_pos=True, user_dnn_hidden_units=(64, 32), dnn_activation='relu', dnn_use_bn=False, l2_reg_dnn=0, l2_reg_embedding=1e-6, dnn_dropout=0, output_activation='linear', sampler_config=None, seed=1024): @@ -45,12 +48,10 @@ def ComiRec(user_feature_columns, item_feature_columns, interest_num=2, p=100, i :param user_feature_columns: An iterable containing user's features used by the model. :param item_feature_columns: An iterable containing item's features used by the model. - :param num_sampled: int, the number of classes to randomly sample per batch. - :param interest_num: int, the max size of user interest embedding + :param k_max: int, the max size of user interest embedding :param p: float,the parameter for adjusting the attention distribution in LabelAwareAttention. :param interest_extractor: string, type of a multi-interest extraction module, 'sa' means self-attentive and 'dr' means dynamic routing :param add_pos: bool. Whether use positional encoding layer - :param dnn_use_bn: bool. Whether use BatchNormalization before activation or not in deep net :param user_dnn_hidden_units: list,list of positive integer or empty list, the layer number and units in each layer of user tower :param dnn_activation: Activation function to use in deep net :param dnn_use_bn: bool. Whether use BatchNormalization before activation or not in deep net @@ -131,29 +132,25 @@ def ComiRec(user_feature_columns, item_feature_columns, interest_num=2, p=100, i if interest_extractor.lower() == 'dr': high_capsule = CapsuleLayer(input_units=item_embedding_dim, out_units=item_embedding_dim, max_len=seq_max_len, - k_max=interest_num)((history_emb, hist_len)) + k_max=k_max)((history_emb, hist_len)) elif interest_extractor.lower() == 'sa': history_emb_add_pos = history_emb if add_pos: position_embedding = PositionEncoding()(history_emb) history_emb_add_pos = add_func([history_emb_add_pos, position_embedding]) # [None, max_len, emb_dim] - attn = DNN((item_embedding_dim * 4, interest_num), activation='tanh', l2_reg=l2_reg_dnn, + attn = DNN((item_embedding_dim * 4, k_max), activation='tanh', l2_reg=l2_reg_dnn, dropout_rate=dnn_dropout, use_bn=dnn_use_bn, output_activation=None, seed=seed, name="user_dnn_attn")(history_emb_add_pos) - mask = Lambda(tile_user_his_mask, arguments={'interest_num': interest_num, + mask = Lambda(tile_user_his_mask, arguments={'k_max': k_max, 'seq_max_len': seq_max_len})( - hist_len) # [None, interest_num, max_len] - # high_capsule = SoftmaxWeightedSum(dropout_rate=0, future_binding=False, - # seed=seed)([attn, history_emb_add_pos, mask]) + hist_len) # [None, k_max, max_len] + high_capsule = Lambda(softmax_Weighted_Sum)((history_emb_add_pos, mask, attn)) - print("high_capsule", - high_capsule) # Tensor("softmax_weighted_sum/MatMul:0", shape=(None, 2, 32), dtype=float32) Tensor("capsule_layer/Reshape_1:0", shape=(None, 2, 32), dtype=float32) if len(dnn_input_emb_list) > 0 or len(dense_value_list) > 0: user_other_feature = combined_dnn_input(dnn_input_emb_list, dense_value_list) - other_feature_tile = Lambda(tile_user_otherfeat, arguments={'interest_num': interest_num})(user_other_feature) - print("other_feature_tile", other_feature_tile, "NoMask", NoMask()(other_feature_tile)) + other_feature_tile = Lambda(tile_user_otherfeat, arguments={'k_max': k_max})(user_other_feature) user_deep_input = Concatenate()([NoMask()(other_feature_tile), high_capsule]) else: user_deep_input = high_capsule @@ -173,7 +170,8 @@ def ComiRec(user_feature_columns, item_feature_columns, interest_num=2, p=100, i pooling_item_embedding_weight = PoolingLayer()([item_embedding_weight]) - user_embedding_final = LabelAwareAttention(k_max=interest_num, pow_p=p)((user_embeddings, target_emb)) + user_embedding_final = LabelAwareAttention(k_max=k_max, pow_p=p)((user_embeddings, target_emb)) + output = SampledSoftmaxLayer(sampler_config._asdict())( [pooling_item_embedding_weight, user_embedding_final, item_features[item_feature_name]]) model = Model(inputs=inputs_list + item_inputs_list, outputs=output) diff --git a/deepmatch/models/mind.py b/deepmatch/models/mind.py index a6f2831..9cd1161 100755 --- a/deepmatch/models/mind.py +++ b/deepmatch/models/mind.py @@ -53,11 +53,9 @@ def MIND(user_feature_columns, item_feature_columns, k_max=2, p=100, dynamic_k=F :param user_feature_columns: An iterable containing user's features used by the model. :param item_feature_columns: An iterable containing item's features used by the model. - :param num_sampled: int, the number of classes to randomly sample per batch. :param k_max: int, the max size of user interest embedding :param p: float,the parameter for adjusting the attention distribution in LabelAwareAttention. :param dynamic_k: bool, whether or not use dynamic interest number - :param dnn_use_bn: bool. Whether use BatchNormalization before activation or not in deep net :param user_dnn_hidden_units: list,list of positive integer or empty list, the layer number and units in each layer of user tower :param dnn_activation: Activation function to use in deep net :param dnn_use_bn: bool. Whether use BatchNormalization before activation or not in deep net @@ -169,7 +167,7 @@ def MIND(user_feature_columns, item_feature_columns, k_max=2, p=100, dynamic_k=F user_embedding_final = LabelAwareAttention(k_max=k_max, pow_p=p)((user_embeddings, target_emb, interest_num)) else: user_embedding_final = LabelAwareAttention(k_max=k_max, pow_p=p)((user_embeddings, target_emb)) - print("swc") + output = SampledSoftmaxLayer(sampler_config._asdict())( [pooling_item_embedding_weight, user_embedding_final, item_features[item_feature_name]]) model = Model(inputs=inputs_list + item_inputs_list, outputs=output) diff --git a/deepmatch/models/sdm.py b/deepmatch/models/sdm.py index 3fb2bdc..773f5e3 100644 --- a/deepmatch/models/sdm.py +++ b/deepmatch/models/sdm.py @@ -30,7 +30,6 @@ def SDM(user_feature_columns, item_feature_columns, history_feature_list, units= :param user_feature_columns: An iterable containing user's features used by the model. :param item_feature_columns: An iterable containing item's features used by the model. :param history_feature_list: list,to indicate short and prefer sequence sparse field - :param num_sampled: int, the number of classes to randomly sample per batch. :param units: int, dimension for each output layer :param rnn_layers: int, layer number of rnn :param dropout_rate: float in [0,1), the probability we will drop out a given DNN coordinate. diff --git a/deepmatch/models/youtubednn.py b/deepmatch/models/youtubednn.py index 5def6f8..b8e19f1 100644 --- a/deepmatch/models/youtubednn.py +++ b/deepmatch/models/youtubednn.py @@ -23,7 +23,6 @@ def YoutubeDNN(user_feature_columns, item_feature_columns, :param user_feature_columns: An iterable containing user's features used by the model. :param item_feature_columns: An iterable containing item's features used by the model. - :param num_sampled: int, the number of classes to randomly sample per batch. :param user_dnn_hidden_units: list,list of positive integer or empty list, the layer number and units in each layer of user tower :param dnn_activation: Activation function to use in deep net :param dnn_use_bn: bool. Whether use BatchNormalization before activation or not in deep net diff --git a/docs/source/History.md b/docs/source/History.md index cb5e222..4d22d8b 100644 --- a/docs/source/History.md +++ b/docs/source/History.md @@ -1,4 +1,5 @@ # History +- 10/31/2022 : [v0.3.1](https://github.com/shenweichen/DeepMatch/releases/tag/v0.3.1) released.Add `ComiRec` model . - 07/04/2022 : [v0.3.0](https://github.com/shenweichen/DeepMatch/releases/tag/v0.3.0) released.Support different negative sampling strategies, including `inbatch`, `uniform`, `frequency`, `adaptive`. - 06/17/2022 : [v0.2.1](https://github.com/shenweichen/DeepMatch/releases/tag/v0.2.1) released.Fix some bugs. - 10/12/2020 : [v0.2.0](https://github.com/shenweichen/DeepMatch/releases/tag/v0.2.0) released.Support different initializers for different embedding weights and loading pretrained embeddings. diff --git a/docs/source/conf.py b/docs/source/conf.py index 6d4ebaf..a999397 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -26,7 +26,7 @@ # The short X.Y version version = '' # The full version, including alpha/beta/rc tags -release = '0.3.0' +release = '0.3.1' # -- General configuration --------------------------------------------------- diff --git a/docs/source/index.rst b/docs/source/index.rst index db54bb5..f7583bc 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -37,12 +37,12 @@ You can read the latest code at https://github.com/shenweichen/DeepMatch News ----- +10/31/2022 : Add `ComiRec` . `Changelog `_ + 07/04/2022 : Support different negative sampling strategies, including `inbatch` , `uniform` , `frequency` , `adaptive` . `Changelog `_ 06/17/2022 : Fix some bugs. `Changelog `_ -10/12/2020 : Support different initializers for different embedding weights and loading pretrained embeddings. `Changelog `_ - DisscussionGroup ----------------------- diff --git a/examples/colab_MovieLen1M_ComiRec.ipynb b/examples/colab_MovieLen1M_ComiRec.ipynb new file mode 100644 index 0000000..386942c --- /dev/null +++ b/examples/colab_MovieLen1M_ComiRec.ipynb @@ -0,0 +1,449 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "id": "rtox72csOQUN" + }, + "source": [ + "# DeepMatch 样例代码\n", + "- https://github.com/shenweichen/DeepMatch\n", + "- https://deepmatch.readthedocs.io/en/latest/" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "bTWHz-heMkyw" + }, + "source": [ + "# 下载movielens-1M数据 安装依赖包" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "yTl6d6jO1oqf", + "outputId": "ee7303f1-8970-4726-a9f1-368798077228" + }, + "outputs": [], + "source": [ + "! wget http://files.grouplens.org/datasets/movielens/ml-1m.zip -O ./ml-1m.zip \n", + "! wget https://raw.githubusercontent.com/shenweichen/DeepMatch/master/examples/preprocess.py -O preprocess.py\n", + "! unzip -o ml-1m.zip \n", + "! pip uninstall -y -q tensorflow\n", + "! pip install -q tensorflow-gpu==2.5.0\n", + "! pip install -q deepmatch" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "p9UxNHuPMuW2" + }, + "source": [ + "# 导入需要的库" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": { + "id": "C_ZR6gzp1E2N" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/swc/anaconda3/lib/python3.6/site-packages/tensorflow/python/framework/dtypes.py:469: FutureWarning: Passing (type, 1) or '1type' as a synonym of type is deprecated; in a future version of numpy, it will be understood as (type, (1,)) / '(1,)type'.\n", + " _np_qint8 = np.dtype([(\"qint8\", np.int8, 1)])\n", + "/Users/swc/anaconda3/lib/python3.6/site-packages/tensorflow/python/framework/dtypes.py:470: FutureWarning: Passing (type, 1) or '1type' as a synonym of type is deprecated; in a future version of numpy, it will be understood as (type, (1,)) / '(1,)type'.\n", + " _np_quint8 = np.dtype([(\"quint8\", np.uint8, 1)])\n", + "/Users/swc/anaconda3/lib/python3.6/site-packages/tensorflow/python/framework/dtypes.py:471: FutureWarning: Passing (type, 1) or '1type' as a synonym of type is deprecated; in a future version of numpy, it will be understood as (type, (1,)) / '(1,)type'.\n", + " _np_qint16 = np.dtype([(\"qint16\", np.int16, 1)])\n", + "/Users/swc/anaconda3/lib/python3.6/site-packages/tensorflow/python/framework/dtypes.py:472: FutureWarning: Passing (type, 1) or '1type' as a synonym of type is deprecated; in a future version of numpy, it will be understood as (type, (1,)) / '(1,)type'.\n", + " _np_quint16 = np.dtype([(\"quint16\", np.uint16, 1)])\n", + "/Users/swc/anaconda3/lib/python3.6/site-packages/tensorflow/python/framework/dtypes.py:473: FutureWarning: Passing (type, 1) or '1type' as a synonym of type is deprecated; in a future version of numpy, it will be understood as (type, (1,)) / '(1,)type'.\n", + " _np_qint32 = np.dtype([(\"qint32\", np.int32, 1)])\n", + "/Users/swc/anaconda3/lib/python3.6/site-packages/tensorflow/python/framework/dtypes.py:476: FutureWarning: Passing (type, 1) or '1type' as a synonym of type is deprecated; in a future version of numpy, it will be understood as (type, (1,)) / '(1,)type'.\n", + " np_resource = np.dtype([(\"resource\", np.ubyte, 1)])\n", + "/Users/swc/anaconda3/lib/python3.6/importlib/_bootstrap.py:219: RuntimeWarning: compiletime version 3.5 of module 'tensorflow.python.framework.fast_tensor_util' does not match runtime version 3.6\n", + " return f(*args, **kwds)\n" + ] + } + ], + "source": [ + "import pandas as pd\n", + "from deepctr.feature_column import SparseFeat, VarLenSparseFeat\n", + "from preprocess import gen_data_set, gen_model_input\n", + "from sklearn.preprocessing import LabelEncoder\n", + "from tensorflow.python.keras import backend as K\n", + "from tensorflow.python.keras.models import Model\n", + "\n", + "from deepmatch.models import *\n", + "from deepmatch.utils import sampledsoftmaxloss, NegativeSampler" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "fQq6O9XAMzPF" + }, + "source": [ + "# 读取数据" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "lcO29zFb21Od", + "outputId": "bfeed1ac-99f2-425f-dda6-10b83be721fe" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/swc/anaconda3/lib/python3.6/site-packages/ipykernel_launcher.py:4: ParserWarning: Falling back to the 'python' engine because the 'c' engine does not support regex separators (separators > 1 char and different from '\\s+' are interpreted as regex); you can avoid this warning by specifying engine='python'.\n", + " after removing the cwd from sys.path.\n", + "/Users/swc/anaconda3/lib/python3.6/site-packages/ipykernel_launcher.py:6: ParserWarning: Falling back to the 'python' engine because the 'c' engine does not support regex separators (separators > 1 char and different from '\\s+' are interpreted as regex); you can avoid this warning by specifying engine='python'.\n", + " \n", + "/Users/swc/anaconda3/lib/python3.6/site-packages/ipykernel_launcher.py:8: ParserWarning: Falling back to the 'python' engine because the 'c' engine does not support regex separators (separators > 1 char and different from '\\s+' are interpreted as regex); you can avoid this warning by specifying engine='python'.\n", + " \n" + ] + } + ], + "source": [ + "data_path = \"./\"\n", + "\n", + "unames = ['user_id','gender','age','occupation','zip']\n", + "user = pd.read_csv(data_path+'ml-1m/users.dat',sep='::',header=None,names=unames)\n", + "rnames = ['user_id','movie_id','rating','timestamp']\n", + "ratings = pd.read_csv(data_path+'ml-1m/ratings.dat',sep='::',header=None,names=rnames)\n", + "mnames = ['movie_id','title','genres']\n", + "movies = pd.read_csv(data_path+'ml-1m/movies.dat',sep='::',header=None,names=mnames,encoding=\"unicode_escape\")\n", + "movies['genres'] = list(map(lambda x: x.split('|')[0], movies['genres'].values))\n", + "\n", + "data = pd.merge(pd.merge(ratings,movies),user)#.iloc[:10000]" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "L0yCWxQxM3se" + }, + "source": [ + "# 构建特征列,训练模型,导出embedding" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "BMOvk_de2ML3", + "outputId": "962afe1c-d387-4345-861f-e9b974a0b495" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 6040/6040 [00:11<00:00, 508.25it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "8 8\n", + "make sure the activation function use training flag properly call() got an unexpected keyword argument 'training'\n", + "make sure the activation function use training flag properly call() got an unexpected keyword argument 'training'\n", + "make sure the activation function use training flag properly call() got an unexpected keyword argument 'training'\n", + "make sure the activation function use training flag properly call() got an unexpected keyword argument 'training'\n", + "make sure the activation function use training flag properly call() got an unexpected keyword argument 'training'\n", + "make sure the activation function use training flag properly call() got an unexpected keyword argument 'training'\n", + "make sure the activation function use training flag properly call() got an unexpected keyword argument 'training'\n", + "make sure the activation function use training flag properly call() got an unexpected keyword argument 'training'\n", + "make sure the activation function use training flag properly call() got an unexpected keyword argument 'training'\n", + "make sure the activation function use training flag properly call() got an unexpected keyword argument 'training'\n", + "Epoch 1/20\n", + "988129/988129 [==============================] - 111s - loss: 5.1306 \n", + "Epoch 2/20\n", + "988129/988129 [==============================] - 110s - loss: 4.4118 \n", + "Epoch 3/20\n", + "988129/988129 [==============================] - 111s - loss: 4.1463 \n", + "Epoch 4/20\n", + "988129/988129 [==============================] - 116s - loss: 3.9994 \n", + "Epoch 5/20\n", + "988129/988129 [==============================] - 115s - loss: 3.8970 \n", + "Epoch 6/20\n", + "988129/988129 [==============================] - 124s - loss: 3.8210 \n", + "Epoch 7/20\n", + "988129/988129 [==============================] - 117s - loss: 3.7645 \n", + "Epoch 8/20\n", + "988129/988129 [==============================] - 112s - loss: 3.7182 \n", + "Epoch 9/20\n", + "988129/988129 [==============================] - 112s - loss: 3.6805 \n", + "Epoch 10/20\n", + "988129/988129 [==============================] - 111s - loss: 3.6507 \n", + "Epoch 11/20\n", + "988129/988129 [==============================] - 137s - loss: 3.6256 \n", + "Epoch 12/20\n", + "988129/988129 [==============================] - 132s - loss: 3.6034 \n", + "Epoch 13/20\n", + "988129/988129 [==============================] - 118s - loss: 3.5852 \n", + "Epoch 14/20\n", + "988129/988129 [==============================] - 108s - loss: 3.5706 \n", + "Epoch 15/20\n", + "988129/988129 [==============================] - 108s - loss: 3.5567 \n", + "Epoch 16/20\n", + "988129/988129 [==============================] - 109s - loss: 3.5453 \n", + "Epoch 17/20\n", + "988129/988129 [==============================] - 148s - loss: 3.5338 \n", + "Epoch 18/20\n", + "988129/988129 [==============================] - 123s - loss: 3.5255 \n", + "Epoch 19/20\n", + "988129/988129 [==============================] - 296s - loss: 3.5165 \n", + "Epoch 20/20\n", + "988129/988129 [==============================] - 121s - loss: 3.5099 \n", + "(6040, 2, 32)\n", + "(3706, 32)\n" + ] + } + ], + "source": [ + "#data = pd.read_csvdata = pd.read_csv(\"./movielens_sample.txt\")\n", + "sparse_features = [\"movie_id\", \"user_id\",\n", + " \"gender\", \"age\", \"occupation\", \"zip\", \"genres\"]\n", + "SEQ_LEN = 50\n", + "negsample = 0\n", + "\n", + "# 1.Label Encoding for sparse features,and process sequence features with `gen_date_set` and `gen_model_input`\n", + "\n", + "feature_max_idx = {}\n", + "for feature in sparse_features:\n", + " lbe = LabelEncoder()\n", + " data[feature] = lbe.fit_transform(data[feature]) + 1\n", + " feature_max_idx[feature] = data[feature].max() + 1\n", + "\n", + "user_profile = data[[\"user_id\", \"gender\", \"age\", \"occupation\", \"zip\"]].drop_duplicates('user_id')\n", + "\n", + "item_profile = data[[\"movie_id\"]].drop_duplicates('movie_id')\n", + "\n", + "user_profile.set_index(\"user_id\", inplace=True)\n", + "\n", + "user_item_list = data.groupby(\"user_id\")['movie_id'].apply(list)\n", + "\n", + "train_set, test_set = gen_data_set(data, SEQ_LEN, negsample)\n", + "\n", + "train_model_input, train_label = gen_model_input(train_set, user_profile, SEQ_LEN)\n", + "test_model_input, test_label = gen_model_input(test_set, user_profile, SEQ_LEN)\n", + "\n", + "# 2.count #unique features for each sparse field and generate feature config for sequence feature\n", + "\n", + "embedding_dim = 32\n", + "\n", + "user_feature_columns = [SparseFeat('user_id', feature_max_idx['user_id'], 16),\n", + " SparseFeat(\"gender\", feature_max_idx['gender'], 16),\n", + " SparseFeat(\"age\", feature_max_idx['age'], 16),\n", + " SparseFeat(\"occupation\", feature_max_idx['occupation'], 16),\n", + " SparseFeat(\"zip\", feature_max_idx['zip'], 16),\n", + " VarLenSparseFeat(SparseFeat('hist_movie_id', feature_max_idx['movie_id'], embedding_dim,\n", + " embedding_name=\"movie_id\"), SEQ_LEN, 'mean', 'hist_len'),\n", + " VarLenSparseFeat(SparseFeat('hist_genres', feature_max_idx['genres'], embedding_dim,\n", + " embedding_name=\"genres\"), SEQ_LEN, 'mean', 'hist_len'),\n", + " ]\n", + "\n", + "item_feature_columns = [SparseFeat('movie_id', feature_max_idx['movie_id'], embedding_dim)]\n", + "\n", + "from collections import Counter\n", + "train_counter = Counter(train_model_input['movie_id'])\n", + "item_count = [train_counter.get(i,0) for i in range(item_feature_columns[0].vocabulary_size)]\n", + "sampler_config = NegativeSampler('frequency',num_sampled=255,item_name=\"movie_id\",item_count=item_count)\n", + "\n", + "# 3.Define Model and train\n", + "\n", + "import tensorflow as tf\n", + "if tf.__version__ >= '2.0.0':\n", + " tf.compat.v1.disable_eager_execution()\n", + "else:\n", + " K.set_learning_phase(True)\n", + " \n", + "#model = YoutubeDNN(user_feature_columns, item_feature_columns, user_dnn_hidden_units=(128,64, embedding_dim), sampler_config=sampler_config)\n", + "model = ComiRec(user_feature_columns,item_feature_columns,k_max=2, user_dnn_hidden_units=(128,64, embedding_dim), sampler_config=sampler_config)\n", + "\n", + "model.compile(optimizer=\"adam\", loss=sampledsoftmaxloss)\n", + "\n", + "history = model.fit(train_model_input, train_label, # train_label,\n", + " batch_size=512, epochs=20, verbose=1, validation_split=0.0, )\n", + "\n", + "# 4. Generate user features for testing and full item features for retrieval\n", + "test_user_model_input = test_model_input\n", + "all_item_model_input = {\"movie_id\": item_profile['movie_id'].values,}\n", + "\n", + "user_embedding_model = Model(inputs=model.user_input, outputs=model.user_embedding)\n", + "item_embedding_model = Model(inputs=model.item_input, outputs=model.item_embedding)\n", + "\n", + "user_embs = user_embedding_model.predict(test_user_model_input, batch_size=2 ** 12)\n", + "# user_embs = user_embs[:, i, :] # i in [0,k_max) if MIND\n", + "item_embs = item_embedding_model.predict(all_item_model_input, batch_size=2 ** 12)\n", + "\n", + "print(user_embs.shape)\n", + "print(item_embs.shape)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "w_G3KWslKmJo" + }, + "source": [ + "# 使用faiss进行ANN查找并评估结果" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "5SvyQLNVKkcs" + }, + "source": [] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "j2ZNYNBOOqrN", + "outputId": "2eec5e82-2d2b-4fe0-9b83-2a74a4dc52ba" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Looking in indexes: https://pypi.org/simple, https://us-python.pkg.dev/colab-wheels/public/simple/\n", + "Requirement already satisfied: faiss-cpu in /usr/local/lib/python3.7/dist-packages (1.7.2)\n" + ] + } + ], + "source": [ + "! pip install faiss-cpu" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "6TY1l27iJU8U", + "outputId": "5a8ccdd3-af70-4c48-b859-84c4befddfdd" + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "100%|██████████| 6040/6040 [00:00<00:00, 6105.92it/s]\n", + "100%|██████████| 6040/6040 [00:01<00:00, 5487.52it/s]\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "recall 0.43642384105960264\n", + "hr 0.43642384105960264\n" + ] + } + ], + "source": [ + "import heapq\n", + "from collections import defaultdict\n", + "from tqdm import tqdm\n", + "import numpy as np\n", + "import faiss\n", + "from deepmatch.utils import recall_N\n", + "\n", + "k_max = 2\n", + "topN = 50\n", + "test_true_label = {line[0]: [line[1]] for line in test_set}\n", + "\n", + "index = faiss.IndexFlatIP(embedding_dim)\n", + "# faiss.normalize_L2(item_embs)\n", + "index.add(item_embs)\n", + "# faiss.normalize_L2(user_embs)\n", + "\n", + "if len(user_embs.shape) == 2: # multi interests model's shape = 3 (MIND,ComiRec)\n", + " user_embs = np.expand_dims(user_embs, axis=1)\n", + "\n", + "score_dict = defaultdict(dict)\n", + "for k in range(k_max):\n", + " user_emb = user_embs[:, k, :]\n", + " D, I = index.search(np.ascontiguousarray(user_emb), topN)\n", + " for i, uid in tqdm(enumerate(test_user_model_input['user_id']), total=len(test_user_model_input['user_id'])):\n", + " if np.abs(user_emb[i]).max() < 1e-8:\n", + " continue\n", + " for score, itemid in zip(D[i], I[i]):\n", + " score_dict[uid][itemid] = max(score, score_dict[uid].get(itemid, float(\"-inf\")))\n", + "\n", + "s = []\n", + "hit = 0\n", + "for i, uid in enumerate(test_user_model_input['user_id']):\n", + " pred = [item_profile['movie_id'].values[x[0]] for x in\n", + " heapq.nlargest(topN, score_dict[uid].items(), key=lambda x: x[1])]\n", + " filter_item = None\n", + " recall_score = recall_N(test_true_label[uid], pred, N=topN)\n", + " s.append(recall_score)\n", + " if test_true_label[uid] in pred:\n", + " hit += 1\n", + "\n", + "print(\"recall\", np.mean(s))\n", + "print(\"hr\", hit / len(test_user_model_input['user_id']))" + ] + } + ], + "metadata": { + "accelerator": "GPU", + "colab": { + "collapsed_sections": [], + "name": "colab_MovieLen1M_YoutubeDNN.ipynb", + "provenance": [] + }, + "gpuClass": "standard", + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.5" + } + }, + "nbformat": 4, + "nbformat_minor": 1 +} diff --git a/examples/run_sdm.py b/examples/run_sdm.py index acf5ccc..0be81e9 100644 --- a/examples/run_sdm.py +++ b/examples/run_sdm.py @@ -101,30 +101,47 @@ print(user_embs.shape) print(item_embs.shape) - # test_true_label = {line[0]: [line[1]] for line in test_set} + # #5. [Optional] ANN search by faiss and evaluate the result # + # import heapq + # from collections import defaultdict + # from tqdm import tqdm # import numpy as np # import faiss - # from tqdm import tqdm # from deepmatch.utils import recall_N # + # k_max = 1 + # topN = 50 + # test_true_label = {line[0]: [line[1]] for line in test_set} + # # index = faiss.IndexFlatIP(embedding_dim) # # faiss.normalize_L2(item_embs) # index.add(item_embs) # # faiss.normalize_L2(user_embs) - # D, I = index.search(np.ascontiguousarray(user_embs), 50) + # + # if len(user_embs.shape) == 2: # multi interests model's shape = 3 (MIND,ComiRec) + # user_embs = np.expand_dims(user_embs, axis=1) + # + # score_dict = defaultdict(dict) + # for k in range(k_max): + # user_emb = user_embs[:, k, :] + # D, I = index.search(np.ascontiguousarray(user_emb), topN) + # for i, uid in tqdm(enumerate(test_user_model_input['user_id']), total=len(test_user_model_input['user_id'])): + # if np.abs(user_emb[i]).max() < 1e-8: + # continue + # for score, itemid in zip(D[i], I[i]): + # score_dict[uid][itemid] = max(score, score_dict[uid].get(itemid, float("-inf"))) + # # s = [] # hit = 0 - # for i, uid in tqdm(enumerate(test_user_model_input['user_id'])): - # try: - # pred = [item_profile['movie_id'].values[x] for x in I[i]] - # filter_item = None - # recall_score = recall_N(test_true_label[uid], pred, N=50) - # s.append(recall_score) - # if test_true_label[uid] in pred: - # hit += 1 - # except: - # print(i) - # print("") + # for i, uid in enumerate(test_user_model_input['user_id']): + # pred = [item_profile['movie_id'].values[x[0]] for x in + # heapq.nlargest(topN, score_dict[uid].items(), key=lambda x: x[1])] + # filter_item = None + # recall_score = recall_N(test_true_label[uid], pred, N=topN) + # s.append(recall_score) + # if test_true_label[uid] in pred: + # hit += 1 + # # print("recall", np.mean(s)) - # print("hit rate", hit / len(test_user_model_input['user_id'])) + # print("hr", hit / len(test_user_model_input['user_id'])) diff --git a/examples/run_youtubednn.py b/examples/run_youtubednn.py index 5e85385..6d7ca05 100644 --- a/examples/run_youtubednn.py +++ b/examples/run_youtubednn.py @@ -87,7 +87,6 @@ item_embedding_model = Model(inputs=model.item_input, outputs=model.item_embedding) user_embs = user_embedding_model.predict(test_user_model_input, batch_size=2 ** 12) - # user_embs = user_embs[:, i, :] # i in [0,k_max) if MIND item_embs = item_embedding_model.predict(all_item_model_input, batch_size=2 ** 12) print(user_embs.shape) @@ -95,29 +94,45 @@ # 5. [Optional] ANN search by faiss and evaluate the result - # test_true_label = {line[0]:[line[1]] for line in test_set} - # + # import heapq + # from collections import defaultdict + # from tqdm import tqdm # import numpy as np # import faiss - # from tqdm import tqdm # from deepmatch.utils import recall_N - # + # + # k_max = 2 + # topN = 50 + # test_true_label = {line[0]: [line[1]] for line in test_set} + # # index = faiss.IndexFlatIP(embedding_dim) # # faiss.normalize_L2(item_embs) # index.add(item_embs) # # faiss.normalize_L2(user_embs) - # D, I = index.search(np.ascontiguousarray(user_embs), 50) + # + # if len(user_embs.shape) == 2: # multi interests model's shape = 3 (MIND,ComiRec) + # user_embs = np.expand_dims(user_embs, axis=1) + # + # score_dict = defaultdict(dict) + # for k in range(k_max): + # user_emb = user_embs[:, k, :] + # D, I = index.search(np.ascontiguousarray(user_emb), topN) + # for i, uid in tqdm(enumerate(test_user_model_input['user_id']), total=len(test_user_model_input['user_id'])): + # if np.abs(user_emb[i]).max() < 1e-8: + # continue + # for score, itemid in zip(D[i], I[i]): + # score_dict[uid][itemid] = max(score, score_dict[uid].get(itemid, float("-inf"))) + # # s = [] # hit = 0 - # for i, uid in tqdm(enumerate(test_user_model_input['user_id'])): - # try: - # pred = [item_profile['movie_id'].values[x] for x in I[i]] - # filter_item = None - # recall_score = recall_N(test_true_label[uid], pred, N=50) - # s.append(recall_score) - # if test_true_label[uid] in pred: - # hit += 1 - # except: - # print(i) + # for i, uid in enumerate(test_user_model_input['user_id']): + # pred = [item_profile['movie_id'].values[x[0]] for x in + # heapq.nlargest(topN, score_dict[uid].items(), key=lambda x: x[1])] + # filter_item = None + # recall_score = recall_N(test_true_label[uid], pred, N=topN) + # s.append(recall_score) + # if test_true_label[uid] in pred: + # hit += 1 + # # print("recall", np.mean(s)) # print("hr", hit / len(test_user_model_input['user_id'])) diff --git a/setup.py b/setup.py index ad3b8fd..d8a60f2 100644 --- a/setup.py +++ b/setup.py @@ -4,12 +4,12 @@ long_description = fh.read() REQUIRED_PACKAGES = [ - 'requests', "deepctr~=0.9.1" + 'requests', "deepctr~=0.9.2" ] setuptools.setup( name="deepmatch", - version="0.3.0", + version="0.3.1", author="Weichen Shen", author_email="weichenswc@163.com", description="Deep matching model library for recommendations, advertising. It's easy to train models and to **export representation vectors** for user and item which can be used for **ANN search**.", @@ -34,10 +34,11 @@ 'Intended Audience :: Education', 'Intended Audience :: Science/Research', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', 'Topic :: Scientific/Engineering', 'Topic :: Scientific/Engineering :: Artificial Intelligence', 'Topic :: Software Development', diff --git a/tests/models/COMIREC_test.py b/tests/models/COMIREC_test.py index f43b71e..c0a7cf0 100644 --- a/tests/models/COMIREC_test.py +++ b/tests/models/COMIREC_test.py @@ -4,15 +4,15 @@ from deepmatch.utils import sampledsoftmaxloss, NegativeSampler from tensorflow.python.keras import backend as K -from ..utils import check_model, get_xy_fd +from tests.utils import check_model, get_xy_fd @pytest.mark.parametrize( - 'interest_num,p,interest_extractor,add_pos', - [(2, 1, 'sa',True), (1, 100, 'dr',False), (3, 50, 'dr', True), + 'k_max,p,interest_extractor,add_pos', + [(2, 1, 'sa', True), (1, 100, 'dr', False), (3, 50, 'dr', True), ] ) -def test_COMIREC(interest_num, p, interest_extractor,add_pos): +def test_COMIREC(k_max, p, interest_extractor, add_pos): model_name = "COMIREC" x, y, user_feature_columns, item_feature_columns = get_xy_fd(False) @@ -22,9 +22,8 @@ def test_COMIREC(interest_num, p, interest_extractor,add_pos): else: K.set_learning_phase(True) sampler_config = NegativeSampler(sampler='uniform', num_sampled=2, item_name='item') - model = ComiRec(user_feature_columns, item_feature_columns, p=p, interest_num=interest_num, interest_extractor=interest_extractor, - add_pos=add_pos,sampler_config=sampler_config) - + model = ComiRec(user_feature_columns, item_feature_columns, k_max=k_max, p=p, interest_extractor=interest_extractor, + add_pos=add_pos, sampler_config=sampler_config) model.compile('adam', sampledsoftmaxloss) check_model(model, model_name, x, y) diff --git a/tests/models/SDM_test.py b/tests/models/SDM_test.py index e213a8d..0c04c0e 100644 --- a/tests/models/SDM_test.py +++ b/tests/models/SDM_test.py @@ -12,6 +12,7 @@ def test_SDM(): if tf.__version__ >= '2.0.0': tf.compat.v1.disable_eager_execution() + #tf.compat.v1.disable_v2_behavior() else: K.set_learning_phase(True)