From ced170bbe6cfaaa1ff1259addbd213f3d78378cb Mon Sep 17 00:00:00 2001 From: Lanling Xu Date: Thu, 14 Jul 2022 18:17:14 +0800 Subject: [PATCH 1/4] FEA: Add Python code formatting in github action according to PEP8 --- .github/workflows/python-package.yml | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 8737db634..207fafeb2 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -66,4 +66,23 @@ jobs: python -m pytest -v tests/config/test_config.py export PYTHONPATH=. python tests/config/test_command_line.py --use_gpu=False --valid_metric=Recall@10 --split_ratio=[0.7,0.2,0.1] --metrics='["Recall"]' --topk=[10] --epochs=200 --eval_setting='LO_RS' --learning_rate=0.3 - + # Use black to test code format + # Reference code: + # https://black.readthedocs.io/en/stable/integrations/github_actions.html + # https://github.com/marketplace/actions/run-black-formatter + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Install dependencies + run: | + pip install black[jupyter] + - name: Test code format + uses: psf/black@stable + id: action-black + with: + options: "." + - name: Apply code-format changes + uses: stefanzweifel/git-auto-commit-action@v4 + with: + commit_message: Format Python code according to PEP8 \ No newline at end of file From cdea5fd2110a94b525016c49844efe7094e92316 Mon Sep 17 00:00:00 2001 From: Sherry-XLL Date: Thu, 14 Jul 2022 10:17:54 +0000 Subject: [PATCH 2/4] Format Python code according to PEP8 --- docs/source/conf.py | 27 +- recbole/__init__.py | 2 +- recbole/config/configurator.py | 480 ++++--- recbole/data/__init__.py | 7 +- .../data/dataloader/abstract_dataloader.py | 77 +- recbole/data/dataloader/general_dataloader.py | 81 +- .../data/dataloader/knowledge_dataloader.py | 27 +- recbole/data/dataloader/user_dataloader.py | 4 +- recbole/data/dataset/customized_dataset.py | 42 +- recbole/data/dataset/dataset.py | 752 +++++++---- recbole/data/dataset/decisiontree_dataset.py | 17 +- recbole/data/dataset/kg_dataset.py | 250 ++-- recbole/data/dataset/sequential_dataset.py | 94 +- recbole/data/interaction.py | 65 +- recbole/data/utils.py | 178 ++- recbole/evaluator/base_metric.py | 29 +- recbole/evaluator/collector.py | 150 ++- recbole/evaluator/evaluator.py | 5 +- recbole/evaluator/metrics.py | 127 +- recbole/evaluator/register.py | 21 +- recbole/evaluator/utils.py | 6 +- recbole/model/abstract_recommender.py | 183 ++- .../model/context_aware_recommender/afm.py | 26 +- .../context_aware_recommender/autoint.py | 44 +- .../model/context_aware_recommender/dcn.py | 32 +- .../model/context_aware_recommender/deepfm.py | 20 +- .../model/context_aware_recommender/dssm.py | 36 +- .../model/context_aware_recommender/ffm.py | 148 +- recbole/model/context_aware_recommender/fm.py | 8 +- .../model/context_aware_recommender/fnn.py | 20 +- .../model/context_aware_recommender/fwfm.py | 48 +- .../model/context_aware_recommender/nfm.py | 20 +- .../model/context_aware_recommender/pnn.py | 44 +- .../context_aware_recommender/widedeep.py | 16 +- .../context_aware_recommender/xdeepfm.py | 49 +- recbole/model/exlib_recommender/lightgbm.py | 7 +- recbole/model/exlib_recommender/xgboost.py | 7 +- recbole/model/general_recommender/__init__.py | 1 - recbole/model/general_recommender/admmslim.py | 42 +- recbole/model/general_recommender/bpr.py | 14 +- recbole/model/general_recommender/cdae.py | 67 +- recbole/model/general_recommender/convncf.py | 32 +- recbole/model/general_recommender/dgcf.py | 80 +- recbole/model/general_recommender/dmf.py | 108 +- recbole/model/general_recommender/ease.py | 12 +- recbole/model/general_recommender/enmf.py | 43 +- recbole/model/general_recommender/fism.py | 81 +- recbole/model/general_recommender/gcmc.py | 184 ++- recbole/model/general_recommender/itemknn.py | 58 +- recbole/model/general_recommender/lightgcn.py | 54 +- recbole/model/general_recommender/line.py | 60 +- .../model/general_recommender/macridvae.py | 51 +- recbole/model/general_recommender/multidae.py | 19 +- recbole/model/general_recommender/multivae.py | 35 +- recbole/model/general_recommender/nais.py | 129 +- recbole/model/general_recommender/nceplrec.py | 41 +- recbole/model/general_recommender/ncl.py | 99 +- recbole/model/general_recommender/neumf.py | 54 +- recbole/model/general_recommender/ngcf.py | 57 +- recbole/model/general_recommender/nncf.py | 125 +- recbole/model/general_recommender/pop.py | 10 +- recbole/model/general_recommender/ract.py | 75 +- recbole/model/general_recommender/recvae.py | 44 +- recbole/model/general_recommender/sgl.py | 86 +- recbole/model/general_recommender/simplex.py | 119 +- .../model/general_recommender/slimelastic.py | 22 +- .../model/general_recommender/spectralcf.py | 56 +- recbole/model/init.py | 4 +- .../model/knowledge_aware_recommender/cfkg.py | 31 +- .../model/knowledge_aware_recommender/cke.py | 27 +- .../model/knowledge_aware_recommender/kgat.py | 105 +- .../model/knowledge_aware_recommender/kgcn.py | 81 +- .../knowledge_aware_recommender/kgnnls.py | 111 +- .../model/knowledge_aware_recommender/ktup.py | 97 +- .../model/knowledge_aware_recommender/mkr.py | 142 +- .../knowledge_aware_recommender/ripplenet.py | 50 +- recbole/model/layers.py | 436 ++++-- recbole/model/loss.py | 21 +- .../model/sequential_recommender/bert4rec.py | 125 +- recbole/model/sequential_recommender/caser.py | 65 +- recbole/model/sequential_recommender/core.py | 81 +- recbole/model/sequential_recommender/dien.py | 238 ++-- recbole/model/sequential_recommender/din.py | 69 +- recbole/model/sequential_recommender/fdsa.py | 80 +- .../model/sequential_recommender/fossil.py | 33 +- recbole/model/sequential_recommender/fpmc.py | 10 +- recbole/model/sequential_recommender/gcsan.py | 70 +- .../model/sequential_recommender/gru4rec.py | 24 +- .../model/sequential_recommender/gru4recf.py | 48 +- .../model/sequential_recommender/gru4reckg.py | 36 +- recbole/model/sequential_recommender/hgn.py | 34 +- recbole/model/sequential_recommender/hrm.py | 41 +- recbole/model/sequential_recommender/ksr.py | 91 +- .../model/sequential_recommender/lightsans.py | 77 +- recbole/model/sequential_recommender/narm.py | 30 +- .../model/sequential_recommender/nextitnet.py | 84 +- recbole/model/sequential_recommender/npe.py | 16 +- .../model/sequential_recommender/repeatnet.py | 65 +- recbole/model/sequential_recommender/s3rec.py | 258 ++-- .../model/sequential_recommender/sasrec.py | 46 +- .../model/sequential_recommender/sasrecf.py | 82 +- recbole/model/sequential_recommender/shan.py | 102 +- recbole/model/sequential_recommender/sine.py | 47 +- recbole/model/sequential_recommender/srgnn.py | 49 +- recbole/model/sequential_recommender/stamp.py | 22 +- .../model/sequential_recommender/transrec.py | 26 +- recbole/quick_start/__init__.py | 7 +- recbole/quick_start/quick_start.py | 96 +- recbole/sampler/sampler.py | 91 +- recbole/trainer/__init__.py | 2 +- recbole/trainer/hyper_tuning.py | 229 ++-- recbole/trainer/trainer.py | 825 ++++++++---- recbole/utils/__init__.py | 42 +- recbole/utils/case_study.py | 14 +- recbole/utils/enum_type.py | 22 +- recbole/utils/logger.py | 45 +- recbole/utils/url.py | 54 +- recbole/utils/utils.py | 57 +- recbole/utils/wandblogger.py | 25 +- run_example/case_study_example.py | 16 +- run_example/save_and_load_example.py | 26 +- run_example/session_based_rec_example.py | 78 +- run_hyper.py | 28 +- run_recbole.py | 47 +- setup.py | 59 +- tests/config/test_command_line.py | 18 +- tests/config/test_config.py | 172 +-- tests/config/test_overall.py | 120 +- tests/data/test_dataloader.py | 394 +++--- tests/data/test_dataset.py | 1199 ++++++++++------- .../test_evaluation_setting.py | 128 +- tests/metrics/test_loss_metrics.py | 34 +- tests/metrics/test_rank_metrics.py | 30 +- tests/metrics/test_topk_metrics.py | 160 ++- tests/model/test_model_auto.py | 557 ++++---- tests/model/test_model_manual.py | 24 +- 136 files changed, 7860 insertions(+), 4800 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index befbeb492..ac35ef63b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -13,17 +13,18 @@ import sphinx_rtd_theme import os import sys -sys.path.insert(0, os.path.abspath('../..')) + +sys.path.insert(0, os.path.abspath("../..")) # -- Project information ----------------------------------------------------- -project = 'RecBole' -copyright = '2020, RecBole Contributors' -author = 'AIBox RecBole group' +project = "RecBole" +copyright = "2020, RecBole Contributors" +author = "AIBox RecBole group" # The full version, including alpha/beta/rc tags -release = '0.2.0' +release = "0.2.0" # -- General configuration --------------------------------------------------- @@ -32,24 +33,24 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.napoleon', - 'sphinx.ext.viewcode', - 'sphinx_copybutton', + "sphinx.ext.autodoc", + "sphinx.ext.napoleon", + "sphinx.ext.viewcode", + "sphinx_copybutton", ] autodoc_mock_imports = ["pandas", "pyecharts"] # autoclass_content = 'both' # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = 'en' +language = "en" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -65,10 +66,10 @@ # html_theme = 'alabaster' -html_theme = 'sphinx_rtd_theme' +html_theme = "sphinx_rtd_theme" html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] diff --git a/recbole/__init__.py b/recbole/__init__.py index f229eca4f..4f425d7e1 100644 --- a/recbole/__init__.py +++ b/recbole/__init__.py @@ -2,4 +2,4 @@ from __future__ import print_function from __future__ import division -__version__ = '1.0.1' +__version__ = "1.0.1" diff --git a/recbole/config/configurator.py b/recbole/config/configurator.py index c23cfe716..8cf05d1eb 100644 --- a/recbole/config/configurator.py +++ b/recbole/config/configurator.py @@ -19,12 +19,22 @@ from logging import getLogger from recbole.evaluator import metric_types, smaller_metrics -from recbole.utils import get_model, Enum, EvaluatorType, ModelType, InputType, \ - general_arguments, training_arguments, evaluation_arguments, dataset_arguments, set_color +from recbole.utils import ( + get_model, + Enum, + EvaluatorType, + ModelType, + InputType, + general_arguments, + training_arguments, + evaluation_arguments, + dataset_arguments, + set_color, +) class Config(object): - """ Configurator module that load the defined parameters. + """Configurator module that load the defined parameters. Configurator module will first load the default parameters from the fixed properties in RecBole and then load parameters from the external input. @@ -54,7 +64,9 @@ class Config(object): Finally the learning_rate is equal to 0.02. """ - def __init__(self, model=None, dataset=None, config_file_list=None, config_dict=None): + def __init__( + self, model=None, dataset=None, config_file_list=None, config_dict=None + ): """ Args: model (str/AbstractRecommender): the model name or the model class, default is None, if it is None, config @@ -71,7 +83,9 @@ def __init__(self, model=None, dataset=None, config_file_list=None, config_dict= self.cmd_config_dict = self._load_cmd_line() self._merge_external_config_dict() - self.model, self.model_class, self.dataset = self._get_model_and_dataset(model, dataset) + self.model, self.model_class, self.dataset = self._get_model_and_dataset( + model, dataset + ) self._load_internal_config_dict(self.model, self.model_class, self.dataset) self.final_config_dict = self._get_final_config_dict() self._set_default_parameters() @@ -81,38 +95,40 @@ def __init__(self, model=None, dataset=None, config_file_list=None, config_dict= def _init_parameters_category(self): self.parameters = dict() - self.parameters['General'] = general_arguments - self.parameters['Training'] = training_arguments - self.parameters['Evaluation'] = evaluation_arguments - self.parameters['Dataset'] = dataset_arguments + self.parameters["General"] = general_arguments + self.parameters["Training"] = training_arguments + self.parameters["Evaluation"] = evaluation_arguments + self.parameters["Dataset"] = dataset_arguments def _build_yaml_loader(self): loader = yaml.FullLoader loader.add_implicit_resolver( - u'tag:yaml.org,2002:float', + "tag:yaml.org,2002:float", re.compile( - u'''^(?: + """^(?: [-+]?(?:[0-9][0-9_]*)\\.[0-9_]*(?:[eE][-+]?[0-9]+)? |[-+]?(?:[0-9][0-9_]*)(?:[eE][-+]?[0-9]+) |\\.[0-9_]+(?:[eE][-+][0-9]+)? |[-+]?[0-9][0-9_]*(?::[0-5]?[0-9])+\\.[0-9_]* |[-+]?\\.(?:inf|Inf|INF) - |\\.(?:nan|NaN|NAN))$''', re.X - ), list(u'-+0123456789.') + |\\.(?:nan|NaN|NAN))$""", + re.X, + ), + list("-+0123456789."), ) return loader def _convert_config_dict(self, config_dict): - r"""This function convert the str parameters to their original type. - - """ + r"""This function convert the str parameters to their original type.""" for key in config_dict: param = config_dict[key] if not isinstance(param, str): continue try: value = eval(param) - if value is not None and not isinstance(value, (str, int, float, list, tuple, dict, bool, Enum)): + if value is not None and not isinstance( + value, (str, int, float, list, tuple, dict, bool, Enum) + ): value = param except (NameError, SyntaxError, TypeError): if isinstance(param, str): @@ -131,8 +147,10 @@ def _load_config_files(self, file_list): file_config_dict = dict() if file_list: for file in file_list: - with open(file, 'r', encoding='utf-8') as f: - file_config_dict.update(yaml.load(f.read(), Loader=self.yaml_loader)) + with open(file, "r", encoding="utf-8") as f: + file_config_dict.update( + yaml.load(f.read(), Loader=self.yaml_loader) + ) return file_config_dict def _load_variable_config_dict(self, config_dict): @@ -142,9 +160,7 @@ def _load_variable_config_dict(self, config_dict): return self._convert_config_dict(config_dict) if config_dict else dict() def _load_cmd_line(self): - r""" Read parameters from command line and convert it to str. - - """ + r"""Read parameters from command line and convert it to str.""" cmd_config_dict = dict() unrecognized_args = [] if "ipykernel_launcher" not in sys.argv[0]: @@ -153,13 +169,23 @@ def _load_cmd_line(self): unrecognized_args.append(arg) continue cmd_arg_name, cmd_arg_value = arg[2:].split("=") - if cmd_arg_name in cmd_config_dict and cmd_arg_value != cmd_config_dict[cmd_arg_name]: - raise SyntaxError("There are duplicate commend arg '%s' with different value." % arg) + if ( + cmd_arg_name in cmd_config_dict + and cmd_arg_value != cmd_config_dict[cmd_arg_name] + ): + raise SyntaxError( + "There are duplicate commend arg '%s' with different value." + % arg + ) else: cmd_config_dict[cmd_arg_name] = cmd_arg_value if len(unrecognized_args) > 0: logger = getLogger() - logger.warning('command line args [{}] will not be used in RecBole'.format(' '.join(unrecognized_args))) + logger.warning( + "command line args [{}] will not be used in RecBole".format( + " ".join(unrecognized_args) + ) + ) cmd_config_dict = self._convert_config_dict(cmd_config_dict) return cmd_config_dict @@ -174,11 +200,11 @@ def _get_model_and_dataset(self, model, dataset): if model is None: try: - model = self.external_config_dict['model'] + model = self.external_config_dict["model"] except KeyError: raise KeyError( - 'model need to be specified in at least one of the these ways: ' - '[model variable, config file, config dict, command line] ' + "model need to be specified in at least one of the these ways: " + "[model variable, config file, config dict, command line] " ) if not isinstance(model, str): final_model_class = model @@ -189,11 +215,11 @@ def _get_model_and_dataset(self, model, dataset): if dataset is None: try: - final_dataset = self.external_config_dict['dataset'] + final_dataset = self.external_config_dict["dataset"] except KeyError: raise KeyError( - 'dataset need to be specified in at least one of the these ways: ' - '[dataset variable, config file, config dict, command line] ' + "dataset need to be specified in at least one of the these ways: " + "[dataset variable, config file, config dict, command line] " ) else: final_dataset = dataset @@ -201,7 +227,7 @@ def _get_model_and_dataset(self, model, dataset): return final_model, final_model_class, final_dataset def _update_internal_config_dict(self, file): - with open(file, 'r', encoding='utf-8') as f: + with open(file, "r", encoding="utf-8") as f: config_dict = yaml.load(f.read(), Loader=self.yaml_loader) if config_dict is not None: self.internal_config_dict.update(config_dict) @@ -209,50 +235,85 @@ def _update_internal_config_dict(self, file): def _load_internal_config_dict(self, model, model_class, dataset): current_path = os.path.dirname(os.path.realpath(__file__)) - overall_init_file = os.path.join(current_path, '../properties/overall.yaml') - model_init_file = os.path.join(current_path, '../properties/model/' + model + '.yaml') - sample_init_file = os.path.join(current_path, '../properties/dataset/sample.yaml') - dataset_init_file = os.path.join(current_path, '../properties/dataset/' + dataset + '.yaml') - - quick_start_config_path = os.path.join(current_path, '../properties/quick_start_config/') - context_aware_init = os.path.join(quick_start_config_path, 'context-aware.yaml') - context_aware_on_ml_100k_init = os.path.join(quick_start_config_path, 'context-aware_ml-100k.yaml') - DIN_init = os.path.join(quick_start_config_path, 'sequential_DIN.yaml') - DIN_on_ml_100k_init = os.path.join(quick_start_config_path, 'sequential_DIN_on_ml-100k.yaml') - sequential_init = os.path.join(quick_start_config_path, 'sequential.yaml') - special_sequential_on_ml_100k_init = os.path.join(quick_start_config_path, 'special_sequential_on_ml-100k.yaml') - sequential_embedding_model_init = os.path.join(quick_start_config_path, 'sequential_embedding_model.yaml') - knowledge_base_init = os.path.join(quick_start_config_path, 'knowledge_base.yaml') + overall_init_file = os.path.join(current_path, "../properties/overall.yaml") + model_init_file = os.path.join( + current_path, "../properties/model/" + model + ".yaml" + ) + sample_init_file = os.path.join( + current_path, "../properties/dataset/sample.yaml" + ) + dataset_init_file = os.path.join( + current_path, "../properties/dataset/" + dataset + ".yaml" + ) + + quick_start_config_path = os.path.join( + current_path, "../properties/quick_start_config/" + ) + context_aware_init = os.path.join(quick_start_config_path, "context-aware.yaml") + context_aware_on_ml_100k_init = os.path.join( + quick_start_config_path, "context-aware_ml-100k.yaml" + ) + DIN_init = os.path.join(quick_start_config_path, "sequential_DIN.yaml") + DIN_on_ml_100k_init = os.path.join( + quick_start_config_path, "sequential_DIN_on_ml-100k.yaml" + ) + sequential_init = os.path.join(quick_start_config_path, "sequential.yaml") + special_sequential_on_ml_100k_init = os.path.join( + quick_start_config_path, "special_sequential_on_ml-100k.yaml" + ) + sequential_embedding_model_init = os.path.join( + quick_start_config_path, "sequential_embedding_model.yaml" + ) + knowledge_base_init = os.path.join( + quick_start_config_path, "knowledge_base.yaml" + ) self.internal_config_dict = dict() - for file in [overall_init_file, model_init_file, sample_init_file, dataset_init_file]: + for file in [ + overall_init_file, + model_init_file, + sample_init_file, + dataset_init_file, + ]: if os.path.isfile(file): config_dict = self._update_internal_config_dict(file) if file == dataset_init_file: - self.parameters['Dataset'] += [ - key for key in config_dict.keys() if key not in self.parameters['Dataset'] + self.parameters["Dataset"] += [ + key + for key in config_dict.keys() + if key not in self.parameters["Dataset"] ] - self.internal_config_dict['MODEL_TYPE'] = model_class.type - if self.internal_config_dict['MODEL_TYPE'] == ModelType.GENERAL: + self.internal_config_dict["MODEL_TYPE"] = model_class.type + if self.internal_config_dict["MODEL_TYPE"] == ModelType.GENERAL: pass - elif self.internal_config_dict['MODEL_TYPE'] in {ModelType.CONTEXT, ModelType.DECISIONTREE}: + elif self.internal_config_dict["MODEL_TYPE"] in { + ModelType.CONTEXT, + ModelType.DECISIONTREE, + }: self._update_internal_config_dict(context_aware_init) - if dataset == 'ml-100k': + if dataset == "ml-100k": self._update_internal_config_dict(context_aware_on_ml_100k_init) - elif self.internal_config_dict['MODEL_TYPE'] == ModelType.SEQUENTIAL: - if model in ['DIN', 'DIEN']: + elif self.internal_config_dict["MODEL_TYPE"] == ModelType.SEQUENTIAL: + if model in ["DIN", "DIEN"]: self._update_internal_config_dict(DIN_init) - if dataset == 'ml-100k': + if dataset == "ml-100k": self._update_internal_config_dict(DIN_on_ml_100k_init) - elif model in ['GRU4RecKG', 'KSR']: + elif model in ["GRU4RecKG", "KSR"]: self._update_internal_config_dict(sequential_embedding_model_init) else: self._update_internal_config_dict(sequential_init) - if dataset == 'ml-100k' and model in ['GRU4RecF', 'SASRecF', 'FDSA', 'S3Rec']: - self._update_internal_config_dict(special_sequential_on_ml_100k_init) + if dataset == "ml-100k" and model in [ + "GRU4RecF", + "SASRecF", + "FDSA", + "S3Rec", + ]: + self._update_internal_config_dict( + special_sequential_on_ml_100k_init + ) - elif self.internal_config_dict['MODEL_TYPE'] == ModelType.KNOWLEDGE: + elif self.internal_config_dict["MODEL_TYPE"] == ModelType.KNOWLEDGE: self._update_internal_config_dict(knowledge_base_init) def _get_final_config_dict(self): @@ -262,158 +323,214 @@ def _get_final_config_dict(self): return final_config_dict def _set_default_parameters(self): - self.final_config_dict['dataset'] = self.dataset - self.final_config_dict['model'] = self.model - if self.dataset == 'ml-100k': + self.final_config_dict["dataset"] = self.dataset + self.final_config_dict["model"] = self.model + if self.dataset == "ml-100k": current_path = os.path.dirname(os.path.realpath(__file__)) - self.final_config_dict['data_path'] = os.path.join(current_path, '../dataset_example/' + self.dataset) + self.final_config_dict["data_path"] = os.path.join( + current_path, "../dataset_example/" + self.dataset + ) else: - self.final_config_dict['data_path'] = os.path.join(self.final_config_dict['data_path'], self.dataset) - - if hasattr(self.model_class, 'input_type'): - self.final_config_dict['MODEL_INPUT_TYPE'] = self.model_class.input_type - elif 'loss_type' in self.final_config_dict: - if self.final_config_dict['loss_type'] in ['CE']: - if self.final_config_dict['MODEL_TYPE'] == ModelType.SEQUENTIAL and \ - self.final_config_dict['train_neg_sample_args'] is not None: - raise ValueError(f"train_neg_sample_args [{self.final_config_dict['train_neg_sample_args']}] should be None " - f"when the loss_type is CE.") - self.final_config_dict['MODEL_INPUT_TYPE'] = InputType.POINTWISE - elif self.final_config_dict['loss_type'] in ['BPR']: - self.final_config_dict['MODEL_INPUT_TYPE'] = InputType.PAIRWISE + self.final_config_dict["data_path"] = os.path.join( + self.final_config_dict["data_path"], self.dataset + ) + + if hasattr(self.model_class, "input_type"): + self.final_config_dict["MODEL_INPUT_TYPE"] = self.model_class.input_type + elif "loss_type" in self.final_config_dict: + if self.final_config_dict["loss_type"] in ["CE"]: + if ( + self.final_config_dict["MODEL_TYPE"] == ModelType.SEQUENTIAL + and self.final_config_dict["train_neg_sample_args"] is not None + ): + raise ValueError( + f"train_neg_sample_args [{self.final_config_dict['train_neg_sample_args']}] should be None " + f"when the loss_type is CE." + ) + self.final_config_dict["MODEL_INPUT_TYPE"] = InputType.POINTWISE + elif self.final_config_dict["loss_type"] in ["BPR"]: + self.final_config_dict["MODEL_INPUT_TYPE"] = InputType.PAIRWISE else: - raise ValueError('Either Model has attr \'input_type\',' 'or arg \'loss_type\' should exist in config.') + raise ValueError( + "Either Model has attr 'input_type'," + "or arg 'loss_type' should exist in config." + ) - metrics = self.final_config_dict['metrics'] + metrics = self.final_config_dict["metrics"] if isinstance(metrics, str): - self.final_config_dict['metrics'] = [metrics] + self.final_config_dict["metrics"] = [metrics] eval_type = set() - for metric in self.final_config_dict['metrics']: + for metric in self.final_config_dict["metrics"]: if metric.lower() in metric_types: eval_type.add(metric_types[metric.lower()]) else: raise NotImplementedError(f"There is no metric named '{metric}'") if len(eval_type) > 1: - raise RuntimeError('Ranking metrics and value metrics can not be used at the same time.') - self.final_config_dict['eval_type'] = eval_type.pop() - - if self.final_config_dict['MODEL_TYPE'] == ModelType.SEQUENTIAL and not self.final_config_dict['repeatable']: - raise ValueError('Sequential models currently only support repeatable recommendation, ' - 'please set `repeatable` as `True`.') + raise RuntimeError( + "Ranking metrics and value metrics can not be used at the same time." + ) + self.final_config_dict["eval_type"] = eval_type.pop() + + if ( + self.final_config_dict["MODEL_TYPE"] == ModelType.SEQUENTIAL + and not self.final_config_dict["repeatable"] + ): + raise ValueError( + "Sequential models currently only support repeatable recommendation, " + "please set `repeatable` as `True`." + ) - valid_metric = self.final_config_dict['valid_metric'].split('@')[0] - self.final_config_dict['valid_metric_bigger'] = False if valid_metric.lower() in smaller_metrics else True + valid_metric = self.final_config_dict["valid_metric"].split("@")[0] + self.final_config_dict["valid_metric_bigger"] = ( + False if valid_metric.lower() in smaller_metrics else True + ) - topk = self.final_config_dict['topk'] + topk = self.final_config_dict["topk"] if isinstance(topk, (int, list)): if isinstance(topk, int): topk = [topk] for k in topk: if k <= 0: raise ValueError( - f'topk must be a positive integer or a list of positive integers, but get `{k}`' + f"topk must be a positive integer or a list of positive integers, but get `{k}`" ) - self.final_config_dict['topk'] = topk + self.final_config_dict["topk"] = topk else: - raise TypeError(f'The topk [{topk}] must be a integer, list') + raise TypeError(f"The topk [{topk}] must be a integer, list") - if 'additional_feat_suffix' in self.final_config_dict: - ad_suf = self.final_config_dict['additional_feat_suffix'] + if "additional_feat_suffix" in self.final_config_dict: + ad_suf = self.final_config_dict["additional_feat_suffix"] if isinstance(ad_suf, str): - self.final_config_dict['additional_feat_suffix'] = [ad_suf] + self.final_config_dict["additional_feat_suffix"] = [ad_suf] # train_neg_sample_args checking default_train_neg_sample_args = { - 'distribution': 'uniform', - 'sample_num': 1, - 'dynamic': False, - 'candidate_num': 0 + "distribution": "uniform", + "sample_num": 1, + "dynamic": False, + "candidate_num": 0, } - if self.final_config_dict['train_neg_sample_args'] is not None: - if not isinstance(self.final_config_dict['train_neg_sample_args'], dict): - raise ValueError(f"train_neg_sample_args:[{self.final_config_dict['train_neg_sample_args']}] should be a dict.") + if self.final_config_dict["train_neg_sample_args"] is not None: + if not isinstance(self.final_config_dict["train_neg_sample_args"], dict): + raise ValueError( + f"train_neg_sample_args:[{self.final_config_dict['train_neg_sample_args']}] should be a dict." + ) for op_args in default_train_neg_sample_args: - if op_args not in self.final_config_dict['train_neg_sample_args']: - self.final_config_dict['train_neg_sample_args'][op_args] = default_train_neg_sample_args[op_args] + if op_args not in self.final_config_dict["train_neg_sample_args"]: + self.final_config_dict["train_neg_sample_args"][ + op_args + ] = default_train_neg_sample_args[op_args] # eval_args checking default_eval_args = { - 'split': {'RS': [0.8, 0.1, 0.1]}, - 'order': 'RO', - 'group_by': 'user', - 'mode': 'full' + "split": {"RS": [0.8, 0.1, 0.1]}, + "order": "RO", + "group_by": "user", + "mode": "full", } - if not isinstance(self.final_config_dict['eval_args'], dict): - raise ValueError(f"eval_args:[{self.final_config_dict['eval_args']}] should be a dict.") + if not isinstance(self.final_config_dict["eval_args"], dict): + raise ValueError( + f"eval_args:[{self.final_config_dict['eval_args']}] should be a dict." + ) for op_args in default_eval_args: - if op_args not in self.final_config_dict['eval_args']: - self.final_config_dict['eval_args'][op_args] = default_eval_args[op_args] - - if (self.final_config_dict['eval_args']['mode'] == 'full' - and self.final_config_dict['eval_type'] == EvaluatorType.VALUE): - raise NotImplementedError('Full sort evaluation do not match value-based metrics!') + if op_args not in self.final_config_dict["eval_args"]: + self.final_config_dict["eval_args"][op_args] = default_eval_args[ + op_args + ] + + if ( + self.final_config_dict["eval_args"]["mode"] == "full" + and self.final_config_dict["eval_type"] == EvaluatorType.VALUE + ): + raise NotImplementedError( + "Full sort evaluation do not match value-based metrics!" + ) def _init_device(self): - gpu_id = self.final_config_dict['gpu_id'] + gpu_id = self.final_config_dict["gpu_id"] os.environ["CUDA_VISIBLE_DEVICES"] = gpu_id import torch - if 'local_rank' not in self.final_config_dict: - self.final_config_dict['single_spec'] = True - self.final_config_dict['local_rank'] = 0 - self.final_config_dict['device'] = torch.device("cpu") if len(gpu_id) == 0 or not torch.cuda.is_available() else torch.device("cuda") + if "local_rank" not in self.final_config_dict: + self.final_config_dict["single_spec"] = True + self.final_config_dict["local_rank"] = 0 + self.final_config_dict["device"] = ( + torch.device("cpu") + if len(gpu_id) == 0 or not torch.cuda.is_available() + else torch.device("cuda") + ) else: - assert len(gpu_id.split(',')) >= self.final_config_dict['nproc'] + assert len(gpu_id.split(",")) >= self.final_config_dict["nproc"] torch.distributed.init_process_group( - backend='nccl', - rank=self.final_config_dict['local_rank'], - world_size=self.final_config_dict['world_size'], - init_method='tcp://' + self.final_config_dict['ip'] + ':' + str(self.final_config_dict['port']) + backend="nccl", + rank=self.final_config_dict["local_rank"], + world_size=self.final_config_dict["world_size"], + init_method="tcp://" + + self.final_config_dict["ip"] + + ":" + + str(self.final_config_dict["port"]), + ) + self.final_config_dict["device"] = torch.device( + "cuda", self.final_config_dict["local_rank"] ) - self.final_config_dict['device'] = torch.device("cuda", self.final_config_dict['local_rank']) - self.final_config_dict['single_spec'] = False - torch.cuda.set_device(self.final_config_dict['local_rank']) - if self.final_config_dict['local_rank'] != 0: - self.final_config_dict['state'] = 'error' - self.final_config_dict['show_progress'] = False - self.final_config_dict['verbose'] = False + self.final_config_dict["single_spec"] = False + torch.cuda.set_device(self.final_config_dict["local_rank"]) + if self.final_config_dict["local_rank"] != 0: + self.final_config_dict["state"] = "error" + self.final_config_dict["show_progress"] = False + self.final_config_dict["verbose"] = False def _set_train_neg_sample_args(self): - train_neg_sample_args = self.final_config_dict['train_neg_sample_args'] - if train_neg_sample_args is None or train_neg_sample_args == 'None': - self.final_config_dict['train_neg_sample_args'] = {'distribution': 'none', 'sample_num': 'none', - 'dynamic': False, 'candidate_num': 0} + train_neg_sample_args = self.final_config_dict["train_neg_sample_args"] + if train_neg_sample_args is None or train_neg_sample_args == "None": + self.final_config_dict["train_neg_sample_args"] = { + "distribution": "none", + "sample_num": "none", + "dynamic": False, + "candidate_num": 0, + } else: if not isinstance(train_neg_sample_args, dict): - raise ValueError(f"train_neg_sample_args:[{train_neg_sample_args}] should be a dict.") + raise ValueError( + f"train_neg_sample_args:[{train_neg_sample_args}] should be a dict." + ) - distribution = train_neg_sample_args['distribution'] - if distribution is None or distribution == 'None': - self.final_config_dict['train_neg_sample_args'] = {'distribution': 'none', 'sample_num': 'none', - 'dynamic': False, 'candidate_num': 0} - elif distribution not in ['uniform', 'popularity']: - raise ValueError(f"The distribution [{distribution}] of train_neg_sample_args " - f"should in ['uniform', 'popularity']") + distribution = train_neg_sample_args["distribution"] + if distribution is None or distribution == "None": + self.final_config_dict["train_neg_sample_args"] = { + "distribution": "none", + "sample_num": "none", + "dynamic": False, + "candidate_num": 0, + } + elif distribution not in ["uniform", "popularity"]: + raise ValueError( + f"The distribution [{distribution}] of train_neg_sample_args " + f"should in ['uniform', 'popularity']" + ) def _set_eval_neg_sample_args(self): - eval_mode = self.final_config_dict['eval_args']['mode'] + eval_mode = self.final_config_dict["eval_args"]["mode"] if not isinstance(eval_mode, str): raise ValueError(f"mode [{eval_mode}] in eval_args should be a str.") - if eval_mode == 'labeled': - eval_neg_sample_args = {'distribution': 'none', 'sample_num': 'none'} - elif eval_mode == 'full': - eval_neg_sample_args = {'distribution': 'uniform', 'sample_num': 'none'} - elif eval_mode[0:3] == 'uni': + if eval_mode == "labeled": + eval_neg_sample_args = {"distribution": "none", "sample_num": "none"} + elif eval_mode == "full": + eval_neg_sample_args = {"distribution": "uniform", "sample_num": "none"} + elif eval_mode[0:3] == "uni": sample_num = int(eval_mode[3:]) - eval_neg_sample_args = {'distribution': 'uniform', 'sample_num': sample_num} - elif eval_mode[0:3] == 'pop': + eval_neg_sample_args = {"distribution": "uniform", "sample_num": sample_num} + elif eval_mode[0:3] == "pop": sample_num = int(eval_mode[3:]) - eval_neg_sample_args = {'distribution': 'popularity', 'sample_num': sample_num} + eval_neg_sample_args = { + "distribution": "popularity", + "sample_num": sample_num, + } else: - raise ValueError(f'the mode [{eval_mode}] in eval_args is not supported.') - self.final_config_dict['eval_neg_sample_args'] = eval_neg_sample_args + raise ValueError(f"the mode [{eval_mode}] in eval_args is not supported.") + self.final_config_dict["eval_neg_sample_args"] = eval_neg_sample_args def __setitem__(self, key, value): if not isinstance(key, str): @@ -421,8 +538,10 @@ def __setitem__(self, key, value): self.final_config_dict[key] = value def __getattr__(self, item): - if 'final_config_dict' not in self.__dict__: - raise AttributeError(f"'Config' object has no attribute 'final_config_dict'") + if "final_config_dict" not in self.__dict__: + raise AttributeError( + f"'Config' object has no attribute 'final_config_dict'" + ) if item in self.final_config_dict: return self.final_config_dict[item] raise AttributeError(f"'Config' object has no attribute '{item}'") @@ -439,23 +558,34 @@ def __contains__(self, key): return key in self.final_config_dict def __str__(self): - args_info = '\n' + args_info = "\n" for category in self.parameters: - args_info += set_color(category + ' Hyper Parameters:\n', 'pink') - args_info += '\n'.join([(set_color("{}", 'cyan') + " =" + set_color(" {}", 'yellow')).format(arg, value) - for arg, value in self.final_config_dict.items() - if arg in self.parameters[category]]) - args_info += '\n\n' - - args_info += set_color('Other Hyper Parameters: \n', 'pink') - args_info += '\n'.join([ - (set_color("{}", 'cyan') + " = " + set_color("{}", 'yellow')).format(arg, value) - for arg, value in self.final_config_dict.items() - if arg not in { - _ for args in self.parameters.values() for _ in args - }.union({'model', 'dataset', 'config_files'}) - ]) - args_info += '\n\n' + args_info += set_color(category + " Hyper Parameters:\n", "pink") + args_info += "\n".join( + [ + ( + set_color("{}", "cyan") + " =" + set_color(" {}", "yellow") + ).format(arg, value) + for arg, value in self.final_config_dict.items() + if arg in self.parameters[category] + ] + ) + args_info += "\n\n" + + args_info += set_color("Other Hyper Parameters: \n", "pink") + args_info += "\n".join( + [ + (set_color("{}", "cyan") + " = " + set_color("{}", "yellow")).format( + arg, value + ) + for arg, value in self.final_config_dict.items() + if arg + not in {_ for args in self.parameters.values() for _ in args}.union( + {"model", "dataset", "config_files"} + ) + ] + ) + args_info += "\n\n" return args_info def __repr__(self): diff --git a/recbole/data/__init__.py b/recbole/data/__init__.py index 2cd80d1ed..1bcc8da92 100644 --- a/recbole/data/__init__.py +++ b/recbole/data/__init__.py @@ -1,3 +1,8 @@ from recbole.data.utils import * -__all__ = ['create_dataset', 'data_preparation', 'save_split_dataloaders', 'load_split_dataloaders'] +__all__ = [ + "create_dataset", + "data_preparation", + "save_split_dataloaders", + "load_split_dataloaders", +] diff --git a/recbole/data/dataloader/abstract_dataloader.py b/recbole/data/dataloader/abstract_dataloader.py index f208f0a2d..ed36e94af 100644 --- a/recbole/data/dataloader/abstract_dataloader.py +++ b/recbole/data/dataloader/abstract_dataloader.py @@ -50,26 +50,28 @@ def __init__(self, config, dataset, sampler, shuffle=False): self._init_batch_size_and_step() index_sampler = None self.generator = torch.Generator() - self.generator.manual_seed(config['seed']) - if not config['single_spec']: + self.generator.manual_seed(config["seed"]) + if not config["single_spec"]: index_sampler = torch.utils.data.distributed.DistributedSampler( list(range(self.sample_size)), shuffle=shuffle, drop_last=False ) - self.step = max(1, self.step // config['world_size']) + self.step = max(1, self.step // config["world_size"]) shuffle = False super().__init__( dataset=list(range(self.sample_size)), batch_size=self.step, collate_fn=self.collate_fn, - num_workers=config['worker'], + num_workers=config["worker"], shuffle=shuffle, sampler=index_sampler, - generator=self.generator + generator=self.generator, ) def _init_batch_size_and_step(self): """Initializing :attr:`step` and :attr:`batch_size`.""" - raise NotImplementedError('Method [init_batch_size_and_step] should be implemented') + raise NotImplementedError( + "Method [init_batch_size_and_step] should be implemented" + ) def update_config(self, config): """Update configure of dataloader, such as :attr:`batch_size`, :attr:`step` etc. @@ -89,9 +91,8 @@ def set_batch_size(self, batch_size): self._batch_size = batch_size def collate_fn(self): - """Collect the sampled index, and apply neg_sampling or other methods to get the final data. - """ - raise NotImplementedError('Method [collate_fn] must be implemented.') + """Collect the sampled index, and apply neg_sampling or other methods to get the final data.""" + raise NotImplementedError("Method [collate_fn] must be implemented.") class NegSampleDataLoader(AbstractDataLoader): @@ -116,35 +117,52 @@ def _set_neg_sample_args(self, config, dataset, dl_format, neg_sample_args): self.dl_format = dl_format self.neg_sample_args = neg_sample_args self.times = 1 - if self.neg_sample_args['distribution'] == 'uniform' or 'popularity' and self.neg_sample_args['sample_num'] != 'none': - self.neg_sample_num = self.neg_sample_args['sample_num'] + if ( + self.neg_sample_args["distribution"] == "uniform" + or "popularity" + and self.neg_sample_args["sample_num"] != "none" + ): + self.neg_sample_num = self.neg_sample_args["sample_num"] if self.dl_format == InputType.POINTWISE: self.times = 1 + self.neg_sample_num self.sampling_func = self._neg_sample_by_point_wise_sampling - self.label_field = config['LABEL_FIELD'] - dataset.set_field_property(self.label_field, FeatureType.FLOAT, FeatureSource.INTERACTION, 1) + self.label_field = config["LABEL_FIELD"] + dataset.set_field_property( + self.label_field, FeatureType.FLOAT, FeatureSource.INTERACTION, 1 + ) elif self.dl_format == InputType.PAIRWISE: self.times = self.neg_sample_num self.sampling_func = self._neg_sample_by_pair_wise_sampling - self.neg_prefix = config['NEG_PREFIX'] + self.neg_prefix = config["NEG_PREFIX"] self.neg_item_id = self.neg_prefix + self.iid_field - columns = [self.iid_field] if dataset.item_feat is None else dataset.item_feat.columns + columns = ( + [self.iid_field] + if dataset.item_feat is None + else dataset.item_feat.columns + ) for item_feat_col in columns: neg_item_feat_col = self.neg_prefix + item_feat_col dataset.copy_field_property(neg_item_feat_col, item_feat_col) else: - raise ValueError(f'`neg sampling by` with dl_format [{self.dl_format}] not been implemented.') - - elif self.neg_sample_args['distribution'] != 'none' and self.neg_sample_args['sample_num'] != 'none': - raise ValueError(f'`neg_sample_args` [{self.neg_sample_args["distribution"]}] is not supported!') + raise ValueError( + f"`neg sampling by` with dl_format [{self.dl_format}] not been implemented." + ) + + elif ( + self.neg_sample_args["distribution"] != "none" + and self.neg_sample_args["sample_num"] != "none" + ): + raise ValueError( + f'`neg_sample_args` [{self.neg_sample_args["distribution"]}] is not supported!' + ) def _neg_sampling(self, inter_feat): - if self.neg_sample_args.get('dynamic', False): - candidate_num = self.neg_sample_args['candidate_num'] + if self.neg_sample_args.get("dynamic", False): + candidate_num = self.neg_sample_args["candidate_num"] user_ids = inter_feat[self.uid_field].numpy() item_ids = inter_feat[self.iid_field].numpy() neg_candidate_ids = self._sampler.sample_by_user_ids( @@ -153,18 +171,27 @@ def _neg_sampling(self, inter_feat): self.model.eval() interaction = copy.deepcopy(inter_feat).to(self.model.device) interaction = interaction.repeat(self.neg_sample_num * candidate_num) - neg_item_feat = Interaction({self.iid_field: neg_candidate_ids.to(self.model.device)}) + neg_item_feat = Interaction( + {self.iid_field: neg_candidate_ids.to(self.model.device)} + ) interaction.update(neg_item_feat) scores = self.model.predict(interaction).reshape(candidate_num, -1) indices = torch.max(scores, dim=0)[1].detach() neg_candidate_ids = neg_candidate_ids.reshape(candidate_num, -1) - neg_item_ids = neg_candidate_ids[indices, [i for i in range(neg_candidate_ids.shape[1])]].view(-1) + neg_item_ids = neg_candidate_ids[ + indices, [i for i in range(neg_candidate_ids.shape[1])] + ].view(-1) self.model.train() return self.sampling_func(inter_feat, neg_item_ids) - elif self.neg_sample_args['distribution'] != 'none' and self.neg_sample_args['sample_num'] != 'none': + elif ( + self.neg_sample_args["distribution"] != "none" + and self.neg_sample_args["sample_num"] != "none" + ): user_ids = inter_feat[self.uid_field].numpy() item_ids = inter_feat[self.iid_field].numpy() - neg_item_ids = self._sampler.sample_by_user_ids(user_ids, item_ids, self.neg_sample_num) + neg_item_ids = self._sampler.sample_by_user_ids( + user_ids, item_ids, self.neg_sample_num + ) return self.sampling_func(inter_feat, neg_item_ids) else: return inter_feat diff --git a/recbole/data/dataloader/general_dataloader.py b/recbole/data/dataloader/general_dataloader.py index a0d8615fb..ea067fd65 100644 --- a/recbole/data/dataloader/general_dataloader.py +++ b/recbole/data/dataloader/general_dataloader.py @@ -15,7 +15,10 @@ import numpy as np import torch from logging import getLogger -from recbole.data.dataloader.abstract_dataloader import AbstractDataLoader, NegSampleDataLoader +from recbole.data.dataloader.abstract_dataloader import ( + AbstractDataLoader, + NegSampleDataLoader, +) from recbole.data.interaction import Interaction, cat_interactions from recbole.utils import InputType, ModelType @@ -35,13 +38,15 @@ class TrainDataLoader(NegSampleDataLoader): def __init__(self, config, dataset, sampler, shuffle=False): self.logger = getLogger() - self._set_neg_sample_args(config, dataset, config['MODEL_INPUT_TYPE'], config['train_neg_sample_args']) + self._set_neg_sample_args( + config, dataset, config["MODEL_INPUT_TYPE"], config["train_neg_sample_args"] + ) self.sample_size = len(dataset) super().__init__(config, dataset, sampler, shuffle=shuffle) def _init_batch_size_and_step(self): - batch_size = self.config['train_batch_size'] - if self.neg_sample_args['distribution'] != 'none': + batch_size = self.config["train_batch_size"] + if self.neg_sample_args["distribution"] != "none": batch_num = max(batch_size // self.times, 1) new_batch_size = batch_num * self.times self.step = batch_num @@ -51,7 +56,12 @@ def _init_batch_size_and_step(self): self.set_batch_size(batch_size) def update_config(self, config): - self._set_neg_sample_args(config, self._dataset, config['MODEL_INPUT_TYPE'], config['train_neg_sample_args']) + self._set_neg_sample_args( + config, + self._dataset, + config["MODEL_INPUT_TYPE"], + config["train_neg_sample_args"], + ) super().update_config(config) def collate_fn(self, index): @@ -75,8 +85,13 @@ class NegSampleEvalDataLoader(NegSampleDataLoader): def __init__(self, config, dataset, sampler, shuffle=False): self.logger = getLogger() - self._set_neg_sample_args(config, dataset, InputType.POINTWISE, config['eval_neg_sample_args']) - if self.neg_sample_args['distribution'] != 'none' and self.neg_sample_args['sample_num'] != 'none': + self._set_neg_sample_args( + config, dataset, InputType.POINTWISE, config["eval_neg_sample_args"] + ) + if ( + self.neg_sample_args["distribution"] != "none" + and self.neg_sample_args["sample_num"] != "none" + ): user_num = dataset.user_num dataset.sort(by=dataset.uid_field, ascending=True) self.uid_list = [] @@ -96,13 +111,16 @@ def __init__(self, config, dataset, sampler, shuffle=False): else: self.sample_size = len(dataset) if shuffle: - self.logger.warnning('NegSampleEvalDataLoader can\'t shuffle') + self.logger.warnning("NegSampleEvalDataLoader can't shuffle") shuffle = False super().__init__(config, dataset, sampler, shuffle=shuffle) def _init_batch_size_and_step(self): - batch_size = self.config['eval_batch_size'] - if self.neg_sample_args['distribution'] != 'none' and self.neg_sample_args['sample_num'] != 'none': + batch_size = self.config["eval_batch_size"] + if ( + self.neg_sample_args["distribution"] != "none" + and self.neg_sample_args["sample_num"] != "none" + ): inters_num = sorted(self.uid2items_num * self.times, reverse=True) batch_num = 1 new_batch_size = inters_num[0] @@ -118,12 +136,17 @@ def _init_batch_size_and_step(self): self.set_batch_size(batch_size) def update_config(self, config): - self._set_neg_sample_args(config, self._dataset, InputType.POINTWISE, config['eval_neg_sample_args']) + self._set_neg_sample_args( + config, self._dataset, InputType.POINTWISE, config["eval_neg_sample_args"] + ) super().update_config(config) def collate_fn(self, index): index = np.array(index) - if self.neg_sample_args['distribution'] != 'none' and self.neg_sample_args['sample_num'] != 'none': + if ( + self.neg_sample_args["distribution"] != "none" + and self.neg_sample_args["sample_num"] != "none" + ): uid_list = self.uid_list[index] data_list = [] idx_list = [] @@ -135,7 +158,9 @@ def collate_fn(self, index): data_list.append(self._neg_sampling(self._dataset[index])) idx_list += [idx for i in range(self.uid2items_num[uid] * self.times)] positive_u += [idx for i in range(self.uid2items_num[uid])] - positive_i = torch.cat((positive_i, self._dataset[index][self.iid_field]), 0) + positive_i = torch.cat( + (positive_i, self._dataset[index][self.iid_field]), 0 + ) cur_data = cat_interactions(data_list) idx_list = torch.from_numpy(np.array(idx_list)).long() @@ -164,7 +189,7 @@ def __init__(self, config, dataset, sampler, shuffle=False): self.logger = getLogger() self.uid_field = dataset.uid_field self.iid_field = dataset.iid_field - self.is_sequential = config['MODEL_TYPE'] == ModelType.SEQUENTIAL + self.is_sequential = config["MODEL_TYPE"] == ModelType.SEQUENTIAL if not self.is_sequential: user_num = dataset.user_num self.uid_list = [] @@ -176,9 +201,14 @@ def __init__(self, config, dataset, sampler, shuffle=False): last_uid = None positive_item = set() uid2used_item = sampler.used_ids - for uid, iid in zip(dataset.inter_feat[self.uid_field].numpy(), dataset.inter_feat[self.iid_field].numpy()): + for uid, iid in zip( + dataset.inter_feat[self.uid_field].numpy(), + dataset.inter_feat[self.iid_field].numpy(), + ): if uid != last_uid: - self._set_user_property(last_uid, uid2used_item[last_uid], positive_item) + self._set_user_property( + last_uid, uid2used_item[last_uid], positive_item + ) last_uid = uid self.uid_list.append(uid) positive_item = set() @@ -189,7 +219,7 @@ def __init__(self, config, dataset, sampler, shuffle=False): self.sample_size = len(self.user_df) if not self.is_sequential else len(dataset) if shuffle: - self.logger.warnning('FullSortEvalDataLoader can\'t shuffle') + self.logger.warnning("FullSortEvalDataLoader can't shuffle") shuffle = False super().__init__(config, dataset, sampler, shuffle=shuffle) @@ -197,12 +227,14 @@ def _set_user_property(self, uid, used_item, positive_item): if uid is None: return history_item = used_item - positive_item - self.uid2positive_item[uid] = torch.tensor(list(positive_item), dtype=torch.int64) + self.uid2positive_item[uid] = torch.tensor( + list(positive_item), dtype=torch.int64 + ) self.uid2items_num[uid] = len(positive_item) self.uid2history_item[uid] = torch.tensor(list(history_item), dtype=torch.int64) def _init_batch_size_and_step(self): - batch_size = self.config['eval_batch_size'] + batch_size = self.config["eval_batch_size"] if not self.is_sequential: batch_num = max(batch_size // self._dataset.item_num, 1) new_batch_size = batch_num * self._dataset.item_num @@ -221,10 +253,17 @@ def collate_fn(self, index): history_item = self.uid2history_item[uid_list] positive_item = self.uid2positive_item[uid_list] - history_u = torch.cat([torch.full_like(hist_iid, i) for i, hist_iid in enumerate(history_item)]) + history_u = torch.cat( + [ + torch.full_like(hist_iid, i) + for i, hist_iid in enumerate(history_item) + ] + ) history_i = torch.cat(list(history_item)) - positive_u = torch.cat([torch.full_like(pos_iid, i) for i, pos_iid in enumerate(positive_item)]) + positive_u = torch.cat( + [torch.full_like(pos_iid, i) for i, pos_iid in enumerate(positive_item)] + ) positive_i = torch.cat(list(positive_item)) return user_df, (history_u, history_i), positive_u, positive_i diff --git a/recbole/data/dataloader/knowledge_dataloader.py b/recbole/data/dataloader/knowledge_dataloader.py index 311a3ab63..f9402a259 100644 --- a/recbole/data/dataloader/knowledge_dataloader.py +++ b/recbole/data/dataloader/knowledge_dataloader.py @@ -38,11 +38,11 @@ def __init__(self, config, dataset, sampler, shuffle=False): self.logger = getLogger() if shuffle is False: shuffle = True - self.logger.warning('kg based dataloader must shuffle the data') + self.logger.warning("kg based dataloader must shuffle the data") self.neg_sample_num = 1 - self.neg_prefix = config['NEG_PREFIX'] + self.neg_prefix = config["NEG_PREFIX"] self.hid_field = dataset.head_entity_field self.tid_field = dataset.tail_entity_field @@ -54,7 +54,7 @@ def __init__(self, config, dataset, sampler, shuffle=False): super().__init__(config, dataset, sampler, shuffle=shuffle) def _init_batch_size_and_step(self): - batch_size = self.config['train_batch_size'] + batch_size = self.config["train_batch_size"] self.step = batch_size self.set_batch_size(batch_size) @@ -67,7 +67,7 @@ def collate_fn(self, index): return cur_data -class KnowledgeBasedDataLoader(): +class KnowledgeBasedDataLoader: """:class:`KnowledgeBasedDataLoader` is used for knowledge based model. It has three states, which is saved in :attr:`state`. In different states, :meth:`~_next_batch_data` will return different :class:`~recbole.data.interaction.Interaction`. @@ -94,7 +94,9 @@ class KnowledgeBasedDataLoader(): def __init__(self, config, dataset, sampler, kg_sampler, shuffle=False): self.logger = getLogger() # using sampler - self.general_dataloader = TrainDataLoader(config, dataset, sampler, shuffle=shuffle) + self.general_dataloader = TrainDataLoader( + config, dataset, sampler, shuffle=shuffle + ) # using kg_sampler self.kg_dataloader = KGDataLoader(config, dataset, kg_sampler, shuffle=True) @@ -111,8 +113,8 @@ def update_config(self, config): def __iter__(self): if self.state is None: raise ValueError( - 'The dataloader\'s state must be set when using the kg based dataloader, ' - 'you should call set_mode() before __iter__()' + "The dataloader's state must be set when using the kg based dataloader, " + "you should call set_mode() before __iter__()" ) if self.state == KGDataLoaderState.KG: return self.kg_dataloader.__iter__() @@ -149,17 +151,18 @@ def set_mode(self, state): state (KGDataLoaderState): the state of :class:`KnowledgeBasedDataLoader`. """ if state not in set(KGDataLoaderState): - raise NotImplementedError(f'Kg data loader has no state named [{self.state}].') + raise NotImplementedError( + f"Kg data loader has no state named [{self.state}]." + ) self.state = state def get_model(self, model): - """Let the general_dataloader get the model, used for dynamic sampling. - """ + """Let the general_dataloader get the model, used for dynamic sampling.""" self.general_dataloader.get_model(model) - + def knowledge_shuffle(self, epoch_seed): """Reset the seed to ensure that each subprocess generates the same index squence.""" self.kg_dataloader.sampler.set_epoch(epoch_seed) - + if self.general_dataloader.shuffle: self.general_dataloader.sampler.set_epoch(epoch_seed) diff --git a/recbole/data/dataloader/user_dataloader.py b/recbole/data/dataloader/user_dataloader.py index 49c4bae3f..89eb6f284 100644 --- a/recbole/data/dataloader/user_dataloader.py +++ b/recbole/data/dataloader/user_dataloader.py @@ -36,7 +36,7 @@ def __init__(self, config, dataset, sampler, shuffle=False): self.logger = getLogger() if shuffle is False: shuffle = True - self.logger.warning('UserDataLoader must shuffle the data.') + self.logger.warning("UserDataLoader must shuffle the data.") self.uid_field = dataset.uid_field self.user_list = Interaction({self.uid_field: torch.arange(dataset.user_num)}) @@ -44,7 +44,7 @@ def __init__(self, config, dataset, sampler, shuffle=False): super().__init__(config, dataset, sampler, shuffle=shuffle) def _init_batch_size_and_step(self): - batch_size = self.config['train_batch_size'] + batch_size = self.config["train_batch_size"] self.step = batch_size self.set_batch_size(batch_size) diff --git a/recbole/data/dataset/customized_dataset.py b/recbole/data/dataset/customized_dataset.py index f002c79d2..da278882e 100644 --- a/recbole/data/dataset/customized_dataset.py +++ b/recbole/data/dataset/customized_dataset.py @@ -26,13 +26,11 @@ class GRU4RecKGDataset(KGSeqDataset): - def __init__(self, config): super().__init__(config) class KSRDataset(KGSeqDataset): - def __init__(self, config): super().__init__(config) @@ -56,11 +54,13 @@ class DIENDataset(SequentialDataset): def __init__(self, config): super().__init__(config) - list_suffix = config['LIST_SUFFIX'] - neg_prefix = config['NEG_PREFIX'] + list_suffix = config["LIST_SUFFIX"] + neg_prefix = config["NEG_PREFIX"] self.seq_sampler = SeqSampler(self) self.neg_item_list_field = neg_prefix + self.iid_field + list_suffix - self.neg_item_list = self.seq_sampler.sample_neg_sequence(self.inter_feat[self.iid_field]) + self.neg_item_list = self.seq_sampler.sample_neg_sequence( + self.inter_feat[self.iid_field] + ) def data_augmentation(self): """Augmentation processing for sequential dataset. @@ -79,12 +79,12 @@ def data_augmentation(self): ``u1, | i4`` """ - self.logger.debug('data_augmentation') + self.logger.debug("data_augmentation") self._aug_presets() - self._check_field('uid_field', 'time_field') - max_item_list_len = self.config['MAX_ITEM_LIST_LENGTH'] + self._check_field("uid_field", "time_field") + max_item_list_len = self.config["MAX_ITEM_LIST_LENGTH"] self.sort(by=[self.uid_field, self.time_field], ascending=True) last_uid = None uid_list, item_list_index, target_index, item_list_length = [], [], [], [] @@ -114,22 +114,36 @@ def data_augmentation(self): for field in self.inter_feat: if field != self.uid_field: - list_field = getattr(self, f'{field}_list_field') + list_field = getattr(self, f"{field}_list_field") list_len = self.field2seqlen[list_field] - shape = (new_length, list_len) if isinstance(list_len, int) else (new_length,) + list_len + shape = ( + (new_length, list_len) + if isinstance(list_len, int) + else (new_length,) + list_len + ) list_ftype = self.field2type[list_field] - dtype = torch.int64 if list_ftype in [FeatureType.TOKEN, FeatureType.TOKEN_SEQ] else torch.float64 + dtype = ( + torch.int64 + if list_ftype in [FeatureType.TOKEN, FeatureType.TOKEN_SEQ] + else torch.float64 + ) new_dict[list_field] = torch.zeros(shape, dtype=dtype) value = self.inter_feat[field] - for i, (index, length) in enumerate(zip(item_list_index, item_list_length)): + for i, (index, length) in enumerate( + zip(item_list_index, item_list_length) + ): new_dict[list_field][i][:length] = value[index] # DIEN if field == self.iid_field: new_dict[self.neg_item_list_field] = torch.zeros(shape, dtype=dtype) - for i, (index, length) in enumerate(zip(item_list_index, item_list_length)): - new_dict[self.neg_item_list_field][i][:length] = self.neg_item_list[index] + for i, (index, length) in enumerate( + zip(item_list_index, item_list_length) + ): + new_dict[self.neg_item_list_field][i][ + :length + ] = self.neg_item_list[index] new_data.update(Interaction(new_dict)) self.inter_feat = new_data diff --git a/recbole/data/dataset/dataset.py b/recbole/data/dataset/dataset.py index e6a27977c..9fd3c35bf 100644 --- a/recbole/data/dataset/dataset.py +++ b/recbole/data/dataset/dataset.py @@ -25,8 +25,20 @@ import torch.nn.utils.rnn as rnn_utils from scipy.sparse import coo_matrix from recbole.data.interaction import Interaction -from recbole.utils import FeatureSource, FeatureType, get_local_time, set_color, ensure_dir -from recbole.utils.url import decide_download, download_url, extract_zip, makedirs, rename_atomic_files +from recbole.utils import ( + FeatureSource, + FeatureType, + get_local_time, + set_color, + ensure_dir, +) +from recbole.utils.url import ( + decide_download, + download_url, + extract_zip, + makedirs, + rename_atomic_files, +) class Dataset(torch.utils.data.Dataset): @@ -91,7 +103,7 @@ class Dataset(torch.utils.data.Dataset): def __init__(self, config): super().__init__() self.config = config - self.dataset_name = config['dataset'] + self.dataset_name = config["dataset"] self.logger = getLogger() self._from_scratch() @@ -99,7 +111,7 @@ def _from_scratch(self): """Load dataset from scratch. Initialize attributes firstly, then load data from atomic files, pre-process the dataset lastly. """ - self.logger.debug(set_color(f'Loading {self.__class__} from scratch.', 'green')) + self.logger.debug(set_color(f"Loading {self.__class__} from scratch.", "green")) self._get_preset() self._get_field_from_config() @@ -108,34 +120,32 @@ def _from_scratch(self): self._data_processing() def _get_preset(self): - """Initialization useful inside attributes. - """ - self.dataset_path = self.config['data_path'] + """Initialization useful inside attributes.""" + self.dataset_path = self.config["data_path"] self.field2type = {} self.field2source = {} self.field2id_token = {} self.field2token_id = {} - self.field2seqlen = self.config['seq_len'] or {} + self.field2seqlen = self.config["seq_len"] or {} self.alias = {} self._preloaded_weight = {} - self.benchmark_filename_list = self.config['benchmark_filename'] + self.benchmark_filename_list = self.config["benchmark_filename"] def _get_field_from_config(self): - """Initialization common field names. - """ - self.uid_field = self.config['USER_ID_FIELD'] - self.iid_field = self.config['ITEM_ID_FIELD'] - self.label_field = self.config['LABEL_FIELD'] - self.time_field = self.config['TIME_FIELD'] + """Initialization common field names.""" + self.uid_field = self.config["USER_ID_FIELD"] + self.iid_field = self.config["ITEM_ID_FIELD"] + self.label_field = self.config["LABEL_FIELD"] + self.time_field = self.config["TIME_FIELD"] if (self.uid_field is None) ^ (self.iid_field is None): raise ValueError( - 'USER_ID_FIELD and ITEM_ID_FIELD need to be set at the same time or not set at the same time.' + "USER_ID_FIELD and ITEM_ID_FIELD need to be set at the same time or not set at the same time." ) - self.logger.debug(set_color('uid_field', 'blue') + f': {self.uid_field}') - self.logger.debug(set_color('iid_field', 'blue') + f': {self.iid_field}') + self.logger.debug(set_color("uid_field", "blue") + f": {self.uid_field}") + self.logger.debug(set_color("iid_field", "blue") + f": {self.iid_field}") def _data_processing(self): """Data preprocessing, including: @@ -189,18 +199,21 @@ def _build_feat_name_list(self): Subclasses can inherit this method to add new feat. """ feat_name_list = [ - feat_name for feat_name in ['inter_feat', 'user_feat', 'item_feat'] + feat_name + for feat_name in ["inter_feat", "user_feat", "item_feat"] if getattr(self, feat_name, None) is not None ] - if self.config['additional_feat_suffix'] is not None: - for suf in self.config['additional_feat_suffix']: - if getattr(self, f'{suf}_feat', None) is not None: - feat_name_list.append(f'{suf}_feat') + if self.config["additional_feat_suffix"] is not None: + for suf in self.config["additional_feat_suffix"]: + if getattr(self, f"{suf}_feat", None) is not None: + feat_name_list.append(f"{suf}_feat") return feat_name_list def _get_download_url(self, url_file, allow_none=False): current_path = os.path.dirname(os.path.realpath(__file__)) - with open(os.path.join(current_path, f'../../properties/dataset/{url_file}.yaml')) as f: + with open( + os.path.join(current_path, f"../../properties/dataset/{url_file}.yaml") + ) as f: dataset2url = yaml.load(f.read(), Loader=self.config.yaml_loader) if self.dataset_name in dataset2url: @@ -210,14 +223,16 @@ def _get_download_url(self, url_file, allow_none=False): return None else: raise ValueError( - f'Neither [{self.dataset_path}] exists in the device ' - f'nor [{self.dataset_name}] a known dataset name.' + f"Neither [{self.dataset_path}] exists in the device " + f"nor [{self.dataset_name}] a known dataset name." ) def _download(self): - if self.config['local_rank'] == 0: - url = self._get_download_url('url') - self.logger.info(f'Prepare to download dataset [{self.dataset_name}] from [{url}].') + if self.config["local_rank"] == 0: + url = self._get_download_url("url") + self.logger.info( + f"Prepare to download dataset [{self.dataset_name}] from [{url}]." + ) if decide_download(url): makedirs(self.dataset_path) @@ -228,9 +243,9 @@ def _download(self): basename = os.path.splitext(os.path.basename(path))[0] rename_atomic_files(self.dataset_path, basename, self.dataset_name) - self.logger.info('Downloading done.') + self.logger.info("Downloading done.") else: - self.logger.info('Stop download.') + self.logger.info("Stop download.") exit(-1) torch.distributed.barrier() else: @@ -249,8 +264,12 @@ def _load_data(self, token, dataset_path): if not os.path.exists(dataset_path): self._download() self._load_inter_feat(token, dataset_path) - self.user_feat = self._load_user_or_item_feat(token, dataset_path, FeatureSource.USER, 'uid_field') - self.item_feat = self._load_user_or_item_feat(token, dataset_path, FeatureSource.ITEM, 'iid_field') + self.user_feat = self._load_user_or_item_feat( + token, dataset_path, FeatureSource.USER, "uid_field" + ) + self.item_feat = self._load_user_or_item_feat( + token, dataset_path, FeatureSource.ITEM, "iid_field" + ) self._load_additional_feat(token, dataset_path) def _load_inter_feat(self, token, dataset_path): @@ -267,27 +286,31 @@ def _load_inter_feat(self, token, dataset_path): dataset_path (str): path of dataset dir. """ if self.benchmark_filename_list is None: - inter_feat_path = os.path.join(dataset_path, f'{token}.inter') + inter_feat_path = os.path.join(dataset_path, f"{token}.inter") if not os.path.isfile(inter_feat_path): - raise ValueError(f'File {inter_feat_path} not exist.') + raise ValueError(f"File {inter_feat_path} not exist.") inter_feat = self._load_feat(inter_feat_path, FeatureSource.INTERACTION) - self.logger.debug(f'Interaction feature loaded successfully from [{inter_feat_path}].') + self.logger.debug( + f"Interaction feature loaded successfully from [{inter_feat_path}]." + ) self.inter_feat = inter_feat else: sub_inter_lens = [] sub_inter_feats = [] overall_field2seqlen = defaultdict(int) for filename in self.benchmark_filename_list: - file_path = os.path.join(dataset_path, f'{token}.{filename}.inter') + file_path = os.path.join(dataset_path, f"{token}.{filename}.inter") if os.path.isfile(file_path): temp = self._load_feat(file_path, FeatureSource.INTERACTION) sub_inter_feats.append(temp) sub_inter_lens.append(len(temp)) for field in self.field2seqlen: - overall_field2seqlen[field] = max(overall_field2seqlen[field], self.field2seqlen[field]) + overall_field2seqlen[field] = max( + overall_field2seqlen[field], self.field2seqlen[field] + ) else: - raise ValueError(f'File {file_path} not exist.') + raise ValueError(f"File {file_path} not exist.") inter_feat = pd.concat(sub_inter_feats, ignore_index=True) self.inter_feat, self.file_size_list = inter_feat, sub_inter_lens self.field2seqlen = overall_field2seqlen @@ -308,25 +331,33 @@ def _load_user_or_item_feat(self, token, dataset_path, source, field_name): ``user_id`` and ``item_id`` has source :obj:`~recbole.utils.enum_type.FeatureSource.USER_ID` and :obj:`~recbole.utils.enum_type.FeatureSource.ITEM_ID` """ - feat_path = os.path.join(dataset_path, f'{token}.{source.value}') + feat_path = os.path.join(dataset_path, f"{token}.{source.value}") field = getattr(self, field_name, None) if os.path.isfile(feat_path): feat = self._load_feat(feat_path, source) - self.logger.debug(f'[{source.value}] feature loaded successfully from [{feat_path}].') + self.logger.debug( + f"[{source.value}] feature loaded successfully from [{feat_path}]." + ) else: feat = None - self.logger.debug(f'[{feat_path}] not found, [{source.value}] features are not loaded.') + self.logger.debug( + f"[{feat_path}] not found, [{source.value}] features are not loaded." + ) if feat is not None and field is None: - raise ValueError(f'{field_name} must be exist if {source.value}_feat exist.') + raise ValueError( + f"{field_name} must be exist if {source.value}_feat exist." + ) if feat is not None and field not in feat: - raise ValueError(f'{field_name} must be loaded if {source.value}_feat is loaded.') + raise ValueError( + f"{field_name} must be loaded if {source.value}_feat is loaded." + ) if feat is not None: - feat.drop_duplicates(subset=[field], keep='first', inplace=True) + feat.drop_duplicates(subset=[field], keep="first", inplace=True) if field in self.field2source: - self.field2source[field] = FeatureSource(source.value + '_id') + self.field2source[field] = FeatureSource(source.value + "_id") return feat def _load_additional_feat(self, token, dataset_path): @@ -340,17 +371,17 @@ def _load_additional_feat(self, token, dataset_path): token (str): dataset name. dataset_path (str): path of dataset dir. """ - if self.config['additional_feat_suffix'] is None: + if self.config["additional_feat_suffix"] is None: return - for suf in self.config['additional_feat_suffix']: - if hasattr(self, f'{suf}_feat'): - raise ValueError(f'{suf}_feat already exist.') - feat_path = os.path.join(dataset_path, f'{token}.{suf}') + for suf in self.config["additional_feat_suffix"]: + if hasattr(self, f"{suf}_feat"): + raise ValueError(f"{suf}_feat already exist.") + feat_path = os.path.join(dataset_path, f"{token}.{suf}") if os.path.isfile(feat_path): feat = self._load_feat(feat_path, suf) else: - raise ValueError(f'Additional feature file [{feat_path}] not found.') - setattr(self, f'{suf}_feat', feat) + raise ValueError(f"Additional feature file [{feat_path}] not found.") + setattr(self, f"{suf}_feat", feat) def _get_load_and_unload_col(self, source): """Parsing ``config['load_col']`` and ``config['unload_col']`` according to source. @@ -364,26 +395,31 @@ def _get_load_and_unload_col(self, source): """ if isinstance(source, FeatureSource): source = source.value - if self.config['load_col'] is None: + if self.config["load_col"] is None: load_col = None - elif source not in self.config['load_col']: + elif source not in self.config["load_col"]: load_col = set() - elif self.config['load_col'][source] == '*': + elif self.config["load_col"][source] == "*": load_col = None else: - load_col = set(self.config['load_col'][source]) + load_col = set(self.config["load_col"][source]) - if self.config['unload_col'] is not None and source in self.config['unload_col']: - unload_col = set(self.config['unload_col'][source]) + if ( + self.config["unload_col"] is not None + and source in self.config["unload_col"] + ): + unload_col = set(self.config["unload_col"][source]) else: unload_col = None if load_col and unload_col: - raise ValueError(f'load_col [{load_col}] and unload_col [{unload_col}] can not be set the same time.') + raise ValueError( + f"load_col [{load_col}] and unload_col [{unload_col}] can not be set the same time." + ) - self.logger.debug(set_color(f'[{source}]: ', 'pink')) - self.logger.debug(set_color('\t load_col', 'blue') + f': [{load_col}]') - self.logger.debug(set_color('\t unload_col', 'blue') + f': [{unload_col}]') + self.logger.debug(set_color(f"[{source}]: ", "pink")) + self.logger.debug(set_color("\t load_col", "blue") + f": [{load_col}]") + self.logger.debug(set_color("\t unload_col", "blue") + f": [{unload_col}]") return load_col, unload_col def _load_feat(self, filepath, source): @@ -403,71 +439,85 @@ def _load_feat(self, filepath, source): Their length is limited only after calling :meth:`~_dict_to_interaction` or :meth:`~_dataframe_to_interaction` """ - self.logger.debug(set_color(f'Loading feature from [{filepath}] (source: [{source}]).', 'green')) + self.logger.debug( + set_color( + f"Loading feature from [{filepath}] (source: [{source}]).", "green" + ) + ) load_col, unload_col = self._get_load_and_unload_col(source) if load_col == set(): return None - field_separator = self.config['field_separator'] + field_separator = self.config["field_separator"] columns = [] usecols = [] dtype = {} - encoding = self.config['encoding'] - with open(filepath, 'r', encoding=encoding) as f: + encoding = self.config["encoding"] + with open(filepath, "r", encoding=encoding) as f: head = f.readline()[:-1] for field_type in head.split(field_separator): - field, ftype = field_type.split(':') + field, ftype = field_type.split(":") try: ftype = FeatureType(ftype) except ValueError: - raise ValueError(f'Type {ftype} from field {field} is not supported.') + raise ValueError(f"Type {ftype} from field {field} is not supported.") if load_col is not None and field not in load_col: continue if unload_col is not None and field in unload_col: continue - if isinstance(source, FeatureSource) or source != 'link': + if isinstance(source, FeatureSource) or source != "link": self.field2source[field] = source self.field2type[field] = ftype - if not ftype.value.endswith('seq'): + if not ftype.value.endswith("seq"): self.field2seqlen[field] = 1 columns.append(field) usecols.append(field_type) dtype[field_type] = np.float64 if ftype == FeatureType.FLOAT else str if len(columns) == 0: - self.logger.warning(f'No columns has been loaded from [{source}]') + self.logger.warning(f"No columns has been loaded from [{source}]") return None df = pd.read_csv( - filepath, delimiter=field_separator, usecols=usecols, dtype=dtype, encoding=encoding, engine='python' + filepath, + delimiter=field_separator, + usecols=usecols, + dtype=dtype, + encoding=encoding, + engine="python", ) df.columns = columns - seq_separator = self.config['seq_separator'] + seq_separator = self.config["seq_separator"] for field in columns: ftype = self.field2type[field] - if not ftype.value.endswith('seq'): + if not ftype.value.endswith("seq"): continue - df[field].fillna(value='', inplace=True) + df[field].fillna(value="", inplace=True) if ftype == FeatureType.TOKEN_SEQ: - df[field] = [np.array(list(filter(None, _.split(seq_separator)))) for _ in df[field].values] + df[field] = [ + np.array(list(filter(None, _.split(seq_separator)))) + for _ in df[field].values + ] elif ftype == FeatureType.FLOAT_SEQ: - df[field] = [np.array(list(map(float, filter(None, _.split(seq_separator))))) for _ in df[field].values] + df[field] = [ + np.array(list(map(float, filter(None, _.split(seq_separator))))) + for _ in df[field].values + ] self.field2seqlen[field] = max(map(len, df[field].values)) return df def _set_alias(self, alias_name, default_value): - alias = self.config[f'alias_of_{alias_name}'] or [] + alias = self.config[f"alias_of_{alias_name}"] or [] alias = np.array(list(filter(None, default_value)) + alias) _, idx = np.unique(alias, return_index=True) self.alias[alias_name] = alias[np.sort(idx)] def _init_alias(self): - """Set :attr:`alias_of_user_id` and :attr:`alias_of_item_id`. And set :attr:`_rest_fields`. - """ - self._set_alias('user_id', [self.uid_field]) - self._set_alias('item_id', [self.iid_field]) + """Set :attr:`alias_of_user_id` and :attr:`alias_of_item_id`. And set :attr:`_rest_fields`.""" + self._set_alias("user_id", [self.uid_field]) + self._set_alias("item_id", [self.iid_field]) for alias_name_1, alias_1 in self.alias.items(): for alias_name_2, alias_2 in self.alias.items(): @@ -475,8 +525,8 @@ def _init_alias(self): intersect = np.intersect1d(alias_1, alias_2, assume_unique=True) if len(intersect) > 0: raise ValueError( - f'`alias_of_{alias_name_1}` and `alias_of_{alias_name_2}` ' - f'should not have the same field {list(intersect)}.' + f"`alias_of_{alias_name_1}` and `alias_of_{alias_name_2}` " + f"should not have the same field {list(intersect)}." ) self._rest_fields = self.token_like_fields @@ -484,10 +534,12 @@ def _init_alias(self): isin = np.isin(alias, self._rest_fields, assume_unique=True) if isin.all() is False: raise ValueError( - f'`alias_of_{alias_name}` should not contain ' - f'non-token-like field {list(alias[~isin])}.' + f"`alias_of_{alias_name}` should not contain " + f"non-token-like field {list(alias[~isin])}." ) - self._rest_fields = np.setdiff1d(self._rest_fields, alias, assume_unique=True) + self._rest_fields = np.setdiff1d( + self._rest_fields, alias, assume_unique=True + ) def _user_item_feat_preparation(self): """Sort :attr:`user_feat` and :attr:`item_feat` by ``user_id`` or ``item_id``. @@ -495,45 +547,53 @@ def _user_item_feat_preparation(self): """ if self.user_feat is not None: new_user_df = pd.DataFrame({self.uid_field: np.arange(self.user_num)}) - self.user_feat = pd.merge(new_user_df, self.user_feat, on=self.uid_field, how='left') - self.logger.debug(set_color('ordering user features by user id.', 'green')) + self.user_feat = pd.merge( + new_user_df, self.user_feat, on=self.uid_field, how="left" + ) + self.logger.debug(set_color("ordering user features by user id.", "green")) if self.item_feat is not None: new_item_df = pd.DataFrame({self.iid_field: np.arange(self.item_num)}) - self.item_feat = pd.merge(new_item_df, self.item_feat, on=self.iid_field, how='left') - self.logger.debug(set_color('ordering item features by item id.', 'green')) + self.item_feat = pd.merge( + new_item_df, self.item_feat, on=self.iid_field, how="left" + ) + self.logger.debug(set_color("ordering item features by item id.", "green")) def _preload_weight_matrix(self): """Transfer preload weight features into :class:`numpy.ndarray` with shape ``[id_token_length]`` or ``[id_token_length, seqlen]``. See :doc:`../user_guide/data/data_args` for detail arg setting. """ - preload_fields = self.config['preload_weight'] + preload_fields = self.config["preload_weight"] if preload_fields is None: return - self.logger.debug(f'Preload weight matrix for {preload_fields}.') + self.logger.debug(f"Preload weight matrix for {preload_fields}.") for preload_id_field, preload_value_field in preload_fields.items(): if preload_id_field not in self.field2source: - raise ValueError(f'Preload id field [{preload_id_field}] not exist.') + raise ValueError(f"Preload id field [{preload_id_field}] not exist.") if preload_value_field not in self.field2source: - raise ValueError(f'Preload value field [{preload_value_field}] not exist.') + raise ValueError( + f"Preload value field [{preload_value_field}] not exist." + ) pid_source = self.field2source[preload_id_field] pv_source = self.field2source[preload_value_field] if pid_source != pv_source: raise ValueError( - f'Preload id field [{preload_id_field}] is from source [{pid_source}],' - f'while preload value field [{preload_value_field}] is from source [{pv_source}], ' - f'which should be the same.' + f"Preload id field [{preload_id_field}] is from source [{pid_source}]," + f"while preload value field [{preload_value_field}] is from source [{pv_source}], " + f"which should be the same." ) id_ftype = self.field2type[preload_id_field] value_ftype = self.field2type[preload_value_field] if id_ftype != FeatureType.TOKEN: - raise ValueError(f'Preload id field [{preload_id_field}] should be type token, but is [{id_ftype}].') + raise ValueError( + f"Preload id field [{preload_id_field}] should be type token, but is [{id_ftype}]." + ) if value_ftype not in {FeatureType.FLOAT, FeatureType.FLOAT_SEQ}: self.logger.warning( - f'Field [{preload_value_field}] with type [{value_ftype}] is not `float` or `float_seq`, ' - f'which will not be handled by preload matrix.' + f"Field [{preload_value_field}] with type [{value_ftype}] is not `float` or `float_seq`, " + f"which will not be handled by preload matrix." ) continue @@ -564,7 +624,7 @@ def _fill_nan(self): For fields with type :obj:`~recbole.utils.enum_type.FeatureType.FLOAT`, missing value will be filled by the average of original data. """ - self.logger.debug(set_color('Filling nan', 'green')) + self.logger.debug(set_color("Filling nan", "green")) for feat_name in self.feat_name_list: feat = getattr(self, feat_name) @@ -576,7 +636,11 @@ def _fill_nan(self): feat[field].fillna(value=feat[field].mean(), inplace=True) else: dtype = np.int64 if ftype == FeatureType.TOKEN_SEQ else np.float - feat[field] = feat[field].apply(lambda x: np.array([], dtype=dtype) if isinstance(x, float) else x) + feat[field] = feat[field].apply( + lambda x: np.array([], dtype=dtype) + if isinstance(x, float) + else x + ) def _normalize(self): """Normalization if ``config['normalize_field']`` or ``config['normalize_all']`` is set. @@ -588,23 +652,30 @@ def _normalize(self): Note: Only float-like fields can be normalized. """ - if self.config['normalize_field'] is not None and self.config['normalize_all'] is True: - raise ValueError('Normalize_field and normalize_all can\'t be set at the same time.') + if ( + self.config["normalize_field"] is not None + and self.config["normalize_all"] is True + ): + raise ValueError( + "Normalize_field and normalize_all can't be set at the same time." + ) - if self.config['normalize_field']: - fields = self.config['normalize_field'] + if self.config["normalize_field"]: + fields = self.config["normalize_field"] for field in fields: if field not in self.field2type: - raise ValueError(f'Field [{field}] does not exist.') + raise ValueError(f"Field [{field}] does not exist.") ftype = self.field2type[field] if ftype != FeatureType.FLOAT and ftype != FeatureType.FLOAT_SEQ: - self.logger.warning(f'{field} is not a FLOAT/FLOAT_SEQ feat, which will not be normalized.') - elif self.config['normalize_all']: + self.logger.warning( + f"{field} is not a FLOAT/FLOAT_SEQ feat, which will not be normalized." + ) + elif self.config["normalize_all"]: fields = self.float_like_fields else: return - self.logger.debug(set_color('Normalized fields', 'blue') + f': {fields}') + self.logger.debug(set_color("Normalized fields", "blue") + f": {fields}") for field in fields: for feat in self.field2feats(field): @@ -612,7 +683,9 @@ def _normalize(self): def norm(arr): mx, mn = max(arr), min(arr) if mx == mn: - self.logger.warning(f'All the same value in [{field}] from [{feat}_feat].') + self.logger.warning( + f"All the same value in [{field}] from [{feat}_feat]." + ) arr = 1.0 else: arr = (arr - mn) / (mx - mn) @@ -623,27 +696,30 @@ def norm(arr): feat[field] = norm(feat[field].values) elif ftype == FeatureType.FLOAT_SEQ: split_point = np.cumsum(feat[field].agg(len))[:-1] - feat[field] = np.split(norm(feat[field].agg(np.concatenate)), split_point) + feat[field] = np.split( + norm(feat[field].agg(np.concatenate)), split_point + ) def _filter_nan_user_or_item(self): - """Filter NaN user_id and item_id - """ - for field, name in zip([self.uid_field, self.iid_field], ['user', 'item']): - feat = getattr(self, name + '_feat') + """Filter NaN user_id and item_id""" + for field, name in zip([self.uid_field, self.iid_field], ["user", "item"]): + feat = getattr(self, name + "_feat") if feat is not None: dropped_feat = feat.index[feat[field].isnull()] if len(dropped_feat): self.logger.warning( - f'In {name}_feat, line {list(dropped_feat + 2)}, {field} do not exist, so they will be removed.' + f"In {name}_feat, line {list(dropped_feat + 2)}, {field} do not exist, so they will be removed." ) feat.drop(feat.index[dropped_feat], inplace=True) if field is not None: dropped_inter = self.inter_feat.index[self.inter_feat[field].isnull()] if len(dropped_inter): self.logger.warning( - f'In inter_feat, line {list(dropped_inter + 2)}, {field} do not exist, so they will be removed.' + f"In inter_feat, line {list(dropped_inter + 2)}, {field} do not exist, so they will be removed." + ) + self.inter_feat.drop( + self.inter_feat.index[dropped_inter], inplace=True ) - self.inter_feat.drop(self.inter_feat.index[dropped_inter], inplace=True) def _remove_duplication(self): """Remove duplications in inter_feat. @@ -654,58 +730,74 @@ def _remove_duplication(self): Before removing duplicated user-item interactions, if :attr:`time_field` existed, :attr:`inter_feat` will be sorted by :attr:`time_field` in ascending order. """ - keep = self.config['rm_dup_inter'] + keep = self.config["rm_dup_inter"] if keep is None: return - self._check_field('uid_field', 'iid_field') + self._check_field("uid_field", "iid_field") if self.time_field in self.inter_feat: - self.inter_feat.sort_values(by=[self.time_field], ascending=True, inplace=True) + self.inter_feat.sort_values( + by=[self.time_field], ascending=True, inplace=True + ) self.logger.info( - f'Records in original dataset have been sorted by value of [{self.time_field}] in ascending order.' + f"Records in original dataset have been sorted by value of [{self.time_field}] in ascending order." ) else: self.logger.warning( - f'Timestamp field has not been loaded or specified, ' - f'thus strategy [{keep}] of duplication removal may be meaningless.' + f"Timestamp field has not been loaded or specified, " + f"thus strategy [{keep}] of duplication removal may be meaningless." ) - self.inter_feat.drop_duplicates(subset=[self.uid_field, self.iid_field], keep=keep, inplace=True) + self.inter_feat.drop_duplicates( + subset=[self.uid_field, self.iid_field], keep=keep, inplace=True + ) def _filter_by_inter_num(self): """Filter by number of interaction. - The interval of the number of interactions can be set, and only users/items whose number + The interval of the number of interactions can be set, and only users/items whose number of interactions is in the specified interval can be retained. See :doc:`../user_guide/data/data_args` for detail arg setting. Note: - Lower bound of the interval is also called k-core filtering, which means this method + Lower bound of the interval is also called k-core filtering, which means this method will filter loops until all the users and items has at least k interactions. """ if self.uid_field is None or self.iid_field is None: return - user_inter_num_interval = self._parse_intervals_str(self.config['user_inter_num_interval']) - item_inter_num_interval = self._parse_intervals_str(self.config['item_inter_num_interval']) + user_inter_num_interval = self._parse_intervals_str( + self.config["user_inter_num_interval"] + ) + item_inter_num_interval = self._parse_intervals_str( + self.config["item_inter_num_interval"] + ) if user_inter_num_interval is None and item_inter_num_interval is None: return - user_inter_num = Counter(self.inter_feat[self.uid_field].values) if user_inter_num_interval else Counter() - item_inter_num = Counter(self.inter_feat[self.iid_field].values) if item_inter_num_interval else Counter() + user_inter_num = ( + Counter(self.inter_feat[self.uid_field].values) + if user_inter_num_interval + else Counter() + ) + item_inter_num = ( + Counter(self.inter_feat[self.iid_field].values) + if item_inter_num_interval + else Counter() + ) while True: ban_users = self._get_illegal_ids_by_inter_num( field=self.uid_field, feat=self.user_feat, inter_num=user_inter_num, - inter_interval=user_inter_num_interval + inter_interval=user_inter_num_interval, ) ban_items = self._get_illegal_ids_by_inter_num( field=self.iid_field, feat=self.item_feat, inter_num=item_inter_num, - inter_interval=item_inter_num_interval + inter_interval=item_inter_num_interval, ) if len(ban_users) == 0 and len(ban_items) == 0: @@ -729,38 +821,47 @@ def _filter_by_inter_num(self): item_inter_num -= Counter(item_inter[dropped_inter].values) dropped_index = self.inter_feat.index[dropped_inter] - self.logger.debug(f'[{len(dropped_index)}] dropped interactions.') + self.logger.debug(f"[{len(dropped_index)}] dropped interactions.") self.inter_feat.drop(dropped_index, inplace=True) - def _get_illegal_ids_by_inter_num(self, field, feat, inter_num, inter_interval=None): + def _get_illegal_ids_by_inter_num( + self, field, feat, inter_num, inter_interval=None + ): """Given inter feat, return illegal ids, whose inter num out of [min_num, max_num] Args: field (str): field name of user_id or item_id. feat (pandas.DataFrame): interaction feature. inter_num (Counter): interaction number counter. - inter_interval (list, optional): the allowed interval(s) of the number of interactions. + inter_interval (list, optional): the allowed interval(s) of the number of interactions. Defaults to ``None``. Returns: set: illegal ids, whose inter num out of inter_intervals. """ self.logger.debug( - set_color('get_illegal_ids_by_inter_num', 'blue') + f': field=[{field}], inter_interval=[{inter_interval}]' + set_color("get_illegal_ids_by_inter_num", "blue") + + f": field=[{field}], inter_interval=[{inter_interval}]" ) if inter_interval is not None: if len(inter_interval) > 1: - self.logger.warning(f'More than one interval of interaction number are given!') + self.logger.warning( + f"More than one interval of interaction number are given!" + ) - ids = {id_ for id_ in inter_num if not self._within_intervals(inter_num[id_], inter_interval)} + ids = { + id_ + for id_ in inter_num + if not self._within_intervals(inter_num[id_], inter_interval) + } if feat is not None: min_num = inter_interval[0][1] if inter_interval else -1 for id_ in feat[field].values: if inter_num[id_] < min_num: ids.add(id_) - self.logger.debug(f'[{len(ids)}] illegal_ids_by_inter_num, field=[{field}]') + self.logger.debug(f"[{len(ids)}] illegal_ids_by_inter_num, field=[{field}]") return ids def _parse_intervals_str(self, intervals_str): @@ -776,60 +877,77 @@ def _parse_intervals_str(self, intervals_str): return None endpoints = [] - for endpoint_pair_str in str(intervals_str).split(';'): + for endpoint_pair_str in str(intervals_str).split(";"): endpoint_pair_str = endpoint_pair_str.strip() left_bracket, right_bracket = endpoint_pair_str[0], endpoint_pair_str[-1] - endpoint_pair = endpoint_pair_str[1:-1].split(',') - if not (len(endpoint_pair) == 2 and left_bracket in ['(', '['] and right_bracket in [')', ']']): - self.logger.warning(f'{endpoint_pair_str} is an illegal interval!') + endpoint_pair = endpoint_pair_str[1:-1].split(",") + if not ( + len(endpoint_pair) == 2 + and left_bracket in ["(", "["] + and right_bracket in [")", "]"] + ): + self.logger.warning(f"{endpoint_pair_str} is an illegal interval!") continue left_point, right_point = float(endpoint_pair[0]), float(endpoint_pair[1]) if left_point > right_point: - self.logger.warning(f'{endpoint_pair_str} is an illegal interval!') + self.logger.warning(f"{endpoint_pair_str} is an illegal interval!") endpoints.append((left_bracket, left_point, right_point, right_bracket)) return endpoints def _within_intervals(self, num, intervals): - """ return Ture if the num is in the intervals. + """return Ture if the num is in the intervals. Note: return true when the intervals is None. """ result = True - for i, (left_bracket, left_point, right_point, right_bracket) in enumerate(intervals): - temp_result = num >= left_point if left_bracket == '[' else num > left_point - temp_result &= num <= right_point if right_bracket == ']' else num < right_point + for i, (left_bracket, left_point, right_point, right_bracket) in enumerate( + intervals + ): + temp_result = num >= left_point if left_bracket == "[" else num > left_point + temp_result &= ( + num <= right_point if right_bracket == "]" else num < right_point + ) result = temp_result if i == 0 else result | temp_result return result def _filter_by_field_value(self): - """Filter features according to its values. - """ + """Filter features according to its values.""" - val_intervals = {} if self.config['val_interval'] is None else self.config['val_interval'] - self.logger.debug(set_color('drop_by_value', 'blue') + f': val={val_intervals}') + val_intervals = ( + {} if self.config["val_interval"] is None else self.config["val_interval"] + ) + self.logger.debug(set_color("drop_by_value", "blue") + f": val={val_intervals}") for field, interval in val_intervals.items(): if field not in self.field2type: - raise ValueError(f'Field [{field}] not defined in dataset.') + raise ValueError(f"Field [{field}] not defined in dataset.") if self.field2type[field] in {FeatureType.FLOAT, FeatureType.FLOAT_SEQ}: field_val_interval = self._parse_intervals_str(interval) for feat in self.field2feats(field): - feat.drop(feat.index[~self._within_intervals(feat[field].values, field_val_interval)], inplace=True) + feat.drop( + feat.index[ + ~self._within_intervals( + feat[field].values, field_val_interval + ) + ], + inplace=True, + ) else: # token-like field for feat in self.field2feats(field): feat.drop(feat.index[~feat[field].isin(interval)], inplace=True) def _reset_index(self): - """Reset index for all feats in :attr:`feat_name_list`. - """ + """Reset index for all feats in :attr:`feat_name_list`.""" for feat_name in self.feat_name_list: feat = getattr(self, feat_name) if feat.empty: - raise ValueError('Some feat is empty, please check the filtering settings.') + raise ValueError( + "Some feat is empty, please check the filtering settings." + ) feat.reset_index(drop=True, inplace=True) def _del_col(self, feat, field): @@ -839,19 +957,24 @@ def _del_col(self, feat, field): feat (pandas.DataFrame or Interaction): the feat contains field. field (str): field name to be dropped. """ - self.logger.debug(f'Delete column [{field}].') + self.logger.debug(f"Delete column [{field}].") if isinstance(feat, Interaction): feat.drop(column=field) else: feat.drop(columns=field, inplace=True) - for dct in [self.field2id_token, self.field2token_id, self.field2seqlen, self.field2source, self.field2type]: + for dct in [ + self.field2id_token, + self.field2token_id, + self.field2seqlen, + self.field2source, + self.field2type, + ]: if field in dct: del dct[field] def _filter_inter_by_user_or_item(self): - """Remove interaction in inter_feat which user or item is not in user_feat or item_feat. - """ - if self.config['filter_inter_by_user_or_item'] is not True: + """Remove interaction in inter_feat which user or item is not in user_feat or item_feat.""" + if self.config["filter_inter_by_user_or_item"] is not True: return remained_inter = pd.Series(True, index=self.inter_feat.index) @@ -877,21 +1000,25 @@ def _set_label_by_threshold(self): Key of ``config['threshold']`` if a field name. This field will be dropped after label generation. """ - threshold = self.config['threshold'] + threshold = self.config["threshold"] if threshold is None: return - self.logger.debug(f'Set label by {threshold}.') + self.logger.debug(f"Set label by {threshold}.") if len(threshold) != 1: - raise ValueError('Threshold length should be 1.') + raise ValueError("Threshold length should be 1.") - self.set_field_property(self.label_field, FeatureType.FLOAT, FeatureSource.INTERACTION, 1) + self.set_field_property( + self.label_field, FeatureType.FLOAT, FeatureSource.INTERACTION, 1 + ) for field, value in threshold.items(): if field in self.inter_feat: - self.inter_feat[self.label_field] = (self.inter_feat[field] >= value).astype(int) + self.inter_feat[self.label_field] = ( + self.inter_feat[field] >= value + ).astype(int) else: - raise ValueError(f'Field [{field}] not in inter_feat.') + raise ValueError(f"Field [{field}] not in inter_feat.") if field != self.label_field: self._del_col(self.inter_feat, field) @@ -922,8 +1049,7 @@ def _get_remap_list(self, field_list): return remap_list def _remap_ID_all(self): - """Remap all token-like fields. - """ + """Remap all token-like fields.""" for alias in self.alias.values(): remap_list = self._get_remap_list(alias) self._remap(remap_list) @@ -964,7 +1090,7 @@ def _remap(self, remap_list): tokens, split_point = self._concat_remaped_tokens(remap_list) new_ids_list, mp = pd.factorize(tokens) new_ids_list = np.split(new_ids_list + 1, split_point) - mp = np.array(['[PAD]'] + list(mp)) + mp = np.array(["[PAD]"] + list(mp)) token_id = {t: i for i, t in enumerate(mp)} for (feat, field, ftype), new_ids in zip(remap_list, new_ids_list): @@ -978,8 +1104,7 @@ def _remap(self, remap_list): feat[field] = np.split(new_ids, split_point) def _change_feat_format(self): - """Change feat format from :class:`pandas.DataFrame` to :class:`Interaction`. - """ + """Change feat format from :class:`pandas.DataFrame` to :class:`Interaction`.""" for feat_name in self.feat_name_list: feat = getattr(self, feat_name) setattr(self, feat_name, self._dataframe_to_interaction(feat)) @@ -995,7 +1120,7 @@ def num(self, field): int: The number of different tokens (``1`` if ``field`` is a float-like field). """ if field not in self.field2type: - raise ValueError(f'Field [{field}] not defined in dataset.') + raise ValueError(f"Field [{field}] not defined in dataset.") if self.field2type[field] not in {FeatureType.TOKEN, FeatureType.TOKEN_SEQ}: return self.field2seqlen[field] else: @@ -1090,7 +1215,7 @@ def copy_field_property(self, dest_field, source_field): def field2feats(self, field): if field not in self.field2source: - raise ValueError(f'Field [{field}] not defined in dataset.') + raise ValueError(f"Field [{field}] not defined in dataset.") if field == self.uid_field: feats = [self.inter_feat] if self.user_feat is not None: @@ -1103,7 +1228,7 @@ def field2feats(self, field): source = self.field2source[field] if not isinstance(source, str): source = source.value - feats = [getattr(self, f'{source}_feat')] + feats = [getattr(self, f"{source}_feat")] return feats def token2id(self, field, tokens): @@ -1120,11 +1245,11 @@ def token2id(self, field, tokens): if tokens in self.field2token_id[field]: return self.field2token_id[field][tokens] else: - raise ValueError(f'token [{tokens}] is not existed in {field}') + raise ValueError(f"token [{tokens}] is not existed in {field}") elif isinstance(tokens, (list, np.ndarray)): return np.array([self.token2id(field, token) for token in tokens]) else: - raise TypeError(f'The type of tokens [{tokens}] is not supported') + raise TypeError(f"The type of tokens [{tokens}] is not supported") def id2token(self, field, ids): """Map internal ids to external tokens. @@ -1140,9 +1265,9 @@ def id2token(self, field, ids): return self.field2id_token[field][ids] except IndexError: if isinstance(ids, list): - raise ValueError(f'[{ids}] is not a one-dimensional list.') + raise ValueError(f"[{ids}] is not a one-dimensional list.") else: - raise ValueError(f'[{ids}] is not a valid ids.') + raise ValueError(f"[{ids}] is not a valid ids.") def counter(self, field): """Given ``field``, if it is a token field in ``inter_feat``, @@ -1156,14 +1281,14 @@ def counter(self, field): Counter: The counter of different tokens. """ if field not in self.inter_feat: - raise ValueError(f'Field [{field}] is not defined in ``inter_feat``.') + raise ValueError(f"Field [{field}] is not defined in ``inter_feat``.") if self.field2type[field] == FeatureType.TOKEN: if isinstance(self.inter_feat, pd.DataFrame): return Counter(self.inter_feat[field].values) else: return Counter(self.inter_feat[field].numpy()) else: - raise ValueError(f'Field [{field}] is not a token field.') + raise ValueError(f"Field [{field}] is not a token field.") @property def user_counter(self): @@ -1172,7 +1297,7 @@ def user_counter(self): Returns: Counter: The counter of different users. """ - self._check_field('uid_field') + self._check_field("uid_field") return self.counter(self.uid_field) @property @@ -1182,7 +1307,7 @@ def item_counter(self): Returns: Counter: The counter of different items. """ - self._check_field('iid_field') + self._check_field("iid_field") return self.counter(self.iid_field) @property @@ -1192,7 +1317,7 @@ def user_num(self): Returns: int: Number of different tokens of ``self.uid_field``. """ - self._check_field('uid_field') + self._check_field("uid_field") return self.num(self.uid_field) @property @@ -1202,7 +1327,7 @@ def item_num(self): Returns: int: Number of different tokens of ``self.iid_field``. """ - self._check_field('iid_field') + self._check_field("iid_field") return self.num(self.iid_field) @property @@ -1224,7 +1349,9 @@ def avg_actions_of_users(self): if isinstance(self.inter_feat, pd.DataFrame): return np.mean(self.inter_feat.groupby(self.uid_field).size()) else: - return np.mean(list(Counter(self.inter_feat[self.uid_field].numpy()).values())) + return np.mean( + list(Counter(self.inter_feat[self.uid_field].numpy()).values()) + ) @property def avg_actions_of_items(self): @@ -1236,7 +1363,9 @@ def avg_actions_of_items(self): if isinstance(self.inter_feat, pd.DataFrame): return np.mean(self.inter_feat.groupby(self.iid_field).size()) else: - return np.mean(list(Counter(self.inter_feat[self.iid_field].numpy()).values())) + return np.mean( + list(Counter(self.inter_feat[self.iid_field].numpy()).values()) + ) @property def sparsity(self): @@ -1255,7 +1384,7 @@ def _check_field(self, *field_names): """ for field_name in field_names: if getattr(self, field_name, None) is None: - raise ValueError(f'{field_name} isn\'t set.') + raise ValueError(f"{field_name} isn't set.") def join(self, df): """Given interaction feature, join user/item feature into it. @@ -1283,22 +1412,31 @@ def __repr__(self): return self.__str__() def __str__(self): - info = [set_color(self.dataset_name, 'pink')] + info = [set_color(self.dataset_name, "pink")] if self.uid_field: - info.extend([ - set_color('The number of users', 'blue') + f': {self.user_num}', - set_color('Average actions of users', 'blue') + f': {self.avg_actions_of_users}' - ]) + info.extend( + [ + set_color("The number of users", "blue") + f": {self.user_num}", + set_color("Average actions of users", "blue") + + f": {self.avg_actions_of_users}", + ] + ) if self.iid_field: - info.extend([ - set_color('The number of items', 'blue') + f': {self.item_num}', - set_color('Average actions of items', 'blue') + f': {self.avg_actions_of_items}' - ]) - info.append(set_color('The number of inters', 'blue') + f': {self.inter_num}') + info.extend( + [ + set_color("The number of items", "blue") + f": {self.item_num}", + set_color("Average actions of items", "blue") + + f": {self.avg_actions_of_items}", + ] + ) + info.append(set_color("The number of inters", "blue") + f": {self.inter_num}") if self.uid_field and self.iid_field: - info.append(set_color('The sparsity of the dataset', 'blue') + f': {self.sparsity * 100}%') - info.append(set_color('Remain Fields', 'blue') + f': {list(self.field2type)}') - return '\n'.join(info) + info.append( + set_color("The sparsity of the dataset", "blue") + + f": {self.sparsity * 100}%" + ) + info.append(set_color("Remain Fields", "blue") + f": {list(self.field2type)}") + return "\n".join(info) def copy(self, new_inter_feat): """Given a new interaction feature, return a new :class:`Dataset` object, @@ -1315,18 +1453,17 @@ def copy(self, new_inter_feat): return nxt def _drop_unused_col(self): - """Drop columns which are loaded for data preparation but not used in model. - """ - unused_col = self.config['unused_col'] + """Drop columns which are loaded for data preparation but not used in model.""" + unused_col = self.config["unused_col"] if unused_col is None: return for feat_name, unused_fields in unused_col.items(): - feat = getattr(self, feat_name + '_feat') + feat = getattr(self, feat_name + "_feat") for field in unused_fields: if field not in feat: self.logger.warning( - f'Field [{field}] is not in [{feat_name}_feat], which can not be set in `unused_col`.' + f"Field [{field}] is not in [{feat_name}_feat], which can not be set in `unused_col`." ) continue self._del_col(feat, field) @@ -1377,21 +1514,28 @@ def split_by_ratio(self, ratios, group_by=None): Note: Other than the first one, each part is rounded down. """ - self.logger.debug(f'split by ratios [{ratios}], group_by=[{group_by}]') + self.logger.debug(f"split by ratios [{ratios}], group_by=[{group_by}]") tot_ratio = sum(ratios) ratios = [_ / tot_ratio for _ in ratios] if group_by is None: tot_cnt = self.__len__() split_ids = self._calcu_split_ids(tot=tot_cnt, ratios=ratios) - next_index = [range(start, end) for start, end in zip([0] + split_ids, split_ids + [tot_cnt])] + next_index = [ + range(start, end) + for start, end in zip([0] + split_ids, split_ids + [tot_cnt]) + ] else: - grouped_inter_feat_index = self._grouped_index(self.inter_feat[group_by].numpy()) + grouped_inter_feat_index = self._grouped_index( + self.inter_feat[group_by].numpy() + ) next_index = [[] for _ in range(len(ratios))] for grouped_index in grouped_inter_feat_index: tot_cnt = len(grouped_index) split_ids = self._calcu_split_ids(tot=tot_cnt, ratios=ratios) - for index, start, end in zip(next_index, [0] + split_ids, split_ids + [tot_cnt]): + for index, start, end in zip( + next_index, [0] + split_ids, split_ids + [tot_cnt] + ): index.extend(grouped_index[start:end]) self._drop_unused_col() @@ -1432,21 +1576,33 @@ def leave_one_out(self, group_by, leave_one_mode): Returns: list: List of :class:`~Dataset`, whose interaction features has been split. """ - self.logger.debug(f'leave one out, group_by=[{group_by}], leave_one_mode=[{leave_one_mode}]') + self.logger.debug( + f"leave one out, group_by=[{group_by}], leave_one_mode=[{leave_one_mode}]" + ) if group_by is None: - raise ValueError('leave one out strategy require a group field') + raise ValueError("leave one out strategy require a group field") - grouped_inter_feat_index = self._grouped_index(self.inter_feat[group_by].numpy()) - if leave_one_mode == 'valid_and_test': - next_index = self._split_index_by_leave_one_out(grouped_inter_feat_index, leave_one_num=2) - elif leave_one_mode == 'valid_only': - next_index = self._split_index_by_leave_one_out(grouped_inter_feat_index, leave_one_num=1) + grouped_inter_feat_index = self._grouped_index( + self.inter_feat[group_by].numpy() + ) + if leave_one_mode == "valid_and_test": + next_index = self._split_index_by_leave_one_out( + grouped_inter_feat_index, leave_one_num=2 + ) + elif leave_one_mode == "valid_only": + next_index = self._split_index_by_leave_one_out( + grouped_inter_feat_index, leave_one_num=1 + ) next_index.append([]) - elif leave_one_mode == 'test_only': - next_index = self._split_index_by_leave_one_out(grouped_inter_feat_index, leave_one_num=1) + elif leave_one_mode == "test_only": + next_index = self._split_index_by_leave_one_out( + grouped_inter_feat_index, leave_one_num=1 + ) next_index = [next_index[0], [], next_index[1]] else: - raise NotImplementedError(f'The leave_one_mode [{leave_one_mode}] has not been implemented.') + raise NotImplementedError( + f"The leave_one_mode [{leave_one_mode}] has not been implemented." + ) self._drop_unused_col() next_df = [self.inter_feat[index] for index in next_index] @@ -1454,8 +1610,7 @@ def leave_one_out(self, group_by, leave_one_mode): return next_ds def shuffle(self): - """Shuffle the interaction records inplace. - """ + """Shuffle the interaction records inplace.""" self.inter_feat.shuffle() def sort(self, by, ascending=True): @@ -1480,52 +1635,66 @@ def build(self): if self.benchmark_filename_list is not None: self._drop_unused_col() cumsum = list(np.cumsum(self.file_size_list)) - datasets = [self.copy(self.inter_feat[start:end]) for start, end in zip([0] + cumsum[:-1], cumsum)] + datasets = [ + self.copy(self.inter_feat[start:end]) + for start, end in zip([0] + cumsum[:-1], cumsum) + ] return datasets # ordering - ordering_args = self.config['eval_args']['order'] - if ordering_args == 'RO': + ordering_args = self.config["eval_args"]["order"] + if ordering_args == "RO": self.shuffle() - elif ordering_args == 'TO': + elif ordering_args == "TO": self.sort(by=self.time_field) else: - raise NotImplementedError(f'The ordering_method [{ordering_args}] has not been implemented.') + raise NotImplementedError( + f"The ordering_method [{ordering_args}] has not been implemented." + ) # splitting & grouping - split_args = self.config['eval_args']['split'] + split_args = self.config["eval_args"]["split"] if split_args is None: - raise ValueError('The split_args in eval_args should not be None.') + raise ValueError("The split_args in eval_args should not be None.") if not isinstance(split_args, dict): - raise ValueError(f'The split_args [{split_args}] should be a dict.') + raise ValueError(f"The split_args [{split_args}] should be a dict.") split_mode = list(split_args.keys())[0] assert len(split_args.keys()) == 1 - group_by = self.config['eval_args']['group_by'] - if split_mode == 'RS': - if not isinstance(split_args['RS'], list): + group_by = self.config["eval_args"]["group_by"] + if split_mode == "RS": + if not isinstance(split_args["RS"], list): raise ValueError(f'The value of "RS" [{split_args}] should be a list.') - if group_by is None or group_by.lower() == 'none': - datasets = self.split_by_ratio(split_args['RS'], group_by=None) - elif group_by == 'user': - datasets = self.split_by_ratio(split_args['RS'], group_by=self.uid_field) + if group_by is None or group_by.lower() == "none": + datasets = self.split_by_ratio(split_args["RS"], group_by=None) + elif group_by == "user": + datasets = self.split_by_ratio( + split_args["RS"], group_by=self.uid_field + ) else: - raise NotImplementedError(f'The grouping method [{group_by}] has not been implemented.') - elif split_mode == 'LS': - datasets = self.leave_one_out(group_by=self.uid_field, leave_one_mode=split_args['LS']) + raise NotImplementedError( + f"The grouping method [{group_by}] has not been implemented." + ) + elif split_mode == "LS": + datasets = self.leave_one_out( + group_by=self.uid_field, leave_one_mode=split_args["LS"] + ) else: - raise NotImplementedError(f'The splitting_method [{split_mode}] has not been implemented.') + raise NotImplementedError( + f"The splitting_method [{split_mode}] has not been implemented." + ) return datasets def save(self): - """Saving this :class:`Dataset` object to :attr:`config['checkpoint_dir']`. - """ - save_dir = self.config['checkpoint_dir'] + """Saving this :class:`Dataset` object to :attr:`config['checkpoint_dir']`.""" + save_dir = self.config["checkpoint_dir"] ensure_dir(save_dir) file = os.path.join(save_dir, f'{self.config["dataset"]}-dataset.pth') - self.logger.info(set_color('Saving filtered dataset into ', 'pink') + f'[{file}]') - with open(file, 'wb') as f: + self.logger.info( + set_color("Saving filtered dataset into ", "pink") + f"[{file}]" + ) + with open(file, "wb") as f: pickle.dump(self, f) def get_user_feature(self): @@ -1534,7 +1703,7 @@ def get_user_feature(self): Interaction: user features """ if self.user_feat is None: - self._check_field('uid_field') + self._check_field("uid_field") return Interaction({self.uid_field: torch.arange(self.user_num)}) else: return self.user_feat @@ -1545,12 +1714,14 @@ def get_item_feature(self): Interaction: item features """ if self.item_feat is None: - self._check_field('iid_field') + self._check_field("iid_field") return Interaction({self.iid_field: torch.arange(self.item_num)}) else: return self.item_feat - def _create_sparse_matrix(self, df_feat, source_field, target_field, form='coo', value_field=None): + def _create_sparse_matrix( + self, df_feat, source_field, target_field, form="coo", value_field=None + ): """Get sparse matrix that describe relations between two fields. Source and target should be token-like fields. @@ -1577,18 +1748,26 @@ def _create_sparse_matrix(self, df_feat, source_field, target_field, form='coo', data = np.ones(len(df_feat)) else: if value_field not in df_feat: - raise ValueError(f'Value_field [{value_field}] should be one of `df_feat`\'s features.') + raise ValueError( + f"Value_field [{value_field}] should be one of `df_feat`'s features." + ) data = df_feat[value_field] - mat = coo_matrix((data, (src, tgt)), shape=(self.num(source_field), self.num(target_field))) + mat = coo_matrix( + (data, (src, tgt)), shape=(self.num(source_field), self.num(target_field)) + ) - if form == 'coo': + if form == "coo": return mat - elif form == 'csr': + elif form == "csr": return mat.tocsr() else: - raise NotImplementedError(f'Sparse matrix format [{form}] has not been implemented.') + raise NotImplementedError( + f"Sparse matrix format [{form}] has not been implemented." + ) - def _create_graph(self, tensor_feat, source_field, target_field, form='dgl', value_field=None): + def _create_graph( + self, tensor_feat, source_field, target_field, form="dgl", value_field=None + ): """Get graph that describe relations between two fields. Source and target should be token-like fields. @@ -1618,8 +1797,9 @@ def _create_graph(self, tensor_feat, source_field, target_field, form='dgl', val src = tensor_feat[source_field] tgt = tensor_feat[target_field] - if form == 'dgl': + if form == "dgl": import dgl + graph = dgl.graph((src, tgt)) if value_field is not None: if isinstance(value_field, str): @@ -1627,15 +1807,18 @@ def _create_graph(self, tensor_feat, source_field, target_field, form='dgl', val for k in value_field: graph.edata[k] = tensor_feat[k] return graph - elif form == 'pyg': + elif form == "pyg": from torch_geometric.data import Data + edge_attr = tensor_feat[value_field] if value_field else None graph = Data(edge_index=torch.stack([src, tgt]), edge_attr=edge_attr) return graph else: - raise NotImplementedError(f'Graph format [{form}] has not been implemented.') + raise NotImplementedError( + f"Graph format [{form}] has not been implemented." + ) - def inter_matrix(self, form='coo', value_field=None): + def inter_matrix(self, form="coo", value_field=None): """Get sparse matrix that describe interactions between user_id and item_id. Sparse matrix has shape (user_num, item_num). @@ -1652,8 +1835,12 @@ def inter_matrix(self, form='coo', value_field=None): scipy.sparse: Sparse matrix in form ``coo`` or ``csr``. """ if not self.uid_field or not self.iid_field: - raise ValueError('dataset does not exist uid/iid, thus can not converted to sparse matrix.') - return self._create_sparse_matrix(self.inter_feat, self.uid_field, self.iid_field, form, value_field) + raise ValueError( + "dataset does not exist uid/iid, thus can not converted to sparse matrix." + ) + return self._create_sparse_matrix( + self.inter_feat, self.uid_field, self.iid_field, form, value_field + ) def _history_matrix(self, row, value_field=None): """Get dense matrix describe user/item's history interaction records. @@ -1678,17 +1865,22 @@ def _history_matrix(self, row, value_field=None): - History values matrix (torch.Tensor): ``history_value`` described above. - History length matrix (torch.Tensor): ``history_len`` described above. """ - self._check_field('uid_field', 'iid_field') + self._check_field("uid_field", "iid_field") - user_ids, item_ids = self.inter_feat[self.uid_field].numpy(), self.inter_feat[self.iid_field].numpy() + user_ids, item_ids = ( + self.inter_feat[self.uid_field].numpy(), + self.inter_feat[self.iid_field].numpy(), + ) if value_field is None: values = np.ones(len(self.inter_feat)) else: if value_field not in self.inter_feat: - raise ValueError(f'Value_field [{value_field}] should be one of `inter_feat`\'s features.') + raise ValueError( + f"Value_field [{value_field}] should be one of `inter_feat`'s features." + ) values = self.inter_feat[value_field].numpy() - if row == 'user': + if row == "user": row_num, max_col_num = self.user_num, self.item_num row_ids, col_ids = user_ids, item_ids else: @@ -1702,8 +1894,8 @@ def _history_matrix(self, row, value_field=None): col_num = np.max(history_len) if col_num > max_col_num * 0.2: self.logger.warning( - f'Max value of {row}\'s history interaction records has reached ' - f'{col_num / max_col_num * 100}% of the total.' + f"Max value of {row}'s history interaction records has reached " + f"{col_num / max_col_num * 100}% of the total." ) history_matrix = np.zeros((row_num, col_num), dtype=np.int64) @@ -1714,7 +1906,11 @@ def _history_matrix(self, row, value_field=None): history_value[row_id, history_len[row_id]] = value history_len[row_id] += 1 - return torch.LongTensor(history_matrix), torch.FloatTensor(history_value), torch.LongTensor(history_len) + return ( + torch.LongTensor(history_matrix), + torch.FloatTensor(history_value), + torch.LongTensor(history_len), + ) def history_item_matrix(self, value_field=None): """Get dense matrix describe user's history interaction records. @@ -1738,7 +1934,7 @@ def history_item_matrix(self, value_field=None): - History values matrix (torch.Tensor): ``history_value`` described above. - History length matrix (torch.Tensor): ``history_len`` described above. """ - return self._history_matrix(row='user', value_field=value_field) + return self._history_matrix(row="user", value_field=value_field) def history_user_matrix(self, value_field=None): """Get dense matrix describe item's history interaction records. @@ -1762,7 +1958,7 @@ def history_user_matrix(self, value_field=None): - History values matrix (torch.Tensor): ``history_value`` described above. - History length matrix (torch.Tensor): ``history_len`` described above. """ - return self._history_matrix(row='item', value_field=value_field) + return self._history_matrix(row="item", value_field=value_field) def get_preload_weight(self, field): """Get preloaded weight matrix, whose rows are sorted by token ids. @@ -1776,7 +1972,7 @@ def get_preload_weight(self, field): numpy.ndarray: preloaded weight matrix. See :doc:`../user_guide/config/data_settings` for details. """ if field not in self._preloaded_weight: - raise ValueError(f'Field [{field}] not in preload_weight') + raise ValueError(f"Field [{field}] not in preload_weight") return self._preloaded_weight[field] def _dataframe_to_interaction(self, data): @@ -1797,9 +1993,9 @@ def _dataframe_to_interaction(self, data): elif ftype == FeatureType.FLOAT: new_data[k] = torch.FloatTensor(value) elif ftype == FeatureType.TOKEN_SEQ: - seq_data = [torch.LongTensor(d[:self.field2seqlen[k]]) for d in value] + seq_data = [torch.LongTensor(d[: self.field2seqlen[k]]) for d in value] new_data[k] = rnn_utils.pad_sequence(seq_data, batch_first=True) elif ftype == FeatureType.FLOAT_SEQ: - seq_data = [torch.FloatTensor(d[:self.field2seqlen[k]]) for d in value] + seq_data = [torch.FloatTensor(d[: self.field2seqlen[k]]) for d in value] new_data[k] = rnn_utils.pad_sequence(seq_data, batch_first=True) return Interaction(new_data) diff --git a/recbole/data/dataset/decisiontree_dataset.py b/recbole/data/dataset/decisiontree_dataset.py index b28e4bc05..2c675f46d 100644 --- a/recbole/data/dataset/decisiontree_dataset.py +++ b/recbole/data/dataset/decisiontree_dataset.py @@ -13,7 +13,7 @@ class DecisionTreeDataset(Dataset): """:class:`DecisionTreeDataset` is based on :class:`~recbole.data.dataset.dataset.Dataset`, - and + and Attributes: @@ -30,7 +30,10 @@ def _judge_token_and_convert(self, feat): continue if self.field2type[col_name] == FeatureType.TOKEN: col_list.append(col_name) - elif self.field2type[col_name] in {FeatureType.TOKEN_SEQ, FeatureType.FLOAT_SEQ}: + elif self.field2type[col_name] in { + FeatureType.TOKEN_SEQ, + FeatureType.FLOAT_SEQ, + }: feat = feat.drop([col_name], axis=1, inplace=False) # get hash map @@ -46,7 +49,7 @@ def _judge_token_and_convert(self, feat): if value not in self.hash_map[col]: self.hash_map[col][value] = self.hash_count[col] self.hash_count[col] = self.hash_count[col] + 1 - if self.hash_count[col] > self.config['token_num_threshold']: + if self.hash_count[col] > self.config["token_num_threshold"]: del_col.append(col) break @@ -64,14 +67,12 @@ def _judge_token_and_convert(self, feat): return feat def _convert_token_to_hash(self): - """Convert the data of token type to hash form - - """ + """Convert the data of token type to hash form""" self.hash_map = {} self.hash_count = {} self.convert_col_list = [] - if self.config['convert_token_to_onehot']: - for feat_name in ['inter_feat', 'user_feat', 'item_feat']: + if self.config["convert_token_to_onehot"]: + for feat_name in ["inter_feat", "user_feat", "item_feat"]: feat = getattr(self, feat_name) if feat is not None: feat = self._judge_token_and_convert(feat) diff --git a/recbole/data/dataset/kg_dataset.py b/recbole/data/dataset/kg_dataset.py index b5a4d444d..77b06315f 100644 --- a/recbole/data/dataset/kg_dataset.py +++ b/recbole/data/dataset/kg_dataset.py @@ -70,18 +70,24 @@ def __init__(self, config): def _get_field_from_config(self): super()._get_field_from_config() - self.head_entity_field = self.config['HEAD_ENTITY_ID_FIELD'] - self.tail_entity_field = self.config['TAIL_ENTITY_ID_FIELD'] - self.relation_field = self.config['RELATION_ID_FIELD'] - self.entity_field = self.config['ENTITY_ID_FIELD'] - self.kg_reverse_r = self.config['kg_reverse_r'] - self.entity_kg_num_interval = self.config['entity_kg_num_interval'] - self.relation_kg_num_interval = self.config['relation_kg_num_interval'] - self._check_field('head_entity_field', 'tail_entity_field', 'relation_field', 'entity_field') - self.set_field_property(self.entity_field, FeatureType.TOKEN, FeatureSource.KG, 1) - - self.logger.debug(set_color('relation_field', 'blue') + f': {self.relation_field}') - self.logger.debug(set_color('entity_field', 'blue') + f': {self.entity_field}') + self.head_entity_field = self.config["HEAD_ENTITY_ID_FIELD"] + self.tail_entity_field = self.config["TAIL_ENTITY_ID_FIELD"] + self.relation_field = self.config["RELATION_ID_FIELD"] + self.entity_field = self.config["ENTITY_ID_FIELD"] + self.kg_reverse_r = self.config["kg_reverse_r"] + self.entity_kg_num_interval = self.config["entity_kg_num_interval"] + self.relation_kg_num_interval = self.config["relation_kg_num_interval"] + self._check_field( + "head_entity_field", "tail_entity_field", "relation_field", "entity_field" + ) + self.set_field_property( + self.entity_field, FeatureType.TOKEN, FeatureSource.KG, 1 + ) + + self.logger.debug( + set_color("relation_field", "blue") + f": {self.relation_field}" + ) + self.logger.debug(set_color("entity_field", "blue") + f": {self.entity_field}") def _data_filtering(self): super()._data_filtering() @@ -91,16 +97,20 @@ def _data_filtering(self): def _filter_kg_by_triple_num(self): """Filter by number of triples. - The interval of the number of triples can be set, and only entities/relations + The interval of the number of triples can be set, and only entities/relations whose number of triples is in the specified interval can be retained. See :doc:`../user_guide/data/data_args` for detail arg setting. Note: - Lower bound of the interval is also called k-core filtering, which means this method + Lower bound of the interval is also called k-core filtering, which means this method will filter loops until all the entities and relations has at least k triples. """ - entity_kg_num_interval = self._parse_intervals_str(self.config['entity_kg_num_interval']) - relation_kg_num_interval = self._parse_intervals_str(self.config['relation_kg_num_interval']) + entity_kg_num_interval = self._parse_intervals_str( + self.config["entity_kg_num_interval"] + ) + relation_kg_num_interval = self._parse_intervals_str( + self.config["relation_kg_num_interval"] + ) if entity_kg_num_interval is None and relation_kg_num_interval is None: return @@ -110,27 +120,31 @@ def _filter_kg_by_triple_num(self): head_entity_kg_num = Counter(self.kg_feat[self.head_entity_field].values) tail_entity_kg_num = Counter(self.kg_feat[self.tail_entity_field].values) entity_kg_num = head_entity_kg_num + tail_entity_kg_num - relation_kg_num = Counter(self.kg_feat[self.relation_field].values) if relation_kg_num_interval else Counter() + relation_kg_num = ( + Counter(self.kg_feat[self.relation_field].values) + if relation_kg_num_interval + else Counter() + ) while True: ban_head_entities = self._get_illegal_ids_by_inter_num( field=self.head_entity_field, feat=None, inter_num=entity_kg_num, - inter_interval=entity_kg_num_interval + inter_interval=entity_kg_num_interval, ) ban_tail_entities = self._get_illegal_ids_by_inter_num( field=self.tail_entity_field, feat=None, inter_num=entity_kg_num, - inter_interval=entity_kg_num_interval + inter_interval=entity_kg_num_interval, ) ban_entities = ban_head_entities | ban_tail_entities ban_relations = self._get_illegal_ids_by_inter_num( field=self.relation_field, feat=None, inter_num=relation_kg_num, - inter_interval=relation_kg_num_interval + inter_interval=relation_kg_num_interval, ) if len(ban_entities) == 0 and len(ban_relations) == 0: break @@ -148,7 +162,7 @@ def _filter_kg_by_triple_num(self): relation_kg_num -= Counter(relation_kg[dropped_kg].values) dropped_index = self.kg_feat.index[dropped_kg] - self.logger.debug(f'[{len(dropped_index)}] dropped triples.') + self.logger.debug(f"[{len(dropped_index)}] dropped triples.") self.kg_feat.drop(dropped_index, inplace=True) def _filter_link(self): @@ -176,10 +190,10 @@ def _filter_link(self): def _download(self): super()._download() - url = self._get_download_url('kg_url', allow_none=True) + url = self._get_download_url("kg_url", allow_none=True) if url is None: return - self.logger.info(f'Prepare to download linked knowledge graph from [{url}].') + self.logger.info(f"Prepare to download linked knowledge graph from [{url}].") if decide_download(url): # No need to create dir, as `super()._download()` has created one. @@ -187,93 +201,100 @@ def _download(self): extract_zip(path, self.dataset_path) os.unlink(path) self.logger.info( - f'\nLinked KG for [{self.dataset_name}] requires additional conversion ' - f'to atomic files (.kg and .link).\n' - f'Please refer to https://github.com/RUCAIBox/RecSysDatasets/tree/master/conversion_tools#knowledge-aware-datasets ' - f'for detailed instructions.\n' - f'You can run RecBole after the conversion, see you soon.' + f"\nLinked KG for [{self.dataset_name}] requires additional conversion " + f"to atomic files (.kg and .link).\n" + f"Please refer to https://github.com/RUCAIBox/RecSysDatasets/tree/master/conversion_tools#knowledge-aware-datasets " + f"for detailed instructions.\n" + f"You can run RecBole after the conversion, see you soon." ) exit(0) else: - self.logger.info('Stop download.') + self.logger.info("Stop download.") exit(-1) def _load_data(self, token, dataset_path): super()._load_data(token, dataset_path) self.kg_feat = self._load_kg(self.dataset_name, self.dataset_path) - self.item2entity, self.entity2item = self._load_link(self.dataset_name, self.dataset_path) + self.item2entity, self.entity2item = self._load_link( + self.dataset_name, self.dataset_path + ) def __str__(self): info = [ super().__str__(), - f'The number of entities: {self.entity_num}', - f'The number of relations: {self.relation_num}', - f'The number of triples: {len(self.kg_feat)}', - f'The number of items that have been linked to KG: {len(self.item2entity)}' + f"The number of entities: {self.entity_num}", + f"The number of relations: {self.relation_num}", + f"The number of triples: {len(self.kg_feat)}", + f"The number of items that have been linked to KG: {len(self.item2entity)}", ] # yapf: disable - return '\n'.join(info) + return "\n".join(info) def _build_feat_name_list(self): feat_name_list = super()._build_feat_name_list() if self.kg_feat is not None: - feat_name_list.append('kg_feat') + feat_name_list.append("kg_feat") return feat_name_list def _load_kg(self, token, dataset_path): - self.logger.debug(set_color(f'Loading kg from [{dataset_path}].', 'green')) - kg_path = os.path.join(dataset_path, f'{token}.kg') + self.logger.debug(set_color(f"Loading kg from [{dataset_path}].", "green")) + kg_path = os.path.join(dataset_path, f"{token}.kg") if not os.path.isfile(kg_path): - raise ValueError(f'[{token}.kg] not found in [{dataset_path}].') + raise ValueError(f"[{token}.kg] not found in [{dataset_path}].") df = self._load_feat(kg_path, FeatureSource.KG) self._check_kg(df) return df def _check_kg(self, kg): - kg_warn_message = 'kg data requires field [{}]' - assert self.head_entity_field in kg, kg_warn_message.format(self.head_entity_field) - assert self.tail_entity_field in kg, kg_warn_message.format(self.tail_entity_field) + kg_warn_message = "kg data requires field [{}]" + assert self.head_entity_field in kg, kg_warn_message.format( + self.head_entity_field + ) + assert self.tail_entity_field in kg, kg_warn_message.format( + self.tail_entity_field + ) assert self.relation_field in kg, kg_warn_message.format(self.relation_field) def _load_link(self, token, dataset_path): - self.logger.debug(set_color(f'Loading link from [{dataset_path}].', 'green')) - link_path = os.path.join(dataset_path, f'{token}.link') + self.logger.debug(set_color(f"Loading link from [{dataset_path}].", "green")) + link_path = os.path.join(dataset_path, f"{token}.link") if not os.path.isfile(link_path): - raise ValueError(f'[{token}.link] not found in [{dataset_path}].') - df = self._load_feat(link_path, 'link') + raise ValueError(f"[{token}.link] not found in [{dataset_path}].") + df = self._load_feat(link_path, "link") self._check_link(df) item2entity, entity2item = {}, {} - for item_id, entity_id in zip(df[self.iid_field].values, df[self.entity_field].values): + for item_id, entity_id in zip( + df[self.iid_field].values, df[self.entity_field].values + ): item2entity[item_id] = entity_id entity2item[entity_id] = item_id return item2entity, entity2item def _check_link(self, link): - link_warn_message = 'link data requires field [{}]' + link_warn_message = "link data requires field [{}]" assert self.entity_field in link, link_warn_message.format(self.entity_field) assert self.iid_field in link, link_warn_message.format(self.iid_field) def _init_alias(self): - """Add :attr:`alias_of_entity_id`, :attr:`alias_of_relation_id` and update :attr:`_rest_fields`. - """ - self._set_alias('entity_id', [self.head_entity_field, self.tail_entity_field]) - self._set_alias('relation_id', [self.relation_field]) + """Add :attr:`alias_of_entity_id`, :attr:`alias_of_relation_id` and update :attr:`_rest_fields`.""" + self._set_alias("entity_id", [self.head_entity_field, self.tail_entity_field]) + self._set_alias("relation_id", [self.relation_field]) super()._init_alias() - self._rest_fields = np.setdiff1d(self._rest_fields, [self.entity_field], assume_unique=True) + self._rest_fields = np.setdiff1d( + self._rest_fields, [self.entity_field], assume_unique=True + ) def _get_rec_item_token(self): - """Get set of entity tokens from fields in ``rec`` level. - """ - remap_list = self._get_remap_list(self.alias['item_id']) + """Get set of entity tokens from fields in ``rec`` level.""" + remap_list = self._get_remap_list(self.alias["item_id"]) tokens, _ = self._concat_remaped_tokens(remap_list) return set(tokens) def _get_entity_token(self): - """Get set of entity tokens from fields in ``ent`` level. - """ - remap_list = self._get_remap_list(self.alias['entity_id']) + """Get set of entity tokens from fields in ``ent`` level.""" + remap_list = self._get_remap_list(self.alias["entity_id"]) tokens, _ = self._concat_remaped_tokens(remap_list) return set(tokens) @@ -296,8 +317,7 @@ def _reset_ent_remapID(self, field, idmap, id2token, token2id): feat[field] = np.split(new_idx, split_point) def _merge_item_and_entity(self): - """Merge item-id and entity-id into the same id-space. - """ + """Merge item-id and entity-id into the same id-space.""" item_token = self.field2id_token[self.iid_field] entity_token = self.field2id_token[self.head_entity_field] item_num = len(item_token) @@ -306,33 +326,45 @@ def _merge_item_and_entity(self): # reset item id item_priority = np.array([token in self.item2entity for token in item_token]) - item_order = np.argsort(item_priority, kind='stable') + item_order = np.argsort(item_priority, kind="stable") item_id_map = np.zeros_like(item_order) item_id_map[item_order] = np.arange(item_num) new_item_id2token = item_token[item_order] new_item_token2id = {t: i for i, t in enumerate(new_item_id2token)} - for field in self.alias['item_id']: - self._reset_ent_remapID(field, item_id_map, new_item_id2token, new_item_token2id) + for field in self.alias["item_id"]: + self._reset_ent_remapID( + field, item_id_map, new_item_id2token, new_item_token2id + ) # reset entity id - entity_priority = np.array([token != '[PAD]' and token not in self.entity2item for token in entity_token]) - entity_order = np.argsort(entity_priority, kind='stable') + entity_priority = np.array( + [ + token != "[PAD]" and token not in self.entity2item + for token in entity_token + ] + ) + entity_order = np.argsort(entity_priority, kind="stable") entity_id_map = np.zeros_like(entity_order) - for i in entity_order[1:link_num + 1]: + for i in entity_order[1 : link_num + 1]: entity_id_map[i] = new_item_token2id[self.entity2item[entity_token[i]]] - entity_id_map[entity_order[link_num + 1:]] = np.arange(item_num, item_num + entity_num - link_num - 1) - new_entity_id2token = np.concatenate([new_item_id2token, entity_token[entity_order[link_num + 1:]]]) + entity_id_map[entity_order[link_num + 1 :]] = np.arange( + item_num, item_num + entity_num - link_num - 1 + ) + new_entity_id2token = np.concatenate( + [new_item_id2token, entity_token[entity_order[link_num + 1 :]]] + ) for i in range(item_num - link_num, item_num): new_entity_id2token[i] = self.item2entity[new_entity_id2token[i]] new_entity_token2id = {t: i for i, t in enumerate(new_entity_id2token)} - for field in self.alias['entity_id']: - self._reset_ent_remapID(field, entity_id_map, new_entity_id2token, new_entity_token2id) + for field in self.alias["entity_id"]: + self._reset_ent_remapID( + field, entity_id_map, new_entity_id2token, new_entity_token2id + ) self.field2id_token[self.entity_field] = new_entity_id2token self.field2token_id[self.entity_field] = new_entity_token2id def _add_auxiliary_relation(self): - """Add auxiliary relations in ``self.relation_field``. - """ + """Add auxiliary relations in ``self.relation_field``.""" if self.kg_reverse_r: # '0' is used for padding, so the number needs to be reduced by one original_rel_num = len(self.field2id_token[self.relation_field]) - 1 @@ -346,25 +378,29 @@ def _add_auxiliary_relation(self): # Add mapping for internal and external ID of relations for i in range(1, original_rel_num + 1): original_token = self.field2id_token[self.relation_field][i] - reverse_token = original_token + '_r' - self.field2token_id[self.relation_field][reverse_token] = i + original_rel_num + reverse_token = original_token + "_r" + self.field2token_id[self.relation_field][reverse_token] = ( + i + original_rel_num + ) self.field2id_token[self.relation_field] = np.append( - self.field2id_token[self.relation_field], reverse_token) + self.field2id_token[self.relation_field], reverse_token + ) # Update knowledge graph triples with reverse relations reverse_kg_data = { self.head_entity_field: original_tids, self.relation_field: reverse_rels, - self.head_entity_field: original_hids + self.head_entity_field: original_hids, } reverse_kg_feat = pd.DataFrame(reverse_kg_data) self.kg_feat = pd.concat([self.kg_feat, reverse_kg_feat]) # Add UI-relation pairs in the relation field kg_rel_num = len(self.field2id_token[self.relation_field]) - self.field2token_id[self.relation_field]['[UI-Relation]'] = kg_rel_num + self.field2token_id[self.relation_field]["[UI-Relation]"] = kg_rel_num self.field2id_token[self.relation_field] = np.append( - self.field2id_token[self.relation_field], '[UI-Relation]') + self.field2id_token[self.relation_field], "[UI-Relation]" + ) def _remap_ID_all(self): super()._remap_ID_all() @@ -421,7 +457,7 @@ def entities(self): """ return np.arange(self.entity_num) - def kg_graph(self, form='coo', value_field=None): + def kg_graph(self, form="coo", value_field=None): """Get graph or sparse matrix that describe relations between entities. For an edge of , ``graph[src, tgt] = 1`` if ``value_field`` is ``None``, @@ -445,15 +481,21 @@ def kg_graph(self, form='coo', value_field=None): .. _PyG: https://github.com/rusty1s/pytorch_geometric """ - args = [self.kg_feat, self.head_entity_field, self.tail_entity_field, form, value_field] - if form in ['coo', 'csr']: + args = [ + self.kg_feat, + self.head_entity_field, + self.tail_entity_field, + form, + value_field, + ] + if form in ["coo", "csr"]: return self._create_sparse_matrix(*args) - elif form in ['dgl', 'pyg']: + elif form in ["dgl", "pyg"]: return self._create_graph(*args) else: - raise NotImplementedError('kg graph format [{}] has not been implemented.') + raise NotImplementedError("kg graph format [{}] has not been implemented.") - def _create_ckg_sparse_matrix(self, form='coo', show_relation=False): + def _create_ckg_sparse_matrix(self, form="coo", show_relation=False): user_num = self.user_num hids = self.head_entities + user_num @@ -464,7 +506,7 @@ def _create_ckg_sparse_matrix(self, form='coo', show_relation=False): ui_rel_num = len(uids) ui_rel_id = self.relation_num - 1 - assert self.field2id_token[self.relation_field][ui_rel_id] == '[UI-Relation]' + assert self.field2id_token[self.relation_field][ui_rel_id] == "[UI-Relation]" src = np.concatenate([uids, iids, hids]) tgt = np.concatenate([iids, uids, tids]) @@ -477,14 +519,16 @@ def _create_ckg_sparse_matrix(self, form='coo', show_relation=False): data = np.concatenate([ui_rel, kg_rel]) node_num = self.entity_num + self.user_num mat = coo_matrix((data, (src, tgt)), shape=(node_num, node_num)) - if form == 'coo': + if form == "coo": return mat - elif form == 'csr': + elif form == "csr": return mat.tocsr() else: - raise NotImplementedError(f'Sparse matrix format [{form}] has not been implemented.') + raise NotImplementedError( + f"Sparse matrix format [{form}] has not been implemented." + ) - def _create_ckg_graph(self, form='dgl', show_relation=False): + def _create_ckg_graph(self, form="dgl", show_relation=False): user_num = self.user_num kg_tensor = self.kg_feat @@ -502,26 +546,32 @@ def _create_ckg_graph(self, form='dgl', show_relation=False): if show_relation: ui_rel_num = user.shape[0] ui_rel_id = self.relation_num - 1 - assert self.field2id_token[self.relation_field][ui_rel_id] == '[UI-Relation]' + assert ( + self.field2id_token[self.relation_field][ui_rel_id] == "[UI-Relation]" + ) kg_rel = kg_tensor[self.relation_field] ui_rel = torch.full((2 * ui_rel_num,), ui_rel_id, dtype=kg_rel.dtype) edge = torch.cat([ui_rel, kg_rel]) - if form == 'dgl': + if form == "dgl": import dgl + graph = dgl.graph((src, tgt)) if show_relation: graph.edata[self.relation_field] = edge return graph - elif form == 'pyg': + elif form == "pyg": from torch_geometric.data import Data + edge_attr = edge if show_relation else None graph = Data(edge_index=torch.stack([src, tgt]), edge_attr=edge_attr) return graph else: - raise NotImplementedError(f'Graph format [{form}] has not been implemented.') + raise NotImplementedError( + f"Graph format [{form}] has not been implemented." + ) - def ckg_graph(self, form='coo', value_field=None): + def ckg_graph(self, form="coo", value_field=None): """Get graph or sparse matrix that describe relations of CKG, which combines interactions and kg triplets into the same graph. @@ -550,12 +600,14 @@ def ckg_graph(self, form='coo', value_field=None): https://github.com/rusty1s/pytorch_geometric """ if value_field is not None and value_field != self.relation_field: - raise ValueError(f'Value_field [{value_field}] can only be [{self.relation_field}] in ckg_graph.') + raise ValueError( + f"Value_field [{value_field}] can only be [{self.relation_field}] in ckg_graph." + ) show_relation = value_field is not None - if form in ['coo', 'csr']: + if form in ["coo", "csr"]: return self._create_ckg_sparse_matrix(form, show_relation) - elif form in ['dgl', 'pyg']: + elif form in ["dgl", "pyg"]: return self._create_ckg_graph(form, show_relation) else: - raise NotImplementedError('ckg graph format [{}] has not been implemented.') + raise NotImplementedError("ckg graph format [{}] has not been implemented.") diff --git a/recbole/data/dataset/sequential_dataset.py b/recbole/data/dataset/sequential_dataset.py index ac749a780..efc9a2a0e 100644 --- a/recbole/data/dataset/sequential_dataset.py +++ b/recbole/data/dataset/sequential_dataset.py @@ -31,29 +31,29 @@ class SequentialDataset(Dataset): """ def __init__(self, config): - self.max_item_list_len = config['MAX_ITEM_LIST_LENGTH'] - self.item_list_length_field = config['ITEM_LIST_LENGTH_FIELD'] + self.max_item_list_len = config["MAX_ITEM_LIST_LENGTH"] + self.item_list_length_field = config["ITEM_LIST_LENGTH_FIELD"] super().__init__(config) - if config['benchmark_filename'] is not None: + if config["benchmark_filename"] is not None: self._benchmark_presets() def _change_feat_format(self): """Change feat format from :class:`pandas.DataFrame` to :class:`Interaction`, - then perform data augmentation. + then perform data augmentation. """ super()._change_feat_format() - if self.config['benchmark_filename'] is not None: + if self.config["benchmark_filename"] is not None: return - self.logger.debug('Augmentation for sequential recommendation.') + self.logger.debug("Augmentation for sequential recommendation.") self.data_augmentation() def _aug_presets(self): - list_suffix = self.config['LIST_SUFFIX'] + list_suffix = self.config["LIST_SUFFIX"] for field in self.inter_feat: if field != self.uid_field: list_field = field + list_suffix - setattr(self, f'{field}_list_field', list_field) + setattr(self, f"{field}_list_field", list_field) ftype = self.field2type[field] if ftype in [FeatureType.TOKEN, FeatureType.TOKEN_SEQ]: @@ -66,9 +66,13 @@ def _aug_presets(self): else: list_len = self.max_item_list_len - self.set_field_property(list_field, list_ftype, FeatureSource.INTERACTION, list_len) + self.set_field_property( + list_field, list_ftype, FeatureSource.INTERACTION, list_len + ) - self.set_field_property(self.item_list_length_field, FeatureType.TOKEN, FeatureSource.INTERACTION, 1) + self.set_field_property( + self.item_list_length_field, FeatureType.TOKEN, FeatureSource.INTERACTION, 1 + ) def data_augmentation(self): """Augmentation processing for sequential dataset. @@ -87,12 +91,12 @@ def data_augmentation(self): ``u1, | i4`` """ - self.logger.debug('data_augmentation') + self.logger.debug("data_augmentation") self._aug_presets() - self._check_field('uid_field', 'time_field') - max_item_list_len = self.config['MAX_ITEM_LIST_LENGTH'] + self._check_field("uid_field", "time_field") + max_item_list_len = self.config["MAX_ITEM_LIST_LENGTH"] self.sort(by=[self.uid_field, self.time_field], ascending=True) last_uid = None uid_list, item_list_index, target_index, item_list_length = [], [], [], [] @@ -122,28 +126,40 @@ def data_augmentation(self): for field in self.inter_feat: if field != self.uid_field: - list_field = getattr(self, f'{field}_list_field') + list_field = getattr(self, f"{field}_list_field") list_len = self.field2seqlen[list_field] - shape = (new_length, list_len) if isinstance(list_len, int) else (new_length,) + list_len - new_dict[list_field] = torch.zeros(shape, dtype=self.inter_feat[field].dtype) + shape = ( + (new_length, list_len) + if isinstance(list_len, int) + else (new_length,) + list_len + ) + new_dict[list_field] = torch.zeros( + shape, dtype=self.inter_feat[field].dtype + ) value = self.inter_feat[field] - for i, (index, length) in enumerate(zip(item_list_index, item_list_length)): + for i, (index, length) in enumerate( + zip(item_list_index, item_list_length) + ): new_dict[list_field][i][:length] = value[index] new_data.update(Interaction(new_dict)) self.inter_feat = new_data def _benchmark_presets(self): - list_suffix = self.config['LIST_SUFFIX'] + list_suffix = self.config["LIST_SUFFIX"] for field in self.inter_feat: if field + list_suffix in self.inter_feat: list_field = field + list_suffix - setattr(self, f'{field}_list_field', list_field) - self.set_field_property(self.item_list_length_field, FeatureType.TOKEN, FeatureSource.INTERACTION, 1) - self.inter_feat[self.item_list_length_field] = self.inter_feat[self.item_id_list_field].agg(len) - - def inter_matrix(self, form='coo', value_field=None): + setattr(self, f"{field}_list_field", list_field) + self.set_field_property( + self.item_list_length_field, FeatureType.TOKEN, FeatureSource.INTERACTION, 1 + ) + self.inter_feat[self.item_list_length_field] = self.inter_feat[ + self.item_id_list_field + ].agg(len) + + def inter_matrix(self, form="coo", value_field=None): """Get sparse matrix that describe interactions between user_id and item_id. Sparse matrix has shape (user_num, item_num). For a row of , ``matrix[src, tgt] = 1`` if ``value_field`` is ``None``, @@ -158,21 +174,31 @@ def inter_matrix(self, form='coo', value_field=None): scipy.sparse: Sparse matrix in form ``coo`` or ``csr``. """ if not self.uid_field or not self.iid_field: - raise ValueError('dataset does not exist uid/iid, thus can not converted to sparse matrix.') + raise ValueError( + "dataset does not exist uid/iid, thus can not converted to sparse matrix." + ) - l1_idx = (self.inter_feat[self.item_list_length_field] == 1) + l1_idx = self.inter_feat[self.item_list_length_field] == 1 l1_inter_dict = self.inter_feat[l1_idx].interaction new_dict = {} - list_suffix = self.config['LIST_SUFFIX'] + list_suffix = self.config["LIST_SUFFIX"] candidate_field_set = set() for field in l1_inter_dict: if field != self.uid_field and field + list_suffix in l1_inter_dict: candidate_field_set.add(field) - new_dict[field] = torch.cat([self.inter_feat[field], l1_inter_dict[field + list_suffix][:, 0]]) - elif (not field.endswith(list_suffix)) and (field != self.item_list_length_field): - new_dict[field] = torch.cat([self.inter_feat[field], l1_inter_dict[field]]) + new_dict[field] = torch.cat( + [self.inter_feat[field], l1_inter_dict[field + list_suffix][:, 0]] + ) + elif (not field.endswith(list_suffix)) and ( + field != self.item_list_length_field + ): + new_dict[field] = torch.cat( + [self.inter_feat[field], l1_inter_dict[field]] + ) local_inter_feat = Interaction(new_dict) - return self._create_sparse_matrix(local_inter_feat, self.uid_field, self.iid_field, form, value_field) + return self._create_sparse_matrix( + local_inter_feat, self.uid_field, self.iid_field, form, value_field + ) def build(self): """Processing dataset according to evaluation setting, including Group, Order and Split. @@ -185,8 +211,10 @@ def build(self): Returns: list: List of built :class:`Dataset`. """ - ordering_args = self.config['eval_args']['order'] - if ordering_args != 'TO': - raise ValueError(f'The ordering args for sequential recommendation has to be \'TO\'') + ordering_args = self.config["eval_args"]["order"] + if ordering_args != "TO": + raise ValueError( + f"The ordering args for sequential recommendation has to be 'TO'" + ) return super().build() diff --git a/recbole/data/interaction.py b/recbole/data/interaction.py index fef2d28bd..d83355af1 100644 --- a/recbole/data/interaction.py +++ b/recbole/data/interaction.py @@ -34,7 +34,7 @@ def _convert_to_tensor(data): seq_data = [torch.as_tensor(d) for d in data] new_data = rnn_utils.pad_sequence(seq_data, batch_first=True) else: - raise ValueError(f'[{type(elem)}] is not supported!') + raise ValueError(f"[{type(elem)}] is not supported!") if new_data.dtype == torch.float64: new_data = new_data.float() return new_data @@ -105,13 +105,17 @@ def __init__(self, interaction): elif isinstance(value, torch.Tensor): self.interaction[key] = value else: - raise ValueError(f'The type of {key}[{type(value)}] is not supported!') + raise ValueError( + f"The type of {key}[{type(value)}] is not supported!" + ) elif isinstance(interaction, pd.DataFrame): for key in interaction: value = interaction[key].values self.interaction[key] = _convert_to_tensor(value) else: - raise ValueError(f'[{type(interaction)}] is not supported for initialize `Interaction`!') + raise ValueError( + f"[{type(interaction)}] is not supported for initialize `Interaction`!" + ) self.length = -1 for k in self.interaction: self.length = max(self.length, self.interaction[k].unsqueeze(-1).shape[0]) @@ -120,7 +124,7 @@ def __iter__(self): return self.interaction.__iter__() def __getattr__(self, item): - if 'interaction' not in self.__dict__: + if "interaction" not in self.__dict__: raise AttributeError(f"'Interaction' object has no attribute 'interaction'") if item in self.interaction: return self.interaction[item] @@ -137,12 +141,12 @@ def __getitem__(self, index): def __setitem__(self, key, value): if not isinstance(key, str): - raise KeyError(f'{type(key)} object does not support item assigment') + raise KeyError(f"{type(key)} object does not support item assigment") self.interaction[key] = value def __delitem__(self, key): if key not in self.interaction: - raise KeyError(f'{type(key)} object does not in this interaction') + raise KeyError(f"{type(key)} object does not in this interaction") del self.interaction[key] def __contains__(self, item): @@ -152,13 +156,13 @@ def __len__(self): return self.length def __str__(self): - info = [f'The batch_size of interaction: {self.length}'] + info = [f"The batch_size of interaction: {self.length}"] for k in self.interaction: inter = self.interaction[k] temp_str = f" {k}, {inter.shape}, {inter.device.type}, {inter.dtype}" info.append(temp_str) - info.append('\n') - return '\n'.join(info) + info.append("\n") + return "\n".join(info) def __repr__(self): return self.__str__() @@ -244,7 +248,9 @@ def repeat(self, sizes): """ ret = {} for k in self.interaction: - ret[k] = self.interaction[k].repeat([sizes] + [1] * (len(self.interaction[k].shape) - 1)) + ret[k] = self.interaction[k].repeat( + [sizes] + [1] * (len(self.interaction[k].shape) - 1) + ) return Interaction(ret) def repeat_interleave(self, repeats, dim=0): @@ -278,7 +284,7 @@ def drop(self, column): column (str): the column to be dropped. """ if column not in self.interaction: - raise ValueError(f'Column [{column}] is not in [{self}].') + raise ValueError(f"Column [{column}] is not in [{self}].") del self.interaction[column] def _reindex(self, index): @@ -291,8 +297,7 @@ def _reindex(self, index): self.interaction[k] = self.interaction[k][index] def shuffle(self): - """Shuffle current interaction inplace. - """ + """Shuffle current interaction inplace.""" index = torch.randperm(self.length) self._reindex(index) @@ -306,32 +311,34 @@ def sort(self, by, ascending=True): """ if isinstance(by, str): if by not in self.interaction: - raise ValueError(f'[{by}] is not exist in interaction [{self}].') + raise ValueError(f"[{by}] is not exist in interaction [{self}].") by = [by] elif isinstance(by, (list, tuple)): for b in by: if b not in self.interaction: - raise ValueError(f'[{b}] is not exist in interaction [{self}].') + raise ValueError(f"[{b}] is not exist in interaction [{self}].") else: - raise TypeError(f'Wrong type of by [{by}].') + raise TypeError(f"Wrong type of by [{by}].") if isinstance(ascending, bool): ascending = [ascending] elif isinstance(ascending, (list, tuple)): for a in ascending: if not isinstance(a, bool): - raise TypeError(f'Wrong type of ascending [{ascending}].') + raise TypeError(f"Wrong type of ascending [{ascending}].") else: - raise TypeError(f'Wrong type of ascending [{ascending}].') + raise TypeError(f"Wrong type of ascending [{ascending}].") if len(by) != len(ascending): if len(ascending) == 1: ascending = ascending * len(by) else: - raise ValueError(f'by [{by}] and ascending [{ascending}] should have same length.') + raise ValueError( + f"by [{by}] and ascending [{ascending}] should have same length." + ) for b, a in zip(by[::-1], ascending[::-1]): - index = np.argsort(self.interaction[b], kind='stable') + index = np.argsort(self.interaction[b], kind="stable") if not a: index = index[::-1] self._reindex(index) @@ -342,7 +349,9 @@ def add_prefix(self, prefix): Args: prefix (str): The prefix to be added. """ - self.interaction = {prefix + key: value for key, value in self.interaction.items()} + self.interaction = { + prefix + key: value for key, value in self.interaction.items() + } def cat_interactions(interactions): @@ -355,14 +364,20 @@ def cat_interactions(interactions): :class:`Interaction`: Concatenated interaction. """ if not isinstance(interactions, (list, tuple)): - raise TypeError(f'Interactions [{interactions}] should be list or tuple.') + raise TypeError(f"Interactions [{interactions}] should be list or tuple.") if len(interactions) == 0: - raise ValueError(f'Interactions [{interactions}] should have some interactions.') + raise ValueError( + f"Interactions [{interactions}] should have some interactions." + ) columns_set = set(interactions[0].columns) for inter in interactions: if columns_set != set(inter.columns): - raise ValueError(f'Interactions [{interactions}] should have some interactions.') + raise ValueError( + f"Interactions [{interactions}] should have some interactions." + ) - new_inter = {col: torch.cat([inter[col] for inter in interactions]) for col in columns_set} + new_inter = { + col: torch.cat([inter[col] for inter in interactions]) for col in columns_set + } return Interaction(new_inter) diff --git a/recbole/data/utils.py b/recbole/data/utils.py index eb4eb87d3..043ecbd0a 100644 --- a/recbole/data/utils.py +++ b/recbole/data/utils.py @@ -35,38 +35,40 @@ def create_dataset(config): Returns: Dataset: Constructed dataset. """ - dataset_module = importlib.import_module('recbole.data.dataset') - if hasattr(dataset_module, config['model'] + 'Dataset'): - dataset_class = getattr(dataset_module, config['model'] + 'Dataset') + dataset_module = importlib.import_module("recbole.data.dataset") + if hasattr(dataset_module, config["model"] + "Dataset"): + dataset_class = getattr(dataset_module, config["model"] + "Dataset") else: - model_type = config['MODEL_TYPE'] + model_type = config["MODEL_TYPE"] type2class = { - ModelType.GENERAL: 'Dataset', - ModelType.SEQUENTIAL: 'SequentialDataset', - ModelType.CONTEXT: 'Dataset', - ModelType.KNOWLEDGE: 'KnowledgeBasedDataset', - ModelType.TRADITIONAL: 'Dataset', - ModelType.DECISIONTREE: 'Dataset', + ModelType.GENERAL: "Dataset", + ModelType.SEQUENTIAL: "SequentialDataset", + ModelType.CONTEXT: "Dataset", + ModelType.KNOWLEDGE: "KnowledgeBasedDataset", + ModelType.TRADITIONAL: "Dataset", + ModelType.DECISIONTREE: "Dataset", } dataset_class = getattr(dataset_module, type2class[model_type]) - default_file = os.path.join(config['checkpoint_dir'], f'{config["dataset"]}-{dataset_class.__name__}.pth') - file = config['dataset_save_path'] or default_file + default_file = os.path.join( + config["checkpoint_dir"], f'{config["dataset"]}-{dataset_class.__name__}.pth' + ) + file = config["dataset_save_path"] or default_file if os.path.exists(file): - with open(file, 'rb') as f: + with open(file, "rb") as f: dataset = pickle.load(f) dataset_args_unchanged = True - for arg in dataset_arguments + ['seed', 'repeatable']: + for arg in dataset_arguments + ["seed", "repeatable"]: if config[arg] != dataset.config[arg]: dataset_args_unchanged = False break if dataset_args_unchanged: logger = getLogger() - logger.info(set_color('Load filtered dataset from', 'pink') + f': [{file}]') + logger.info(set_color("Load filtered dataset from", "pink") + f": [{file}]") return dataset dataset = dataset_class(config) - if config['save_dataset']: + if config["save_dataset"]: dataset.save() return dataset @@ -78,13 +80,13 @@ def save_split_dataloaders(config, dataloaders): config (Config): An instance object of Config, used to record parameter information. dataloaders (tuple of AbstractDataLoader): The split dataloaders. """ - ensure_dir(config['checkpoint_dir']) - save_path = config['checkpoint_dir'] + ensure_dir(config["checkpoint_dir"]) + save_path = config["checkpoint_dir"] saved_dataloaders_file = f'{config["dataset"]}-for-{config["model"]}-dataloader.pth' file_path = os.path.join(save_path, saved_dataloaders_file) logger = getLogger() - logger.info(set_color('Saving split dataloaders into', 'pink') + f': [{file_path}]') - with open(file_path, 'wb') as f: + logger.info(set_color("Saving split dataloaders into", "pink") + f": [{file_path}]") + with open(file_path, "wb") as f: pickle.dump(dataloaders, f) @@ -99,20 +101,26 @@ def load_split_dataloaders(config): dataloaders (tuple of AbstractDataLoader or None): The split dataloaders. """ - default_file = os.path.join(config['checkpoint_dir'], f'{config["dataset"]}-for-{config["model"]}-dataloader.pth') - dataloaders_save_path = config['dataloaders_save_path'] or default_file + default_file = os.path.join( + config["checkpoint_dir"], + f'{config["dataset"]}-for-{config["model"]}-dataloader.pth', + ) + dataloaders_save_path = config["dataloaders_save_path"] or default_file if not os.path.exists(dataloaders_save_path): return None - with open(dataloaders_save_path, 'rb') as f: + with open(dataloaders_save_path, "rb") as f: train_data, valid_data, test_data = pickle.load(f) - for arg in dataset_arguments + ['seed', 'repeatable', 'eval_args']: + for arg in dataset_arguments + ["seed", "repeatable", "eval_args"]: if config[arg] != train_data.config[arg]: return None train_data.update_config(config) valid_data.update_config(config) test_data.update_config(config) logger = getLogger() - logger.info(set_color('Load split dataloaders from', 'pink') + f': [{dataloaders_save_path}]') + logger.info( + set_color("Load split dataloaders from", "pink") + + f": [{dataloaders_save_path}]" + ) return train_data, valid_data, test_data @@ -136,33 +144,55 @@ def data_preparation(config, dataset): if dataloaders is not None: train_data, valid_data, test_data = dataloaders else: - model_type = config['MODEL_TYPE'] + model_type = config["MODEL_TYPE"] built_datasets = dataset.build() train_dataset, valid_dataset, test_dataset = built_datasets - train_sampler, valid_sampler, test_sampler = create_samplers(config, dataset, built_datasets) + train_sampler, valid_sampler, test_sampler = create_samplers( + config, dataset, built_datasets + ) if model_type != ModelType.KNOWLEDGE: - train_data = get_dataloader(config, 'train')(config, train_dataset, train_sampler, shuffle=config['shuffle']) + train_data = get_dataloader(config, "train")( + config, train_dataset, train_sampler, shuffle=config["shuffle"] + ) else: - kg_sampler = KGSampler(dataset, config['train_neg_sample_args']['distribution']) - train_data = get_dataloader(config, 'train')(config, train_dataset, train_sampler, kg_sampler, shuffle=True) - - valid_data = get_dataloader(config, 'evaluation')(config, valid_dataset, valid_sampler, shuffle=False) - test_data = get_dataloader(config, 'evaluation')(config, test_dataset, test_sampler, shuffle=False) - if config['save_dataloaders']: - save_split_dataloaders(config, dataloaders=(train_data, valid_data, test_data)) + kg_sampler = KGSampler( + dataset, config["train_neg_sample_args"]["distribution"] + ) + train_data = get_dataloader(config, "train")( + config, train_dataset, train_sampler, kg_sampler, shuffle=True + ) + + valid_data = get_dataloader(config, "evaluation")( + config, valid_dataset, valid_sampler, shuffle=False + ) + test_data = get_dataloader(config, "evaluation")( + config, test_dataset, test_sampler, shuffle=False + ) + if config["save_dataloaders"]: + save_split_dataloaders( + config, dataloaders=(train_data, valid_data, test_data) + ) logger = getLogger() logger.info( - set_color('[Training]: ', 'pink') + set_color('train_batch_size', 'cyan') + ' = ' + - set_color(f'[{config["train_batch_size"]}]', 'yellow') + set_color(' train_neg_sample_args', 'cyan') + ': ' + - set_color(f'[{config["train_neg_sample_args"]}]', 'yellow') + set_color("[Training]: ", "pink") + + set_color("train_batch_size", "cyan") + + " = " + + set_color(f'[{config["train_batch_size"]}]', "yellow") + + set_color(" train_neg_sample_args", "cyan") + + ": " + + set_color(f'[{config["train_neg_sample_args"]}]', "yellow") ) logger.info( - set_color('[Evaluation]: ', 'pink') + set_color('eval_batch_size', 'cyan') + ' = ' + - set_color(f'[{config["eval_batch_size"]}]', 'yellow') + set_color(' eval_args', 'cyan') + ': ' + - set_color(f'[{config["eval_args"]}]', 'yellow') + set_color("[Evaluation]: ", "pink") + + set_color("eval_batch_size", "cyan") + + " = " + + set_color(f'[{config["eval_batch_size"]}]', "yellow") + + set_color(" eval_args", "cyan") + + ": " + + set_color(f'[{config["eval_args"]}]', "yellow") ) return train_data, valid_data, test_data @@ -180,25 +210,25 @@ def get_dataloader(config, phase): register_table = { "MultiDAE": _get_AE_dataloader, "MultiVAE": _get_AE_dataloader, - 'MacridVAE': _get_AE_dataloader, - 'CDAE': _get_AE_dataloader, - 'ENMF': _get_AE_dataloader, - 'RaCT': _get_AE_dataloader, - 'RecVAE': _get_AE_dataloader, + "MacridVAE": _get_AE_dataloader, + "CDAE": _get_AE_dataloader, + "ENMF": _get_AE_dataloader, + "RaCT": _get_AE_dataloader, + "RecVAE": _get_AE_dataloader, } - if config['model'] in register_table: - return register_table[config['model']](config, phase) + if config["model"] in register_table: + return register_table[config["model"]](config, phase) - model_type = config['MODEL_TYPE'] - if phase == 'train': + model_type = config["MODEL_TYPE"] + if phase == "train": if model_type != ModelType.KNOWLEDGE: return TrainDataLoader else: return KnowledgeBasedDataLoader else: - eval_mode = config['eval_args']['mode'] - if eval_mode == 'full': + eval_mode = config["eval_args"]["mode"] + if eval_mode == "full": return FullSortEvalDataLoader else: return NegSampleEvalDataLoader @@ -214,11 +244,11 @@ def _get_AE_dataloader(config, phase): Returns: type: The dataloader class that meets the requirements in :attr:`config` and :attr:`phase`. """ - if phase == 'train': + if phase == "train": return UserDataLoader else: - eval_mode = config['eval_args']['mode'] - if eval_mode == 'full': + eval_mode = config["eval_args"]["mode"] + if eval_mode == "full": return FullSortEvalDataLoader else: return NegSampleEvalDataLoader @@ -239,28 +269,36 @@ def create_samplers(config, dataset, built_datasets): - valid_sampler (AbstractSampler): The sampler for validation. - test_sampler (AbstractSampler): The sampler for testing. """ - phases = ['train', 'valid', 'test'] - train_neg_sample_args = config['train_neg_sample_args'] - eval_neg_sample_args = config['eval_neg_sample_args'] + phases = ["train", "valid", "test"] + train_neg_sample_args = config["train_neg_sample_args"] + eval_neg_sample_args = config["eval_neg_sample_args"] sampler = None train_sampler, valid_sampler, test_sampler = None, None, None - if train_neg_sample_args['distribution'] != 'none': - if not config['repeatable']: - sampler = Sampler(phases, built_datasets, train_neg_sample_args['distribution']) + if train_neg_sample_args["distribution"] != "none": + if not config["repeatable"]: + sampler = Sampler( + phases, built_datasets, train_neg_sample_args["distribution"] + ) else: - sampler = RepeatableSampler(phases, dataset, train_neg_sample_args['distribution']) - train_sampler = sampler.set_phase('train') + sampler = RepeatableSampler( + phases, dataset, train_neg_sample_args["distribution"] + ) + train_sampler = sampler.set_phase("train") - if eval_neg_sample_args['distribution'] != 'none': + if eval_neg_sample_args["distribution"] != "none": if sampler is None: - if not config['repeatable']: - sampler = Sampler(phases, built_datasets, eval_neg_sample_args['distribution']) + if not config["repeatable"]: + sampler = Sampler( + phases, built_datasets, eval_neg_sample_args["distribution"] + ) else: - sampler = RepeatableSampler(phases, dataset, eval_neg_sample_args['distribution']) + sampler = RepeatableSampler( + phases, dataset, eval_neg_sample_args["distribution"] + ) else: - sampler.set_distribution(eval_neg_sample_args['distribution']) - valid_sampler = sampler.set_phase('valid') - test_sampler = sampler.set_phase('test') + sampler.set_distribution(eval_neg_sample_args["distribution"]) + valid_sampler = sampler.set_phase("valid") + test_sampler = sampler.set_phase("test") return train_sampler, valid_sampler, test_sampler diff --git a/recbole/evaluator/base_metric.py b/recbole/evaluator/base_metric.py index b075beb2d..06918eee2 100644 --- a/recbole/evaluator/base_metric.py +++ b/recbole/evaluator/base_metric.py @@ -23,10 +23,11 @@ class AbstractMetric(object): Args: config (Config): the config of evaluator. """ + smaller = False def __init__(self, config): - self.decimal_place = config['metric_decimal_place'] + self.decimal_place = config["metric_decimal_place"] def calculate_metric(self, dataobject): """Get the dictionary of a metric. @@ -37,7 +38,7 @@ def calculate_metric(self, dataobject): Returns: dict: such as ``{'metric@10': 3153, 'metric@20': 0.3824}`` """ - raise NotImplementedError('Method [calculate_metric] should be implemented.') + raise NotImplementedError("Method [calculate_metric] should be implemented.") class TopkMetric(AbstractMetric): @@ -47,18 +48,19 @@ class TopkMetric(AbstractMetric): Args: config (Config): The config of evaluator. """ + metric_type = EvaluatorType.RANKING - metric_need = ['rec.topk'] + metric_need = ["rec.topk"] def __init__(self, config): super().__init__(config) - self.topk = config['topk'] + self.topk = config["topk"] def used_info(self, dataobject): """Get the bool matrix indicating whether the corresponding item is positive and number of positive items for each user. """ - rec_mat = dataobject.get('rec.topk') + rec_mat = dataobject.get("rec.topk") topk_idx, pos_len_list = torch.split(rec_mat, [max(self.topk), 1], dim=1) return topk_idx.to(torch.bool).numpy(), pos_len_list.squeeze(-1).numpy() @@ -75,7 +77,7 @@ def topk_result(self, metric, value): metric_dict = {} avg_result = value.mean(axis=0) for k in self.topk: - key = '{}@{}'.format(metric, k) + key = "{}@{}".format(metric, k) metric_dict[key] = round(avg_result[k - 1], self.decimal_place) return metric_dict @@ -90,7 +92,9 @@ def metric_info(self, pos_index, pos_len=None): Returns: numpy.ndarray: metrics for each user, including values from `metric@1` to `metric@max(self.topk)`. """ - raise NotImplementedError('Method [metric_info] of top-k metric should be implemented.') + raise NotImplementedError( + "Method [metric_info] of top-k metric should be implemented." + ) class LossMetric(AbstractMetric): @@ -100,16 +104,17 @@ class LossMetric(AbstractMetric): Args: config (Config): The config of evaluator. """ + metric_type = EvaluatorType.VALUE - metric_need = ['rec.score', 'data.label'] + metric_need = ["rec.score", "data.label"] def __init__(self, config): super().__init__(config) def used_info(self, dataobject): """Get scores that model predicted and the ground truth.""" - preds = dataobject.get('rec.score') - trues = dataobject.get('data.label') + preds = dataobject.get("rec.score") + trues = dataobject.get("data.label") return preds.squeeze(-1).numpy(), trues.squeeze(-1).numpy() @@ -128,4 +133,6 @@ def metric_info(self, preds, trues): Returns: float: The value of the metric. """ - raise NotImplementedError('Method [metric_info] of loss-based metric should be implemented.') + raise NotImplementedError( + "Method [metric_info] of loss-based metric should be implemented." + ) diff --git a/recbole/evaluator/collector.py b/recbole/evaluator/collector.py index 9f856a12d..7ad7253cd 100644 --- a/recbole/evaluator/collector.py +++ b/recbole/evaluator/collector.py @@ -18,7 +18,6 @@ class DataStruct(object): - def __init__(self): self._data_dict = {} @@ -48,22 +47,24 @@ def update_tensor(self, name: str, value: torch.Tensor): else: if not isinstance(self._data_dict[name], torch.Tensor): raise ValueError("{} is not a tensor.".format(name)) - self._data_dict[name] = torch.cat((self._data_dict[name], value.cpu().clone().detach()), dim=0) + self._data_dict[name] = torch.cat( + (self._data_dict[name], value.cpu().clone().detach()), dim=0 + ) def __str__(self): - data_info = '\nContaining:\n' + data_info = "\nContaining:\n" for data_key in self._data_dict.keys(): - data_info += data_key + '\n' + data_info += data_key + "\n" return data_info class Collector(object): """The collector is used to collect the resource for evaluator. - As the evaluation metrics are various, the needed resource not only contain the recommended result - but also other resource from data and model. They all can be collected by the collector during the training - and evaluation process. + As the evaluation metrics are various, the needed resource not only contain the recommended result + but also other resource from data and model. They all can be collected by the collector during the training + and evaluation process. - This class is only used in Trainer. + This class is only used in Trainer. """ @@ -71,26 +72,26 @@ def __init__(self, config): self.config = config self.data_struct = DataStruct() self.register = Register(config) - self.full = ('full' in config['eval_args']['mode']) - self.topk = self.config['topk'] - self.device = self.config['device'] + self.full = "full" in config["eval_args"]["mode"] + self.topk = self.config["topk"] + self.device = self.config["device"] def data_collect(self, train_data): - """ Collect the evaluation resource from training data. - Args: - train_data (AbstractDataLoader): the training dataloader which contains the training data. + """Collect the evaluation resource from training data. + Args: + train_data (AbstractDataLoader): the training dataloader which contains the training data. """ - if self.register.need('data.num_items'): - item_id = self.config['ITEM_ID_FIELD'] - self.data_struct.set('data.num_items', train_data.dataset.num(item_id)) - if self.register.need('data.num_users'): - user_id = self.config['USER_ID_FIELD'] - self.data_struct.set('data.num_users', train_data.dataset.num(user_id)) - if self.register.need('data.count_items'): - self.data_struct.set('data.count_items', train_data.dataset.item_counter) - if self.register.need('data.count_users'): - self.data_struct.set('data.count_items', train_data.dataset.user_counter) + if self.register.need("data.num_items"): + item_id = self.config["ITEM_ID_FIELD"] + self.data_struct.set("data.num_items", train_data.dataset.num(item_id)) + if self.register.need("data.num_users"): + user_id = self.config["USER_ID_FIELD"] + self.data_struct.set("data.num_users", train_data.dataset.num(user_id)) + if self.register.need("data.count_items"): + self.data_struct.set("data.count_items", train_data.dataset.item_counter) + if self.register.need("data.count_users"): + self.data_struct.set("data.count_items", train_data.dataset.user_counter) def _average_rank(self, scores): """Get the ranking of an ordered tensor, and take the average of the ranking for positions with equal values. @@ -111,48 +112,63 @@ def _average_rank(self, scores): """ length, width = scores.shape - true_tensor = torch.full((length, 1), True, dtype=torch.bool, device=self.device) + true_tensor = torch.full( + (length, 1), True, dtype=torch.bool, device=self.device + ) obs = torch.cat([true_tensor, scores[:, 1:] != scores[:, :-1]], dim=1) # bias added to dense - bias = torch.arange(0, length, device=self.device).repeat(width).reshape(width, -1). \ - transpose(1, 0).reshape(-1) + bias = ( + torch.arange(0, length, device=self.device) + .repeat(width) + .reshape(width, -1) + .transpose(1, 0) + .reshape(-1) + ) dense = obs.view(-1).cumsum(0) + bias # cumulative counts of each unique value count = torch.where(torch.cat([obs, true_tensor], dim=1))[1] # get average rank - avg_rank = .5 * (count[dense] + count[dense - 1] + 1).view(length, -1) + avg_rank = 0.5 * (count[dense] + count[dense - 1] + 1).view(length, -1) return avg_rank def eval_batch_collect( - self, scores_tensor: torch.Tensor, interaction, positive_u: torch.Tensor, positive_i: torch.Tensor + self, + scores_tensor: torch.Tensor, + interaction, + positive_u: torch.Tensor, + positive_i: torch.Tensor, ): - """ Collect the evaluation resource from batched eval data and batched model output. - Args: - scores_tensor (Torch.Tensor): the output tensor of model with the shape of `(N, )` - interaction(Interaction): batched eval data. - positive_u(Torch.Tensor): the row index of positive items for each user. - positive_i(Torch.Tensor): the positive item id for each user. + """Collect the evaluation resource from batched eval data and batched model output. + Args: + scores_tensor (Torch.Tensor): the output tensor of model with the shape of `(N, )` + interaction(Interaction): batched eval data. + positive_u(Torch.Tensor): the row index of positive items for each user. + positive_i(Torch.Tensor): the positive item id for each user. """ - if self.register.need('rec.items'): + if self.register.need("rec.items"): # get topk - _, topk_idx = torch.topk(scores_tensor, max(self.topk), dim=-1) # n_users x k - self.data_struct.update_tensor('rec.items', topk_idx) + _, topk_idx = torch.topk( + scores_tensor, max(self.topk), dim=-1 + ) # n_users x k + self.data_struct.update_tensor("rec.items", topk_idx) - if self.register.need('rec.topk'): + if self.register.need("rec.topk"): - _, topk_idx = torch.topk(scores_tensor, max(self.topk), dim=-1) # n_users x k + _, topk_idx = torch.topk( + scores_tensor, max(self.topk), dim=-1 + ) # n_users x k pos_matrix = torch.zeros_like(scores_tensor, dtype=torch.int) pos_matrix[positive_u, positive_i] = 1 pos_len_list = pos_matrix.sum(dim=1, keepdim=True) pos_idx = torch.gather(pos_matrix, dim=1, index=topk_idx) result = torch.cat((pos_idx, pos_len_list), dim=1) - self.data_struct.update_tensor('rec.topk', result) + self.data_struct.update_tensor("rec.topk", result) - if self.register.need('rec.meanrank'): + if self.register.need("rec.meanrank"): desc_scores, desc_index = torch.sort(scores_tensor, dim=-1, descending=True) @@ -162,49 +178,53 @@ def eval_batch_collect( pos_index = torch.gather(pos_matrix, dim=1, index=desc_index) avg_rank = self._average_rank(desc_scores) - pos_rank_sum = torch.where(pos_index == 1, avg_rank, torch.zeros_like(avg_rank)).sum(dim=-1, keepdim=True) + pos_rank_sum = torch.where( + pos_index == 1, avg_rank, torch.zeros_like(avg_rank) + ).sum(dim=-1, keepdim=True) pos_len_list = pos_matrix.sum(dim=1, keepdim=True) user_len_list = desc_scores.argmin(dim=1, keepdim=True) result = torch.cat((pos_rank_sum, user_len_list, pos_len_list), dim=1) - self.data_struct.update_tensor('rec.meanrank', result) + self.data_struct.update_tensor("rec.meanrank", result) - if self.register.need('rec.score'): + if self.register.need("rec.score"): - self.data_struct.update_tensor('rec.score', scores_tensor) + self.data_struct.update_tensor("rec.score", scores_tensor) - if self.register.need('data.label'): - self.label_field = self.config['LABEL_FIELD'] - self.data_struct.update_tensor('data.label', interaction[self.label_field].to(self.device)) + if self.register.need("data.label"): + self.label_field = self.config["LABEL_FIELD"] + self.data_struct.update_tensor( + "data.label", interaction[self.label_field].to(self.device) + ) def model_collect(self, model: torch.nn.Module): - """ Collect the evaluation resource from model. - Args: - model (nn.Module): the trained recommendation model. + """Collect the evaluation resource from model. + Args: + model (nn.Module): the trained recommendation model. """ pass # TODO: def eval_collect(self, eval_pred: torch.Tensor, data_label: torch.Tensor): - """ Collect the evaluation resource from total output and label. - It was designed for those models that can not predict with batch. - Args: - eval_pred (torch.Tensor): the output score tensor of model. - data_label (torch.Tensor): the label tensor. + """Collect the evaluation resource from total output and label. + It was designed for those models that can not predict with batch. + Args: + eval_pred (torch.Tensor): the output score tensor of model. + data_label (torch.Tensor): the label tensor. """ - if self.register.need('rec.score'): - self.data_struct.update_tensor('rec.score', eval_pred) + if self.register.need("rec.score"): + self.data_struct.update_tensor("rec.score", eval_pred) - if self.register.need('data.label'): - self.label_field = self.config['LABEL_FIELD'] - self.data_struct.update_tensor('data.label', data_label.to(self.device)) + if self.register.need("data.label"): + self.label_field = self.config["LABEL_FIELD"] + self.data_struct.update_tensor("data.label", data_label.to(self.device)) def get_data_struct(self): - """ Get all the evaluation resource that been collected. - And reset some of outdated resource. + """Get all the evaluation resource that been collected. + And reset some of outdated resource. """ returned_struct = copy.deepcopy(self.data_struct) - for key in ['rec.topk', 'rec.meanrank', 'rec.score', 'rec.items', 'data.label']: + for key in ["rec.topk", "rec.meanrank", "rec.score", "rec.items", "data.label"]: if key in self.data_struct: del self.data_struct[key] return returned_struct diff --git a/recbole/evaluator/evaluator.py b/recbole/evaluator/evaluator.py index 215c8eae6..96aa40791 100644 --- a/recbole/evaluator/evaluator.py +++ b/recbole/evaluator/evaluator.py @@ -14,12 +14,11 @@ class Evaluator(object): - """Evaluator is used to check parameter correctness, and summarize the results of all metrics. - """ + """Evaluator is used to check parameter correctness, and summarize the results of all metrics.""" def __init__(self, config): self.config = config - self.metrics = [metric.lower() for metric in self.config['metrics']] + self.metrics = [metric.lower() for metric in self.config["metrics"]] self.metric_class = {} for metric in self.metrics: diff --git a/recbole/evaluator/metrics.py b/recbole/evaluator/metrics.py index 47dd16ca5..ceff7b37e 100644 --- a/recbole/evaluator/metrics.py +++ b/recbole/evaluator/metrics.py @@ -56,7 +56,7 @@ def __init__(self, config): def calculate_metric(self, dataobject): pos_index, _ = self.used_info(dataobject) result = self.metric_info(pos_index) - metric_dict = self.topk_result('hit', result) + metric_dict = self.topk_result("hit", result) return metric_dict def metric_info(self, pos_index): @@ -82,7 +82,7 @@ def __init__(self, config): def calculate_metric(self, dataobject): pos_index, _ = self.used_info(dataobject) result = self.metric_info(pos_index) - metric_dict = self.topk_result('mrr', result) + metric_dict = self.topk_result("mrr", result) return metric_dict def metric_info(self, pos_index): @@ -120,7 +120,7 @@ def __init__(self, config): def calculate_metric(self, dataobject): pos_index, pos_len = self.used_info(dataobject) result = self.metric_info(pos_index, pos_len) - metric_dict = self.topk_result('map', result) + metric_dict = self.topk_result("map", result) return metric_dict def metric_info(self, pos_index, pos_len): @@ -153,7 +153,7 @@ def __init__(self, config): def calculate_metric(self, dataobject): pos_index, pos_len = self.used_info(dataobject) result = self.metric_info(pos_index, pos_len) - metric_dict = self.topk_result('recall', result) + metric_dict = self.topk_result("recall", result) return metric_dict def metric_info(self, pos_index, pos_len): @@ -180,7 +180,7 @@ def __init__(self, config): def calculate_metric(self, dataobject): pos_index, pos_len = self.used_info(dataobject) result = self.metric_info(pos_index, pos_len) - metric_dict = self.topk_result('ndcg', result) + metric_dict = self.topk_result("ndcg", result) return metric_dict def metric_info(self, pos_index, pos_len): @@ -220,7 +220,7 @@ def __init__(self, config): def calculate_metric(self, dataobject): pos_index, _ = self.used_info(dataobject) result = self.metric_info(pos_index) - metric_dict = self.topk_result('precision', result) + metric_dict = self.topk_result("precision", result) return metric_dict def metric_info(self, pos_index): @@ -254,17 +254,19 @@ class GAUC(AbstractMetric): :math:`rank_i` is the descending rank of the i-th items in :math:`R(u)`. """ metric_type = EvaluatorType.RANKING - metric_need = ['rec.meanrank'] + metric_need = ["rec.meanrank"] def __init__(self, config): super().__init__(config) def calculate_metric(self, dataobject): - mean_rank = dataobject.get('rec.meanrank').numpy() + mean_rank = dataobject.get("rec.meanrank").numpy() pos_rank_sum, user_len_list, pos_len_list = np.split(mean_rank, 3, axis=1) - user_len_list, pos_len_list = user_len_list.squeeze(-1), pos_len_list.squeeze(-1) + user_len_list, pos_len_list = user_len_list.squeeze(-1), pos_len_list.squeeze( + -1 + ) result = self.metric_info(pos_rank_sum, user_len_list, pos_len_list) - return {'gauc': round(result, self.decimal_place)} + return {"gauc": round(result, self.decimal_place)} def metric_info(self, pos_rank_sum, user_len_list, pos_len_list): """Get the value of GAUC metric. @@ -289,7 +291,7 @@ def metric_info(self, pos_rank_sum, user_len_list, pos_len_list): "true positive value should be meaningless, " "these users have been removed from GAUC calculation" ) - non_zero_idx *= (pos_len_list != 0) + non_zero_idx *= pos_len_list != 0 if any_without_neg: logger = getLogger() logger.warning( @@ -297,12 +299,18 @@ def metric_info(self, pos_rank_sum, user_len_list, pos_len_list): "false positive value should be meaningless, " "these users have been removed from GAUC calculation" ) - non_zero_idx *= (neg_len_list != 0) + non_zero_idx *= neg_len_list != 0 if any_without_pos or any_without_neg: item_list = user_len_list, neg_len_list, pos_len_list, pos_rank_sum - user_len_list, neg_len_list, pos_len_list, pos_rank_sum = map(lambda x: x[non_zero_idx], item_list) + user_len_list, neg_len_list, pos_len_list, pos_rank_sum = map( + lambda x: x[non_zero_idx], item_list + ) - pair_num = (user_len_list + 1) * pos_len_list - pos_len_list * (pos_len_list + 1) / 2 - np.squeeze(pos_rank_sum) + pair_num = ( + (user_len_list + 1) * pos_len_list + - pos_len_list * (pos_len_list + 1) / 2 + - np.squeeze(pos_rank_sum) + ) user_auc = pair_num / (neg_len_list * pos_len_list) result = (user_auc * pos_len_list).sum() / pos_len_list.sum() return result @@ -333,12 +341,14 @@ def __init__(self, config): super().__init__(config) def calculate_metric(self, dataobject): - return self.output_metric('auc', dataobject) + return self.output_metric("auc", dataobject) def metric_info(self, preds, trues): fps, tps = _binary_clf_curve(trues, preds) if len(fps) > 2: - optimal_idxs = np.where(np.r_[True, np.logical_or(np.diff(fps, 2), np.diff(tps, 2)), True])[0] + optimal_idxs = np.where( + np.r_[True, np.logical_or(np.diff(fps, 2), np.diff(tps, 2)), True] + )[0] fps = fps[optimal_idxs] tps = tps[optimal_idxs] @@ -347,14 +357,20 @@ def metric_info(self, preds, trues): if fps[-1] <= 0: logger = getLogger() - logger.warning("No negative samples in y_true, " "false positive value should be meaningless") + logger.warning( + "No negative samples in y_true, " + "false positive value should be meaningless" + ) fpr = np.repeat(np.nan, fps.shape) else: fpr = fps / fps[-1] if tps[-1] <= 0: logger = getLogger() - logger.warning("No positive samples in y_true, " "true positive value should be meaningless") + logger.warning( + "No positive samples in y_true, " + "true positive value should be meaningless" + ) tpr = np.repeat(np.nan, tps.shape) else: tpr = tps / tps[-1] @@ -383,7 +399,7 @@ def __init__(self, config): super().__init__(config) def calculate_metric(self, dataobject): - return self.output_metric('mae', dataobject) + return self.output_metric("mae", dataobject) def metric_info(self, preds, trues): return mean_absolute_error(trues, preds) @@ -403,7 +419,7 @@ def __init__(self, config): super().__init__(config) def calculate_metric(self, dataobject): - return self.output_metric('rmse', dataobject) + return self.output_metric("rmse", dataobject) def metric_info(self, preds, trues): return np.sqrt(mean_squared_error(trues, preds)) @@ -424,7 +440,7 @@ def __init__(self, config): super().__init__(config) def calculate_metric(self, dataobject): - return self.output_metric('logloss', dataobject) + return self.output_metric("logloss", dataobject) def metric_info(self, preds, trues): eps = 1e-15 @@ -446,24 +462,26 @@ class ItemCoverage(AbstractMetric): \mathrm{Coverage@K}=\frac{\left| \bigcup_{u \in U} \hat{R}(u) \right|}{|I|} """ metric_type = EvaluatorType.RANKING - metric_need = ['rec.items', 'data.num_items'] + metric_need = ["rec.items", "data.num_items"] def __init__(self, config): super().__init__(config) - self.topk = config['topk'] + self.topk = config["topk"] def used_info(self, dataobject): """Get the matrix of recommendation items and number of items in total item set""" - item_matrix = dataobject.get('rec.items') - num_items = dataobject.get('data.num_items') + item_matrix = dataobject.get("rec.items") + num_items = dataobject.get("data.num_items") return item_matrix.numpy(), num_items def calculate_metric(self, dataobject): item_matrix, num_items = self.used_info(dataobject) metric_dict = {} for k in self.topk: - key = '{}@{}'.format('itemcoverage', k) - metric_dict[key] = round(self.get_coverage(item_matrix[:, :k], num_items), self.decimal_place) + key = "{}@{}".format("itemcoverage", k) + metric_dict[key] = round( + self.get_coverage(item_matrix[:, :k], num_items), self.decimal_place + ) return metric_dict def get_coverage(self, item_matrix, num_items): @@ -493,22 +511,22 @@ class AveragePopularity(AbstractMetric): """ metric_type = EvaluatorType.RANKING smaller = True - metric_need = ['rec.items', 'data.count_items'] + metric_need = ["rec.items", "data.count_items"] def __init__(self, config): super().__init__(config) - self.topk = config['topk'] + self.topk = config["topk"] def used_info(self, dataobject): """Get the matrix of recommendation items and the popularity of items in training data""" - item_counter = dataobject.get('data.count_items') - item_matrix = dataobject.get('rec.items') + item_counter = dataobject.get("data.count_items") + item_matrix = dataobject.get("rec.items") return item_matrix.numpy(), dict(item_counter) def calculate_metric(self, dataobject): item_matrix, item_count = self.used_info(dataobject) result = self.metric_info(self.get_pop(item_matrix, item_count)) - metric_dict = self.topk_result('averagepopularity', result) + metric_dict = self.topk_result("averagepopularity", result) return metric_dict def get_pop(self, item_matrix, item_count): @@ -544,7 +562,7 @@ def topk_result(self, metric, value): metric_dict = {} avg_result = value.mean(axis=0) for k in self.topk: - key = '{}@{}'.format(metric, k) + key = "{}@{}".format(metric, k) metric_dict[key] = round(avg_result[k - 1], self.decimal_place) return metric_dict @@ -565,24 +583,25 @@ class ShannonEntropy(AbstractMetric): which is the number of item i in recommended list over all items. """ metric_type = EvaluatorType.RANKING - metric_need = ['rec.items'] + metric_need = ["rec.items"] def __init__(self, config): super().__init__(config) - self.topk = config['topk'] + self.topk = config["topk"] def used_info(self, dataobject): - """Get the matrix of recommendation items. - """ - item_matrix = dataobject.get('rec.items') + """Get the matrix of recommendation items.""" + item_matrix = dataobject.get("rec.items") return item_matrix.numpy() def calculate_metric(self, dataobject): item_matrix = self.used_info(dataobject) metric_dict = {} for k in self.topk: - key = '{}@{}'.format('shannonentropy', k) - metric_dict[key] = round(self.get_entropy(item_matrix[:, :k]), self.decimal_place) + key = "{}@{}".format("shannonentropy", k) + metric_dict[key] = round( + self.get_entropy(item_matrix[:, :k]), self.decimal_place + ) return metric_dict def get_entropy(self, item_matrix): @@ -620,24 +639,26 @@ class GiniIndex(AbstractMetric): """ metric_type = EvaluatorType.RANKING smaller = True - metric_need = ['rec.items', 'data.num_items'] + metric_need = ["rec.items", "data.num_items"] def __init__(self, config): super().__init__(config) - self.topk = config['topk'] + self.topk = config["topk"] def used_info(self, dataobject): """Get the matrix of recommendation items and number of items in total item set""" - item_matrix = dataobject.get('rec.items') - num_items = dataobject.get('data.num_items') + item_matrix = dataobject.get("rec.items") + num_items = dataobject.get("data.num_items") return item_matrix.numpy(), num_items def calculate_metric(self, dataobject): item_matrix, num_items = self.used_info(dataobject) metric_dict = {} for k in self.topk: - key = '{}@{}'.format('giniindex', k) - metric_dict[key] = round(self.get_gini(item_matrix[:, :k], num_items), self.decimal_place) + key = "{}@{}".format("giniindex", k) + metric_dict[key] = round( + self.get_gini(item_matrix[:, :k], num_items), self.decimal_place + ) return metric_dict def get_gini(self, item_matrix, num_items): @@ -679,19 +700,19 @@ class TailPercentage(AbstractMetric): which can be an integer or a float in (0,1]. Otherwise it will default to 0.1. """ metric_type = EvaluatorType.RANKING - metric_need = ['rec.items', 'data.count_items'] + metric_need = ["rec.items", "data.count_items"] def __init__(self, config): super().__init__(config) - self.topk = config['topk'] - self.tail = config['tail_ratio'] + self.topk = config["topk"] + self.tail = config["tail_ratio"] if self.tail is None or self.tail <= 0: self.tail = 0.1 def used_info(self, dataobject): """Get the matrix of recommendation items and number of items in total item set.""" - item_matrix = dataobject.get('rec.items') - count_items = dataobject.get('data.count_items') + item_matrix = dataobject.get("rec.items") + count_items = dataobject.get("data.count_items") return item_matrix.numpy(), dict(count_items) def get_tail(self, item_matrix, count_items): @@ -721,7 +742,7 @@ def get_tail(self, item_matrix, count_items): def calculate_metric(self, dataobject): item_matrix, count_items = self.used_info(dataobject) result = self.metric_info(self.get_tail(item_matrix, count_items)) - metric_dict = self.topk_result('tailpercentage', result) + metric_dict = self.topk_result("tailpercentage", result) return metric_dict def metric_info(self, values): @@ -740,6 +761,6 @@ def topk_result(self, metric, value): metric_dict = {} avg_result = value.mean(axis=0) for k in self.topk: - key = '{}@{}'.format(metric, k) + key = "{}@{}".format(metric, k) metric_dict[key] = round(avg_result[k - 1], self.decimal_place) return metric_dict diff --git a/recbole/evaluator/register.py b/recbole/evaluator/register.py index fbf041878..3de9e00b0 100644 --- a/recbole/evaluator/register.py +++ b/recbole/evaluator/register.py @@ -40,16 +40,17 @@ def cluster_info(module_name): smaller_m = [] m_dict, m_info, m_types = {}, {}, {} metric_class = inspect.getmembers( - sys.modules[module_name], lambda x: inspect.isclass(x) and x.__module__ == module_name + sys.modules[module_name], + lambda x: inspect.isclass(x) and x.__module__ == module_name, ) for name, metric_cls in metric_class: name = name.lower() m_dict[name] = metric_cls - if hasattr(metric_cls, 'metric_need'): + if hasattr(metric_cls, "metric_need"): m_info[name] = metric_cls.metric_need else: raise AttributeError(f"Metric '{name}' has no attribute [metric_need].") - if hasattr(metric_cls, 'metric_type'): + if hasattr(metric_cls, "metric_type"): m_types[name] = metric_cls.metric_type else: raise AttributeError(f"Metric '{name}' has no attribute [metric_type].") @@ -58,20 +59,22 @@ def cluster_info(module_name): return smaller_m, m_info, m_types, m_dict -metric_module_name = 'recbole.evaluator.metrics' -smaller_metrics, metric_information, metric_types, metrics_dict = cluster_info(metric_module_name) +metric_module_name = "recbole.evaluator.metrics" +smaller_metrics, metric_information, metric_types, metrics_dict = cluster_info( + metric_module_name +) class Register(object): - """ Register module load the registry according to the metrics in config. - It is a member of DataCollector. - The DataCollector collect the resource that need for Evaluator under the guidance of Register + """Register module load the registry according to the metrics in config. + It is a member of DataCollector. + The DataCollector collect the resource that need for Evaluator under the guidance of Register """ def __init__(self, config): self.config = config - self.metrics = [metric.lower() for metric in self.config['metrics']] + self.metrics = [metric.lower() for metric in self.config["metrics"]] self._build_register() def _build_register(self): diff --git a/recbole/evaluator/utils.py b/recbole/evaluator/utils.py index 392873493..8d8e1da14 100644 --- a/recbole/evaluator/utils.py +++ b/recbole/evaluator/utils.py @@ -66,7 +66,9 @@ def trunc(scores, method): try: cut_method = getattr(np, method) except NotImplementedError: - raise NotImplementedError("module 'numpy' has no function named '{}'".format(method)) + raise NotImplementedError( + "module 'numpy' has no function named '{}'".format(method) + ) scores = cut_method(scores) return scores @@ -102,7 +104,7 @@ def _binary_clf_curve(trues, preds): in SkLearn and made some optimizations. """ - trues = (trues == 1) + trues = trues == 1 desc_idxs = np.argsort(preds)[::-1] preds = preds[desc_idxs] diff --git a/recbole/model/abstract_recommender.py b/recbole/model/abstract_recommender.py index bf61aad69..8e8194765 100644 --- a/recbole/model/abstract_recommender.py +++ b/recbole/model/abstract_recommender.py @@ -23,8 +23,7 @@ class AbstractRecommender(nn.Module): - r"""Base class for all models - """ + r"""Base class for all models""" def __init__(self): self.logger = getLogger() @@ -66,7 +65,7 @@ def full_sort_predict(self, interaction): raise NotImplementedError def other_parameter(self): - if hasattr(self, 'other_parameter_name'): + if hasattr(self, "other_parameter_name"): return {key: getattr(self, key) for key in self.other_parameter_name} return dict() @@ -82,50 +81,56 @@ def __str__(self): """ model_parameters = filter(lambda p: p.requires_grad, self.parameters()) params = sum([np.prod(p.size()) for p in model_parameters]) - return super().__str__() + set_color('\nTrainable parameters', 'blue') + f': {params}' + return ( + super().__str__() + + set_color("\nTrainable parameters", "blue") + + f": {params}" + ) class GeneralRecommender(AbstractRecommender): """This is a abstract general recommender. All the general model should implement this class. The base general recommender class provide the basic dataset and parameters information. """ + type = ModelType.GENERAL def __init__(self, config, dataset): super(GeneralRecommender, self).__init__() # load dataset info - self.USER_ID = config['USER_ID_FIELD'] - self.ITEM_ID = config['ITEM_ID_FIELD'] - self.NEG_ITEM_ID = config['NEG_PREFIX'] + self.ITEM_ID + self.USER_ID = config["USER_ID_FIELD"] + self.ITEM_ID = config["ITEM_ID_FIELD"] + self.NEG_ITEM_ID = config["NEG_PREFIX"] + self.ITEM_ID self.n_users = dataset.num(self.USER_ID) self.n_items = dataset.num(self.ITEM_ID) # load parameters info - self.device = config['device'] + self.device = config["device"] class SequentialRecommender(AbstractRecommender): """ This is a abstract sequential recommender. All the sequential model should implement This class. """ + type = ModelType.SEQUENTIAL def __init__(self, config, dataset): super(SequentialRecommender, self).__init__() # load dataset info - self.USER_ID = config['USER_ID_FIELD'] - self.ITEM_ID = config['ITEM_ID_FIELD'] - self.ITEM_SEQ = self.ITEM_ID + config['LIST_SUFFIX'] - self.ITEM_SEQ_LEN = config['ITEM_LIST_LENGTH_FIELD'] + self.USER_ID = config["USER_ID_FIELD"] + self.ITEM_ID = config["ITEM_ID_FIELD"] + self.ITEM_SEQ = self.ITEM_ID + config["LIST_SUFFIX"] + self.ITEM_SEQ_LEN = config["ITEM_LIST_LENGTH_FIELD"] self.POS_ITEM_ID = self.ITEM_ID - self.NEG_ITEM_ID = config['NEG_PREFIX'] + self.ITEM_ID - self.max_seq_length = config['MAX_ITEM_LIST_LENGTH'] + self.NEG_ITEM_ID = config["NEG_PREFIX"] + self.ITEM_ID + self.max_seq_length = config["MAX_ITEM_LIST_LENGTH"] self.n_items = dataset.num(self.ITEM_ID) # load parameters info - self.device = config['device'] + self.device = config["device"] def gather_indexes(self, output, gather_index): """Gathers the vectors at the specific positions over a minibatch""" @@ -135,11 +140,13 @@ def gather_indexes(self, output, gather_index): def get_attention_mask(self, item_seq, bidirectional=False): """Generate left-to-right uni-directional or bidirectional attention mask for multi-head attention.""" - attention_mask = (item_seq != 0) + attention_mask = item_seq != 0 extended_attention_mask = attention_mask.unsqueeze(1).unsqueeze(2) # torch.bool if not bidirectional: - extended_attention_mask = torch.tril(extended_attention_mask.expand((-1, -1, item_seq.size(-1), -1))) - extended_attention_mask = torch.where(extended_attention_mask, 0., -10000.) + extended_attention_mask = torch.tril( + extended_attention_mask.expand((-1, -1, item_seq.size(-1), -1)) + ) + extended_attention_mask = torch.where(extended_attention_mask, 0.0, -10000.0) return extended_attention_mask @@ -147,27 +154,28 @@ class KnowledgeRecommender(AbstractRecommender): """This is a abstract knowledge-based recommender. All the knowledge-based model should implement this class. The base knowledge-based recommender class provide the basic dataset and parameters information. """ + type = ModelType.KNOWLEDGE def __init__(self, config, dataset): super(KnowledgeRecommender, self).__init__() # load dataset info - self.USER_ID = config['USER_ID_FIELD'] - self.ITEM_ID = config['ITEM_ID_FIELD'] - self.NEG_ITEM_ID = config['NEG_PREFIX'] + self.ITEM_ID - self.ENTITY_ID = config['ENTITY_ID_FIELD'] - self.RELATION_ID = config['RELATION_ID_FIELD'] - self.HEAD_ENTITY_ID = config['HEAD_ENTITY_ID_FIELD'] - self.TAIL_ENTITY_ID = config['TAIL_ENTITY_ID_FIELD'] - self.NEG_TAIL_ENTITY_ID = config['NEG_PREFIX'] + self.TAIL_ENTITY_ID + self.USER_ID = config["USER_ID_FIELD"] + self.ITEM_ID = config["ITEM_ID_FIELD"] + self.NEG_ITEM_ID = config["NEG_PREFIX"] + self.ITEM_ID + self.ENTITY_ID = config["ENTITY_ID_FIELD"] + self.RELATION_ID = config["RELATION_ID_FIELD"] + self.HEAD_ENTITY_ID = config["HEAD_ENTITY_ID_FIELD"] + self.TAIL_ENTITY_ID = config["TAIL_ENTITY_ID_FIELD"] + self.NEG_TAIL_ENTITY_ID = config["NEG_PREFIX"] + self.TAIL_ENTITY_ID self.n_users = dataset.num(self.USER_ID) self.n_items = dataset.num(self.ITEM_ID) self.n_entities = dataset.num(self.ENTITY_ID) self.n_relations = dataset.num(self.RELATION_ID) # load parameters info - self.device = config['device'] + self.device = config["device"] class ContextRecommender(AbstractRecommender): @@ -175,6 +183,7 @@ class ContextRecommender(AbstractRecommender): The base context-aware recommender class provide the basic embedding function of feature fields which also contains a first-order part of feature fields. """ + type = ModelType.CONTEXT input_type = InputType.POINTWISE @@ -190,10 +199,10 @@ def __init__(self, config, dataset): FeatureSource.ITEM_ID, ] ) - self.LABEL = config['LABEL_FIELD'] - self.embedding_size = config['embedding_size'] - self.device = config['device'] - self.double_tower = config['double_tower'] + self.LABEL = config["LABEL_FIELD"] + self.embedding_size = config["embedding_size"] + self.device = config["device"] + self.double_tower = config["double_tower"] if self.double_tower is None: self.double_tower = False self.token_field_names = [] @@ -205,8 +214,12 @@ def __init__(self, config, dataset): self.num_feature_field = 0 if self.double_tower: - self.user_field_names = dataset.fields(source=[FeatureSource.USER, FeatureSource.USER_ID]) - self.item_field_names = dataset.fields(source=[FeatureSource.ITEM, FeatureSource.ITEM_ID]) + self.user_field_names = dataset.fields( + source=[FeatureSource.USER, FeatureSource.USER_ID] + ) + self.item_field_names = dataset.fields( + source=[FeatureSource.ITEM, FeatureSource.ITEM_ID] + ) self.field_names = self.user_field_names + self.item_field_names self.user_token_field_num = 0 self.user_float_field_num = 0 @@ -243,7 +256,9 @@ def __init__(self, config, dataset): self.float_field_dims.append(dataset.num(field_name)) self.num_feature_field += 1 if len(self.token_field_dims) > 0: - self.token_field_offsets = np.array((0, *np.cumsum(self.token_field_dims)[:-1]), dtype=np.long) + self.token_field_offsets = np.array( + (0, *np.cumsum(self.token_field_dims)[:-1]), dtype=np.long + ) self.token_embedding_table = FMEmbedding( self.token_field_dims, self.token_field_offsets, self.embedding_size ) @@ -254,7 +269,9 @@ def __init__(self, config, dataset): if len(self.token_seq_field_dims) > 0: self.token_seq_embedding_table = nn.ModuleList() for token_seq_field_dim in self.token_seq_field_dims: - self.token_seq_embedding_table.append(nn.Embedding(token_seq_field_dim, self.embedding_size)) + self.token_seq_embedding_table.append( + nn.Embedding(token_seq_field_dim, self.embedding_size) + ) self.first_order_linear = FMFirstOrderLinear(config, dataset) @@ -274,7 +291,13 @@ def embed_float_fields(self, float_fields, embed=True): num_float_field = float_fields.shape[1] # [batch_size, num_float_field] - index = torch.arange(0, num_float_field).unsqueeze(0).expand_as(float_fields).long().to(self.device) + index = ( + torch.arange(0, num_float_field) + .unsqueeze(0) + .expand_as(float_fields) + .long() + .to(self.device) + ) # [batch_size, num_float_field, embed_dim] float_embedding = self.float_embedding_table(index) @@ -299,7 +322,7 @@ def embed_token_fields(self, token_fields): return token_embedding - def embed_token_seq_fields(self, token_seq_fields, mode='mean'): + def embed_token_seq_fields(self, token_seq_fields, mode="mean"): """Embed the token feature columns Args: @@ -317,18 +340,30 @@ def embed_token_seq_fields(self, token_seq_fields, mode='mean'): mask = mask.float() value_cnt = torch.sum(mask, dim=1, keepdim=True) # [batch_size, 1] - token_seq_embedding = embedding_table(token_seq_field) # [batch_size, seq_len, embed_dim] - - mask = mask.unsqueeze(2).expand_as(token_seq_embedding) # [batch_size, seq_len, embed_dim] - if mode == 'max': - masked_token_seq_embedding = token_seq_embedding - (1 - mask) * 1e9 # [batch_size, seq_len, embed_dim] - result = torch.max(masked_token_seq_embedding, dim=1, keepdim=True) # [batch_size, 1, embed_dim] - elif mode == 'sum': + token_seq_embedding = embedding_table( + token_seq_field + ) # [batch_size, seq_len, embed_dim] + + mask = mask.unsqueeze(2).expand_as( + token_seq_embedding + ) # [batch_size, seq_len, embed_dim] + if mode == "max": + masked_token_seq_embedding = ( + token_seq_embedding - (1 - mask) * 1e9 + ) # [batch_size, seq_len, embed_dim] + result = torch.max( + masked_token_seq_embedding, dim=1, keepdim=True + ) # [batch_size, 1, embed_dim] + elif mode == "sum": masked_token_seq_embedding = token_seq_embedding * mask.float() - result = torch.sum(masked_token_seq_embedding, dim=1, keepdim=True) # [batch_size, 1, embed_dim] + result = torch.sum( + masked_token_seq_embedding, dim=1, keepdim=True + ) # [batch_size, 1, embed_dim] else: masked_token_seq_embedding = token_seq_embedding * mask.float() - result = torch.sum(masked_token_seq_embedding, dim=1) # [batch_size, embed_dim] + result = torch.sum( + masked_token_seq_embedding, dim=1 + ) # [batch_size, embed_dim] eps = torch.FloatTensor([1e-8]).to(self.device) result = torch.div(result, value_cnt + eps) # [batch_size, embed_dim] result = result.unsqueeze(1) # [batch_size, 1, embed_dim] @@ -336,7 +371,9 @@ def embed_token_seq_fields(self, token_seq_fields, mode='mean'): if len(fields_result) == 0: return None else: - return torch.cat(fields_result, dim=1) # [batch_size, num_token_seq_field, embed_dim] + return torch.cat( + fields_result, dim=1 + ) # [batch_size, num_token_seq_field, embed_dim] def double_tower_embed_input_fields(self, interaction): """Embed the whole feature columns in a double tower way. @@ -352,27 +389,47 @@ def double_tower_embed_input_fields(self, interaction): """ if not self.double_tower: - raise RuntimeError('Please check your model hyper parameters and set \'double tower\' as True') + raise RuntimeError( + "Please check your model hyper parameters and set 'double tower' as True" + ) sparse_embedding, dense_embedding = self.embed_input_fields(interaction) if dense_embedding is not None: - first_dense_embedding, second_dense_embedding = \ - torch.split(dense_embedding, [self.user_float_field_num, self.item_float_field_num], dim=1) + first_dense_embedding, second_dense_embedding = torch.split( + dense_embedding, + [self.user_float_field_num, self.item_float_field_num], + dim=1, + ) else: first_dense_embedding, second_dense_embedding = None, None if sparse_embedding is not None: sizes = [ - self.user_token_seq_field_num, self.item_token_seq_field_num, self.user_token_field_num, - self.item_token_field_num + self.user_token_seq_field_num, + self.item_token_seq_field_num, + self.user_token_field_num, + self.item_token_field_num, ] - first_token_seq_embedding, second_token_seq_embedding, first_token_embedding, second_token_embedding = \ - torch.split(sparse_embedding, sizes, dim=1) - first_sparse_embedding = torch.cat([first_token_seq_embedding, first_token_embedding], dim=1) - second_sparse_embedding = torch.cat([second_token_seq_embedding, second_token_embedding], dim=1) + ( + first_token_seq_embedding, + second_token_seq_embedding, + first_token_embedding, + second_token_embedding, + ) = torch.split(sparse_embedding, sizes, dim=1) + first_sparse_embedding = torch.cat( + [first_token_seq_embedding, first_token_embedding], dim=1 + ) + second_sparse_embedding = torch.cat( + [second_token_seq_embedding, second_token_embedding], dim=1 + ) else: first_sparse_embedding, second_sparse_embedding = None, None - return first_sparse_embedding, first_dense_embedding, second_sparse_embedding, second_dense_embedding + return ( + first_sparse_embedding, + first_dense_embedding, + second_sparse_embedding, + second_dense_embedding, + ) def concat_embed_input_fields(self, interaction): sparse_embedding, dense_embedding = self.embed_input_fields(interaction) @@ -400,7 +457,9 @@ def embed_input_fields(self, interaction): else: float_fields.append(interaction[field_name].unsqueeze(1)) if len(float_fields) > 0: - float_fields = torch.cat(float_fields, dim=1) # [batch_size, num_float_field] + float_fields = torch.cat( + float_fields, dim=1 + ) # [batch_size, num_float_field] else: float_fields = None # [batch_size, num_float_field] or [batch_size, num_float_field, embed_dim] or None @@ -410,7 +469,9 @@ def embed_input_fields(self, interaction): for field_name in self.token_field_names: token_fields.append(interaction[field_name].unsqueeze(1)) if len(token_fields) > 0: - token_fields = torch.cat(token_fields, dim=1) # [batch_size, num_token_field] + token_fields = torch.cat( + token_fields, dim=1 + ) # [batch_size, num_token_field] else: token_fields = None # [batch_size, num_token_field, embed_dim] or None @@ -428,7 +489,9 @@ def embed_input_fields(self, interaction): if token_seq_fields_embedding is None: sparse_embedding = token_fields_embedding else: - sparse_embedding = torch.cat([token_seq_fields_embedding, token_fields_embedding], dim=1) + sparse_embedding = torch.cat( + [token_seq_fields_embedding, token_fields_embedding], dim=1 + ) dense_embedding = float_fields_embedding diff --git a/recbole/model/context_aware_recommender/afm.py b/recbole/model/context_aware_recommender/afm.py index e7257caa0..df0198766 100644 --- a/recbole/model/context_aware_recommender/afm.py +++ b/recbole/model/context_aware_recommender/afm.py @@ -21,17 +21,15 @@ class AFM(ContextRecommender): - """ AFM is a attention based FM model that predict the final score with the attention of input feature. - - """ + """AFM is a attention based FM model that predict the final score with the attention of input feature.""" def __init__(self, config, dataset): super(AFM, self).__init__(config, dataset) # load parameters info - self.attention_size = config['attention_size'] - self.dropout_prob = config['dropout_prob'] - self.reg_weight = config['reg_weight'] + self.attention_size = config["attention_size"] + self.dropout_prob = config["dropout_prob"] + self.reg_weight = config["reg_weight"] self.num_pair = self.num_feature_field * (self.num_feature_field - 1) / 2 # define layers and loss @@ -53,7 +51,7 @@ def _init_weights(self, module): constant_(module.bias.data, 0) def build_cross(self, feat_emb): - """ Build the cross feature columns of feature columns + """Build the cross feature columns of feature columns Args: feat_emb (torch.FloatTensor): input feature embedding tensor. shape of [batch_size, field_size, embed_dim]. @@ -75,7 +73,7 @@ def build_cross(self, feat_emb): return p, q def afm_layer(self, infeature): - """ Get the attention-based feature interaction score + """Get the attention-based feature interaction score Args: infeature (torch.FloatTensor): input feature embedding tensor. shape of [batch_size, field_size, embed_dim]. @@ -89,7 +87,9 @@ def afm_layer(self, infeature): # [batch_size, num_pairs, 1] att_signal = self.attlayer(pair_wise_inter).unsqueeze(dim=2) - att_inter = torch.mul(att_signal, pair_wise_inter) # [batch_size, num_pairs, emb_dim] + att_inter = torch.mul( + att_signal, pair_wise_inter + ) # [batch_size, num_pairs, emb_dim] att_pooling = torch.sum(att_inter, dim=1) # [batch_size, emb_dim] att_pooling = self.dropout_layer(att_pooling) # [batch_size, emb_dim] @@ -99,9 +99,13 @@ def afm_layer(self, infeature): return att_pooling def forward(self, interaction): - afm_all_embeddings = self.concat_embed_input_fields(interaction) # [batch_size, num_field, embed_dim] + afm_all_embeddings = self.concat_embed_input_fields( + interaction + ) # [batch_size, num_field, embed_dim] - output = self.first_order_linear(interaction) + self.afm_layer(afm_all_embeddings) + output = self.first_order_linear(interaction) + self.afm_layer( + afm_all_embeddings + ) return output.squeeze(-1) def calculate_loss(self, interaction): diff --git a/recbole/model/context_aware_recommender/autoint.py b/recbole/model/context_aware_recommender/autoint.py index 8276e1e5c..ee0543eec 100644 --- a/recbole/model/context_aware_recommender/autoint.py +++ b/recbole/model/context_aware_recommender/autoint.py @@ -22,7 +22,7 @@ class AutoInt(ContextRecommender): - """ AutoInt is a novel CTR prediction model based on self-attention mechanism, + """AutoInt is a novel CTR prediction model based on self-attention mechanism, which can automatically learn high-order feature interactions in an explicit fashion. """ @@ -31,12 +31,12 @@ def __init__(self, config, dataset): super(AutoInt, self).__init__(config, dataset) # load parameters info - self.attention_size = config['attention_size'] - self.dropout_probs = config['dropout_probs'] - self.n_layers = config['n_layers'] - self.num_heads = config['num_heads'] - self.mlp_hidden_size = config['mlp_hidden_size'] - self.has_residual = config['has_residual'] + self.attention_size = config["attention_size"] + self.dropout_probs = config["dropout_probs"] + self.n_layers = config["n_layers"] + self.num_heads = config["num_heads"] + self.mlp_hidden_size = config["mlp_hidden_size"] + self.has_residual = config["has_residual"] # define layers and loss self.att_embedding = nn.Linear(self.embedding_size, self.attention_size) @@ -45,14 +45,20 @@ def __init__(self, config, dataset): size_list = [self.embed_output_dim] + self.mlp_hidden_size self.mlp_layers = MLPLayers(size_list, dropout=self.dropout_probs[1]) # multi-head self-attention network - self.self_attns = nn.ModuleList([ - nn.MultiheadAttention(self.attention_size, self.num_heads, dropout=self.dropout_probs[0]) - for _ in range(self.n_layers) - ]) + self.self_attns = nn.ModuleList( + [ + nn.MultiheadAttention( + self.attention_size, self.num_heads, dropout=self.dropout_probs[0] + ) + for _ in range(self.n_layers) + ] + ) self.attn_fc = torch.nn.Linear(self.atten_output_dim, 1) self.deep_predict_layer = nn.Linear(self.mlp_hidden_size[-1], 1) if self.has_residual: - self.v_res_res_embedding = torch.nn.Linear(self.embedding_size, self.attention_size) + self.v_res_res_embedding = torch.nn.Linear( + self.embedding_size, self.attention_size + ) self.dropout_layer = nn.Dropout(p=self.dropout_probs[2]) self.sigmoid = nn.Sigmoid() @@ -70,7 +76,7 @@ def _init_weights(self, module): constant_(module.bias.data, 0) def autoint_layer(self, infeature): - """ Get the attention-based feature interaction score + """Get the attention-based feature interaction score Args: infeature (torch.FloatTensor): input feature embedding tensor. shape of[batch_size,field_size,embed_dim]. @@ -91,12 +97,18 @@ def autoint_layer(self, infeature): # Interacting layer cross_term = F.relu(cross_term).contiguous().view(-1, self.atten_output_dim) batch_size = infeature.shape[0] - att_output = self.attn_fc(cross_term) + self.deep_predict_layer(self.mlp_layers(infeature.view(batch_size, -1))) + att_output = self.attn_fc(cross_term) + self.deep_predict_layer( + self.mlp_layers(infeature.view(batch_size, -1)) + ) return att_output def forward(self, interaction): - autoint_all_embeddings = self.concat_embed_input_fields(interaction) # [batch_size, num_field, embed_dim] - output = self.first_order_linear(interaction) + self.autoint_layer(autoint_all_embeddings) + autoint_all_embeddings = self.concat_embed_input_fields( + interaction + ) # [batch_size, num_field, embed_dim] + output = self.first_order_linear(interaction) + self.autoint_layer( + autoint_all_embeddings + ) return output.squeeze(1) def calculate_loss(self, interaction): diff --git a/recbole/model/context_aware_recommender/dcn.py b/recbole/model/context_aware_recommender/dcn.py index b3354a235..839fb94c8 100644 --- a/recbole/model/context_aware_recommender/dcn.py +++ b/recbole/model/context_aware_recommender/dcn.py @@ -37,26 +37,38 @@ def __init__(self, config, dataset): super(DCN, self).__init__(config, dataset) # load parameters info - self.mlp_hidden_size = config['mlp_hidden_size'] - self.cross_layer_num = config['cross_layer_num'] - self.reg_weight = config['reg_weight'] - self.dropout_prob = config['dropout_prob'] + self.mlp_hidden_size = config["mlp_hidden_size"] + self.cross_layer_num = config["cross_layer_num"] + self.reg_weight = config["reg_weight"] + self.dropout_prob = config["dropout_prob"] # define layers and loss # init weight and bias of each cross layer self.cross_layer_w = nn.ParameterList( - nn.Parameter(torch.randn(self.num_feature_field * self.embedding_size).to(self.device)) + nn.Parameter( + torch.randn(self.num_feature_field * self.embedding_size).to( + self.device + ) + ) for _ in range(self.cross_layer_num) ) self.cross_layer_b = nn.ParameterList( - nn.Parameter(torch.zeros(self.num_feature_field * self.embedding_size).to(self.device)) + nn.Parameter( + torch.zeros(self.num_feature_field * self.embedding_size).to( + self.device + ) + ) for _ in range(self.cross_layer_num) ) # size of mlp hidden layer - size_list = [self.embedding_size * self.num_feature_field] + self.mlp_hidden_size + size_list = [ + self.embedding_size * self.num_feature_field + ] + self.mlp_hidden_size # size of cross network output - in_feature_num = self.embedding_size * self.num_feature_field + self.mlp_hidden_size[-1] + in_feature_num = ( + self.embedding_size * self.num_feature_field + self.mlp_hidden_size[-1] + ) self.mlp_layers = MLPLayers(size_list, dropout=self.dropout_prob, bn=True) self.predict_layer = nn.Linear(in_feature_num, 1) @@ -99,7 +111,9 @@ def cross_network(self, x_0): return x_l def forward(self, interaction): - dcn_all_embeddings = self.concat_embed_input_fields(interaction) # [batch_size, num_field, embed_dim] + dcn_all_embeddings = self.concat_embed_input_fields( + interaction + ) # [batch_size, num_field, embed_dim] batch_size = dcn_all_embeddings.shape[0] dcn_all_embeddings = dcn_all_embeddings.view(batch_size, -1) diff --git a/recbole/model/context_aware_recommender/deepfm.py b/recbole/model/context_aware_recommender/deepfm.py index 34dd3977d..2ee2d9d68 100644 --- a/recbole/model/context_aware_recommender/deepfm.py +++ b/recbole/model/context_aware_recommender/deepfm.py @@ -33,14 +33,18 @@ def __init__(self, config, dataset): super(DeepFM, self).__init__(config, dataset) # load parameters info - self.mlp_hidden_size = config['mlp_hidden_size'] - self.dropout_prob = config['dropout_prob'] + self.mlp_hidden_size = config["mlp_hidden_size"] + self.dropout_prob = config["dropout_prob"] # define layers and loss self.fm = BaseFactorizationMachine(reduce_sum=True) - size_list = [self.embedding_size * self.num_feature_field] + self.mlp_hidden_size + size_list = [ + self.embedding_size * self.num_feature_field + ] + self.mlp_hidden_size self.mlp_layers = MLPLayers(size_list, self.dropout_prob) - self.deep_predict_layer = nn.Linear(self.mlp_hidden_size[-1], 1) # Linear product to the final score + self.deep_predict_layer = nn.Linear( + self.mlp_hidden_size[-1], 1 + ) # Linear product to the final score self.sigmoid = nn.Sigmoid() self.loss = nn.BCEWithLogitsLoss() @@ -56,11 +60,15 @@ def _init_weights(self, module): constant_(module.bias.data, 0) def forward(self, interaction): - deepfm_all_embeddings = self.concat_embed_input_fields(interaction) # [batch_size, num_field, embed_dim] + deepfm_all_embeddings = self.concat_embed_input_fields( + interaction + ) # [batch_size, num_field, embed_dim] batch_size = deepfm_all_embeddings.shape[0] y_fm = self.first_order_linear(interaction) + self.fm(deepfm_all_embeddings) - y_deep = self.deep_predict_layer(self.mlp_layers(deepfm_all_embeddings.view(batch_size, -1))) + y_deep = self.deep_predict_layer( + self.mlp_layers(deepfm_all_embeddings.view(batch_size, -1)) + ) y = y_fm + y_deep return y.squeeze(-1) diff --git a/recbole/model/context_aware_recommender/dssm.py b/recbole/model/context_aware_recommender/dssm.py index bc382e6cb..296a4d1d6 100644 --- a/recbole/model/context_aware_recommender/dssm.py +++ b/recbole/model/context_aware_recommender/dssm.py @@ -20,7 +20,7 @@ class DSSM(ContextRecommender): - """ DSSM respectively expresses user and item as low dimensional vectors with mlp layers, + """DSSM respectively expresses user and item as low dimensional vectors with mlp layers, and uses cosine distance to calculate the distance between the two semantic vectors. """ @@ -29,17 +29,33 @@ def __init__(self, config, dataset): super(DSSM, self).__init__(config, dataset) # load parameters info - self.mlp_hidden_size = config['mlp_hidden_size'] - self.dropout_prob = config['dropout_prob'] - - self.user_feature_num = self.user_token_field_num + self.user_float_field_num + self.user_token_seq_field_num - self.item_feature_num = self.item_token_field_num + self.item_float_field_num + self.item_token_seq_field_num - user_size_list = [self.embedding_size * self.user_feature_num] + self.mlp_hidden_size - item_size_list = [self.embedding_size * self.item_feature_num] + self.mlp_hidden_size + self.mlp_hidden_size = config["mlp_hidden_size"] + self.dropout_prob = config["dropout_prob"] + + self.user_feature_num = ( + self.user_token_field_num + + self.user_float_field_num + + self.user_token_seq_field_num + ) + self.item_feature_num = ( + self.item_token_field_num + + self.item_float_field_num + + self.item_token_seq_field_num + ) + user_size_list = [ + self.embedding_size * self.user_feature_num + ] + self.mlp_hidden_size + item_size_list = [ + self.embedding_size * self.item_feature_num + ] + self.mlp_hidden_size # define layers and loss - self.user_mlp_layers = MLPLayers(user_size_list, self.dropout_prob, activation='tanh', bn=True) - self.item_mlp_layers = MLPLayers(item_size_list, self.dropout_prob, activation='tanh', bn=True) + self.user_mlp_layers = MLPLayers( + user_size_list, self.dropout_prob, activation="tanh", bn=True + ) + self.item_mlp_layers = MLPLayers( + item_size_list, self.dropout_prob, activation="tanh", bn=True + ) self.loss = nn.BCEWithLogitsLoss() self.sigmoid = nn.Sigmoid() diff --git a/recbole/model/context_aware_recommender/ffm.py b/recbole/model/context_aware_recommender/ffm.py index 3e334e3e4..b47b4897a 100644 --- a/recbole/model/context_aware_recommender/ffm.py +++ b/recbole/model/context_aware_recommender/ffm.py @@ -23,7 +23,7 @@ class FFM(ContextRecommender): - r"""FFM is a context-based recommendation model. It aims to model the different feature interactions + r"""FFM is a context-based recommendation model. It aims to model the different feature interactions between different fields. Each feature has several latent vectors :math:`v_{i,F(j)}`, which depend on the field of other features, and one of them is used to do the inner product. @@ -37,21 +37,34 @@ def __init__(self, config, dataset): super(FFM, self).__init__(config, dataset) # load parameters info - self.fields = config['fields'] # a dict; key: field_id; value: feature_list + self.fields = config["fields"] # a dict; key: field_id; value: feature_list self.sigmoid = nn.Sigmoid() self.feature2id = {} self.feature2field = {} - self.feature_names = (self.token_field_names, self.float_field_names, self.token_seq_field_names) - self.feature_dims = (self.token_field_dims, self.float_field_dims, self.token_seq_field_dims) + self.feature_names = ( + self.token_field_names, + self.float_field_names, + self.token_seq_field_names, + ) + self.feature_dims = ( + self.token_field_dims, + self.float_field_dims, + self.token_seq_field_dims, + ) self._get_feature2field() self.num_fields = len(set(self.feature2field.values())) # the number of fields self.ffm = FieldAwareFactorizationMachine( - self.feature_names, self.feature_dims, self.feature2id, self.feature2field, self.num_fields, - self.embedding_size, self.device + self.feature_names, + self.feature_dims, + self.feature2id, + self.feature2field, + self.num_fields, + self.embedding_size, + self.device, ) self.loss = nn.BCEWithLogitsLoss() @@ -67,9 +80,7 @@ def _init_weights(self, module): constant_(module.bias.data, 0) def _get_feature2field(self): - r"""Create a mapping between features and fields. - - """ + r"""Create a mapping between features and fields.""" fea_id = 0 for names in self.feature_names: if names is not None: @@ -91,21 +102,23 @@ def _get_feature2field(self): pass def get_ffm_input(self, interaction): - r"""Get different types of ffm layer's input. - - """ + r"""Get different types of ffm layer's input.""" token_ffm_input = [] if self.token_field_names is not None: for tn in self.token_field_names: token_ffm_input.append(torch.unsqueeze(interaction[tn], 1)) if len(token_ffm_input) > 0: - token_ffm_input = torch.cat(token_ffm_input, dim=1) # [batch_size, num_token_features] + token_ffm_input = torch.cat( + token_ffm_input, dim=1 + ) # [batch_size, num_token_features] float_ffm_input = [] if self.float_field_names is not None: for fn in self.float_field_names: float_ffm_input.append(torch.unsqueeze(interaction[fn], 1)) if len(float_ffm_input) > 0: - float_ffm_input = torch.cat(float_ffm_input, dim=1) # [batch_size, num_float_features] + float_ffm_input = torch.cat( + float_ffm_input, dim=1 + ) # [batch_size, num_float_features] token_seq_ffm_input = [] if self.token_seq_field_names is not None: for tsn in self.token_seq_field_names: @@ -115,7 +128,9 @@ def get_ffm_input(self, interaction): def forward(self, interaction): ffm_input = self.get_ffm_input(interaction) - ffm_output = torch.sum(torch.sum(self.ffm(ffm_input), dim=1), dim=1, keepdim=True) + ffm_output = torch.sum( + torch.sum(self.ffm(ffm_input), dim=1), dim=1, keepdim=True + ) output = self.first_order_linear(interaction) + ffm_output return output.squeeze(-1) @@ -131,11 +146,18 @@ def predict(self, interaction): class FieldAwareFactorizationMachine(nn.Module): - r"""This is Field-Aware Factorization Machine Module for FFM. - - """ - - def __init__(self, feature_names, feature_dims, feature2id, feature2field, num_fields, embed_dim, device): + r"""This is Field-Aware Factorization Machine Module for FFM.""" + + def __init__( + self, + feature_names, + feature_dims, + feature2id, + feature2field, + num_fields, + embed_dim, + device, + ): super(FieldAwareFactorizationMachine, self).__init__() self.token_feature_names = feature_names[0] @@ -147,8 +169,11 @@ def __init__(self, feature_names, feature_dims, feature2id, feature2field, num_f self.feature2id = feature2id self.feature2field = feature2field - self.num_features = len(self.token_feature_names) + len(self.float_feature_names) \ - + len(self.token_seq_feature_names) + self.num_features = ( + len(self.token_feature_names) + + len(self.float_feature_names) + + len(self.token_seq_feature_names) + ) self.num_fields = num_fields self.embed_dim = embed_dim self.device = device @@ -156,19 +181,29 @@ def __init__(self, feature_names, feature_dims, feature2id, feature2field, num_f # init token field-aware embeddings if there is token type of features. if len(self.token_feature_names) > 0: self.num_token_features = len(self.token_feature_names) - self.token_embeddings = torch.nn.ModuleList([ - nn.Embedding(sum(self.token_feature_dims), self.embed_dim) for _ in range(self.num_fields) - ]) - self.token_offsets = np.array((0, *np.cumsum(self.token_feature_dims)[:-1]), dtype=np.long) + self.token_embeddings = torch.nn.ModuleList( + [ + nn.Embedding(sum(self.token_feature_dims), self.embed_dim) + for _ in range(self.num_fields) + ] + ) + self.token_offsets = np.array( + (0, *np.cumsum(self.token_feature_dims)[:-1]), dtype=np.long + ) for embedding in self.token_embeddings: nn.init.xavier_uniform_(embedding.weight.data) # init float field-aware embeddings if there is float type of features. if len(self.float_feature_names) > 0: self.num_float_features = len(self.float_feature_names) - self.float_embeddings = nn.Embedding(np.sum(self.token_feature_dims, dtype=np.int32), self.embed_dim) - self.float_embeddings = torch.nn.ModuleList([ - nn.Embedding(self.num_float_features, self.embed_dim) for _ in range(self.num_fields) - ]) + self.float_embeddings = nn.Embedding( + np.sum(self.token_feature_dims, dtype=np.int32), self.embed_dim + ) + self.float_embeddings = torch.nn.ModuleList( + [ + nn.Embedding(self.num_float_features, self.embed_dim) + for _ in range(self.num_fields) + ] + ) for embedding in self.float_embeddings: nn.init.xavier_uniform_(embedding.weight.data) # init token_seq field-aware embeddings if there is token_seq type of features. @@ -178,14 +213,16 @@ def __init__(self, feature_names, feature_dims, feature2id, feature2field, num_f self.token_seq_embedding = torch.nn.ModuleList() for i in range(self.num_fields): for token_seq_feature_dim in self.token_seq_feature_dims: - self.token_seq_embedding.append(nn.Embedding(token_seq_feature_dim, self.embed_dim)) + self.token_seq_embedding.append( + nn.Embedding(token_seq_feature_dim, self.embed_dim) + ) for embedding in self.token_seq_embedding: nn.init.xavier_uniform_(embedding.weight.data) self.token_seq_embeddings.append(self.token_seq_embedding) def forward(self, input_x): r"""Model the different interaction strengths of different field pairs. - + Args: input_x (a tuple): (token_ffm_input, float_ffm_input, token_seq_ffm_input) @@ -200,23 +237,34 @@ def forward(self, input_x): torch.cuda.FloatTensor: The results of all features' field-aware interactions. shape: [batch_size, num_fields, emb_dim] """ - token_ffm_input, float_ffm_input, token_seq_ffm_input = input_x[0], input_x[1], input_x[2] + token_ffm_input, float_ffm_input, token_seq_ffm_input = ( + input_x[0], + input_x[1], + input_x[2], + ) token_input_x_emb = self._emb_token_ffm_input(token_ffm_input) float_input_x_emb = self._emb_float_ffm_input(float_ffm_input) token_seq_input_x_emb = self._emb_token_seq_ffm_input(token_seq_ffm_input) - input_x_emb = self._get_input_x_emb(token_input_x_emb, float_input_x_emb, token_seq_input_x_emb) + input_x_emb = self._get_input_x_emb( + token_input_x_emb, float_input_x_emb, token_seq_input_x_emb + ) output = list() for i in range(self.num_features - 1): for j in range(i + 1, self.num_features): - output.append(input_x_emb[self.feature2field[j]][:, i] * input_x_emb[self.feature2field[i]][:, j]) + output.append( + input_x_emb[self.feature2field[j]][:, i] + * input_x_emb[self.feature2field[i]][:, j] + ) output = torch.stack(output, dim=1) # [batch_size, num_fields, emb_dim] return output - def _get_input_x_emb(self, token_input_x_emb, float_input_x_emb, token_seq_input_x_emb): + def _get_input_x_emb( + self, token_input_x_emb, float_input_x_emb, token_seq_input_x_emb + ): # merge different types of field-aware embeddings input_x_emb = [] # [num_fields: [batch_size, num_fields, emb_dim]] @@ -237,7 +285,9 @@ def _emb_token_ffm_input(self, token_ffm_input): # get token field-aware embeddings token_input_x_emb = [] if len(self.token_feature_names) > 0: - token_input_x = token_ffm_input + token_ffm_input.new_tensor(self.token_offsets).unsqueeze(0) + token_input_x = token_ffm_input + token_ffm_input.new_tensor( + self.token_offsets + ).unsqueeze(0) token_input_x_emb = [ self.token_embeddings[i](token_input_x) for i in range(self.num_fields) ] # [num_fields: [batch_size, num_token_features, emb_dim]] @@ -248,8 +298,12 @@ def _emb_float_ffm_input(self, float_ffm_input): # get float field-aware embeddings float_input_x_emb = [] if len(self.float_feature_names) > 0: - index = torch.arange(0, self.num_float_features).unsqueeze(0).expand_as(float_ffm_input).long().to( - self.device + index = ( + torch.arange(0, self.num_float_features) + .unsqueeze(0) + .expand_as(float_ffm_input) + .long() + .to(self.device) ) # [batch_size, num_float_features] float_input_x_emb = [ torch.mul(self.float_embeddings[i](index), float_ffm_input.unsqueeze(2)) @@ -270,13 +324,21 @@ def _emb_token_seq_ffm_input(self, token_seq_ffm_input): mask = mask.float() value_cnt = torch.sum(mask, dim=1, keepdim=True) # [batch_size, 1] - token_seq_embedding = embedding_table(token_seq) # [batch_size, seq_len, embed_dim] - mask = mask.unsqueeze(2).expand_as(token_seq_embedding) # [batch_size, seq_len, embed_dim] + token_seq_embedding = embedding_table( + token_seq + ) # [batch_size, seq_len, embed_dim] + mask = mask.unsqueeze(2).expand_as( + token_seq_embedding + ) # [batch_size, seq_len, embed_dim] # mean masked_token_seq_embedding = token_seq_embedding * mask.float() - result = torch.sum(masked_token_seq_embedding, dim=1) # [batch_size, embed_dim] + result = torch.sum( + masked_token_seq_embedding, dim=1 + ) # [batch_size, embed_dim] eps = torch.FloatTensor([1e-8]).to(self.device) - result = torch.div(result, value_cnt + eps) # [batch_size, embed_dim] + result = torch.div( + result, value_cnt + eps + ) # [batch_size, embed_dim] result = result.unsqueeze(1) # [batch_size, 1, embed_dim] token_seq_result.append(result) diff --git a/recbole/model/context_aware_recommender/fm.py b/recbole/model/context_aware_recommender/fm.py index 63f712aec..492876422 100644 --- a/recbole/model/context_aware_recommender/fm.py +++ b/recbole/model/context_aware_recommender/fm.py @@ -24,9 +24,7 @@ class FM(ContextRecommender): - """Factorization Machine considers the second-order interaction with features to predict the final score. - - """ + """Factorization Machine considers the second-order interaction with features to predict the final score.""" def __init__(self, config, dataset): @@ -45,7 +43,9 @@ def _init_weights(self, module): xavier_normal_(module.weight.data) def forward(self, interaction): - fm_all_embeddings = self.concat_embed_input_fields(interaction) # [batch_size, num_field, embed_dim] + fm_all_embeddings = self.concat_embed_input_fields( + interaction + ) # [batch_size, num_field, embed_dim] y = self.first_order_linear(interaction) + self.fm(fm_all_embeddings) return y.squeeze(-1) diff --git a/recbole/model/context_aware_recommender/fnn.py b/recbole/model/context_aware_recommender/fnn.py index f7334d127..1c81451fc 100644 --- a/recbole/model/context_aware_recommender/fnn.py +++ b/recbole/model/context_aware_recommender/fnn.py @@ -34,13 +34,17 @@ def __init__(self, config, dataset): super(FNN, self).__init__(config, dataset) # load parameters info - self.mlp_hidden_size = config['mlp_hidden_size'] - self.dropout_prob = config['dropout_prob'] + self.mlp_hidden_size = config["mlp_hidden_size"] + self.dropout_prob = config["dropout_prob"] - size_list = [self.embedding_size * self.num_feature_field] + self.mlp_hidden_size + size_list = [ + self.embedding_size * self.num_feature_field + ] + self.mlp_hidden_size # define layers and loss - self.mlp_layers = MLPLayers(size_list, self.dropout_prob, activation='tanh', bn=False) # use tanh as activation + self.mlp_layers = MLPLayers( + size_list, self.dropout_prob, activation="tanh", bn=False + ) # use tanh as activation self.predict_layer = nn.Linear(self.mlp_hidden_size[-1], 1, bias=True) self.sigmoid = nn.Sigmoid() @@ -58,10 +62,14 @@ def _init_weights(self, module): constant_(module.bias.data, 0) def forward(self, interaction): - fnn_all_embeddings = self.concat_embed_input_fields(interaction) # [batch_size, num_field, embed_dim] + fnn_all_embeddings = self.concat_embed_input_fields( + interaction + ) # [batch_size, num_field, embed_dim] batch_size = fnn_all_embeddings.shape[0] - output = self.predict_layer(self.mlp_layers(fnn_all_embeddings.view(batch_size, -1))) + output = self.predict_layer( + self.mlp_layers(fnn_all_embeddings.view(batch_size, -1)) + ) return output.squeeze(-1) def calculate_loss(self, interaction): diff --git a/recbole/model/context_aware_recommender/fwfm.py b/recbole/model/context_aware_recommender/fwfm.py index d3db3988b..5ef4eba43 100644 --- a/recbole/model/context_aware_recommender/fwfm.py +++ b/recbole/model/context_aware_recommender/fwfm.py @@ -21,7 +21,7 @@ class FwFM(ContextRecommender): r"""FwFM is a context-based recommendation model. It aims to model the different feature interactions - between different fields in a much more memory-efficient way. It proposes a field pair weight matrix + between different fields in a much more memory-efficient way. It proposes a field pair weight matrix :math:`r_{F(i),F(j)}`, to capture the heterogeneity of field pair interactions. The model defines as follows: @@ -34,8 +34,8 @@ def __init__(self, config, dataset): super(FwFM, self).__init__(config, dataset) # load parameters info - self.dropout_prob = config['dropout_prob'] - self.fields = config['fields'] # a dict; key: field_id; value: feature_list + self.dropout_prob = config["dropout_prob"] + self.fields = config["fields"] # a dict; key: field_id; value: feature_list self.num_features = self.num_feature_field @@ -45,8 +45,16 @@ def __init__(self, config, dataset): self.feature2id = {} self.feature2field = {} - self.feature_names = (self.token_field_names, self.token_seq_field_names, self.float_field_names) - self.feature_dims = (self.token_field_dims, self.token_seq_field_dims, self.float_field_dims) + self.feature_names = ( + self.token_field_names, + self.token_seq_field_names, + self.float_field_names, + ) + self.feature_dims = ( + self.token_field_dims, + self.token_seq_field_dims, + self.float_field_dims, + ) self._get_feature2field() self.num_fields = len(set(self.feature2field.values())) # the number of fields self.num_pair = self.num_fields * self.num_fields @@ -65,9 +73,7 @@ def _init_weights(self, module): constant_(module.bias.data, 0) def _get_feature2field(self): - r"""Create a mapping between features and fields. - - """ + r"""Create a mapping between features and fields.""" fea_id = 0 for names in self.feature_names: if names is not None: @@ -89,7 +95,7 @@ def _get_feature2field(self): pass def fwfm_layer(self, infeature): - r"""Get the field pair weight matrix r_{F(i),F(j)}, and model the different interaction strengths of + r"""Get the field pair weight matrix r_{F(i),F(j)}, and model the different interaction strengths of different field pairs :math:`\sum_{i=1}^{m}\sum_{j=i+1}^{m}x_{i}x_{j}r_{F(i),F(j)}`. Args: @@ -100,11 +106,17 @@ def fwfm_layer(self, infeature): """ # get r(Fi, Fj) batch_size = infeature.shape[0] - para = torch.randn(self.num_fields * self.num_fields * self.embedding_size).\ - expand(batch_size, self.num_fields * self.num_fields * self.embedding_size).\ - to(self.device) # [batch_size*num_pairs*emb_dim] - para = para.reshape(batch_size, self.num_fields, self.num_fields, self.embedding_size) - r = nn.Parameter(para, requires_grad=True) # [batch_size, num_fields, num_fields, emb_dim] + para = ( + torch.randn(self.num_fields * self.num_fields * self.embedding_size) + .expand(batch_size, self.num_fields * self.num_fields * self.embedding_size) + .to(self.device) + ) # [batch_size*num_pairs*emb_dim] + para = para.reshape( + batch_size, self.num_fields, self.num_fields, self.embedding_size + ) + r = nn.Parameter( + para, requires_grad=True + ) # [batch_size, num_fields, num_fields, emb_dim] fwfm_inter = list() # [batch_size, num_fields, emb_dim] for i in range(self.num_features - 1): @@ -120,9 +132,13 @@ def fwfm_layer(self, infeature): return fwfm_output def forward(self, interaction): - fwfm_all_embeddings = self.concat_embed_input_fields(interaction) # [batch_size, num_field, embed_dim] + fwfm_all_embeddings = self.concat_embed_input_fields( + interaction + ) # [batch_size, num_field, embed_dim] - output = self.first_order_linear(interaction) + self.fwfm_layer(fwfm_all_embeddings) + output = self.first_order_linear(interaction) + self.fwfm_layer( + fwfm_all_embeddings + ) return output.squeeze(-1) diff --git a/recbole/model/context_aware_recommender/nfm.py b/recbole/model/context_aware_recommender/nfm.py index d9f47a84c..c672f29e0 100644 --- a/recbole/model/context_aware_recommender/nfm.py +++ b/recbole/model/context_aware_recommender/nfm.py @@ -19,22 +19,22 @@ class NFM(ContextRecommender): - """ NFM replace the fm part as a mlp to model the feature interaction. - - """ + """NFM replace the fm part as a mlp to model the feature interaction.""" def __init__(self, config, dataset): super(NFM, self).__init__(config, dataset) # load parameters info - self.mlp_hidden_size = config['mlp_hidden_size'] - self.dropout_prob = config['dropout_prob'] + self.mlp_hidden_size = config["mlp_hidden_size"] + self.dropout_prob = config["dropout_prob"] # define layers and loss size_list = [self.embedding_size] + self.mlp_hidden_size self.fm = BaseFactorizationMachine(reduce_sum=False) self.bn = nn.BatchNorm1d(num_features=self.embedding_size) - self.mlp_layers = MLPLayers(size_list, self.dropout_prob, activation='sigmoid', bn=True) + self.mlp_layers = MLPLayers( + size_list, self.dropout_prob, activation="sigmoid", bn=True + ) self.predict_layer = nn.Linear(self.mlp_hidden_size[-1], 1, bias=False) self.sigmoid = nn.Sigmoid() self.loss = nn.BCEWithLogitsLoss() @@ -51,10 +51,14 @@ def _init_weights(self, module): constant_(module.bias.data, 0) def forward(self, interaction): - nfm_all_embeddings = self.concat_embed_input_fields(interaction) # [batch_size, num_field, embed_dim] + nfm_all_embeddings = self.concat_embed_input_fields( + interaction + ) # [batch_size, num_field, embed_dim] bn_nfm_all_embeddings = self.bn(self.fm(nfm_all_embeddings)) - output = self.predict_layer(self.mlp_layers(bn_nfm_all_embeddings)) + self.first_order_linear(interaction) + output = self.predict_layer( + self.mlp_layers(bn_nfm_all_embeddings) + ) + self.first_order_linear(interaction) return output.squeeze(-1) def calculate_loss(self, interaction): diff --git a/recbole/model/context_aware_recommender/pnn.py b/recbole/model/context_aware_recommender/pnn.py index 5423edb66..efd8f9c04 100644 --- a/recbole/model/context_aware_recommender/pnn.py +++ b/recbole/model/context_aware_recommender/pnn.py @@ -34,11 +34,11 @@ def __init__(self, config, dataset): super(PNN, self).__init__(config, dataset) # load parameters info - self.mlp_hidden_size = config['mlp_hidden_size'] - self.dropout_prob = config['dropout_prob'] - self.use_inner = config['use_inner'] - self.use_outer = config['use_outer'] - self.reg_weight = config['reg_weight'] + self.mlp_hidden_size = config["mlp_hidden_size"] + self.dropout_prob = config["dropout_prob"] + self.use_inner = config["use_inner"] + self.use_outer = config["use_outer"] + self.reg_weight = config["reg_weight"] self.num_pair = int(self.num_feature_field * (self.num_feature_field - 1) / 2) @@ -46,11 +46,15 @@ def __init__(self, config, dataset): product_out_dim = self.num_feature_field * self.embedding_size if self.use_inner: product_out_dim += self.num_pair - self.inner_product = InnerProductLayer(self.num_feature_field, device=self.device) + self.inner_product = InnerProductLayer( + self.num_feature_field, device=self.device + ) if self.use_outer: product_out_dim += self.num_pair - self.outer_product = OuterProductLayer(self.num_feature_field, self.embedding_size, device=self.device) + self.outer_product = OuterProductLayer( + self.num_feature_field, self.embedding_size, device=self.device + ) size_list = [product_out_dim] + self.mlp_hidden_size self.mlp_layers = MLPLayers(size_list, self.dropout_prob, bn=False) self.predict_layer = nn.Linear(self.mlp_hidden_size[-1], 1) @@ -70,7 +74,7 @@ def reg_loss(self): """ reg_loss = 0 for name, parm in self.mlp_layers.named_parameters(): - if name.endswith('weight'): + if name.endswith("weight"): reg_loss = reg_loss + self.reg_weight * parm.norm(2) return reg_loss @@ -83,17 +87,25 @@ def _init_weights(self, module): constant_(module.bias.data, 0) def forward(self, interaction): - pnn_all_embeddings = self.concat_embed_input_fields(interaction) # [batch_size, num_field, embed_dim] + pnn_all_embeddings = self.concat_embed_input_fields( + interaction + ) # [batch_size, num_field, embed_dim] batch_size = pnn_all_embeddings.shape[0] # linear part - linear_part = pnn_all_embeddings.view(batch_size, -1) # [batch_size,num_field*embed_dim] + linear_part = pnn_all_embeddings.view( + batch_size, -1 + ) # [batch_size,num_field*embed_dim] output = [linear_part] # second order part if self.use_inner: - inner_product = self.inner_product(pnn_all_embeddings).view(batch_size, -1) # [batch_size,num_pairs] + inner_product = self.inner_product(pnn_all_embeddings).view( + batch_size, -1 + ) # [batch_size,num_pairs] output.append(inner_product) if self.use_outer: - outer_product = self.outer_product(pnn_all_embeddings).view(batch_size, -1) # [batch_size,num_pairs] + outer_product = self.outer_product(pnn_all_embeddings).view( + batch_size, -1 + ) # [batch_size,num_pairs] output.append(outer_product) output = torch.cat(output, dim=1) # [batch_size,d] @@ -167,7 +179,9 @@ def __init__(self, num_feature_field, embedding_size, device): num_pairs = int(num_feature_field * (num_feature_field - 1) / 2) embed_size = embedding_size - self.kernel = nn.Parameter(torch.rand(embed_size, num_pairs, embed_size), requires_grad=True) + self.kernel = nn.Parameter( + torch.rand(embed_size, num_pairs, embed_size), requires_grad=True + ) nn.init.xavier_uniform_(self.kernel) self.to(device) @@ -193,7 +207,9 @@ def forward(self, feat_emb): p.unsqueeze_(dim=1) # [batch_size, 1, num_pairs, emb_dim] - p = torch.mul(p, self.kernel.unsqueeze(0)) # [batch_size,emb_dim,num_pairs,emb_dim] + p = torch.mul( + p, self.kernel.unsqueeze(0) + ) # [batch_size,emb_dim,num_pairs,emb_dim] p = torch.sum(p, dim=-1) # [batch_size,emb_dim,num_pairs] p = torch.transpose(p, 2, 1) # [batch_size,num_pairs,emb_dim] diff --git a/recbole/model/context_aware_recommender/widedeep.py b/recbole/model/context_aware_recommender/widedeep.py index ea3658389..dc80328d4 100644 --- a/recbole/model/context_aware_recommender/widedeep.py +++ b/recbole/model/context_aware_recommender/widedeep.py @@ -31,11 +31,13 @@ def __init__(self, config, dataset): super(WideDeep, self).__init__(config, dataset) # load parameters info - self.mlp_hidden_size = config['mlp_hidden_size'] - self.dropout_prob = config['dropout_prob'] + self.mlp_hidden_size = config["mlp_hidden_size"] + self.dropout_prob = config["dropout_prob"] # define layers and loss - size_list = [self.embedding_size * self.num_feature_field] + self.mlp_hidden_size + size_list = [ + self.embedding_size * self.num_feature_field + ] + self.mlp_hidden_size self.mlp_layers = MLPLayers(size_list, self.dropout_prob) self.deep_predict_layer = nn.Linear(self.mlp_hidden_size[-1], 1) self.sigmoid = nn.Sigmoid() @@ -53,11 +55,15 @@ def _init_weights(self, module): constant_(module.bias.data, 0) def forward(self, interaction): - widedeep_all_embeddings = self.concat_embed_input_fields(interaction) # [batch_size, num_field, embed_dim] + widedeep_all_embeddings = self.concat_embed_input_fields( + interaction + ) # [batch_size, num_field, embed_dim] batch_size = widedeep_all_embeddings.shape[0] fm_output = self.first_order_linear(interaction) - deep_output = self.deep_predict_layer(self.mlp_layers(widedeep_all_embeddings.view(batch_size, -1))) + deep_output = self.deep_predict_layer( + self.mlp_layers(widedeep_all_embeddings.view(batch_size, -1)) + ) output = fm_output + deep_output return output.squeeze(-1) diff --git a/recbole/model/context_aware_recommender/xdeepfm.py b/recbole/model/context_aware_recommender/xdeepfm.py index a4cabc2e4..88b8bd235 100644 --- a/recbole/model/context_aware_recommender/xdeepfm.py +++ b/recbole/model/context_aware_recommender/xdeepfm.py @@ -38,26 +38,28 @@ def __init__(self, config, dataset): super(xDeepFM, self).__init__(config, dataset) # load parameters info - self.mlp_hidden_size = config['mlp_hidden_size'] - self.reg_weight = config['reg_weight'] - self.dropout_prob = config['dropout_prob'] - self.direct = config['direct'] - self.cin_layer_size = temp_cin_size = list(config['cin_layer_size']) + self.mlp_hidden_size = config["mlp_hidden_size"] + self.reg_weight = config["reg_weight"] + self.dropout_prob = config["dropout_prob"] + self.direct = config["direct"] + self.cin_layer_size = temp_cin_size = list(config["cin_layer_size"]) # Check whether the size of the CIN layer is legal. if not self.direct: self.cin_layer_size = list(map(lambda x: int(x // 2 * 2), temp_cin_size)) if self.cin_layer_size[:-1] != temp_cin_size[:-1]: self.logger.warning( - 'Layer size of CIN should be even except for the last layer when direct is True.' - 'It is changed to {}'.format(self.cin_layer_size) + "Layer size of CIN should be even except for the last layer when direct is True." + "It is changed to {}".format(self.cin_layer_size) ) # Create a convolutional layer for each CIN layer self.conv1d_list = nn.ModuleList() self.field_nums = [self.num_feature_field] for i, layer_size in enumerate(self.cin_layer_size): - conv1d = nn.Conv1d(self.field_nums[-1] * self.field_nums[0], layer_size, 1).to(self.device) + conv1d = nn.Conv1d( + self.field_nums[-1] * self.field_nums[0], layer_size, 1 + ).to(self.device) self.conv1d_list.append(conv1d) if self.direct: self.field_nums.append(layer_size) @@ -65,14 +67,18 @@ def __init__(self, config, dataset): self.field_nums.append(layer_size // 2) # Create MLP layer - size_list = [self.embedding_size * self.num_feature_field] + self.mlp_hidden_size + [1] + size_list = ( + [self.embedding_size * self.num_feature_field] + self.mlp_hidden_size + [1] + ) self.mlp_layers = MLPLayers(size_list, dropout=self.dropout_prob) # Get the output size of CIN if self.direct: self.final_len = sum(self.cin_layer_size) else: - self.final_len = sum(self.cin_layer_size[:-1]) // 2 + self.cin_layer_size[-1] + self.final_len = ( + sum(self.cin_layer_size[:-1]) // 2 + self.cin_layer_size[-1] + ) self.cin_linear = nn.Linear(self.final_len, 1) self.sigmoid = nn.Sigmoid() @@ -95,7 +101,7 @@ def reg_loss(self, parameters): """ reg_loss = 0 for name, parm in parameters: - if name.endswith('weight'): + if name.endswith("weight"): reg_loss = reg_loss + parm.norm(2) return reg_loss @@ -113,7 +119,7 @@ def calculate_reg_loss(self): l2_reg += self.reg_loss(conv1d.named_parameters()) return l2_reg - def compressed_interaction_network(self, input_features, activation='ReLU'): + def compressed_interaction_network(self, input_features, activation="ReLU"): r"""For k-th CIN layer, the output :math:`X_k` is calculated via .. math:: @@ -137,12 +143,16 @@ def compressed_interaction_network(self, input_features, activation='ReLU'): hidden_nn_layers = [input_features] final_result = [] for i, layer_size in enumerate(self.cin_layer_size): - z_i = torch.einsum('bhd,bmd->bhmd', hidden_nn_layers[-1], hidden_nn_layers[0]) - z_i = z_i.view(batch_size, self.field_nums[0] * self.field_nums[i], embedding_size) + z_i = torch.einsum( + "bhd,bmd->bhmd", hidden_nn_layers[-1], hidden_nn_layers[0] + ) + z_i = z_i.view( + batch_size, self.field_nums[0] * self.field_nums[i], embedding_size + ) z_i = self.conv1d_list[i](z_i) # Pass the CIN intermediate result through the activation function. - if activation.lower() == 'identity': + if activation.lower() == "identity": output = z_i else: activate_func = activation_layer(activation) @@ -157,7 +167,9 @@ def compressed_interaction_network(self, input_features, activation='ReLU'): next_hidden = output else: if i != len(self.cin_layer_size) - 1: - next_hidden, direct_connect = torch.split(output, 2 * [layer_size // 2], 1) + next_hidden, direct_connect = torch.split( + output, 2 * [layer_size // 2], 1 + ) else: direct_connect = output next_hidden = 0 @@ -170,7 +182,9 @@ def compressed_interaction_network(self, input_features, activation='ReLU'): def forward(self, interaction): # Get the output of CIN. - xdeepfm_input = self.concat_embed_input_fields(interaction) # [batch_size, num_field, embed_dim] + xdeepfm_input = self.concat_embed_input_fields( + interaction + ) # [batch_size, num_field, embed_dim] cin_output = self.compressed_interaction_network(xdeepfm_input) cin_output = self.cin_linear(cin_output) @@ -181,7 +195,6 @@ def forward(self, interaction): # Get predicted score. y_p = self.first_order_linear(interaction) + cin_output + dnn_output - return y_p.squeeze(1) def calculate_loss(self, interaction): diff --git a/recbole/model/exlib_recommender/lightgbm.py b/recbole/model/exlib_recommender/lightgbm.py index cc6c9be0b..c64f64d8c 100644 --- a/recbole/model/exlib_recommender/lightgbm.py +++ b/recbole/model/exlib_recommender/lightgbm.py @@ -13,9 +13,7 @@ class lightgbm(lgb.Booster): - r"""lightgbm is inherited from lgb.Booster - - """ + r"""lightgbm is inherited from lgb.Booster""" type = ModelType.DECISIONTREE input_type = InputType.POINTWISE @@ -35,6 +33,5 @@ def load_state_dict(self, model_file): self = lgb.Booster(model_file=model_file) def load_other_parameter(self, other_parameter): - r"""Load other parameters - """ + r"""Load other parameters""" pass diff --git a/recbole/model/exlib_recommender/xgboost.py b/recbole/model/exlib_recommender/xgboost.py index 2a5af5f21..4981a666c 100644 --- a/recbole/model/exlib_recommender/xgboost.py +++ b/recbole/model/exlib_recommender/xgboost.py @@ -13,9 +13,7 @@ class xgboost(xgb.Booster): - r"""xgboost is inherited from xgb.Booster - - """ + r"""xgboost is inherited from xgb.Booster""" type = ModelType.DECISIONTREE input_type = InputType.POINTWISE @@ -35,6 +33,5 @@ def load_state_dict(self, model_file): self.load_model(model_file) def load_other_parameter(self, other_parameter): - r"""Load other parameters - """ + r"""Load other parameters""" pass diff --git a/recbole/model/general_recommender/__init__.py b/recbole/model/general_recommender/__init__.py index 71cf8cbb1..59cc41b47 100644 --- a/recbole/model/general_recommender/__init__.py +++ b/recbole/model/general_recommender/__init__.py @@ -28,4 +28,3 @@ from recbole.model.general_recommender.sgl import SGL from recbole.model.general_recommender.admmslim import ADMMSLIM from recbole.model.general_recommender.simplex import SimpleX - diff --git a/recbole/model/general_recommender/admmslim.py b/recbole/model/general_recommender/admmslim.py index d8242904e..31aa710c1 100644 --- a/recbole/model/general_recommender/admmslim.py +++ b/recbole/model/general_recommender/admmslim.py @@ -40,29 +40,30 @@ def __init__(self, config, dataset): # need at least one param self.dummy_param = torch.nn.Parameter(torch.zeros(1)) - X = dataset.inter_matrix(form='csr').astype(np.float32) + X = dataset.inter_matrix(form="csr").astype(np.float32) num_users, num_items = X.shape - lambda1 = config['lambda1'] - lambda2 = config['lambda2'] - alpha = config['alpha'] - rho = config['rho'] - k = config['k'] - positive_only = config['positive_only'] - self.center_columns = config['center_columns'] + lambda1 = config["lambda1"] + lambda2 = config["lambda2"] + alpha = config["alpha"] + rho = config["rho"] + k = config["k"] + positive_only = config["positive_only"] + self.center_columns = config["center_columns"] self.item_means = X.mean(axis=0).getA1() if self.center_columns: zero_mean_X = X.toarray() - self.item_means - G = (zero_mean_X.T @ zero_mean_X) + G = zero_mean_X.T @ zero_mean_X # large memory cost because we need to make X dense to subtract mean, delete asap del zero_mean_X else: G = (X.T @ X).toarray() - diag = lambda2 * np.diag(np.power(self.item_means, alpha)) + \ - rho * np.identity(num_items) + diag = lambda2 * np.diag(np.power(self.item_means, alpha)) + rho * np.identity( + num_items + ) P = np.linalg.inv(G + diag).astype(np.float32) B_aux = (P @ G).astype(np.float32) @@ -97,10 +98,18 @@ def predict(self, interaction): user_interactions = self.interaction_matrix[user, :].toarray() if self.center_columns: - r = (((user_interactions - self.item_means) * - self.item_similarity[:, item].T).sum(axis=1)).flatten() + self.item_means[item] + r = ( + ( + (user_interactions - self.item_means) + * self.item_similarity[:, item].T + ).sum(axis=1) + ).flatten() + self.item_means[item] else: - r = (user_interactions * self.item_similarity[:, item].T).sum(axis=1).flatten() + r = ( + (user_interactions * self.item_similarity[:, item].T) + .sum(axis=1) + .flatten() + ) return add_noise(torch.from_numpy(r)) @@ -110,7 +119,10 @@ def full_sort_predict(self, interaction): user_interactions = self.interaction_matrix[user, :].toarray() if self.center_columns: - r = ((user_interactions - self.item_means) @ self.item_similarity + self.item_means).flatten() + r = ( + (user_interactions - self.item_means) @ self.item_similarity + + self.item_means + ).flatten() else: r = (user_interactions @ self.item_similarity).flatten() diff --git a/recbole/model/general_recommender/bpr.py b/recbole/model/general_recommender/bpr.py index 8886ab25d..4ab37be69 100644 --- a/recbole/model/general_recommender/bpr.py +++ b/recbole/model/general_recommender/bpr.py @@ -25,16 +25,14 @@ class BPR(GeneralRecommender): - r"""BPR is a basic matrix factorization model that be trained in the pairwise way. - - """ + r"""BPR is a basic matrix factorization model that be trained in the pairwise way.""" input_type = InputType.PAIRWISE def __init__(self, config, dataset): super(BPR, self).__init__(config, dataset) # load parameters info - self.embedding_size = config['embedding_size'] + self.embedding_size = config["embedding_size"] # define layers and loss self.user_embedding = nn.Embedding(self.n_users, self.embedding_size) @@ -45,7 +43,7 @@ def __init__(self, config, dataset): self.apply(xavier_normal_initialization) def get_user_embedding(self, user): - r""" Get a batch of user embedding tensor according to input user's id. + r"""Get a batch of user embedding tensor according to input user's id. Args: user (torch.LongTensor): The input tensor that contains user's id, shape: [batch_size, ] @@ -56,7 +54,7 @@ def get_user_embedding(self, user): return self.user_embedding(user) def get_item_embedding(self, item): - r""" Get a batch of item embedding tensor according to input item's id. + r"""Get a batch of item embedding tensor according to input item's id. Args: item (torch.LongTensor): The input tensor that contains item's id, shape: [batch_size, ] @@ -78,7 +76,9 @@ def calculate_loss(self, interaction): user_e, pos_e = self.forward(user, pos_item) neg_e = self.get_item_embedding(neg_item) - pos_item_score, neg_item_score = torch.mul(user_e, pos_e).sum(dim=1), torch.mul(user_e, neg_e).sum(dim=1) + pos_item_score, neg_item_score = torch.mul(user_e, pos_e).sum(dim=1), torch.mul( + user_e, neg_e + ).sum(dim=1) loss = self.loss(pos_item_score, neg_item_score) return loss diff --git a/recbole/model/general_recommender/cdae.py b/recbole/model/general_recommender/cdae.py index 938498839..9b0c8542f 100644 --- a/recbole/model/general_recommender/cdae.py +++ b/recbole/model/general_recommender/cdae.py @@ -22,7 +22,7 @@ class CDAE(GeneralRecommender): - r"""Collaborative Denoising Auto-Encoder (CDAE) is a recommendation model + r"""Collaborative Denoising Auto-Encoder (CDAE) is a recommendation model for top-N recommendation that utilizes the idea of Denoising Auto-Encoders. We implement the the CDAE model with only user dataloader. """ @@ -31,33 +31,33 @@ class CDAE(GeneralRecommender): def __init__(self, config, dataset): super(CDAE, self).__init__(config, dataset) - self.reg_weight_1 = config['reg_weight_1'] - self.reg_weight_2 = config['reg_weight_2'] - self.loss_type = config['loss_type'] - self.hid_activation = config['hid_activation'] - self.out_activation = config['out_activation'] - self.embedding_size = config['embedding_size'] - self.corruption_ratio = config['corruption_ratio'] + self.reg_weight_1 = config["reg_weight_1"] + self.reg_weight_2 = config["reg_weight_2"] + self.loss_type = config["loss_type"] + self.hid_activation = config["hid_activation"] + self.out_activation = config["out_activation"] + self.embedding_size = config["embedding_size"] + self.corruption_ratio = config["corruption_ratio"] self.history_item_id, self.history_item_value, _ = dataset.history_item_matrix() self.history_item_id = self.history_item_id.to(self.device) self.history_item_value = self.history_item_value.to(self.device) - if self.hid_activation == 'sigmoid': + if self.hid_activation == "sigmoid": self.h_act = nn.Sigmoid() - elif self.hid_activation == 'relu': + elif self.hid_activation == "relu": self.h_act = nn.ReLU() - elif self.hid_activation == 'tanh': + elif self.hid_activation == "tanh": self.h_act = nn.Tanh() else: - raise ValueError('Invalid hidden layer activation function') + raise ValueError("Invalid hidden layer activation function") - if self.out_activation == 'sigmoid': + if self.out_activation == "sigmoid": self.o_act = nn.Sigmoid() - elif self.out_activation == 'relu': + elif self.out_activation == "relu": self.o_act = nn.ReLU() else: - raise ValueError('Invalid output layer activation function') + raise ValueError("Invalid output layer activation function") self.dropout = nn.Dropout(p=self.corruption_ratio) @@ -88,10 +88,17 @@ def get_rating_matrix(self, user): """ # Following lines construct tensor of shape [B,n_items] using the tensor of shape [B,H] col_indices = self.history_item_id[user].flatten() - row_indices = torch.arange(user.shape[0]).to(self.device) \ + row_indices = ( + torch.arange(user.shape[0]) + .to(self.device) .repeat_interleave(self.history_item_id.shape[1], dim=0) - rating_matrix = torch.zeros(1).to(self.device).repeat(user.shape[0], self.n_items) - rating_matrix.index_put_((row_indices, col_indices), self.history_item_value[user].flatten()) + ) + rating_matrix = ( + torch.zeros(1).to(self.device).repeat(user.shape[0], self.n_items) + ) + rating_matrix.index_put_( + (row_indices, col_indices), self.history_item_value[user].flatten() + ) return rating_matrix def calculate_loss(self, interaction): @@ -99,18 +106,22 @@ def calculate_loss(self, interaction): x_items = self.get_rating_matrix(x_users) predict = self.forward(x_items, x_users) - if self.loss_type == 'MSE': - predict=self.o_act(predict) - loss_func = nn.MSELoss(reduction='sum') - elif self.loss_type == 'BCE': - loss_func = nn.BCEWithLogitsLoss(reduction='sum') + if self.loss_type == "MSE": + predict = self.o_act(predict) + loss_func = nn.MSELoss(reduction="sum") + elif self.loss_type == "BCE": + loss_func = nn.BCEWithLogitsLoss(reduction="sum") else: - raise ValueError('Invalid loss_type, loss_type must in [MSE, BCE]') + raise ValueError("Invalid loss_type, loss_type must in [MSE, BCE]") loss = loss_func(predict, x_items) # l1-regularization - loss += self.reg_weight_1 * (self.h_user.weight.norm(p=1) + self.h_item.weight.norm(p=1)) + loss += self.reg_weight_1 * ( + self.h_user.weight.norm(p=1) + self.h_item.weight.norm(p=1) + ) # l2-regularization - loss += self.reg_weight_2 * (self.h_user.weight.norm() + self.h_item.weight.norm()) + loss += self.reg_weight_2 * ( + self.h_user.weight.norm() + self.h_item.weight.norm() + ) return loss @@ -120,7 +131,7 @@ def predict(self, interaction): items = self.get_rating_matrix(users) scores = self.forward(items, users) - scores=self.o_act(scores) + scores = self.o_act(scores) return scores[[torch.arange(len(predict_items)).to(self.device), predict_items]] def full_sort_predict(self, interaction): @@ -128,5 +139,5 @@ def full_sort_predict(self, interaction): items = self.get_rating_matrix(users) predict = self.forward(items, users) - predict=self.o_act(predict) + predict = self.o_act(predict) return predict.view(-1) diff --git a/recbole/model/general_recommender/convncf.py b/recbole/model/general_recommender/convncf.py index 7af96a01a..c9683872a 100644 --- a/recbole/model/general_recommender/convncf.py +++ b/recbole/model/general_recommender/convncf.py @@ -22,8 +22,8 @@ class ConvNCFBPRLoss(nn.Module): - """ ConvNCFBPRLoss, based on Bayesian Personalized Ranking, - + """ConvNCFBPRLoss, based on Bayesian Personalized Ranking, + Shape: - Pos_score: (N) - Neg_score: (N), same shape as the Pos_score @@ -49,7 +49,7 @@ def forward(self, pos_score, neg_score): class ConvNCF(GeneralRecommender): r"""ConvNCF is a a new neural network framework for collaborative filtering based on NCF. - It uses an outer product operation above the embedding layer, + It uses an outer product operation above the embedding layer, which results in a semantic-rich interaction map that encodes pairwise correlations between embedding dimensions. We carefully design the data interface and use sparse tensor to train and test efficiently. We implement the model following the original author with a pairwise training mode. @@ -60,21 +60,25 @@ def __init__(self, config, dataset): super(ConvNCF, self).__init__(config, dataset) # load dataset info - self.LABEL = config['LABEL_FIELD'] + self.LABEL = config["LABEL_FIELD"] # load parameters info - self.embedding_size = config['embedding_size'] - self.cnn_channels = config['cnn_channels'] - self.cnn_kernels = config['cnn_kernels'] - self.cnn_strides = config['cnn_strides'] - self.dropout_prob = config['dropout_prob'] - self.regs = config['reg_weights'] + self.embedding_size = config["embedding_size"] + self.cnn_channels = config["cnn_channels"] + self.cnn_kernels = config["cnn_kernels"] + self.cnn_strides = config["cnn_strides"] + self.dropout_prob = config["dropout_prob"] + self.regs = config["reg_weights"] # define layers and loss self.user_embedding = nn.Embedding(self.n_users, self.embedding_size) self.item_embedding = nn.Embedding(self.n_items, self.embedding_size) - self.cnn_layers = CNNLayers(self.cnn_channels, self.cnn_kernels, self.cnn_strides, activation='relu') - self.predict_layers = MLPLayers([self.cnn_channels[-1], 1], self.dropout_prob, activation='none') + self.cnn_layers = CNNLayers( + self.cnn_channels, self.cnn_kernels, self.cnn_strides, activation="relu" + ) + self.predict_layers = MLPLayers( + [self.cnn_channels[-1], 1], self.dropout_prob, activation="none" + ) self.loss = ConvNCFBPRLoss() def forward(self, user, item): @@ -104,10 +108,10 @@ def reg_loss(self): loss_2 = reg_1 * self.item_embedding.weight.norm(2) loss_3 = 0 for name, parm in self.cnn_layers.named_parameters(): - if name.endswith('weight'): + if name.endswith("weight"): loss_3 = loss_3 + reg_2 * parm.norm(2) for name, parm in self.predict_layers.named_parameters(): - if name.endswith('weight'): + if name.endswith("weight"): loss_3 = loss_3 + reg_2 * parm.norm(2) return loss_1 + loss_2 + loss_3 diff --git a/recbole/model/general_recommender/dgcf.py b/recbole/model/general_recommender/dgcf.py index b6399bb57..d4a74f061 100644 --- a/recbole/model/general_recommender/dgcf.py +++ b/recbole/model/general_recommender/dgcf.py @@ -65,16 +65,16 @@ def __init__(self, config, dataset): super(DGCF, self).__init__(config, dataset) # load dataset info - self.interaction_matrix = dataset.inter_matrix(form='coo').astype(np.float32) + self.interaction_matrix = dataset.inter_matrix(form="coo").astype(np.float32) # load parameters info - self.embedding_size = config['embedding_size'] - self.n_factors = config['n_factors'] - self.n_iterations = config['n_iterations'] - self.n_layers = config['n_layers'] - self.reg_weight = config['reg_weight'] - self.cor_weight = config['cor_weight'] - n_batch = dataset.inter_num // config['train_batch_size'] + 1 + self.embedding_size = config["embedding_size"] + self.n_factors = config["n_factors"] + self.n_iterations = config["n_iterations"] + self.n_layers = config["n_layers"] + self.reg_weight = config["reg_weight"] + self.cor_weight = config["cor_weight"] + n_batch = dataset.inter_num // config["train_batch_size"] + 1 self.cor_batch_size = int(max(self.n_users / n_batch, self.n_items / n_batch)) # ensure embedding can be divided into intent assert self.embedding_size % self.n_factors == 0 @@ -94,9 +94,15 @@ def __init__(self, config, dataset): self.tail2edge = torch.LongTensor([edge_ids, all_t_list]).to(self.device) val_one = torch.ones_like(self.all_h_list).float().to(self.device) num_node = self.n_users + self.n_items - self.edge2head_mat = self._build_sparse_tensor(self.edge2head, val_one, (num_node, num_edge)) - self.head2edge_mat = self._build_sparse_tensor(self.head2edge, val_one, (num_edge, num_node)) - self.tail2edge_mat = self._build_sparse_tensor(self.tail2edge, val_one, (num_edge, num_node)) + self.edge2head_mat = self._build_sparse_tensor( + self.edge2head, val_one, (num_node, num_edge) + ) + self.head2edge_mat = self._build_sparse_tensor( + self.head2edge, val_one, (num_edge, num_node) + ) + self.tail2edge_mat = self._build_sparse_tensor( + self.tail2edge, val_one, (num_edge, num_node) + ) self.num_edge = num_edge self.num_node = num_node @@ -109,7 +115,7 @@ def __init__(self, config, dataset): self.restore_user_e = None self.restore_item_e = None - self.other_parameter_name = ['restore_user_e', 'restore_item_e'] + self.other_parameter_name = ["restore_user_e", "restore_item_e"] # parameters initialization self.apply(xavier_normal_initialization) @@ -182,7 +188,9 @@ def forward(self): # update the embeddings via simplified graph convolution layer edge_weight = factor_edge_weight[i] # (num_edge, 1) - edge_val = torch.sparse.mm(self.tail2edge_mat, ego_layer_embeddings[i]) + edge_val = torch.sparse.mm( + self.tail2edge_mat, ego_layer_embeddings[i] + ) # (num_edge, dim / n_factors) edge_val = edge_val * edge_weight # (num_edge, dim / n_factors) @@ -197,19 +205,29 @@ def forward(self): # get the factor-wise embeddings # .... head_factor_embeddings is a dense tensor with the size of [all_h_list, embed_size/n_factors] # .... analogous to tail_factor_embeddings - head_factor_embeddings = torch.index_select(factor_embeddings, dim=0, index=self.all_h_list) - tail_factor_embeddings = torch.index_select(ego_layer_embeddings[i], dim=0, index=self.all_t_list) + head_factor_embeddings = torch.index_select( + factor_embeddings, dim=0, index=self.all_h_list + ) + tail_factor_embeddings = torch.index_select( + ego_layer_embeddings[i], dim=0, index=self.all_t_list + ) # .... constrain the vector length # .... make the following attentive weights within the range of (0,1) # to adapt to torch version - head_factor_embeddings = F.normalize(head_factor_embeddings, p=2, dim=1) - tail_factor_embeddings = F.normalize(tail_factor_embeddings, p=2, dim=1) + head_factor_embeddings = F.normalize( + head_factor_embeddings, p=2, dim=1 + ) + tail_factor_embeddings = F.normalize( + tail_factor_embeddings, p=2, dim=1 + ) # get the attentive weights # .... A_factor_values is a dense tensor with the size of [num_edge, 1] A_factor_values = torch.sum( - head_factor_embeddings * torch.tanh(tail_factor_embeddings), dim=1, keepdim=True + head_factor_embeddings * torch.tanh(tail_factor_embeddings), + dim=1, + keepdim=True, ) # update the attentive weights @@ -231,8 +249,8 @@ def forward(self): all_embeddings = torch.mean(all_embeddings, dim=1, keepdim=False) # (num_node, embedding_size) - u_g_embeddings = all_embeddings[:self.n_users, :] - i_g_embeddings = all_embeddings[self.n_users:, :] + u_g_embeddings = all_embeddings[: self.n_users, :] + i_g_embeddings = all_embeddings[self.n_users :, :] return u_g_embeddings, i_g_embeddings @@ -258,10 +276,14 @@ def calculate_loss(self, interaction): u_ego_embeddings = self.user_embedding(user) pos_ego_embeddings = self.item_embedding(pos_item) neg_ego_embeddings = self.item_embedding(neg_item) - reg_loss = self.reg_loss(u_ego_embeddings, pos_ego_embeddings, neg_ego_embeddings) + reg_loss = self.reg_loss( + u_ego_embeddings, pos_ego_embeddings, neg_ego_embeddings + ) if self.n_factors > 1 and self.cor_weight > 1e-9: - cor_users, cor_items = sample_cor_samples(self.n_users, self.n_items, self.cor_batch_size) + cor_users, cor_items = sample_cor_samples( + self.n_users, self.n_items, self.cor_batch_size + ) cor_users = torch.LongTensor(cor_users).to(self.device) cor_items = torch.LongTensor(cor_items).to(self.device) cor_u_embeddings = user_all_embeddings[cor_users] @@ -297,17 +319,16 @@ def create_cor_loss(self, cor_u_embeddings, cor_i_embeddings): else: cor_loss += self._create_distance_correlation(x, y) - cor_loss /= ((self.n_factors + 1.0) * self.n_factors / 2) + cor_loss /= (self.n_factors + 1.0) * self.n_factors / 2 return cor_loss def _create_distance_correlation(self, X1, X2): - def _create_centered_distance(X): - ''' + """ X: (batch_size, dim) return: X - E(X) - ''' + """ # calculate the pairwise distance of X # .... A with the size of [batch_size, embed_size/n_factors] # .... D with the size of [batch_size, batch_size] @@ -322,7 +343,12 @@ def _create_centered_distance(X): # # calculate the centered distance of X # # .... D with the size of [batch_size, batch_size] # matrix - average over row - average over col + average over matrix - D = D - torch.mean(D, dim=0, keepdim=True) - torch.mean(D, dim=1, keepdim=True) + torch.mean(D) + D = ( + D + - torch.mean(D, dim=0, keepdim=True) + - torch.mean(D, dim=1, keepdim=True) + + torch.mean(D) + ) return D def _create_distance_covariance(D1, D2): diff --git a/recbole/model/general_recommender/dmf.py b/recbole/model/general_recommender/dmf.py index 049b199d9..a07cc37ad 100644 --- a/recbole/model/general_recommender/dmf.py +++ b/recbole/model/general_recommender/dmf.py @@ -43,29 +43,53 @@ def __init__(self, config, dataset): super(DMF, self).__init__(config, dataset) # load dataset info - self.LABEL = config['LABEL_FIELD'] - self.RATING = config['RATING_FIELD'] + self.LABEL = config["LABEL_FIELD"] + self.RATING = config["RATING_FIELD"] # load parameters info - self.user_embedding_size = config['user_embedding_size'] - self.item_embedding_size = config['item_embedding_size'] - self.user_hidden_size_list = config['user_hidden_size_list'] - self.item_hidden_size_list = config['item_hidden_size_list'] + self.user_embedding_size = config["user_embedding_size"] + self.item_embedding_size = config["item_embedding_size"] + self.user_hidden_size_list = config["user_hidden_size_list"] + self.item_hidden_size_list = config["item_hidden_size_list"] # The dimensions of the last layer of users and items must be the same assert self.user_hidden_size_list[-1] == self.item_hidden_size_list[-1] - self.inter_matrix_type = config['inter_matrix_type'] + self.inter_matrix_type = config["inter_matrix_type"] # generate intermediate data - if self.inter_matrix_type == '01': - self.history_user_id, self.history_user_value, _ = dataset.history_user_matrix() - self.history_item_id, self.history_item_value, _ = dataset.history_item_matrix() - self.interaction_matrix = dataset.inter_matrix(form='csr').astype(np.float32) - elif self.inter_matrix_type == 'rating': - self.history_user_id, self.history_user_value, _ = dataset.history_user_matrix(value_field=self.RATING) - self.history_item_id, self.history_item_value, _ = dataset.history_item_matrix(value_field=self.RATING) - self.interaction_matrix = dataset.inter_matrix(form='csr', value_field=self.RATING).astype(np.float32) + if self.inter_matrix_type == "01": + ( + self.history_user_id, + self.history_user_value, + _, + ) = dataset.history_user_matrix() + ( + self.history_item_id, + self.history_item_value, + _, + ) = dataset.history_item_matrix() + self.interaction_matrix = dataset.inter_matrix(form="csr").astype( + np.float32 + ) + elif self.inter_matrix_type == "rating": + ( + self.history_user_id, + self.history_user_value, + _, + ) = dataset.history_user_matrix(value_field=self.RATING) + ( + self.history_item_id, + self.history_item_value, + _, + ) = dataset.history_item_matrix(value_field=self.RATING) + self.interaction_matrix = dataset.inter_matrix( + form="csr", value_field=self.RATING + ).astype(np.float32) else: - raise ValueError("The inter_matrix_type must in ['01', 'rating'] but get {}".format(self.inter_matrix_type)) + raise ValueError( + "The inter_matrix_type must in ['01', 'rating'] but get {}".format( + self.inter_matrix_type + ) + ) self.max_rating = self.history_user_value.max() # tensor of shape [n_items, H] where H is max length of history interaction. self.history_user_id = self.history_user_id.to(self.device) @@ -74,10 +98,18 @@ def __init__(self, config, dataset): self.history_item_value = self.history_item_value.to(self.device) # define layers - self.user_linear = nn.Linear(in_features=self.n_items, out_features=self.user_embedding_size, bias=False) - self.item_linear = nn.Linear(in_features=self.n_users, out_features=self.item_embedding_size, bias=False) - self.user_fc_layers = MLPLayers([self.user_embedding_size] + self.user_hidden_size_list) - self.item_fc_layers = MLPLayers([self.item_embedding_size] + self.item_hidden_size_list) + self.user_linear = nn.Linear( + in_features=self.n_items, out_features=self.user_embedding_size, bias=False + ) + self.item_linear = nn.Linear( + in_features=self.n_users, out_features=self.item_embedding_size, bias=False + ) + self.user_fc_layers = MLPLayers( + [self.user_embedding_size] + self.user_hidden_size_list + ) + self.item_fc_layers = MLPLayers( + [self.item_embedding_size] + self.item_hidden_size_list + ) self.sigmoid = nn.Sigmoid() self.bce_loss = nn.BCEWithLogitsLoss() @@ -86,7 +118,7 @@ def __init__(self, config, dataset): # parameters initialization self.apply(self._init_weights) - self.other_parameter_name = ['i_embedding'] + self.other_parameter_name = ["i_embedding"] def _init_weights(self, module): # We just initialize the module with normal distribution as the paper said @@ -103,10 +135,15 @@ def forward(self, user, item): # Following lines construct tensor of shape [B,n_users] using the tensor of shape [B,H] col_indices = self.history_user_id[item].flatten() - row_indices = torch.arange(item.shape[0]).to(self.device). \ - repeat_interleave(self.history_user_id.shape[1], dim=0) + row_indices = ( + torch.arange(item.shape[0]) + .to(self.device) + .repeat_interleave(self.history_user_id.shape[1], dim=0) + ) matrix_01 = torch.zeros(1).to(self.device).repeat(item.shape[0], self.n_users) - matrix_01.index_put_((row_indices, col_indices), self.history_user_value[item].flatten()) + matrix_01.index_put_( + (row_indices, col_indices), self.history_user_value[item].flatten() + ) item = self.item_linear(matrix_01) user = self.user_fc_layers(user) @@ -124,12 +161,12 @@ def calculate_loss(self, interaction): user = interaction[self.USER_ID] item = interaction[self.ITEM_ID] - if self.inter_matrix_type == '01': + if self.inter_matrix_type == "01": label = interaction[self.LABEL] - elif self.inter_matrix_type == 'rating': + elif self.inter_matrix_type == "rating": label = interaction[self.RATING] * interaction[self.LABEL] output = self.forward(user, item) - + label = label / self.max_rating # normalize the label to calculate BCE loss. loss = self.bce_loss(output, label) return loss @@ -137,7 +174,7 @@ def calculate_loss(self, interaction): def predict(self, interaction): user = interaction[self.USER_ID] item = interaction[self.ITEM_ID] - predict=self.sigmoid(self.forward(user, item)) + predict = self.sigmoid(self.forward(user, item)) return predict def get_user_embedding(self, user): @@ -152,9 +189,13 @@ def get_user_embedding(self, user): # Following lines construct tensor of shape [B,n_items] using the tensor of shape [B,H] col_indices = self.history_item_id[user].flatten() row_indices = torch.arange(user.shape[0]).to(self.device) - row_indices = row_indices.repeat_interleave(self.history_item_id.shape[1], dim=0) + row_indices = row_indices.repeat_interleave( + self.history_item_id.shape[1], dim=0 + ) matrix_01 = torch.zeros(1).to(self.device).repeat(user.shape[0], self.n_items) - matrix_01.index_put_((row_indices, col_indices), self.history_item_value[user].flatten()) + matrix_01.index_put_( + (row_indices, col_indices), self.history_item_value[user].flatten() + ) user = self.user_linear(matrix_01) return user @@ -172,8 +213,11 @@ def get_item_embedding(self): col = interaction_matrix.col i = torch.LongTensor([row, col]) data = torch.FloatTensor(interaction_matrix.data) - item_matrix = torch.sparse.FloatTensor(i, data, torch.Size(interaction_matrix.shape)).to(self.device).\ - transpose(0, 1) + item_matrix = ( + torch.sparse.FloatTensor(i, data, torch.Size(interaction_matrix.shape)) + .to(self.device) + .transpose(0, 1) + ) item = torch.sparse.mm(item_matrix, self.item_linear.weight.t()) item = self.item_fc_layers(item) diff --git a/recbole/model/general_recommender/ease.py b/recbole/model/general_recommender/ease.py index d5c6d17eb..2d7a93192 100644 --- a/recbole/model/general_recommender/ease.py +++ b/recbole/model/general_recommender/ease.py @@ -25,12 +25,12 @@ def __init__(self, config, dataset): super().__init__(config, dataset) # load parameters info - reg_weight = config['reg_weight'] + reg_weight = config["reg_weight"] # need at least one param self.dummy_param = torch.nn.Parameter(torch.zeros(1)) - X = dataset.inter_matrix(form='csr').astype(np.float32) + X = dataset.inter_matrix(form="csr").astype(np.float32) # just directly calculate the entire score matrix in init # (can't be done incrementally) @@ -47,7 +47,7 @@ def __init__(self, config, dataset): P = np.linalg.inv(G) B = P / (-np.diag(P)) # zero out diag - np.fill_diagonal(B, 0.) + np.fill_diagonal(B, 0.0) # instead of computing and storing the entire score matrix, # just store B and compute the scores on demand @@ -61,7 +61,7 @@ def __init__(self, config, dataset): # so will do everything with np/scipy self.item_similarity = B self.interaction_matrix = X - self.other_parameter_name = ['interaction_matrix', 'item_similarity'] + self.other_parameter_name = ["interaction_matrix", "item_similarity"] def forward(self): pass @@ -74,7 +74,9 @@ def predict(self, interaction): item = interaction[self.ITEM_ID].cpu().numpy() return torch.from_numpy( - (self.interaction_matrix[user, :].multiply(self.item_similarity[:, item].T)).sum(axis=1).getA1() + (self.interaction_matrix[user, :].multiply(self.item_similarity[:, item].T)) + .sum(axis=1) + .getA1() ) def full_sort_predict(self, interaction): diff --git a/recbole/model/general_recommender/enmf.py b/recbole/model/general_recommender/enmf.py index 188bcfe3b..4cfb57602 100644 --- a/recbole/model/general_recommender/enmf.py +++ b/recbole/model/general_recommender/enmf.py @@ -31,18 +31,22 @@ class ENMF(GeneralRecommender): def __init__(self, config, dataset): super(ENMF, self).__init__(config, dataset) - self.embedding_size = config['embedding_size'] - self.dropout_prob = config['dropout_prob'] - self.reg_weight = config['reg_weight'] - self.negative_weight = config['negative_weight'] + self.embedding_size = config["embedding_size"] + self.dropout_prob = config["dropout_prob"] + self.reg_weight = config["reg_weight"] + self.negative_weight = config["negative_weight"] # get all users' history interaction information. # matrix is padding by the maximum number of a user's interactions self.history_item_matrix, _, self.history_lens = dataset.history_item_matrix() self.history_item_matrix = self.history_item_matrix.to(self.device) - self.user_embedding = nn.Embedding(self.n_users, self.embedding_size, padding_idx=0) - self.item_embedding = nn.Embedding(self.n_items, self.embedding_size, padding_idx=0) + self.user_embedding = nn.Embedding( + self.n_users, self.embedding_size, padding_idx=0 + ) + self.item_embedding = nn.Embedding( + self.n_items, self.embedding_size, padding_idx=0 + ) self.H_i = nn.Linear(self.embedding_size, 1, bias=False) self.dropout = nn.Dropout(self.dropout_prob) @@ -65,8 +69,12 @@ def forward(self, user): user_embedding = self.dropout(user_embedding) # shape:[B, embedding_size] user_inter = self.history_item_matrix[user] # shape :[B, max_len] - item_embedding = self.item_embedding(user_inter) # shape: [B, max_len, embedding_size] - score = torch.mul(user_embedding.unsqueeze(1), item_embedding) # shape: [B, max_len, embedding_size] + item_embedding = self.item_embedding( + user_inter + ) # shape: [B, max_len, embedding_size] + score = torch.mul( + user_embedding.unsqueeze(1), item_embedding + ) # shape: [B, max_len, embedding_size] score = self.H_i(score) # shape: [B,max_len,1] score = score.squeeze(-1) # shape:[B,max_len] @@ -78,13 +86,16 @@ def calculate_loss(self, interaction): pos_score = self.forward(user) # shape: [embedding_size, embedding_size] - item_sum = torch.bmm(self.item_embedding.weight.unsqueeze(2), - self.item_embedding.weight.unsqueeze(1)).sum(dim=0) + item_sum = torch.bmm( + self.item_embedding.weight.unsqueeze(2), + self.item_embedding.weight.unsqueeze(1), + ).sum(dim=0) # shape: [embedding_size, embedding_size] batch_user = self.user_embedding(user) - user_sum = torch.bmm(batch_user.unsqueeze(2), - batch_user.unsqueeze(1)).sum(dim=0) + user_sum = torch.bmm(batch_user.unsqueeze(2), batch_user.unsqueeze(1)).sum( + dim=0 + ) # shape: [embedding_size, embedding_size] H_sum = torch.matmul(self.H_i.weight.t(), self.H_i.weight) @@ -93,7 +104,9 @@ def calculate_loss(self, interaction): loss = self.negative_weight * t - loss = loss + torch.sum((1 - self.negative_weight) * torch.square(pos_score) - 2 * pos_score) + loss = loss + torch.sum( + (1 - self.negative_weight) * torch.square(pos_score) - 2 * pos_score + ) loss = loss + self.reg_loss() @@ -118,7 +131,9 @@ def full_sort_predict(self, interaction): all_i_e = self.item_embedding.weight # shape: [n_item,embedding_dim] - score = torch.mul(u_e.unsqueeze(1), all_i_e.unsqueeze(0)) # shape: [B, n_item, embedding_dim] + score = torch.mul( + u_e.unsqueeze(1), all_i_e.unsqueeze(0) + ) # shape: [B, n_item, embedding_dim] score = self.H_i(score).squeeze(2) # shape: [B, n_item] diff --git a/recbole/model/general_recommender/fism.py b/recbole/model/general_recommender/fism.py index 9199d0bdc..3d64c8ba1 100644 --- a/recbole/model/general_recommender/fism.py +++ b/recbole/model/general_recommender/fism.py @@ -25,38 +25,51 @@ class FISM(GeneralRecommender): """FISM is an item-based model for generating top-N recommendations that learns the item-item similarity matrix as the product of two low dimensional latent factor matrices. These matrices are learned using a structural equation modeling approach, where in the - value being estimated is not used for its own estimation. + value being estimated is not used for its own estimation. """ + input_type = InputType.POINTWISE def __init__(self, config, dataset): super(FISM, self).__init__(config, dataset) # load dataset info - self.LABEL = config['LABEL_FIELD'] + self.LABEL = config["LABEL_FIELD"] # get all users' history interaction information.the history item # matrix is padding by the maximum number of a user's interactions - self.history_item_matrix, self.history_lens, self.mask_mat = self.get_history_info(dataset) + ( + self.history_item_matrix, + self.history_lens, + self.mask_mat, + ) = self.get_history_info(dataset) # load parameters info - self.embedding_size = config['embedding_size'] - self.reg_weights = config['reg_weights'] - self.alpha = config['alpha'] - self.split_to = config['split_to'] + self.embedding_size = config["embedding_size"] + self.reg_weights = config["reg_weights"] + self.alpha = config["alpha"] + self.split_to = config["split_to"] # split the too large dataset into the specified pieces if self.split_to > 0: - self.group = torch.chunk(torch.arange(self.n_items).to(self.device), self.split_to) + self.group = torch.chunk( + torch.arange(self.n_items).to(self.device), self.split_to + ) else: - self.logger.warning('Pay Attetion!! the `split_to` is set to 0. If you catch a OMM error in this case, ' + \ - 'you need to increase it \n\t\t\tuntil the error disappears. For example, ' + \ - 'you can append it in the command line such as `--split_to=5`') + self.logger.warning( + "Pay Attetion!! the `split_to` is set to 0. If you catch a OMM error in this case, " + + "you need to increase it \n\t\t\tuntil the error disappears. For example, " + + "you can append it in the command line such as `--split_to=5`" + ) # define layers and loss # construct source and destination item embedding matrix - self.item_src_embedding = nn.Embedding(self.n_items, self.embedding_size, padding_idx=0) - self.item_dst_embedding = nn.Embedding(self.n_items, self.embedding_size, padding_idx=0) + self.item_src_embedding = nn.Embedding( + self.n_items, self.embedding_size, padding_idx=0 + ) + self.item_dst_embedding = nn.Embedding( + self.n_items, self.embedding_size, padding_idx=0 + ) self.user_bias = nn.Parameter(torch.zeros(self.n_users)) self.item_bias = nn.Parameter(torch.zeros(self.n_items)) self.bceloss = nn.BCEWithLogitsLoss() @@ -106,23 +119,29 @@ def _init_weights(self, module): normal_(module.weight.data, 0, 0.01) def inter_forward(self, user, item): - """forward the model by interaction - - """ + """forward the model by interaction""" user_inter = self.history_item_matrix[user] item_num = self.history_lens[user].unsqueeze(1) batch_mask_mat = self.mask_mat[user] - user_history = self.item_src_embedding(user_inter) # batch_size x max_len x embedding_size + user_history = self.item_src_embedding( + user_inter + ) # batch_size x max_len x embedding_size target = self.item_dst_embedding(item) # batch_size x embedding_size user_bias = self.user_bias[user] # batch_size x 1 item_bias = self.item_bias[item] - similarity = torch.bmm(user_history, target.unsqueeze(2)).squeeze(2) # batch_size x max_len + similarity = torch.bmm(user_history, target.unsqueeze(2)).squeeze( + 2 + ) # batch_size x max_len similarity = batch_mask_mat * similarity coeff = torch.pow(item_num.squeeze(1), -self.alpha) - scores = torch.sigmoid(coeff.float() * torch.sum(similarity, dim=1) + user_bias + item_bias) + scores = torch.sigmoid( + coeff.float() * torch.sum(similarity, dim=1) + user_bias + item_bias + ) return scores - def user_forward(self, user_input, item_num, user_bias, repeats=None, pred_slc=None): + def user_forward( + self, user_input, item_num, user_bias, repeats=None, pred_slc=None + ): """forward the model by user Args: @@ -138,14 +157,18 @@ def user_forward(self, user_input, item_num, user_bias, repeats=None, pred_slc=N """ item_num = item_num.repeat(repeats, 1) user_history = self.item_src_embedding(user_input) # inter_num x embedding_size - user_history = user_history.repeat(repeats, 1, 1) # target_items x inter_num x embedding_size + user_history = user_history.repeat( + repeats, 1, 1 + ) # target_items x inter_num x embedding_size if pred_slc is None: targets = self.item_dst_embedding.weight # target_items x embedding_size item_bias = self.item_bias else: targets = self.item_dst_embedding(pred_slc) item_bias = self.item_bias[pred_slc] - similarity = torch.bmm(user_history, targets.unsqueeze(2)).squeeze(2) # inter_num x target_items + similarity = torch.bmm(user_history, targets.unsqueeze(2)).squeeze( + 2 + ) # inter_num x target_items coeff = torch.pow(item_num.squeeze(1), -self.alpha) scores = coeff.float() * torch.sum(similarity, dim=1) + user_bias + item_bias return scores @@ -169,14 +192,22 @@ def full_sort_predict(self, interaction): scores = [] # test users one by one, if the number of items is too large, we will split it to some pieces - for user_input, item_num, user_bias in zip(user_inters, item_nums.unsqueeze(1), batch_user_bias): + for user_input, item_num, user_bias in zip( + user_inters, item_nums.unsqueeze(1), batch_user_bias + ): if self.split_to <= 0: - output = self.user_forward(user_input[:item_num], item_num, user_bias, repeats=self.n_items) + output = self.user_forward( + user_input[:item_num], item_num, user_bias, repeats=self.n_items + ) else: output = [] for mask in self.group: tmp_output = self.user_forward( - user_input[:item_num], item_num, user_bias, repeats=len(mask), pred_slc=mask + user_input[:item_num], + item_num, + user_bias, + repeats=len(mask), + pred_slc=mask, ) output.append(tmp_output) output = torch.cat(output, dim=0) diff --git a/recbole/model/general_recommender/gcmc.py b/recbole/model/general_recommender/gcmc.py index e3715493d..f52914686 100644 --- a/recbole/model/general_recommender/gcmc.py +++ b/recbole/model/general_recommender/gcmc.py @@ -34,14 +34,14 @@ class GCMC(GeneralRecommender): r"""GCMC is a model that incorporate graph autoencoders for recommendation. - Graph autoencoders are comprised of: + Graph autoencoders are comprised of: - 1) a graph encoder model :math:`Z = f(X; A)`, which take as input an :math:`N \times D` feature matrix X and + 1) a graph encoder model :math:`Z = f(X; A)`, which take as input an :math:`N \times D` feature matrix X and a graph adjacency matrix A, and produce an :math:`N \times E` node embedding matrix :math:`Z = [z_1^T,..., z_N^T ]^T`; - 2) a pairwise decoder model :math:`\hat A = g(Z)`, which takes pairs of node embeddings :math:`(z_i, z_j)` and - predicts respective entries :math:`\hat A_{ij}` in the adjacency matrix. + 2) a pairwise decoder model :math:`\hat A = g(Z)`, which takes pairs of node embeddings :math:`(z_i, z_j)` and + predicts respective entries :math:`\hat A_{ij}` in the adjacency matrix. Note that :math:`N` denotes the number of nodes, :math:`D` the number of input features, and :math:`E` the embedding size. @@ -55,15 +55,17 @@ def __init__(self, config, dataset): # load dataset info self.num_all = self.n_users + self.n_items - self.interaction_matrix = dataset.inter_matrix(form='coo').astype(np.float32) # csr + self.interaction_matrix = dataset.inter_matrix(form="coo").astype( + np.float32 + ) # csr # load parameters info - self.dropout_prob = config['dropout_prob'] - self.sparse_feature = config['sparse_feature'] - self.gcn_output_dim = config['gcn_output_dim'] - self.dense_output_dim = config['embedding_size'] - self.n_class = config['class_num'] - self.num_basis_functions = config['num_basis_functions'] + self.dropout_prob = config["dropout_prob"] + self.sparse_feature = config["sparse_feature"] + self.gcn_output_dim = config["gcn_output_dim"] + self.dense_output_dim = config["embedding_size"] + self.n_class = config["class_num"] + self.num_basis_functions = config["num_basis_functions"] # generate node feature if self.sparse_feature: @@ -71,16 +73,20 @@ def __init__(self, config, dataset): i = features._indices() v = features._values() self.user_features = torch.sparse.FloatTensor( - i[:, :self.n_users], v[:self.n_users], torch.Size([self.n_users, self.num_all]) + i[:, : self.n_users], + v[: self.n_users], + torch.Size([self.n_users, self.num_all]), ).to(self.device) - item_i = i[:, self.n_users:] + item_i = i[:, self.n_users :] item_i[0, :] = item_i[0, :] - self.n_users self.item_features = torch.sparse.FloatTensor( - item_i, v[self.n_users:], torch.Size([self.n_items, self.num_all]) + item_i, v[self.n_users :], torch.Size([self.n_items, self.num_all]) ).to(self.device) else: features = torch.eye(self.num_all).to(self.device) - self.user_features, self.item_features = torch.split(features, [self.n_users, self.n_items]) + self.user_features, self.item_features = torch.split( + features, [self.n_users, self.n_items] + ) self.input_dim = self.user_features.shape[1] # adj matrices for each relation are stored in self.support @@ -88,13 +94,13 @@ def __init__(self, config, dataset): self.support = [self.Graph] # accumulation operation - self.accum = config['accum'] - if self.accum == 'stack': + self.accum = config["accum"] + if self.accum == "stack": div = self.gcn_output_dim // len(self.support) if self.gcn_output_dim % len(self.support) != 0: self.logger.warning( - "HIDDEN[0] (=%d) of stack layer is adjusted to %d (in %d splits)." % - (self.gcn_output_dim, len(self.support) * div, len(self.support)) + "HIDDEN[0] (=%d) of stack layer is adjusted to %d (in %d splits)." + % (self.gcn_output_dim, len(self.support) * div, len(self.support)) ) self.gcn_output_dim = len(self.support) * div @@ -109,14 +115,14 @@ def __init__(self, config, dataset): dense_output_dim=self.dense_output_dim, drop_prob=self.dropout_prob, device=self.device, - sparse_feature=self.sparse_feature + sparse_feature=self.sparse_feature, ).to(self.device) self.BiDecoder = BiDecoder( input_dim=self.dense_output_dim, output_dim=self.n_class, - drop_prob=0., + drop_prob=0.0, device=self.device, - num_weights=self.num_basis_functions + num_weights=self.num_basis_functions, ).to(self.device) self.loss_function = nn.CrossEntropyLoss() @@ -148,11 +154,22 @@ def get_norm_adj_mat(self): Sparse tensor of the normalized interaction matrix. """ # build adj matrix - A = sp.dok_matrix((self.n_users + self.n_items, self.n_users + self.n_items), dtype=np.float32) + A = sp.dok_matrix( + (self.n_users + self.n_items, self.n_users + self.n_items), dtype=np.float32 + ) inter_M = self.interaction_matrix inter_M_t = self.interaction_matrix.transpose() - data_dict = dict(zip(zip(inter_M.row, inter_M.col + self.n_users), [1] * inter_M.nnz)) - data_dict.update(dict(zip(zip(inter_M_t.row + self.n_users, inter_M_t.col), [1] * inter_M_t.nnz))) + data_dict = dict( + zip(zip(inter_M.row, inter_M.col + self.n_users), [1] * inter_M.nnz) + ) + data_dict.update( + dict( + zip( + zip(inter_M_t.row + self.n_users, inter_M_t.col), + [1] * inter_M_t.nnz, + ) + ) + ) A._update(data_dict) # norm adj matrix sumArr = (A > 0).sum(axis=1) @@ -187,7 +204,7 @@ def calculate_loss(self, interaction): user_X, item_X = self.user_features, self.item_features predict = self.forward(user_X, item_X, users, items) target = torch.zeros(len(user) * 2, dtype=torch.long).to(self.device) - target[:len(user)] = 1 + target[: len(user)] = 1 loss = self.loss_function(predict, target) return loss @@ -234,7 +251,7 @@ def __init__( sparse_feature=True, act_dense=lambda x: x, share_user_item_weights=True, - bias=False + bias=False, ): super(GcEncoder, self).__init__() self.num_users = num_user @@ -262,56 +279,90 @@ def __init__( self.num_support = len(support) # gcn layer - if self.accum == 'sum': - self.weights_u = nn.ParameterList([ - nn.Parameter( - torch.FloatTensor(self.input_dim, self.gcn_output_dim).to(self.device), requires_grad=True - ) for _ in range(self.num_support) - ]) + if self.accum == "sum": + self.weights_u = nn.ParameterList( + [ + nn.Parameter( + torch.FloatTensor(self.input_dim, self.gcn_output_dim).to( + self.device + ), + requires_grad=True, + ) + for _ in range(self.num_support) + ] + ) if share_user_item_weights: self.weights_v = self.weights_u else: - self.weights_v = nn.ParameterList([ - nn.Parameter( - torch.FloatTensor(self.input_dim, self.gcn_output_dim).to(self.device), requires_grad=True - ) for _ in range(self.num_support) - ]) + self.weights_v = nn.ParameterList( + [ + nn.Parameter( + torch.FloatTensor(self.input_dim, self.gcn_output_dim).to( + self.device + ), + requires_grad=True, + ) + for _ in range(self.num_support) + ] + ) else: - assert self.gcn_output_dim % self.num_support == 0, 'output_dim must be multiple of num_support for stackGC' + assert ( + self.gcn_output_dim % self.num_support == 0 + ), "output_dim must be multiple of num_support for stackGC" self.sub_hidden_dim = self.gcn_output_dim // self.num_support - self.weights_u = nn.ParameterList([ - nn.Parameter( - torch.FloatTensor(self.input_dim, self.sub_hidden_dim).to(self.device), requires_grad=True - ) for _ in range(self.num_support) - ]) + self.weights_u = nn.ParameterList( + [ + nn.Parameter( + torch.FloatTensor(self.input_dim, self.sub_hidden_dim).to( + self.device + ), + requires_grad=True, + ) + for _ in range(self.num_support) + ] + ) if share_user_item_weights: self.weights_v = self.weights_u else: - self.weights_v = nn.ParameterList([ - nn.Parameter( - torch.FloatTensor(self.input_dim, self.sub_hidden_dim).to(self.device), requires_grad=True - ) for _ in range(self.num_support) - ]) + self.weights_v = nn.ParameterList( + [ + nn.Parameter( + torch.FloatTensor(self.input_dim, self.sub_hidden_dim).to( + self.device + ), + requires_grad=True, + ) + for _ in range(self.num_support) + ] + ) # dense layer - self.dense_layer_u = nn.Linear(self.gcn_output_dim, self.dense_output_dim, bias=self.bias) + self.dense_layer_u = nn.Linear( + self.gcn_output_dim, self.dense_output_dim, bias=self.bias + ) if share_user_item_weights: self.dense_layer_v = self.dense_layer_u else: - self.dense_layer_v = nn.Linear(self.gcn_output_dim, self.dense_output_dim, bias=self.bias) + self.dense_layer_v = nn.Linear( + self.gcn_output_dim, self.dense_output_dim, bias=self.bias + ) self._init_weights() def _init_weights(self): - init_range = math.sqrt((self.num_support + 1) / (self.input_dim + self.gcn_output_dim)) + init_range = math.sqrt( + (self.num_support + 1) / (self.input_dim + self.gcn_output_dim) + ) for w in range(self.num_support): self.weights_u[w].data.uniform_(-init_range, init_range) if not self.share_weights: for w in range(self.num_support): self.weights_v[w].data.uniform_(-init_range, init_range) - dense_init_range = math.sqrt((self.num_support + 1) / (self.dense_output_dim + self.gcn_output_dim)) + dense_init_range = math.sqrt( + (self.num_support + 1) / (self.dense_output_dim + self.gcn_output_dim) + ) self.dense_layer_u.weight.data.uniform_(-dense_init_range, dense_init_range) if not self.share_weights: self.dense_layer_v.weight.data.uniform_(-dense_init_range, dense_init_range) @@ -328,9 +379,9 @@ def forward(self, user_X, item_X): item_X = self.sparse_dropout(item_X) embeddings = [] - if self.accum == 'sum': - wu = 0. - wv = 0. + if self.accum == "sum": + wu = 0.0 + wv = 0.0 for i in range(self.num_support): # weight sharing wu = self.weights_u[i] + wu @@ -394,7 +445,9 @@ class BiDecoder(nn.Module): BiDecoder takes pairs of node embeddings and predicts respective entries in the adjacency matrix. """ - def __init__(self, input_dim, output_dim, drop_prob, device, num_weights=3, act=lambda x: x): + def __init__( + self, input_dim, output_dim, drop_prob, device, num_weights=3, act=lambda x: x + ): super(BiDecoder, self).__init__() self.input_dim = input_dim self.output_dim = output_dim @@ -405,14 +458,21 @@ def __init__(self, input_dim, output_dim, drop_prob, device, num_weights=3, act= self.dropout_prob = drop_prob self.dropout = nn.Dropout(p=self.dropout_prob) - self.weights = nn.ParameterList([ - nn.Parameter(orthogonal([self.input_dim, self.input_dim]).to(self.device)) for _ in range(self.num_weights) - ]) + self.weights = nn.ParameterList( + [ + nn.Parameter( + orthogonal([self.input_dim, self.input_dim]).to(self.device) + ) + for _ in range(self.num_weights) + ] + ) self.dense_layer = nn.Linear(self.num_weights, self.output_dim, bias=False) self._init_weights() def _init_weights(self): - dense_init_range = math.sqrt(self.output_dim / (self.num_weights + self.output_dim)) + dense_init_range = math.sqrt( + self.output_dim / (self.num_weights + self.output_dim) + ) self.dense_layer.weight.data.uniform_(-dense_init_range, dense_init_range) def forward(self, u_inputs, i_inputs, users, items=None): @@ -458,4 +518,4 @@ def orthogonal(shape, scale=1.1): # pick the one with the correct shape q = u if u.shape == flat_shape else v q = q.reshape(shape) - return torch.tensor(scale * q[:shape[0], :shape[1]], dtype=torch.float32) + return torch.tensor(scale * q[: shape[0], : shape[1]], dtype=torch.float32) diff --git a/recbole/model/general_recommender/itemknn.py b/recbole/model/general_recommender/itemknn.py index 6026b08a1..1e6b5d55e 100644 --- a/recbole/model/general_recommender/itemknn.py +++ b/recbole/model/general_recommender/itemknn.py @@ -20,7 +20,6 @@ class ComputeSimilarity: - def __init__(self, dataMatrix, topk=100, shrink=0, normalize=True): r"""Computes the cosine similarity of dataMatrix @@ -51,11 +50,11 @@ def compute_similarity(self, method, block_size=100): Args: method (str) : Caculate the similarity of users if method is 'user', otherwise, calculate the similarity of items. block_size (int): divide matrix to :math:`n\_rows \div block\_size` to calculate cosine_distance if method is 'user', - otherwise, divide matrix to :math:`n\_columns \div block\_size`. + otherwise, divide matrix to :math:`n\_columns \div block\_size`. Returns: - list: The similar nodes, if method is 'user', the shape is [number of users, neigh_num], + list: The similar nodes, if method is 'user', the shape is [number of users, neigh_num], else, the shape is [number of items, neigh_num]. scipy.sparse.csr_matrix: sparse matrix W, if method is 'user', the shape is [self.n_rows, self.n_rows], else, the shape is [self.n_columns, self.n_columns]. @@ -69,10 +68,10 @@ def compute_similarity(self, method, block_size=100): self.dataMatrix = self.dataMatrix.astype(np.float32) # Compute sum of squared values to be used in normalization - if method == 'user': + if method == "user": sumOfSquared = np.array(self.dataMatrix.power(2).sum(axis=1)).ravel() end_local = self.n_rows - elif method == 'item': + elif method == "item": sumOfSquared = np.array(self.dataMatrix.power(2).sum(axis=0)).ravel() end_local = self.n_columns else: @@ -88,7 +87,7 @@ def compute_similarity(self, method, block_size=100): this_block_size = end_block - start_block # All data points for a given user or item - if method == 'user': + if method == "user": data = self.dataMatrix[start_block:end_block, :] else: data = self.dataMatrix[:, start_block:end_block] @@ -96,7 +95,7 @@ def compute_similarity(self, method, block_size=100): # Compute similarities - if method == 'user': + if method == "user": this_block_weights = self.dataMatrix.dot(data.T) else: this_block_weights = self.dataMatrix.T.dot(data) @@ -109,7 +108,9 @@ def compute_similarity(self, method, block_size=100): # Apply normalization and shrinkage, ensure denominator != 0 if self.normalize: - denominator = sumOfSquared[Index] * sumOfSquared + self.shrink + 1e-6 + denominator = ( + sumOfSquared[Index] * sumOfSquared + self.shrink + 1e-6 + ) this_line_weights = np.multiply(this_line_weights, 1 / denominator) elif self.shrink != 0: @@ -120,8 +121,12 @@ def compute_similarity(self, method, block_size=100): # - Partition the data to extract the set of relevant users or items # - Sort only the relevant users or items # - Get the original index - relevant_partition = (-this_line_weights).argpartition(self.TopK - 1)[0:self.TopK] - relevant_partition_sorting = np.argsort(-this_line_weights[relevant_partition]) + relevant_partition = (-this_line_weights).argpartition(self.TopK - 1)[ + 0 : self.TopK + ] + relevant_partition_sorting = np.argsort( + -this_line_weights[relevant_partition] + ) top_k_idx = relevant_partition[relevant_partition_sorting] neigh.append(top_k_idx) @@ -130,7 +135,7 @@ def compute_similarity(self, method, block_size=100): numNotZeros = np.sum(notZerosMask) values.extend(this_line_weights[top_k_idx][notZerosMask]) - if method == 'user': + if method == "user": rows.extend(np.ones(numNotZeros) * Index) cols.extend(top_k_idx[notZerosMask]) else: @@ -140,17 +145,23 @@ def compute_similarity(self, method, block_size=100): start_block += block_size # End while - if method == 'user': - W_sparse = sp.csr_matrix((values, (rows, cols)), shape=(self.n_rows, self.n_rows), dtype=np.float32) + if method == "user": + W_sparse = sp.csr_matrix( + (values, (rows, cols)), + shape=(self.n_rows, self.n_rows), + dtype=np.float32, + ) else: - W_sparse = sp.csr_matrix((values, (rows, cols)), shape=(self.n_columns, self.n_columns), dtype=np.float32) + W_sparse = sp.csr_matrix( + (values, (rows, cols)), + shape=(self.n_columns, self.n_columns), + dtype=np.float32, + ) return neigh, W_sparse.tocsc() class ItemKNN(GeneralRecommender): - r"""ItemKNN is a basic model that compute item similarity with the interaction matrix. - - """ + r"""ItemKNN is a basic model that compute item similarity with the interaction matrix.""" input_type = InputType.POINTWISE type = ModelType.TRADITIONAL @@ -158,18 +169,19 @@ def __init__(self, config, dataset): super(ItemKNN, self).__init__(config, dataset) # load parameters info - self.k = config['k'] - self.shrink = config['shrink'] if 'shrink' in config else 0.0 + self.k = config["k"] + self.shrink = config["shrink"] if "shrink" in config else 0.0 - self.interaction_matrix = dataset.inter_matrix(form='csr').astype(np.float32) + self.interaction_matrix = dataset.inter_matrix(form="csr").astype(np.float32) shape = self.interaction_matrix.shape assert self.n_users == shape[0] and self.n_items == shape[1] - _, self.w = ComputeSimilarity(self.interaction_matrix, topk=self.k, - shrink=self.shrink).compute_similarity('item') + _, self.w = ComputeSimilarity( + self.interaction_matrix, topk=self.k, shrink=self.shrink + ).compute_similarity("item") self.pred_mat = self.interaction_matrix.dot(self.w).tolil() self.fake_loss = torch.nn.Parameter(torch.zeros(1)) - self.other_parameter_name = ['w', 'pred_mat'] + self.other_parameter_name = ["w", "pred_mat"] def forward(self, user, item): pass diff --git a/recbole/model/general_recommender/lightgcn.py b/recbole/model/general_recommender/lightgcn.py index 78844ce34..36308b9dd 100644 --- a/recbole/model/general_recommender/lightgcn.py +++ b/recbole/model/general_recommender/lightgcn.py @@ -33,7 +33,7 @@ class LightGCN(GeneralRecommender): r"""LightGCN is a GCN-based recommender model. LightGCN includes only the most essential component in GCN — neighborhood aggregation — for - collaborative filtering. Specifically, LightGCN learns user and item embeddings by linearly + collaborative filtering. Specifically, LightGCN learns user and item embeddings by linearly propagating them on the user-item interaction graph, and uses the weighted sum of the embeddings learned at all layers as the final embedding. @@ -45,17 +45,25 @@ def __init__(self, config, dataset): super(LightGCN, self).__init__(config, dataset) # load dataset info - self.interaction_matrix = dataset.inter_matrix(form='coo').astype(np.float32) + self.interaction_matrix = dataset.inter_matrix(form="coo").astype(np.float32) # load parameters info - self.latent_dim = config['embedding_size'] # int type:the embedding size of lightGCN - self.n_layers = config['n_layers'] # int type:the layer num of lightGCN - self.reg_weight = config['reg_weight'] # float32 type: the weight decay for l2 normalization - self.require_pow = config['require_pow'] + self.latent_dim = config[ + "embedding_size" + ] # int type:the embedding size of lightGCN + self.n_layers = config["n_layers"] # int type:the layer num of lightGCN + self.reg_weight = config[ + "reg_weight" + ] # float32 type: the weight decay for l2 normalization + self.require_pow = config["require_pow"] # define layers and loss - self.user_embedding = torch.nn.Embedding(num_embeddings=self.n_users, embedding_dim=self.latent_dim) - self.item_embedding = torch.nn.Embedding(num_embeddings=self.n_items, embedding_dim=self.latent_dim) + self.user_embedding = torch.nn.Embedding( + num_embeddings=self.n_users, embedding_dim=self.latent_dim + ) + self.item_embedding = torch.nn.Embedding( + num_embeddings=self.n_items, embedding_dim=self.latent_dim + ) self.mf_loss = BPRLoss() self.reg_loss = EmbLoss() @@ -68,7 +76,7 @@ def __init__(self, config, dataset): # parameters initialization self.apply(xavier_uniform_initialization) - self.other_parameter_name = ['restore_user_e', 'restore_item_e'] + self.other_parameter_name = ["restore_user_e", "restore_item_e"] def get_norm_adj_mat(self): r"""Get the normalized interaction matrix of users and items. @@ -83,11 +91,22 @@ def get_norm_adj_mat(self): Sparse tensor of the normalized interaction matrix. """ # build adj matrix - A = sp.dok_matrix((self.n_users + self.n_items, self.n_users + self.n_items), dtype=np.float32) + A = sp.dok_matrix( + (self.n_users + self.n_items, self.n_users + self.n_items), dtype=np.float32 + ) inter_M = self.interaction_matrix inter_M_t = self.interaction_matrix.transpose() - data_dict = dict(zip(zip(inter_M.row, inter_M.col + self.n_users), [1] * inter_M.nnz)) - data_dict.update(dict(zip(zip(inter_M_t.row + self.n_users, inter_M_t.col), [1] * inter_M_t.nnz))) + data_dict = dict( + zip(zip(inter_M.row, inter_M.col + self.n_users), [1] * inter_M.nnz) + ) + data_dict.update( + dict( + zip( + zip(inter_M_t.row + self.n_users, inter_M_t.col), + [1] * inter_M_t.nnz, + ) + ) + ) A._update(data_dict) # norm adj matrix sumArr = (A > 0).sum(axis=1) @@ -126,7 +145,9 @@ def forward(self): lightgcn_all_embeddings = torch.stack(embeddings_list, dim=1) lightgcn_all_embeddings = torch.mean(lightgcn_all_embeddings, dim=1) - user_all_embeddings, item_all_embeddings = torch.split(lightgcn_all_embeddings, [self.n_users, self.n_items]) + user_all_embeddings, item_all_embeddings = torch.split( + lightgcn_all_embeddings, [self.n_users, self.n_items] + ) return user_all_embeddings, item_all_embeddings def calculate_loss(self, interaction): @@ -153,7 +174,12 @@ def calculate_loss(self, interaction): pos_ego_embeddings = self.item_embedding(pos_item) neg_ego_embeddings = self.item_embedding(neg_item) - reg_loss = self.reg_loss(u_ego_embeddings, pos_ego_embeddings, neg_ego_embeddings, require_pow=self.require_pow) + reg_loss = self.reg_loss( + u_ego_embeddings, + pos_ego_embeddings, + neg_ego_embeddings, + require_pow=self.require_pow, + ) loss = mf_loss + self.reg_weight * reg_loss diff --git a/recbole/model/general_recommender/line.py b/recbole/model/general_recommender/line.py index ef08cbfbc..822e56ef1 100644 --- a/recbole/model/general_recommender/line.py +++ b/recbole/model/general_recommender/line.py @@ -25,7 +25,6 @@ class NegSamplingLoss(nn.Module): - def __init__(self): super(NegSamplingLoss, self).__init__() @@ -43,9 +42,9 @@ class LINE(GeneralRecommender): def __init__(self, config, dataset): super(LINE, self).__init__(config, dataset) - self.embedding_size = config['embedding_size'] - self.order = config['order'] - self.second_order_loss_weight = config['second_order_loss_weight'] + self.embedding_size = config["embedding_size"] + self.order = config["order"] + self.second_order_loss_weight = config["second_order_loss_weight"] self.interaction_feat = dataset.inter_feat @@ -53,8 +52,12 @@ def __init__(self, config, dataset): self.item_embedding = nn.Embedding(self.n_items, self.embedding_size) if self.order == 2: - self.user_context_embedding = nn.Embedding(self.n_users, self.embedding_size) - self.item_context_embedding = nn.Embedding(self.n_items, self.embedding_size) + self.user_context_embedding = nn.Embedding( + self.n_users, self.embedding_size + ) + self.item_context_embedding = nn.Embedding( + self.n_items, self.embedding_size + ) self.loss_fct = NegSamplingLoss() @@ -68,7 +71,10 @@ def __init__(self, config, dataset): def get_used_ids(self): cur = np.array([set() for _ in range(self.n_items)]) - for uid, iid in zip(self.interaction_feat[self.USER_ID].numpy(), self.interaction_feat[self.ITEM_ID].numpy()): + for uid, iid in zip( + self.interaction_feat[self.USER_ID].numpy(), + self.interaction_feat[self.ITEM_ID].numpy(), + ): cur[iid].add(uid) return cur @@ -82,10 +88,17 @@ def sampler(self, key_ids): key_ids = np.tile(key_ids, 1) while len(check_list) > 0: value_ids[check_list] = self.random_num(len(check_list)) - check_list = np.array([ - i for i, used, v in zip(check_list, self.used_ids[key_ids[check_list]], value_ids[check_list]) - if v in used - ]) + check_list = np.array( + [ + i + for i, used, v in zip( + check_list, + self.used_ids[key_ids[check_list]], + value_ids[check_list], + ) + if v in used + ] + ) return torch.tensor(value_ids, device=self.device) @@ -94,11 +107,11 @@ def random_num(self, num): self.random_pr %= self.random_list_length while True: if self.random_pr + num <= self.random_list_length: - value_id.append(self.random_list[self.random_pr:self.random_pr + num]) + value_id.append(self.random_list[self.random_pr : self.random_pr + num]) self.random_pr += num break else: - value_id.append(self.random_list[self.random_pr:]) + value_id.append(self.random_list[self.random_pr :]) num -= self.random_list_length - self.random_pr self.random_pr = 0 np.random.shuffle(self.random_list) @@ -147,19 +160,22 @@ def calculate_loss(self, interaction): # randomly train i-i relation and u-u relation with u-i relation if random.random() < 0.5: score_neg = self.forward(user, neg_item) - score_pos_con = self.context_forward(user, pos_item, 'uu') - score_neg_con = self.context_forward(user, neg_item, 'uu') + score_pos_con = self.context_forward(user, pos_item, "uu") + score_neg_con = self.context_forward(user, neg_item, "uu") else: # sample negative user for item neg_user = self.sampler(pos_item) score_neg = self.forward(neg_user, pos_item) - score_pos_con = self.context_forward(pos_item, user, 'ii') - score_neg_con = self.context_forward(pos_item, neg_user, 'ii') - - return self.loss_fct(ones, score_pos) \ - + self.loss_fct(-1 * ones, score_neg) \ - + self.loss_fct(ones, score_pos_con) * self.second_order_loss_weight \ - + self.loss_fct(-1 * ones, score_neg_con) * self.second_order_loss_weight + score_pos_con = self.context_forward(pos_item, user, "ii") + score_neg_con = self.context_forward(pos_item, neg_user, "ii") + + return ( + self.loss_fct(ones, score_pos) + + self.loss_fct(-1 * ones, score_neg) + + self.loss_fct(ones, score_pos_con) * self.second_order_loss_weight + + self.loss_fct(-1 * ones, score_neg_con) + * self.second_order_loss_weight + ) def predict(self, interaction): diff --git a/recbole/model/general_recommender/macridvae.py b/recbole/model/general_recommender/macridvae.py index 4c3d40218..32ba087bf 100644 --- a/recbole/model/general_recommender/macridvae.py +++ b/recbole/model/general_recommender/macridvae.py @@ -39,23 +39,25 @@ class MacridVAE(GeneralRecommender): def __init__(self, config, dataset): super(MacridVAE, self).__init__(config, dataset) - self.layers = config['encoder_hidden_size'] - self.embedding_size = config['embedding_size'] - self.drop_out = config['dropout_prob'] - self.kfac = config['kfac'] - self.tau = config['tau'] - self.nogb = config['nogb'] - self.anneal_cap = config['anneal_cap'] - self.total_anneal_steps = config['total_anneal_steps'] - self.regs = config['reg_weights'] - self.std = config['std'] + self.layers = config["encoder_hidden_size"] + self.embedding_size = config["embedding_size"] + self.drop_out = config["dropout_prob"] + self.kfac = config["kfac"] + self.tau = config["tau"] + self.nogb = config["nogb"] + self.anneal_cap = config["anneal_cap"] + self.total_anneal_steps = config["total_anneal_steps"] + self.regs = config["reg_weights"] + self.std = config["std"] self.update = 0 self.history_item_id, self.history_item_value, _ = dataset.history_item_matrix() self.history_item_id = self.history_item_id.to(self.device) self.history_item_value = self.history_item_value.to(self.device) - self.encode_layer_dims = [self.n_items] + self.layers + [self.embedding_size * 2] + self.encode_layer_dims = ( + [self.n_items] + self.layers + [self.embedding_size * 2] + ) self.encoder = self.mlp_layers(self.encode_layer_dims) @@ -77,10 +79,17 @@ def get_rating_matrix(self, user): """ # Following lines construct tensor of shape [B,n_items] using the tensor of shape [B,H] col_indices = self.history_item_id[user].flatten() - row_indices = torch.arange(user.shape[0]).to(self.device) \ + row_indices = ( + torch.arange(user.shape[0]) + .to(self.device) .repeat_interleave(self.history_item_id.shape[1], dim=0) - rating_matrix = torch.zeros(1).to(self.device).repeat(user.shape[0], self.n_items) - rating_matrix.index_put_((row_indices, col_indices), self.history_item_value[user].flatten()) + ) + rating_matrix = ( + torch.zeros(1).to(self.device).repeat(user.shape[0], self.n_items) + ) + rating_matrix.index_put_( + (row_indices, col_indices), self.history_item_value[user].flatten() + ) return rating_matrix def mlp_layers(self, layer_dims): @@ -114,7 +123,7 @@ def forward(self, rating_matrix): else: cates_sample = F.gumbel_softmax(cates_logits, tau=1, hard=False, dim=-1) cates_mode = torch.softmax(cates_logits, dim=-1) - cates = (self.training * cates_sample + (1 - self.training) * cates_mode) + cates = self.training * cates_sample + (1 - self.training) * cates_mode probs = None mulist = [] @@ -124,9 +133,9 @@ def forward(self, rating_matrix): # encoder x_k = rating_matrix * cates_k h = self.encoder(x_k) - mu = h[:, :self.embedding_size] + mu = h[:, : self.embedding_size] mu = F.normalize(mu, dim=1) - logvar = h[:, self.embedding_size:] + logvar = h[:, self.embedding_size :] mulist.append(mu) logvarlist.append(logvar) @@ -138,7 +147,7 @@ def forward(self, rating_matrix): logits_k = torch.matmul(z_k, items.transpose(0, 1)) / self.tau probs_k = torch.exp(logits_k) probs_k = probs_k * cates_k - probs = (probs_k if (probs is None) else (probs + probs_k)) + probs = probs_k if (probs is None) else (probs + probs_k) logits = torch.log(probs) @@ -152,7 +161,7 @@ def calculate_loss(self, interaction): self.update += 1 if self.total_anneal_steps > 0: - anneal = min(self.anneal_cap, 1. * self.update / self.total_anneal_steps) + anneal = min(self.anneal_cap, 1.0 * self.update / self.total_anneal_steps) else: anneal = self.anneal_cap @@ -160,7 +169,7 @@ def calculate_loss(self, interaction): kl_loss = None for i in range(self.kfac): kl_ = -0.5 * torch.mean(torch.sum(1 + logvar[i] - logvar[i].exp(), dim=1)) - kl_loss = (kl_ if (kl_loss is None) else (kl_loss + kl_)) + kl_loss = kl_ if (kl_loss is None) else (kl_loss + kl_) # CE loss ce_loss = -(F.log_softmax(z, 1) * rating_matrix).sum(1).mean() @@ -182,7 +191,7 @@ def reg_loss(self): loss_2 = reg_1 * self.k_embedding.weight.norm(2) loss_3 = 0 for name, parm in self.encoder.named_parameters(): - if name.endswith('weight'): + if name.endswith("weight"): loss_3 = loss_3 + reg_2 * parm.norm(2) return loss_1 + loss_2 + loss_3 diff --git a/recbole/model/general_recommender/multidae.py b/recbole/model/general_recommender/multidae.py index 377d38607..cf0f524c4 100644 --- a/recbole/model/general_recommender/multidae.py +++ b/recbole/model/general_recommender/multidae.py @@ -32,8 +32,8 @@ def __init__(self, config, dataset): super(MultiDAE, self).__init__(config, dataset) self.layers = config["mlp_hidden_size"] - self.lat_dim = config['latent_dimension'] - self.drop_out = config['dropout_prob'] + self.lat_dim = config["latent_dimension"] + self.drop_out = config["dropout_prob"] self.history_item_id, self.history_item_value, _ = dataset.history_item_matrix() self.history_item_id = self.history_item_id.to(self.device) @@ -42,7 +42,7 @@ def __init__(self, config, dataset): self.encode_layer_dims = [self.n_items] + self.layers + [self.lat_dim] self.decode_layer_dims = [self.lat_dim] + self.encode_layer_dims[::-1][1:] - self.encoder = MLPLayers(self.encode_layer_dims, activation='tanh') + self.encoder = MLPLayers(self.encode_layer_dims, activation="tanh") self.decoder = self.mlp_layers(self.decode_layer_dims) # parameters initialization @@ -59,10 +59,17 @@ def get_rating_matrix(self, user): """ # Following lines construct tensor of shape [B,n_items] using the tensor of shape [B,H] col_indices = self.history_item_id[user].flatten() - row_indices = torch.arange(user.shape[0]).to(self.device) \ + row_indices = ( + torch.arange(user.shape[0]) + .to(self.device) .repeat_interleave(self.history_item_id.shape[1], dim=0) - rating_matrix = torch.zeros(1).to(self.device).repeat(user.shape[0], self.n_items) - rating_matrix.index_put_((row_indices, col_indices), self.history_item_value[user].flatten()) + ) + rating_matrix = ( + torch.zeros(1).to(self.device).repeat(user.shape[0], self.n_items) + ) + rating_matrix.index_put_( + (row_indices, col_indices), self.history_item_value[user].flatten() + ) return rating_matrix def mlp_layers(self, layer_dims): diff --git a/recbole/model/general_recommender/multivae.py b/recbole/model/general_recommender/multivae.py index 41119924e..3931c75b2 100644 --- a/recbole/model/general_recommender/multivae.py +++ b/recbole/model/general_recommender/multivae.py @@ -31,9 +31,9 @@ def __init__(self, config, dataset): super(MultiVAE, self).__init__(config, dataset) self.layers = config["mlp_hidden_size"] - self.lat_dim = config['latent_dimension'] - self.drop_out = config['dropout_prob'] - self.anneal_cap = config['anneal_cap'] + self.lat_dim = config["latent_dimension"] + self.drop_out = config["dropout_prob"] + self.anneal_cap = config["anneal_cap"] self.total_anneal_steps = config["total_anneal_steps"] self.history_item_id, self.history_item_value, _ = dataset.history_item_matrix() @@ -43,7 +43,9 @@ def __init__(self, config, dataset): self.update = 0 self.encode_layer_dims = [self.n_items] + self.layers + [self.lat_dim] - self.decode_layer_dims = [int(self.lat_dim / 2)] + self.encode_layer_dims[::-1][1:] + self.decode_layer_dims = [int(self.lat_dim / 2)] + self.encode_layer_dims[::-1][ + 1: + ] self.encoder = self.mlp_layers(self.encode_layer_dims) self.decoder = self.mlp_layers(self.decode_layer_dims) @@ -62,10 +64,17 @@ def get_rating_matrix(self, user): """ # Following lines construct tensor of shape [B,n_items] using the tensor of shape [B,H] col_indices = self.history_item_id[user].flatten() - row_indices = torch.arange(user.shape[0]).to(self.device) \ + row_indices = ( + torch.arange(user.shape[0]) + .to(self.device) .repeat_interleave(self.history_item_id.shape[1], dim=0) - rating_matrix = torch.zeros(1).to(self.device).repeat(user.shape[0], self.n_items) - rating_matrix.index_put_((row_indices, col_indices), self.history_item_value[user].flatten()) + ) + rating_matrix = ( + torch.zeros(1).to(self.device).repeat(user.shape[0], self.n_items) + ) + rating_matrix.index_put_( + (row_indices, col_indices), self.history_item_value[user].flatten() + ) return rating_matrix def mlp_layers(self, layer_dims): @@ -92,8 +101,8 @@ def forward(self, rating_matrix): h = self.encoder(h) - mu = h[:, :int(self.lat_dim / 2)] - logvar = h[:, int(self.lat_dim / 2):] + mu = h[:, : int(self.lat_dim / 2)] + logvar = h[:, int(self.lat_dim / 2) :] z = self.reparameterize(mu, logvar) z = self.decoder(z) @@ -106,14 +115,18 @@ def calculate_loss(self, interaction): self.update += 1 if self.total_anneal_steps > 0: - anneal = min(self.anneal_cap, 1. * self.update / self.total_anneal_steps) + anneal = min(self.anneal_cap, 1.0 * self.update / self.total_anneal_steps) else: anneal = self.anneal_cap z, mu, logvar = self.forward(rating_matrix) # KL loss - kl_loss = -0.5 * torch.mean(torch.sum(1 + logvar - mu.pow(2) - logvar.exp(), dim=1)) * anneal + kl_loss = ( + -0.5 + * torch.mean(torch.sum(1 + logvar - mu.pow(2) - logvar.exp(), dim=1)) + * anneal + ) # CE loss ce_loss = -(F.log_softmax(z, 1) * rating_matrix).sum(1).mean() diff --git a/recbole/model/general_recommender/nais.py b/recbole/model/general_recommender/nais.py index 541e627b3..d213ced01 100644 --- a/recbole/model/general_recommender/nais.py +++ b/recbole/model/general_recommender/nais.py @@ -37,57 +37,74 @@ class NAIS(GeneralRecommender): mentioned in the original paper, we still train the model by a randomly sampled interactions. """ + input_type = InputType.POINTWISE def __init__(self, config, dataset): super(NAIS, self).__init__(config, dataset) # load dataset info - self.LABEL = config['LABEL_FIELD'] + self.LABEL = config["LABEL_FIELD"] # get all users' history interaction information.the history item # matrix is padding by the maximum number of a user's interactions - self.history_item_matrix, self.history_lens, self.mask_mat = self.get_history_info(dataset) + ( + self.history_item_matrix, + self.history_lens, + self.mask_mat, + ) = self.get_history_info(dataset) # load parameters info - self.embedding_size = config['embedding_size'] - self.weight_size = config['weight_size'] - self.algorithm = config['algorithm'] - self.reg_weights = config['reg_weights'] - self.alpha = config['alpha'] - self.beta = config['beta'] - self.split_to = config['split_to'] - self.pretrain_path = config['pretrain_path'] + self.embedding_size = config["embedding_size"] + self.weight_size = config["weight_size"] + self.algorithm = config["algorithm"] + self.reg_weights = config["reg_weights"] + self.alpha = config["alpha"] + self.beta = config["beta"] + self.split_to = config["split_to"] + self.pretrain_path = config["pretrain_path"] # split the too large dataset into the specified pieces if self.split_to > 0: - self.logger.info('split the n_items to {} pieces'.format(self.split_to)) - self.group = torch.chunk(torch.arange(self.n_items).to(self.device), self.split_to) + self.logger.info("split the n_items to {} pieces".format(self.split_to)) + self.group = torch.chunk( + torch.arange(self.n_items).to(self.device), self.split_to + ) else: - self.logger.warning('Pay Attetion!! the `split_to` is set to 0. If you catch a OMM error in this case, ' + \ - 'you need to increase it \n\t\t\tuntil the error disappears. For example, ' + \ - 'you can append it in the command line such as `--split_to=5`') + self.logger.warning( + "Pay Attetion!! the `split_to` is set to 0. If you catch a OMM error in this case, " + + "you need to increase it \n\t\t\tuntil the error disappears. For example, " + + "you can append it in the command line such as `--split_to=5`" + ) # define layers and loss # construct source and destination item embedding matrix - self.item_src_embedding = nn.Embedding(self.n_items, self.embedding_size, padding_idx=0) - self.item_dst_embedding = nn.Embedding(self.n_items, self.embedding_size, padding_idx=0) + self.item_src_embedding = nn.Embedding( + self.n_items, self.embedding_size, padding_idx=0 + ) + self.item_dst_embedding = nn.Embedding( + self.n_items, self.embedding_size, padding_idx=0 + ) self.bias = nn.Parameter(torch.zeros(self.n_items)) - if self.algorithm == 'concat': + if self.algorithm == "concat": self.mlp_layers = MLPLayers([self.embedding_size * 2, self.weight_size]) - elif self.algorithm == 'prod': + elif self.algorithm == "prod": self.mlp_layers = MLPLayers([self.embedding_size, self.weight_size]) else: - raise ValueError("NAIS just support attention type in ['concat', 'prod'] but get {}".format(self.algorithm)) + raise ValueError( + "NAIS just support attention type in ['concat', 'prod'] but get {}".format( + self.algorithm + ) + ) self.weight_layer = nn.Parameter(torch.ones(self.weight_size, 1)) self.bceloss = nn.BCEWithLogitsLoss() # parameters initialization if self.pretrain_path is not None: - self.logger.info('use pretrain from [{}]...'.format(self.pretrain_path)) + self.logger.info("use pretrain from [{}]...".format(self.pretrain_path)) self._load_pretrain() else: - self.logger.info('unused pretrain...') + self.logger.info("unused pretrain...") self.apply(self._init_weights) def _init_weights(self, module): @@ -106,16 +123,14 @@ def _init_weights(self, module): constant_(module.bias.data, 0) def _load_pretrain(self): - """A simple implementation of loading pretrained parameters. - - """ - fism = torch.load(self.pretrain_path)['state_dict'] - self.item_src_embedding.weight.data.copy_(fism['item_src_embedding.weight']) - self.item_dst_embedding.weight.data.copy_(fism['item_dst_embedding.weight']) + """A simple implementation of loading pretrained parameters.""" + fism = torch.load(self.pretrain_path)["state_dict"] + self.item_src_embedding.weight.data.copy_(fism["item_src_embedding.weight"]) + self.item_dst_embedding.weight.data.copy_(fism["item_dst_embedding.weight"]) for name, parm in self.mlp_layers.named_parameters(): - if name.endswith('weight'): + if name.endswith("weight"): xavier_normal_(parm.data) - elif name.endswith('bias'): + elif name.endswith("bias"): constant_(parm.data, 0) def get_history_info(self, dataset): @@ -147,7 +162,7 @@ def reg_loss(self): loss_2 = reg_2 * self.item_dst_embedding.weight.norm(2) loss_3 = 0 for name, parm in self.mlp_layers.named_parameters(): - if name.endswith('weight'): + if name.endswith("weight"): loss_3 = loss_3 + reg_3 * parm.norm(2) return loss_1 + loss_2 + loss_3 @@ -162,14 +177,19 @@ def attention_mlp(self, inter, target): torch.Tensor: the result of attention """ - if self.algorithm == 'prod': - mlp_input = inter * target.unsqueeze(1) # batch_size x max_len x embedding_size + if self.algorithm == "prod": + mlp_input = inter * target.unsqueeze( + 1 + ) # batch_size x max_len x embedding_size else: - mlp_input = torch.cat([inter, target.unsqueeze(1).expand_as(inter)], - dim=2) # batch_size x max_len x embedding_size*2 + mlp_input = torch.cat( + [inter, target.unsqueeze(1).expand_as(inter)], dim=2 + ) # batch_size x max_len x embedding_size*2 mlp_output = self.mlp_layers(mlp_input) # batch_size x max_len x weight_size - logits = torch.matmul(mlp_output, self.weight_layer).squeeze(2) # batch_size x max_len + logits = torch.matmul(mlp_output, self.weight_layer).squeeze( + 2 + ) # batch_size x max_len return logits def mask_softmax(self, similarity, logits, bias, item_num, batch_mask_mat): @@ -194,7 +214,7 @@ def mask_softmax(self, similarity, logits, bias, item_num, batch_mask_mat): weights = torch.div(exp_logits, exp_sum) coeff = torch.pow(item_num.squeeze(1), -self.alpha) - output =coeff.float() * torch.sum(weights * similarity, dim=1) + bias + output = coeff.float() * torch.sum(weights * similarity, dim=1) + bias return output @@ -216,21 +236,25 @@ def softmax(self, similarity, logits, item_num, bias): exp_sum = torch.pow(exp_sum, self.beta) weights = torch.div(exp_logits, exp_sum) coeff = torch.pow(item_num.squeeze(1), -self.alpha) - output = torch.sigmoid(coeff.float() * torch.sum(weights * similarity, dim=1) + bias) + output = torch.sigmoid( + coeff.float() * torch.sum(weights * similarity, dim=1) + bias + ) return output def inter_forward(self, user, item): - """forward the model by interaction - - """ + """forward the model by interaction""" user_inter = self.history_item_matrix[user] item_num = self.history_lens[user].unsqueeze(1) batch_mask_mat = self.mask_mat[user] - user_history = self.item_src_embedding(user_inter) # batch_size x max_len x embedding_size + user_history = self.item_src_embedding( + user_inter + ) # batch_size x max_len x embedding_size target = self.item_dst_embedding(item) # batch_size x embedding_size bias = self.bias[item] # batch_size x 1 - similarity = torch.bmm(user_history, target.unsqueeze(2)).squeeze(2) # batch_size x max_len + similarity = torch.bmm(user_history, target.unsqueeze(2)).squeeze( + 2 + ) # batch_size x max_len logits = self.attention_mlp(user_history, target) scores = self.mask_softmax(similarity, logits, bias, item_num, batch_mask_mat) return scores @@ -251,14 +275,18 @@ def user_forward(self, user_input, item_num, repeats=None, pred_slc=None): """ item_num = item_num.repeat(repeats, 1) user_history = self.item_src_embedding(user_input) # inter_num x embedding_size - user_history = user_history.repeat(repeats, 1, 1) # target_items x inter_num x embedding_size + user_history = user_history.repeat( + repeats, 1, 1 + ) # target_items x inter_num x embedding_size if pred_slc is None: targets = self.item_dst_embedding.weight # target_items x embedding_size bias = self.bias else: targets = self.item_dst_embedding(pred_slc) bias = self.bias[pred_slc] - similarity = torch.bmm(user_history, targets.unsqueeze(2)).squeeze(2) # inter_num x target_items + similarity = torch.bmm(user_history, targets.unsqueeze(2)).squeeze( + 2 + ) # inter_num x target_items logits = self.attention_mlp(user_history, targets) scores = self.softmax(similarity, logits, item_num, bias) return scores @@ -283,11 +311,18 @@ def full_sort_predict(self, interaction): # test users one by one, if the number of items is too large, we will split it to some pieces for user_input, item_num in zip(user_inters, item_nums.unsqueeze(1)): if self.split_to <= 0: - output = self.user_forward(user_input[:item_num], item_num, repeats=self.n_items) + output = self.user_forward( + user_input[:item_num], item_num, repeats=self.n_items + ) else: output = [] for mask in self.group: - tmp_output = self.user_forward(user_input[:item_num], item_num, repeats=len(mask), pred_slc=mask) + tmp_output = self.user_forward( + user_input[:item_num], + item_num, + repeats=len(mask), + pred_slc=mask, + ) output.append(tmp_output) output = torch.cat(output, dim=0) scores.append(output) diff --git a/recbole/model/general_recommender/nceplrec.py b/recbole/model/general_recommender/nceplrec.py index fbd8654ec..62b88b62f 100644 --- a/recbole/model/general_recommender/nceplrec.py +++ b/recbole/model/general_recommender/nceplrec.py @@ -21,6 +21,7 @@ from recbole.model.abstract_recommender import GeneralRecommender from recbole.utils import InputType + class NCEPLRec(GeneralRecommender): input_type = InputType.POINTWISE @@ -30,13 +31,12 @@ def __init__(self, config, dataset): # need at least one param self.dummy_param = torch.nn.Parameter(torch.zeros(1)) - R = dataset.inter_matrix( - form='csr').astype(np.float32) + R = dataset.inter_matrix(form="csr").astype(np.float32) - beta = config['beta'] - rank = int(config['rank']) - reg_weight = config['reg_weight'] - seed = config['seed'] + beta = config["beta"] + rank = int(config["rank"]) + reg_weight = config["reg_weight"] + seed = config["seed"] # just directly calculate the entire score matrix in init # (can't be done incrementally) @@ -51,21 +51,26 @@ def __init__(self, config, dataset): values = item_popularities[:, col_index].getA1() # note this is a slight variation of what's in the paper, for convenience # see https://github.com/wuga214/NCE_Projected_LRec/issues/38 - values = np.maximum( - np.log(num_users/np.power(values, beta)), 0) - D_rows.append(sp.coo_matrix( - (values, (row_index, col_index)), shape=(1, num_items))) + values = np.maximum(np.log(num_users / np.power(values, beta)), 0) + D_rows.append( + sp.coo_matrix( + (values, (row_index, col_index)), shape=(1, num_items) + ) + ) else: D_rows.append(sp.coo_matrix((1, num_items))) D = sp.vstack(D_rows) - _, sigma, Vt = randomized_svd(D, n_components=rank, - n_iter='auto', - power_iteration_normalizer='QR', - random_state=seed) + _, sigma, Vt = randomized_svd( + D, + n_components=rank, + n_iter="auto", + power_iteration_normalizer="QR", + random_state=seed, + ) - sqrt_Sigma = np.diag(np.power(sigma, 1/2)) + sqrt_Sigma = np.diag(np.power(sigma, 1 / 2)) V_star = Vt.T @ sqrt_Sigma @@ -87,11 +92,13 @@ def calculate_loss(self, interaction): def predict(self, interaction): user = interaction[self.USER_ID] item = interaction[self.ITEM_ID] - result = (self.user_embeddings[user, :] * self.item_embeddings[:, item].T).sum(axis=1) + result = (self.user_embeddings[user, :] * self.item_embeddings[:, item].T).sum( + axis=1 + ) return result.float() def full_sort_predict(self, interaction): user = interaction[self.USER_ID] result = self.user_embeddings[user, :] @ self.item_embeddings - return result.flatten() \ No newline at end of file + return result.flatten() diff --git a/recbole/model/general_recommender/ncl.py b/recbole/model/general_recommender/ncl.py index 1c78c5fcd..3c298dfa4 100644 --- a/recbole/model/general_recommender/ncl.py +++ b/recbole/model/general_recommender/ncl.py @@ -29,25 +29,33 @@ def __init__(self, config, dataset): super(NCL, self).__init__(config, dataset) # load dataset info - self.interaction_matrix = dataset.inter_matrix(form='coo').astype(np.float32) + self.interaction_matrix = dataset.inter_matrix(form="coo").astype(np.float32) # load parameters info - self.latent_dim = config['embedding_size'] # int type: the embedding size of the base model - self.n_layers = config['n_layers'] # int type: the layer num of the base model - self.reg_weight = config['reg_weight'] # float32 type: the weight decay for l2 normalization + self.latent_dim = config[ + "embedding_size" + ] # int type: the embedding size of the base model + self.n_layers = config["n_layers"] # int type: the layer num of the base model + self.reg_weight = config[ + "reg_weight" + ] # float32 type: the weight decay for l2 normalization - self.ssl_temp = config['ssl_temp'] - self.ssl_reg = config['ssl_reg'] - self.hyper_layers = config['hyper_layers'] + self.ssl_temp = config["ssl_temp"] + self.ssl_reg = config["ssl_reg"] + self.hyper_layers = config["hyper_layers"] - self.alpha = config['alpha'] + self.alpha = config["alpha"] - self.proto_reg = config['proto_reg'] - self.k = config['num_clusters'] + self.proto_reg = config["proto_reg"] + self.k = config["num_clusters"] # define layers and loss - self.user_embedding = torch.nn.Embedding(num_embeddings=self.n_users, embedding_dim=self.latent_dim) - self.item_embedding = torch.nn.Embedding(num_embeddings=self.n_items, embedding_dim=self.latent_dim) + self.user_embedding = torch.nn.Embedding( + num_embeddings=self.n_users, embedding_dim=self.latent_dim + ) + self.item_embedding = torch.nn.Embedding( + num_embeddings=self.n_items, embedding_dim=self.latent_dim + ) self.mf_loss = BPRLoss() self.reg_loss = EmbLoss() @@ -60,7 +68,7 @@ def __init__(self, config, dataset): # parameters initialization self.apply(xavier_uniform_initialization) - self.other_parameter_name = ['restore_user_e', 'restore_item_e'] + self.other_parameter_name = ["restore_user_e", "restore_item_e"] self.user_centroids = None self.user_2cluster = None @@ -74,9 +82,9 @@ def e_step(self): self.item_centroids, self.item_2cluster = self.run_kmeans(item_embeddings) def run_kmeans(self, x): - """Run K-means algorithm to get k clusters of the input tensor x - """ + """Run K-means algorithm to get k clusters of the input tensor x""" import faiss + kmeans = faiss.Kmeans(d=self.latent_dim, k=self.k, gpu=True) kmeans.train(x) cluster_cents = kmeans.centroids @@ -103,11 +111,22 @@ def get_norm_adj_mat(self): Sparse tensor of the normalized interaction matrix. """ # build adj matrix - A = sp.dok_matrix((self.n_users + self.n_items, self.n_users + self.n_items), dtype=np.float32) + A = sp.dok_matrix( + (self.n_users + self.n_items, self.n_users + self.n_items), dtype=np.float32 + ) inter_M = self.interaction_matrix inter_M_t = self.interaction_matrix.transpose() - data_dict = dict(zip(zip(inter_M.row, inter_M.col + self.n_users), [1] * inter_M.nnz)) - data_dict.update(dict(zip(zip(inter_M_t.row + self.n_users, inter_M_t.col), [1] * inter_M_t.nnz))) + data_dict = dict( + zip(zip(inter_M.row, inter_M.col + self.n_users), [1] * inter_M.nnz) + ) + data_dict.update( + dict( + zip( + zip(inter_M_t.row + self.n_users, inter_M_t.col), + [1] * inter_M_t.nnz, + ) + ) + ) A._update(data_dict) # norm adj matrix sumArr = (A > 0).sum(axis=1) @@ -140,27 +159,35 @@ def get_ego_embeddings(self): def forward(self): all_embeddings = self.get_ego_embeddings() embeddings_list = [all_embeddings] - for layer_idx in range(max(self.n_layers, self.hyper_layers*2)): + for layer_idx in range(max(self.n_layers, self.hyper_layers * 2)): all_embeddings = torch.sparse.mm(self.norm_adj_mat, all_embeddings) embeddings_list.append(all_embeddings) - lightgcn_all_embeddings = torch.stack(embeddings_list[:self.n_layers+1], dim=1) + lightgcn_all_embeddings = torch.stack( + embeddings_list[: self.n_layers + 1], dim=1 + ) lightgcn_all_embeddings = torch.mean(lightgcn_all_embeddings, dim=1) - user_all_embeddings, item_all_embeddings = torch.split(lightgcn_all_embeddings, [self.n_users, self.n_items]) + user_all_embeddings, item_all_embeddings = torch.split( + lightgcn_all_embeddings, [self.n_users, self.n_items] + ) return user_all_embeddings, item_all_embeddings, embeddings_list def ProtoNCE_loss(self, node_embedding, user, item): - user_embeddings_all, item_embeddings_all = torch.split(node_embedding, [self.n_users, self.n_items]) + user_embeddings_all, item_embeddings_all = torch.split( + node_embedding, [self.n_users, self.n_items] + ) - user_embeddings = user_embeddings_all[user] # [B, e] + user_embeddings = user_embeddings_all[user] # [B, e] norm_user_embeddings = F.normalize(user_embeddings) - user2cluster = self.user_2cluster[user] # [B,] - user2centroids = self.user_centroids[user2cluster] # [B, e] + user2cluster = self.user_2cluster[user] # [B,] + user2centroids = self.user_centroids[user2cluster] # [B, e] pos_score_user = torch.mul(norm_user_embeddings, user2centroids).sum(dim=1) pos_score_user = torch.exp(pos_score_user / self.ssl_temp) - ttl_score_user = torch.matmul(norm_user_embeddings, self.user_centroids.transpose(0, 1)) + ttl_score_user = torch.matmul( + norm_user_embeddings, self.user_centroids.transpose(0, 1) + ) ttl_score_user = torch.exp(ttl_score_user / self.ssl_temp).sum(dim=1) proto_nce_loss_user = -torch.log(pos_score_user / ttl_score_user).sum() @@ -172,7 +199,9 @@ def ProtoNCE_loss(self, node_embedding, user, item): item2centroids = self.item_centroids[item2cluster] # [B, e] pos_score_item = torch.mul(norm_item_embeddings, item2centroids).sum(dim=1) pos_score_item = torch.exp(pos_score_item / self.ssl_temp) - ttl_score_item = torch.matmul(norm_item_embeddings, self.item_centroids.transpose(0, 1)) + ttl_score_item = torch.matmul( + norm_item_embeddings, self.item_centroids.transpose(0, 1) + ) ttl_score_item = torch.exp(ttl_score_item / self.ssl_temp).sum(dim=1) proto_nce_loss_item = -torch.log(pos_score_item / ttl_score_item).sum() @@ -180,8 +209,12 @@ def ProtoNCE_loss(self, node_embedding, user, item): return proto_nce_loss def ssl_layer_loss(self, current_embedding, previous_embedding, user, item): - current_user_embeddings, current_item_embeddings = torch.split(current_embedding, [self.n_users, self.n_items]) - previous_user_embeddings_all, previous_item_embeddings_all = torch.split(previous_embedding, [self.n_users, self.n_items]) + current_user_embeddings, current_item_embeddings = torch.split( + current_embedding, [self.n_users, self.n_items] + ) + previous_user_embeddings_all, previous_item_embeddings_all = torch.split( + previous_embedding, [self.n_users, self.n_items] + ) current_user_embeddings = current_user_embeddings[user] previous_user_embeddings = previous_user_embeddings_all[user] @@ -224,7 +257,9 @@ def calculate_loss(self, interaction): center_embedding = embeddings_list[0] context_embedding = embeddings_list[self.hyper_layers * 2] - ssl_loss = self.ssl_layer_loss(context_embedding, center_embedding, user, pos_item) + ssl_loss = self.ssl_layer_loss( + context_embedding, center_embedding, user, pos_item + ) proto_loss = self.ProtoNCE_loss(center_embedding, user, pos_item) u_embeddings = user_all_embeddings[user] @@ -241,7 +276,9 @@ def calculate_loss(self, interaction): pos_ego_embeddings = self.item_embedding(pos_item) neg_ego_embeddings = self.item_embedding(neg_item) - reg_loss = self.reg_loss(u_ego_embeddings, pos_ego_embeddings, neg_ego_embeddings) + reg_loss = self.reg_loss( + u_ego_embeddings, pos_ego_embeddings, neg_ego_embeddings + ) return mf_loss + self.reg_weight * reg_loss, ssl_loss, proto_loss diff --git a/recbole/model/general_recommender/neumf.py b/recbole/model/general_recommender/neumf.py index 1bd105050..991fff8bc 100644 --- a/recbole/model/general_recommender/neumf.py +++ b/recbole/model/general_recommender/neumf.py @@ -39,28 +39,32 @@ def __init__(self, config, dataset): super(NeuMF, self).__init__(config, dataset) # load dataset info - self.LABEL = config['LABEL_FIELD'] + self.LABEL = config["LABEL_FIELD"] # load parameters info - self.mf_embedding_size = config['mf_embedding_size'] - self.mlp_embedding_size = config['mlp_embedding_size'] - self.mlp_hidden_size = config['mlp_hidden_size'] - self.dropout_prob = config['dropout_prob'] - self.mf_train = config['mf_train'] - self.mlp_train = config['mlp_train'] - self.use_pretrain = config['use_pretrain'] - self.mf_pretrain_path = config['mf_pretrain_path'] - self.mlp_pretrain_path = config['mlp_pretrain_path'] + self.mf_embedding_size = config["mf_embedding_size"] + self.mlp_embedding_size = config["mlp_embedding_size"] + self.mlp_hidden_size = config["mlp_hidden_size"] + self.dropout_prob = config["dropout_prob"] + self.mf_train = config["mf_train"] + self.mlp_train = config["mlp_train"] + self.use_pretrain = config["use_pretrain"] + self.mf_pretrain_path = config["mf_pretrain_path"] + self.mlp_pretrain_path = config["mlp_pretrain_path"] # define layers and loss self.user_mf_embedding = nn.Embedding(self.n_users, self.mf_embedding_size) self.item_mf_embedding = nn.Embedding(self.n_items, self.mf_embedding_size) self.user_mlp_embedding = nn.Embedding(self.n_users, self.mlp_embedding_size) self.item_mlp_embedding = nn.Embedding(self.n_items, self.mlp_embedding_size) - self.mlp_layers = MLPLayers([2 * self.mlp_embedding_size] + self.mlp_hidden_size, self.dropout_prob) + self.mlp_layers = MLPLayers( + [2 * self.mlp_embedding_size] + self.mlp_hidden_size, self.dropout_prob + ) self.mlp_layers.logger = None # remove logger to use torch.save() if self.mf_train and self.mlp_train: - self.predict_layer = nn.Linear(self.mf_embedding_size + self.mlp_hidden_size[-1], 1) + self.predict_layer = nn.Linear( + self.mf_embedding_size + self.mlp_hidden_size[-1], 1 + ) elif self.mf_train: self.predict_layer = nn.Linear(self.mf_embedding_size, 1) elif self.mlp_train: @@ -75,9 +79,7 @@ def __init__(self, config, dataset): self.apply(self._init_weights) def load_pretrain(self): - r"""A simple implementation of loading pretrained parameters. - - """ + r"""A simple implementation of loading pretrained parameters.""" mf = torch.load(self.mf_pretrain_path) mlp = torch.load(self.mlp_pretrain_path) self.user_mf_embedding.weight.data.copy_(mf.user_mf_embedding.weight) @@ -90,7 +92,9 @@ def load_pretrain(self): m1.weight.data.copy_(m2.weight) m1.bias.data.copy_(m2.bias) - predict_weight = torch.cat([mf.predict_layer.weight, mlp.predict_layer.weight], dim=1) + predict_weight = torch.cat( + [mf.predict_layer.weight, mlp.predict_layer.weight], dim=1 + ) predict_bias = mf.predict_layer.bias + mlp.predict_layer.bias self.predict_layer.weight.data.copy_(predict_weight) @@ -108,7 +112,9 @@ def forward(self, user, item): if self.mf_train: mf_output = torch.mul(user_mf_e, item_mf_e) # [batch_size, embedding_size] if self.mlp_train: - mlp_output = self.mlp_layers(torch.cat((user_mlp_e, item_mlp_e), -1)) # [batch_size, layers[-1]] + mlp_output = self.mlp_layers( + torch.cat((user_mlp_e, item_mlp_e), -1) + ) # [batch_size, layers[-1]] if self.mf_train and self.mlp_train: output = self.predict_layer(torch.cat((mf_output, mlp_output), -1)) elif self.mf_train: @@ -116,27 +122,27 @@ def forward(self, user, item): elif self.mlp_train: output = self.predict_layer(mlp_output) else: - raise RuntimeError('mf_train and mlp_train can not be False at the same time') + raise RuntimeError( + "mf_train and mlp_train can not be False at the same time" + ) return output.squeeze(-1) def calculate_loss(self, interaction): user = interaction[self.USER_ID] item = interaction[self.ITEM_ID] label = interaction[self.LABEL] - - output = self.forward(user, item) + + output = self.forward(user, item) return self.loss(output, label) def predict(self, interaction): user = interaction[self.USER_ID] item = interaction[self.ITEM_ID] - predict=self.sigmoid(self.forward(user, item)) + predict = self.sigmoid(self.forward(user, item)) return predict def dump_parameters(self): - r"""A simple implementation of dumping model parameters for pretrain. - - """ + r"""A simple implementation of dumping model parameters for pretrain.""" if self.mf_train and not self.mlp_train: save_path = self.mf_pretrain_path torch.save(self, save_path) diff --git a/recbole/model/general_recommender/ngcf.py b/recbole/model/general_recommender/ngcf.py index 0080f32eb..e98d7dad4 100644 --- a/recbole/model/general_recommender/ngcf.py +++ b/recbole/model/general_recommender/ngcf.py @@ -42,22 +42,24 @@ def __init__(self, config, dataset): super(NGCF, self).__init__(config, dataset) # load dataset info - self.interaction_matrix = dataset.inter_matrix(form='coo').astype(np.float32) + self.interaction_matrix = dataset.inter_matrix(form="coo").astype(np.float32) # load parameters info - self.embedding_size = config['embedding_size'] - self.hidden_size_list = config['hidden_size_list'] + self.embedding_size = config["embedding_size"] + self.hidden_size_list = config["hidden_size_list"] self.hidden_size_list = [self.embedding_size] + self.hidden_size_list - self.node_dropout = config['node_dropout'] - self.message_dropout = config['message_dropout'] - self.reg_weight = config['reg_weight'] + self.node_dropout = config["node_dropout"] + self.message_dropout = config["message_dropout"] + self.reg_weight = config["reg_weight"] # define layers and loss self.sparse_dropout = SparseDropout(self.node_dropout) self.user_embedding = nn.Embedding(self.n_users, self.embedding_size) self.item_embedding = nn.Embedding(self.n_items, self.embedding_size) self.GNNlayers = torch.nn.ModuleList() - for idx, (input_size, output_size) in enumerate(zip(self.hidden_size_list[:-1], self.hidden_size_list[1:])): + for idx, (input_size, output_size) in enumerate( + zip(self.hidden_size_list[:-1], self.hidden_size_list[1:]) + ): self.GNNlayers.append(BiGNNLayer(input_size, output_size)) self.mf_loss = BPRLoss() self.reg_loss = EmbLoss() @@ -72,7 +74,7 @@ def __init__(self, config, dataset): # parameters initialization self.apply(xavier_normal_initialization) - self.other_parameter_name = ['restore_user_e', 'restore_item_e'] + self.other_parameter_name = ["restore_user_e", "restore_item_e"] def get_norm_adj_mat(self): r"""Get the normalized interaction matrix of users and items. @@ -87,15 +89,28 @@ def get_norm_adj_mat(self): Sparse tensor of the normalized interaction matrix. """ # build adj matrix - A = sp.dok_matrix((self.n_users + self.n_items, self.n_users + self.n_items), dtype=np.float32) + A = sp.dok_matrix( + (self.n_users + self.n_items, self.n_users + self.n_items), dtype=np.float32 + ) inter_M = self.interaction_matrix inter_M_t = self.interaction_matrix.transpose() - data_dict = dict(zip(zip(inter_M.row, inter_M.col + self.n_users), [1] * inter_M.nnz)) - data_dict.update(dict(zip(zip(inter_M_t.row + self.n_users, inter_M_t.col), [1] * inter_M_t.nnz))) + data_dict = dict( + zip(zip(inter_M.row, inter_M.col + self.n_users), [1] * inter_M.nnz) + ) + data_dict.update( + dict( + zip( + zip(inter_M_t.row + self.n_users, inter_M_t.col), + [1] * inter_M_t.nnz, + ) + ) + ) A._update(data_dict) # norm adj matrix sumArr = (A > 0).sum(axis=1) - diag = np.array(sumArr.flatten())[0] + 1e-7 # add epsilon to avoid divide by zero Warning + diag = ( + np.array(sumArr.flatten())[0] + 1e-7 + ) # add epsilon to avoid divide by zero Warning diag = np.power(diag, -0.5) D = sp.diags(diag) L = D * A * D @@ -132,7 +147,11 @@ def get_ego_embeddings(self): def forward(self): - A_hat = self.sparse_dropout(self.norm_adj_matrix) if self.node_dropout != 0 else self.norm_adj_matrix + A_hat = ( + self.sparse_dropout(self.norm_adj_matrix) + if self.node_dropout != 0 + else self.norm_adj_matrix + ) all_embeddings = self.get_ego_embeddings() embeddings_list = [all_embeddings] for gnn in self.GNNlayers: @@ -140,10 +159,14 @@ def forward(self): all_embeddings = nn.LeakyReLU(negative_slope=0.2)(all_embeddings) all_embeddings = nn.Dropout(self.message_dropout)(all_embeddings) all_embeddings = F.normalize(all_embeddings, p=2, dim=1) - embeddings_list += [all_embeddings] # storage output embedding of each layer + embeddings_list += [ + all_embeddings + ] # storage output embedding of each layer ngcf_all_embeddings = torch.cat(embeddings_list, dim=1) - user_all_embeddings, item_all_embeddings = torch.split(ngcf_all_embeddings, [self.n_users, self.n_items]) + user_all_embeddings, item_all_embeddings = torch.split( + ngcf_all_embeddings, [self.n_users, self.n_items] + ) return user_all_embeddings, item_all_embeddings @@ -165,7 +188,9 @@ def calculate_loss(self, interaction): neg_scores = torch.mul(u_embeddings, neg_embeddings).sum(dim=1) mf_loss = self.mf_loss(pos_scores, neg_scores) # calculate BPR Loss - reg_loss = self.reg_loss(u_embeddings, pos_embeddings, neg_embeddings) # L2 regularization of embeddings + reg_loss = self.reg_loss( + u_embeddings, pos_embeddings, neg_embeddings + ) # L2 regularization of embeddings return mf_loss + self.reg_weight * reg_loss diff --git a/recbole/model/general_recommender/nncf.py b/recbole/model/general_recommender/nncf.py index 52bcb8def..8a7dc5a1b 100644 --- a/recbole/model/general_recommender/nncf.py +++ b/recbole/model/general_recommender/nncf.py @@ -36,43 +36,56 @@ def __init__(self, config, dataset): super(NNCF, self).__init__(config, dataset) # load dataset info - self.LABEL = config['LABEL_FIELD'] - self.interaction_matrix = dataset.inter_matrix(form='coo').astype(np.float32) + self.LABEL = config["LABEL_FIELD"] + self.interaction_matrix = dataset.inter_matrix(form="coo").astype(np.float32) # load parameters info - self.ui_embedding_size = config['ui_embedding_size'] - self.neigh_embedding_size = config['neigh_embedding_size'] - self.num_conv_kernel = config['num_conv_kernel'] - self.conv_kernel_size = config['conv_kernel_size'] - self.pool_kernel_size = config['pool_kernel_size'] - self.mlp_hidden_size = config['mlp_hidden_size'] - self.neigh_num = config['neigh_num'] - self.neigh_info_method = config['neigh_info_method'] - self.resolution = config['resolution'] + self.ui_embedding_size = config["ui_embedding_size"] + self.neigh_embedding_size = config["neigh_embedding_size"] + self.num_conv_kernel = config["num_conv_kernel"] + self.conv_kernel_size = config["conv_kernel_size"] + self.pool_kernel_size = config["pool_kernel_size"] + self.mlp_hidden_size = config["mlp_hidden_size"] + self.neigh_num = config["neigh_num"] + self.neigh_info_method = config["neigh_info_method"] + self.resolution = config["resolution"] # define layers and loss self.user_embedding = nn.Embedding(self.n_users, self.ui_embedding_size) self.item_embedding = nn.Embedding(self.n_items, self.ui_embedding_size) - self.user_neigh_embedding = nn.Embedding(self.n_items, self.neigh_embedding_size) - self.item_neigh_embedding = nn.Embedding(self.n_users, self.neigh_embedding_size) + self.user_neigh_embedding = nn.Embedding( + self.n_items, self.neigh_embedding_size + ) + self.item_neigh_embedding = nn.Embedding( + self.n_users, self.neigh_embedding_size + ) self.user_conv = nn.Sequential( - nn.Conv1d(self.neigh_embedding_size, self.num_conv_kernel, self.conv_kernel_size), - nn.MaxPool1d(self.pool_kernel_size), nn.ReLU() + nn.Conv1d( + self.neigh_embedding_size, self.num_conv_kernel, self.conv_kernel_size + ), + nn.MaxPool1d(self.pool_kernel_size), + nn.ReLU(), ) self.item_conv = nn.Sequential( - nn.Conv1d(self.neigh_embedding_size, self.num_conv_kernel, self.conv_kernel_size), - nn.MaxPool1d(self.pool_kernel_size), nn.ReLU() + nn.Conv1d( + self.neigh_embedding_size, self.num_conv_kernel, self.conv_kernel_size + ), + nn.MaxPool1d(self.pool_kernel_size), + nn.ReLU(), ) conved_size = self.neigh_num - (self.conv_kernel_size - 1) - pooled_size = (conved_size - (self.pool_kernel_size - 1) - 1) // self.pool_kernel_size + 1 + pooled_size = ( + conved_size - (self.pool_kernel_size - 1) - 1 + ) // self.pool_kernel_size + 1 self.mlp_layers = MLPLayers( - [2 * pooled_size * self.num_conv_kernel + self.ui_embedding_size] + self.mlp_hidden_size, - config['dropout'] + [2 * pooled_size * self.num_conv_kernel + self.ui_embedding_size] + + self.mlp_hidden_size, + config["dropout"], ) self.out_layer = nn.Linear(self.mlp_hidden_size[-1], 1) - self.dropout_layer = torch.nn.Dropout(p=config['dropout']) + self.dropout_layer = torch.nn.Dropout(p=config["dropout"]) self.loss = nn.BCEWithLogitsLoss() - + # choose the method to use neighborhood information if self.neigh_info_method == "random": self.u_neigh, self.i_neigh = self.get_neigh_random() @@ -82,8 +95,8 @@ def __init__(self, config, dataset): self.u_neigh, self.i_neigh = self.get_neigh_louvain() else: raise RuntimeError( - 'You need to choose the right algorithm of processing neighborhood information. \ - The parameter neigh_info_method can be set to random, knn or louvain.' + "You need to choose the right algorithm of processing neighborhood information. \ + The parameter neigh_info_method can be set to random, knn or louvain." ) # parameters initialization @@ -95,9 +108,9 @@ def _init_weights(self, module): # Unify embedding length def Max_ner(self, lst, max_ner): - r"""Unify embedding length of neighborhood information for efficiency consideration. + r"""Unify embedding length of neighborhood information for efficiency consideration. Truncate the list if the length is larger than max_ner. - Otherwise, pad it with 0. + Otherwise, pad it with 0. Args: lst (list): The input list contains node's neighbors. @@ -119,8 +132,8 @@ def Max_ner(self, lst, max_ner): # Find other nodes in the same community def get_community_member(self, partition, community_dict, node, kind): - r"""Find other nodes in the same community. - e.g. If the node starts with letter "i", + r"""Find other nodes in the same community. + e.g. If the node starts with letter "i", the other nodes start with letter "i" in the same community dict group are its community neighbors. Args: @@ -155,19 +168,23 @@ def prepare_vector_element(self, partition, relation, community_dict): for r in range(len(relation)): user, item = relation[r][0], relation[r][1] - item2user_neighbor = self.get_community_member(partition, community_dict, user, 'u') + item2user_neighbor = self.get_community_member( + partition, community_dict, user, "u" + ) np.random.shuffle(item2user_neighbor) - user2item_neighbor = self.get_community_member(partition, community_dict, item, 'i') + user2item_neighbor = self.get_community_member( + partition, community_dict, item, "i" + ) np.random.shuffle(user2item_neighbor) - _, user = user.split('_', 1) + _, user = user.split("_", 1) user = int(user) - _, item = item.split('_', 1) + _, item = item.split("_", 1) item = int(item) for i in range(len(item2user_neighbor)): - name, index = item2user_neighbor[i].split('_', 1) + name, index = item2user_neighbor[i].split("_", 1) item2user_neighbor[i] = int(index) for i in range(len(user2item_neighbor)): - name, index = user2item_neighbor[i].split('_', 1) + name, index = user2item_neighbor[i].split("_", 1) user2item_neighbor[i] = int(index) item2user_neighbor_lst[item] = item2user_neighbor @@ -178,7 +195,7 @@ def prepare_vector_element(self, partition, relation, community_dict): # Get neighborhood embeddings using louvain method def get_neigh_louvain(self): r"""Get neighborhood information using louvain algorithm. - First, change the id of node, + First, change the id of node, for example, the id of user node "1" will be set to "u_1" in order to use louvain algorithm. Second, use louvain algorithm to seperate nodes into different communities. Finally, find the community neighbors of each node with the same type and reset the id of the nodes. @@ -191,13 +208,17 @@ def get_neigh_louvain(self): tmp_relation = [] for i in range(len(pairs)): - tmp_relation.append(['user_' + str(pairs[i][0]), 'item_' + str(pairs[i][1])]) + tmp_relation.append( + ["user_" + str(pairs[i][0]), "item_" + str(pairs[i][1])] + ) import networkx as nx + G = nx.Graph() G.add_edges_from(tmp_relation) resolution = self.resolution import community + partition = community.best_partition(G, resolution=resolution) community_dict = {} @@ -207,7 +228,9 @@ def get_neigh_louvain(self): for node, part in partition.items(): community_dict[part] = community_dict[part] + [node] - tmp_user2item, tmp_item2user = self.prepare_vector_element(partition, tmp_relation, community_dict) + tmp_user2item, tmp_item2user = self.prepare_vector_element( + partition, tmp_relation, community_dict + ) u_neigh = self.Max_ner(tmp_user2item, self.neigh_num) i_neigh = self.Max_ner(tmp_item2user, self.neigh_num) @@ -218,9 +241,9 @@ def get_neigh_louvain(self): # Get neighborhood embeddings using knn method def get_neigh_knn(self): r"""Get neighborhood information using knn algorithm. - Find direct neighbors of each node, if the number of direct neighbors is less than neigh_num, + Find direct neighbors of each node, if the number of direct neighbors is less than neigh_num, add other similar neighbors using knn algorithm. - Otherwise, select random top k direct neighbors, k equals to the number of neighbors. + Otherwise, select random top k direct neighbors, k equals to the number of neighbors. Returns: torch.IntTensor: The neighborhood nodes of a batch of user or item, shape: [batch_size, neigh_num] @@ -233,8 +256,12 @@ def get_neigh_knn(self): ui_inters[pairs[i][0], pairs[i][1]] = 1 # Get similar neighbors using knn algorithm - user_knn, _ = ComputeSimilarity(self.interaction_matrix.tocsr(), topk=self.neigh_num).compute_similarity('user') - item_knn, _ = ComputeSimilarity(self.interaction_matrix.tocsr(), topk=self.neigh_num).compute_similarity('item') + user_knn, _ = ComputeSimilarity( + self.interaction_matrix.tocsr(), topk=self.neigh_num + ).compute_similarity("user") + item_knn, _ = ComputeSimilarity( + self.interaction_matrix.tocsr(), topk=self.neigh_num + ).compute_similarity("item") u_neigh, i_neigh = [], [] @@ -247,7 +274,7 @@ def get_neigh_knn(self): tmp_k = self.neigh_num - direct_neigh_num mask = np.random.randint(0, len(neigh_list), size=1) neigh_list = list(neigh_list) + list(item_knn[neigh_list[mask[0]]]) - u_neigh.append(neigh_list[:self.neigh_num]) + u_neigh.append(neigh_list[: self.neigh_num]) else: mask = np.random.randint(0, len(neigh_list), size=self.neigh_num) u_neigh.append(neigh_list[mask]) @@ -261,7 +288,7 @@ def get_neigh_knn(self): tmp_k = self.neigh_num - direct_neigh_num mask = np.random.randint(0, len(neigh_list), size=1) neigh_list = list(neigh_list) + list(user_knn[neigh_list[mask[0]]]) - i_neigh.append(neigh_list[:self.neigh_num]) + i_neigh.append(neigh_list[: self.neigh_num]) else: mask = np.random.randint(0, len(neigh_list), size=self.neigh_num) i_neigh.append(neigh_list[mask]) @@ -273,8 +300,8 @@ def get_neigh_knn(self): # Get neighborhood embeddings using random method def get_neigh_random(self): r"""Get neighborhood information using random algorithm. - Select random top k direct neighbors, k equals to the number of neighbors. - + Select random top k direct neighbors, k equals to the number of neighbors. + Returns: torch.IntTensor: The neighborhood nodes of a batch of user or item, shape: [batch_size, neigh_num] """ @@ -341,7 +368,9 @@ def forward(self, user, item): # batch_size * out_channel * pool_size item_neigh_conv_embedding = item_neigh_conv_embedding.view(batch_size, -1) mf_vec = torch.mul(user_embedding, item_embedding) - last = torch.cat((mf_vec, user_neigh_conv_embedding, item_neigh_conv_embedding), dim=-1) + last = torch.cat( + (mf_vec, user_neigh_conv_embedding, item_neigh_conv_embedding), dim=-1 + ) output = self.mlp_layers(last) out = self.out_layer(output) @@ -353,8 +382,8 @@ def calculate_loss(self, interaction): item = interaction[self.ITEM_ID] label = interaction[self.LABEL] - output = self.forward(user, item) - return self.loss(output, label) + output = self.forward(user, item) + return self.loss(output, label) def predict(self, interaction): user = interaction[self.USER_ID] diff --git a/recbole/model/general_recommender/pop.py b/recbole/model/general_recommender/pop.py index da346faf2..2984d23c6 100644 --- a/recbole/model/general_recommender/pop.py +++ b/recbole/model/general_recommender/pop.py @@ -20,19 +20,19 @@ class Pop(GeneralRecommender): - r"""Pop is an fundamental model that always recommend the most popular item. - - """ + r"""Pop is an fundamental model that always recommend the most popular item.""" input_type = InputType.POINTWISE type = ModelType.TRADITIONAL def __init__(self, config, dataset): super(Pop, self).__init__(config, dataset) - self.item_cnt = torch.zeros(self.n_items, 1, dtype=torch.long, device=self.device, requires_grad=False) + self.item_cnt = torch.zeros( + self.n_items, 1, dtype=torch.long, device=self.device, requires_grad=False + ) self.max_cnt = None self.fake_loss = torch.nn.Parameter(torch.zeros(1)) - self.other_parameter_name = ['item_cnt', 'max_cnt'] + self.other_parameter_name = ["item_cnt", "max_cnt"] def forward(self): pass diff --git a/recbole/model/general_recommender/ract.py b/recbole/model/general_recommender/ract.py index 138b64e0e..e7aa9b9fc 100644 --- a/recbole/model/general_recommender/ract.py +++ b/recbole/model/general_recommender/ract.py @@ -32,9 +32,9 @@ def __init__(self, config, dataset): super(RaCT, self).__init__(config, dataset) self.layers = config["mlp_hidden_size"] - self.lat_dim = config['latent_dimension'] - self.drop_out = config['dropout_prob'] - self.anneal_cap = config['anneal_cap'] + self.lat_dim = config["latent_dimension"] + self.drop_out = config["dropout_prob"] + self.anneal_cap = config["anneal_cap"] self.total_anneal_steps = config["total_anneal_steps"] self.history_item_id, self.history_item_value, _ = dataset.history_item_matrix() @@ -44,7 +44,9 @@ def __init__(self, config, dataset): self.update = 0 self.encode_layer_dims = [self.n_items] + self.layers + [self.lat_dim] - self.decode_layer_dims = [int(self.lat_dim / 2)] + self.encode_layer_dims[::-1][1:] + self.decode_layer_dims = [int(self.lat_dim / 2)] + self.encode_layer_dims[::-1][ + 1: + ] self.encoder = self.mlp_layers(self.encode_layer_dims) self.decoder = self.mlp_layers(self.decode_layer_dims) @@ -60,20 +62,20 @@ def __init__(self, config, dataset): self.true_matrix = None self.critic_net = self.construct_critic_layers(self.critic_layer_dims) - self.train_stage = config['train_stage'] - self.pre_model_path = config['pre_model_path'] + self.train_stage = config["train_stage"] + self.pre_model_path = config["pre_model_path"] # parameters initialization - assert self.train_stage in ['actor_pretrain', 'critic_pretrain', 'finetune'] - if self.train_stage == 'actor_pretrain': + assert self.train_stage in ["actor_pretrain", "critic_pretrain", "finetune"] + if self.train_stage == "actor_pretrain": self.apply(xavier_normal_initialization) for p in self.critic_net.parameters(): p.requires_grad = False - elif self.train_stage == 'critic_pretrain': + elif self.train_stage == "critic_pretrain": # load pretrained model for finetune pretrained = torch.load(self.pre_model_path) - self.logger.info('Load pretrained model from', self.pre_model_path) - self.load_state_dict(pretrained['state_dict']) + self.logger.info("Load pretrained model from", self.pre_model_path) + self.load_state_dict(pretrained["state_dict"]) for p in self.encoder.parameters(): p.requires_grad = False for p in self.decoder.parameters(): @@ -81,8 +83,8 @@ def __init__(self, config, dataset): else: # load pretrained model for finetune pretrained = torch.load(self.pre_model_path) - self.logger.info('Load pretrained model from', self.pre_model_path) - self.load_state_dict(pretrained['state_dict']) + self.logger.info("Load pretrained model from", self.pre_model_path) + self.load_state_dict(pretrained["state_dict"]) for p in self.critic_net.parameters(): p.requires_grad = False @@ -97,10 +99,17 @@ def get_rating_matrix(self, user): """ # Following lines construct tensor of shape [B,n_items] using the tensor of shape [B,H] col_indices = self.history_item_id[user].flatten() - row_indices = torch.arange(user.shape[0]).to(self.device) \ + row_indices = ( + torch.arange(user.shape[0]) + .to(self.device) .repeat_interleave(self.history_item_id.shape[1], dim=0) - rating_matrix = torch.zeros(1).to(self.device).repeat(user.shape[0], self.n_items) - rating_matrix.index_put_((row_indices, col_indices), self.history_item_value[user].flatten()) + ) + rating_matrix = ( + torch.zeros(1).to(self.device).repeat(user.shape[0], self.n_items) + ) + rating_matrix.index_put_( + (row_indices, col_indices), self.history_item_value[user].flatten() + ) return rating_matrix def mlp_layers(self, layer_dims): @@ -129,12 +138,14 @@ def forward(self, rating_matrix): mask = (h > 0) * (t > 0) self.true_matrix = t * ~mask - self.number_of_unseen_items = (self.true_matrix != 0).sum(dim=1) # remaining input + self.number_of_unseen_items = (self.true_matrix != 0).sum( + dim=1 + ) # remaining input h = self.encoder(h) - mu = h[:, :int(self.lat_dim / 2)] - logvar = h[:, int(self.lat_dim / 2):] + mu = h[:, : int(self.lat_dim / 2)] + logvar = h[:, int(self.lat_dim / 2) :] z = self.reparameterize(mu, logvar) z = self.decoder(z) @@ -148,14 +159,16 @@ def calculate_actor_loss(self, interaction): self.update += 1 if self.total_anneal_steps > 0: - anneal = min(self.anneal_cap, 1. * self.update / self.total_anneal_steps) + anneal = min(self.anneal_cap, 1.0 * self.update / self.total_anneal_steps) else: anneal = self.anneal_cap z, mu, logvar = self.forward(rating_matrix) # KL loss - kl_loss = -0.5 * (torch.sum(1 + logvar - mu.pow(2) - logvar.exp(), dim=1)) * anneal + kl_loss = ( + -0.5 * (torch.sum(1 + logvar - mu.pow(2) - logvar.exp(), dim=1)) * anneal + ) # CE loss ce_loss = -(F.log_softmax(z, 1) * rating_matrix).sum(1) @@ -185,13 +198,19 @@ def calculate_ndcg(self, predict_matrix, true_matrix, input_matrix, k): predict_matrix[input_matrix.nonzero(as_tuple=True)] = -np.inf _, idx_sorted = torch.sort(predict_matrix, dim=1, descending=True) - topk_result = true_matrix[np.arange(users_num)[:, np.newaxis], idx_sorted[:, :k]] + topk_result = true_matrix[ + np.arange(users_num)[:, np.newaxis], idx_sorted[:, :k] + ] number_non_zero = ((true_matrix > 0) * 1).sum(dim=1) - tp = 1. / torch.log2(torch.arange(2, k + 2).type(torch.FloatTensor)).to(topk_result.device) + tp = 1.0 / torch.log2(torch.arange(2, k + 2).type(torch.FloatTensor)).to( + topk_result.device + ) DCG = (topk_result * tp).sum(dim=1) - IDCG = torch.Tensor([(tp[:min(n, k)]).sum() for n in number_non_zero]).to(topk_result.device) + IDCG = torch.Tensor([(tp[: min(n, k)]).sum() for n in number_non_zero]).to( + topk_result.device + ) IDCG = torch.maximum(0.1 * torch.ones_like(IDCG).to(IDCG.device), IDCG) return DCG / IDCG @@ -205,7 +224,9 @@ def critic_forward(self, actor_loss): def calculate_critic_loss(self, interaction): actor_loss = self.calculate_actor_loss(interaction) y = self.critic_forward(actor_loss) - score = self.calculate_ndcg(self.predict_matrix, self.true_matrix, self.input_matrix, self.metrics_k) + score = self.calculate_ndcg( + self.predict_matrix, self.true_matrix, self.input_matrix, self.metrics_k + ) mse_loss = (y - score) ** 2 return mse_loss @@ -218,10 +239,10 @@ def calculate_ac_loss(self, interaction): def calculate_loss(self, interaction): # actor_pretrain - if self.train_stage == 'actor_pretrain': + if self.train_stage == "actor_pretrain": return self.calculate_actor_loss(interaction).mean() # critic_pretrain - elif self.train_stage == 'critic_pretrain': + elif self.train_stage == "critic_pretrain": return self.calculate_critic_loss(interaction).mean() # finetune else: diff --git a/recbole/model/general_recommender/recvae.py b/recbole/model/general_recommender/recvae.py index 1c38f22e6..6c5abee10 100644 --- a/recbole/model/general_recommender/recvae.py +++ b/recbole/model/general_recommender/recvae.py @@ -39,7 +39,6 @@ def log_norm_pdf(x, mu, logvar): class CompositePrior(nn.Module): - def __init__(self, hidden_dim, latent_dim, input_dim, mixture_weights): super(CompositePrior, self).__init__() @@ -48,10 +47,14 @@ def __init__(self, hidden_dim, latent_dim, input_dim, mixture_weights): self.mu_prior = nn.Parameter(torch.Tensor(1, latent_dim), requires_grad=False) self.mu_prior.data.fill_(0) - self.logvar_prior = nn.Parameter(torch.Tensor(1, latent_dim), requires_grad=False) + self.logvar_prior = nn.Parameter( + torch.Tensor(1, latent_dim), requires_grad=False + ) self.logvar_prior.data.fill_(0) - self.logvar_uniform_prior = nn.Parameter(torch.Tensor(1, latent_dim), requires_grad=False) + self.logvar_uniform_prior = nn.Parameter( + torch.Tensor(1, latent_dim), requires_grad=False + ) self.logvar_uniform_prior.data.fill_(10) self.encoder_old = Encoder(hidden_dim, latent_dim, input_dim) @@ -73,7 +76,6 @@ def forward(self, x, z): class Encoder(nn.Module): - def __init__(self, hidden_dim, latent_dim, input_dim, eps=1e-1): super(Encoder, self).__init__() @@ -114,18 +116,20 @@ def __init__(self, config, dataset): super(RecVAE, self).__init__(config, dataset) self.hidden_dim = config["hidden_dimension"] - self.latent_dim = config['latent_dimension'] - self.dropout_prob = config['dropout_prob'] - self.beta = config['beta'] - self.mixture_weights = config['mixture_weights'] - self.gamma = config['gamma'] + self.latent_dim = config["latent_dimension"] + self.dropout_prob = config["dropout_prob"] + self.beta = config["beta"] + self.mixture_weights = config["mixture_weights"] + self.gamma = config["gamma"] self.history_item_id, self.history_item_value, _ = dataset.history_item_matrix() self.history_item_id = self.history_item_id.to(self.device) self.history_item_value = self.history_item_value.to(self.device) self.encoder = Encoder(self.hidden_dim, self.latent_dim, self.n_items) - self.prior = CompositePrior(self.hidden_dim, self.latent_dim, self.n_items, self.mixture_weights) + self.prior = CompositePrior( + self.hidden_dim, self.latent_dim, self.n_items, self.mixture_weights + ) self.decoder = nn.Linear(self.latent_dim, self.n_items) # parameters initialization @@ -142,10 +146,17 @@ def get_rating_matrix(self, user): """ # Following lines construct tensor of shape [B,n_items] using the tensor of shape [B,H] col_indices = self.history_item_id[user].flatten() - row_indices = torch.arange(user.shape[0]).to(self.device) \ + row_indices = ( + torch.arange(user.shape[0]) + .to(self.device) .repeat_interleave(self.history_item_id.shape[1], dim=0) - rating_matrix = torch.zeros(1).to(self.device).repeat(user.shape[0], self.n_items) - rating_matrix.index_put_((row_indices, col_indices), self.history_item_value[user].flatten()) + ) + rating_matrix = ( + torch.zeros(1).to(self.device).repeat(user.shape[0], self.n_items) + ) + rating_matrix.index_put_( + (row_indices, col_indices), self.history_item_value[user].flatten() + ) return rating_matrix def reparameterize(self, mu, logvar): @@ -178,7 +189,12 @@ def calculate_loss(self, interaction, encoder_flag): kl_weight = self.beta mll = (F.log_softmax(x_pred, dim=-1) * rating_matrix).sum(dim=-1).mean() - kld = (log_norm_pdf(z, mu, logvar) - self.prior(rating_matrix, z)).sum(dim=-1).mul(kl_weight).mean() + kld = ( + (log_norm_pdf(z, mu, logvar) - self.prior(rating_matrix, z)) + .sum(dim=-1) + .mul(kl_weight) + .mean() + ) negative_elbo = -(mll - kld) return negative_elbo diff --git a/recbole/model/general_recommender/sgl.py b/recbole/model/general_recommender/sgl.py index 453fefc1c..01ed4dcc9 100644 --- a/recbole/model/general_recommender/sgl.py +++ b/recbole/model/general_recommender/sgl.py @@ -26,11 +26,11 @@ class SGL(GeneralRecommender): r"""SGL is a GCN-based recommender model. - SGL supplements the classical supervised task of recommendation with an auxiliary + SGL supplements the classical supervised task of recommendation with an auxiliary self supervised task, which reinforces node representation learning via self- discrimination.Specifically,SGL generates multiple views of a node, maximizing the agreement between different views of the same node compared to that of other nodes. - SGL devises three operators to generate the views — node dropout, edge dropout, and + SGL devises three operators to generate the views — node dropout, edge dropout, and random walk — that change the graph structure in different manners. We implement the model following the original author with a pairwise training mode. @@ -55,12 +55,10 @@ def __init__(self, config, dataset): self.restore_user_e = None self.restore_item_e = None self.apply(xavier_uniform_initialization) - self.other_parameter_name = ['restore_user_e', 'restore_item_e'] + self.other_parameter_name = ["restore_user_e", "restore_item_e"] def graph_construction(self): - r"""Devise three operators to generate the views — node dropout, edge dropout, and random walk of a node. - - """ + r"""Devise three operators to generate the views — node dropout, edge dropout, and random walk of a node.""" self.sub_graph1 = [] if self.type == "ND" or self.type == "ED": self.sub_graph1 = self.csr2tensor(self.create_adjust_matrix(is_sub=True)) @@ -108,36 +106,58 @@ def create_adjust_matrix(self, is_sub: bool): matrix = None if not is_sub: ratings = np.ones_like(self._user, dtype=np.float32) - matrix = sp.csr_matrix((ratings, (self._user, self._item + self.n_users)), - shape=(self.n_users + self.n_items, self.n_users + self.n_items)) + matrix = sp.csr_matrix( + (ratings, (self._user, self._item + self.n_users)), + shape=(self.n_users + self.n_items, self.n_users + self.n_items), + ) else: if self.type == "ND": - drop_user = self.rand_sample(self.n_users, size=int(self.n_users * self.drop_ratio), replace=False) - drop_item = self.rand_sample(self.n_items, size=int(self.n_items * self.drop_ratio), replace=False) + drop_user = self.rand_sample( + self.n_users, + size=int(self.n_users * self.drop_ratio), + replace=False, + ) + drop_item = self.rand_sample( + self.n_items, + size=int(self.n_items * self.drop_ratio), + replace=False, + ) R_user = np.ones(self.n_users, dtype=np.float32) - R_user[drop_user] = 0. + R_user[drop_user] = 0.0 R_item = np.ones(self.n_items, dtype=np.float32) - R_item[drop_item] = 0. + R_item[drop_item] = 0.0 R_user = sp.diags(R_user) R_item = sp.diags(R_item) - R_G = sp.csr_matrix((np.ones_like(self._user, dtype=np.float32), (self._user, self._item)), - shape=(self.n_users, self.n_items)) + R_G = sp.csr_matrix( + ( + np.ones_like(self._user, dtype=np.float32), + (self._user, self._item), + ), + shape=(self.n_users, self.n_items), + ) res = R_user.dot(R_G) res = res.dot(R_item) user, item = res.nonzero() ratings = res.data - matrix = sp.csr_matrix((ratings, (user, item + self.n_users)), shape=(self.n_users + self.n_items, self.n_users + self.n_items)) + matrix = sp.csr_matrix( + (ratings, (user, item + self.n_users)), + shape=(self.n_users + self.n_items, self.n_users + self.n_items), + ) elif self.type == "ED" or self.type == "RW": keep_item = self.rand_sample( - len(self._user), size=int(len(self._user) * (1 - self.drop_ratio)), replace=False + len(self._user), + size=int(len(self._user) * (1 - self.drop_ratio)), + replace=False, ) user = self._user[keep_item] item = self._item[keep_item] - matrix = sp.csr_matrix((np.ones_like(user), (user, item + self.n_users)), - shape=(self.n_users + self.n_items, self.n_users + self.n_items)) + matrix = sp.csr_matrix( + (np.ones_like(user), (user, item + self.n_users)), + shape=(self.n_users + self.n_items, self.n_users + self.n_items), + ) matrix = matrix + matrix.T D = np.array(matrix.sum(axis=1)) + 1e-7 @@ -157,7 +177,8 @@ def csr2tensor(self, matrix: sp.csr_matrix): matrix = matrix.tocoo() x = torch.sparse.FloatTensor( torch.LongTensor(np.array([matrix.row, matrix.col])), - torch.FloatTensor(matrix.data.astype(np.float32)), matrix.shape + torch.FloatTensor(matrix.data.astype(np.float32)), + matrix.shape, ).to(self.device) return x @@ -181,18 +202,23 @@ def forward(self, graph): def calculate_loss(self, interaction): if self.restore_user_e is not None or self.restore_item_e is not None: self.restore_user_e, self.restore_item_e = None, None - + user_list = interaction[self.USER_ID] pos_item_list = interaction[self.ITEM_ID] neg_item_list = interaction[self.NEG_ITEM_ID] user_emd, item_emd = self.forward(self.train_graph) user_sub1, item_sub1 = self.forward(self.sub_graph1) user_sub2, item_sub2 = self.forward(self.sub_graph2) - total_loss = self.calc_bpr_loss(user_emd,item_emd,user_list,pos_item_list,neg_item_list) + \ - self.calc_ssl_loss(user_list,pos_item_list,user_sub1,user_sub2,item_sub1,item_sub2) + total_loss = self.calc_bpr_loss( + user_emd, item_emd, user_list, pos_item_list, neg_item_list + ) + self.calc_ssl_loss( + user_list, pos_item_list, user_sub1, user_sub2, item_sub1, item_sub2 + ) return total_loss - def calc_bpr_loss(self, user_emd, item_emd, user_list, pos_item_list, neg_item_list): + def calc_bpr_loss( + self, user_emd, item_emd, user_list, pos_item_list, neg_item_list + ): r"""Calculate the the pairwise Bayesian Personalized Ranking (BPR) loss and parameter regularization loss. Args: @@ -221,7 +247,9 @@ def calc_bpr_loss(self, user_emd, item_emd, user_list, pos_item_list, neg_item_l return l1 + l2 * self.reg_weight - def calc_ssl_loss(self, user_list, pos_item_list, user_sub1, user_sub2, item_sub1, item_sub2): + def calc_ssl_loss( + self, user_list, pos_item_list, user_sub1, user_sub2, item_sub1, item_sub2 + ): r"""Calculate the loss of self-supervised tasks. Args: @@ -238,7 +266,7 @@ def calc_ssl_loss(self, user_list, pos_item_list, user_sub1, user_sub2, item_sub u_emd1 = F.normalize(user_sub1[user_list], dim=1) u_emd2 = F.normalize(user_sub2[user_list], dim=1) - all_user2 = F.normalize(user_sub2,dim=1) + all_user2 = F.normalize(user_sub2, dim=1) v1 = torch.sum(u_emd1 * u_emd2, dim=1) v2 = u_emd1.matmul(all_user2.T) v1 = torch.exp(v1 / self.ssl_tau) @@ -247,7 +275,7 @@ def calc_ssl_loss(self, user_list, pos_item_list, user_sub1, user_sub2, item_sub i_emd1 = F.normalize(item_sub1[pos_item_list], dim=1) i_emd2 = F.normalize(item_sub2[pos_item_list], dim=1) - all_item2 = F.normalize(item_sub2,dim=1) + all_item2 = F.normalize(item_sub2, dim=1) v3 = torch.sum(i_emd1 * i_emd2, dim=1) v4 = i_emd1.matmul(all_item2.T) v3 = torch.exp(v3 / self.ssl_tau) @@ -267,14 +295,12 @@ def predict(self, interaction): def full_sort_predict(self, interaction): if self.restore_user_e is None or self.restore_item_e is None: self.restore_user_e, self.restore_item_e = self.forward(self.train_graph) - + user = self.restore_user_e[interaction[self.USER_ID]] return user.matmul(self.restore_item_e.T) def train(self, mode: bool = True): - r"""Override train method of base class.The subgraph is reconstructed each time it is called. - - """ + r"""Override train method of base class.The subgraph is reconstructed each time it is called.""" T = super().train(mode=mode) if mode: self.graph_construction() diff --git a/recbole/model/general_recommender/simplex.py b/recbole/model/general_recommender/simplex.py index f548ab707..10927911e 100644 --- a/recbole/model/general_recommender/simplex.py +++ b/recbole/model/general_recommender/simplex.py @@ -26,10 +26,10 @@ class SimpleX(GeneralRecommender): r"""SimpleX is a simple, unified collaborative filtering model. - SimpleX presents a simple and easy-to-understand model. Its advantage lies - in its loss function, which uses a larger number of negative samples and - sets a threshold to filter out less informative samples, it also uses - relative weights to control the balance of positive-sample loss + SimpleX presents a simple and easy-to-understand model. Its advantage lies + in its loss function, which uses a larger number of negative samples and + sets a threshold to filter out less informative samples, it also uses + relative weights to control the balance of positive-sample loss and negative-sample loss. We implement the model following the original author with a pairwise training mode. @@ -45,33 +45,34 @@ def __init__(self, config, dataset): self.history_item_len = self.history_item_len.to(self.device) # load parameters info - self.embedding_size = config['embedding_size'] - self.margin = config['margin'] - self.negative_weight = config['negative_weight'] - self.gamma = config['gamma'] - self.neg_seq_len = config['train_neg_sample_args']['sample_num'] - self.reg_weight = config['reg_weight'] - self.aggregator = config['aggregator'] - if self.aggregator not in ['mean', 'user_attention', 'self_attention']: - raise ValueError('aggregator must be mean, user_attention or self_attention') - self.history_len=min(config['history_len'],self.history_item_len.shape[0]) - + self.embedding_size = config["embedding_size"] + self.margin = config["margin"] + self.negative_weight = config["negative_weight"] + self.gamma = config["gamma"] + self.neg_seq_len = config["train_neg_sample_args"]["sample_num"] + self.reg_weight = config["reg_weight"] + self.aggregator = config["aggregator"] + if self.aggregator not in ["mean", "user_attention", "self_attention"]: + raise ValueError( + "aggregator must be mean, user_attention or self_attention" + ) + self.history_len = min(config["history_len"], self.history_item_len.shape[0]) + # user embedding matrix self.user_emb = nn.Embedding(self.n_users, self.embedding_size) # item embedding matrix - self.item_emb = nn.Embedding( - self.n_items, self.embedding_size, padding_idx=0) + self.item_emb = nn.Embedding(self.n_items, self.embedding_size, padding_idx=0) # feature space mapping matrix of user and item - self.UI_map = nn.Linear(self.embedding_size, - self.embedding_size, bias=False) - if self.aggregator in ['user_attention', 'self_attention']: - self.W_k = nn.Sequential(nn.Linear(self.embedding_size, self.embedding_size), - nn.Tanh()) - if self.aggregator == 'self_attention': + self.UI_map = nn.Linear(self.embedding_size, self.embedding_size, bias=False) + if self.aggregator in ["user_attention", "self_attention"]: + self.W_k = nn.Sequential( + nn.Linear(self.embedding_size, self.embedding_size), nn.Tanh() + ) + if self.aggregator == "self_attention": self.W_q = nn.Linear(self.embedding_size, 1, bias=False) # dropout self.dropout = nn.Dropout(0.1) - self.require_pow = config['require_pow'] + self.require_pow = config["require_pow"] # l2 regularization loss self.reg_loss = EmbLoss() @@ -79,51 +80,53 @@ def __init__(self, config, dataset): self.apply(xavier_normal_initialization) # get the mask self.item_emb.weight.data[0, :] = 0 - + def get_UI_aggregation(self, user_e, history_item_e, history_len): r"""Get the combined vector of user and historically interacted items Args: user_e (torch.Tensor): User's feature vector, shape: [user_num, embedding_size] - history_item_e (torch.Tensor): History item's feature vector, + history_item_e (torch.Tensor): History item's feature vector, shape: [user_num, max_history_len, embedding_size] history_len (torch.Tensor): User's history length, shape: [user_num] Returns: torch.Tensor: Combined vector of user and item sequences, shape: [user_num, embedding_size] """ - if self.aggregator == 'mean': + if self.aggregator == "mean": pos_item_sum = history_item_e.sum(dim=1) # [user_num, embedding_size] - out = pos_item_sum / (history_len + 1.e-10).unsqueeze(1) - elif self.aggregator in ['user_attention', 'self_attention']: + out = pos_item_sum / (history_len + 1.0e-10).unsqueeze(1) + elif self.aggregator in ["user_attention", "self_attention"]: # [user_num, max_history_len, embedding_size] key = self.W_k(history_item_e) - if self.aggregator == 'user_attention': + if self.aggregator == "user_attention": # [user_num, max_history_len] attention = torch.matmul(key, user_e.unsqueeze(2)).squeeze(2) - elif self.aggregator == 'self_attention': + elif self.aggregator == "self_attention": # [user_num, max_history_len] attention = self.W_q(key).squeeze(2) e_attention = torch.exp(attention) mask = (history_item_e.sum(dim=-1) != 0).int() - e_attention = e_attention*mask + e_attention = e_attention * mask # [user_num, max_history_len] - attention_weight = e_attention / (e_attention.sum(dim=1, keepdim=True)+1.e-10) + attention_weight = e_attention / ( + e_attention.sum(dim=1, keepdim=True) + 1.0e-10 + ) # [user_num, embedding_size] out = torch.matmul(attention_weight.unsqueeze(1), history_item_e).squeeze(1) # Combined vector of user and item sequences out = self.UI_map(out) g = self.gamma - UI_aggregation_e = g*user_e+(1-g)*out + UI_aggregation_e = g * user_e + (1 - g) * out return UI_aggregation_e def get_cos(self, user_e, item_e): r"""Get the cosine similarity between user and item - Args: + Args: user_e (torch.Tensor): User's feature vector, shape: [user_num, embedding_size] - item_e (torch.Tensor): Item's feature vector, + item_e (torch.Tensor): Item's feature vector, shape: [user_num, item_num, embedding_size] Returns: @@ -166,24 +169,29 @@ def forward(self, user, pos_item, history_item, history_len, neg_item_seq): neg_cos = self.get_cos(UI_aggregation_e, neg_item_seq_e) # CCL loss - pos_loss = torch.relu(1-pos_cos) - neg_loss = torch.relu(neg_cos-self.margin) - neg_loss = neg_loss.mean(1, keepdim=True)*self.negative_weight - CCL_loss = (pos_loss+neg_loss).mean() + pos_loss = torch.relu(1 - pos_cos) + neg_loss = torch.relu(neg_cos - self.margin) + neg_loss = neg_loss.mean(1, keepdim=True) * self.negative_weight + CCL_loss = (pos_loss + neg_loss).mean() # l2 regularization loss reg_loss = self.reg_loss( - user_e, pos_item_e, history_item_e, neg_item_seq_e, require_pow=self.require_pow) - - loss = CCL_loss+self.reg_weight*reg_loss.sum() + user_e, + pos_item_e, + history_item_e, + neg_item_seq_e, + require_pow=self.require_pow, + ) + + loss = CCL_loss + self.reg_weight * reg_loss.sum() return loss def calculate_loss(self, interaction): r"""Data processing and call function forward(), return loss - To use SimpleX, a user must have a historical transaction record, - a pos item and a sequence of neg items. Based on the RecBole - framework, the data in the interaction object is ordered, so + To use SimpleX, a user must have a historical transaction record, + a pos item and a sequence of neg items. Based on the RecBole + framework, the data in the interaction object is ordered, so we can get the data quickly. """ user = interaction[self.USER_ID] @@ -193,18 +201,19 @@ def calculate_loss(self, interaction): # get the sequence of neg items neg_item_seq = neg_item.reshape((self.neg_seq_len, -1)) neg_item_seq = neg_item_seq.T - user_number = int(len(user)/self.neg_seq_len) + user_number = int(len(user) / self.neg_seq_len) # user's id user = user[0:user_number] # historical transaction record history_item = self.history_item_id[user] - history_item = history_item[:, :self.history_len] + history_item = history_item[:, : self.history_len] # positive item's id pos_item = pos_item[0:user_number] # history_len history_len = self.history_item_len[user] history_len = torch.minimum( - history_len, torch.zeros(1, device=self.device)+self.history_len) + history_len, torch.zeros(1, device=self.device) + self.history_len + ) loss = self.forward(user, pos_item, history_item, history_len, neg_item_seq) return loss @@ -212,10 +221,11 @@ def calculate_loss(self, interaction): def predict(self, interaction): user = interaction[self.USER_ID] history_item = self.history_item_id[user] - history_item = history_item[:, :self.history_len] + history_item = history_item[:, : self.history_len] history_len = self.history_item_len[user] - history_len = torch.minimum(history_len, torch.zeros( - 1, device=self.device)+self.history_len) + history_len = torch.minimum( + history_len, torch.zeros(1, device=self.device) + self.history_len + ) test_item = interaction[self.ITEM_ID] # [user_num, embedding_size] @@ -234,10 +244,11 @@ def predict(self, interaction): def full_sort_predict(self, interaction): user = interaction[self.USER_ID] history_item = self.history_item_id[user] - history_item = history_item[:, :self.history_len] + history_item = history_item[:, : self.history_len] history_len = self.history_item_len[user] history_len = torch.minimum( - history_len, torch.zeros(1, device=self.device)+self.history_len) + history_len, torch.zeros(1, device=self.device) + self.history_len + ) # [user_num, embedding_size] user_e = self.user_emb(user) diff --git a/recbole/model/general_recommender/slimelastic.py b/recbole/model/general_recommender/slimelastic.py index f28b14fca..18b4033d0 100644 --- a/recbole/model/general_recommender/slimelastic.py +++ b/recbole/model/general_recommender/slimelastic.py @@ -23,7 +23,7 @@ class SLIMElastic(GeneralRecommender): r"""SLIMElastic is a sparse linear method for top-K recommendation, which learns a sparse aggregation coefficient matrix by solving an L1-norm and L2-norm - regularized optimization problem. + regularized optimization problem. """ input_type = InputType.POINTWISE @@ -33,15 +33,15 @@ def __init__(self, config, dataset): super().__init__(config, dataset) # load parameters info - self.hide_item = config['hide_item'] - self.alpha = config['alpha'] - self.l1_ratio = config['l1_ratio'] - self.positive_only = config['positive_only'] + self.hide_item = config["hide_item"] + self.alpha = config["alpha"] + self.l1_ratio = config["l1_ratio"] + self.positive_only = config["positive_only"] # need at least one param self.dummy_param = torch.nn.Parameter(torch.zeros(1)) - X = dataset.inter_matrix(form='csr').astype(np.float32) + X = dataset.inter_matrix(form="csr").astype(np.float32) X = X.tolil() self.interaction_matrix = X @@ -52,9 +52,9 @@ def __init__(self, config, dataset): fit_intercept=False, copy_X=False, precompute=True, - selection='random', + selection="random", max_iter=100, - tol=1e-4 + tol=1e-4, ) item_coeffs = [] @@ -83,7 +83,7 @@ def __init__(self, config, dataset): X[:, j] = r self.item_similarity = sp.vstack(item_coeffs).T - self.other_parameter_name = ['interaction_matrix', 'item_similarity'] + self.other_parameter_name = ["interaction_matrix", "item_similarity"] def forward(self): pass @@ -96,7 +96,9 @@ def predict(self, interaction): item = interaction[self.ITEM_ID].cpu().numpy() r = torch.from_numpy( - (self.interaction_matrix[user, :].multiply(self.item_similarity[:, item].T)).sum(axis=1).getA1() + (self.interaction_matrix[user, :].multiply(self.item_similarity[:, item].T)) + .sum(axis=1) + .getA1() ) return r diff --git a/recbole/model/general_recommender/spectralcf.py b/recbole/model/general_recommender/spectralcf.py index a3333de63..be95471ef 100644 --- a/recbole/model/general_recommender/spectralcf.py +++ b/recbole/model/general_recommender/spectralcf.py @@ -52,27 +52,36 @@ def __init__(self, config, dataset): super(SpectralCF, self).__init__(config, dataset) # load parameters info - self.n_layers = config['n_layers'] - self.emb_dim = config['embedding_size'] - self.reg_weight = config['reg_weight'] + self.n_layers = config["n_layers"] + self.emb_dim = config["embedding_size"] + self.reg_weight = config["reg_weight"] # generate intermediate data # "A_hat = I + L" is equivalent to "A_hat = U U^T + U \Lambda U^T" - self.interaction_matrix = dataset.inter_matrix(form='coo').astype(np.float32) + self.interaction_matrix = dataset.inter_matrix(form="coo").astype(np.float32) I = self.get_eye_mat(self.n_items + self.n_users) L = self.get_laplacian_matrix() A_hat = I + L self.A_hat = A_hat.to(self.device) # define layers and loss - self.user_embedding = torch.nn.Embedding(num_embeddings=self.n_users, embedding_dim=self.emb_dim) - self.item_embedding = torch.nn.Embedding(num_embeddings=self.n_items, embedding_dim=self.emb_dim) - self.filters = torch.nn.ParameterList([ - torch.nn.Parameter( - torch.normal(mean=0.01, std=0.02, size=(self.emb_dim, self.emb_dim)).to(self.device), - requires_grad=True - ) for _ in range(self.n_layers) - ]) + self.user_embedding = torch.nn.Embedding( + num_embeddings=self.n_users, embedding_dim=self.emb_dim + ) + self.item_embedding = torch.nn.Embedding( + num_embeddings=self.n_items, embedding_dim=self.emb_dim + ) + self.filters = torch.nn.ParameterList( + [ + torch.nn.Parameter( + torch.normal( + mean=0.01, std=0.02, size=(self.emb_dim, self.emb_dim) + ).to(self.device), + requires_grad=True, + ) + for _ in range(self.n_layers) + ] + ) self.sigmoid = torch.nn.Sigmoid() self.mf_loss = BPRLoss() @@ -80,7 +89,7 @@ def __init__(self, config, dataset): self.restore_user_e = None self.restore_item_e = None - self.other_parameter_name = ['restore_user_e', 'restore_item_e'] + self.other_parameter_name = ["restore_user_e", "restore_item_e"] # parameters initialization self.apply(xavier_uniform_initialization) @@ -94,11 +103,22 @@ def get_laplacian_matrix(self): Sparse tensor of the laplacian matrix. """ # build adj matrix - A = sp.dok_matrix((self.n_users + self.n_items, self.n_users + self.n_items), dtype=np.float32) + A = sp.dok_matrix( + (self.n_users + self.n_items, self.n_users + self.n_items), dtype=np.float32 + ) inter_M = self.interaction_matrix inter_M_t = self.interaction_matrix.transpose() - data_dict = dict(zip(zip(inter_M.row, inter_M.col + self.n_users), [1] * inter_M.nnz)) - data_dict.update(dict(zip(zip(inter_M_t.row + self.n_users, inter_M_t.col), [1] * inter_M_t.nnz))) + data_dict = dict( + zip(zip(inter_M.row, inter_M.col + self.n_users), [1] * inter_M.nnz) + ) + data_dict.update( + dict( + zip( + zip(inter_M_t.row + self.n_users, inter_M_t.col), + [1] * inter_M_t.nnz, + ) + ) + ) A._update(data_dict) # norm adj matrix @@ -154,7 +174,9 @@ def forward(self): embeddings_list.append(all_embeddings) new_embeddings = torch.cat(embeddings_list, dim=1) - user_all_embeddings, item_all_embeddings = torch.split(new_embeddings, [self.n_users, self.n_items]) + user_all_embeddings, item_all_embeddings = torch.split( + new_embeddings, [self.n_users, self.n_items] + ) return user_all_embeddings, item_all_embeddings def calculate_loss(self, interaction): diff --git a/recbole/model/init.py b/recbole/model/init.py index 18975d6b9..8ea030ea9 100644 --- a/recbole/model/init.py +++ b/recbole/model/init.py @@ -13,7 +13,7 @@ def xavier_normal_initialization(module): - r""" using `xavier_normal_`_ in PyTorch to initialize the parameters in + r"""using `xavier_normal_`_ in PyTorch to initialize the parameters in nn.Embedding and nn.Linear layers. For bias in nn.Linear layers, using constant 0 to initialize. @@ -32,7 +32,7 @@ def xavier_normal_initialization(module): def xavier_uniform_initialization(module): - r""" using `xavier_uniform_`_ in PyTorch to initialize the parameters in + r"""using `xavier_uniform_`_ in PyTorch to initialize the parameters in nn.Embedding and nn.Linear layers. For bias in nn.Linear layers, using constant 0 to initialize. diff --git a/recbole/model/knowledge_aware_recommender/cfkg.py b/recbole/model/knowledge_aware_recommender/cfkg.py index dc21aa935..9705ba28c 100644 --- a/recbole/model/knowledge_aware_recommender/cfkg.py +++ b/recbole/model/knowledge_aware_recommender/cfkg.py @@ -40,17 +40,21 @@ def __init__(self, config, dataset): super(CFKG, self).__init__(config, dataset) # load parameters info - self.embedding_size = config['embedding_size'] - self.loss_function = config['loss_function'] - self.margin = config['margin'] - assert self.loss_function in ['inner_product', 'transe'] + self.embedding_size = config["embedding_size"] + self.loss_function = config["loss_function"] + self.margin = config["margin"] + assert self.loss_function in ["inner_product", "transe"] # define layers and loss self.user_embedding = nn.Embedding(self.n_users, self.embedding_size) self.entity_embedding = nn.Embedding(self.n_entities, self.embedding_size) - self.relation_embedding = nn.Embedding(self.n_relations + 1, self.embedding_size) - if self.loss_function == 'transe': - self.rec_loss = nn.TripletMarginLoss(margin=self.margin, p=2, reduction='mean') + self.relation_embedding = nn.Embedding( + self.n_relations + 1, self.embedding_size + ) + if self.loss_function == "transe": + self.rec_loss = nn.TripletMarginLoss( + margin=self.margin, p=2, reduction="mean" + ) else: self.rec_loss = InnerProductLoss() @@ -82,7 +86,7 @@ def _get_kg_embedding(self, head, pos_tail, neg_tail, relation): return head_e, pos_tail_e, neg_tail_e, relation_e def _get_score(self, h_e, t_e, r_e): - if self.loss_function == 'transe': + if self.loss_function == "transe": return -torch.norm(h_e + r_e - t_e, p=2, dim=1) else: return torch.mul(h_e + r_e, t_e).sum(dim=1) @@ -96,8 +100,12 @@ def calculate_loss(self, interaction): pos_tail = interaction[self.TAIL_ENTITY_ID] neg_tail = interaction[self.NEG_TAIL_ENTITY_ID] - user_e, pos_item_e, neg_item_e, rec_r_e = self._get_rec_embedding(user, pos_item, neg_item) - head_e, pos_tail_e, neg_tail_e, relation_e = self._get_kg_embedding(head, pos_tail, neg_tail, relation) + user_e, pos_item_e, neg_item_e, rec_r_e = self._get_rec_embedding( + user, pos_item, neg_item + ) + head_e, pos_tail_e, neg_tail_e, relation_e = self._get_kg_embedding( + head, pos_tail, neg_tail, relation + ) h_e = torch.cat([user_e, head_e]) r_e = torch.cat([rec_r_e, relation_e]) @@ -115,8 +123,7 @@ def predict(self, interaction): class InnerProductLoss(nn.Module): - r"""This is the inner-product loss used in CFKG for optimization. - """ + r"""This is the inner-product loss used in CFKG for optimization.""" def __init__(self): super(InnerProductLoss, self).__init__() diff --git a/recbole/model/knowledge_aware_recommender/cke.py b/recbole/model/knowledge_aware_recommender/cke.py index eff8138c0..978b9ad1b 100644 --- a/recbole/model/knowledge_aware_recommender/cke.py +++ b/recbole/model/knowledge_aware_recommender/cke.py @@ -36,16 +36,18 @@ def __init__(self, config, dataset): super(CKE, self).__init__(config, dataset) # load parameters info - self.embedding_size = config['embedding_size'] - self.kg_embedding_size = config['kg_embedding_size'] - self.reg_weights = config['reg_weights'] + self.embedding_size = config["embedding_size"] + self.kg_embedding_size = config["kg_embedding_size"] + self.reg_weights = config["reg_weights"] # define layers and loss self.user_embedding = nn.Embedding(self.n_users, self.embedding_size) self.item_embedding = nn.Embedding(self.n_items, self.embedding_size) self.entity_embedding = nn.Embedding(self.n_entities, self.embedding_size) self.relation_embedding = nn.Embedding(self.n_relations, self.kg_embedding_size) - self.trans_w = nn.Embedding(self.n_relations, self.embedding_size * self.kg_embedding_size) + self.trans_w = nn.Embedding( + self.n_relations, self.embedding_size * self.kg_embedding_size + ) self.rec_loss = BPRLoss() self.kg_loss = BPRLoss() self.reg_loss = EmbLoss() @@ -58,7 +60,9 @@ def _get_kg_embedding(self, h, r, pos_t, neg_t): pos_t_e = self.entity_embedding(pos_t).unsqueeze(1) neg_t_e = self.entity_embedding(neg_t).unsqueeze(1) r_e = self.relation_embedding(r) - r_trans_w = self.trans_w(r).view(r.size(0), self.embedding_size, self.kg_embedding_size) + r_trans_w = self.trans_w(r).view( + r.size(0), self.embedding_size, self.kg_embedding_size + ) h_e = torch.bmm(h_e, r_trans_w).squeeze(1) pos_t_e = torch.bmm(pos_t_e, r_trans_w).squeeze(1) @@ -108,11 +112,14 @@ def calculate_loss(self, interaction): rec_loss = self._get_rec_loss(user_e, pos_item_final_e, neg_item_final_e) - h_e, r_e, pos_t_e, neg_t_e, r_trans_w = self._get_kg_embedding(h, r, pos_t, neg_t) + h_e, r_e, pos_t_e, neg_t_e, r_trans_w = self._get_kg_embedding( + h, r, pos_t, neg_t + ) kg_loss = self._get_kg_loss(h_e, r_e, pos_t_e, neg_t_e) - reg_loss = self.reg_weights[0] * self.reg_loss(user_e, pos_item_final_e, neg_item_final_e) + \ - self.reg_weights[1] * self.reg_loss(h_e, r_e, pos_t_e, neg_t_e) + reg_loss = self.reg_weights[0] * self.reg_loss( + user_e, pos_item_final_e, neg_item_final_e + ) + self.reg_weights[1] * self.reg_loss(h_e, r_e, pos_t_e, neg_t_e) return rec_loss, kg_loss, reg_loss @@ -124,6 +131,8 @@ def predict(self, interaction): def full_sort_predict(self, interaction): user = interaction[self.USER_ID] user_e = self.user_embedding(user) - all_item_e = self.item_embedding.weight + self.entity_embedding.weight[:self.n_items] + all_item_e = ( + self.item_embedding.weight + self.entity_embedding.weight[: self.n_items] + ) score = torch.matmul(user_e, all_item_e.transpose(0, 1)) return score.view(-1) diff --git a/recbole/model/knowledge_aware_recommender/kgat.py b/recbole/model/knowledge_aware_recommender/kgat.py index dd181389d..0bd8f5719 100644 --- a/recbole/model/knowledge_aware_recommender/kgat.py +++ b/recbole/model/knowledge_aware_recommender/kgat.py @@ -26,8 +26,7 @@ class Aggregator(nn.Module): - """ GNN Aggregator layer - """ + """GNN Aggregator layer""" def __init__(self, input_dim, output_dim, dropout, aggregator_type): super(Aggregator, self).__init__() @@ -38,11 +37,11 @@ def __init__(self, input_dim, output_dim, dropout, aggregator_type): self.message_dropout = nn.Dropout(dropout) - if self.aggregator_type == 'gcn': + if self.aggregator_type == "gcn": self.W = nn.Linear(self.input_dim, self.output_dim) - elif self.aggregator_type == 'graphsage': + elif self.aggregator_type == "graphsage": self.W = nn.Linear(self.input_dim * 2, self.output_dim) - elif self.aggregator_type == 'bi': + elif self.aggregator_type == "bi": self.W1 = nn.Linear(self.input_dim, self.output_dim) self.W2 = nn.Linear(self.input_dim, self.output_dim) else: @@ -53,11 +52,13 @@ def __init__(self, input_dim, output_dim, dropout, aggregator_type): def forward(self, norm_matrix, ego_embeddings): side_embeddings = torch.sparse.mm(norm_matrix, ego_embeddings) - if self.aggregator_type == 'gcn': + if self.aggregator_type == "gcn": ego_embeddings = self.activation(self.W(ego_embeddings + side_embeddings)) - elif self.aggregator_type == 'graphsage': - ego_embeddings = self.activation(self.W(torch.cat([ego_embeddings, side_embeddings], dim=1))) - elif self.aggregator_type == 'bi': + elif self.aggregator_type == "graphsage": + ego_embeddings = self.activation( + self.W(torch.cat([ego_embeddings, side_embeddings], dim=1)) + ) + elif self.aggregator_type == "bi": add_embeddings = ego_embeddings + side_embeddings sum_embeddings = self.activation(self.W1(add_embeddings)) bi_embeddings = torch.mul(ego_embeddings, side_embeddings) @@ -83,31 +84,49 @@ def __init__(self, config, dataset): super(KGAT, self).__init__(config, dataset) # load dataset info - self.ckg = dataset.ckg_graph(form='dgl', value_field='relation_id') - self.all_hs = torch.LongTensor(dataset.ckg_graph(form='coo', value_field='relation_id').row).to(self.device) - self.all_ts = torch.LongTensor(dataset.ckg_graph(form='coo', value_field='relation_id').col).to(self.device) - self.all_rs = torch.LongTensor(dataset.ckg_graph(form='coo', value_field='relation_id').data).to(self.device) - self.matrix_size = torch.Size([self.n_users + self.n_entities, self.n_users + self.n_entities]) + self.ckg = dataset.ckg_graph(form="dgl", value_field="relation_id") + self.all_hs = torch.LongTensor( + dataset.ckg_graph(form="coo", value_field="relation_id").row + ).to(self.device) + self.all_ts = torch.LongTensor( + dataset.ckg_graph(form="coo", value_field="relation_id").col + ).to(self.device) + self.all_rs = torch.LongTensor( + dataset.ckg_graph(form="coo", value_field="relation_id").data + ).to(self.device) + self.matrix_size = torch.Size( + [self.n_users + self.n_entities, self.n_users + self.n_entities] + ) # load parameters info - self.embedding_size = config['embedding_size'] - self.kg_embedding_size = config['kg_embedding_size'] - self.layers = [self.embedding_size] + config['layers'] - self.aggregator_type = config['aggregator_type'] - self.mess_dropout = config['mess_dropout'] - self.reg_weight = config['reg_weight'] + self.embedding_size = config["embedding_size"] + self.kg_embedding_size = config["kg_embedding_size"] + self.layers = [self.embedding_size] + config["layers"] + self.aggregator_type = config["aggregator_type"] + self.mess_dropout = config["mess_dropout"] + self.reg_weight = config["reg_weight"] # generate intermediate data - self.A_in = self.init_graph() # init the attention matrix by the structure of ckg + self.A_in = ( + self.init_graph() + ) # init the attention matrix by the structure of ckg # define layers and loss self.user_embedding = nn.Embedding(self.n_users, self.embedding_size) self.entity_embedding = nn.Embedding(self.n_entities, self.embedding_size) self.relation_embedding = nn.Embedding(self.n_relations, self.kg_embedding_size) - self.trans_w = nn.Embedding(self.n_relations, self.embedding_size * self.kg_embedding_size) + self.trans_w = nn.Embedding( + self.n_relations, self.embedding_size * self.kg_embedding_size + ) self.aggregator_layers = nn.ModuleList() - for idx, (input_dim, output_dim) in enumerate(zip(self.layers[:-1], self.layers[1:])): - self.aggregator_layers.append(Aggregator(input_dim, output_dim, self.mess_dropout, self.aggregator_type)) + for idx, (input_dim, output_dim) in enumerate( + zip(self.layers[:-1], self.layers[1:]) + ): + self.aggregator_layers.append( + Aggregator( + input_dim, output_dim, self.mess_dropout, self.aggregator_type + ) + ) self.tanh = nn.Tanh() self.mf_loss = BPRLoss() self.reg_loss = EmbLoss() @@ -116,7 +135,7 @@ def __init__(self, config, dataset): # parameters initialization self.apply(xavier_normal_initialization) - self.other_parameter_name = ['restore_user_e', 'restore_entity_e'] + self.other_parameter_name = ["restore_user_e", "restore_entity_e"] def init_graph(self): r"""Get the initial attention matrix through the collaborative knowledge graph @@ -125,14 +144,20 @@ def init_graph(self): torch.sparse.FloatTensor: Sparse tensor of the attention matrix """ import dgl + adj_list = [] for rel_type in range(1, self.n_relations, 1): - edge_idxs = self.ckg.filter_edges(lambda edge: edge.data['relation_id'] == rel_type) - sub_graph = dgl.edge_subgraph(self.ckg, edge_idxs, preserve_nodes=True). \ - adjacency_matrix(transpose=False, scipy_fmt='coo').astype('float') + edge_idxs = self.ckg.filter_edges( + lambda edge: edge.data["relation_id"] == rel_type + ) + sub_graph = ( + dgl.edge_subgraph(self.ckg, edge_idxs, preserve_nodes=True) + .adjacency_matrix(transpose=False, scipy_fmt="coo") + .astype("float") + ) rowsum = np.array(sub_graph.sum(1)) d_inv = np.power(rowsum, -1).flatten() - d_inv[np.isinf(d_inv)] = 0. + d_inv[np.isinf(d_inv)] = 0.0 d_mat_inv = sp.diags(d_inv) norm_adj = d_mat_inv.dot(sub_graph).tocoo() adj_list.append(norm_adj) @@ -157,7 +182,9 @@ def forward(self): norm_embeddings = F.normalize(ego_embeddings, p=2, dim=1) embeddings_list.append(norm_embeddings) kgat_all_embeddings = torch.cat(embeddings_list, dim=1) - user_all_embeddings, entity_all_embeddings = torch.split(kgat_all_embeddings, [self.n_users, self.n_entities]) + user_all_embeddings, entity_all_embeddings = torch.split( + kgat_all_embeddings, [self.n_users, self.n_entities] + ) return user_all_embeddings, entity_all_embeddings def _get_kg_embedding(self, h, r, pos_t, neg_t): @@ -165,7 +192,9 @@ def _get_kg_embedding(self, h, r, pos_t, neg_t): pos_t_e = self.entity_embedding(pos_t).unsqueeze(1) neg_t_e = self.entity_embedding(neg_t).unsqueeze(1) r_e = self.relation_embedding(r) - r_trans_w = self.trans_w(r).view(r.size(0), self.embedding_size, self.kg_embedding_size) + r_trans_w = self.trans_w(r).view( + r.size(0), self.embedding_size, self.kg_embedding_size + ) h_e = torch.bmm(h_e, r_trans_w).squeeze(1) pos_t_e = torch.bmm(pos_t_e, r_trans_w).squeeze(1) @@ -239,7 +268,9 @@ def generate_transE_score(self, hs, ts, r): h_e = all_embeddings[hs] t_e = all_embeddings[ts] r_e = self.relation_embedding.weight[r] - r_trans_w = self.trans_w.weight[r].view(self.embedding_size, self.kg_embedding_size) + r_trans_w = self.trans_w.weight[r].view( + self.embedding_size, self.kg_embedding_size + ) h_e = torch.matmul(h_e, r_trans_w) t_e = torch.matmul(t_e, r_trans_w) @@ -249,15 +280,15 @@ def generate_transE_score(self, hs, ts, r): return kg_score def update_attentive_A(self): - r"""Update the attention matrix using the updated embedding matrix - - """ + r"""Update the attention matrix using the updated embedding matrix""" kg_score_list, row_list, col_list = [], [], [] # To reduce the GPU memory consumption, we calculate the scores of KG triples according to the type of relation for rel_idx in range(1, self.n_relations, 1): triple_index = torch.where(self.all_rs == rel_idx) - kg_score = self.generate_transE_score(self.all_hs[triple_index], self.all_ts[triple_index], rel_idx) + kg_score = self.generate_transE_score( + self.all_hs[triple_index], self.all_ts[triple_index], rel_idx + ) row_list.append(self.all_hs[triple_index]) col_list.append(self.all_ts[triple_index]) kg_score_list.append(kg_score) @@ -286,7 +317,7 @@ def full_sort_predict(self, interaction): if self.restore_user_e is None or self.restore_entity_e is None: self.restore_user_e, self.restore_entity_e = self.forward() u_embeddings = self.restore_user_e[user] - i_embeddings = self.restore_entity_e[:self.n_items] + i_embeddings = self.restore_entity_e[: self.n_items] scores = torch.matmul(u_embeddings, i_embeddings.transpose(0, 1)) diff --git a/recbole/model/knowledge_aware_recommender/kgcn.py b/recbole/model/knowledge_aware_recommender/kgcn.py index 7711ea376..015c3e386 100644 --- a/recbole/model/knowledge_aware_recommender/kgcn.py +++ b/recbole/model/knowledge_aware_recommender/kgcn.py @@ -37,22 +37,26 @@ def __init__(self, config, dataset): super(KGCN, self).__init__(config, dataset) # load parameters info - self.embedding_size = config['embedding_size'] + self.embedding_size = config["embedding_size"] # number of iterations when computing entity representation - self.n_iter = config['n_iter'] - self.aggregator_class = config['aggregator'] # which aggregator to use - self.reg_weight = config['reg_weight'] # weight of l2 regularization - self.neighbor_sample_size = config['neighbor_sample_size'] + self.n_iter = config["n_iter"] + self.aggregator_class = config["aggregator"] # which aggregator to use + self.reg_weight = config["reg_weight"] # weight of l2 regularization + self.neighbor_sample_size = config["neighbor_sample_size"] # define embedding self.user_embedding = nn.Embedding(self.n_users, self.embedding_size) self.entity_embedding = nn.Embedding(self.n_entities, self.embedding_size) - self.relation_embedding = nn.Embedding(self.n_relations + 1, self.embedding_size) + self.relation_embedding = nn.Embedding( + self.n_relations + 1, self.embedding_size + ) # sample neighbors - kg_graph = dataset.kg_graph(form='coo', value_field='relation_id') + kg_graph = dataset.kg_graph(form="coo", value_field="relation_id") adj_entity, adj_relation = self.construct_adj(kg_graph) - self.adj_entity, self.adj_relation = adj_entity.to(self.device), adj_relation.to(self.device) + self.adj_entity, self.adj_relation = adj_entity.to( + self.device + ), adj_relation.to(self.device) # define function self.softmax = nn.Softmax(dim=-1) @@ -60,8 +64,10 @@ def __init__(self, config, dataset): for i in range(self.n_iter): self.linear_layers.append( nn.Linear( - self.embedding_size if not self.aggregator_class == 'concat' else self.embedding_size * 2, self.embedding_size + if not self.aggregator_class == "concat" + else self.embedding_size * 2, + self.embedding_size, ) ) self.ReLU = nn.ReLU() @@ -72,7 +78,7 @@ def __init__(self, config, dataset): # parameters initialization self.apply(xavier_normal_initialization) - self.other_parameter_name = ['adj_entity', 'adj_relation'] + self.other_parameter_name = ["adj_entity", "adj_relation"] def construct_adj(self, kg_graph): r"""Get neighbors and corresponding relations for each entity in the KG. @@ -117,11 +123,15 @@ def construct_adj(self, kg_graph): n_neighbors = len(neighbors) if n_neighbors >= self.neighbor_sample_size: sampled_indices = np.random.choice( - list(range(n_neighbors)), size=self.neighbor_sample_size, replace=False + list(range(n_neighbors)), + size=self.neighbor_sample_size, + replace=False, ) else: sampled_indices = np.random.choice( - list(range(n_neighbors)), size=self.neighbor_sample_size, replace=True + list(range(n_neighbors)), + size=self.neighbor_sample_size, + replace=True, ) adj_entity[entity] = np.array([neighbors[i][0] for i in sampled_indices]) adj_relation[entity] = np.array([neighbors[i][1] for i in sampled_indices]) @@ -150,13 +160,19 @@ def get_neighbors(self, items): relations = [] for i in range(self.n_iter): index = torch.flatten(entities[i]) - neighbor_entities = torch.index_select(self.adj_entity, 0, index).reshape(self.batch_size, -1) - neighbor_relations = torch.index_select(self.adj_relation, 0, index).reshape(self.batch_size, -1) + neighbor_entities = torch.index_select(self.adj_entity, 0, index).reshape( + self.batch_size, -1 + ) + neighbor_relations = torch.index_select( + self.adj_relation, 0, index + ).reshape(self.batch_size, -1) entities.append(neighbor_entities) relations.append(neighbor_relations) return entities, relations - def mix_neighbor_vectors(self, neighbor_vectors, neighbor_relations, user_embeddings): + def mix_neighbor_vectors( + self, neighbor_vectors, neighbor_relations, user_embeddings + ): r"""Mix neighbor vectors on user-specific graph. Args: @@ -179,7 +195,9 @@ def mix_neighbor_vectors(self, neighbor_vectors, neighbor_relations, user_embedd user_relation_scores = torch.mean( user_embeddings * neighbor_relations, dim=-1 ) # [batch_size, -1, n_neighbor] - user_relation_scores_normalized = self.softmax(user_relation_scores) # [batch_size, -1, n_neighbor] + user_relation_scores_normalized = self.softmax( + user_relation_scores + ) # [batch_size, -1, n_neighbor] user_relation_scores_normalized = torch.unsqueeze( user_relation_scores_normalized, dim=-1 @@ -188,7 +206,9 @@ def mix_neighbor_vectors(self, neighbor_vectors, neighbor_relations, user_embedd user_relation_scores_normalized * neighbor_vectors, dim=2 ) # [batch_size, -1, dim] else: - neighbors_aggregated = torch.mean(neighbor_vectors, dim=2) # [batch_size, -1, dim] + neighbors_aggregated = torch.mean( + neighbor_vectors, dim=2 + ) # [batch_size, -1, dim] return neighbors_aggregated def aggregate(self, user_embeddings, entities, relations): @@ -215,7 +235,12 @@ def aggregate(self, user_embeddings, entities, relations): for i in range(self.n_iter): entity_vectors_next_iter = [] for hop in range(self.n_iter - i): - shape = (self.batch_size, -1, self.neighbor_sample_size, self.embedding_size) + shape = ( + self.batch_size, + -1, + self.neighbor_sample_size, + self.embedding_size, + ) self_vectors = entity_vectors[hop] neighbor_vectors = entity_vectors[hop + 1].reshape(shape) neighbor_relations = relation_vectors[hop].reshape(shape) @@ -224,14 +249,18 @@ def aggregate(self, user_embeddings, entities, relations): neighbor_vectors, neighbor_relations, user_embeddings ) # [batch_size, -1, dim] - if self.aggregator_class == 'sum': - output = (self_vectors + neighbors_agg).reshape(-1, self.embedding_size) # [-1, dim] - elif self.aggregator_class == 'neighbor': + if self.aggregator_class == "sum": + output = (self_vectors + neighbors_agg).reshape( + -1, self.embedding_size + ) # [-1, dim] + elif self.aggregator_class == "neighbor": output = neighbors_agg.reshape(-1, self.embedding_size) # [-1, dim] - elif self.aggregator_class == 'concat': + elif self.aggregator_class == "concat": # [batch_size, -1, dim * 2] output = torch.cat([self_vectors, neighbors_agg], dim=-1) - output = output.reshape(-1, self.embedding_size * 2) # [-1, dim * 2] + output = output.reshape( + -1, self.embedding_size * 2 + ) # [-1, dim * 2] else: raise Exception("Unknown aggregator: " + self.aggregator_class) @@ -247,7 +276,9 @@ def aggregate(self, user_embeddings, entities, relations): entity_vectors_next_iter.append(vector) entity_vectors = entity_vectors_next_iter - item_embeddings = entity_vectors[0].reshape(self.batch_size, self.embedding_size) + item_embeddings = entity_vectors[0].reshape( + self.batch_size, self.embedding_size + ) return item_embeddings @@ -276,7 +307,7 @@ def calculate_loss(self, interaction): predict = torch.cat((pos_item_score, neg_item_score)) target = torch.zeros(len(user) * 2, dtype=torch.float32).to(self.device) - target[:len(user)] = 1 + target[: len(user)] = 1 rec_loss = self.bce_loss(predict, target) l2_loss = self.l2_loss(user_e, pos_item_e, neg_item_e) diff --git a/recbole/model/knowledge_aware_recommender/kgnnls.py b/recbole/model/knowledge_aware_recommender/kgnnls.py index 6778e23c0..d800db886 100644 --- a/recbole/model/knowledge_aware_recommender/kgnnls.py +++ b/recbole/model/knowledge_aware_recommender/kgnnls.py @@ -41,31 +41,39 @@ def __init__(self, config, dataset): super(KGNNLS, self).__init__(config, dataset) # load parameters info - self.embedding_size = config['embedding_size'] - self.neighbor_sample_size = config['neighbor_sample_size'] - self.aggregator_class = config['aggregator'] # which aggregator to use + self.embedding_size = config["embedding_size"] + self.neighbor_sample_size = config["neighbor_sample_size"] + self.aggregator_class = config["aggregator"] # which aggregator to use # number of iterations when computing entity representation - self.n_iter = config['n_iter'] - self.reg_weight = config['reg_weight'] # weight of l2 regularization + self.n_iter = config["n_iter"] + self.reg_weight = config["reg_weight"] # weight of l2 regularization # weight of label Smoothness regularization - self.ls_weight = config['ls_weight'] + self.ls_weight = config["ls_weight"] # define embedding self.user_embedding = nn.Embedding(self.n_users, self.embedding_size) self.entity_embedding = nn.Embedding(self.n_entities, self.embedding_size) - self.relation_embedding = nn.Embedding(self.n_relations + 1, self.embedding_size) + self.relation_embedding = nn.Embedding( + self.n_relations + 1, self.embedding_size + ) # sample neighbors and construct interaction table - kg_graph = dataset.kg_graph(form='coo', value_field='relation_id') + kg_graph = dataset.kg_graph(form="coo", value_field="relation_id") adj_entity, adj_relation = self.construct_adj(kg_graph) - self.adj_entity, self.adj_relation = adj_entity.to(self.device), adj_relation.to(self.device) + self.adj_entity, self.adj_relation = adj_entity.to( + self.device + ), adj_relation.to(self.device) inter_feat = dataset.inter_feat pos_users = inter_feat[dataset.uid_field] pos_items = inter_feat[dataset.iid_field] pos_label = torch.ones(pos_items.shape) - pos_interaction_table, self.offset = self.get_interaction_table(pos_users, pos_items, pos_label) - self.interaction_table = self.sample_neg_interaction(pos_interaction_table, self.offset) + pos_interaction_table, self.offset = self.get_interaction_table( + pos_users, pos_items, pos_label + ) + self.interaction_table = self.sample_neg_interaction( + pos_interaction_table, self.offset + ) # define function self.softmax = nn.Softmax(dim=-1) @@ -73,8 +81,10 @@ def __init__(self, config, dataset): for i in range(self.n_iter): self.linear_layers.append( nn.Linear( - self.embedding_size if not self.aggregator_class == 'concat' else self.embedding_size * 2, self.embedding_size + if not self.aggregator_class == "concat" + else self.embedding_size * 2, + self.embedding_size, ) ) self.ReLU = nn.ReLU() @@ -85,7 +95,7 @@ def __init__(self, config, dataset): # parameters initialization self.apply(xavier_normal_initialization) - self.other_parameter_name = ['adj_entity', 'adj_relation'] + self.other_parameter_name = ["adj_entity", "adj_relation"] def get_interaction_table(self, user_id, item_id, y): r"""Get interaction_table that is used for fetching user-item interaction label in LS regularization. @@ -101,7 +111,7 @@ def get_interaction_table(self, user_id, item_id, y): - offset(int): The offset that is used for calculating the key(index) in interaction_table """ offset = len(str(self.n_entities)) - offset = 10 ** offset + offset = 10**offset keys = user_id * offset + item_id keys = keys.int().cpu().numpy().tolist() values = y.float().cpu().numpy().tolist() @@ -127,7 +137,7 @@ def sample_neg_interaction(self, pos_interaction_table, offset): item_id = random.randint(0, self.n_items) keys = user_id * offset + item_id if keys not in pos_interaction_table: - neg_interaction_table[keys] = 0. + neg_interaction_table[keys] = 0.0 neg_num += 1 interaction_table = {**pos_interaction_table, **neg_interaction_table} return interaction_table @@ -175,11 +185,15 @@ def construct_adj(self, kg_graph): n_neighbors = len(neighbors) if n_neighbors >= self.neighbor_sample_size: sampled_indices = np.random.choice( - list(range(n_neighbors)), size=self.neighbor_sample_size, replace=False + list(range(n_neighbors)), + size=self.neighbor_sample_size, + replace=False, ) else: sampled_indices = np.random.choice( - list(range(n_neighbors)), size=self.neighbor_sample_size, replace=True + list(range(n_neighbors)), + size=self.neighbor_sample_size, + replace=True, ) adj_entity[entity] = np.array([neighbors[i][0] for i in sampled_indices]) adj_relation[entity] = np.array([neighbors[i][1] for i in sampled_indices]) @@ -208,8 +222,12 @@ def get_neighbors(self, items): relations = [] for i in range(self.n_iter): index = torch.flatten(entities[i]) - neighbor_entities = torch.index_select(self.adj_entity, 0, index).reshape(self.batch_size, -1) - neighbor_relations = torch.index_select(self.adj_relation, 0, index).reshape(self.batch_size, -1) + neighbor_entities = torch.index_select(self.adj_entity, 0, index).reshape( + self.batch_size, -1 + ) + neighbor_relations = torch.index_select( + self.adj_relation, 0, index + ).reshape(self.batch_size, -1) entities.append(neighbor_entities) relations.append(neighbor_relations) return entities, relations @@ -238,7 +256,12 @@ def aggregate(self, user_embeddings, entities, relations): for i in range(self.n_iter): entity_vectors_next_iter = [] for hop in range(self.n_iter - i): - shape = (self.batch_size, -1, self.neighbor_sample_size, self.embedding_size) + shape = ( + self.batch_size, + -1, + self.neighbor_sample_size, + self.embedding_size, + ) self_vectors = entity_vectors[hop] neighbor_vectors = entity_vectors[hop + 1].reshape(shape) neighbor_relations = relation_vectors[hop].reshape(shape) @@ -257,14 +280,18 @@ def aggregate(self, user_embeddings, entities, relations): user_relation_scores_normalized * neighbor_vectors, dim=2 ) # [batch_size, -1, dim] - if self.aggregator_class == 'sum': - output = (self_vectors + neighbors_agg).reshape(-1, self.embedding_size) # [-1, dim] - elif self.aggregator_class == 'neighbor': + if self.aggregator_class == "sum": + output = (self_vectors + neighbors_agg).reshape( + -1, self.embedding_size + ) # [-1, dim] + elif self.aggregator_class == "neighbor": output = neighbors_agg.reshape(-1, self.embedding_size) # [-1, dim] - elif self.aggregator_class == 'concat': + elif self.aggregator_class == "concat": # [batch_size, -1, dim * 2] output = torch.cat([self_vectors, neighbors_agg], dim=-1) - output = output.reshape(-1, self.embedding_size * 2) # [-1, dim * 2] + output = output.reshape( + -1, self.embedding_size * 2 + ) # [-1, dim * 2] else: raise Exception("Unknown aggregator: " + self.aggregator_class) @@ -309,7 +336,9 @@ def label_smoothness_predict(self, user_embeddings, user, entities, relations): for entities_per_iter in entities: users = torch.unsqueeze(user, dim=1) # [batch_size, 1] - user_entity_concat = users * self.offset + entities_per_iter # [batch_size, n_neighbor^i] + user_entity_concat = ( + users * self.offset + entities_per_iter + ) # [batch_size, n_neighbor^i] # the first one in entities is the items to be held out if holdout_item_for_user is None: @@ -328,9 +357,13 @@ def lookup_interaction_table(x, _): holdout_mask = (holdout_item_for_user - user_entity_concat).bool() # True if the entity is a labeled item reset_mask = (initial_label - 0.5).bool() - reset_mask = torch.logical_and(reset_mask, holdout_mask) # remove held-out items - initial_label = holdout_mask.float() * initial_label + \ - torch.logical_not(holdout_mask).float() * 0.5 # label initialization + reset_mask = torch.logical_and( + reset_mask, holdout_mask + ) # remove held-out items + initial_label = ( + holdout_mask.float() * initial_label + + torch.logical_not(holdout_mask).float() * 0.5 + ) # label initialization reset_masks.append(reset_mask) entity_labels.append(initial_label) @@ -344,7 +377,9 @@ def lookup_interaction_table(x, _): for hop in range(self.n_iter - i): masks = reset_masks[hop] self_labels = entity_labels[hop] - neighbor_labels = entity_labels[hop + 1].reshape(self.batch_size, -1, self.neighbor_sample_size) + neighbor_labels = entity_labels[hop + 1].reshape( + self.batch_size, -1, self.neighbor_sample_size + ) neighbor_relations = relation_vectors[hop].reshape( self.batch_size, -1, self.neighbor_sample_size, self.embedding_size ) @@ -356,13 +391,17 @@ def lookup_interaction_table(x, _): user_relation_scores = torch.mean( user_embeddings * neighbor_relations, dim=-1 ) # [batch_size, -1, n_neighbor] - user_relation_scores_normalized = self.softmax(user_relation_scores) # [batch_size, -1, n_neighbor] + user_relation_scores_normalized = self.softmax( + user_relation_scores + ) # [batch_size, -1, n_neighbor] neighbors_aggregated_label = torch.mean( user_relation_scores_normalized * neighbor_labels, dim=2 ) # [batch_size, -1, dim] # [batch_size, -1] - output = masks.float() * self_labels + \ - torch.logical_not(masks).float() * neighbors_aggregated_label + output = ( + masks.float() * self_labels + + torch.logical_not(masks).float() * neighbors_aggregated_label + ) entity_labels_next_iter.append(output) entity_labels = entity_labels_next_iter @@ -396,7 +435,9 @@ def calculate_ls_loss(self, user, item, target): user_e = self.user_embedding(user) entities, relations = self.get_neighbors(item) - predicted_labels = self.label_smoothness_predict(user_e, user, entities, relations) + predicted_labels = self.label_smoothness_predict( + user_e, user, entities, relations + ) ls_loss = self.bce_loss(predicted_labels, target) return ls_loss @@ -405,7 +446,7 @@ def calculate_loss(self, interaction): pos_item = interaction[self.ITEM_ID] neg_item = interaction[self.NEG_ITEM_ID] target = torch.zeros(len(user) * 2, dtype=torch.float32).to(self.device) - target[:len(user)] = 1 + target[: len(user)] = 1 users = torch.cat((user, user)) items = torch.cat((pos_item, neg_item)) diff --git a/recbole/model/knowledge_aware_recommender/ktup.py b/recbole/model/knowledge_aware_recommender/ktup.py index adafa68e6..781e0b2b4 100644 --- a/recbole/model/knowledge_aware_recommender/ktup.py +++ b/recbole/model/knowledge_aware_recommender/ktup.py @@ -36,12 +36,12 @@ def __init__(self, config, dataset): super(KTUP, self).__init__(config, dataset) # load parameters info - self.embedding_size = config['embedding_size'] - self.L1_flag = config['L1_flag'] - self.use_st_gumbel = config['use_st_gumbel'] - self.kg_weight = config['kg_weight'] - self.align_weight = config['align_weight'] - self.margin = config['margin'] + self.embedding_size = config["embedding_size"] + self.L1_flag = config["L1_flag"] + self.use_st_gumbel = config["use_st_gumbel"] + self.kg_weight = config["kg_weight"] + self.align_weight = config["align_weight"] + self.margin = config["margin"] # define layers and loss self.user_embedding = nn.Embedding(self.n_users, self.embedding_size) @@ -50,7 +50,9 @@ def __init__(self, config, dataset): self.pref_norm_embedding = nn.Embedding(self.n_relations, self.embedding_size) self.entity_embedding = nn.Embedding(self.n_entities, self.embedding_size) self.relation_embedding = nn.Embedding(self.n_relations, self.embedding_size) - self.relation_norm_embedding = nn.Embedding(self.n_relations, self.embedding_size) + self.relation_norm_embedding = nn.Embedding( + self.n_relations, self.embedding_size + ) self.rec_loss = BPRLoss() self.kg_loss = nn.MarginRankingLoss(margin=self.margin) @@ -61,10 +63,16 @@ def __init__(self, config, dataset): normalize_user_emb = F.normalize(self.user_embedding.weight.data, p=2, dim=1) normalize_item_emb = F.normalize(self.item_embedding.weight.data, p=2, dim=1) normalize_pref_emb = F.normalize(self.pref_embedding.weight.data, p=2, dim=1) - normalize_pref_norm_emb = F.normalize(self.pref_norm_embedding.weight.data, p=2, dim=1) - normalize_entity_emb = F.normalize(self.entity_embedding.weight.data, p=2, dim=1) + normalize_pref_norm_emb = F.normalize( + self.pref_norm_embedding.weight.data, p=2, dim=1 + ) + normalize_entity_emb = F.normalize( + self.entity_embedding.weight.data, p=2, dim=1 + ) normalize_rel_emb = F.normalize(self.relation_embedding.weight.data, p=2, dim=1) - normalize_rel_norm_emb = F.normalize(self.relation_norm_embedding.weight.data, p=2, dim=1) + normalize_rel_norm_emb = F.normalize( + self.relation_norm_embedding.weight.data, p=2, dim=1 + ) self.user_embedding.weight.data = normalize_user_emb self.item_embedding.weight_data = normalize_item_emb self.pref_embedding.weight.data = normalize_pref_emb @@ -93,7 +101,11 @@ def convert_to_one_hot(self, indices, num_classes): new_shape = torch.Size([i for i in old_shape] + [num_classes]) indices = indices.unsqueeze(len(old_shape)) - one_hot = Variable(indices.data.new(new_shape).zero_().scatter_(len(old_shape), indices.data, 1)) + one_hot = Variable( + indices.data.new(new_shape) + .zero_() + .scatter_(len(old_shape), indices.data, 1) + ) return one_hot def st_gumbel_softmax(self, logits, temperature=1.0): @@ -120,24 +132,45 @@ def st_gumbel_softmax(self, logits, temperature=1.0): y = logits + gumbel_noise y = self._masked_softmax(logits=y / temperature) y_argmax = y.max(len(y.shape) - 1)[1] - y_hard = self.convert_to_one_hot(indices=y_argmax, num_classes=y.size(len(y.shape) - 1)).float() + y_hard = self.convert_to_one_hot( + indices=y_argmax, num_classes=y.size(len(y.shape) - 1) + ).float() y = (y_hard - y).detach() + y return y def _get_preferences(self, user_e, item_e, use_st_gumbel=False): - pref_probs = torch.matmul( - user_e + item_e, torch.t(self.pref_embedding.weight + self.relation_embedding.weight) - ) / 2 + pref_probs = ( + torch.matmul( + user_e + item_e, + torch.t(self.pref_embedding.weight + self.relation_embedding.weight), + ) + / 2 + ) if use_st_gumbel: # todo: different torch versions may cause the st_gumbel_softmax to report errors, wait to be test pref_probs = self.st_gumbel_softmax(pref_probs) - relation_e = torch.matmul(pref_probs, self.pref_embedding.weight + self.relation_embedding.weight) / 2 - norm_e = torch.matmul(pref_probs, self.pref_norm_embedding.weight + self.relation_norm_embedding.weight) / 2 + relation_e = ( + torch.matmul( + pref_probs, self.pref_embedding.weight + self.relation_embedding.weight + ) + / 2 + ) + norm_e = ( + torch.matmul( + pref_probs, + self.pref_norm_embedding.weight + self.relation_norm_embedding.weight, + ) + / 2 + ) return pref_probs, relation_e, norm_e @staticmethod def _transH_projection(original, norm): - return original - torch.sum(original * norm, dim=len(original.size()) - 1, keepdim=True) * norm + return ( + original + - torch.sum(original * norm, dim=len(original.size()) - 1, keepdim=True) + * norm + ) def _get_score(self, h_e, r_e, t_e): if self.L1_flag: @@ -152,7 +185,9 @@ def forward(self, user, item): entity_e = self.entity_embedding(item) item_e = item_e + entity_e - _, relation_e, norm_e = self._get_preferences(user_e, item_e, use_st_gumbel=self.use_st_gumbel) + _, relation_e, norm_e = self._get_preferences( + user_e, item_e, use_st_gumbel=self.use_st_gumbel + ) proj_user_e = self._transH_projection(user_e, norm_e) proj_item_e = self._transH_projection(item_e, norm_e) @@ -165,13 +200,21 @@ def calculate_loss(self, interaction): proj_pos_user_e, pos_relation_e, proj_pos_item_e = self.forward(user, pos_item) proj_neg_user_e, neg_relation_e, proj_neg_item_e = self.forward(user, neg_item) - pos_item_score = self._get_score(proj_pos_user_e, pos_relation_e, proj_pos_item_e) - neg_item_score = self._get_score(proj_neg_user_e, neg_relation_e, proj_neg_item_e) + pos_item_score = self._get_score( + proj_pos_user_e, pos_relation_e, proj_pos_item_e + ) + neg_item_score = self._get_score( + proj_neg_user_e, neg_relation_e, proj_neg_item_e + ) rec_loss = self.rec_loss(pos_item_score, neg_item_score) - orthogonal_loss = orthogonalLoss(self.pref_embedding.weight, self.pref_norm_embedding.weight) + orthogonal_loss = orthogonalLoss( + self.pref_embedding.weight, self.pref_norm_embedding.weight + ) item = torch.cat([pos_item, neg_item]) - align_loss = self.align_weight * alignLoss(self.item_embedding(item), self.entity_embedding(item), self.L1_flag) + align_loss = self.align_weight * alignLoss( + self.item_embedding(item), self.entity_embedding(item), self.L1_flag + ) return rec_loss, orthogonal_loss, align_loss @@ -203,7 +246,9 @@ def calculate_kg_loss(self, interaction): pos_tail_score = self._get_score(proj_h_e, r_e, proj_pos_t_e) neg_tail_score = self._get_score(proj_h_e, r_e, proj_neg_t_e) - kg_loss = self.kg_loss(pos_tail_score, neg_tail_score, torch.ones(h.size(0)).to(self.device)) + kg_loss = self.kg_loss( + pos_tail_score, neg_tail_score, torch.ones(h.size(0)).to(self.device) + ) orthogonal_loss = orthogonalLoss(r_e, norm_e) reg_loss = self.reg_loss(h_e, pos_t_e, neg_t_e, r_e) loss = self.kg_weight * (kg_loss + orthogonal_loss + reg_loss) @@ -224,8 +269,8 @@ def predict(self, interaction): def orthogonalLoss(rel_embeddings, norm_embeddings): return torch.sum( - torch.sum(norm_embeddings * rel_embeddings, dim=1, keepdim=True) ** 2 / - torch.sum(rel_embeddings ** 2, dim=1, keepdim=True) + torch.sum(norm_embeddings * rel_embeddings, dim=1, keepdim=True) ** 2 + / torch.sum(rel_embeddings**2, dim=1, keepdim=True) ) diff --git a/recbole/model/knowledge_aware_recommender/mkr.py b/recbole/model/knowledge_aware_recommender/mkr.py index 7250dcc81..d032b09db 100644 --- a/recbole/model/knowledge_aware_recommender/mkr.py +++ b/recbole/model/knowledge_aware_recommender/mkr.py @@ -23,9 +23,9 @@ class MKR(KnowledgeRecommender): - r"""MKR is a Multi-task feature learning approach for Knowledge graph enhanced Recommendation. It is a deep - end-to-end framework that utilizes knowledge graph embedding task to assist recommendation task. The two - tasks are associated by cross&compress units, which automatically share latent features and learn high-order + r"""MKR is a Multi-task feature learning approach for Knowledge graph enhanced Recommendation. It is a deep + end-to-end framework that utilizes knowledge graph embedding task to assist recommendation task. The two + tasks are associated by cross&compress units, which automatically share latent features and learn high-order interactions between items in recommender systems and entities in the knowledge graph. """ @@ -35,20 +35,24 @@ def __init__(self, config, dataset): super(MKR, self).__init__(config, dataset) # load parameters info - self.LABEL = config['LABEL_FIELD'] - self.embedding_size = config['embedding_size'] - self.kg_embedding_size = config['kg_embedding_size'] - self.L = config['low_layers_num'] # the number of low layers - self.H = config['high_layers_num'] # the number of high layers - self.reg_weight = config['reg_weight'] - self.use_inner_product = config['use_inner_product'] - self.dropout_prob = config['dropout_prob'] + self.LABEL = config["LABEL_FIELD"] + self.embedding_size = config["embedding_size"] + self.kg_embedding_size = config["kg_embedding_size"] + self.L = config["low_layers_num"] # the number of low layers + self.H = config["high_layers_num"] # the number of high layers + self.reg_weight = config["reg_weight"] + self.use_inner_product = config["use_inner_product"] + self.dropout_prob = config["dropout_prob"] # init embeddings self.user_embeddings_lookup = nn.Embedding(self.n_users, self.embedding_size) self.item_embeddings_lookup = nn.Embedding(self.n_entities, self.embedding_size) - self.entity_embeddings_lookup = nn.Embedding(self.n_entities, self.embedding_size) - self.relation_embeddings_lookup = nn.Embedding(self.n_relations, self.embedding_size) + self.entity_embeddings_lookup = nn.Embedding( + self.n_entities, self.embedding_size + ) + self.relation_embeddings_lookup = nn.Embedding( + self.n_relations, self.embedding_size + ) # define layers lower_mlp_layers = [] @@ -58,16 +62,22 @@ def __init__(self, config, dataset): for i in range(self.H): high_mlp_layers.append(self.embedding_size * 2) - self.user_mlp = MLPLayers(lower_mlp_layers, self.dropout_prob, 'sigmoid') - self.tail_mlp = MLPLayers(lower_mlp_layers, self.dropout_prob, 'sigmoid') + self.user_mlp = MLPLayers(lower_mlp_layers, self.dropout_prob, "sigmoid") + self.tail_mlp = MLPLayers(lower_mlp_layers, self.dropout_prob, "sigmoid") self.cc_unit = nn.Sequential() for i_cnt in range(self.L): - self.cc_unit.add_module('cc_unit{}'.format(i_cnt), CrossCompressUnit(self.embedding_size)) - self.kge_mlp = MLPLayers(high_mlp_layers, self.dropout_prob, 'sigmoid') - self.kge_pred_mlp = MLPLayers([self.embedding_size * 2, self.embedding_size], self.dropout_prob, 'sigmoid') + self.cc_unit.add_module( + "cc_unit{}".format(i_cnt), CrossCompressUnit(self.embedding_size) + ) + self.kge_mlp = MLPLayers(high_mlp_layers, self.dropout_prob, "sigmoid") + self.kge_pred_mlp = MLPLayers( + [self.embedding_size * 2, self.embedding_size], self.dropout_prob, "sigmoid" + ) if self.use_inner_product == False: - self.rs_pred_mlp = MLPLayers([self.embedding_size * 2, 1], self.dropout_prob, 'sigmoid') - self.rs_mlp = MLPLayers(high_mlp_layers, self.dropout_prob, 'sigmoid') + self.rs_pred_mlp = MLPLayers( + [self.embedding_size * 2, 1], self.dropout_prob, "sigmoid" + ) + self.rs_mlp = MLPLayers(high_mlp_layers, self.dropout_prob, "sigmoid") # loss self.sigmoid_BCE = nn.BCEWithLogitsLoss() @@ -76,7 +86,12 @@ def __init__(self, config, dataset): self.apply(xavier_normal_initialization) def forward( - self, user_indices=None, item_indices=None, head_indices=None, relation_indices=None, tail_indices=None + self, + user_indices=None, + item_indices=None, + head_indices=None, + relation_indices=None, + tail_indices=None, ): self.item_embeddings = self.item_embeddings_lookup(item_indices) self.head_embeddings = self.entity_embeddings_lookup(head_indices) @@ -90,15 +105,25 @@ def forward( self.user_embeddings = self.user_mlp(self.user_embeddings) if self.use_inner_product: # get scores by inner product. - self.scores = torch.sum(self.user_embeddings * self.item_embeddings, 1) # [batch_size] + self.scores = torch.sum( + self.user_embeddings * self.item_embeddings, 1 + ) # [batch_size] else: # get scores by mlp layers - self.user_item_concat = torch.cat([self.user_embeddings, self.item_embeddings], - 1) # [batch_size, emb_dim*2] + self.user_item_concat = torch.cat( + [self.user_embeddings, self.item_embeddings], 1 + ) # [batch_size, emb_dim*2] self.user_item_concat = self.rs_mlp(self.user_item_concat) - self.scores = torch.squeeze(self.rs_pred_mlp(self.user_item_concat)) # [batch_size] + self.scores = torch.squeeze( + self.rs_pred_mlp(self.user_item_concat) + ) # [batch_size] self.scores_normalized = torch.sigmoid(self.scores) - outputs = [self.user_embeddings, self.item_embeddings, self.scores, self.scores_normalized] + outputs = [ + self.user_embeddings, + self.item_embeddings, + self.scores, + self.scores_normalized, + ] if relation_indices is not None: # KGE @@ -106,39 +131,51 @@ def forward( self.relation_embeddings = self.relation_embeddings_lookup(relation_indices) self.tail_embeddings = self.tail_mlp(self.tail_embeddings) - self.head_relation_concat = torch.cat([self.head_embeddings, self.relation_embeddings], - 1) # [batch_size, emb_dim*2] + self.head_relation_concat = torch.cat( + [self.head_embeddings, self.relation_embeddings], 1 + ) # [batch_size, emb_dim*2] self.head_relation_concat = self.kge_mlp(self.head_relation_concat) - self.tail_pred = self.kge_pred_mlp(self.head_relation_concat) # [batch_size, 1] + self.tail_pred = self.kge_pred_mlp( + self.head_relation_concat + ) # [batch_size, 1] self.tail_pred = torch.sigmoid(self.tail_pred) - self.scores_kge = torch.sigmoid(torch.sum(self.tail_embeddings * self.tail_pred, 1)) + self.scores_kge = torch.sigmoid( + torch.sum(self.tail_embeddings * self.tail_pred, 1) + ) self.rmse = torch.mean( - torch.sqrt(torch.sum(torch.pow(self.tail_embeddings - self.tail_pred, 2), 1) / self.embedding_size) + torch.sqrt( + torch.sum(torch.pow(self.tail_embeddings - self.tail_pred, 2), 1) + / self.embedding_size + ) ) - outputs = [self.head_embeddings, self.tail_embeddings, self.scores_kge, self.rmse] + outputs = [ + self.head_embeddings, + self.tail_embeddings, + self.scores_kge, + self.rmse, + ] return outputs def _l2_loss(self, inputs): - return torch.sum(inputs ** 2) / 2 + return torch.sum(inputs**2) / 2 def calculate_rs_loss(self, interaction): - r"""Calculate the training loss for a batch data of RS. - - """ + r"""Calculate the training loss for a batch data of RS.""" # inputs self.user_indices = interaction[self.USER_ID] self.item_indices = interaction[self.ITEM_ID] self.head_indices = interaction[self.ITEM_ID] self.labels = interaction[self.LABEL] # RS model - user_embeddings, item_embeddings, \ - scores, scores_normalized = self.forward(user_indices=self.user_indices, - item_indices=self.item_indices, - head_indices=self.head_indices, - relation_indices=None, - tail_indices=None) + user_embeddings, item_embeddings, scores, scores_normalized = self.forward( + user_indices=self.user_indices, + item_indices=self.item_indices, + head_indices=self.head_indices, + relation_indices=None, + tail_indices=None, + ) # loss base_loss_rs = torch.mean(self.sigmoid_BCE(scores, self.labels)) l2_loss_rs = self._l2_loss(user_embeddings) + self._l2_loss(item_embeddings) @@ -147,21 +184,20 @@ def calculate_rs_loss(self, interaction): return loss_rs def calculate_kg_loss(self, interaction): - r"""Calculate the training loss for a batch data of KG. - - """ + r"""Calculate the training loss for a batch data of KG.""" # inputs self.item_indices = interaction[self.HEAD_ENTITY_ID] self.head_indices = interaction[self.HEAD_ENTITY_ID] self.relation_indices = interaction[self.RELATION_ID] self.tail_indices = interaction[self.TAIL_ENTITY_ID] # KGE model - head_embeddings, tail_embeddings, \ - scores_kge, rmse = self.forward(user_indices=None, - item_indices=self.item_indices, - head_indices=self.head_indices, - relation_indices=self.relation_indices, - tail_indices=self.tail_indices) + head_embeddings, tail_embeddings, scores_kge, rmse = self.forward( + user_indices=None, + item_indices=self.item_indices, + head_indices=self.head_indices, + relation_indices=self.relation_indices, + tail_indices=self.tail_indices, + ) # loss base_loss_kge = -scores_kge l2_loss_kge = self._l2_loss(head_embeddings) + self._l2_loss(tail_embeddings) @@ -181,9 +217,7 @@ def predict(self, interaction): class CrossCompressUnit(nn.Module): - r"""This is Cross&Compress Unit for MKR model to model feature interactions between items and entities. - - """ + r"""This is Cross&Compress Unit for MKR model to model feature interactions between items and entities.""" def __init__(self, dim): super(CrossCompressUnit, self).__init__() diff --git a/recbole/model/knowledge_aware_recommender/ripplenet.py b/recbole/model/knowledge_aware_recommender/ripplenet.py index 0b531d0b8..3f1170057 100644 --- a/recbole/model/knowledge_aware_recommender/ripplenet.py +++ b/recbole/model/knowledge_aware_recommender/ripplenet.py @@ -36,15 +36,15 @@ def __init__(self, config, dataset): super(RippleNet, self).__init__(config, dataset) # load dataset info - self.LABEL = config['LABEL_FIELD'] + self.LABEL = config["LABEL_FIELD"] # load parameters info - self.embedding_size = config['embedding_size'] - self.kg_weight = config['kg_weight'] - self.reg_weight = config['reg_weight'] - self.n_hop = config['n_hop'] - self.n_memory = config['n_memory'] - self.interaction_matrix = dataset.inter_matrix(form='coo').astype(np.float32) + self.embedding_size = config["embedding_size"] + self.kg_weight = config["kg_weight"] + self.reg_weight = config["reg_weight"] + self.n_hop = config["n_hop"] + self.n_memory = config["n_memory"] + self.interaction_matrix = dataset.inter_matrix(form="coo").astype(np.float32) head_entities = dataset.head_entities.tolist() tail_entities = dataset.tail_entities.tolist() relations = dataset.relations.tolist() @@ -69,8 +69,12 @@ def __init__(self, config, dataset): # define layers and loss self.entity_embedding = nn.Embedding(self.n_entities, self.embedding_size) - self.relation_embedding = nn.Embedding(self.n_relations, self.embedding_size * self.embedding_size) - self.transform_matrix = nn.Linear(self.embedding_size, self.embedding_size, bias=False) + self.relation_embedding = nn.Embedding( + self.n_relations, self.embedding_size * self.embedding_size + ) + self.transform_matrix = nn.Linear( + self.embedding_size, self.embedding_size, bias=False + ) self.softmax = torch.nn.Softmax(dim=1) self.sigmoid = torch.nn.Sigmoid() self.rec_loss = BPRLoss() @@ -79,7 +83,7 @@ def __init__(self, config, dataset): # parameters initialization self.apply(xavier_normal_initialization) - self.other_parameter_name = ['ripple_set'] + self.other_parameter_name = ["ripple_set"] def _build_ripple_set(self): r"""Get the normalized interaction matrix of users and items according to A_values. @@ -128,7 +132,9 @@ def _build_ripple_set(self): else: # sample a fixed-size 1-hop memory for each user replace = len(memories_h) < self.n_memory - indices = np.random.choice(len(memories_h), size=self.n_memory, replace=replace) + indices = np.random.choice( + len(memories_h), size=self.n_memory, replace=replace + ) memories_h = [memories_h[i] for i in indices] memories_r = [memories_r[i] for i in indices] memories_t = [memories_t[i] for i in indices] @@ -136,7 +142,9 @@ def _build_ripple_set(self): memories_r = torch.LongTensor(memories_r).to(self.device) memories_t = torch.LongTensor(memories_t).to(self.device) ripple_set[user].append((memories_h, memories_r, memories_t)) - self.logger.info("{} among {} users are padded".format(n_padding, len(self.user_dict))) + self.logger.info( + "{} among {} users are padded".format(n_padding, len(self.user_dict)) + ) return ripple_set def forward(self, interaction): @@ -192,7 +200,9 @@ def _key_addressing(self): h_emb = self.h_emb_list[hop].unsqueeze(2) # [batch_size * n_memory, dim, dim] - r_mat = self.r_emb_list[hop].view(-1, self.embedding_size, self.embedding_size) + r_mat = self.r_emb_list[hop].view( + -1, self.embedding_size, self.embedding_size + ) # [batch_size, n_memory, dim] Rh = torch.bmm(r_mat, h_emb).view(-1, self.n_memory, self.embedding_size) @@ -230,7 +240,9 @@ def calculate_loss(self, interaction): # (batch_size * n_memory, dim) t_expanded = self.t_emb_list[hop] # (batch_size * n_memory, dim, dim) - r_mat = self.r_emb_list[hop].view(-1, self.embedding_size, self.embedding_size) + r_mat = self.r_emb_list[hop].view( + -1, self.embedding_size, self.embedding_size + ) # (N, 1, dim) (N, dim, dim) -> (N, 1, dim) hR = torch.bmm(h_expanded, r_mat).squeeze(1) # (N, dim) (N, dim) @@ -242,7 +254,9 @@ def calculate_loss(self, interaction): reg_loss = None for hop in range(self.n_hop): - tp_loss = self.l2_loss(self.h_emb_list[hop], self.t_emb_list[hop], self.r_emb_list[hop]) + tp_loss = self.l2_loss( + self.h_emb_list[hop], self.t_emb_list[hop], self.r_emb_list[hop] + ) if reg_loss is None: reg_loss = tp_loss else: @@ -269,7 +283,9 @@ def _key_addressing_full(self): h_emb = self.h_emb_list[hop].unsqueeze(2) # [batch_size * n_memory, dim, dim] - r_mat = self.r_emb_list[hop].view(-1, self.embedding_size, self.embedding_size) + r_mat = self.r_emb_list[hop].view( + -1, self.embedding_size, self.embedding_size + ) # [batch_size, n_memory, dim] Rh = torch.bmm(r_mat, h_emb).view(-1, self.n_memory, self.embedding_size) @@ -323,7 +339,7 @@ def full_sort_predict(self, interaction): memories_t[hop].append(self.ripple_set[user][hop][2]) # memories_h, memories_r, memories_t = self.ripple_set[user] # item = interaction[self.ITEM_ID] - self.item_embeddings = self.entity_embedding.weight[:self.n_items] + self.item_embeddings = self.entity_embedding.weight[: self.n_items] # self.item_embeddings = self.entity_embedding(item) self.h_emb_list = [] diff --git a/recbole/model/layers.py b/recbole/model/layers.py index f9fe94a29..d89818f14 100644 --- a/recbole/model/layers.py +++ b/recbole/model/layers.py @@ -28,7 +28,7 @@ class MLPLayers(nn.Module): - r""" MLPLayers + r"""MLPLayers Args: - layers(list): a list contains the size of each layer in mlp layers @@ -51,7 +51,9 @@ class MLPLayers(nn.Module): >>> torch.Size([128, 16]) """ - def __init__(self, layers, dropout=0., activation='relu', bn=False, init_method=None): + def __init__( + self, layers, dropout=0.0, activation="relu", bn=False, init_method=None + ): super(MLPLayers, self).__init__() self.layers = layers self.dropout = dropout @@ -60,7 +62,9 @@ def __init__(self, layers, dropout=0., activation='relu', bn=False, init_method= self.init_method = init_method mlp_modules = [] - for idx, (input_size, output_size) in enumerate(zip(self.layers[:-1], self.layers[1:])): + for idx, (input_size, output_size) in enumerate( + zip(self.layers[:-1], self.layers[1:]) + ): mlp_modules.append(nn.Dropout(p=self.dropout)) mlp_modules.append(nn.Linear(input_size, output_size)) if self.use_bn: @@ -76,7 +80,7 @@ def __init__(self, layers, dropout=0., activation='relu', bn=False, init_method= def init_weights(self, module): # We just initialize the module with normal distribution as the paper said if isinstance(module, nn.Linear): - if self.init_method == 'norm': + if self.init_method == "norm": normal_(module.weight.data, 0, 0.01) if module.bias is not None: module.bias.data.fill_(0.0) @@ -85,7 +89,7 @@ def forward(self, input_feature): return self.mlp_layers(input_feature) -def activation_layer(activation_name='relu', emb_dim=None): +def activation_layer(activation_name="relu", emb_dim=None): """Construct activation layers Args: @@ -98,28 +102,30 @@ def activation_layer(activation_name='relu', emb_dim=None): if activation_name is None: activation = None elif isinstance(activation_name, str): - if activation_name.lower() == 'sigmoid': + if activation_name.lower() == "sigmoid": activation = nn.Sigmoid() - elif activation_name.lower() == 'tanh': + elif activation_name.lower() == "tanh": activation = nn.Tanh() - elif activation_name.lower() == 'relu': + elif activation_name.lower() == "relu": activation = nn.ReLU() - elif activation_name.lower() == 'leakyrelu': + elif activation_name.lower() == "leakyrelu": activation = nn.LeakyReLU() - elif activation_name.lower() == 'dice': + elif activation_name.lower() == "dice": activation = Dice(emb_dim) - elif activation_name.lower() == 'none': + elif activation_name.lower() == "none": activation = None elif issubclass(activation_name, nn.Module): activation = activation_name() else: - raise NotImplementedError("activation function {} is not implemented".format(activation_name)) + raise NotImplementedError( + "activation function {} is not implemented".format(activation_name) + ) return activation class FMEmbedding(nn.Module): - r""" Embedding for token fields. + r"""Embedding for token fields. Args: field_dims: list, the number of tokens in each token fields @@ -163,7 +169,7 @@ def __init__(self, reduce_sum=True): def forward(self, input_x): square_of_sum = torch.sum(input_x, dim=1) ** 2 - sum_of_square = torch.sum(input_x ** 2, dim=1) + sum_of_square = torch.sum(input_x**2, dim=1) output = square_of_sum - sum_of_square if self.reduce_sum: output = torch.sum(output, dim=1, keepdim=True) @@ -183,7 +189,9 @@ def __init__(self, in_dim, out_dim): self.in_dim = in_dim self.out_dim = out_dim self.linear = torch.nn.Linear(in_features=in_dim, out_features=out_dim) - self.interActTransform = torch.nn.Linear(in_features=in_dim, out_features=out_dim) + self.interActTransform = torch.nn.Linear( + in_features=in_dim, out_features=out_dim + ) def forward(self, lap_matrix, eye_matrix, features): # for GCF ajdMat is a (N+M) by (N+M) mat @@ -261,7 +269,12 @@ class SequenceAttLayer(nn.Module): """ def __init__( - self, mask_mat, att_hidden_size=(80, 40), activation='sigmoid', softmax_stag=False, return_seq_weight=True + self, + mask_mat, + att_hidden_size=(80, 40), + activation="sigmoid", + softmax_stag=False, + return_seq_weight=True, ): super(SequenceAttLayer, self).__init__() self.att_hidden_size = att_hidden_size @@ -269,7 +282,9 @@ def __init__( self.softmax_stag = softmax_stag self.return_seq_weight = return_seq_weight self.mask_mat = mask_mat - self.att_mlp_layers = MLPLayers(self.att_hidden_size, activation='Sigmoid', bn=False) + self.att_mlp_layers = MLPLayers( + self.att_hidden_size, activation="Sigmoid", bn=False + ) self.dense = nn.Linear(self.att_hidden_size[-1], 1) def forward(self, queries, keys, keys_length): @@ -280,14 +295,16 @@ def forward(self, queries, keys, keys_length): queries = queries.view(-1, hist_len, embedding_size) # MLP Layer - input_tensor = torch.cat([queries, keys, queries - keys, queries * keys], dim=-1) + input_tensor = torch.cat( + [queries, keys, queries - keys, queries * keys], dim=-1 + ) output = self.att_mlp_layers(input_tensor) output = torch.transpose(self.dense(output), -1, -2) # get mask output = output.squeeze(1) mask = self.mask_mat.repeat(output.size(0), 1) - mask = (mask >= keys_length.unsqueeze(1)) + mask = mask >= keys_length.unsqueeze(1) # mask if self.softmax_stag: @@ -297,7 +314,7 @@ def forward(self, queries, keys, keys_length): output = output.masked_fill(mask=mask, value=torch.tensor(mask_value)) output = output.unsqueeze(1) - output = output / (embedding_size ** 0.5) + output = output / (embedding_size**0.5) # get the weight of each user's history list about the target item if self.softmax_stag: @@ -324,7 +341,9 @@ class VanillaAttention(nn.Module): def __init__(self, hidden_dim, attn_dim): super().__init__() - self.projection = nn.Sequential(nn.Linear(hidden_dim, attn_dim), nn.ReLU(True), nn.Linear(attn_dim, 1)) + self.projection = nn.Sequential( + nn.Linear(hidden_dim, attn_dim), nn.ReLU(True), nn.Linear(attn_dim, 1) + ) def forward(self, input_tensor): # (B, Len, num, H) -> (B, Len, num, 1) @@ -348,7 +367,14 @@ class MultiHeadAttention(nn.Module): """ - def __init__(self, n_heads, hidden_size, hidden_dropout_prob, attn_dropout_prob, layer_norm_eps): + def __init__( + self, + n_heads, + hidden_size, + hidden_dropout_prob, + attn_dropout_prob, + layer_norm_eps, + ): super(MultiHeadAttention, self).__init__() if hidden_size % n_heads != 0: raise ValueError( @@ -373,7 +399,10 @@ def __init__(self, n_heads, hidden_size, hidden_dropout_prob, attn_dropout_prob, self.out_dropout = nn.Dropout(hidden_dropout_prob) def transpose_for_scores(self, x): - new_x_shape = x.size()[:-1] + (self.num_attention_heads, self.attention_head_size) + new_x_shape = x.size()[:-1] + ( + self.num_attention_heads, + self.attention_head_size, + ) x = x.view(*new_x_shape) return x @@ -424,7 +453,9 @@ class FeedForward(nn.Module): """ - def __init__(self, hidden_size, inner_size, hidden_dropout_prob, hidden_act, layer_norm_eps): + def __init__( + self, hidden_size, inner_size, hidden_dropout_prob, hidden_act, layer_norm_eps + ): super(FeedForward, self).__init__() self.dense_1 = nn.Linear(hidden_size, inner_size) self.intermediate_act_fn = self.get_hidden_act(hidden_act) @@ -483,14 +514,26 @@ class TransformerLayer(nn.Module): """ def __init__( - self, n_heads, hidden_size, intermediate_size, hidden_dropout_prob, attn_dropout_prob, hidden_act, - layer_norm_eps + self, + n_heads, + hidden_size, + intermediate_size, + hidden_dropout_prob, + attn_dropout_prob, + hidden_act, + layer_norm_eps, ): super(TransformerLayer, self).__init__() self.multi_head_attention = MultiHeadAttention( n_heads, hidden_size, hidden_dropout_prob, attn_dropout_prob, layer_norm_eps ) - self.feed_forward = FeedForward(hidden_size, intermediate_size, hidden_dropout_prob, hidden_act, layer_norm_eps) + self.feed_forward = FeedForward( + hidden_size, + intermediate_size, + hidden_dropout_prob, + hidden_act, + layer_norm_eps, + ) def forward(self, hidden_states, attention_mask): attention_output = self.multi_head_attention(hidden_states, attention_mask) @@ -499,7 +542,7 @@ def forward(self, hidden_states, attention_mask): class TransformerEncoder(nn.Module): - r""" One TransformerEncoder consists of several TransformerLayers. + r"""One TransformerEncoder consists of several TransformerLayers. Args: n_layers(num): num of transformer layers in transformer encoder. Default: 2 @@ -522,13 +565,19 @@ def __init__( inner_size=256, hidden_dropout_prob=0.5, attn_dropout_prob=0.5, - hidden_act='gelu', - layer_norm_eps=1e-12 + hidden_act="gelu", + layer_norm_eps=1e-12, ): super(TransformerEncoder, self).__init__() layer = TransformerLayer( - n_heads, hidden_size, inner_size, hidden_dropout_prob, attn_dropout_prob, hidden_act, layer_norm_eps + n_heads, + hidden_size, + inner_size, + hidden_dropout_prob, + attn_dropout_prob, + hidden_act, + layer_norm_eps, ) self.layer = nn.ModuleList([copy.deepcopy(layer) for _ in range(n_layers)]) @@ -557,42 +606,58 @@ def forward(self, hidden_states, attention_mask, output_all_encoded_layers=True) class ItemToInterestAggregation(nn.Module): def __init__(self, seq_len, hidden_size, k_interests=5): super().__init__() - self.k_interests = k_interests # k latent interests + self.k_interests = k_interests # k latent interests self.theta = nn.Parameter(torch.randn([hidden_size, k_interests])) - - def forward(self, input_tensor): # [B, L, d] -> [B, k, d] - D_matrix = torch.matmul(input_tensor, self.theta) #[B, L, k] + + def forward(self, input_tensor): # [B, L, d] -> [B, k, d] + D_matrix = torch.matmul(input_tensor, self.theta) # [B, L, k] D_matrix = nn.Softmax(dim=-2)(D_matrix) - result = torch.einsum('nij, nik -> nkj', input_tensor, D_matrix) # #[B, k, d] + result = torch.einsum("nij, nik -> nkj", input_tensor, D_matrix) # #[B, k, d] return result class LightMultiHeadAttention(nn.Module): - def __init__(self, n_heads, k_interests, hidden_size, seq_len, hidden_dropout_prob, attn_dropout_prob, layer_norm_eps): + def __init__( + self, + n_heads, + k_interests, + hidden_size, + seq_len, + hidden_dropout_prob, + attn_dropout_prob, + layer_norm_eps, + ): super(LightMultiHeadAttention, self).__init__() if hidden_size % n_heads != 0: raise ValueError( "The hidden size (%d) is not a multiple of the number of attention " - "heads (%d)" % (hidden_size, n_heads)) + "heads (%d)" % (hidden_size, n_heads) + ) self.num_attention_heads = n_heads self.attention_head_size = int(hidden_size / n_heads) - self.all_head_size = self.num_attention_heads * self.attention_head_size + self.all_head_size = self.num_attention_heads * self.attention_head_size # initialization for low-rank decomposed self-attention self.query = nn.Linear(hidden_size, self.all_head_size) self.key = nn.Linear(hidden_size, self.all_head_size) self.value = nn.Linear(hidden_size, self.all_head_size) - self.attpooling_key = ItemToInterestAggregation(seq_len, hidden_size, k_interests) - self.attpooling_value = ItemToInterestAggregation(seq_len, hidden_size, k_interests) + self.attpooling_key = ItemToInterestAggregation( + seq_len, hidden_size, k_interests + ) + self.attpooling_value = ItemToInterestAggregation( + seq_len, hidden_size, k_interests + ) # initialization for decoupled position encoding self.attn_scale_factor = 2 self.pos_q_linear = nn.Linear(hidden_size, self.all_head_size) self.pos_k_linear = nn.Linear(hidden_size, self.all_head_size) - self.pos_scaling = float(self.attention_head_size * self.attn_scale_factor) ** -0.5 + self.pos_scaling = ( + float(self.attention_head_size * self.attn_scale_factor) ** -0.5 + ) self.pos_ln = nn.LayerNorm(hidden_size, eps=layer_norm_eps) self.attn_dropout = nn.Dropout(attn_dropout_prob) @@ -601,8 +666,11 @@ def __init__(self, n_heads, k_interests, hidden_size, seq_len, hidden_dropout_pr self.LayerNorm = nn.LayerNorm(hidden_size, eps=layer_norm_eps) self.out_dropout = nn.Dropout(hidden_dropout_prob) - def transpose_for_scores(self, x): # transfor to multihead - new_x_shape = x.size()[:-1] + (self.num_attention_heads, self.attention_head_size) + def transpose_for_scores(self, x): # transfor to multihead + new_x_shape = x.size()[:-1] + ( + self.num_attention_heads, + self.attention_head_size, + ) x = x.view(*new_x_shape) return x.permute(0, 2, 1, 3) @@ -615,11 +683,13 @@ def forward(self, input_tensor, pos_emb): # low-rank decomposed self-attention: relation of items query_layer = self.transpose_for_scores(mixed_query_layer) key_layer = self.transpose_for_scores(self.attpooling_key(mixed_key_layer)) - value_layer = self.transpose_for_scores(self.attpooling_value(mixed_value_layer)) + value_layer = self.transpose_for_scores( + self.attpooling_value(mixed_value_layer) + ) attention_scores = torch.matmul(query_layer, key_layer.transpose(-1, -2)) attention_scores = attention_scores / math.sqrt(self.attention_head_size) - + # normalize the attention scores to probabilities. attention_probs = nn.Softmax(dim=-2)(attention_scores) attention_probs = self.attn_dropout(attention_probs) @@ -628,7 +698,9 @@ def forward(self, input_tensor, pos_emb): # decoupled position encoding: relation of positions value_layer_pos = self.transpose_for_scores(mixed_value_layer) pos_emb = self.pos_ln(pos_emb).unsqueeze(0) - pos_query_layer = self.transpose_for_scores(self.pos_q_linear(pos_emb)) * self.pos_scaling + pos_query_layer = ( + self.transpose_for_scores(self.pos_q_linear(pos_emb)) * self.pos_scaling + ) pos_key_layer = self.transpose_for_scores(self.pos_k_linear(pos_emb)) abs_pos_bias = torch.matmul(pos_query_layer, pos_key_layer.transpose(-1, -2)) @@ -660,13 +732,36 @@ class LightTransformerLayer(nn.Module): Returns: feedforward_output (torch.Tensor): the output of the point-wise feed-forward sublayer, is the output of the transformer layer """ - def __init__(self, n_heads, k_interests, hidden_size, seq_len, intermediate_size, - hidden_dropout_prob, attn_dropout_prob, hidden_act, layer_norm_eps): + + def __init__( + self, + n_heads, + k_interests, + hidden_size, + seq_len, + intermediate_size, + hidden_dropout_prob, + attn_dropout_prob, + hidden_act, + layer_norm_eps, + ): super(LightTransformerLayer, self).__init__() - self.multi_head_attention = LightMultiHeadAttention(n_heads, k_interests, hidden_size, - seq_len, hidden_dropout_prob, attn_dropout_prob, layer_norm_eps) - self.feed_forward = FeedForward(hidden_size, intermediate_size, - hidden_dropout_prob, hidden_act, layer_norm_eps) + self.multi_head_attention = LightMultiHeadAttention( + n_heads, + k_interests, + hidden_size, + seq_len, + hidden_dropout_prob, + attn_dropout_prob, + layer_norm_eps, + ) + self.feed_forward = FeedForward( + hidden_size, + intermediate_size, + hidden_dropout_prob, + hidden_act, + layer_norm_eps, + ) def forward(self, hidden_states, pos_emb): attention_output = self.multi_head_attention(hidden_states, pos_emb) @@ -675,7 +770,7 @@ def forward(self, hidden_states, pos_emb): class LightTransformerEncoder(nn.Module): - r""" One LightTransformerEncoder consists of several LightTransformerLayers. + r"""One LightTransformerEncoder consists of several LightTransformerLayers. Args: n_layers(num): num of transformer layers in transformer encoder. Default: 2 @@ -688,23 +783,34 @@ class LightTransformerEncoder(nn.Module): candidates: 'gelu', 'relu', 'swish', 'tanh', 'sigmoid' layer_norm_eps(float): a value added to the denominator for numerical stability. Default: 1e-12 """ - def __init__(self, - n_layers=2, - n_heads=2, - k_interests=5, - hidden_size=64, - seq_len=50, - inner_size=256, - hidden_dropout_prob=0.5, - attn_dropout_prob=0.5, - hidden_act='gelu', - layer_norm_eps=1e-12): + + def __init__( + self, + n_layers=2, + n_heads=2, + k_interests=5, + hidden_size=64, + seq_len=50, + inner_size=256, + hidden_dropout_prob=0.5, + attn_dropout_prob=0.5, + hidden_act="gelu", + layer_norm_eps=1e-12, + ): super(LightTransformerEncoder, self).__init__() - layer = LightTransformerLayer(n_heads, k_interests, hidden_size, seq_len, inner_size, - hidden_dropout_prob, attn_dropout_prob, hidden_act, layer_norm_eps) - self.layer = nn.ModuleList([copy.deepcopy(layer) - for _ in range(n_layers)]) + layer = LightTransformerLayer( + n_heads, + k_interests, + hidden_size, + seq_len, + inner_size, + hidden_dropout_prob, + attn_dropout_prob, + hidden_act, + layer_norm_eps, + ) + self.layer = nn.ModuleList([copy.deepcopy(layer) for _ in range(n_layers)]) def forward(self, hidden_states, pos_emb, output_all_encoded_layers=True): """ @@ -746,9 +852,7 @@ def __init__(self): self.num_feature_field = None def get_fields_name_dim(self): - """get user feature field and item feature field. - - """ + """get user feature field and item feature field.""" self.token_field_names = {type: [] for type in self.types} self.token_field_dims = {type: [] for type in self.types} self.float_field_names = {type: [] for type in self.types} @@ -771,25 +875,29 @@ def get_fields_name_dim(self): self.num_feature_field[type] += 1 def get_embedding(self): - """get embedding of all features. - - """ + """get embedding of all features.""" for type in self.types: if len(self.token_field_dims[type]) > 0: - self.token_field_offsets[type] = np.array((0, *np.cumsum(self.token_field_dims[type])[:-1]), - dtype=np.long) + self.token_field_offsets[type] = np.array( + (0, *np.cumsum(self.token_field_dims[type])[:-1]), dtype=np.long + ) self.token_embedding_table[type] = FMEmbedding( - self.token_field_dims[type], self.token_field_offsets[type], self.embedding_size + self.token_field_dims[type], + self.token_field_offsets[type], + self.embedding_size, ).to(self.device) if len(self.float_field_dims[type]) > 0: self.float_embedding_table[type] = nn.Embedding( - np.sum(self.float_field_dims[type], dtype=np.int32), self.embedding_size + np.sum(self.float_field_dims[type], dtype=np.int32), + self.embedding_size, ).to(self.device) if len(self.token_seq_field_dims) > 0: self.token_seq_embedding_table[type] = nn.ModuleList() for token_seq_field_dim in self.token_seq_field_dims[type]: self.token_seq_embedding_table[type].append( - nn.Embedding(token_seq_field_dim, self.embedding_size).to(self.device) + nn.Embedding(token_seq_field_dim, self.embedding_size).to( + self.device + ) ) def embed_float_fields(self, float_fields, type, embed=True): @@ -811,7 +919,13 @@ def embed_float_fields(self, float_fields, type, embed=True): num_float_field = float_fields.shape[-1] # [batch_size, max_item_length, num_float_field] - index = torch.arange(0, num_float_field).unsqueeze(0).expand_as(float_fields).long().to(self.device) + index = ( + torch.arange(0, num_float_field) + .unsqueeze(0) + .expand_as(float_fields) + .long() + .to(self.device) + ) # [batch_size, max_item_length, num_float_field, embed_dim] float_embedding = self.float_embedding_table[type](index) @@ -833,7 +947,7 @@ def embed_token_fields(self, token_fields, type): if token_fields is None: return None # [batch_size, max_item_length, num_token_field, embed_dim] - if type == 'item': + if type == "item": embedding_shape = token_fields.shape + (-1,) token_fields = token_fields.reshape(-1, token_fields.shape[-1]) token_embedding = self.token_embedding_table[type](token_fields) @@ -859,32 +973,44 @@ def embed_token_seq_fields(self, token_seq_fields, type): embedding_table = self.token_seq_embedding_table[type][i] mask = token_seq_field != 0 # [batch_size, max_item_length, seq_len] mask = mask.float() - value_cnt = torch.sum(mask, dim=-1, keepdim=True) # [batch_size, max_item_length, 1] - token_seq_embedding = embedding_table(token_seq_field) # [batch_size, max_item_length, seq_len, embed_dim] + value_cnt = torch.sum( + mask, dim=-1, keepdim=True + ) # [batch_size, max_item_length, 1] + token_seq_embedding = embedding_table( + token_seq_field + ) # [batch_size, max_item_length, seq_len, embed_dim] mask = mask.unsqueeze(-1).expand_as(token_seq_embedding) - if self.pooling_mode == 'max': + if self.pooling_mode == "max": masked_token_seq_embedding = token_seq_embedding - (1 - mask) * 1e9 result = torch.max( masked_token_seq_embedding, dim=-2, keepdim=True ) # [batch_size, max_item_length, 1, embed_dim] result = result.values - elif self.pooling_mode == 'sum': + elif self.pooling_mode == "sum": masked_token_seq_embedding = token_seq_embedding * mask.float() result = torch.sum( masked_token_seq_embedding, dim=-2, keepdim=True ) # [batch_size, max_item_length, 1, embed_dim] else: masked_token_seq_embedding = token_seq_embedding * mask.float() - result = torch.sum(masked_token_seq_embedding, dim=-2) # [batch_size, max_item_length, embed_dim] + result = torch.sum( + masked_token_seq_embedding, dim=-2 + ) # [batch_size, max_item_length, embed_dim] eps = torch.FloatTensor([1e-8]).to(self.device) - result = torch.div(result, value_cnt + eps) # [batch_size, max_item_length, embed_dim] - result = result.unsqueeze(-2) # [batch_size, max_item_length, 1, embed_dim] + result = torch.div( + result, value_cnt + eps + ) # [batch_size, max_item_length, embed_dim] + result = result.unsqueeze( + -2 + ) # [batch_size, max_item_length, 1, embed_dim] fields_result.append(result) if len(fields_result) == 0: return None else: - return torch.cat(fields_result, dim=-2) # [batch_size, max_item_length, num_token_seq_field, embed_dim] + return torch.cat( + fields_result, dim=-2 + ) # [batch_size, max_item_length, num_token_seq_field, embed_dim] def embed_input_fields(self, user_idx, item_idx): """Get the embedding of user_idx and item_idx @@ -897,8 +1023,8 @@ def embed_input_fields(self, user_idx, item_idx): dict: embedding of user feature and item feature """ - user_item_feat = {'user': self.user_feat, 'item': self.item_feat} - user_item_idx = {'user': user_idx, 'item': item_idx} + user_item_feat = {"user": self.user_feat, "item": self.item_feat} + user_item_idx = {"user": user_idx, "item": item_idx} float_fields_embedding = {} token_fields_embedding = {} token_seq_fields_embedding = {} @@ -909,9 +1035,15 @@ def embed_input_fields(self, user_idx, item_idx): float_fields = [] for field_name in self.float_field_names[type]: feature = user_item_feat[type][field_name][user_item_idx[type]] - float_fields.append(feature if len(feature.shape) == (2 + (type == 'item')) else feature.unsqueeze(-1)) + float_fields.append( + feature + if len(feature.shape) == (2 + (type == "item")) + else feature.unsqueeze(-1) + ) if len(float_fields) > 0: - float_fields = torch.cat(float_fields, dim=-1) # [batch_size, max_item_length, num_float_field] + float_fields = torch.cat( + float_fields, dim=-1 + ) # [batch_size, max_item_length, num_float_field] else: float_fields = None # [batch_size, max_item_length, num_float_field] @@ -923,7 +1055,9 @@ def embed_input_fields(self, user_idx, item_idx): feature = user_item_feat[type][field_name][user_item_idx[type]] token_fields.append(feature.unsqueeze(-1)) if len(token_fields) > 0: - token_fields = torch.cat(token_fields, dim=-1) # [batch_size, max_item_length, num_token_field] + token_fields = torch.cat( + token_fields, dim=-1 + ) # [batch_size, max_item_length, num_token_field] else: token_fields = None # [batch_size, max_item_length, num_token_field, embed_dim] or None @@ -934,7 +1068,9 @@ def embed_input_fields(self, user_idx, item_idx): feature = user_item_feat[type][field_name][user_item_idx[type]] token_seq_fields.append(feature) # [batch_size, max_item_length, num_token_seq_field, embed_dim] or None - token_seq_fields_embedding[type] = self.embed_token_seq_fields(token_seq_fields, type) + token_seq_fields_embedding[type] = self.embed_token_seq_fields( + token_seq_fields, type + ) if token_fields_embedding[type] is None: sparse_embedding[type] = token_seq_fields_embedding[type] @@ -942,8 +1078,13 @@ def embed_input_fields(self, user_idx, item_idx): if token_seq_fields_embedding[type] is None: sparse_embedding[type] = token_fields_embedding[type] else: - sparse_embedding[type] = torch.cat([token_fields_embedding[type], token_seq_fields_embedding[type]], - dim=-2) + sparse_embedding[type] = torch.cat( + [ + token_fields_embedding[type], + token_seq_fields_embedding[type], + ], + dim=-2, + ) dense_embedding[type] = float_fields_embedding[type] # sparse_embedding[type] @@ -969,14 +1110,14 @@ def __init__(self, dataset, embedding_size, pooling_mode, device): self.item_feat = self.dataset.get_item_feature().to(self.device) self.field_names = { - 'user': list(self.user_feat.interaction.keys()), - 'item': list(self.item_feat.interaction.keys()) + "user": list(self.user_feat.interaction.keys()), + "item": list(self.item_feat.interaction.keys()), } - self.types = ['user', 'item'] + self.types = ["user", "item"] self.pooling_mode = pooling_mode try: - assert self.pooling_mode in ['mean', 'max', 'sum'] + assert self.pooling_mode in ["mean", "max", "sum"] except AssertionError: raise AssertionError("Make sure 'pooling_mode' in ['mean', 'max', 'sum']!") self.get_fields_name_dim() @@ -987,7 +1128,9 @@ class FeatureSeqEmbLayer(ContextSeqEmbAbstractLayer): """For feature-rich sequential recommenders, return item features embedding matrices according to selected features.""" - def __init__(self, dataset, embedding_size, selected_features, pooling_mode, device): + def __init__( + self, dataset, embedding_size, selected_features, pooling_mode, device + ): super(FeatureSeqEmbLayer, self).__init__() self.device = device @@ -996,12 +1139,12 @@ def __init__(self, dataset, embedding_size, selected_features, pooling_mode, dev self.user_feat = None self.item_feat = self.dataset.get_item_feature().to(self.device) - self.field_names = {'item': selected_features} + self.field_names = {"item": selected_features} - self.types = ['item'] + self.types = ["item"] self.pooling_mode = pooling_mode try: - assert self.pooling_mode in ['mean', 'max', 'sum'] + assert self.pooling_mode in ["mean", "max", "sum"] except AssertionError: raise AssertionError("Make sure 'pooling_mode' in ['mean', 'max', 'sum']!") self.get_fields_name_dim() @@ -1009,7 +1152,7 @@ def __init__(self, dataset, embedding_size, selected_features, pooling_mode, dev class CNNLayers(nn.Module): - r""" CNNLayers + r"""CNNLayers Args: - channels(list): a list contains the channels of each layer in cnn layers @@ -1039,7 +1182,7 @@ class CNNLayers(nn.Module): >>> torch.Size([128, 32, 16, 16]) """ - def __init__(self, channels, kernels, strides, activation='relu', init_method=None): + def __init__(self, channels, kernels, strides, activation="relu", init_method=None): super(CNNLayers, self).__init__() self.channels = channels self.kernels = kernels @@ -1049,23 +1192,28 @@ def __init__(self, channels, kernels, strides, activation='relu', init_method=No self.num_of_nets = len(self.channels) - 1 if len(kernels) != len(strides) or self.num_of_nets != (len(kernels)): - raise RuntimeError('channels, kernels and strides don\'t match\n') + raise RuntimeError("channels, kernels and strides don't match\n") cnn_modules = [] for i in range(self.num_of_nets): cnn_modules.append( - nn.Conv2d(self.channels[i], self.channels[i + 1], self.kernels[i], stride=self.strides[i]) + nn.Conv2d( + self.channels[i], + self.channels[i + 1], + self.kernels[i], + stride=self.strides[i], + ) ) - if self.activation.lower() == 'sigmoid': + if self.activation.lower() == "sigmoid": cnn_modules.append(nn.Sigmoid()) - elif self.activation.lower() == 'tanh': + elif self.activation.lower() == "tanh": cnn_modules.append(nn.Tanh()) - elif self.activation.lower() == 'relu': + elif self.activation.lower() == "relu": cnn_modules.append(nn.ReLU()) - elif self.activation.lower() == 'leakyrelu': + elif self.activation.lower() == "leakyrelu": cnn_modules.append(nn.LeakyReLU()) - elif self.activation.lower() == 'none': + elif self.activation.lower() == "none": pass self.cnn_layers = nn.Sequential(*cnn_modules) @@ -1076,7 +1224,7 @@ def __init__(self, channels, kernels, strides, activation='relu', init_method=No def init_weights(self, module): # We just initialize the module with normal distribution as the paper said if isinstance(module, nn.Conv2d): - if self.init_method == 'norm': + if self.init_method == "norm": normal_(module.weight.data, 0, 0.01) if module.bias is not None: module.bias.data.fill_(0.0) @@ -1103,8 +1251,8 @@ def __init__(self, config, dataset, output_dim=1): FeatureSource.ITEM_ID, ] ) - self.LABEL = config['LABEL_FIELD'] - self.device = config['device'] + self.LABEL = config["LABEL_FIELD"] + self.device = config["device"] self.token_field_names = [] self.token_field_dims = [] self.float_field_names = [] @@ -1124,14 +1272,22 @@ def __init__(self, config, dataset, output_dim=1): self.float_field_names.append(field_name) self.float_field_dims.append(dataset.num(field_name)) if len(self.token_field_dims) > 0: - self.token_field_offsets = np.array((0, *np.cumsum(self.token_field_dims)[:-1]), dtype=np.long) - self.token_embedding_table = FMEmbedding(self.token_field_dims, self.token_field_offsets, output_dim) + self.token_field_offsets = np.array( + (0, *np.cumsum(self.token_field_dims)[:-1]), dtype=np.long + ) + self.token_embedding_table = FMEmbedding( + self.token_field_dims, self.token_field_offsets, output_dim + ) if len(self.float_field_dims) > 0: - self.float_embedding_table = nn.Embedding(np.sum(self.float_field_dims, dtype=np.int32), output_dim) + self.float_embedding_table = nn.Embedding( + np.sum(self.float_field_dims, dtype=np.int32), output_dim + ) if len(self.token_seq_field_dims) > 0: self.token_seq_embedding_table = nn.ModuleList() for token_seq_field_dim in self.token_seq_field_dims: - self.token_seq_embedding_table.append(nn.Embedding(token_seq_field_dim, output_dim)) + self.token_seq_embedding_table.append( + nn.Embedding(token_seq_field_dim, output_dim) + ) self.bias = nn.Parameter(torch.zeros((output_dim,)), requires_grad=True) @@ -1151,7 +1307,13 @@ def embed_float_fields(self, float_fields, embed=True): num_float_field = float_fields.shape[1] # [batch_size, num_float_field] - index = torch.arange(0, num_float_field).unsqueeze(0).expand_as(float_fields).long().to(self.device) + index = ( + torch.arange(0, num_float_field) + .unsqueeze(0) + .expand_as(float_fields) + .long() + .to(self.device) + ) # [batch_size, num_float_field, output_dim] float_embedding = self.float_embedding_table(index) @@ -1198,17 +1360,25 @@ def embed_token_seq_fields(self, token_seq_fields): mask = mask.float() value_cnt = torch.sum(mask, dim=1, keepdim=True) # [batch_size, 1] - token_seq_embedding = embedding_table(token_seq_field) # [batch_size, seq_len, output_dim] + token_seq_embedding = embedding_table( + token_seq_field + ) # [batch_size, seq_len, output_dim] - mask = mask.unsqueeze(2).expand_as(token_seq_embedding) # [batch_size, seq_len, output_dim] + mask = mask.unsqueeze(2).expand_as( + token_seq_embedding + ) # [batch_size, seq_len, output_dim] masked_token_seq_embedding = token_seq_embedding * mask.float() - result = torch.sum(masked_token_seq_embedding, dim=1, keepdim=True) # [batch_size, 1, output_dim] + result = torch.sum( + masked_token_seq_embedding, dim=1, keepdim=True + ) # [batch_size, 1, output_dim] fields_result.append(result) if len(fields_result) == 0: return None else: - return torch.sum(torch.cat(fields_result, dim=1), dim=1, keepdim=True) # [batch_size, 1, output_dim] + return torch.sum( + torch.cat(fields_result, dim=1), dim=1, keepdim=True + ) # [batch_size, 1, output_dim] def forward(self, interaction): total_fields_embedding = [] @@ -1220,7 +1390,9 @@ def forward(self, interaction): float_fields.append(interaction[field_name].unsqueeze(1)) if len(float_fields) > 0: - float_fields = torch.cat(float_fields, dim=1) # [batch_size, num_float_field] + float_fields = torch.cat( + float_fields, dim=1 + ) # [batch_size, num_float_field] else: float_fields = None @@ -1234,7 +1406,9 @@ def forward(self, interaction): for field_name in self.token_field_names: token_fields.append(interaction[field_name].unsqueeze(1)) if len(token_fields) > 0: - token_fields = torch.cat(token_fields, dim=1) # [batch_size, num_token_field] + token_fields = torch.cat( + token_fields, dim=1 + ) # [batch_size, num_token_field] else: token_fields = None # [batch_size, 1, output_dim] or None @@ -1250,7 +1424,9 @@ def forward(self, interaction): if token_seq_fields_embedding is not None: total_fields_embedding.append(token_seq_fields_embedding) - return torch.sum(torch.cat(total_fields_embedding, dim=1), dim=1) + self.bias # [batch_size, output_dim] + return ( + torch.sum(torch.cat(total_fields_embedding, dim=1), dim=1) + self.bias + ) # [batch_size, output_dim] class SparseDropout(nn.Module): diff --git a/recbole/model/loss.py b/recbole/model/loss.py index 1f6b8340d..139438c93 100644 --- a/recbole/model/loss.py +++ b/recbole/model/loss.py @@ -19,7 +19,7 @@ class BPRLoss(nn.Module): - """ BPRLoss, based on Bayesian Personalized Ranking + """BPRLoss, based on Bayesian Personalized Ranking Args: - gamma(float): Small value to avoid division by zero @@ -48,9 +48,7 @@ def forward(self, pos_score, neg_score): class RegLoss(nn.Module): - """ RegLoss, L2 regularization on model parameters - - """ + """RegLoss, L2 regularization on model parameters""" def __init__(self): super(RegLoss, self).__init__() @@ -66,9 +64,7 @@ def forward(self, parameters): class EmbLoss(nn.Module): - """ EmbLoss, regularization on embeddings - - """ + """EmbLoss, regularization on embeddings""" def __init__(self, norm=2): super(EmbLoss, self).__init__() @@ -78,7 +74,9 @@ def forward(self, *embeddings, require_pow=False): if require_pow: emb_loss = torch.zeros(1).to(embeddings[-1].device) for embedding in embeddings: - emb_loss += torch.pow(input=torch.norm(embedding, p=self.norm), exponent=self.norm) + emb_loss += torch.pow( + input=torch.norm(embedding, p=self.norm), exponent=self.norm + ) emb_loss /= embeddings[-1].shape[0] emb_loss /= self.norm return emb_loss @@ -91,8 +89,7 @@ def forward(self, *embeddings, require_pow=False): class EmbMarginLoss(nn.Module): - """ EmbMarginLoss, regularization on embeddings - """ + """EmbMarginLoss, regularization on embeddings""" def __init__(self, power=2): super(EmbMarginLoss, self).__init__() @@ -102,8 +99,8 @@ def forward(self, *embeddings): dev = embeddings[-1].device cache_one = torch.tensor(1.0).to(dev) cache_zero = torch.tensor(0.0).to(dev) - emb_loss = torch.tensor(0.).to(dev) + emb_loss = torch.tensor(0.0).to(dev) for embedding in embeddings: - norm_e = torch.sum(embedding ** self.power, dim=1, keepdim=True) + norm_e = torch.sum(embedding**self.power, dim=1, keepdim=True) emb_loss += torch.sum(torch.max(norm_e - cache_one, cache_zero)) return emb_loss diff --git a/recbole/model/sequential_recommender/bert4rec.py b/recbole/model/sequential_recommender/bert4rec.py index 14c18f68b..5cdee6f85 100644 --- a/recbole/model/sequential_recommender/bert4rec.py +++ b/recbole/model/sequential_recommender/bert4rec.py @@ -26,32 +26,37 @@ class BERT4Rec(SequentialRecommender): - def __init__(self, config, dataset): super(BERT4Rec, self).__init__(config, dataset) # load parameters info - self.n_layers = config['n_layers'] - self.n_heads = config['n_heads'] - self.hidden_size = config['hidden_size'] # same as embedding_size - self.inner_size = config['inner_size'] # the dimensionality in feed-forward layer - self.hidden_dropout_prob = config['hidden_dropout_prob'] - self.attn_dropout_prob = config['attn_dropout_prob'] - self.hidden_act = config['hidden_act'] - self.layer_norm_eps = config['layer_norm_eps'] - - self.mask_ratio = config['mask_ratio'] - - self.loss_type = config['loss_type'] - self.initializer_range = config['initializer_range'] + self.n_layers = config["n_layers"] + self.n_heads = config["n_heads"] + self.hidden_size = config["hidden_size"] # same as embedding_size + self.inner_size = config[ + "inner_size" + ] # the dimensionality in feed-forward layer + self.hidden_dropout_prob = config["hidden_dropout_prob"] + self.attn_dropout_prob = config["attn_dropout_prob"] + self.hidden_act = config["hidden_act"] + self.layer_norm_eps = config["layer_norm_eps"] + + self.mask_ratio = config["mask_ratio"] + + self.loss_type = config["loss_type"] + self.initializer_range = config["initializer_range"] # load dataset info self.mask_token = self.n_items self.mask_item_length = int(self.mask_ratio * self.max_seq_length) # define layers and loss - self.item_embedding = nn.Embedding(self.n_items + 1, self.hidden_size, padding_idx=0) # mask token add 1 - self.position_embedding = nn.Embedding(self.max_seq_length + 1, self.hidden_size) # add mask_token at the last + self.item_embedding = nn.Embedding( + self.n_items + 1, self.hidden_size, padding_idx=0 + ) # mask token add 1 + self.position_embedding = nn.Embedding( + self.max_seq_length + 1, self.hidden_size + ) # add mask_token at the last self.trm_encoder = TransformerEncoder( n_layers=self.n_layers, n_heads=self.n_heads, @@ -60,7 +65,7 @@ def __init__(self, config, dataset): hidden_dropout_prob=self.hidden_dropout_prob, attn_dropout_prob=self.attn_dropout_prob, hidden_act=self.hidden_act, - layer_norm_eps=self.layer_norm_eps + layer_norm_eps=self.layer_norm_eps, ) self.LayerNorm = nn.LayerNorm(self.hidden_size, eps=self.layer_norm_eps) @@ -68,7 +73,7 @@ def __init__(self, config, dataset): # we only need compute the loss at the masked position try: - assert self.loss_type in ['BPR', 'CE'] + assert self.loss_type in ["BPR", "CE"] except AssertionError: raise AssertionError("Make sure 'loss_type' in ['BPR', 'CE']!") @@ -76,7 +81,7 @@ def __init__(self, config, dataset): self.apply(self._init_weights) def _init_weights(self, module): - """ Initialize the weights """ + """Initialize the weights""" if isinstance(module, (nn.Linear, nn.Embedding)): # Slightly different from the TF version which uses truncated_normal for initialization # cf https://github.com/pytorch/pytorch/pull/5617 @@ -134,30 +139,44 @@ def reconstruct_train_data(self, item_seq): masked_item_sequence.append(masked_sequence) pos_items.append(self._padding_sequence(pos_item, self.mask_item_length)) neg_items.append(self._padding_sequence(neg_item, self.mask_item_length)) - masked_index.append(self._padding_sequence(index_ids, self.mask_item_length)) + masked_index.append( + self._padding_sequence(index_ids, self.mask_item_length) + ) # [B Len] - masked_item_sequence = torch.tensor(masked_item_sequence, dtype=torch.long, device=device).view(batch_size, -1) + masked_item_sequence = torch.tensor( + masked_item_sequence, dtype=torch.long, device=device + ).view(batch_size, -1) # [B mask_len] - pos_items = torch.tensor(pos_items, dtype=torch.long, device=device).view(batch_size, -1) + pos_items = torch.tensor(pos_items, dtype=torch.long, device=device).view( + batch_size, -1 + ) # [B mask_len] - neg_items = torch.tensor(neg_items, dtype=torch.long, device=device).view(batch_size, -1) + neg_items = torch.tensor(neg_items, dtype=torch.long, device=device).view( + batch_size, -1 + ) # [B mask_len] - masked_index = torch.tensor(masked_index, dtype=torch.long, device=device).view(batch_size, -1) + masked_index = torch.tensor(masked_index, dtype=torch.long, device=device).view( + batch_size, -1 + ) return masked_item_sequence, pos_items, neg_items, masked_index def reconstruct_test_data(self, item_seq, item_seq_len): """ Add mask token at the last position according to the lengths of item_seq """ - padding = torch.zeros(item_seq.size(0), dtype=torch.long, device=item_seq.device) # [B] + padding = torch.zeros( + item_seq.size(0), dtype=torch.long, device=item_seq.device + ) # [B] item_seq = torch.cat((item_seq, padding.unsqueeze(-1)), dim=-1) # [B max_len+1] for batch_id, last_position in enumerate(item_seq_len): item_seq[batch_id][last_position] = self.mask_token return item_seq def forward(self, item_seq): - position_ids = torch.arange(item_seq.size(1), dtype=torch.long, device=item_seq.device) + position_ids = torch.arange( + item_seq.size(1), dtype=torch.long, device=item_seq.device + ) position_ids = position_ids.unsqueeze(0).expand_as(item_seq) position_embedding = self.position_embedding(position_ids) item_emb = self.item_embedding(item_seq) @@ -165,7 +184,9 @@ def forward(self, item_seq): input_emb = self.LayerNorm(input_emb) input_emb = self.dropout(input_emb) extended_attention_mask = self.get_attention_mask(item_seq, bidirectional=True) - trm_output = self.trm_encoder(input_emb, extended_attention_mask, output_all_encoded_layers=True) + trm_output = self.trm_encoder( + input_emb, extended_attention_mask, output_all_encoded_layers=True + ) output = trm_output[-1] return output # [B L H] @@ -187,40 +208,56 @@ def multi_hot_embed(self, masked_index, max_length): multi_hot_embed: [[0 1 0 0 0], [0 0 0 1 0]] """ masked_index = masked_index.view(-1) - multi_hot = torch.zeros(masked_index.size(0), max_length, device=masked_index.device) + multi_hot = torch.zeros( + masked_index.size(0), max_length, device=masked_index.device + ) multi_hot[torch.arange(masked_index.size(0)), masked_index] = 1 return multi_hot def calculate_loss(self, interaction): item_seq = interaction[self.ITEM_SEQ] - masked_item_seq, pos_items, neg_items, masked_index = self.reconstruct_train_data(item_seq) + ( + masked_item_seq, + pos_items, + neg_items, + masked_index, + ) = self.reconstruct_train_data(item_seq) seq_output = self.forward(masked_item_seq) - pred_index_map = self.multi_hot_embed(masked_index, masked_item_seq.size(-1)) # [B*mask_len max_len] + pred_index_map = self.multi_hot_embed( + masked_index, masked_item_seq.size(-1) + ) # [B*mask_len max_len] # [B mask_len] -> [B mask_len max_len] multi hot - pred_index_map = pred_index_map.view(masked_index.size(0), masked_index.size(1), -1) # [B mask_len max_len] + pred_index_map = pred_index_map.view( + masked_index.size(0), masked_index.size(1), -1 + ) # [B mask_len max_len] # [B mask_len max_len] * [B max_len H] -> [B mask_len H] # only calculate loss for masked position seq_output = torch.bmm(pred_index_map, seq_output) # [B mask_len H] - if self.loss_type == 'BPR': + if self.loss_type == "BPR": pos_items_emb = self.item_embedding(pos_items) # [B mask_len H] neg_items_emb = self.item_embedding(neg_items) # [B mask_len H] pos_score = torch.sum(seq_output * pos_items_emb, dim=-1) # [B mask_len] neg_score = torch.sum(seq_output * neg_items_emb, dim=-1) # [B mask_len] targets = (masked_index > 0).float() - loss = - torch.sum(torch.log(1e-14 + torch.sigmoid(pos_score - neg_score)) * targets) \ - / torch.sum(targets) + loss = -torch.sum( + torch.log(1e-14 + torch.sigmoid(pos_score - neg_score)) * targets + ) / torch.sum(targets) return loss - elif self.loss_type == 'CE': - loss_fct = nn.CrossEntropyLoss(reduction='none') - test_item_emb = self.item_embedding.weight[:self.n_items] # [item_num H] - logits = torch.matmul(seq_output, test_item_emb.transpose(0, 1)) # [B mask_len item_num] + elif self.loss_type == "CE": + loss_fct = nn.CrossEntropyLoss(reduction="none") + test_item_emb = self.item_embedding.weight[: self.n_items] # [item_num H] + logits = torch.matmul( + seq_output, test_item_emb.transpose(0, 1) + ) # [B mask_len item_num] targets = (masked_index > 0).float().view(-1) # [B*mask_len] - loss = torch.sum(loss_fct(logits.view(-1, test_item_emb.size(0)), pos_items.view(-1)) * targets) \ - / torch.sum(targets) + loss = torch.sum( + loss_fct(logits.view(-1, test_item_emb.size(0)), pos_items.view(-1)) + * targets + ) / torch.sum(targets) return loss else: raise NotImplementedError("Make sure 'loss_type' in ['BPR', 'CE']!") @@ -242,6 +279,10 @@ def full_sort_predict(self, interaction): item_seq = self.reconstruct_test_data(item_seq, item_seq_len) seq_output = self.forward(item_seq) seq_output = self.gather_indexes(seq_output, item_seq_len) # [B H] - test_items_emb = self.item_embedding.weight[:self.n_items] # delete masked token - scores = torch.matmul(seq_output, test_items_emb.transpose(0, 1)) # [B, item_num] + test_items_emb = self.item_embedding.weight[ + : self.n_items + ] # delete masked token + scores = torch.matmul( + seq_output, test_items_emb.transpose(0, 1) + ) # [B, item_num] return scores diff --git a/recbole/model/sequential_recommender/caser.py b/recbole/model/sequential_recommender/caser.py index 229816deb..6431e2d39 100644 --- a/recbole/model/sequential_recommender/caser.py +++ b/recbole/model/sequential_recommender/caser.py @@ -43,44 +43,59 @@ def __init__(self, config, dataset): super(Caser, self).__init__(config, dataset) # load parameters info - self.embedding_size = config['embedding_size'] - self.loss_type = config['loss_type'] - self.n_h = config['nh'] - self.n_v = config['nv'] - self.dropout_prob = config['dropout_prob'] - self.reg_weight = config['reg_weight'] + self.embedding_size = config["embedding_size"] + self.loss_type = config["loss_type"] + self.n_h = config["nh"] + self.n_v = config["nv"] + self.dropout_prob = config["dropout_prob"] + self.reg_weight = config["reg_weight"] # load dataset info self.n_users = dataset.user_num # define layers and loss - self.user_embedding = nn.Embedding(self.n_users, self.embedding_size, padding_idx=0) - self.item_embedding = nn.Embedding(self.n_items, self.embedding_size, padding_idx=0) + self.user_embedding = nn.Embedding( + self.n_users, self.embedding_size, padding_idx=0 + ) + self.item_embedding = nn.Embedding( + self.n_items, self.embedding_size, padding_idx=0 + ) # vertical conv layer - self.conv_v = nn.Conv2d(in_channels=1, out_channels=self.n_v, kernel_size=(self.max_seq_length, 1)) + self.conv_v = nn.Conv2d( + in_channels=1, out_channels=self.n_v, kernel_size=(self.max_seq_length, 1) + ) # horizontal conv layer lengths = [i + 1 for i in range(self.max_seq_length)] - self.conv_h = nn.ModuleList([ - nn.Conv2d(in_channels=1, out_channels=self.n_h, kernel_size=(i, self.embedding_size)) for i in lengths - ]) + self.conv_h = nn.ModuleList( + [ + nn.Conv2d( + in_channels=1, + out_channels=self.n_h, + kernel_size=(i, self.embedding_size), + ) + for i in lengths + ] + ) # fully-connected layer self.fc1_dim_v = self.n_v * self.embedding_size self.fc1_dim_h = self.n_h * len(lengths) fc1_dim_in = self.fc1_dim_v + self.fc1_dim_h self.fc1 = nn.Linear(fc1_dim_in, self.embedding_size) - self.fc2 = nn.Linear(self.embedding_size + self.embedding_size, self.embedding_size) + self.fc2 = nn.Linear( + self.embedding_size + self.embedding_size, self.embedding_size + ) self.dropout = nn.Dropout(self.dropout_prob) self.ac_conv = nn.ReLU() self.ac_fc = nn.ReLU() self.reg_loss = RegLoss() - if self.loss_type == 'BPR': + if self.loss_type == "BPR": self.loss_fct = BPRLoss() - elif self.loss_type == 'CE': + elif self.loss_type == "CE": self.loss_fct = nn.CrossEntropyLoss() else: raise NotImplementedError("Make sure 'loss_type' in ['BPR', 'CE']!") @@ -135,7 +150,7 @@ def reg_loss_conv_h(self): """ loss_conv_h = 0 for name, parm in self.conv_h.named_parameters(): - if name.endswith('weight'): + if name.endswith("weight"): loss_conv_h = loss_conv_h + loss_conv_h * parm.norm(2) return self.reg_weight * loss_conv_h @@ -144,7 +159,7 @@ def calculate_loss(self, interaction): user = interaction[self.USER_ID] seq_output = self.forward(user, item_seq) pos_items = interaction[self.POS_ITEM_ID] - if self.loss_type == 'BPR': + if self.loss_type == "BPR": neg_items = interaction[self.NEG_ITEM_ID] pos_items_emb = self.item_embedding(pos_items) neg_items_emb = self.item_embedding(neg_items) @@ -157,9 +172,15 @@ def calculate_loss(self, interaction): logits = torch.matmul(seq_output, test_item_emb.transpose(0, 1)) loss = self.loss_fct(logits, pos_items) - reg_loss = self.reg_loss([ - self.user_embedding.weight, self.item_embedding.weight, self.conv_v.weight, self.fc1.weight, self.fc2.weight - ]) + reg_loss = self.reg_loss( + [ + self.user_embedding.weight, + self.item_embedding.weight, + self.conv_v.weight, + self.fc1.weight, + self.fc2.weight, + ] + ) loss = loss + self.reg_weight * reg_loss + self.reg_loss_conv_h() return loss @@ -177,5 +198,7 @@ def full_sort_predict(self, interaction): user = interaction[self.USER_ID] seq_output = self.forward(user, item_seq) test_items_emb = self.item_embedding.weight - scores = torch.matmul(seq_output, test_items_emb.transpose(0, 1)) # [B, n_items] + scores = torch.matmul( + seq_output, test_items_emb.transpose(0, 1) + ) # [B, n_items] return scores diff --git a/recbole/model/sequential_recommender/core.py b/recbole/model/sequential_recommender/core.py index 8ca4c7d9e..623dc8271 100644 --- a/recbole/model/sequential_recommender/core.py +++ b/recbole/model/sequential_recommender/core.py @@ -22,17 +22,20 @@ class TransNet(nn.Module): def __init__(self, config, dataset): super().__init__() - self.n_layers = config['n_layers'] - self.n_heads = config['n_heads'] - self.hidden_size = config['embedding_size'] - self.inner_size = config['inner_size'] - self.hidden_dropout_prob = config['hidden_dropout_prob'] - self.attn_dropout_prob = config['attn_dropout_prob'] - self.hidden_act = config['hidden_act'] - self.layer_norm_eps = config['layer_norm_eps'] - self.initializer_range = config['initializer_range'] - - self.position_embedding = nn.Embedding(dataset.field2seqlen[config['ITEM_ID_FIELD'] + config['LIST_SUFFIX']], self.hidden_size) + self.n_layers = config["n_layers"] + self.n_heads = config["n_heads"] + self.hidden_size = config["embedding_size"] + self.inner_size = config["inner_size"] + self.hidden_dropout_prob = config["hidden_dropout_prob"] + self.attn_dropout_prob = config["attn_dropout_prob"] + self.hidden_act = config["hidden_act"] + self.layer_norm_eps = config["layer_norm_eps"] + self.initializer_range = config["initializer_range"] + + self.position_embedding = nn.Embedding( + dataset.field2seqlen[config["ITEM_ID_FIELD"] + config["LIST_SUFFIX"]], + self.hidden_size, + ) self.trm_encoder = TransformerEncoder( n_layers=self.n_layers, n_heads=self.n_heads, @@ -41,7 +44,7 @@ def __init__(self, config, dataset): hidden_dropout_prob=self.hidden_dropout_prob, attn_dropout_prob=self.attn_dropout_prob, hidden_act=self.hidden_act, - layer_norm_eps=self.layer_norm_eps + layer_norm_eps=self.layer_norm_eps, ) self.LayerNorm = nn.LayerNorm(self.hidden_size, eps=self.layer_norm_eps) @@ -52,17 +55,21 @@ def __init__(self, config, dataset): def get_attention_mask(self, item_seq, bidirectional=False): """Generate left-to-right uni-directional or bidirectional attention mask for multi-head attention.""" - attention_mask = (item_seq != 0) + attention_mask = item_seq != 0 extended_attention_mask = attention_mask.unsqueeze(1).unsqueeze(2) # torch.bool if not bidirectional: - extended_attention_mask = torch.tril(extended_attention_mask.expand((-1, -1, item_seq.size(-1), -1))) - extended_attention_mask = torch.where(extended_attention_mask, 0., -10000.) + extended_attention_mask = torch.tril( + extended_attention_mask.expand((-1, -1, item_seq.size(-1), -1)) + ) + extended_attention_mask = torch.where(extended_attention_mask, 0.0, -10000.0) return extended_attention_mask def forward(self, item_seq, item_emb): mask = item_seq.gt(0) - position_ids = torch.arange(item_seq.size(1), dtype=torch.long, device=item_seq.device) + position_ids = torch.arange( + item_seq.size(1), dtype=torch.long, device=item_seq.device + ) position_ids = position_ids.unsqueeze(0).expand_as(item_seq) position_embedding = self.position_embedding(position_ids) @@ -72,7 +79,9 @@ def forward(self, item_seq, item_emb): extended_attention_mask = self.get_attention_mask(item_seq) - trm_output = self.trm_encoder(input_emb, extended_attention_mask, output_all_encoded_layers=True) + trm_output = self.trm_encoder( + input_emb, extended_attention_mask, output_all_encoded_layers=True + ) output = trm_output[-1] alpha = self.fn(output).to(torch.double) @@ -81,7 +90,7 @@ def forward(self, item_seq, item_emb): return alpha def _init_weights(self, module): - """ Initialize the weights """ + """Initialize the weights""" if isinstance(module, (nn.Linear, nn.Embedding)): # Slightly different from the TF version which uses truncated_normal for initialization # cf https://github.com/pytorch/pytorch/pull/5617 @@ -95,33 +104,37 @@ def _init_weights(self, module): class CORE(SequentialRecommender): r"""CORE is a simple and effective framewor, which unifies the representation spac - for both the encoding and decoding processes in session-based recommendation. + for both the encoding and decoding processes in session-based recommendation. """ def __init__(self, config, dataset): super(CORE, self).__init__(config, dataset) # load parameters info - self.embedding_size = config['embedding_size'] - self.loss_type = config['loss_type'] + self.embedding_size = config["embedding_size"] + self.loss_type = config["loss_type"] - self.dnn_type = config['dnn_type'] - self.sess_dropout = nn.Dropout(config['sess_dropout']) - self.item_dropout = nn.Dropout(config['item_dropout']) - self.temperature = config['temperature'] + self.dnn_type = config["dnn_type"] + self.sess_dropout = nn.Dropout(config["sess_dropout"]) + self.item_dropout = nn.Dropout(config["item_dropout"]) + self.temperature = config["temperature"] # item embedding - self.item_embedding = nn.Embedding(self.n_items, self.embedding_size, padding_idx=0) + self.item_embedding = nn.Embedding( + self.n_items, self.embedding_size, padding_idx=0 + ) # DNN - if self.dnn_type == 'trm': + if self.dnn_type == "trm": self.net = TransNet(config, dataset) - elif self.dnn_type == 'ave': + elif self.dnn_type == "ave": self.net = self.ave_net else: - raise ValueError(f'dnn_type should be either trm or ave, but have [{self.dnn_type}].') + raise ValueError( + f"dnn_type should be either trm or ave, but have [{self.dnn_type}]." + ) - if self.loss_type == 'CE': + if self.loss_type == "CE": self.loss_fct = nn.CrossEntropyLoss() else: raise NotImplementedError("Make sure 'loss_type' in ['CE']!") @@ -158,7 +171,9 @@ def calculate_loss(self, interaction): all_item_emb = self.item_dropout(all_item_emb) # Robust Distance Measuring (RDM) all_item_emb = F.normalize(all_item_emb, dim=-1) - logits = torch.matmul(seq_output, all_item_emb.transpose(0, 1)) / self.temperature + logits = ( + torch.matmul(seq_output, all_item_emb.transpose(0, 1)) / self.temperature + ) loss = self.loss_fct(logits, pos_items) return loss @@ -177,5 +192,7 @@ def full_sort_predict(self, interaction): test_item_emb = self.item_embedding.weight # no dropout for evaluation test_item_emb = F.normalize(test_item_emb, dim=-1) - scores = torch.matmul(seq_output, test_item_emb.transpose(0, 1)) / self.temperature + scores = ( + torch.matmul(seq_output, test_item_emb.transpose(0, 1)) / self.temperature + ) return scores diff --git a/recbole/model/sequential_recommender/dien.py b/recbole/model/sequential_recommender/dien.py index 28e1d86df..de6a4723a 100644 --- a/recbole/model/sequential_recommender/dien.py +++ b/recbole/model/sequential_recommender/dien.py @@ -27,7 +27,12 @@ from torch.nn.utils.rnn import pack_padded_sequence, pad_packed_sequence, PackedSequence from recbole.utils import ModelType, InputType, FeatureType -from recbole.model.layers import FMEmbedding, MLPLayers, ContextSeqEmbLayer, SequenceAttLayer +from recbole.model.layers import ( + FMEmbedding, + MLPLayers, + ContextSeqEmbLayer, + SequenceAttLayer, +) from recbole.model.abstract_recommender import SequentialRecommender @@ -38,55 +43,72 @@ class DIEN(SequentialRecommender): interests are strengthened during interest evolution. """ + input_type = InputType.POINTWISE def __init__(self, config, dataset): super(DIEN, self).__init__(config, dataset) # get field names and parameter value from config - self.device = config['device'] - self.alpha = config['alpha'] - self.gru = config['gru_type'] - self.pooling_mode = config['pooling_mode'] - self.dropout_prob = config['dropout_prob'] - self.LABEL_FIELD = config['LABEL_FIELD'] - self.embedding_size = config['embedding_size'] - self.mlp_hidden_size = config['mlp_hidden_size'] - self.NEG_ITEM_SEQ = config['NEG_PREFIX'] + self.ITEM_SEQ - - self.types = ['user', 'item'] + self.device = config["device"] + self.alpha = config["alpha"] + self.gru = config["gru_type"] + self.pooling_mode = config["pooling_mode"] + self.dropout_prob = config["dropout_prob"] + self.LABEL_FIELD = config["LABEL_FIELD"] + self.embedding_size = config["embedding_size"] + self.mlp_hidden_size = config["mlp_hidden_size"] + self.NEG_ITEM_SEQ = config["NEG_PREFIX"] + self.ITEM_SEQ + + self.types = ["user", "item"] self.user_feat = dataset.get_user_feature() self.item_feat = dataset.get_item_feature() num_item_feature = sum( - 1 if dataset.field2type[field] != FeatureType.FLOAT_SEQ else dataset.num(field) + 1 + if dataset.field2type[field] != FeatureType.FLOAT_SEQ + else dataset.num(field) for field in self.item_feat.interaction.keys() ) num_user_feature = sum( - 1 if dataset.field2type[field] != FeatureType.FLOAT_SEQ else dataset.num(field) + 1 + if dataset.field2type[field] != FeatureType.FLOAT_SEQ + else dataset.num(field) for field in self.user_feat.interaction.keys() ) item_feat_dim = num_item_feature * self.embedding_size - mask_mat = torch.arange(self.max_seq_length).to(self.device).view(1, -1) # init mask + mask_mat = ( + torch.arange(self.max_seq_length).to(self.device).view(1, -1) + ) # init mask # init sizes of used layers - self.att_list = [4 * num_item_feature * self.embedding_size] + self.mlp_hidden_size + self.att_list = [ + 4 * num_item_feature * self.embedding_size + ] + self.mlp_hidden_size self.interest_mlp_list = [2 * item_feat_dim] + self.mlp_hidden_size + [1] - self.dnn_mlp_list = [2 * item_feat_dim + num_user_feature * self.embedding_size] + self.mlp_hidden_size + self.dnn_mlp_list = [ + 2 * item_feat_dim + num_user_feature * self.embedding_size + ] + self.mlp_hidden_size # init interest extractor layer, interest evolving layer embedding layer, MLP layer and linear layer - self.interset_extractor = InterestExtractorNetwork(item_feat_dim, item_feat_dim, self.interest_mlp_list) + self.interset_extractor = InterestExtractorNetwork( + item_feat_dim, item_feat_dim, self.interest_mlp_list + ) self.interest_evolution = InterestEvolvingLayer( mask_mat, item_feat_dim, item_feat_dim, self.att_list, gru=self.gru ) - self.embedding_layer = ContextSeqEmbLayer(dataset, self.embedding_size, self.pooling_mode, self.device) - self.dnn_mlp_layers = MLPLayers(self.dnn_mlp_list, activation='Dice', dropout=self.dropout_prob, bn=True) + self.embedding_layer = ContextSeqEmbLayer( + dataset, self.embedding_size, self.pooling_mode, self.device + ) + self.dnn_mlp_layers = MLPLayers( + self.dnn_mlp_list, activation="Dice", dropout=self.dropout_prob, bn=True + ) self.dnn_predict_layer = nn.Linear(self.mlp_hidden_size[-1], 1) self.sigmoid = nn.Sigmoid() self.loss = nn.BCEWithLogitsLoss() self.apply(self._init_weights) - self.other_parameter_name = ['embedding_layer'] + self.other_parameter_name = ["embedding_layer"] def _init_weights(self, module): if isinstance(module, nn.Embedding): @@ -100,8 +122,12 @@ def forward(self, user, item_seq, neg_item_seq, item_seq_len, next_items): max_length = item_seq.shape[1] # concatenate the history item seq with the target item to get embedding together - item_seq_next_item = torch.cat((item_seq, neg_item_seq, next_items.unsqueeze(1)), dim=-1) - sparse_embedding, dense_embedding = self.embedding_layer(user, item_seq_next_item) + item_seq_next_item = torch.cat( + (item_seq, neg_item_seq, next_items.unsqueeze(1)), dim=-1 + ) + sparse_embedding, dense_embedding = self.embedding_layer( + user, item_seq_next_item + ) # concat the sparse embedding and float embedding feature_table = {} for type in self.types: @@ -114,17 +140,23 @@ def forward(self, user, item_seq, neg_item_seq, item_seq_len, next_items): feature_table[type] = torch.cat(feature_table[type], dim=-2) table_shape = feature_table[type].shape feat_num, embedding_size = table_shape[-2], table_shape[-1] - feature_table[type] = feature_table[type].view(table_shape[:-2] + (feat_num * embedding_size,)) + feature_table[type] = feature_table[type].view( + table_shape[:-2] + (feat_num * embedding_size,) + ) - user_feat_list = feature_table['user'] - item_feat_list, neg_item_feat_list, target_item_feat_emb = feature_table['item'].split( - [max_length, max_length, 1], dim=1 - ) + user_feat_list = feature_table["user"] + item_feat_list, neg_item_feat_list, target_item_feat_emb = feature_table[ + "item" + ].split([max_length, max_length, 1], dim=1) target_item_feat_emb = target_item_feat_emb.squeeze(1) # interest - interest, aux_loss = self.interset_extractor(item_feat_list, item_seq_len, neg_item_feat_list) - evolution = self.interest_evolution(target_item_feat_emb, interest, item_seq_len) + interest, aux_loss = self.interset_extractor( + item_feat_list, item_seq_len, neg_item_feat_list + ) + evolution = self.interest_evolution( + target_item_feat_emb, interest, item_seq_len + ) dien_in = torch.cat([evolution, target_item_feat_emb, user_feat_list], dim=-1) # input the DNN to get the prediction score @@ -139,7 +171,9 @@ def calculate_loss(self, interaction): user = interaction[self.USER_ID] item_seq_len = interaction[self.ITEM_SEQ_LEN] next_items = interaction[self.POS_ITEM_ID] - output, aux_loss = self.forward(user, item_seq, neg_item_seq, item_seq_len, next_items) + output, aux_loss = self.forward( + user, item_seq, neg_item_seq, item_seq_len, next_items + ) loss = self.loss(output, label) + self.alpha * aux_loss return loss @@ -161,18 +195,24 @@ class InterestExtractorNetwork(nn.Module): def __init__(self, input_size, hidden_size, mlp_size): super(InterestExtractorNetwork, self).__init__() - self.gru = nn.GRU(input_size=input_size, hidden_size=hidden_size, batch_first=True) - self.auxiliary_net = MLPLayers(layers=mlp_size, activation='none') + self.gru = nn.GRU( + input_size=input_size, hidden_size=hidden_size, batch_first=True + ) + self.auxiliary_net = MLPLayers(layers=mlp_size, activation="none") def forward(self, keys, keys_length, neg_keys=None): batch_size, hist_len, embedding_size = keys.shape - packed_keys = pack_padded_sequence(keys, lengths=keys_length.cpu(), batch_first=True, enforce_sorted=False) + packed_keys = pack_padded_sequence( + keys, lengths=keys_length.cpu(), batch_first=True, enforce_sorted=False + ) packed_rnn_outputs, _ = self.gru(packed_keys) rnn_outputs, _ = pad_packed_sequence( packed_rnn_outputs, batch_first=True, padding_value=0, total_length=hist_len ) - aux_loss = self.auxiliary_loss(rnn_outputs[:, :-1, :], keys[:, 1:, :], neg_keys[:, 1:, :], keys_length - 1) + aux_loss = self.auxiliary_loss( + rnn_outputs[:, :-1, :], keys[:, 1:, :], neg_keys[:, 1:, :], keys_length - 1 + ) return rnn_outputs, aux_loss @@ -197,17 +237,22 @@ def auxiliary_loss(self, h_states, click_seq, noclick_seq, keys_length): noclick_input = torch.cat([h_states, noclick_seq], dim=-1) # click predict - click_prop = self.auxiliary_net(click_input.view(batch_size * hist_length, -1)).view(-1, 1) + click_prop = self.auxiliary_net( + click_input.view(batch_size * hist_length, -1) + ).view(-1, 1) # click label click_target = torch.ones(click_prop.shape, device=click_input.device) # non-click predict - noclick_prop = self.auxiliary_net(noclick_input.view(batch_size * hist_length, -1)).view(-1, 1) + noclick_prop = self.auxiliary_net( + noclick_input.view(batch_size * hist_length, -1) + ).view(-1, 1) # non-click label noclick_target = torch.zeros(noclick_prop.shape, device=noclick_input.device) loss = F.binary_cross_entropy_with_logits( - torch.cat([click_prop, noclick_prop], dim=0), torch.cat([click_target, noclick_target], dim=0) + torch.cat([click_prop, noclick_prop], dim=0), + torch.cat([click_target, noclick_target], dim=0), ) return loss @@ -225,26 +270,38 @@ def __init__( input_size, rnn_hidden_size, att_hidden_size=(80, 40), - activation='sigmoid', + activation="sigmoid", softmax_stag=False, - gru='GRU' + gru="GRU", ): super(InterestEvolvingLayer, self).__init__() self.mask_mat = mask_mat self.gru = gru - if gru == 'GRU': - self.attention_layer = SequenceAttLayer(mask_mat, att_hidden_size, activation, softmax_stag, False) - self.dynamic_rnn = nn.GRU(input_size=input_size, hidden_size=rnn_hidden_size, batch_first=True) + if gru == "GRU": + self.attention_layer = SequenceAttLayer( + mask_mat, att_hidden_size, activation, softmax_stag, False + ) + self.dynamic_rnn = nn.GRU( + input_size=input_size, hidden_size=rnn_hidden_size, batch_first=True + ) - elif gru == 'AIGRU': - self.attention_layer = SequenceAttLayer(mask_mat, att_hidden_size, activation, softmax_stag, True) - self.dynamic_rnn = nn.GRU(input_size=input_size, hidden_size=rnn_hidden_size, batch_first=True) + elif gru == "AIGRU": + self.attention_layer = SequenceAttLayer( + mask_mat, att_hidden_size, activation, softmax_stag, True + ) + self.dynamic_rnn = nn.GRU( + input_size=input_size, hidden_size=rnn_hidden_size, batch_first=True + ) - elif gru == 'AGRU' or gru == 'AUGRU': - self.attention_layer = SequenceAttLayer(mask_mat, att_hidden_size, activation, softmax_stag, True) - self.dynamic_rnn = DynamicRNN(input_size=input_size, hidden_size=rnn_hidden_size, gru=gru) + elif gru == "AGRU" or gru == "AUGRU": + self.attention_layer = SequenceAttLayer( + mask_mat, att_hidden_size, activation, softmax_stag, True + ) + self.dynamic_rnn = DynamicRNN( + input_size=input_size, hidden_size=rnn_hidden_size, gru=gru + ) def final_output(self, outputs, keys_length): """get the last effective value in the interest evolution sequence @@ -257,53 +314,69 @@ def final_output(self, outputs, keys_length): """ batch_size, hist_len, _ = outputs.shape # [B, T, H] - mask = ( - torch.arange(hist_len, device=keys_length.device).repeat(batch_size, 1) == (keys_length.view(-1, 1) - 1) - ) + mask = torch.arange(hist_len, device=keys_length.device).repeat( + batch_size, 1 + ) == (keys_length.view(-1, 1) - 1) return outputs[mask] def forward(self, queries, keys, keys_length): hist_len = keys.shape[1] # T keys_length_cpu = keys_length.cpu() - if self.gru == 'GRU': + if self.gru == "GRU": packed_keys = pack_padded_sequence( - input=keys, lengths=keys_length_cpu, batch_first=True, enforce_sorted=False + input=keys, + lengths=keys_length_cpu, + batch_first=True, + enforce_sorted=False, ) packed_rnn_outputs, _ = self.dynamic_rnn(packed_keys) rnn_outputs, _ = pad_packed_sequence( - packed_rnn_outputs, batch_first=True, padding_value=0.0, total_length=hist_len + packed_rnn_outputs, + batch_first=True, + padding_value=0.0, + total_length=hist_len, ) att_outputs = self.attention_layer(queries, rnn_outputs, keys_length) outputs = att_outputs.squeeze(1) # AIGRU - elif self.gru == 'AIGRU': + elif self.gru == "AIGRU": att_outputs = self.attention_layer(queries, keys, keys_length) interest = keys * att_outputs.transpose(1, 2) packed_rnn_outputs = pack_padded_sequence( - interest, lengths=keys_length_cpu, batch_first=True, enforce_sorted=False + interest, + lengths=keys_length_cpu, + batch_first=True, + enforce_sorted=False, ) _, outputs = self.dynamic_rnn(packed_rnn_outputs) outputs = outputs.squeeze(0) - elif self.gru == 'AGRU' or self.gru == 'AUGRU': - att_outputs = self.attention_layer(queries, keys, keys_length).squeeze(1) # [B, T] + elif self.gru == "AGRU" or self.gru == "AUGRU": + att_outputs = self.attention_layer(queries, keys, keys_length).squeeze( + 1 + ) # [B, T] packed_rnn_outputs = pack_padded_sequence( keys, lengths=keys_length_cpu, batch_first=True, enforce_sorted=False ) packed_att_outputs = pack_padded_sequence( - att_outputs, lengths=keys_length_cpu, batch_first=True, enforce_sorted=False + att_outputs, + lengths=keys_length_cpu, + batch_first=True, + enforce_sorted=False, ) outputs = self.dynamic_rnn(packed_rnn_outputs, packed_att_outputs) - outputs, _ = pad_packed_sequence(outputs, batch_first=True, padding_value=0.0, total_length=hist_len) + outputs, _ = pad_packed_sequence( + outputs, batch_first=True, padding_value=0.0, total_length=hist_len + ) outputs = self.final_output(outputs, keys_length) # [B, H] return outputs class AGRUCell(nn.Module): - """ Attention based GRU (AGRU). AGRU uses the attention score to replace the update gate of GRU, and changes the + """Attention based GRU (AGRU). AGRU uses the attention score to replace the update gate of GRU, and changes the hidden state directly. Formally: @@ -329,8 +402,8 @@ def __init__(self, input_size, hidden_size, bias=True): # (b_hr|b_hu|b_hh) self.bias_hh = nn.Parameter(torch.zeros(3 * hidden_size)) else: - self.register_parameter('bias_ih', None) - self.register_parameter('bias_hh', None) + self.register_parameter("bias_ih", None) + self.register_parameter("bias_hh", None) def forward(self, input, hidden_output, att_score): gi = F.linear(input, self.weight_ih, self.bias_ih) @@ -371,8 +444,8 @@ def __init__(self, input_size, hidden_size, bias=True): # (b_hr|b_hu|b_hh) self.bias_hh = nn.Parameter(torch.zeros(3 * hidden_size)) else: - self.register_parameter('bias_ih', None) - self.register_parameter('bias_hh', None) + self.register_parameter("bias_ih", None) + self.register_parameter("bias_hh", None) def forward(self, input, hidden_output, att_score): gi = F.linear(input, self.weight_ih, self.bias_ih) @@ -392,35 +465,46 @@ def forward(self, input, hidden_output, att_score): class DynamicRNN(nn.Module): - - def __init__(self, input_size, hidden_size, bias=True, gru='AGRU'): + def __init__(self, input_size, hidden_size, bias=True, gru="AGRU"): super(DynamicRNN, self).__init__() self.input_size = input_size self.hidden_size = hidden_size - if gru == 'AGRU': + if gru == "AGRU": self.rnn = AGRUCell(input_size, hidden_size, bias) - elif gru == 'AUGRU': + elif gru == "AUGRU": self.rnn = AUGRUCell(input_size, hidden_size, bias) def forward(self, input, att_scores=None, hidden_output=None): - if not isinstance(input, PackedSequence) or not isinstance(att_scores, PackedSequence): - raise NotImplementedError("DynamicRNN only supports packed input and att_scores") + if not isinstance(input, PackedSequence) or not isinstance( + att_scores, PackedSequence + ): + raise NotImplementedError( + "DynamicRNN only supports packed input and att_scores" + ) input, batch_sizes, sorted_indices, unsorted_indices = input att_scores = att_scores.data max_batch_size = int(batch_sizes[0]) if hidden_output is None: - hidden_output = torch.zeros(max_batch_size, self.hidden_size, dtype=input.dtype, device=input.device) + hidden_output = torch.zeros( + max_batch_size, self.hidden_size, dtype=input.dtype, device=input.device + ) - outputs = torch.zeros(input.size(0), self.hidden_size, dtype=input.dtype, device=input.device) + outputs = torch.zeros( + input.size(0), self.hidden_size, dtype=input.dtype, device=input.device + ) begin = 0 for batch in batch_sizes: - new_hx = self.rnn(input[begin:begin + batch], hidden_output[0:batch], att_scores[begin:begin + batch]) - outputs[begin:begin + batch] = new_hx + new_hx = self.rnn( + input[begin : begin + batch], + hidden_output[0:batch], + att_scores[begin : begin + batch], + ) + outputs[begin : begin + batch] = new_hx hidden_output = new_hx begin += batch - + return PackedSequence(outputs, batch_sizes, sorted_indices, unsorted_indices) diff --git a/recbole/model/sequential_recommender/din.py b/recbole/model/sequential_recommender/din.py index 47aa1ff51..7597b8e8f 100644 --- a/recbole/model/sequential_recommender/din.py +++ b/recbole/model/sequential_recommender/din.py @@ -40,20 +40,21 @@ class DIN(SequentialRecommender): Besides, in order to compare with other models, we use AUC instead of GAUC to evaluate the model. """ + input_type = InputType.POINTWISE def __init__(self, config, dataset): super(DIN, self).__init__(config, dataset) # get field names and parameter value from config - self.LABEL_FIELD = config['LABEL_FIELD'] - self.embedding_size = config['embedding_size'] - self.mlp_hidden_size = config['mlp_hidden_size'] - self.device = config['device'] - self.pooling_mode = config['pooling_mode'] - self.dropout_prob = config['dropout_prob'] - - self.types = ['user', 'item'] + self.LABEL_FIELD = config["LABEL_FIELD"] + self.embedding_size = config["embedding_size"] + self.mlp_hidden_size = config["mlp_hidden_size"] + self.device = config["device"] + self.pooling_mode = config["pooling_mode"] + self.dropout_prob = config["dropout_prob"] + + self.types = ["user", "item"] self.user_feat = dataset.get_user_feature() self.item_feat = dataset.get_item_feature() @@ -61,26 +62,42 @@ def __init__(self, config, dataset): # self.dnn_list = [(3 * self.num_feature_field['item'] + self.num_feature_field['user']) # * self.embedding_size] + self.mlp_hidden_size num_item_feature = sum( - 1 if dataset.field2type[field] != FeatureType.FLOAT_SEQ else dataset.num(field) + 1 + if dataset.field2type[field] != FeatureType.FLOAT_SEQ + else dataset.num(field) for field in self.item_feat.interaction.keys() ) - self.dnn_list = [3 * num_item_feature * self.embedding_size] + self.mlp_hidden_size - self.att_list = [4 * num_item_feature * self.embedding_size] + self.mlp_hidden_size - - mask_mat = torch.arange(self.max_seq_length).to(self.device).view(1, -1) # init mask + self.dnn_list = [ + 3 * num_item_feature * self.embedding_size + ] + self.mlp_hidden_size + self.att_list = [ + 4 * num_item_feature * self.embedding_size + ] + self.mlp_hidden_size + + mask_mat = ( + torch.arange(self.max_seq_length).to(self.device).view(1, -1) + ) # init mask self.attention = SequenceAttLayer( - mask_mat, self.att_list, activation='Sigmoid', softmax_stag=False, return_seq_weight=False + mask_mat, + self.att_list, + activation="Sigmoid", + softmax_stag=False, + return_seq_weight=False, + ) + self.dnn_mlp_layers = MLPLayers( + self.dnn_list, activation="Dice", dropout=self.dropout_prob, bn=True ) - self.dnn_mlp_layers = MLPLayers(self.dnn_list, activation='Dice', dropout=self.dropout_prob, bn=True) - self.embedding_layer = ContextSeqEmbLayer(dataset, self.embedding_size, self.pooling_mode, self.device) + self.embedding_layer = ContextSeqEmbLayer( + dataset, self.embedding_size, self.pooling_mode, self.device + ) self.dnn_predict_layers = nn.Linear(self.mlp_hidden_size[-1], 1) self.sigmoid = nn.Sigmoid() self.loss = nn.BCEWithLogitsLoss() # parameters initialization self.apply(self._init_weights) - self.other_parameter_name = ['embedding_layer'] + self.other_parameter_name = ["embedding_layer"] def _init_weights(self, module): if isinstance(module, nn.Embedding): @@ -95,7 +112,9 @@ def forward(self, user, item_seq, item_seq_len, next_items): max_length = item_seq.shape[1] # concatenate the history item seq with the target item to get embedding together item_seq_next_item = torch.cat((item_seq, next_items.unsqueeze(1)), dim=-1) - sparse_embedding, dense_embedding = self.embedding_layer(user, item_seq_next_item) + sparse_embedding, dense_embedding = self.embedding_layer( + user, item_seq_next_item + ) # concat the sparse embedding and float embedding feature_table = {} for type in self.types: @@ -108,10 +127,14 @@ def forward(self, user, item_seq, item_seq_len, next_items): feature_table[type] = torch.cat(feature_table[type], dim=-2) table_shape = feature_table[type].shape feat_num, embedding_size = table_shape[-2], table_shape[-1] - feature_table[type] = feature_table[type].view(table_shape[:-2] + (feat_num * embedding_size,)) + feature_table[type] = feature_table[type].view( + table_shape[:-2] + (feat_num * embedding_size,) + ) - user_feat_list = feature_table['user'] - item_feat_list, target_item_feat_emb = feature_table['item'].split([max_length, 1], dim=1) + user_feat_list = feature_table["user"] + item_feat_list, target_item_feat_emb = feature_table["item"].split( + [max_length, 1], dim=1 + ) target_item_feat_emb = target_item_feat_emb.squeeze(1) # attention @@ -119,7 +142,9 @@ def forward(self, user, item_seq, item_seq_len, next_items): user_emb = user_emb.squeeze(1) # input the DNN to get the prediction score - din_in = torch.cat([user_emb, target_item_feat_emb, user_emb * target_item_feat_emb], dim=-1) + din_in = torch.cat( + [user_emb, target_item_feat_emb, user_emb * target_item_feat_emb], dim=-1 + ) din_out = self.dnn_mlp_layers(din_in) preds = self.dnn_predict_layers(din_out) diff --git a/recbole/model/sequential_recommender/fdsa.py b/recbole/model/sequential_recommender/fdsa.py index ccd4823b9..3b1adef24 100644 --- a/recbole/model/sequential_recommender/fdsa.py +++ b/recbole/model/sequential_recommender/fdsa.py @@ -17,7 +17,11 @@ from torch import nn from recbole.model.abstract_recommender import SequentialRecommender -from recbole.model.layers import TransformerEncoder, FeatureSeqEmbLayer, VanillaAttention +from recbole.model.layers import ( + TransformerEncoder, + FeatureSeqEmbLayer, + VanillaAttention, +) from recbole.model.loss import BPRLoss @@ -32,29 +36,37 @@ def __init__(self, config, dataset): super(FDSA, self).__init__(config, dataset) # load parameters info - self.n_layers = config['n_layers'] - self.n_heads = config['n_heads'] - self.hidden_size = config['hidden_size'] # same as embedding_size - self.inner_size = config['inner_size'] # the dimensionality in feed-forward layer - self.hidden_dropout_prob = config['hidden_dropout_prob'] - self.attn_dropout_prob = config['attn_dropout_prob'] - self.hidden_act = config['hidden_act'] - self.layer_norm_eps = config['layer_norm_eps'] - - self.selected_features = config['selected_features'] - self.pooling_mode = config['pooling_mode'] - self.device = config['device'] - self.num_feature_field = len(config['selected_features']) - - self.initializer_range = config['initializer_range'] - self.loss_type = config['loss_type'] + self.n_layers = config["n_layers"] + self.n_heads = config["n_heads"] + self.hidden_size = config["hidden_size"] # same as embedding_size + self.inner_size = config[ + "inner_size" + ] # the dimensionality in feed-forward layer + self.hidden_dropout_prob = config["hidden_dropout_prob"] + self.attn_dropout_prob = config["attn_dropout_prob"] + self.hidden_act = config["hidden_act"] + self.layer_norm_eps = config["layer_norm_eps"] + + self.selected_features = config["selected_features"] + self.pooling_mode = config["pooling_mode"] + self.device = config["device"] + self.num_feature_field = len(config["selected_features"]) + + self.initializer_range = config["initializer_range"] + self.loss_type = config["loss_type"] # define layers and loss - self.item_embedding = nn.Embedding(self.n_items, self.hidden_size, padding_idx=0) + self.item_embedding = nn.Embedding( + self.n_items, self.hidden_size, padding_idx=0 + ) self.position_embedding = nn.Embedding(self.max_seq_length, self.hidden_size) self.feature_embed_layer = FeatureSeqEmbLayer( - dataset, self.hidden_size, self.selected_features, self.pooling_mode, self.device + dataset, + self.hidden_size, + self.selected_features, + self.pooling_mode, + self.device, ) self.item_trm_encoder = TransformerEncoder( @@ -65,7 +77,7 @@ def __init__(self, config, dataset): hidden_dropout_prob=self.hidden_dropout_prob, attn_dropout_prob=self.attn_dropout_prob, hidden_act=self.hidden_act, - layer_norm_eps=self.layer_norm_eps + layer_norm_eps=self.layer_norm_eps, ) self.feature_att_layer = VanillaAttention(self.hidden_size, self.hidden_size) @@ -78,25 +90,25 @@ def __init__(self, config, dataset): hidden_dropout_prob=self.hidden_dropout_prob, attn_dropout_prob=self.attn_dropout_prob, hidden_act=self.hidden_act, - layer_norm_eps=self.layer_norm_eps + layer_norm_eps=self.layer_norm_eps, ) self.LayerNorm = nn.LayerNorm(self.hidden_size, eps=self.layer_norm_eps) self.dropout = nn.Dropout(self.hidden_dropout_prob) self.concat_layer = nn.Linear(self.hidden_size * 2, self.hidden_size) - if self.loss_type == 'BPR': + if self.loss_type == "BPR": self.loss_fct = BPRLoss() - elif self.loss_type == 'CE': + elif self.loss_type == "CE": self.loss_fct = nn.CrossEntropyLoss() else: raise NotImplementedError("Make sure 'loss_type' in ['BPR', 'CE']!") # parameters initialization self.apply(self._init_weights) - self.other_parameter_name = ['feature_embed_layer'] + self.other_parameter_name = ["feature_embed_layer"] def _init_weights(self, module): - """ Initialize the weights """ + """Initialize the weights""" if isinstance(module, (nn.Linear, nn.Embedding)): # Slightly different from the TF version which uses truncated_normal for initialization # cf https://github.com/pytorch/pytorch/pull/5617 @@ -110,7 +122,9 @@ def _init_weights(self, module): def forward(self, item_seq, item_seq_len): item_emb = self.item_embedding(item_seq) - position_ids = torch.arange(item_seq.size(1), dtype=torch.long, device=item_seq.device) + position_ids = torch.arange( + item_seq.size(1), dtype=torch.long, device=item_seq.device + ) position_ids = position_ids.unsqueeze(0).expand_as(item_seq) position_embedding = self.position_embedding(position_ids) @@ -121,8 +135,8 @@ def forward(self, item_seq, item_seq_len): item_trm_input = self.dropout(item_emb) sparse_embedding, dense_embedding = self.feature_embed_layer(None, item_seq) - sparse_embedding = sparse_embedding['item'] - dense_embedding = dense_embedding['item'] + sparse_embedding = sparse_embedding["item"] + dense_embedding = dense_embedding["item"] # concat the sparse embedding and float embedding feature_table = [] @@ -145,7 +159,9 @@ def forward(self, item_seq, item_seq_len): extended_attention_mask = self.get_attention_mask(item_seq) - item_trm_output = self.item_trm_encoder(item_trm_input, extended_attention_mask, output_all_encoded_layers=True) + item_trm_output = self.item_trm_encoder( + item_trm_input, extended_attention_mask, output_all_encoded_layers=True + ) item_output = item_trm_output[-1] feature_trm_output = self.feature_trm_encoder( @@ -167,7 +183,7 @@ def calculate_loss(self, interaction): item_seq_len = interaction[self.ITEM_SEQ_LEN] seq_output = self.forward(item_seq, item_seq_len) pos_items = interaction[self.POS_ITEM_ID] - if self.loss_type == 'BPR': + if self.loss_type == "BPR": neg_items = interaction[self.NEG_ITEM_ID] pos_items_emb = self.item_embedding(pos_items) neg_items_emb = self.item_embedding(neg_items) @@ -195,5 +211,7 @@ def full_sort_predict(self, interaction): item_seq_len = interaction[self.ITEM_SEQ_LEN] seq_output = self.forward(item_seq, item_seq_len) test_items_emb = self.item_embedding.weight - scores = torch.matmul(seq_output, test_items_emb.transpose(0, 1)) # [B, n_items] + scores = torch.matmul( + seq_output, test_items_emb.transpose(0, 1) + ) # [B, n_items] return scores diff --git a/recbole/model/sequential_recommender/fossil.py b/recbole/model/sequential_recommender/fossil.py index 0432174bf..27a89987d 100644 --- a/recbole/model/sequential_recommender/fossil.py +++ b/recbole/model/sequential_recommender/fossil.py @@ -34,24 +34,28 @@ def __init__(self, config, dataset): # load the dataset information self.n_users = dataset.num(self.USER_ID) - self.device = config['device'] + self.device = config["device"] # load the parameters self.embedding_size = config["embedding_size"] self.order_len = config["order_len"] - assert self.order_len <= self.max_seq_length, "order_len can't longer than the max_seq_length" + assert ( + self.order_len <= self.max_seq_length + ), "order_len can't longer than the max_seq_length" self.reg_weight = config["reg_weight"] self.alpha = config["alpha"] # define the layers and loss type - self.item_embedding = nn.Embedding(self.n_items, self.embedding_size, padding_idx=0) + self.item_embedding = nn.Embedding( + self.n_items, self.embedding_size, padding_idx=0 + ) self.user_lambda = nn.Embedding(self.n_users, self.order_len) self.lambda_ = nn.Parameter(torch.zeros(self.order_len)).to(self.device) - self.loss_type = config['loss_type'] - if self.loss_type == 'BPR': + self.loss_type = config["loss_type"] + if self.loss_type == "BPR": self.loss_fct = BPRLoss() - elif self.loss_type == 'CE': + elif self.loss_type == "CE": self.loss_fct = nn.CrossEntropyLoss() else: raise NotImplementedError("Make sure 'loss_type' in ['BPR', 'CE']!") @@ -78,7 +82,8 @@ def inverse_seq_item_embedding(self, seq_item_embedding, seq_item_len): embedding_list = list() for i in range(self.order_len): embedding = self.gather_indexes( - item_embedding_zeros, self.max_seq_length + seq_item_len - self.order_len + i + item_embedding_zeros, + self.max_seq_length + seq_item_len - self.order_len + i, ) embedding_list.append(embedding.unsqueeze(1)) short_item_embedding = torch.cat(embedding_list, dim=1) @@ -89,9 +94,11 @@ def inverse_seq_item_embedding(self, seq_item_embedding, seq_item_len): def reg_loss(self, user_embedding, item_embedding, seq_output): reg_1 = self.reg_weight - loss_1 = reg_1 * torch.norm(user_embedding, p=2) \ - + reg_1 * torch.norm(item_embedding, p=2) \ - + reg_1 * torch.norm(seq_output, p=2) + loss_1 = ( + reg_1 * torch.norm(user_embedding, p=2) + + reg_1 * torch.norm(item_embedding, p=2) + + reg_1 * torch.norm(seq_output, p=2) + ) return loss_1 @@ -104,7 +111,9 @@ def forward(self, seq_item, seq_item_len, user): seq_item_embedding = self.item_embedding(seq_item) - high_order_seq_item_embedding = self.inverse_seq_item_embedding(seq_item_embedding, seq_item_len) + high_order_seq_item_embedding = self.inverse_seq_item_embedding( + seq_item_embedding, seq_item_len + ) # batch_size * order_len * embedding high_order = self.get_high_order_Markov(high_order_seq_item_embedding, user) @@ -153,7 +162,7 @@ def calculate_loss(self, interaction): user_lambda = self.user_lambda(user) pos_items_embedding = self.item_embedding(pos_items) - if self.loss_type == 'BPR': + if self.loss_type == "BPR": neg_items = interaction[self.NEG_ITEM_ID] neg_items_emb = self.item_embedding(neg_items) pos_score = torch.sum(seq_output * pos_items_emb, dim=-1) diff --git a/recbole/model/sequential_recommender/fpmc.py b/recbole/model/sequential_recommender/fpmc.py index 6654b59d1..55fa36070 100644 --- a/recbole/model/sequential_recommender/fpmc.py +++ b/recbole/model/sequential_recommender/fpmc.py @@ -42,7 +42,7 @@ def __init__(self, config, dataset): super(FPMC, self).__init__(config, dataset) # load parameters info - self.embedding_size = config['embedding_size'] + self.embedding_size = config["embedding_size"] # load dataset info self.n_users = dataset.user_num @@ -67,7 +67,9 @@ def _init_weights(self, module): def forward(self, user, item_seq, item_seq_len, next_item): item_last_click_index = item_seq_len - 1 - item_last_click = torch.gather(item_seq, dim=1, index=item_last_click_index.unsqueeze(1)) + item_last_click = torch.gather( + item_seq, dim=1, index=item_last_click_index.unsqueeze(1) + ) item_seq_emb = self.LI_emb(item_last_click) # [b,1,emb] user_emb = self.UI_emb(user) @@ -122,7 +124,9 @@ def full_sort_predict(self, interaction): all_il_emb = self.IL_emb.weight item_last_click_index = item_seq_len - 1 - item_last_click = torch.gather(item_seq, dim=1, index=item_last_click_index.unsqueeze(1)) + item_last_click = torch.gather( + item_seq, dim=1, index=item_last_click_index.unsqueeze(1) + ) item_seq_emb = self.LI_emb(item_last_click) # [b,1,emb] fmc = torch.matmul(item_seq_emb, all_il_emb.transpose(0, 1)) fmc = torch.squeeze(fmc, dim=1) diff --git a/recbole/model/sequential_recommender/gcsan.py b/recbole/model/sequential_recommender/gcsan.py index 18a9fdcda..80067420a 100644 --- a/recbole/model/sequential_recommender/gcsan.py +++ b/recbole/model/sequential_recommender/gcsan.py @@ -41,8 +41,12 @@ def __init__(self, embedding_size, step=1): self.b_ih = Parameter(torch.Tensor(self.gate_size)) self.b_hh = Parameter(torch.Tensor(self.gate_size)) - self.linear_edge_in = nn.Linear(self.embedding_size, self.embedding_size, bias=True) - self.linear_edge_out = nn.Linear(self.embedding_size, self.embedding_size, bias=True) + self.linear_edge_in = nn.Linear( + self.embedding_size, self.embedding_size, bias=True + ) + self.linear_edge_out = nn.Linear( + self.embedding_size, self.embedding_size, bias=True + ) # parameters initialization self._reset_parameters() @@ -66,8 +70,10 @@ def GNNCell(self, A, hidden): """ - input_in = torch.matmul(A[:, :, :A.size(1)], self.linear_edge_in(hidden)) - input_out = torch.matmul(A[:, :, A.size(1):2 * A.size(1)], self.linear_edge_out(hidden)) + input_in = torch.matmul(A[:, :, : A.size(1)], self.linear_edge_in(hidden)) + input_out = torch.matmul( + A[:, :, A.size(1) : 2 * A.size(1)], self.linear_edge_out(hidden) + ) # [batch_size, max_session_len, embedding_size * 2] inputs = torch.cat([input_in, input_out], 2) @@ -92,7 +98,7 @@ def forward(self, A, hidden): class GCSAN(SequentialRecommender): r"""GCSAN captures rich local dependencies via graph neural network, and learns long-range dependencies by applying the self-attention mechanism. - + Note: In the original paper, the attention mechanism in the self-attention layer is a single head, @@ -104,24 +110,28 @@ def __init__(self, config, dataset): super(GCSAN, self).__init__(config, dataset) # load parameters info - self.n_layers = config['n_layers'] - self.n_heads = config['n_heads'] - self.hidden_size = config['hidden_size'] # same as embedding_size - self.inner_size = config['inner_size'] # the dimensionality in feed-forward layer - self.hidden_dropout_prob = config['hidden_dropout_prob'] - self.attn_dropout_prob = config['attn_dropout_prob'] - self.hidden_act = config['hidden_act'] - self.layer_norm_eps = config['layer_norm_eps'] - - self.step = config['step'] - self.device = config['device'] - self.weight = config['weight'] - self.reg_weight = config['reg_weight'] - self.loss_type = config['loss_type'] - self.initializer_range = config['initializer_range'] + self.n_layers = config["n_layers"] + self.n_heads = config["n_heads"] + self.hidden_size = config["hidden_size"] # same as embedding_size + self.inner_size = config[ + "inner_size" + ] # the dimensionality in feed-forward layer + self.hidden_dropout_prob = config["hidden_dropout_prob"] + self.attn_dropout_prob = config["attn_dropout_prob"] + self.hidden_act = config["hidden_act"] + self.layer_norm_eps = config["layer_norm_eps"] + + self.step = config["step"] + self.device = config["device"] + self.weight = config["weight"] + self.reg_weight = config["reg_weight"] + self.loss_type = config["loss_type"] + self.initializer_range = config["initializer_range"] # define layers and loss - self.item_embedding = nn.Embedding(self.n_items, self.hidden_size, padding_idx=0) + self.item_embedding = nn.Embedding( + self.n_items, self.hidden_size, padding_idx=0 + ) self.gnn = GNN(self.hidden_size, self.step) self.self_attention = TransformerEncoder( n_layers=self.n_layers, @@ -131,12 +141,12 @@ def __init__(self, config, dataset): hidden_dropout_prob=self.hidden_dropout_prob, attn_dropout_prob=self.attn_dropout_prob, hidden_act=self.hidden_act, - layer_norm_eps=self.layer_norm_eps + layer_norm_eps=self.layer_norm_eps, ) self.reg_loss = EmbLoss() - if self.loss_type == 'BPR': + if self.loss_type == "BPR": self.loss_fct = BPRLoss() - elif self.loss_type == 'CE': + elif self.loss_type == "CE": self.loss_fct = nn.CrossEntropyLoss() else: raise NotImplementedError("Make sure 'loss_type' in ['BPR', 'CE']!") @@ -145,7 +155,7 @@ def __init__(self, config, dataset): self.apply(self._init_weights) def _init_weights(self, module): - """ Initialize the weights """ + """Initialize the weights""" if isinstance(module, (nn.Linear, nn.Embedding)): # Slightly different from the TF version which uses truncated_normal for initialization # cf https://github.com/pytorch/pytorch/pull/5617 @@ -195,7 +205,9 @@ def forward(self, item_seq, item_seq_len): alias_inputs, A, items = self._get_slice(item_seq) hidden = self.item_embedding(items) hidden = self.gnn(A, hidden) - alias_inputs = alias_inputs.view(-1, alias_inputs.size(1), 1).expand(-1, -1, self.hidden_size) + alias_inputs = alias_inputs.view(-1, alias_inputs.size(1), 1).expand( + -1, -1, self.hidden_size + ) seq_hidden = torch.gather(hidden, dim=1, index=alias_inputs) # fetch the last hidden state of last timestamp ht = self.gather_indexes(seq_hidden, item_seq_len - 1) @@ -213,7 +225,7 @@ def calculate_loss(self, interaction): item_seq_len = interaction[self.ITEM_SEQ_LEN] seq_output = self.forward(item_seq, item_seq_len) pos_items = interaction[self.POS_ITEM_ID] - if self.loss_type == 'BPR': + if self.loss_type == "BPR": neg_items = interaction[self.NEG_ITEM_ID] pos_items_emb = self.item_embedding(pos_items) neg_items_emb = self.item_embedding(neg_items) @@ -243,5 +255,7 @@ def full_sort_predict(self, interaction): item_seq_len = interaction[self.ITEM_SEQ_LEN] seq_output = self.forward(item_seq, item_seq_len) test_items_emb = self.item_embedding.weight - scores = torch.matmul(seq_output, test_items_emb.transpose(0, 1)) # [B, n_items] + scores = torch.matmul( + seq_output, test_items_emb.transpose(0, 1) + ) # [B, n_items] return scores diff --git a/recbole/model/sequential_recommender/gru4rec.py b/recbole/model/sequential_recommender/gru4rec.py index 0ea93e331..e8b599627 100644 --- a/recbole/model/sequential_recommender/gru4rec.py +++ b/recbole/model/sequential_recommender/gru4rec.py @@ -39,14 +39,16 @@ def __init__(self, config, dataset): super(GRU4Rec, self).__init__(config, dataset) # load parameters info - self.embedding_size = config['embedding_size'] - self.hidden_size = config['hidden_size'] - self.loss_type = config['loss_type'] - self.num_layers = config['num_layers'] - self.dropout_prob = config['dropout_prob'] + self.embedding_size = config["embedding_size"] + self.hidden_size = config["hidden_size"] + self.loss_type = config["loss_type"] + self.num_layers = config["num_layers"] + self.dropout_prob = config["dropout_prob"] # define layers and loss - self.item_embedding = nn.Embedding(self.n_items, self.embedding_size, padding_idx=0) + self.item_embedding = nn.Embedding( + self.n_items, self.embedding_size, padding_idx=0 + ) self.emb_dropout = nn.Dropout(self.dropout_prob) self.gru_layers = nn.GRU( input_size=self.embedding_size, @@ -56,9 +58,9 @@ def __init__(self, config, dataset): batch_first=True, ) self.dense = nn.Linear(self.hidden_size, self.embedding_size) - if self.loss_type == 'BPR': + if self.loss_type == "BPR": self.loss_fct = BPRLoss() - elif self.loss_type == 'CE': + elif self.loss_type == "CE": self.loss_fct = nn.CrossEntropyLoss() else: raise NotImplementedError("Make sure 'loss_type' in ['BPR', 'CE']!") @@ -87,7 +89,7 @@ def calculate_loss(self, interaction): item_seq_len = interaction[self.ITEM_SEQ_LEN] seq_output = self.forward(item_seq, item_seq_len) pos_items = interaction[self.POS_ITEM_ID] - if self.loss_type == 'BPR': + if self.loss_type == "BPR": neg_items = interaction[self.NEG_ITEM_ID] pos_items_emb = self.item_embedding(pos_items) neg_items_emb = self.item_embedding(neg_items) @@ -115,5 +117,7 @@ def full_sort_predict(self, interaction): item_seq_len = interaction[self.ITEM_SEQ_LEN] seq_output = self.forward(item_seq, item_seq_len) test_items_emb = self.item_embedding.weight - scores = torch.matmul(seq_output, test_items_emb.transpose(0, 1)) # [B, n_items] + scores = torch.matmul( + seq_output, test_items_emb.transpose(0, 1) + ) # [B, n_items] return scores diff --git a/recbole/model/sequential_recommender/gru4recf.py b/recbole/model/sequential_recommender/gru4recf.py index 549b24b46..fe41ae8a2 100644 --- a/recbole/model/sequential_recommender/gru4recf.py +++ b/recbole/model/sequential_recommender/gru4recf.py @@ -42,22 +42,28 @@ def __init__(self, config, dataset): super(GRU4RecF, self).__init__(config, dataset) # load parameters info - self.embedding_size = config['embedding_size'] - self.hidden_size = config['hidden_size'] - self.num_layers = config['num_layers'] - self.dropout_prob = config['dropout_prob'] + self.embedding_size = config["embedding_size"] + self.hidden_size = config["hidden_size"] + self.num_layers = config["num_layers"] + self.dropout_prob = config["dropout_prob"] - self.selected_features = config['selected_features'] - self.pooling_mode = config['pooling_mode'] - self.device = config['device'] - self.num_feature_field = len(config['selected_features']) + self.selected_features = config["selected_features"] + self.pooling_mode = config["pooling_mode"] + self.device = config["device"] + self.num_feature_field = len(config["selected_features"]) - self.loss_type = config['loss_type'] + self.loss_type = config["loss_type"] # define layers and loss - self.item_embedding = nn.Embedding(self.n_items, self.embedding_size, padding_idx=0) + self.item_embedding = nn.Embedding( + self.n_items, self.embedding_size, padding_idx=0 + ) self.feature_embed_layer = FeatureSeqEmbLayer( - dataset, self.embedding_size, self.selected_features, self.pooling_mode, self.device + dataset, + self.embedding_size, + self.selected_features, + self.pooling_mode, + self.device, ) self.item_gru_layers = nn.GRU( input_size=self.embedding_size, @@ -76,16 +82,16 @@ def __init__(self, config, dataset): ) self.dense_layer = nn.Linear(self.hidden_size * 2, self.embedding_size) self.dropout = nn.Dropout(self.dropout_prob) - if self.loss_type == 'BPR': + if self.loss_type == "BPR": self.loss_fct = BPRLoss() - elif self.loss_type == 'CE': + elif self.loss_type == "CE": self.loss_fct = nn.CrossEntropyLoss() else: raise NotImplementedError("Make sure 'loss_type' in ['BPR', 'CE']!") # parameters initialization self.apply(xavier_normal_initialization) - self.other_parameter_name = ['feature_embed_layer'] + self.other_parameter_name = ["feature_embed_layer"] def forward(self, item_seq, item_seq_len): item_seq_emb = self.item_embedding(item_seq) @@ -93,8 +99,8 @@ def forward(self, item_seq, item_seq_len): item_gru_output, _ = self.item_gru_layers(item_seq_emb_dropout) # [B Len H] sparse_embedding, dense_embedding = self.feature_embed_layer(None, item_seq) - sparse_embedding = sparse_embedding['item'] - dense_embedding = dense_embedding['item'] + sparse_embedding = sparse_embedding["item"] + dense_embedding = dense_embedding["item"] # concat the sparse embedding and float embedding feature_table = [] if sparse_embedding is not None: @@ -107,10 +113,14 @@ def forward(self, item_seq, item_seq_len): table_shape = feature_table.shape feat_num, embedding_size = table_shape[-2], table_shape[-1] - feature_emb = feature_table.view(table_shape[:-2] + (feat_num * embedding_size,)) + feature_emb = feature_table.view( + table_shape[:-2] + (feat_num * embedding_size,) + ) feature_gru_output, _ = self.feature_gru_layers(feature_emb) # [B Len H] - output_concat = torch.cat((item_gru_output, feature_gru_output), -1) # [B Len 2*H] + output_concat = torch.cat( + (item_gru_output, feature_gru_output), -1 + ) # [B Len 2*H] output = self.dense_layer(output_concat) output = self.gather_indexes(output, item_seq_len - 1) # [B H] return output # [B H] @@ -120,7 +130,7 @@ def calculate_loss(self, interaction): item_seq_len = interaction[self.ITEM_SEQ_LEN] seq_output = self.forward(item_seq, item_seq_len) pos_items = interaction[self.POS_ITEM_ID] - if self.loss_type == 'BPR': + if self.loss_type == "BPR": neg_items = interaction[self.NEG_ITEM_ID] pos_items_emb = self.item_embedding(pos_items) # [B H] neg_items_emb = self.item_embedding(neg_items) # [B H] diff --git a/recbole/model/sequential_recommender/gru4reckg.py b/recbole/model/sequential_recommender/gru4reckg.py index e8e68d5d8..7aceb58bb 100644 --- a/recbole/model/sequential_recommender/gru4reckg.py +++ b/recbole/model/sequential_recommender/gru4reckg.py @@ -30,19 +30,23 @@ def __init__(self, config, dataset): super(GRU4RecKG, self).__init__(config, dataset) # load dataset info - self.entity_embedding_matrix = dataset.get_preload_weight('ent_id') + self.entity_embedding_matrix = dataset.get_preload_weight("ent_id") # load parameters info - self.embedding_size = config['embedding_size'] - self.hidden_size = config['hidden_size'] - self.num_layers = config['num_layers'] - self.dropout = config['dropout_prob'] - self.freeze_kg = config['freeze_kg'] - self.loss_type = config['loss_type'] + self.embedding_size = config["embedding_size"] + self.hidden_size = config["hidden_size"] + self.num_layers = config["num_layers"] + self.dropout = config["dropout_prob"] + self.freeze_kg = config["freeze_kg"] + self.loss_type = config["loss_type"] # define layers and loss - self.item_embedding = nn.Embedding(self.n_items, self.embedding_size, padding_idx=0) - self.entity_embedding = nn.Embedding(self.n_items, self.embedding_size, padding_idx=0) + self.item_embedding = nn.Embedding( + self.n_items, self.embedding_size, padding_idx=0 + ) + self.entity_embedding = nn.Embedding( + self.n_items, self.embedding_size, padding_idx=0 + ) self.entity_embedding.weight.requires_grad = not self.freeze_kg self.item_gru_layers = nn.GRU( input_size=self.embedding_size, @@ -59,16 +63,18 @@ def __init__(self, config, dataset): batch_first=True, ) self.dense_layer = nn.Linear(self.hidden_size * 2, self.embedding_size) - if self.loss_type == 'BPR': + if self.loss_type == "BPR": self.loss_fct = BPRLoss() - elif self.loss_type == 'CE': + elif self.loss_type == "CE": self.loss_fct = nn.CrossEntropyLoss() else: raise NotImplementedError("Make sure 'loss_type' in ['BPR', 'CE']!") # parameters initialization self.apply(xavier_normal_initialization) - self.entity_embedding.weight.data.copy_(torch.from_numpy(self.entity_embedding_matrix[:self.n_items])) + self.entity_embedding.weight.data.copy_( + torch.from_numpy(self.entity_embedding_matrix[: self.n_items]) + ) def forward(self, item_seq, item_seq_len): item_emb = self.item_embedding(item_seq) @@ -79,7 +85,9 @@ def forward(self, item_seq, item_seq_len): item_gru_output, _ = self.item_gru_layers(item_emb) # [B Len H] entity_gru_output, _ = self.entity_gru_layers(entity_emb) - output_concat = torch.cat((item_gru_output, entity_gru_output), -1) # [B Len 2*H] + output_concat = torch.cat( + (item_gru_output, entity_gru_output), -1 + ) # [B Len 2*H] output = self.dense_layer(output_concat) output = self.gather_indexes(output, item_seq_len - 1) # [B H] return output @@ -89,7 +97,7 @@ def calculate_loss(self, interaction): item_seq_len = interaction[self.ITEM_SEQ_LEN] seq_output = self.forward(item_seq, item_seq_len) pos_items = interaction[self.POS_ITEM_ID] - if self.loss_type == 'BPR': + if self.loss_type == "BPR": neg_items = interaction[self.NEG_ITEM_ID] pos_items_emb = self.item_embedding(pos_items) # [B H] neg_items_emb = self.item_embedding(neg_items) # [B H] diff --git a/recbole/model/sequential_recommender/hgn.py b/recbole/model/sequential_recommender/hgn.py index dc5f6ec49..7cfb0f32c 100644 --- a/recbole/model/sequential_recommender/hgn.py +++ b/recbole/model/sequential_recommender/hgn.py @@ -44,27 +44,33 @@ def __init__(self, config, dataset): raise NotImplementedError("Make sure 'loss_type' in ['max', 'average']!") # define the layers and loss function - self.item_embedding = nn.Embedding(self.n_items, self.embedding_size, padding_idx=0) + self.item_embedding = nn.Embedding( + self.n_items, self.embedding_size, padding_idx=0 + ) self.user_embedding = nn.Embedding(self.n_user, self.embedding_size) # define the module feature gating need self.w1 = nn.Linear(self.embedding_size, self.embedding_size) self.w2 = nn.Linear(self.embedding_size, self.embedding_size) - self.b = nn.Parameter(torch.zeros(self.embedding_size), requires_grad=True).to(self.device) + self.b = nn.Parameter(torch.zeros(self.embedding_size), requires_grad=True).to( + self.device + ) # define the module instance gating need self.w3 = nn.Linear(self.embedding_size, 1, bias=False) self.w4 = nn.Linear(self.embedding_size, self.max_seq_length, bias=False) # define item_embedding for prediction - self.item_embedding_for_prediction = nn.Embedding(self.n_items, self.embedding_size) + self.item_embedding_for_prediction = nn.Embedding( + self.n_items, self.embedding_size + ) self.sigmoid = nn.Sigmoid() - self.loss_type = config['loss_type'] - if self.loss_type == 'BPR': + self.loss_type = config["loss_type"] + if self.loss_type == "BPR": self.loss_fct = BPRLoss() - elif self.loss_type == 'CE': + elif self.loss_type == "CE": self.loss_fct = nn.CrossEntropyLoss() else: raise NotImplementedError("Make sure 'loss_type' in ['BPR', 'CE']!") @@ -90,7 +96,7 @@ def reg_loss(self, user_embedding, item_embedding, seq_item_embedding): def _init_weights(self, module): if isinstance(module, nn.Embedding): - normal_(module.weight.data, 0., 1 / self.embedding_size) + normal_(module.weight.data, 0.0, 1 / self.embedding_size) elif isinstance(module, nn.Linear): xavier_uniform_(module.weight.data) if module.bias is not None: @@ -140,7 +146,9 @@ def instance_gating(self, user_item, user_embedding): # batch_size * seq_len * embedding_size if self.pool_type == "average": - output = torch.div(output.sum(dim=1), instance_score.sum(dim=1).unsqueeze(1)) + output = torch.div( + output.sum(dim=1), instance_score.sum(dim=1).unsqueeze(1) + ) # batch_size * embedding_size else: # for max_pooling @@ -172,18 +180,22 @@ def calculate_loss(self, interaction): seq_output = self.forward(seq_item, user) pos_items = interaction[self.POS_ITEM_ID] pos_items_emb = self.item_embedding_for_prediction(pos_items) - if self.loss_type == 'BPR': + if self.loss_type == "BPR": neg_items = interaction[self.NEG_ITEM_ID] neg_items_emb = self.item_embedding(neg_items) pos_score = torch.sum(seq_output * pos_items_emb, dim=-1) neg_score = torch.sum(seq_output * neg_items_emb, dim=-1) loss = self.loss_fct(pos_score, neg_score) - return loss + self.reg_loss(user_embedding, pos_items_emb, seq_item_embedding) + return loss + self.reg_loss( + user_embedding, pos_items_emb, seq_item_embedding + ) else: # self.loss_type = 'CE' test_item_emb = self.item_embedding_for_prediction.weight logits = torch.matmul(seq_output, test_item_emb.transpose(0, 1)) loss = self.loss_fct(logits, pos_items) - return loss + self.reg_loss(user_embedding, pos_items_emb, seq_item_embedding) + return loss + self.reg_loss( + user_embedding, pos_items_emb, seq_item_embedding + ) def predict(self, interaction): diff --git a/recbole/model/sequential_recommender/hrm.py b/recbole/model/sequential_recommender/hrm.py index 3dd1b0ae1..16202a64c 100644 --- a/recbole/model/sequential_recommender/hrm.py +++ b/recbole/model/sequential_recommender/hrm.py @@ -26,10 +26,10 @@ class HRM(SequentialRecommender): r""" - HRM can well capture both sequential behavior and users’ general taste by involving transaction and - user representations in prediction. + HRM can well capture both sequential behavior and users’ general taste by involving transaction and + user representations in prediction. - HRM user max- & average- pooling as a good helper. + HRM user max- & average- pooling as a good helper. """ def __init__(self, config, dataset): @@ -44,19 +44,23 @@ def __init__(self, config, dataset): self.pooling_type_layer_1 = config["pooling_type_layer_1"] self.pooling_type_layer_2 = config["pooling_type_layer_2"] self.high_order = config["high_order"] - assert self.high_order <= self.max_seq_length, "high_order can't longer than the max_seq_length" + assert ( + self.high_order <= self.max_seq_length + ), "high_order can't longer than the max_seq_length" self.reg_weight = config["reg_weight"] self.dropout_prob = config["dropout_prob"] # define the layers and loss type - self.item_embedding = nn.Embedding(self.n_items, self.embedding_size, padding_idx=0) + self.item_embedding = nn.Embedding( + self.n_items, self.embedding_size, padding_idx=0 + ) self.user_embedding = nn.Embedding(self.n_user, self.embedding_size) self.dropout = nn.Dropout(self.dropout_prob) - self.loss_type = config['loss_type'] - if self.loss_type == 'BPR': + self.loss_type = config["loss_type"] + if self.loss_type == "BPR": self.loss_fct = BPRLoss() - elif self.loss_type == 'CE': + elif self.loss_type == "CE": self.loss_fct = nn.CrossEntropyLoss() else: raise NotImplementedError("Make sure 'loss_type' in ['BPR', 'CE']!") @@ -94,7 +98,7 @@ def forward(self, seq_item, user, seq_item_len): seq_item_embedding = self.item_embedding(seq_item) # batch_size * seq_len * embedding_size - high_order_item_embedding = seq_item_embedding[:, -self.high_order:, :] + high_order_item_embedding = seq_item_embedding[:, -self.high_order :, :] # batch_size * high_order * embedding_size user_embedding = self.dropout(self.user_embedding(user)) @@ -102,18 +106,27 @@ def forward(self, seq_item, user, seq_item_len): # layer 1 if self.pooling_type_layer_1 == "max": - high_order_item_embedding = torch.max(high_order_item_embedding, dim=1).values + high_order_item_embedding = torch.max( + high_order_item_embedding, dim=1 + ).values # batch_size * embedding_size else: for idx, len in enumerate(seq_item_len): if len > self.high_order: seq_item_len[idx] = self.high_order high_order_item_embedding = torch.sum(seq_item_embedding, dim=1) - high_order_item_embedding = torch.div(high_order_item_embedding, seq_item_len.unsqueeze(1).float()) + high_order_item_embedding = torch.div( + high_order_item_embedding, seq_item_len.unsqueeze(1).float() + ) # batch_size * embedding_size hybrid_user_embedding = self.dropout( - torch.cat([user_embedding.unsqueeze(dim=1), - high_order_item_embedding.unsqueeze(dim=1)], dim=1) + torch.cat( + [ + user_embedding.unsqueeze(dim=1), + high_order_item_embedding.unsqueeze(dim=1), + ], + dim=1, + ) ) # batch_size * 2_mul_embedding_size @@ -135,7 +148,7 @@ def calculate_loss(self, interaction): seq_output = self.forward(seq_item, user, seq_item_len) pos_items = interaction[self.POS_ITEM_ID] pos_items_emb = self.item_embedding(pos_items) - if self.loss_type == 'BPR': + if self.loss_type == "BPR": neg_items = interaction[self.NEG_ITEM_ID] neg_items_emb = self.item_embedding(neg_items) pos_score = torch.sum(seq_output * pos_items_emb, dim=-1) diff --git a/recbole/model/sequential_recommender/ksr.py b/recbole/model/sequential_recommender/ksr.py index bf48c6719..85b7b7791 100644 --- a/recbole/model/sequential_recommender/ksr.py +++ b/recbole/model/sequential_recommender/ksr.py @@ -32,27 +32,31 @@ def __init__(self, config, dataset): super(KSR, self).__init__(config, dataset) # load dataset info - self.ENTITY_ID = config['ENTITY_ID_FIELD'] - self.RELATION_ID = config['RELATION_ID_FIELD'] + self.ENTITY_ID = config["ENTITY_ID_FIELD"] + self.RELATION_ID = config["RELATION_ID_FIELD"] self.n_entities = dataset.num(self.ENTITY_ID) self.n_relations = dataset.num(self.RELATION_ID) - 1 - self.entity_embedding_matrix = dataset.get_preload_weight('ent_id') - self.relation_embedding_matrix = dataset.get_preload_weight('rel_id') + self.entity_embedding_matrix = dataset.get_preload_weight("ent_id") + self.relation_embedding_matrix = dataset.get_preload_weight("rel_id") # load parameters info - self.embedding_size = config['embedding_size'] - self.hidden_size = config['hidden_size'] - self.loss_type = config['loss_type'] - self.num_layers = config['num_layers'] - self.dropout_prob = config['dropout_prob'] - self.gamma = config['gamma'] # Scaling factor - self.device = config['device'] - self.loss_type = config['loss_type'] - self.freeze_kg = config['freeze_kg'] + self.embedding_size = config["embedding_size"] + self.hidden_size = config["hidden_size"] + self.loss_type = config["loss_type"] + self.num_layers = config["num_layers"] + self.dropout_prob = config["dropout_prob"] + self.gamma = config["gamma"] # Scaling factor + self.device = config["device"] + self.loss_type = config["loss_type"] + self.freeze_kg = config["freeze_kg"] # define layers and loss - self.item_embedding = nn.Embedding(self.n_items, self.embedding_size, padding_idx=0) - self.entity_embedding = nn.Embedding(self.n_items, self.embedding_size, padding_idx=0) + self.item_embedding = nn.Embedding( + self.n_items, self.embedding_size, padding_idx=0 + ) + self.entity_embedding = nn.Embedding( + self.n_items, self.embedding_size, padding_idx=0 + ) self.entity_embedding.weight.requires_grad = not self.freeze_kg self.emb_dropout = nn.Dropout(self.dropout_prob) @@ -66,21 +70,26 @@ def __init__(self, config, dataset): self.dense = nn.Linear(self.hidden_size, self.embedding_size) self.dense_layer_u = nn.Linear(self.embedding_size * 2, self.embedding_size) self.dense_layer_i = nn.Linear(self.embedding_size * 2, self.embedding_size) - if self.loss_type == 'BPR': + if self.loss_type == "BPR": self.loss_fct = BPRLoss() - elif self.loss_type == 'CE': + elif self.loss_type == "CE": self.loss_fct = nn.CrossEntropyLoss() else: raise NotImplementedError("Make sure 'loss_type' in ['BPR', 'CE']!") # parameters initialization self.apply(self._init_weights) - self.entity_embedding.weight.data.copy_(torch.from_numpy(self.entity_embedding_matrix[:self.n_items])) - self.relation_Matrix = torch.from_numpy(self.relation_embedding_matrix[:self.n_relations] - ).to(self.device) # [R H] + self.entity_embedding.weight.data.copy_( + torch.from_numpy(self.entity_embedding_matrix[: self.n_items]) + ) + self.relation_Matrix = torch.from_numpy( + self.relation_embedding_matrix[: self.n_relations] + ).to( + self.device + ) # [R H] def _init_weights(self, module): - """ Initialize the weights """ + """Initialize the weights""" if isinstance(module, nn.Embedding): xavier_normal_(module.weight) elif isinstance(module, nn.GRU): @@ -93,35 +102,47 @@ def _get_kg_embedding(self, head): """ head_e = self.entity_embedding(head) # [B H] relation_Matrix = self.relation_Matrix.repeat(head_e.size()[0], 1, 1) # [B R H] - head_Matrix = torch.unsqueeze(head_e, 1).repeat(1, self.n_relations, 1) # [B R H] + head_Matrix = torch.unsqueeze(head_e, 1).repeat( + 1, self.n_relations, 1 + ) # [B R H] tail_Matrix = head_Matrix + relation_Matrix return head_e, tail_Matrix def _memory_update_cell(self, user_memory, update_memory): - z = torch.sigmoid(torch.mul(user_memory, - update_memory).sum(-1).float()).unsqueeze(-1) # [B R 1], the gate vector + z = torch.sigmoid( + torch.mul(user_memory, update_memory).sum(-1).float() + ).unsqueeze( + -1 + ) # [B R 1], the gate vector updated_user_memory = (1.0 - z) * user_memory + z * update_memory return updated_user_memory def memory_update(self, item_seq, item_seq_len): - """ define write operator """ + """define write operator""" step_length = item_seq.size()[1] last_item = item_seq_len - 1 # init user memory with 0s - user_memory = torch.zeros(item_seq.size()[0], self.n_relations, - self.embedding_size).float().to(self.device) # [B R H] + user_memory = ( + torch.zeros(item_seq.size()[0], self.n_relations, self.embedding_size) + .float() + .to(self.device) + ) # [B R H] last_user_memory = torch.zeros_like(user_memory) for i in range(step_length): # [len] _, update_memory = self._get_kg_embedding(item_seq[:, i]) # [B R H] - user_memory = self._memory_update_cell(user_memory, update_memory) # [B R H] + user_memory = self._memory_update_cell( + user_memory, update_memory + ) # [B R H] last_user_memory[last_item == i] = user_memory[last_item == i].float() return last_user_memory def memory_read(self, user_memory): - """ define read operator """ + """define read operator""" attrs = self.relation_Matrix - attentions = nn.functional.softmax(self.gamma * torch.mul(user_memory, attrs).sum(-1).float(), -1) # [B R] + attentions = nn.functional.softmax( + self.gamma * torch.mul(user_memory, attrs).sum(-1).float(), -1 + ) # [B R] u_m = torch.mul(user_memory, attentions.unsqueeze(-1)).sum(1) # [B H] return u_m @@ -155,7 +176,7 @@ def calculate_loss(self, interaction): item_seq_len = interaction[self.ITEM_SEQ_LEN] seq_output = self.forward(item_seq, item_seq_len) pos_items = interaction[self.POS_ITEM_ID] - if self.loss_type == 'BPR': + if self.loss_type == "BPR": neg_items = interaction[self.NEG_ITEM_ID] pos_items_emb = self._get_item_comb_embedding(pos_items) neg_items_emb = self._get_item_comb_embedding(neg_items) @@ -165,7 +186,9 @@ def calculate_loss(self, interaction): return loss else: # self.loss_type = 'CE' test_items_emb = self.dense_layer_i( - torch.cat((self.item_embedding.weight, self.entity_embedding.weight), -1) + torch.cat( + (self.item_embedding.weight, self.entity_embedding.weight), -1 + ) ) # [n_items H] logits = torch.matmul(seq_output, test_items_emb.transpose(0, 1)) loss = self.loss_fct(logits, pos_items) @@ -187,5 +210,7 @@ def full_sort_predict(self, interaction): test_items_emb = self.dense_layer_i( torch.cat((self.item_embedding.weight, self.entity_embedding.weight), -1) ) # [n_items H] - scores = torch.matmul(seq_output, test_items_emb.transpose(0, 1)) # [B, n_items] + scores = torch.matmul( + seq_output, test_items_emb.transpose(0, 1) + ) # [B, n_items] return scores diff --git a/recbole/model/sequential_recommender/lightsans.py b/recbole/model/sequential_recommender/lightsans.py index 5119166bf..bcc4d080b 100644 --- a/recbole/model/sequential_recommender/lightsans.py +++ b/recbole/model/sequential_recommender/lightsans.py @@ -19,43 +19,52 @@ from recbole.model.loss import BPRLoss from recbole.model.layers import LightTransformerEncoder -class LightSANs(SequentialRecommender): +class LightSANs(SequentialRecommender): def __init__(self, config, dataset): super(LightSANs, self).__init__(config, dataset) # load parameters info - self.n_layers = config['n_layers'] - self.n_heads = config['n_heads'] - self.k_interests = config['k_interests'] - self.hidden_size = config['hidden_size'] # same as embedding_size - self.inner_size = config['inner_size'] # the dimensionality in feed-forward layer - self.hidden_dropout_prob = config['hidden_dropout_prob'] - self.attn_dropout_prob = config['attn_dropout_prob'] - self.hidden_act = config['hidden_act'] - self.layer_norm_eps = config['layer_norm_eps'] - - self.initializer_range = config['initializer_range'] - self.loss_type = config['loss_type'] + self.n_layers = config["n_layers"] + self.n_heads = config["n_heads"] + self.k_interests = config["k_interests"] + self.hidden_size = config["hidden_size"] # same as embedding_size + self.inner_size = config[ + "inner_size" + ] # the dimensionality in feed-forward layer + self.hidden_dropout_prob = config["hidden_dropout_prob"] + self.attn_dropout_prob = config["attn_dropout_prob"] + self.hidden_act = config["hidden_act"] + self.layer_norm_eps = config["layer_norm_eps"] + + self.initializer_range = config["initializer_range"] + self.loss_type = config["loss_type"] self.seq_len = self.max_seq_length # define layers and loss - self.item_embedding = nn.Embedding(self.n_items, self.hidden_size , padding_idx=0) + self.item_embedding = nn.Embedding( + self.n_items, self.hidden_size, padding_idx=0 + ) self.position_embedding = nn.Embedding(self.max_seq_length, self.hidden_size) - self.trm_encoder = LightTransformerEncoder(n_layers=self.n_layers, n_heads=self.n_heads, - k_interests=self.k_interests, hidden_size=self.hidden_size, - seq_len=self.seq_len, - inner_size=self.inner_size, - hidden_dropout_prob=self.hidden_dropout_prob, - attn_dropout_prob=self.attn_dropout_prob, - hidden_act=self.hidden_act, layer_norm_eps=self.layer_norm_eps) + self.trm_encoder = LightTransformerEncoder( + n_layers=self.n_layers, + n_heads=self.n_heads, + k_interests=self.k_interests, + hidden_size=self.hidden_size, + seq_len=self.seq_len, + inner_size=self.inner_size, + hidden_dropout_prob=self.hidden_dropout_prob, + attn_dropout_prob=self.attn_dropout_prob, + hidden_act=self.hidden_act, + layer_norm_eps=self.layer_norm_eps, + ) self.LayerNorm = nn.LayerNorm(self.hidden_size, eps=self.layer_norm_eps) self.dropout = nn.Dropout(self.hidden_dropout_prob) - if self.loss_type == 'BPR': + if self.loss_type == "BPR": self.loss_fct = BPRLoss() - elif self.loss_type == 'CE': + elif self.loss_type == "CE": self.loss_fct = nn.CrossEntropyLoss() else: raise NotImplementedError("Make sure 'loss_type' in ['BPR', 'CE']!") @@ -64,7 +73,7 @@ def __init__(self, config, dataset): self.apply(self._init_weights) def _init_weights(self, module): - """ Initialize the weights """ + """Initialize the weights""" if isinstance(module, (nn.Linear, nn.Embedding)): module.weight.data.normal_(mean=0.0, std=self.initializer_range) elif isinstance(module, nn.LayerNorm): @@ -72,9 +81,11 @@ def _init_weights(self, module): module.weight.data.fill_(1.0) if isinstance(module, nn.Linear) and module.bias is not None: module.bias.data.zero_() - + def embedding_layer(self, item_seq): - position_ids = torch.arange(item_seq.size(1), dtype=torch.long, device=item_seq.device) + position_ids = torch.arange( + item_seq.size(1), dtype=torch.long, device=item_seq.device + ) position_embedding = self.position_embedding(position_ids) item_emb = self.item_embedding(item_seq) return item_emb, position_embedding @@ -84,19 +95,19 @@ def forward(self, item_seq, item_seq_len): item_emb = self.LayerNorm(item_emb) item_emb = self.dropout(item_emb) - trm_output = self.trm_encoder(item_emb, - position_embedding, - output_all_encoded_layers=True) + trm_output = self.trm_encoder( + item_emb, position_embedding, output_all_encoded_layers=True + ) output = trm_output[-1] output = self.gather_indexes(output, item_seq_len - 1) - return output # [B H] + return output # [B H] def calculate_loss(self, interaction): item_seq = interaction[self.ITEM_SEQ] item_seq_len = interaction[self.ITEM_SEQ_LEN] seq_output = self.forward(item_seq, item_seq_len) pos_items = interaction[self.POS_ITEM_ID] - if self.loss_type == 'BPR': + if self.loss_type == "BPR": neg_items = interaction[self.NEG_ITEM_ID] pos_items_emb = self.item_embedding(pos_items) neg_items_emb = self.item_embedding(neg_items) @@ -114,7 +125,7 @@ def predict(self, interaction): item_seq = interaction[self.ITEM_SEQ] item_seq_len = interaction[self.ITEM_SEQ_LEN] test_item = interaction[self.ITEM_ID] - + seq_output = self.forward(item_seq, item_seq_len) test_item_emb = self.item_embedding(test_item) scores = torch.mul(seq_output, test_item_emb).sum(dim=1) # [B] @@ -126,4 +137,4 @@ def full_sort_predict(self, interaction): seq_output = self.forward(item_seq, item_seq_len) test_items_emb = self.item_embedding.weight scores = torch.matmul(seq_output, test_items_emb.transpose(0, 1)) # [B n_items] - return scores \ No newline at end of file + return scores diff --git a/recbole/model/sequential_recommender/narm.py b/recbole/model/sequential_recommender/narm.py index 76f5594b7..06512a9c8 100644 --- a/recbole/model/sequential_recommender/narm.py +++ b/recbole/model/sequential_recommender/narm.py @@ -38,25 +38,33 @@ def __init__(self, config, dataset): super(NARM, self).__init__(config, dataset) # load parameters info - self.embedding_size = config['embedding_size'] - self.hidden_size = config['hidden_size'] - self.n_layers = config['n_layers'] - self.dropout_probs = config['dropout_probs'] - self.device = config['device'] + self.embedding_size = config["embedding_size"] + self.hidden_size = config["hidden_size"] + self.n_layers = config["n_layers"] + self.dropout_probs = config["dropout_probs"] + self.device = config["device"] # define layers and loss - self.item_embedding = nn.Embedding(self.n_items, self.embedding_size, padding_idx=0) + self.item_embedding = nn.Embedding( + self.n_items, self.embedding_size, padding_idx=0 + ) self.emb_dropout = nn.Dropout(self.dropout_probs[0]) - self.gru = nn.GRU(self.embedding_size, self.hidden_size, self.n_layers, bias=False, batch_first=True) + self.gru = nn.GRU( + self.embedding_size, + self.hidden_size, + self.n_layers, + bias=False, + batch_first=True, + ) self.a_1 = nn.Linear(self.hidden_size, self.hidden_size, bias=False) self.a_2 = nn.Linear(self.hidden_size, self.hidden_size, bias=False) self.v_t = nn.Linear(self.hidden_size, 1, bias=False) self.ct_dropout = nn.Dropout(self.dropout_probs[1]) self.b = nn.Linear(2 * self.hidden_size, self.embedding_size, bias=False) - self.loss_type = config['loss_type'] - if self.loss_type == 'BPR': + self.loss_type = config["loss_type"] + if self.loss_type == "BPR": self.loss_fct = BPRLoss() - elif self.loss_type == 'CE': + elif self.loss_type == "CE": self.loss_fct = nn.CrossEntropyLoss() else: raise NotImplementedError("Make sure 'loss_type' in ['BPR', 'CE']!") @@ -98,7 +106,7 @@ def calculate_loss(self, interaction): item_seq_len = interaction[self.ITEM_SEQ_LEN] seq_output = self.forward(item_seq, item_seq_len) pos_items = interaction[self.POS_ITEM_ID] - if self.loss_type == 'BPR': + if self.loss_type == "BPR": neg_items = interaction[self.NEG_ITEM_ID] pos_items_emb = self.item_embedding(pos_items) neg_items_emb = self.item_embedding(neg_items) diff --git a/recbole/model/sequential_recommender/nextitnet.py b/recbole/model/sequential_recommender/nextitnet.py index e0ad3def4..18392ffdc 100644 --- a/recbole/model/sequential_recommender/nextitnet.py +++ b/recbole/model/sequential_recommender/nextitnet.py @@ -42,31 +42,37 @@ def __init__(self, config, dataset): super(NextItNet, self).__init__(config, dataset) # load parameters info - self.embedding_size = config['embedding_size'] - self.residual_channels = config['embedding_size'] - self.block_num = config['block_num'] - self.dilations = config['dilations'] * self.block_num - self.kernel_size = config['kernel_size'] - self.reg_weight = config['reg_weight'] - self.loss_type = config['loss_type'] + self.embedding_size = config["embedding_size"] + self.residual_channels = config["embedding_size"] + self.block_num = config["block_num"] + self.dilations = config["dilations"] * self.block_num + self.kernel_size = config["kernel_size"] + self.reg_weight = config["reg_weight"] + self.loss_type = config["loss_type"] # define layers and loss - self.item_embedding = nn.Embedding(self.n_items, self.embedding_size, padding_idx=0) + self.item_embedding = nn.Embedding( + self.n_items, self.embedding_size, padding_idx=0 + ) # residual blocks dilations in blocks:[1,2,4,8,1,2,4,8,...] rb = [ ResidualBlock_b( - self.residual_channels, self.residual_channels, kernel_size=self.kernel_size, dilation=dilation - ) for dilation in self.dilations + self.residual_channels, + self.residual_channels, + kernel_size=self.kernel_size, + dilation=dilation, + ) + for dilation in self.dilations ] self.residual_blocks = nn.Sequential(*rb) # fully-connected layer self.final_layer = nn.Linear(self.residual_channels, self.embedding_size) - if self.loss_type == 'BPR': + if self.loss_type == "BPR": self.loss_fct = BPRLoss() - elif self.loss_type == 'CE': + elif self.loss_type == "CE": self.loss_fct = nn.CrossEntropyLoss() else: raise NotImplementedError("Make sure 'loss_type' in ['BPR', 'CE']!") @@ -77,7 +83,7 @@ def __init__(self, config, dataset): def _init_weights(self, module): if isinstance(module, nn.Embedding): - stdv = np.sqrt(1. / self.n_items) + stdv = np.sqrt(1.0 / self.n_items) uniform_(module.weight.data, -stdv, stdv) elif isinstance(module, nn.Linear): xavier_normal_(module.weight.data) @@ -85,10 +91,14 @@ def _init_weights(self, module): constant_(module.bias.data, 0.1) def forward(self, item_seq): - item_seq_emb = self.item_embedding(item_seq) # [batch_size, seq_len, embed_size] + item_seq_emb = self.item_embedding( + item_seq + ) # [batch_size, seq_len, embed_size] # Residual locks dilate_outputs = self.residual_blocks(item_seq_emb) - hidden = dilate_outputs[:, -1, :].view(-1, self.residual_channels) # [batch_size, embed_size] + hidden = dilate_outputs[:, -1, :].view( + -1, self.residual_channels + ) # [batch_size, embed_size] seq_output = self.final_layer(hidden) # [batch_size, embedding_size] return seq_output @@ -99,7 +109,7 @@ def reg_loss_rb(self): loss_rb = 0 if self.reg_weight > 0.0: for name, parm in self.residual_blocks.named_parameters(): - if name.endswith('weight'): + if name.endswith("weight"): loss_rb += torch.norm(parm, 2) return self.reg_weight * loss_rb @@ -108,7 +118,7 @@ def calculate_loss(self, interaction): # item_seq_len = interaction[self.ITEM_SEQ_LEN] seq_output = self.forward(item_seq) pos_items = interaction[self.POS_ITEM_ID] - if self.loss_type == 'BPR': + if self.loss_type == "BPR": neg_items = interaction[self.NEG_ITEM_ID] pos_items_emb = self.item_embedding(pos_items) neg_items_emb = self.item_embedding(neg_items) @@ -136,7 +146,9 @@ def full_sort_predict(self, interaction): # item_seq_len = interaction[self.ITEM_SEQ_LEN] seq_output = self.forward(item_seq) test_items_emb = self.item_embedding.weight - scores = torch.matmul(seq_output, test_items_emb.transpose(0, 1)) # [B, item_num] + scores = torch.matmul( + seq_output, test_items_emb.transpose(0, 1) + ) # [B, item_num] return scores @@ -153,7 +165,13 @@ def __init__(self, in_channel, out_channel, kernel_size=3, dilation=None): self.conv1 = nn.Conv2d(in_channel, half_channel, kernel_size=(1, 1), padding=0) self.ln2 = nn.LayerNorm(half_channel, eps=1e-8) - self.conv2 = nn.Conv2d(half_channel, half_channel, kernel_size=(1, kernel_size), padding=0, dilation=dilation) + self.conv2 = nn.Conv2d( + half_channel, + half_channel, + kernel_size=(1, kernel_size), + padding=0, + dilation=dilation, + ) self.ln3 = nn.LayerNorm(half_channel, eps=1e-8) self.conv3 = nn.Conv2d(half_channel, out_channel, kernel_size=(1, 1), padding=0) @@ -177,7 +195,7 @@ def forward(self, x): # x: [batch_size, seq_len, embed_size] return out3 + x def conv_pad(self, x, dilation): # x: [batch_size, seq_len, embed_size] - r""" Dropout-mask: To avoid the future information leakage problem, this paper proposed a masking-based dropout + r"""Dropout-mask: To avoid the future information leakage problem, this paper proposed a masking-based dropout trick for the 1D dilated convolution to prevent the network from seeing the future items. Also the One-dimensional transformation is completed in this function. """ @@ -185,7 +203,9 @@ def conv_pad(self, x, dilation): # x: [batch_size, seq_len, embed_size] inputs_pad = inputs_pad.unsqueeze(2) # [batch_size, embed_size, 1, seq_len] pad = nn.ZeroPad2d(((self.kernel_size - 1) * dilation, 0, 0, 0)) # padding operation args:(left,right,top,bottom) - inputs_pad = pad(inputs_pad) # [batch_size, embed_size, 1, seq_len+(self.kernel_size-1)*dilations] + inputs_pad = pad( + inputs_pad + ) # [batch_size, embed_size, 1, seq_len+(self.kernel_size-1)*dilations] return inputs_pad @@ -197,16 +217,30 @@ class ResidualBlock_b(nn.Module): def __init__(self, in_channel, out_channel, kernel_size=3, dilation=None): super(ResidualBlock_b, self).__init__() - self.conv1 = nn.Conv2d(in_channel, out_channel, kernel_size=(1, kernel_size), padding=0, dilation=dilation) + self.conv1 = nn.Conv2d( + in_channel, + out_channel, + kernel_size=(1, kernel_size), + padding=0, + dilation=dilation, + ) self.ln1 = nn.LayerNorm(out_channel, eps=1e-8) - self.conv2 = nn.Conv2d(out_channel, out_channel, kernel_size=(1, kernel_size), padding=0, dilation=dilation * 2) + self.conv2 = nn.Conv2d( + out_channel, + out_channel, + kernel_size=(1, kernel_size), + padding=0, + dilation=dilation * 2, + ) self.ln2 = nn.LayerNorm(out_channel, eps=1e-8) self.dilation = dilation self.kernel_size = kernel_size def forward(self, x): # x: [batch_size, seq_len, embed_size] - x_pad = self.conv_pad(x, self.dilation) # [batch_size, embed_size, 1, seq_len+(self.kernel_size-1)*dilations] + x_pad = self.conv_pad( + x, self.dilation + ) # [batch_size, embed_size, 1, seq_len+(self.kernel_size-1)*dilations] out = self.conv1(x_pad).squeeze(2).permute(0, 2, 1) # [batch_size, seq_len+(self.kernel_size-1)*dilations-kernel_size+1, embed_size] out = F.relu(self.ln1(out)) @@ -216,7 +250,7 @@ def forward(self, x): # x: [batch_size, seq_len, embed_size] return out2 + x def conv_pad(self, x, dilation): - r""" Dropout-mask: To avoid the future information leakage problem, this paper proposed a masking-based dropout + r"""Dropout-mask: To avoid the future information leakage problem, this paper proposed a masking-based dropout trick for the 1D dilated convolution to prevent the network from seeing the future items. Also the One-dimensional transformation is completed in this function. """ diff --git a/recbole/model/sequential_recommender/npe.py b/recbole/model/sequential_recommender/npe.py index 29674241a..0fc9cde3e 100644 --- a/recbole/model/sequential_recommender/npe.py +++ b/recbole/model/sequential_recommender/npe.py @@ -26,8 +26,8 @@ class NPE(SequentialRecommender): r""" - models a user’s click to an item in two terms: the personal preference of the user for the item, - and the relationships between this item and other items clicked by the user + models a user’s click to an item in two terms: the personal preference of the user for the item, + and the relationships between this item and other items clicked by the user """ @@ -45,14 +45,16 @@ def __init__(self, config, dataset): # define layers and loss type self.user_embedding = nn.Embedding(self.n_user, self.embedding_size) self.item_embedding = nn.Embedding(self.n_items, self.embedding_size) - self.embedding_seq_item = nn.Embedding(self.n_items, self.embedding_size, padding_idx=0) + self.embedding_seq_item = nn.Embedding( + self.n_items, self.embedding_size, padding_idx=0 + ) self.relu = nn.ReLU() self.dropout = nn.Dropout(self.dropout_prob) - self.loss_type = config['loss_type'] - if self.loss_type == 'BPR': + self.loss_type = config["loss_type"] + if self.loss_type == "BPR": self.loss_fct = BPRLoss() - elif self.loss_type == 'CE': + elif self.loss_type == "CE": self.loss_fct = nn.CrossEntropyLoss() else: raise NotImplementedError("Make sure 'loss_type' in ['BPR', 'CE']!") @@ -81,7 +83,7 @@ def calculate_loss(self, interaction): seq_output = self.forward(seq_item, user) pos_items = interaction[self.POS_ITEM_ID] pos_items_embs = self.item_embedding(pos_items) - if self.loss_type == 'BPR': + if self.loss_type == "BPR": neg_items = interaction[self.NEG_ITEM_ID] neg_items_emb = self.relu(self.item_embedding(neg_items)) pos_items_emb = self.relu(pos_items_embs) diff --git a/recbole/model/sequential_recommender/repeatnet.py b/recbole/model/sequential_recommender/repeatnet.py index 33dc5b14e..c03d8f22b 100644 --- a/recbole/model/sequential_recommender/repeatnet.py +++ b/recbole/model/sequential_recommender/repeatnet.py @@ -50,24 +50,29 @@ def __init__(self, config, dataset): self.dropout_prob = config["dropout_prob"] # define the layers and loss function - self.item_matrix = nn.Embedding(self.n_items, self.embedding_size, padding_idx=0) + self.item_matrix = nn.Embedding( + self.n_items, self.embedding_size, padding_idx=0 + ) self.gru = nn.GRU(self.embedding_size, self.hidden_size, batch_first=True) self.repeat_explore_mechanism = Repeat_Explore_Mechanism( - self.device, hidden_size=self.hidden_size, seq_len=self.max_seq_length, dropout_prob=self.dropout_prob + self.device, + hidden_size=self.hidden_size, + seq_len=self.max_seq_length, + dropout_prob=self.dropout_prob, ) self.repeat_recommendation_decoder = Repeat_Recommendation_Decoder( self.device, hidden_size=self.hidden_size, seq_len=self.max_seq_length, num_item=self.n_items, - dropout_prob=self.dropout_prob + dropout_prob=self.dropout_prob, ) self.explore_recommendation_decoder = Explore_Recommendation_Decoder( hidden_size=self.hidden_size, seq_len=self.max_seq_length, num_item=self.n_items, device=self.device, - dropout_prob=self.dropout_prob + dropout_prob=self.dropout_prob, ) self.loss_fct = F.nll_loss @@ -93,20 +98,33 @@ def forward(self, item_seq, item_seq_len): last_memory = self.gather_indexes(all_memory, item_seq_len - 1) # all_memory: batch_size * item_seq * hidden_size # last_memory: batch_size * hidden_size - timeline_mask = (item_seq == 0) + timeline_mask = item_seq == 0 - self.repeat_explore = self.repeat_explore_mechanism.forward(all_memory=all_memory, last_memory=last_memory) + self.repeat_explore = self.repeat_explore_mechanism.forward( + all_memory=all_memory, last_memory=last_memory + ) # batch_size * 2 repeat_recommendation_decoder = self.repeat_recommendation_decoder.forward( - all_memory=all_memory, last_memory=last_memory, item_seq=item_seq, mask=timeline_mask + all_memory=all_memory, + last_memory=last_memory, + item_seq=item_seq, + mask=timeline_mask, ) # batch_size * num_item explore_recommendation_decoder = self.explore_recommendation_decoder.forward( - all_memory=all_memory, last_memory=last_memory, item_seq=item_seq, mask=timeline_mask + all_memory=all_memory, + last_memory=last_memory, + item_seq=item_seq, + mask=timeline_mask, ) # batch_size * num_item - prediction = repeat_recommendation_decoder * self.repeat_explore[:, 0].unsqueeze(1) \ - + explore_recommendation_decoder * self.repeat_explore[:, 1].unsqueeze(1) + prediction = repeat_recommendation_decoder * self.repeat_explore[ + :, 0 + ].unsqueeze(1) + explore_recommendation_decoder * self.repeat_explore[ + :, 1 + ].unsqueeze( + 1 + ) # batch_size * num_item return prediction @@ -126,15 +144,21 @@ def calculate_loss(self, interaction): def repeat_explore_loss(self, item_seq, pos_item): batch_size = item_seq.size(0) - repeat, explore = torch.zeros(batch_size).to(self.device), torch.ones(batch_size).to(self.device) + repeat, explore = torch.zeros(batch_size).to(self.device), torch.ones( + batch_size + ).to(self.device) index = 0 for seq_item_ex, pos_item_ex in zip(item_seq, pos_item): if pos_item_ex in seq_item_ex: repeat[index] = 1 explore[index] = 0 index += 1 - repeat_loss = torch.mul(repeat.unsqueeze(1), torch.log(self.repeat_explore[:, 0] + 1e-8)).mean() - explore_loss = torch.mul(explore.unsqueeze(1), torch.log(self.repeat_explore[:, 1] + 1e-8)).mean() + repeat_loss = torch.mul( + repeat.unsqueeze(1), torch.log(self.repeat_explore[:, 0] + 1e-8) + ).mean() + explore_loss = torch.mul( + explore.unsqueeze(1), torch.log(self.repeat_explore[:, 1] + 1e-8) + ).mean() return (-repeat_loss - explore_loss) / 2 @@ -161,7 +185,6 @@ def predict(self, interaction): class Repeat_Explore_Mechanism(nn.Module): - def __init__(self, device, hidden_size, seq_len, dropout_prob): super(Repeat_Explore_Mechanism, self).__init__() self.dropout = nn.Dropout(dropout_prob) @@ -202,7 +225,6 @@ def forward(self, all_memory, last_memory): class Repeat_Recommendation_Decoder(nn.Module): - def __init__(self, device, hidden_size, seq_len, num_item, dropout_prob): super(Repeat_Recommendation_Decoder, self).__init__() self.dropout = nn.Dropout(dropout_prob) @@ -243,7 +265,6 @@ def forward(self, all_memory, last_memory, item_seq, mask=None): class Explore_Recommendation_Decoder(nn.Module): - def __init__(self, hidden_size, seq_len, num_item, device, dropout_prob): super(Explore_Recommendation_Decoder, self).__init__() self.dropout = nn.Dropout(dropout_prob) @@ -255,7 +276,9 @@ def __init__(self, hidden_size, seq_len, num_item, device, dropout_prob): self.Ue = nn.Linear(hidden_size, hidden_size) self.tanh = nn.Tanh() self.Ve = nn.Linear(hidden_size, 1) - self.matrix_for_explore = nn.Linear(2 * self.hidden_size, self.num_item, bias=False) + self.matrix_for_explore = nn.Linear( + 2 * self.hidden_size, self.num_item, bias=False + ) def forward(self, all_memory, last_memory, item_seq, mask=None): """ @@ -284,8 +307,10 @@ def forward(self, all_memory, last_memory, item_seq, mask=None): output_e = self.dropout(self.matrix_for_explore(output_e)) map_matrix = build_map(item_seq, self.device, max_index=self.num_item) - explore_mask = torch.bmm((item_seq > 0).float().unsqueeze(1), map_matrix).squeeze(1) - output_e = output_e.masked_fill(explore_mask.bool(), float('-inf')) + explore_mask = torch.bmm( + (item_seq > 0).float().unsqueeze(1), map_matrix + ).squeeze(1) + output_e = output_e.masked_fill(explore_mask.bool(), float("-inf")) explore_recommendation_decoder = nn.Softmax(1)(output_e) return explore_recommendation_decoder @@ -325,6 +350,6 @@ def build_map(b_map, device, max_index=None): b_map_ = torch.FloatTensor(batch_size, b_len, max_index).fill_(0).to(device) else: b_map_ = torch.zeros(batch_size, b_len, max_index) - b_map_.scatter_(2, b_map.unsqueeze(2), 1.) + b_map_.scatter_(2, b_map.unsqueeze(2), 1.0) b_map_.requires_grad = False return b_map_ diff --git a/recbole/model/sequential_recommender/s3rec.py b/recbole/model/sequential_recommender/s3rec.py index 9b2dd8168..98d638588 100644 --- a/recbole/model/sequential_recommender/s3rec.py +++ b/recbole/model/sequential_recommender/s3rec.py @@ -41,27 +41,29 @@ def __init__(self, config, dataset): super(S3Rec, self).__init__(config, dataset) # load parameters info - self.n_layers = config['n_layers'] - self.n_heads = config['n_heads'] - self.hidden_size = config['hidden_size'] # same as embedding_size - self.inner_size = config['inner_size'] # the dimensionality in feed-forward layer - self.hidden_dropout_prob = config['hidden_dropout_prob'] - self.attn_dropout_prob = config['attn_dropout_prob'] - self.hidden_act = config['hidden_act'] - self.layer_norm_eps = config['layer_norm_eps'] - - self.FEATURE_FIELD = config['item_attribute'] - self.FEATURE_LIST = self.FEATURE_FIELD + config['LIST_SUFFIX'] - self.train_stage = config['train_stage'] # pretrain or finetune - self.pre_model_path = config['pre_model_path'] # We need this for finetune - self.mask_ratio = config['mask_ratio'] - self.aap_weight = config['aap_weight'] - self.mip_weight = config['mip_weight'] - self.map_weight = config['map_weight'] - self.sp_weight = config['sp_weight'] - - self.initializer_range = config['initializer_range'] - self.loss_type = config['loss_type'] + self.n_layers = config["n_layers"] + self.n_heads = config["n_heads"] + self.hidden_size = config["hidden_size"] # same as embedding_size + self.inner_size = config[ + "inner_size" + ] # the dimensionality in feed-forward layer + self.hidden_dropout_prob = config["hidden_dropout_prob"] + self.attn_dropout_prob = config["attn_dropout_prob"] + self.hidden_act = config["hidden_act"] + self.layer_norm_eps = config["layer_norm_eps"] + + self.FEATURE_FIELD = config["item_attribute"] + self.FEATURE_LIST = self.FEATURE_FIELD + config["LIST_SUFFIX"] + self.train_stage = config["train_stage"] # pretrain or finetune + self.pre_model_path = config["pre_model_path"] # We need this for finetune + self.mask_ratio = config["mask_ratio"] + self.aap_weight = config["aap_weight"] + self.mip_weight = config["mip_weight"] + self.map_weight = config["map_weight"] + self.sp_weight = config["sp_weight"] + + self.initializer_range = config["initializer_range"] + self.loss_type = config["loss_type"] # load dataset info self.n_items = dataset.item_num + 1 # for mask token @@ -71,9 +73,13 @@ def __init__(self, config, dataset): # define layers and loss # modules shared by pre-training stage and fine-tuning stage - self.item_embedding = nn.Embedding(self.n_items, self.hidden_size, padding_idx=0) + self.item_embedding = nn.Embedding( + self.n_items, self.hidden_size, padding_idx=0 + ) self.position_embedding = nn.Embedding(self.max_seq_length, self.hidden_size) - self.feature_embedding = nn.Embedding(self.n_features, self.hidden_size, padding_idx=0) + self.feature_embedding = nn.Embedding( + self.n_features, self.hidden_size, padding_idx=0 + ) self.trm_encoder = TransformerEncoder( n_layers=self.n_layers, @@ -83,7 +89,7 @@ def __init__(self, config, dataset): hidden_dropout_prob=self.hidden_dropout_prob, attn_dropout_prob=self.attn_dropout_prob, hidden_act=self.hidden_act, - layer_norm_eps=self.layer_norm_eps + layer_norm_eps=self.layer_norm_eps, ) self.LayerNorm = nn.LayerNorm(self.hidden_size, eps=self.layer_norm_eps) @@ -95,28 +101,28 @@ def __init__(self, config, dataset): self.mip_norm = nn.Linear(self.hidden_size, self.hidden_size) self.map_norm = nn.Linear(self.hidden_size, self.hidden_size) self.sp_norm = nn.Linear(self.hidden_size, self.hidden_size) - self.loss_fct = nn.BCEWithLogitsLoss(reduction='none') + self.loss_fct = nn.BCEWithLogitsLoss(reduction="none") # modules for finetune - if self.loss_type == 'BPR' and self.train_stage == 'finetune': + if self.loss_type == "BPR" and self.train_stage == "finetune": self.loss_fct = BPRLoss() - elif self.loss_type == 'CE' and self.train_stage == 'finetune': + elif self.loss_type == "CE" and self.train_stage == "finetune": self.loss_fct = nn.CrossEntropyLoss() - elif self.train_stage == 'finetune': + elif self.train_stage == "finetune": raise NotImplementedError("Make sure 'loss_type' in ['BPR', 'CE']!") # parameters initialization - assert self.train_stage in ['pretrain', 'finetune'] - if self.train_stage == 'pretrain': + assert self.train_stage in ["pretrain", "finetune"] + if self.train_stage == "pretrain": self.apply(self._init_weights) else: # load pretrained model for finetune pretrained = torch.load(self.pre_model_path) - self.logger.info(f'Load pretrained model from {self.pre_model_path}') - self.load_state_dict(pretrained['state_dict']) + self.logger.info(f"Load pretrained model from {self.pre_model_path}") + self.load_state_dict(pretrained["state_dict"]) def _init_weights(self, module): - """ Initialize the weights """ + """Initialize the weights""" if isinstance(module, (nn.Linear, nn.Embedding)): # Slightly different from the TF version which uses truncated_normal for initialization # cf https://github.com/pytorch/pytorch/pull/5617 @@ -129,20 +135,28 @@ def _init_weights(self, module): def _associated_attribute_prediction(self, sequence_output, feature_embedding): sequence_output = self.aap_norm(sequence_output) # [B L H] - sequence_output = sequence_output.view([-1, sequence_output.size(-1), 1]) # [B*L H 1] + sequence_output = sequence_output.view( + [-1, sequence_output.size(-1), 1] + ) # [B*L H 1] # [feature_num H] [B*L H 1] -> [B*L feature_num 1] score = torch.matmul(feature_embedding, sequence_output) return score.squeeze(-1) # [B*L feature_num] def _masked_item_prediction(self, sequence_output, target_item_emb): - sequence_output = self.mip_norm(sequence_output.view([-1, sequence_output.size(-1)])) # [B*L H] - target_item_emb = target_item_emb.view([-1, sequence_output.size(-1)]) # [B*L H] + sequence_output = self.mip_norm( + sequence_output.view([-1, sequence_output.size(-1)]) + ) # [B*L H] + target_item_emb = target_item_emb.view( + [-1, sequence_output.size(-1)] + ) # [B*L H] score = torch.mul(sequence_output, target_item_emb) # [B*L H] return torch.sigmoid(torch.sum(score, -1)) # [B*L] def _masked_attribute_prediction(self, sequence_output, feature_embedding): sequence_output = self.map_norm(sequence_output) # [B L H] - sequence_output = sequence_output.view([-1, sequence_output.size(-1), 1]) # [B*L H 1] + sequence_output = sequence_output.view( + [-1, sequence_output.size(-1), 1] + ) # [B*L H 1] # [feature_num H] [B*L H 1] -> [B*L feature_num 1] score = torch.matmul(feature_embedding, sequence_output) return score.squeeze(-1) # [B*L feature_num] @@ -153,7 +167,9 @@ def _segment_prediction(self, context, segment_emb): return torch.sigmoid(torch.sum(score, dim=-1)) # [B] def forward(self, item_seq, bidirectional=True): - position_ids = torch.arange(item_seq.size(1), dtype=torch.long, device=item_seq.device) + position_ids = torch.arange( + item_seq.size(1), dtype=torch.long, device=item_seq.device + ) position_ids = position_ids.unsqueeze(0).expand_as(item_seq) position_embedding = self.position_embedding(position_ids) @@ -162,33 +178,45 @@ def forward(self, item_seq, bidirectional=True): input_emb = self.LayerNorm(input_emb) input_emb = self.dropout(input_emb) attention_mask = self.get_attention_mask(item_seq, bidirectional=bidirectional) - trm_output = self.trm_encoder(input_emb, attention_mask, output_all_encoded_layers=True) + trm_output = self.trm_encoder( + input_emb, attention_mask, output_all_encoded_layers=True + ) seq_output = trm_output[-1] # [B L H] return seq_output def pretrain( - self, features, masked_item_sequence, pos_items, neg_items, masked_segment_sequence, pos_segment, neg_segment + self, + features, + masked_item_sequence, + pos_items, + neg_items, + masked_segment_sequence, + pos_segment, + neg_segment, ): """Pretrain out model using four pre-training tasks: - 1. Associated Attribute Prediction + 1. Associated Attribute Prediction - 2. Masked Item Prediction + 2. Masked Item Prediction - 3. Masked Attribute Prediction + 3. Masked Attribute Prediction - 4. Segment Prediction + 4. Segment Prediction """ # Encode masked sequence sequence_output = self.forward(masked_item_sequence) feature_embedding = self.feature_embedding.weight # AAP - aap_score = self._associated_attribute_prediction(sequence_output, feature_embedding) + aap_score = self._associated_attribute_prediction( + sequence_output, feature_embedding + ) aap_loss = self.loss_fct(aap_score, features.view(-1, self.n_features).float()) # only compute loss at non-masked position - aap_mask = (masked_item_sequence != self.mask_token).float() * \ - (masked_item_sequence != 0).float() + aap_mask = (masked_item_sequence != self.mask_token).float() * ( + masked_item_sequence != 0 + ).float() aap_loss = torch.sum(aap_loss * aap_mask.flatten().unsqueeze(-1)) # MIP @@ -196,13 +224,17 @@ def pretrain( neg_item_embs = self.item_embedding(neg_items) pos_score = self._masked_item_prediction(sequence_output, pos_item_embs) neg_score = self._masked_item_prediction(sequence_output, neg_item_embs) - mip_distance =pos_score - neg_score - mip_loss = self.loss_fct(mip_distance, torch.ones_like(mip_distance, dtype=torch.float32)) + mip_distance = pos_score - neg_score + mip_loss = self.loss_fct( + mip_distance, torch.ones_like(mip_distance, dtype=torch.float32) + ) mip_mask = (masked_item_sequence == self.mask_token).float() mip_loss = torch.sum(mip_loss * mip_mask.flatten()) # MAP - map_score = self._masked_attribute_prediction(sequence_output, feature_embedding) + map_score = self._masked_attribute_prediction( + sequence_output, feature_embedding + ) map_loss = self.loss_fct(map_score, features.view(-1, self.n_features).float()) map_mask = (masked_item_sequence == self.mask_token).float() map_loss = torch.sum(map_loss * map_mask.flatten().unsqueeze(-1)) @@ -216,12 +248,18 @@ def pretrain( pos_segment_score = self._segment_prediction(segment_context, pos_segment_emb) neg_segment_score = self._segment_prediction(segment_context, neg_segment_emb) sp_distance = pos_segment_score - neg_segment_score - sp_loss = torch.sum(self.loss_fct(sp_distance, torch.ones_like(sp_distance, dtype=torch.float32))) + sp_loss = torch.sum( + self.loss_fct( + sp_distance, torch.ones_like(sp_distance, dtype=torch.float32) + ) + ) - pretrain_loss = self.aap_weight * aap_loss \ - + self.mip_weight * mip_loss \ - + self.map_weight * map_loss \ - + self.sp_weight * sp_loss + pretrain_loss = ( + self.aap_weight * aap_loss + + self.mip_weight * mip_loss + + self.map_weight * map_loss + + self.sp_weight * sp_loss + ) return pretrain_loss @@ -252,13 +290,17 @@ def reconstruct_pretrain_data(self, item_seq, item_seq_len): # we will padding zeros at the left side # these will be train_instances, after will be reshaped to batch sequence_instances = [] - associated_features = [] # For Associated Attribute Prediction and Masked Attribute Prediction + associated_features = ( + [] + ) # For Associated Attribute Prediction and Masked Attribute Prediction long_sequence = [] for i, end_i in enumerate(end_index): sequence_instances.append(item_seq[i][:end_i]) long_sequence.extend(item_seq[i][:end_i]) # padding feature at the left side - associated_features.extend([[0] * self.n_features] * (self.max_seq_length - end_i)) + associated_features.extend( + [[0] * self.n_features] * (self.max_seq_length - end_i) + ) for indexes in item_feature_seq[i][:end_i]: features = [0] * self.n_features try: @@ -302,42 +344,86 @@ def reconstruct_pretrain_data(self, item_seq, item_seq_len): sample_length = random.randint(1, len(instance) // 2) start_id = random.randint(0, len(instance) - sample_length) neg_start_id = random.randint(0, len(long_sequence) - sample_length) - pos_segment = instance[start_id:start_id + sample_length] - neg_segment = long_sequence[neg_start_id:neg_start_id + sample_length] - masked_segment = instance[:start_id] + [self.mask_token] * sample_length \ - + instance[start_id + sample_length:] - pos_segment = [self.mask_token] * start_id + pos_segment + \ - [self.mask_token] * (len(instance) - (start_id + sample_length)) - neg_segment = [self.mask_token] * start_id + neg_segment + \ - [self.mask_token] * (len(instance) - (start_id + sample_length)) + pos_segment = instance[start_id : start_id + sample_length] + neg_segment = long_sequence[neg_start_id : neg_start_id + sample_length] + masked_segment = ( + instance[:start_id] + + [self.mask_token] * sample_length + + instance[start_id + sample_length :] + ) + pos_segment = ( + [self.mask_token] * start_id + + pos_segment + + [self.mask_token] * (len(instance) - (start_id + sample_length)) + ) + neg_segment = ( + [self.mask_token] * start_id + + neg_segment + + [self.mask_token] * (len(instance) - (start_id + sample_length)) + ) masked_segment_list.append(self._padding_zero_at_left(masked_segment)) pos_segment_list.append(self._padding_zero_at_left(pos_segment)) neg_segment_list.append(self._padding_zero_at_left(neg_segment)) - associated_features = torch.tensor(associated_features, dtype=torch.long, device=device) - associated_features = associated_features.view(-1, self.max_seq_length, self.n_features) - - masked_item_sequence = torch.tensor(masked_item_sequence, dtype=torch.long, device=device).view(batch_size, -1) - pos_items = torch.tensor(pos_items, dtype=torch.long, device=device).view(batch_size, -1) - neg_items = torch.tensor(neg_items, dtype=torch.long, device=device).view(batch_size, -1) - masked_segment_list = torch.tensor(masked_segment_list, dtype=torch.long, device=device).view(batch_size, -1) - pos_segment_list = torch.tensor(pos_segment_list, dtype=torch.long, device=device).view(batch_size, -1) - neg_segment_list = torch.tensor(neg_segment_list, dtype=torch.long, device=device).view(batch_size, -1) + associated_features = torch.tensor( + associated_features, dtype=torch.long, device=device + ) + associated_features = associated_features.view( + -1, self.max_seq_length, self.n_features + ) - return associated_features, masked_item_sequence, pos_items, neg_items, \ - masked_segment_list, pos_segment_list, neg_segment_list + masked_item_sequence = torch.tensor( + masked_item_sequence, dtype=torch.long, device=device + ).view(batch_size, -1) + pos_items = torch.tensor(pos_items, dtype=torch.long, device=device).view( + batch_size, -1 + ) + neg_items = torch.tensor(neg_items, dtype=torch.long, device=device).view( + batch_size, -1 + ) + masked_segment_list = torch.tensor( + masked_segment_list, dtype=torch.long, device=device + ).view(batch_size, -1) + pos_segment_list = torch.tensor( + pos_segment_list, dtype=torch.long, device=device + ).view(batch_size, -1) + neg_segment_list = torch.tensor( + neg_segment_list, dtype=torch.long, device=device + ).view(batch_size, -1) + + return ( + associated_features, + masked_item_sequence, + pos_items, + neg_items, + masked_segment_list, + pos_segment_list, + neg_segment_list, + ) def calculate_loss(self, interaction): item_seq = interaction[self.ITEM_SEQ] item_seq_len = interaction[self.ITEM_SEQ_LEN] # pretrain - if self.train_stage == 'pretrain': - features, masked_item_sequence, pos_items, neg_items, \ - masked_segment_sequence, pos_segment, neg_segment \ - = self.reconstruct_pretrain_data(item_seq, item_seq_len) + if self.train_stage == "pretrain": + ( + features, + masked_item_sequence, + pos_items, + neg_items, + masked_segment_sequence, + pos_segment, + neg_segment, + ) = self.reconstruct_pretrain_data(item_seq, item_seq_len) loss = self.pretrain( - features, masked_item_sequence, pos_items, neg_items, masked_segment_sequence, pos_segment, neg_segment + features, + masked_item_sequence, + pos_items, + neg_items, + masked_segment_sequence, + pos_segment, + neg_segment, ) # finetune else: @@ -346,7 +432,7 @@ def calculate_loss(self, interaction): seq_output = self.forward(item_seq, bidirectional=False) seq_output = self.gather_indexes(seq_output, item_seq_len - 1) - if self.loss_type == 'BPR': + if self.loss_type == "BPR": neg_items = interaction[self.NEG_ITEM_ID] pos_items_emb = self.item_embedding(pos_items) neg_items_emb = self.item_embedding(neg_items) @@ -374,6 +460,10 @@ def full_sort_predict(self, interaction): item_seq_len = interaction[self.ITEM_SEQ_LEN] seq_output = self.forward(item_seq, bidirectional=False) seq_output = self.gather_indexes(seq_output, item_seq_len - 1) - test_items_emb = self.item_embedding.weight[:self.n_items - 1] # delete masked token - scores = torch.matmul(seq_output, test_items_emb.transpose(0, 1)) # [B, n_items] + test_items_emb = self.item_embedding.weight[ + : self.n_items - 1 + ] # delete masked token + scores = torch.matmul( + seq_output, test_items_emb.transpose(0, 1) + ) # [B, n_items] return scores diff --git a/recbole/model/sequential_recommender/sasrec.py b/recbole/model/sequential_recommender/sasrec.py index ca16028fc..721ce323b 100644 --- a/recbole/model/sequential_recommender/sasrec.py +++ b/recbole/model/sequential_recommender/sasrec.py @@ -37,20 +37,24 @@ def __init__(self, config, dataset): super(SASRec, self).__init__(config, dataset) # load parameters info - self.n_layers = config['n_layers'] - self.n_heads = config['n_heads'] - self.hidden_size = config['hidden_size'] # same as embedding_size - self.inner_size = config['inner_size'] # the dimensionality in feed-forward layer - self.hidden_dropout_prob = config['hidden_dropout_prob'] - self.attn_dropout_prob = config['attn_dropout_prob'] - self.hidden_act = config['hidden_act'] - self.layer_norm_eps = config['layer_norm_eps'] - - self.initializer_range = config['initializer_range'] - self.loss_type = config['loss_type'] + self.n_layers = config["n_layers"] + self.n_heads = config["n_heads"] + self.hidden_size = config["hidden_size"] # same as embedding_size + self.inner_size = config[ + "inner_size" + ] # the dimensionality in feed-forward layer + self.hidden_dropout_prob = config["hidden_dropout_prob"] + self.attn_dropout_prob = config["attn_dropout_prob"] + self.hidden_act = config["hidden_act"] + self.layer_norm_eps = config["layer_norm_eps"] + + self.initializer_range = config["initializer_range"] + self.loss_type = config["loss_type"] # define layers and loss - self.item_embedding = nn.Embedding(self.n_items, self.hidden_size, padding_idx=0) + self.item_embedding = nn.Embedding( + self.n_items, self.hidden_size, padding_idx=0 + ) self.position_embedding = nn.Embedding(self.max_seq_length, self.hidden_size) self.trm_encoder = TransformerEncoder( n_layers=self.n_layers, @@ -60,15 +64,15 @@ def __init__(self, config, dataset): hidden_dropout_prob=self.hidden_dropout_prob, attn_dropout_prob=self.attn_dropout_prob, hidden_act=self.hidden_act, - layer_norm_eps=self.layer_norm_eps + layer_norm_eps=self.layer_norm_eps, ) self.LayerNorm = nn.LayerNorm(self.hidden_size, eps=self.layer_norm_eps) self.dropout = nn.Dropout(self.hidden_dropout_prob) - if self.loss_type == 'BPR': + if self.loss_type == "BPR": self.loss_fct = BPRLoss() - elif self.loss_type == 'CE': + elif self.loss_type == "CE": self.loss_fct = nn.CrossEntropyLoss() else: raise NotImplementedError("Make sure 'loss_type' in ['BPR', 'CE']!") @@ -77,7 +81,7 @@ def __init__(self, config, dataset): self.apply(self._init_weights) def _init_weights(self, module): - """ Initialize the weights """ + """Initialize the weights""" if isinstance(module, (nn.Linear, nn.Embedding)): # Slightly different from the TF version which uses truncated_normal for initialization # cf https://github.com/pytorch/pytorch/pull/5617 @@ -89,7 +93,9 @@ def _init_weights(self, module): module.bias.data.zero_() def forward(self, item_seq, item_seq_len): - position_ids = torch.arange(item_seq.size(1), dtype=torch.long, device=item_seq.device) + position_ids = torch.arange( + item_seq.size(1), dtype=torch.long, device=item_seq.device + ) position_ids = position_ids.unsqueeze(0).expand_as(item_seq) position_embedding = self.position_embedding(position_ids) @@ -100,7 +106,9 @@ def forward(self, item_seq, item_seq_len): extended_attention_mask = self.get_attention_mask(item_seq) - trm_output = self.trm_encoder(input_emb, extended_attention_mask, output_all_encoded_layers=True) + trm_output = self.trm_encoder( + input_emb, extended_attention_mask, output_all_encoded_layers=True + ) output = trm_output[-1] output = self.gather_indexes(output, item_seq_len - 1) return output # [B H] @@ -110,7 +118,7 @@ def calculate_loss(self, interaction): item_seq_len = interaction[self.ITEM_SEQ_LEN] seq_output = self.forward(item_seq, item_seq_len) pos_items = interaction[self.POS_ITEM_ID] - if self.loss_type == 'BPR': + if self.loss_type == "BPR": neg_items = interaction[self.NEG_ITEM_ID] pos_items_emb = self.item_embedding(pos_items) neg_items_emb = self.item_embedding(neg_items) diff --git a/recbole/model/sequential_recommender/sasrecf.py b/recbole/model/sequential_recommender/sasrecf.py index 497f9fe45..b1755cd44 100644 --- a/recbole/model/sequential_recommender/sasrecf.py +++ b/recbole/model/sequential_recommender/sasrecf.py @@ -26,31 +26,41 @@ def __init__(self, config, dataset): super(SASRecF, self).__init__(config, dataset) # load parameters info - self.n_layers = config['n_layers'] - self.n_heads = config['n_heads'] - self.hidden_size = config['hidden_size'] # same as embedding_size - self.inner_size = config['inner_size'] # the dimensionality in feed-forward layer - self.hidden_dropout_prob = config['hidden_dropout_prob'] - self.attn_dropout_prob = config['attn_dropout_prob'] - self.hidden_act = config['hidden_act'] - self.layer_norm_eps = config['layer_norm_eps'] - - self.selected_features = config['selected_features'] - self.pooling_mode = config['pooling_mode'] - self.device = config['device'] + self.n_layers = config["n_layers"] + self.n_heads = config["n_heads"] + self.hidden_size = config["hidden_size"] # same as embedding_size + self.inner_size = config[ + "inner_size" + ] # the dimensionality in feed-forward layer + self.hidden_dropout_prob = config["hidden_dropout_prob"] + self.attn_dropout_prob = config["attn_dropout_prob"] + self.hidden_act = config["hidden_act"] + self.layer_norm_eps = config["layer_norm_eps"] + + self.selected_features = config["selected_features"] + self.pooling_mode = config["pooling_mode"] + self.device = config["device"] self.num_feature_field = sum( - 1 if dataset.field2type[field] != FeatureType.FLOAT_SEQ else dataset.num(field) - for field in config['selected_features'] + 1 + if dataset.field2type[field] != FeatureType.FLOAT_SEQ + else dataset.num(field) + for field in config["selected_features"] ) - self.initializer_range = config['initializer_range'] - self.loss_type = config['loss_type'] + self.initializer_range = config["initializer_range"] + self.loss_type = config["loss_type"] # define layers and loss - self.item_embedding = nn.Embedding(self.n_items, self.hidden_size, padding_idx=0) + self.item_embedding = nn.Embedding( + self.n_items, self.hidden_size, padding_idx=0 + ) self.position_embedding = nn.Embedding(self.max_seq_length, self.hidden_size) self.feature_embed_layer = FeatureSeqEmbLayer( - dataset, self.hidden_size, self.selected_features, self.pooling_mode, self.device + dataset, + self.hidden_size, + self.selected_features, + self.pooling_mode, + self.device, ) self.trm_encoder = TransformerEncoder( @@ -61,27 +71,29 @@ def __init__(self, config, dataset): hidden_dropout_prob=self.hidden_dropout_prob, attn_dropout_prob=self.attn_dropout_prob, hidden_act=self.hidden_act, - layer_norm_eps=self.layer_norm_eps + layer_norm_eps=self.layer_norm_eps, ) - self.concat_layer = nn.Linear(self.hidden_size * (1 + self.num_feature_field), self.hidden_size) + self.concat_layer = nn.Linear( + self.hidden_size * (1 + self.num_feature_field), self.hidden_size + ) self.LayerNorm = nn.LayerNorm(self.hidden_size, eps=self.layer_norm_eps) self.dropout = nn.Dropout(self.hidden_dropout_prob) - if self.loss_type == 'BPR': + if self.loss_type == "BPR": self.loss_fct = BPRLoss() - elif self.loss_type == 'CE': + elif self.loss_type == "CE": self.loss_fct = nn.CrossEntropyLoss() else: raise NotImplementedError("Make sure 'loss_type' in ['BPR', 'CE']!") # parameters initialization self.apply(self._init_weights) - self.other_parameter_name = ['feature_embed_layer'] + self.other_parameter_name = ["feature_embed_layer"] def _init_weights(self, module): - """ Initialize the weights """ + """Initialize the weights""" if isinstance(module, (nn.Linear, nn.Embedding)): # Slightly different from the TF version which uses truncated_normal for initialization # cf https://github.com/pytorch/pytorch/pull/5617 @@ -96,13 +108,15 @@ def forward(self, item_seq, item_seq_len): item_emb = self.item_embedding(item_seq) # position embedding - position_ids = torch.arange(item_seq.size(1), dtype=torch.long, device=item_seq.device) + position_ids = torch.arange( + item_seq.size(1), dtype=torch.long, device=item_seq.device + ) position_ids = position_ids.unsqueeze(0).expand_as(item_seq) position_embedding = self.position_embedding(position_ids) sparse_embedding, dense_embedding = self.feature_embed_layer(None, item_seq) - sparse_embedding = sparse_embedding['item'] - dense_embedding = dense_embedding['item'] + sparse_embedding = sparse_embedding["item"] + dense_embedding = dense_embedding["item"] # concat the sparse embedding and float embedding feature_table = [] if sparse_embedding is not None: @@ -113,7 +127,9 @@ def forward(self, item_seq, item_seq_len): feature_table = torch.cat(feature_table, dim=-2) table_shape = feature_table.shape feat_num, embedding_size = table_shape[-2], table_shape[-1] - feature_emb = feature_table.view(table_shape[:-2] + (feat_num * embedding_size,)) + feature_emb = feature_table.view( + table_shape[:-2] + (feat_num * embedding_size,) + ) input_concat = torch.cat((item_emb, feature_emb), -1) # [B 1+field_num*H] input_emb = self.concat_layer(input_concat) @@ -122,7 +138,9 @@ def forward(self, item_seq, item_seq_len): input_emb = self.dropout(input_emb) extended_attention_mask = self.get_attention_mask(item_seq) - trm_output = self.trm_encoder(input_emb, extended_attention_mask, output_all_encoded_layers=True) + trm_output = self.trm_encoder( + input_emb, extended_attention_mask, output_all_encoded_layers=True + ) output = trm_output[-1] seq_output = self.gather_indexes(output, item_seq_len - 1) return seq_output # [B H] @@ -132,7 +150,7 @@ def calculate_loss(self, interaction): item_seq_len = interaction[self.ITEM_SEQ_LEN] seq_output = self.forward(item_seq, item_seq_len) pos_items = interaction[self.POS_ITEM_ID] - if self.loss_type == 'BPR': + if self.loss_type == "BPR": neg_items = interaction[self.NEG_ITEM_ID] pos_items_emb = self.item_embedding(pos_items) neg_items_emb = self.item_embedding(neg_items) @@ -160,5 +178,7 @@ def full_sort_predict(self, interaction): item_seq_len = interaction[self.ITEM_SEQ_LEN] seq_output = self.forward(item_seq, item_seq_len) test_items_emb = self.item_embedding.weight - scores = torch.matmul(seq_output, test_items_emb.transpose(0, 1)) # [B, item_num] + scores = torch.matmul( + seq_output, test_items_emb.transpose(0, 1) + ) # [B, item_num] return scores diff --git a/recbole/model/sequential_recommender/shan.py b/recbole/model/sequential_recommender/shan.py index 19d7d435d..58f3f4f5d 100644 --- a/recbole/model/sequential_recommender/shan.py +++ b/recbole/model/sequential_recommender/shan.py @@ -35,16 +35,22 @@ def __init__(self, config, dataset): # load the dataset information self.n_users = dataset.num(self.USER_ID) - self.device = config['device'] + self.device = config["device"] # load the parameter information self.embedding_size = config["embedding_size"] - self.short_item_length = config["short_item_length"] # the length of the short session items - assert self.short_item_length <= self.max_seq_length, "short_item_length can't longer than the max_seq_length" + self.short_item_length = config[ + "short_item_length" + ] # the length of the short session items + assert ( + self.short_item_length <= self.max_seq_length + ), "short_item_length can't longer than the max_seq_length" self.reg_weight = config["reg_weight"] # define layers and loss - self.item_embedding = nn.Embedding(self.n_items, self.embedding_size, padding_idx=0) + self.item_embedding = nn.Embedding( + self.n_items, self.embedding_size, padding_idx=0 + ) self.user_embedding = nn.Embedding(self.n_users, self.embedding_size) self.long_w = nn.Linear(self.embedding_size, self.embedding_size) @@ -52,26 +58,26 @@ def __init__(self, config, dataset): uniform_( tensor=torch.zeros(self.embedding_size), a=-np.sqrt(3 / self.embedding_size), - b=np.sqrt(3 / self.embedding_size) + b=np.sqrt(3 / self.embedding_size), ), - requires_grad=True + requires_grad=True, ).to(self.device) self.long_short_w = nn.Linear(self.embedding_size, self.embedding_size) self.long_short_b = nn.Parameter( uniform_( tensor=torch.zeros(self.embedding_size), a=-np.sqrt(3 / self.embedding_size), - b=np.sqrt(3 / self.embedding_size) + b=np.sqrt(3 / self.embedding_size), ), - requires_grad=True + requires_grad=True, ).to(self.device) self.relu = nn.ReLU() - self.loss_type = config['loss_type'] - if self.loss_type == 'BPR': + self.loss_type = config["loss_type"] + if self.loss_type == "BPR": self.loss_fct = BPRLoss() - elif self.loss_type == 'CE': + elif self.loss_type == "CE": self.loss_fct = nn.CrossEntropyLoss() else: raise NotImplementedError("Make sure 'loss_type' in ['BPR', 'CE']!") @@ -82,8 +88,12 @@ def __init__(self, config, dataset): def reg_loss(self, user_embedding, item_embedding): reg_1, reg_2 = self.reg_weight - loss_1 = reg_1 * torch.norm(self.long_w.weight, p=2) + reg_1 * torch.norm(self.long_short_w.weight, p=2) - loss_2 = reg_2 * torch.norm(user_embedding, p=2) + reg_2 * torch.norm(item_embedding, p=2) + loss_1 = reg_1 * torch.norm(self.long_w.weight, p=2) + reg_1 * torch.norm( + self.long_short_w.weight, p=2 + ) + loss_2 = reg_2 * torch.norm(user_embedding, p=2) + reg_2 * torch.norm( + item_embedding, p=2 + ) return loss_1 + loss_2 @@ -106,11 +116,19 @@ def inverse_seq_item(self, seq_item, seq_item_len): def init_weights(self, module): if isinstance(module, nn.Embedding): - normal_(module.weight.data, 0., 0.01) + normal_(module.weight.data, 0.0, 0.01) elif isinstance(module, nn.Linear): - uniform_(module.weight.data, -np.sqrt(3 / self.embedding_size), np.sqrt(3 / self.embedding_size)) + uniform_( + module.weight.data, + -np.sqrt(3 / self.embedding_size), + np.sqrt(3 / self.embedding_size), + ) elif isinstance(module, nn.Parameter): - uniform_(module.data, -np.sqrt(3 / self.embedding_size), np.sqrt(3 / self.embedding_size)) + uniform_( + module.data, + -np.sqrt(3 / self.embedding_size), + np.sqrt(3 / self.embedding_size), + ) print(module.data) def forward(self, seq_item, user, seq_item_len): @@ -122,22 +140,28 @@ def forward(self, seq_item, user, seq_item_len): # get the mask mask = seq_item.data.eq(0) - long_term_attention_based_pooling_layer = self.long_term_attention_based_pooling_layer( - seq_item_embedding, user_embedding, mask + long_term_attention_based_pooling_layer = ( + self.long_term_attention_based_pooling_layer( + seq_item_embedding, user_embedding, mask + ) ) # batch_size * 1 * embedding_size - short_item_embedding = seq_item_embedding[:, -self.short_item_length:, :] - mask_long_short = mask[:, -self.short_item_length:] + short_item_embedding = seq_item_embedding[:, -self.short_item_length :, :] + mask_long_short = mask[:, -self.short_item_length :] batch_size = mask_long_short.size(0) x = torch.zeros(size=(batch_size, 1)).eq(1).to(self.device) mask_long_short = torch.cat([x, mask_long_short], dim=1) # batch_size * short_item_length * embedding_size - long_short_item_embedding = torch.cat([long_term_attention_based_pooling_layer, short_item_embedding], dim=1) + long_short_item_embedding = torch.cat( + [long_term_attention_based_pooling_layer, short_item_embedding], dim=1 + ) # batch_size * 1_plus_short_item_length * embedding_size - long_short_item_embedding = self.long_and_short_term_attention_based_pooling_layer( - long_short_item_embedding, user_embedding, mask_long_short + long_short_item_embedding = ( + self.long_and_short_term_attention_based_pooling_layer( + long_short_item_embedding, user_embedding, mask_long_short + ) ) # batch_size * embedding_size @@ -152,7 +176,7 @@ def calculate_loss(self, interaction): seq_output = self.forward(seq_item, user, seq_item_len) pos_items = interaction[self.POS_ITEM_ID] pos_items_emb = self.item_embedding(pos_items) - if self.loss_type == 'BPR': + if self.loss_type == "BPR": neg_items = interaction[self.NEG_ITEM_ID] neg_items_emb = self.item_embedding(neg_items) pos_score = torch.sum(seq_output * pos_items_emb, dim=-1) @@ -186,25 +210,34 @@ def full_sort_predict(self, interaction): scores = torch.matmul(seq_output, test_items_emb.transpose(0, 1)) return scores - def long_and_short_term_attention_based_pooling_layer(self, long_short_item_embedding, user_embedding, mask=None): + def long_and_short_term_attention_based_pooling_layer( + self, long_short_item_embedding, user_embedding, mask=None + ): """ fusing the long term purpose with the short-term preference """ long_short_item_embedding_value = long_short_item_embedding - long_short_item_embedding = self.relu(self.long_short_w(long_short_item_embedding) + self.long_short_b) - long_short_item_embedding = torch.matmul(long_short_item_embedding, user_embedding.unsqueeze(2)).squeeze(-1) + long_short_item_embedding = self.relu( + self.long_short_w(long_short_item_embedding) + self.long_short_b + ) + long_short_item_embedding = torch.matmul( + long_short_item_embedding, user_embedding.unsqueeze(2) + ).squeeze(-1) # batch_size * seq_len if mask is not None: long_short_item_embedding.masked_fill_(mask, -1e9) long_short_item_embedding = nn.Softmax(dim=-1)(long_short_item_embedding) - long_short_item_embedding = torch.mul(long_short_item_embedding_value, - long_short_item_embedding.unsqueeze(2)).sum(dim=1) + long_short_item_embedding = torch.mul( + long_short_item_embedding_value, long_short_item_embedding.unsqueeze(2) + ).sum(dim=1) return long_short_item_embedding - def long_term_attention_based_pooling_layer(self, seq_item_embedding, user_embedding, mask=None): + def long_term_attention_based_pooling_layer( + self, seq_item_embedding, user_embedding, mask=None + ): """ get the long term purpose of user @@ -212,13 +245,16 @@ def long_term_attention_based_pooling_layer(self, seq_item_embedding, user_embed seq_item_embedding_value = seq_item_embedding seq_item_embedding = self.relu(self.long_w(seq_item_embedding) + self.long_b) - user_item_embedding = torch.matmul(seq_item_embedding, user_embedding.unsqueeze(2)).squeeze(-1) + user_item_embedding = torch.matmul( + seq_item_embedding, user_embedding.unsqueeze(2) + ).squeeze(-1) # batch_size * seq_len if mask is not None: user_item_embedding.masked_fill_(mask, -1e9) user_item_embedding = nn.Softmax(dim=1)(user_item_embedding) - user_item_embedding = torch.mul(seq_item_embedding_value, - user_item_embedding.unsqueeze(2)).sum(dim=1, keepdim=True) + user_item_embedding = torch.mul( + seq_item_embedding_value, user_item_embedding.unsqueeze(2) + ).sum(dim=1, keepdim=True) # batch_size * 1 * embedding_size return user_item_embedding diff --git a/recbole/model/sequential_recommender/sine.py b/recbole/model/sequential_recommender/sine.py index d1a052610..5cacb9577 100644 --- a/recbole/model/sequential_recommender/sine.py +++ b/recbole/model/sequential_recommender/sine.py @@ -37,24 +37,24 @@ def __init__(self, config, dataset): # load parameters info self.device = config["device"] - self.embedding_size = config['embedding_size'] - self.loss_type = config['loss_type'] - self.layer_norm_eps = config['layer_norm_eps'] + self.embedding_size = config["embedding_size"] + self.loss_type = config["loss_type"] + self.layer_norm_eps = config["layer_norm_eps"] - if self.loss_type == 'BPR': + if self.loss_type == "BPR": self.loss_fct = BPRLoss() - elif self.loss_type == 'CE': + elif self.loss_type == "CE": self.loss_fct = nn.CrossEntropyLoss() - elif self.loss_type == 'NLL': + elif self.loss_type == "NLL": self.loss_fct = nn.NLLLoss() else: raise NotImplementedError("Make sure 'loss_type' in ['BPR', 'CE', 'NLL']!") - self.D = config['embedding_size'] - self.L = config['prototype_size'] # 500 for movie-len dataset - self.k = config['interest_size'] # 4 for movie-len dataset - self.tau = config['tau_ratio'] # 0.1 in paper - self.reg_loss_ratio = config['reg_loss_ratio'] # 0.1 in paper + self.D = config["embedding_size"] + self.L = config["prototype_size"] # 500 for movie-len dataset + self.k = config["interest_size"] # 4 for movie-len dataset + self.tau = config["tau_ratio"] # 0.1 in paper + self.reg_loss_ratio = config["reg_loss_ratio"] # 0.1 in paper self.initializer_range = 0.01 @@ -76,7 +76,9 @@ def __init__(self, config, dataset): def _init_weight(self, shape): mat = np.random.normal(0, self.initializer_range, shape) - return torch.tensor(mat, dtype=torch.float32, requires_grad=True).to(self.device) + return torch.tensor(mat, dtype=torch.float32, requires_grad=True).to( + self.device + ) def _init_weights(self, module): if isinstance(module, nn.Embedding): @@ -91,7 +93,7 @@ def calculate_loss(self, interaction): seq_output = self.forward(item_seq, item_seq_len) pos_items = interaction[self.POS_ITEM_ID] - if self.loss_type == 'BPR': + if self.loss_type == "BPR": neg_items = interaction[self.NEG_ITEM_ID] pos_items_emb = self.item_embedding(pos_items) neg_items_emb = self.item_embedding(neg_items) @@ -99,7 +101,7 @@ def calculate_loss(self, interaction): neg_score = torch.sum(seq_output * neg_items_emb, dim=-1) # [B] loss = self.loss_fct(pos_score, neg_score) return loss - elif self.loss_type == 'CE': + elif self.loss_type == "CE": test_item_emb = self.item_embedding.weight logits = torch.matmul(seq_output, test_item_emb.transpose(0, 1)) loss = self.loss_fct(logits, pos_items) @@ -113,7 +115,7 @@ def calculate_loss(self, interaction): def calculate_reg_loss(self): C_mean = torch.mean(self.C.weight, dim=1, keepdim=True) - C_reg = (self.C.weight - C_mean) + C_reg = self.C.weight - C_mean C_reg = C_reg.matmul(C_reg.T) / self.D return (torch.norm(C_reg) ** 2 - torch.norm(torch.diag(C_reg)) ** 2) / 2 @@ -138,8 +140,8 @@ def forward(self, item_seq, item_seq_len): z_u = torch.matmul(a.unsqueeze(2).transpose(1, 2), x_u).transpose(1, 2) s_u = torch.matmul(self.C.weight, z_u) s_u = s_u.squeeze(2) - idx = s_u.argsort(1)[:, -self.k:] - s_u_idx = s_u.sort(1)[0][:, -self.k:] + idx = s_u.argsort(1)[:, -self.k :] + s_u_idx = s_u.sort(1)[0][:, -self.k :] c_u = self.C(idx) sigs = torch.sigmoid(s_u_idx.unsqueeze(2).repeat(1, 1, self.embedding_size)) C_u = c_u.mul(sigs) @@ -154,7 +156,12 @@ def forward(self, item_seq, item_seq_len): # attention weighting a_k = x_u.unsqueeze(1).repeat(1, self.k, 1, 1).matmul(self.w_k_1) - P_t_k = F.softmax(torch.tanh(a_k).matmul(self.w_k_2.reshape(self.k, self.embedding_size, 1)).squeeze(3), dim=2) + P_t_k = F.softmax( + torch.tanh(a_k) + .matmul(self.w_k_2.reshape(self.k, self.embedding_size, 1)) + .squeeze(3), + dim=2, + ) # interest embedding generation mul_p = P_k_t_b_t.mul(P_t_k) @@ -181,5 +188,7 @@ def full_sort_predict(self, interaction): item_seq_len = interaction[self.ITEM_SEQ_LEN] seq_output = self.forward(item_seq, item_seq_len) test_items_emb = self.item_embedding.weight - scores = torch.matmul(seq_output, test_items_emb.transpose(0, 1)) # [B, n_items] + scores = torch.matmul( + seq_output, test_items_emb.transpose(0, 1) + ) # [B, n_items] return scores diff --git a/recbole/model/sequential_recommender/srgnn.py b/recbole/model/sequential_recommender/srgnn.py index 5e94459df..c9371fa86 100644 --- a/recbole/model/sequential_recommender/srgnn.py +++ b/recbole/model/sequential_recommender/srgnn.py @@ -44,8 +44,12 @@ def __init__(self, embedding_size, step=1): self.b_iah = Parameter(torch.Tensor(self.embedding_size)) self.b_ioh = Parameter(torch.Tensor(self.embedding_size)) - self.linear_edge_in = nn.Linear(self.embedding_size, self.embedding_size, bias=True) - self.linear_edge_out = nn.Linear(self.embedding_size, self.embedding_size, bias=True) + self.linear_edge_in = nn.Linear( + self.embedding_size, self.embedding_size, bias=True + ) + self.linear_edge_out = nn.Linear( + self.embedding_size, self.embedding_size, bias=True + ) def GNNCell(self, A, hidden): r"""Obtain latent vectors of nodes via graph neural networks. @@ -61,8 +65,15 @@ def GNNCell(self, A, hidden): """ - input_in = torch.matmul(A[:, :, :A.size(1)], self.linear_edge_in(hidden)) + self.b_iah - input_out = torch.matmul(A[:, :, A.size(1):2 * A.size(1)], self.linear_edge_out(hidden)) + self.b_ioh + input_in = ( + torch.matmul(A[:, :, : A.size(1)], self.linear_edge_in(hidden)) + self.b_iah + ) + input_out = ( + torch.matmul( + A[:, :, A.size(1) : 2 * A.size(1)], self.linear_edge_out(hidden) + ) + + self.b_ioh + ) # [batch_size, max_session_len, embedding_size * 2] inputs = torch.cat([input_in, input_out], 2) @@ -116,23 +127,27 @@ def __init__(self, config, dataset): super(SRGNN, self).__init__(config, dataset) # load parameters info - self.embedding_size = config['embedding_size'] - self.step = config['step'] - self.device = config['device'] - self.loss_type = config['loss_type'] + self.embedding_size = config["embedding_size"] + self.step = config["step"] + self.device = config["device"] + self.loss_type = config["loss_type"] # define layers and loss # item embedding - self.item_embedding = nn.Embedding(self.n_items, self.embedding_size, padding_idx=0) + self.item_embedding = nn.Embedding( + self.n_items, self.embedding_size, padding_idx=0 + ) # define layers and loss self.gnn = GNN(self.embedding_size, self.step) self.linear_one = nn.Linear(self.embedding_size, self.embedding_size, bias=True) self.linear_two = nn.Linear(self.embedding_size, self.embedding_size, bias=True) self.linear_three = nn.Linear(self.embedding_size, 1, bias=False) - self.linear_transform = nn.Linear(self.embedding_size * 2, self.embedding_size, bias=True) - if self.loss_type == 'BPR': + self.linear_transform = nn.Linear( + self.embedding_size * 2, self.embedding_size, bias=True + ) + if self.loss_type == "BPR": self.loss_fct = BPRLoss() - elif self.loss_type == 'CE': + elif self.loss_type == "CE": self.loss_fct = nn.CrossEntropyLoss() else: raise NotImplementedError("Make sure 'loss_type' in ['BPR', 'CE']!") @@ -188,7 +203,9 @@ def forward(self, item_seq, item_seq_len): alias_inputs, A, items, mask = self._get_slice(item_seq) hidden = self.item_embedding(items) hidden = self.gnn(A, hidden) - alias_inputs = alias_inputs.view(-1, alias_inputs.size(1), 1).expand(-1, -1, self.embedding_size) + alias_inputs = alias_inputs.view(-1, alias_inputs.size(1), 1).expand( + -1, -1, self.embedding_size + ) seq_hidden = torch.gather(hidden, dim=1, index=alias_inputs) # fetch the last hidden state of last timestamp ht = self.gather_indexes(seq_hidden, item_seq_len - 1) @@ -205,7 +222,7 @@ def calculate_loss(self, interaction): item_seq_len = interaction[self.ITEM_SEQ_LEN] seq_output = self.forward(item_seq, item_seq_len) pos_items = interaction[self.POS_ITEM_ID] - if self.loss_type == 'BPR': + if self.loss_type == "BPR": neg_items = interaction[self.NEG_ITEM_ID] pos_items_emb = self.item_embedding(pos_items) neg_items_emb = self.item_embedding(neg_items) @@ -233,5 +250,7 @@ def full_sort_predict(self, interaction): item_seq_len = interaction[self.ITEM_SEQ_LEN] seq_output = self.forward(item_seq, item_seq_len) test_items_emb = self.item_embedding.weight - scores = torch.matmul(seq_output, test_items_emb.transpose(0, 1)) # [B, n_items] + scores = torch.matmul( + seq_output, test_items_emb.transpose(0, 1) + ) # [B, n_items] return scores diff --git a/recbole/model/sequential_recommender/stamp.py b/recbole/model/sequential_recommender/stamp.py index f9982734e..b1a4e4675 100644 --- a/recbole/model/sequential_recommender/stamp.py +++ b/recbole/model/sequential_recommender/stamp.py @@ -41,10 +41,12 @@ def __init__(self, config, dataset): super(STAMP, self).__init__(config, dataset) # load parameters info - self.embedding_size = config['embedding_size'] + self.embedding_size = config["embedding_size"] # define layers and loss - self.item_embedding = nn.Embedding(self.n_items, self.embedding_size, padding_idx=0) + self.item_embedding = nn.Embedding( + self.n_items, self.embedding_size, padding_idx=0 + ) self.w1 = nn.Linear(self.embedding_size, self.embedding_size, bias=False) self.w2 = nn.Linear(self.embedding_size, self.embedding_size, bias=False) self.w3 = nn.Linear(self.embedding_size, self.embedding_size, bias=False) @@ -54,10 +56,10 @@ def __init__(self, config, dataset): self.mlp_b = nn.Linear(self.embedding_size, self.embedding_size, bias=True) self.sigmoid = nn.Sigmoid() self.tanh = nn.Tanh() - self.loss_type = config['loss_type'] - if self.loss_type == 'BPR': + self.loss_type = config["loss_type"] + if self.loss_type == "BPR": self.loss_fct = BPRLoss() - elif self.loss_type == 'CE': + elif self.loss_type == "CE": self.loss_fct = nn.CrossEntropyLoss() else: raise NotImplementedError("Make sure 'loss_type' in ['BPR', 'CE']!") @@ -98,8 +100,12 @@ def count_alpha(self, context, aspect, output): torch.Tensor:attention weights, shape of [batch_size, time_steps] """ timesteps = context.size(1) - aspect_3dim = aspect.repeat(1, timesteps).view(-1, timesteps, self.embedding_size) - output_3dim = output.repeat(1, timesteps).view(-1, timesteps, self.embedding_size) + aspect_3dim = aspect.repeat(1, timesteps).view( + -1, timesteps, self.embedding_size + ) + output_3dim = output.repeat(1, timesteps).view( + -1, timesteps, self.embedding_size + ) res_ctx = self.w1(context) res_asp = self.w2(aspect_3dim) res_output = self.w3(output_3dim) @@ -113,7 +119,7 @@ def calculate_loss(self, interaction): item_seq_len = interaction[self.ITEM_SEQ_LEN] seq_output = self.forward(item_seq, item_seq_len) pos_items = interaction[self.POS_ITEM_ID] - if self.loss_type == 'BPR': + if self.loss_type == "BPR": neg_items = interaction[self.NEG_ITEM_ID] pos_items_emb = self.item_embedding(pos_items) neg_items_emb = self.item_embedding(neg_items) diff --git a/recbole/model/sequential_recommender/transrec.py b/recbole/model/sequential_recommender/transrec.py index 5e431c8bd..74ccabf48 100644 --- a/recbole/model/sequential_recommender/transrec.py +++ b/recbole/model/sequential_recommender/transrec.py @@ -34,15 +34,21 @@ def __init__(self, config, dataset): super(TransRec, self).__init__(config, dataset) # load parameters info - self.embedding_size = config['embedding_size'] + self.embedding_size = config["embedding_size"] # load dataset info self.n_users = dataset.user_num - self.user_embedding = nn.Embedding(self.n_users, self.embedding_size, padding_idx=0) - self.item_embedding = nn.Embedding(self.n_items, self.embedding_size, padding_idx=0) + self.user_embedding = nn.Embedding( + self.n_users, self.embedding_size, padding_idx=0 + ) + self.item_embedding = nn.Embedding( + self.n_items, self.embedding_size, padding_idx=0 + ) self.bias = nn.Embedding(self.n_items, 1, padding_idx=0) # Beta popularity bias - self.T = nn.Parameter(torch.zeros(self.embedding_size)) # average user representation 'global' + self.T = nn.Parameter( + torch.zeros(self.embedding_size) + ) # average user representation 'global' self.bpr_loss = BPRLoss() self.emb_loss = EmbLoss() @@ -118,12 +124,18 @@ def full_sort_predict(self, interaction): seq_output = self.forward(user, item_seq, item_seq_len) # [B H] test_items_emb = self.item_embedding.weight # [item_num H] - test_items_emb = test_items_emb.repeat(seq_output.size(0), 1, 1) # [user_num item_num H] + test_items_emb = test_items_emb.repeat( + seq_output.size(0), 1, 1 + ) # [user_num item_num H] - user_hidden = seq_output.unsqueeze(1).expand_as(test_items_emb) # [user_num item_num H] + user_hidden = seq_output.unsqueeze(1).expand_as( + test_items_emb + ) # [user_num item_num H] test_bias = self.bias.weight # [item_num 1] test_bias = test_bias.repeat(user_hidden.size(0), 1, 1) # [user_num item_num 1] - scores = test_bias - self._l2_distance(user_hidden, test_items_emb) # [user_num item_num 1] + scores = test_bias - self._l2_distance( + user_hidden, test_items_emb + ) # [user_num item_num 1] scores = scores.squeeze(-1) # [B n_items] return scores diff --git a/recbole/quick_start/__init__.py b/recbole/quick_start/__init__.py index 97594b87e..58b937d6a 100644 --- a/recbole/quick_start/__init__.py +++ b/recbole/quick_start/__init__.py @@ -1 +1,6 @@ -from recbole.quick_start.quick_start import run_recbole, objective_function, load_data_and_model, run_recboles +from recbole.quick_start.quick_start import ( + run_recbole, + objective_function, + load_data_and_model, + run_recboles, +) diff --git a/recbole/quick_start/quick_start.py b/recbole/quick_start/quick_start.py index 0aac9a56b..c0beeef3c 100644 --- a/recbole/quick_start/quick_start.py +++ b/recbole/quick_start/quick_start.py @@ -20,12 +20,19 @@ import pickle from recbole.config import Config -from recbole.data import create_dataset, data_preparation, save_split_dataloaders, load_split_dataloaders +from recbole.data import ( + create_dataset, + data_preparation, + save_split_dataloaders, + load_split_dataloaders, +) from recbole.utils import init_logger, get_model, get_trainer, init_seed, set_color -def run_recbole(model=None, dataset=None, config_file_list=None, config_dict=None, saved=True): - r""" A fast running api, which includes the complete process of +def run_recbole( + model=None, dataset=None, config_file_list=None, config_dict=None, saved=True +): + r"""A fast running api, which includes the complete process of training and testing a model on a specified dataset Args: @@ -36,8 +43,13 @@ def run_recbole(model=None, dataset=None, config_file_list=None, config_dict=Non saved (bool, optional): Whether to save the model. Defaults to ``True``. """ # configurations initialization - config = Config(model=model, dataset=dataset, config_file_list=config_file_list, config_dict=config_dict) - init_seed(config['seed'], config['reproducibility']) + config = Config( + model=model, + dataset=dataset, + config_file_list=config_file_list, + config_dict=config_dict, + ) + init_seed(config["seed"], config["reproducibility"]) # logger initialization init_logger(config) logger = getLogger() @@ -52,29 +64,31 @@ def run_recbole(model=None, dataset=None, config_file_list=None, config_dict=Non train_data, valid_data, test_data = data_preparation(config, dataset) # model loading and initialization - init_seed(config['seed'] + config['local_rank'], config['reproducibility']) - model = get_model(config['model'])(config, train_data._dataset).to(config['device']) + init_seed(config["seed"] + config["local_rank"], config["reproducibility"]) + model = get_model(config["model"])(config, train_data._dataset).to(config["device"]) logger.info(model) # trainer loading and initialization - trainer = get_trainer(config['MODEL_TYPE'], config['model'])(config, model) + trainer = get_trainer(config["MODEL_TYPE"], config["model"])(config, model) # model training best_valid_score, best_valid_result = trainer.fit( - train_data, valid_data, saved=saved, show_progress=config['show_progress'] + train_data, valid_data, saved=saved, show_progress=config["show_progress"] ) # model evaluation - test_result = trainer.evaluate(test_data, load_best_model=saved, show_progress=config['show_progress']) + test_result = trainer.evaluate( + test_data, load_best_model=saved, show_progress=config["show_progress"] + ) - logger.info(set_color('best valid ', 'yellow') + f': {best_valid_result}') - logger.info(set_color('test result', 'yellow') + f': {test_result}') + logger.info(set_color("best valid ", "yellow") + f": {best_valid_result}") + logger.info(set_color("test result", "yellow") + f": {test_result}") return { - 'best_valid_score': best_valid_score, - 'valid_score_bigger': config['valid_metric_bigger'], - 'best_valid_result': best_valid_result, - 'test_result': test_result + "best_valid_score": best_valid_score, + "valid_score_bigger": config["valid_metric_bigger"], + "best_valid_result": best_valid_result, + "test_result": test_result, } @@ -82,18 +96,19 @@ def run_recboles(rank, *args): ip, port, world_size, nproc = args[3:] args = args[:3] run_recbole( - *args, config_dict={ - 'local_rank': rank, - 'world_size': world_size, - 'ip': ip, - 'port': port, - 'nproc': nproc - } + *args, + config_dict={ + "local_rank": rank, + "world_size": world_size, + "ip": ip, + "port": port, + "nproc": nproc, + }, ) def objective_function(config_dict=None, config_file_list=None, saved=True): - r""" The default objective_function used in HyperTuning + r"""The default objective_function used in HyperTuning Args: config_dict (dict, optional): Parameters dictionary used to modify experiment parameters. Defaults to ``None``. @@ -102,7 +117,7 @@ def objective_function(config_dict=None, config_file_list=None, saved=True): """ config = Config(config_dict=config_dict, config_file_list=config_file_list) - init_seed(config['seed'], config['reproducibility']) + init_seed(config["seed"], config["reproducibility"]) logger = getLogger() for hdlr in logger.handlers[:]: # remove all old handlers logger.removeHandler(hdlr) @@ -110,17 +125,19 @@ def objective_function(config_dict=None, config_file_list=None, saved=True): logging.basicConfig(level=logging.ERROR) dataset = create_dataset(config) train_data, valid_data, test_data = data_preparation(config, dataset) - init_seed(config['seed'], config['reproducibility']) - model = get_model(config['model'])(config, train_data._dataset).to(config['device']) - trainer = get_trainer(config['MODEL_TYPE'], config['model'])(config, model) - best_valid_score, best_valid_result = trainer.fit(train_data, valid_data, verbose=False, saved=saved) + init_seed(config["seed"], config["reproducibility"]) + model = get_model(config["model"])(config, train_data._dataset).to(config["device"]) + trainer = get_trainer(config["MODEL_TYPE"], config["model"])(config, model) + best_valid_score, best_valid_result = trainer.fit( + train_data, valid_data, verbose=False, saved=saved + ) test_result = trainer.evaluate(test_data, load_best_model=saved) return { - 'best_valid_score': best_valid_score, - 'valid_score_bigger': config['valid_metric_bigger'], - 'best_valid_result': best_valid_result, - 'test_result': test_result + "best_valid_score": best_valid_score, + "valid_score_bigger": config["valid_metric_bigger"], + "best_valid_result": best_valid_result, + "test_result": test_result, } @@ -140,9 +157,10 @@ def load_data_and_model(model_file): - test_data (AbstractDataLoader): The dataloader for testing. """ import torch + checkpoint = torch.load(model_file) - config = checkpoint['config'] - init_seed(config['seed'], config['reproducibility']) + config = checkpoint["config"] + init_seed(config["seed"], config["reproducibility"]) init_logger(config) logger = getLogger() logger.info(config) @@ -151,9 +169,9 @@ def load_data_and_model(model_file): logger.info(dataset) train_data, valid_data, test_data = data_preparation(config, dataset) - init_seed(config['seed'], config['reproducibility']) - model = get_model(config['model'])(config, train_data._dataset).to(config['device']) - model.load_state_dict(checkpoint['state_dict']) - model.load_other_parameter(checkpoint.get('other_parameter')) + init_seed(config["seed"], config["reproducibility"]) + model = get_model(config["model"])(config, train_data._dataset).to(config["device"]) + model.load_state_dict(checkpoint["state_dict"]) + model.load_other_parameter(checkpoint.get("other_parameter")) return config, model, dataset, train_data, valid_data, test_data diff --git a/recbole/sampler/sampler.py b/recbole/sampler/sampler.py index 805226b3d..e62c1b958 100644 --- a/recbole/sampler/sampler.py +++ b/recbole/sampler/sampler.py @@ -34,7 +34,7 @@ class AbstractSampler(object): """ def __init__(self, distribution): - self.distribution = '' + self.distribution = "" self.set_distribution(distribution) self.used_ids = self.get_used_ids() @@ -45,7 +45,7 @@ def set_distribution(self, distribution): distribution (str): Distribution of the negative items. """ self.distribution = distribution - if distribution == 'popularity': + if distribution == "popularity": self._build_alias_table() def _uni_sampling(self, sample_num): @@ -53,11 +53,11 @@ def _uni_sampling(self, sample_num): Args: sample_num (int): the number of samples. - + Returns: - sample_list (np.array): a list of samples. + sample_list (np.array): a list of samples. """ - raise NotImplementedError('Method [_uni_sampling] should be implemented') + raise NotImplementedError("Method [_uni_sampling] should be implemented") def _get_candidates_list(self): """Get sample candidates list for _pop_sampling() @@ -65,11 +65,10 @@ def _get_candidates_list(self): Returns: candidates_list (list): a list of candidates id. """ - raise NotImplementedError('Method [_get_candidates_list] should be implemented') + raise NotImplementedError("Method [_get_candidates_list] should be implemented") def _build_alias_table(self): - """Build alias table for popularity_biased sampling. - """ + """Build alias table for popularity_biased sampling.""" candidates_list = self._get_candidates_list() self.prob = dict(Counter(candidates_list)) self.alias = self.prob.copy() @@ -99,9 +98,9 @@ def _pop_sampling(self, sample_num): Args: sample_num (int): the number of samples. - + Returns: - sample_list (np.array): a list of samples. + sample_list (np.array): a list of samples. """ keys = list(self.prob.keys()) @@ -119,26 +118,28 @@ def _pop_sampling(self, sample_num): def sampling(self, sample_num): """Sampling [sample_num] item_ids. - + Args: sample_num (int): the number of samples. - + Returns: sample_list (np.array): a list of samples and the len is [sample_num]. """ - if self.distribution == 'uniform': + if self.distribution == "uniform": return self._uni_sampling(sample_num) - elif self.distribution == 'popularity': + elif self.distribution == "popularity": return self._pop_sampling(sample_num) else: - raise NotImplementedError(f'The sampling distribution [{self.distribution}] is not implemented.') + raise NotImplementedError( + f"The sampling distribution [{self.distribution}] is not implemented." + ) def get_used_ids(self): """ Returns: numpy.ndarray: Used ids. Index is key_id, and element is a set of value_ids. """ - raise NotImplementedError('Method [get_used_ids] should be implemented') + raise NotImplementedError("Method [get_used_ids] should be implemented") def sample_by_key_ids(self, key_ids, num): """Sampling by key_ids. @@ -172,10 +173,17 @@ def sample_by_key_ids(self, key_ids, num): key_ids = np.tile(key_ids, num) while len(check_list) > 0: value_ids[check_list] = self.sampling(len(check_list)) - check_list = np.array([ - i for i, used, v in zip(check_list, self.used_ids[key_ids[check_list]], value_ids[check_list]) - if v in used - ]) + check_list = np.array( + [ + i + for i, used, v in zip( + check_list, + self.used_ids[key_ids[check_list]], + value_ids[check_list], + ) + if v in used + ] + ) return torch.tensor(value_ids) @@ -194,13 +202,15 @@ class Sampler(AbstractSampler): phase (str): the phase of sampler. It will not be set until :meth:`set_phase` is called. """ - def __init__(self, phases, datasets, distribution='uniform'): + def __init__(self, phases, datasets, distribution="uniform"): if not isinstance(phases, list): phases = [phases] if not isinstance(datasets, list): datasets = [datasets] if len(phases) != len(datasets): - raise ValueError(f'Phases {phases} and datasets {datasets} should have the same length.') + raise ValueError( + f"Phases {phases} and datasets {datasets} should have the same length." + ) self.phases = phases self.datasets = datasets @@ -232,16 +242,19 @@ def get_used_ids(self): last = [set() for _ in range(self.user_num)] for phase, dataset in zip(self.phases, self.datasets): cur = np.array([set(s) for s in last]) - for uid, iid in zip(dataset.inter_feat[self.uid_field].numpy(), dataset.inter_feat[self.iid_field].numpy()): + for uid, iid in zip( + dataset.inter_feat[self.uid_field].numpy(), + dataset.inter_feat[self.iid_field].numpy(), + ): cur[uid].add(iid) last = used_item_id[phase] = cur for used_item_set in used_item_id[self.phases[-1]]: if len(used_item_set) + 1 == self.item_num: # [pad] is a item. raise ValueError( - 'Some users have interacted with all items, ' - 'which we can not sample negative items for them. ' - 'Please set `user_inter_num_interval` to filter those users.' + "Some users have interacted with all items, " + "which we can not sample negative items for them. " + "Please set `user_inter_num_interval` to filter those users." ) return used_item_id @@ -256,7 +269,7 @@ def set_phase(self, phase): is set to the value of corresponding phase. """ if phase not in self.phases: - raise ValueError(f'Phase [{phase}] not exist.') + raise ValueError(f"Phase [{phase}] not exist.") new_sampler = copy.copy(self) new_sampler.phase = phase new_sampler.used_ids = new_sampler.used_ids[phase] @@ -282,7 +295,7 @@ def sample_by_user_ids(self, user_ids, item_ids, num): except IndexError: for user_id in user_ids: if user_id < 0 or user_id >= self.user_num: - raise ValueError(f'user_id [{user_id}] not exist.') + raise ValueError(f"user_id [{user_id}] not exist.") class KGSampler(AbstractSampler): @@ -293,7 +306,7 @@ class KGSampler(AbstractSampler): distribution (str, optional): Distribution of the negative entities. Defaults to 'uniform'. """ - def __init__(self, dataset, distribution='uniform'): + def __init__(self, dataset, distribution="uniform"): self.dataset = dataset self.hid_field = dataset.head_entity_field @@ -325,8 +338,8 @@ def get_used_ids(self): for used_tail_set in used_tail_entity_id: if len(used_tail_set) + 1 == self.entity_num: # [pad] is a entity. raise ValueError( - 'Some head entities have relation with all entities, ' - 'which we can not sample negative entities for them.' + "Some head entities have relation with all entities, " + "which we can not sample negative entities for them." ) return used_tail_entity_id @@ -349,7 +362,7 @@ def sample_by_entity_ids(self, head_entity_ids, num=1): except IndexError: for head_entity_id in head_entity_ids: if head_entity_id not in self.head_entities: - raise ValueError(f'head_entity_id [{head_entity_id}] not exist.') + raise ValueError(f"head_entity_id [{head_entity_id}] not exist.") class RepeatableSampler(AbstractSampler): @@ -365,7 +378,7 @@ class RepeatableSampler(AbstractSampler): phase (str): the phase of sampler. It will not be set until :meth:`set_phase` is called. """ - def __init__(self, phases, dataset, distribution='uniform'): + def __init__(self, phases, dataset, distribution="uniform"): if not isinstance(phases, list): phases = [phases] self.phases = phases @@ -412,7 +425,7 @@ def sample_by_user_ids(self, user_ids, item_ids, num): except IndexError: for user_id in user_ids: if user_id < 0 or user_id >= self.user_num: - raise ValueError(f'user_id [{user_id}] not exist.') + raise ValueError(f"user_id [{user_id}] not exist.") def set_phase(self, phase): """Get the sampler of corresponding phase. @@ -424,7 +437,7 @@ def set_phase(self, phase): Sampler: the copy of this sampler, and :attr:`phase` is set the same as input phase. """ if phase not in self.phases: - raise ValueError(f'Phase [{phase}] not exist.') + raise ValueError(f"Phase [{phase}] not exist.") new_sampler = copy.copy(self) new_sampler.phase = phase return new_sampler @@ -433,12 +446,12 @@ def set_phase(self, phase): class SeqSampler(AbstractSampler): """:class:`SeqSampler` is used to sample negative item sequence. - Args: - datasets (Dataset or list of Dataset): All the dataset for each phase. - distribution (str, optional): Distribution of the negative items. Defaults to 'uniform'. + Args: + datasets (Dataset or list of Dataset): All the dataset for each phase. + distribution (str, optional): Distribution of the negative items. Defaults to 'uniform'. """ - def __init__(self, dataset, distribution='uniform'): + def __init__(self, dataset, distribution="uniform"): self.dataset = dataset self.iid_field = dataset.iid_field diff --git a/recbole/trainer/__init__.py b/recbole/trainer/__init__.py index 2707e86f1..677db7c97 100644 --- a/recbole/trainer/__init__.py +++ b/recbole/trainer/__init__.py @@ -1,4 +1,4 @@ from recbole.trainer.hyper_tuning import HyperTuning from recbole.trainer.trainer import * -__all__ = ['Trainer', 'KGTrainer', 'KGATTrainer', 'S3RecTrainer'] +__all__ = ["Trainer", "KGTrainer", "KGATTrainer", "S3RecTrainer"] diff --git a/recbole/trainer/hyper_tuning.py b/recbole/trainer/hyper_tuning.py index 2ef2aebe0..1975ee61a 100644 --- a/recbole/trainer/hyper_tuning.py +++ b/recbole/trainer/hyper_tuning.py @@ -21,8 +21,9 @@ from recbole.utils.utils import dict2str -def _recursiveFindNodes(root, node_type='switch'): +def _recursiveFindNodes(root, node_type="switch"): from hyperopt.pyll.base import Apply + nodes = [] if isinstance(root, (list, tuple)): for node in root: @@ -48,10 +49,10 @@ def _parameters(space): parameters = {} if isinstance(space, dict): space = list(space.values()) - for node in _recursiveFindNodes(space, 'switch'): + for node in _recursiveFindNodes(space, "switch"): # Find the name of this parameter paramNode = node.pos_args[0] - assert paramNode.name == 'hyperopt_param' + assert paramNode.name == "hyperopt_param" paramName = paramNode.pos_args[0].obj # Find all possible choices for this parameter @@ -67,38 +68,50 @@ def _spacesize(space): class ExhaustiveSearchError(Exception): - r""" ExhaustiveSearchError - - """ + r"""ExhaustiveSearchError""" pass def _validate_space_exhaustive_search(space): from hyperopt.pyll.base import dfs, as_apply from hyperopt.pyll.stochastic import implicit_stochastic_symbols - supported_stochastic_symbols = ['randint', 'quniform', 'qloguniform', 'qnormal', 'qlognormal', 'categorical'] + + supported_stochastic_symbols = [ + "randint", + "quniform", + "qloguniform", + "qnormal", + "qlognormal", + "categorical", + ] for node in dfs(as_apply(space)): if node.name in implicit_stochastic_symbols: if node.name not in supported_stochastic_symbols: raise ExhaustiveSearchError( - 'Exhaustive search is only possible with the following stochastic symbols: ' - '' + ', '.join(supported_stochastic_symbols) + "Exhaustive search is only possible with the following stochastic symbols: " + "" + ", ".join(supported_stochastic_symbols) ) def exhaustive_search(new_ids, domain, trials, seed, nbMaxSucessiveFailures=1000): - r""" This is for exhaustive search in HyperTuning. - - """ + r"""This is for exhaustive search in HyperTuning.""" from hyperopt import pyll from hyperopt.base import miscs_update_idxs_vals + # Build a hash set for previous trials - hashset = set([ - hash( - frozenset([(key, value[0]) if len(value) > 0 else ((key, None)) - for key, value in trial['misc']['vals'].items()]) - ) for trial in trials.trials - ]) + hashset = set( + [ + hash( + frozenset( + [ + (key, value[0]) if len(value) > 0 else ((key, None)) + for key, value in trial["misc"]["vals"].items() + ] + ) + ) + for trial in trials.trials + ] + ) rng = np.random.RandomState(seed) rval = [] @@ -107,16 +120,26 @@ def exhaustive_search(new_ids, domain, trials, seed, nbMaxSucessiveFailures=1000 nbSucessiveFailures = 0 while not newSample: # -- sample new specs, idxs, vals - idxs, vals = pyll.rec_eval(domain.s_idxs_vals, memo={ - domain.s_new_ids: [new_id], - domain.s_rng: rng, - }) + idxs, vals = pyll.rec_eval( + domain.s_idxs_vals, + memo={ + domain.s_new_ids: [new_id], + domain.s_rng: rng, + }, + ) new_result = domain.new_result() new_misc = dict(tid=new_id, cmd=domain.cmd, workdir=domain.workdir) miscs_update_idxs_vals([new_misc], idxs, vals) # Compare with previous hashes - h = hash(frozenset([(key, value[0]) if len(value) > 0 else ((key, None)) for key, value in vals.items()])) + h = hash( + frozenset( + [ + (key, value[0]) if len(value) > 0 else ((key, None)) + for key, value in vals.items() + ] + ) + ) if h not in hashset: newSample = True else: @@ -150,8 +173,8 @@ def __init__( params_file=None, params_dict=None, fixed_config_file_list=None, - algo='exhaustive', - max_evals=100 + algo="exhaustive", + max_evals=100, ): self.best_score = None self.best_params = None @@ -170,109 +193,129 @@ def __init__( elif params_dict: self.space = self._build_space_from_dict(params_dict) else: - raise ValueError('at least one of `space`, `params_file` and `params_dict` is provided') + raise ValueError( + "at least one of `space`, `params_file` and `params_dict` is provided" + ) if isinstance(algo, str): - if algo == 'exhaustive': + if algo == "exhaustive": self.algo = partial(exhaustive_search, nbMaxSucessiveFailures=1000) self.max_evals = _spacesize(self.space) else: - raise ValueError('Illegal algo [{}]'.format(algo)) + raise ValueError("Illegal algo [{}]".format(algo)) else: self.algo = algo @staticmethod def _build_space_from_file(file): from hyperopt import hp + space = {} - with open(file, 'r') as fp: + with open(file, "r") as fp: for line in fp: - para_list = line.strip().split(' ') + para_list = line.strip().split(" ") if len(para_list) < 3: continue - para_name, para_type, para_value = para_list[0], para_list[1], "".join(para_list[2:]) - if para_type == 'choice': + para_name, para_type, para_value = ( + para_list[0], + para_list[1], + "".join(para_list[2:]), + ) + if para_type == "choice": para_value = eval(para_value) space[para_name] = hp.choice(para_name, para_value) - elif para_type == 'uniform': - low, high = para_value.strip().split(',') + elif para_type == "uniform": + low, high = para_value.strip().split(",") space[para_name] = hp.uniform(para_name, float(low), float(high)) - elif para_type == 'quniform': - low, high, q = para_value.strip().split(',') - space[para_name] = hp.quniform(para_name, float(low), float(high), float(q)) - elif para_type == 'loguniform': - low, high = para_value.strip().split(',') + elif para_type == "quniform": + low, high, q = para_value.strip().split(",") + space[para_name] = hp.quniform( + para_name, float(low), float(high), float(q) + ) + elif para_type == "loguniform": + low, high = para_value.strip().split(",") space[para_name] = hp.loguniform(para_name, float(low), float(high)) else: - raise ValueError('Illegal param type [{}]'.format(para_type)) + raise ValueError("Illegal param type [{}]".format(para_type)) return space @staticmethod def _build_space_from_dict(config_dict): from hyperopt import hp + space = {} for para_type in config_dict: - if para_type == 'choice': - for para_name in config_dict['choice']: - para_value = config_dict['choice'][para_name] + if para_type == "choice": + for para_name in config_dict["choice"]: + para_value = config_dict["choice"][para_name] space[para_name] = hp.choice(para_name, para_value) - elif para_type == 'uniform': - for para_name in config_dict['uniform']: - para_value = config_dict['uniform'][para_name] + elif para_type == "uniform": + for para_name in config_dict["uniform"]: + para_value = config_dict["uniform"][para_name] low = para_value[0] high = para_value[1] space[para_name] = hp.uniform(para_name, float(low), float(high)) - elif para_type == 'quniform': - for para_name in config_dict['quniform']: - para_value = config_dict['quniform'][para_name] + elif para_type == "quniform": + for para_name in config_dict["quniform"]: + para_value = config_dict["quniform"][para_name] low = para_value[0] high = para_value[1] q = para_value[2] - space[para_name] = hp.quniform(para_name, float(low), float(high), float(q)) - elif para_type == 'loguniform': - for para_name in config_dict['loguniform']: - para_value = config_dict['loguniform'][para_name] + space[para_name] = hp.quniform( + para_name, float(low), float(high), float(q) + ) + elif para_type == "loguniform": + for para_name in config_dict["loguniform"]: + para_value = config_dict["loguniform"][para_name] low = para_value[0] high = para_value[1] space[para_name] = hp.loguniform(para_name, float(low), float(high)) else: - raise ValueError('Illegal param type [{}]'.format(para_type)) + raise ValueError("Illegal param type [{}]".format(para_type)) return space @staticmethod def params2str(params): - r""" convert dict to str + r"""convert dict to str Args: params (dict): parameters dict Returns: str: parameters string """ - params_str = '' + params_str = "" for param_name in params: - params_str += param_name + ':' + str(params[param_name]) + ', ' + params_str += param_name + ":" + str(params[param_name]) + ", " return params_str[:-2] @staticmethod def _print_result(result_dict: dict): - print('current best valid score: %.4f' % result_dict['best_valid_score']) - print('current best valid result:') - print(result_dict['best_valid_result']) - print('current test result:') - print(result_dict['test_result']) + print("current best valid score: %.4f" % result_dict["best_valid_score"]) + print("current best valid result:") + print(result_dict["best_valid_result"]) + print("current test result:") + print(result_dict["test_result"]) print() def export_result(self, output_file=None): - r""" Write the searched parameters and corresponding results to the file + r"""Write the searched parameters and corresponding results to the file Args: output_file (str): the output file """ - with open(output_file, 'w') as fp: + with open(output_file, "w") as fp: for params in self.params2result: - fp.write(params + '\n') - fp.write('Valid result:\n' + dict2str(self.params2result[params]['best_valid_result']) + '\n') - fp.write('Test result:\n' + dict2str(self.params2result[params]['test_result']) + '\n\n') + fp.write(params + "\n") + fp.write( + "Valid result:\n" + + dict2str(self.params2result[params]["best_valid_result"]) + + "\n" + ) + fp.write( + "Test result:\n" + + dict2str(self.params2result[params]["test_result"]) + + "\n\n" + ) def trial(self, params): r"""Given a set of parameters, return results and optimization status @@ -281,13 +324,17 @@ def trial(self, params): params (dict): the parameter dictionary """ import hyperopt + config_dict = params.copy() params_str = self.params2str(params) self.params_list.append(params_str) - print('running parameters:', config_dict) + print("running parameters:", config_dict) result_dict = self.objective_function(config_dict, self.fixed_config_file_list) self.params2result[params_str] = result_dict - score, bigger = result_dict['best_valid_score'], result_dict['valid_score_bigger'] + score, bigger = ( + result_dict["best_valid_score"], + result_dict["valid_score_bigger"], + ) self.score_list.append(score) if not self.best_score: @@ -308,42 +355,42 @@ def trial(self, params): if bigger: score = -score - return {'loss': score, 'status': hyperopt.STATUS_OK} + return {"loss": score, "status": hyperopt.STATUS_OK} def plot_hyper(self): import plotly.graph_objs as go from plotly.offline import plot import pandas as pd - data_dict = {'valid_score': self.score_list, 'params': self.params_list} + + data_dict = {"valid_score": self.score_list, "params": self.params_list} trial_df = pd.DataFrame(data_dict) - trial_df['trial_number'] = trial_df.index + 1 - trial_df['trial_number'] = trial_df['trial_number'].astype(dtype=np.str) + trial_df["trial_number"] = trial_df.index + 1 + trial_df["trial_number"] = trial_df["trial_number"].astype(dtype=np.str) trace = go.Scatter( - x=trial_df['trial_number'], - y=trial_df['valid_score'], - text=trial_df['params'], - mode='lines+markers', - marker=dict( - color='green' - ), - showlegend=True, - textposition='top center', - name='tuning process' - ) + x=trial_df["trial_number"], + y=trial_df["valid_score"], + text=trial_df["params"], + mode="lines+markers", + marker=dict(color="green"), + showlegend=True, + textposition="top center", + name="tuning process", + ) data = [trace] - layout = go.Layout(title='hyperparams_tuning', - xaxis=dict(title='trials'), - yaxis=dict(title='valid_score')) + layout = go.Layout( + title="hyperparams_tuning", + xaxis=dict(title="trials"), + yaxis=dict(title="valid_score"), + ) fig = go.Figure(data=data, layout=layout) - plot(fig, filename='hyperparams_tuning.html') + plot(fig, filename="hyperparams_tuning.html") def run(self): - r""" begin to search the best parameters - - """ + r"""begin to search the best parameters""" from hyperopt import fmin + fmin(self.trial, self.space, algo=self.algo, max_evals=self.max_evals) self.plot_hyper() diff --git a/recbole/trainer/trainer.py b/recbole/trainer/trainer.py index 7ef509ba5..4c8653653 100644 --- a/recbole/trainer/trainer.py +++ b/recbole/trainer/trainer.py @@ -30,13 +30,24 @@ import torch.optim as optim from torch.nn.utils.clip_grad import clip_grad_norm_ from tqdm import tqdm -import torch.cuda.amp as amp +import torch.cuda.amp as amp from recbole.data.interaction import Interaction from recbole.data.dataloader import FullSortEvalDataLoader from recbole.evaluator import Evaluator, Collector -from recbole.utils import ensure_dir, get_local_time, early_stopping, calculate_valid_score, dict2str, \ - EvaluatorType, KGDataLoaderState, get_tensorboard, set_color, get_gpu_usage, WandbLogger +from recbole.utils import ( + ensure_dir, + get_local_time, + early_stopping, + calculate_valid_score, + dict2str, + EvaluatorType, + KGDataLoaderState, + get_tensorboard, + set_color, + get_gpu_usage, + WandbLogger, +) from torch.nn.parallel import DistributedDataParallel @@ -49,36 +60,34 @@ class AbstractTrainer(object): def __init__(self, config, model): self.config = config self.model = model - if not config['single_spec']: + if not config["single_spec"]: self.model = torch.nn.SyncBatchNorm.convert_sync_batchnorm(model) - self.distributed_model = DistributedDataParallel(self.model , device_ids=[config['local_rank']]) + self.distributed_model = DistributedDataParallel( + self.model, device_ids=[config["local_rank"]] + ) def fit(self, train_data): - r"""Train the model based on the train data. - - """ - raise NotImplementedError('Method [next] should be implemented.') + r"""Train the model based on the train data.""" + raise NotImplementedError("Method [next] should be implemented.") def evaluate(self, eval_data): - r"""Evaluate the model based on the eval data. + r"""Evaluate the model based on the eval data.""" - """ + raise NotImplementedError("Method [next] should be implemented.") - raise NotImplementedError('Method [next] should be implemented.') - def set_reduce_hook(self): r"""Call the forward function of 'distributed_model' to apply grads reduce hook to each parameter of its module. """ t = self.model.forward - self.model.forward = lambda x : x + self.model.forward = lambda x: x self.distributed_model(torch.LongTensor([0]).to(self.device)) self.model.forward = t - + def sync_grad_loss(self): r"""Ensure that each parameter appears to the loss function to - make the grads reduce sync in each node. + make the grads reduce sync in each node. """ sync_loss = 0 @@ -86,6 +95,7 @@ def sync_grad_loss(self): sync_loss += torch.sum(params) * 0 return sync_loss + class Trainer(AbstractTrainer): r"""The basic Trainer for basic training and evaluation strategies in recommender systems. This class defines common functions for training and evaluation processes of most recommender system models, including fit(), evaluate(), @@ -107,24 +117,24 @@ def __init__(self, config, model): self.logger = getLogger() self.tensorboard = get_tensorboard(self.logger) self.wandblogger = WandbLogger(config) - self.learner = config['learner'] - self.learning_rate = config['learning_rate'] - self.epochs = config['epochs'] - self.eval_step = min(config['eval_step'], self.epochs) - self.stopping_step = config['stopping_step'] - self.clip_grad_norm = config['clip_grad_norm'] - self.valid_metric = config['valid_metric'].lower() - self.valid_metric_bigger = config['valid_metric_bigger'] - self.test_batch_size = config['eval_batch_size'] - self.gpu_available = torch.cuda.is_available() and config['use_gpu'] - self.device = config['device'] - self.checkpoint_dir = config['checkpoint_dir'] - self.enable_amp=config['enable_amp'] - self.enable_scaler=torch.cuda.is_available() and config['enable_scaler'] + self.learner = config["learner"] + self.learning_rate = config["learning_rate"] + self.epochs = config["epochs"] + self.eval_step = min(config["eval_step"], self.epochs) + self.stopping_step = config["stopping_step"] + self.clip_grad_norm = config["clip_grad_norm"] + self.valid_metric = config["valid_metric"].lower() + self.valid_metric_bigger = config["valid_metric_bigger"] + self.test_batch_size = config["eval_batch_size"] + self.gpu_available = torch.cuda.is_available() and config["use_gpu"] + self.device = config["device"] + self.checkpoint_dir = config["checkpoint_dir"] + self.enable_amp = config["enable_amp"] + self.enable_scaler = torch.cuda.is_available() and config["enable_scaler"] ensure_dir(self.checkpoint_dir) - saved_model_file = '{}-{}.pth'.format(self.config['model'], get_local_time()) + saved_model_file = "{}-{}.pth".format(self.config["model"], get_local_time()) self.saved_model_file = os.path.join(self.checkpoint_dir, saved_model_file) - self.weight_decay = config['weight_decay'] + self.weight_decay = config["weight_decay"] self.start_epoch = 0 self.cur_step = 0 @@ -132,7 +142,7 @@ def __init__(self, config, model): self.best_valid_result = None self.train_loss_dict = dict() self.optimizer = self._build_optimizer() - self.eval_type = config['eval_type'] + self.eval_type = config["eval_type"] self.eval_collector = Collector(config) self.evaluator = Evaluator(config) self.item_tensor = None @@ -151,31 +161,43 @@ def _build_optimizer(self, **kwargs): Returns: torch.optim: the optimizer """ - params = kwargs.pop('params', self.model.parameters()) - learner = kwargs.pop('learner', self.learner) - learning_rate = kwargs.pop('learning_rate', self.learning_rate) - weight_decay = kwargs.pop('weight_decay', self.weight_decay) - - if self.config['reg_weight'] and weight_decay and weight_decay * self.config['reg_weight'] > 0: + params = kwargs.pop("params", self.model.parameters()) + learner = kwargs.pop("learner", self.learner) + learning_rate = kwargs.pop("learning_rate", self.learning_rate) + weight_decay = kwargs.pop("weight_decay", self.weight_decay) + + if ( + self.config["reg_weight"] + and weight_decay + and weight_decay * self.config["reg_weight"] > 0 + ): self.logger.warning( - 'The parameters [weight_decay] and [reg_weight] are specified simultaneously, ' - 'which may lead to double regularization.' + "The parameters [weight_decay] and [reg_weight] are specified simultaneously, " + "which may lead to double regularization." ) - if learner.lower() == 'adam': + if learner.lower() == "adam": optimizer = optim.Adam(params, lr=learning_rate, weight_decay=weight_decay) - elif learner.lower() == 'sgd': + elif learner.lower() == "sgd": optimizer = optim.SGD(params, lr=learning_rate, weight_decay=weight_decay) - elif learner.lower() == 'adagrad': - optimizer = optim.Adagrad(params, lr=learning_rate, weight_decay=weight_decay) - elif learner.lower() == 'rmsprop': - optimizer = optim.RMSprop(params, lr=learning_rate, weight_decay=weight_decay) - elif learner.lower() == 'sparse_adam': + elif learner.lower() == "adagrad": + optimizer = optim.Adagrad( + params, lr=learning_rate, weight_decay=weight_decay + ) + elif learner.lower() == "rmsprop": + optimizer = optim.RMSprop( + params, lr=learning_rate, weight_decay=weight_decay + ) + elif learner.lower() == "sparse_adam": optimizer = optim.SparseAdam(params, lr=learning_rate) if weight_decay > 0: - self.logger.warning('Sparse Adam cannot argument received argument [{weight_decay}]') + self.logger.warning( + "Sparse Adam cannot argument received argument [{weight_decay}]" + ) else: - self.logger.warning('Received unrecognized optimizer, set default Adam optimizer') + self.logger.warning( + "Received unrecognized optimizer, set default Adam optimizer" + ) optimizer = optim.Adam(params, lr=learning_rate) return optimizer @@ -202,11 +224,13 @@ def _train_epoch(self, train_data, epoch_idx, loss_func=None, show_progress=Fals train_data, total=len(train_data), ncols=100, - desc=set_color(f"Train {epoch_idx:>5}", 'pink'), - ) if show_progress else train_data + desc=set_color(f"Train {epoch_idx:>5}", "pink"), + ) + if show_progress + else train_data ) - - if not self.config['single_spec'] and train_data.shuffle: + + if not self.config["single_spec"] and train_data.shuffle: train_data.sampler.set_epoch(epoch_idx) scaler = amp.GradScaler(enabled=self.enable_scaler) @@ -214,20 +238,26 @@ def _train_epoch(self, train_data, epoch_idx, loss_func=None, show_progress=Fals interaction = interaction.to(self.device) self.optimizer.zero_grad() sync_loss = 0 - if not self.config['single_spec']: + if not self.config["single_spec"]: self.set_reduce_hook() sync_loss = self.sync_grad_loss() - + with torch.autocast(device_type=self.device.type, enabled=self.enable_amp): losses = loss_func(interaction) if isinstance(losses, tuple): loss = sum(losses) loss_tuple = tuple(per_loss.item() for per_loss in losses) - total_loss = loss_tuple if total_loss is None else tuple(map(sum, zip(total_loss, loss_tuple))) + total_loss = ( + loss_tuple + if total_loss is None + else tuple(map(sum, zip(total_loss, loss_tuple))) + ) else: loss = losses - total_loss = losses.item() if total_loss is None else total_loss + losses.item() + total_loss = ( + losses.item() if total_loss is None else total_loss + losses.item() + ) self._check_nan(loss) scaler.scale(loss + sync_loss).backward() if self.clip_grad_norm: @@ -235,7 +265,9 @@ def _train_epoch(self, train_data, epoch_idx, loss_func=None, show_progress=Fals scaler.step(self.optimizer) scaler.update() if self.gpu_available and show_progress: - iter_data.set_postfix_str(set_color('GPU RAM: ' + get_gpu_usage(self.device), 'yellow')) + iter_data.set_postfix_str( + set_color("GPU RAM: " + get_gpu_usage(self.device), "yellow") + ) return total_loss def _valid_epoch(self, valid_data, show_progress=False): @@ -249,7 +281,9 @@ def _valid_epoch(self, valid_data, show_progress=False): float: valid score dict: valid result """ - valid_result = self.evaluate(valid_data, load_best_model=False, show_progress=show_progress) + valid_result = self.evaluate( + valid_data, load_best_model=False, show_progress=show_progress + ) valid_score = calculate_valid_score(valid_result, self.valid_metric) return valid_score, valid_result @@ -260,21 +294,23 @@ def _save_checkpoint(self, epoch, verbose=True, **kwargs): epoch (int): the current epoch id """ - if not self.config['single_spec'] and self.config['local_rank'] != 0: + if not self.config["single_spec"] and self.config["local_rank"] != 0: return - saved_model_file = kwargs.pop('saved_model_file', self.saved_model_file) + saved_model_file = kwargs.pop("saved_model_file", self.saved_model_file) state = { - 'config': self.config, - 'epoch': epoch, - 'cur_step': self.cur_step, - 'best_valid_score': self.best_valid_score, - 'state_dict': self.model.state_dict(), - 'other_parameter': self.model.other_parameter(), - 'optimizer': self.optimizer.state_dict(), + "config": self.config, + "epoch": epoch, + "cur_step": self.cur_step, + "best_valid_score": self.best_valid_score, + "state_dict": self.model.state_dict(), + "other_parameter": self.model.other_parameter(), + "optimizer": self.optimizer.state_dict(), } torch.save(state, saved_model_file) if verbose: - self.logger.info(set_color('Saving current', 'blue') + f': {saved_model_file}') + self.logger.info( + set_color("Saving current", "blue") + f": {saved_model_file}" + ) def resume_checkpoint(self, resume_file): r"""Load the model parameters information and training information. @@ -286,41 +322,49 @@ def resume_checkpoint(self, resume_file): resume_file = str(resume_file) self.saved_model_file = resume_file checkpoint = torch.load(resume_file, map_location=self.device) - self.start_epoch = checkpoint['epoch'] + 1 - self.cur_step = checkpoint['cur_step'] - self.best_valid_score = checkpoint['best_valid_score'] + self.start_epoch = checkpoint["epoch"] + 1 + self.cur_step = checkpoint["cur_step"] + self.best_valid_score = checkpoint["best_valid_score"] # load architecture params from checkpoint - if checkpoint['config']['model'].lower() != self.config['model'].lower(): + if checkpoint["config"]["model"].lower() != self.config["model"].lower(): self.logger.warning( - 'Architecture configuration given in config file is different from that of checkpoint. ' - 'This may yield an exception while state_dict is being loaded.' + "Architecture configuration given in config file is different from that of checkpoint. " + "This may yield an exception while state_dict is being loaded." ) - self.model.load_state_dict(checkpoint['state_dict']) - self.model.load_other_parameter(checkpoint.get('other_parameter')) + self.model.load_state_dict(checkpoint["state_dict"]) + self.model.load_other_parameter(checkpoint.get("other_parameter")) # load optimizer state from checkpoint only when optimizer type is not changed - self.optimizer.load_state_dict(checkpoint['optimizer']) - message_output = 'Checkpoint loaded. Resume training from epoch {}'.format(self.start_epoch) + self.optimizer.load_state_dict(checkpoint["optimizer"]) + message_output = "Checkpoint loaded. Resume training from epoch {}".format( + self.start_epoch + ) self.logger.info(message_output) def _check_nan(self, loss): if torch.isnan(loss): - raise ValueError('Training loss is nan') + raise ValueError("Training loss is nan") def _generate_train_loss_output(self, epoch_idx, s_time, e_time, losses): - des = self.config['loss_decimal_place'] or 4 - train_loss_output = (set_color('epoch %d training', 'green') + ' [' + set_color('time', 'blue') + - ': %.2fs, ') % (epoch_idx, e_time - s_time) + des = self.config["loss_decimal_place"] or 4 + train_loss_output = ( + set_color("epoch %d training", "green") + + " [" + + set_color("time", "blue") + + ": %.2fs, " + ) % (epoch_idx, e_time - s_time) if isinstance(losses, tuple): - des = (set_color('train_loss%d', 'blue') + ': %.' + str(des) + 'f') - train_loss_output += ', '.join(des % (idx + 1, loss) for idx, loss in enumerate(losses)) + des = set_color("train_loss%d", "blue") + ": %." + str(des) + "f" + train_loss_output += ", ".join( + des % (idx + 1, loss) for idx, loss in enumerate(losses) + ) else: - des = '%.' + str(des) + 'f' - train_loss_output += set_color('train loss', 'blue') + ': ' + des % losses - return train_loss_output + ']' + des = "%." + str(des) + "f" + train_loss_output += set_color("train loss", "blue") + ": " + des % losses + return train_loss_output + "]" - def _add_train_loss_to_tensorboard(self, epoch_idx, losses, tag='Loss/Train'): + def _add_train_loss_to_tensorboard(self, epoch_idx, losses, tag="Loss/Train"): if isinstance(losses, tuple): for idx, loss in enumerate(losses): self.tensorboard.add_scalar(tag + str(idx), loss, epoch_idx) @@ -330,27 +374,43 @@ def _add_train_loss_to_tensorboard(self, epoch_idx, losses, tag='Loss/Train'): def _add_hparam_to_tensorboard(self, best_valid_result): # base hparam hparam_dict = { - 'learner': self.config['learner'], - 'learning_rate': self.config['learning_rate'], - 'train_batch_size': self.config['train_batch_size'] + "learner": self.config["learner"], + "learning_rate": self.config["learning_rate"], + "train_batch_size": self.config["train_batch_size"], } # unrecorded parameter unrecorded_parameter = { parameter - for parameters in self.config.parameters.values() for parameter in parameters - }.union({'model', 'dataset', 'config_files', 'device'}) + for parameters in self.config.parameters.values() + for parameter in parameters + }.union({"model", "dataset", "config_files", "device"}) # other model-specific hparam - hparam_dict.update({ - para: val - for para, val in self.config.final_config_dict.items() if para not in unrecorded_parameter - }) + hparam_dict.update( + { + para: val + for para, val in self.config.final_config_dict.items() + if para not in unrecorded_parameter + } + ) for k in hparam_dict: - if hparam_dict[k] is not None and not isinstance(hparam_dict[k], (bool, str, float, int)): + if hparam_dict[k] is not None and not isinstance( + hparam_dict[k], (bool, str, float, int) + ): hparam_dict[k] = str(hparam_dict[k]) - self.tensorboard.add_hparams(hparam_dict, {'hparam/best_valid_result': best_valid_result}) + self.tensorboard.add_hparams( + hparam_dict, {"hparam/best_valid_result": best_valid_result} + ) - def fit(self, train_data, valid_data=None, verbose=True, saved=True, show_progress=False, callback_fn=None): + def fit( + self, + train_data, + valid_data=None, + verbose=True, + saved=True, + show_progress=False, + callback_fn=None, + ): r"""Train the model based on the train data and the valid data. Args: @@ -370,22 +430,30 @@ def fit(self, train_data, valid_data=None, verbose=True, saved=True, show_progre self._save_checkpoint(-1, verbose=verbose) self.eval_collector.data_collect(train_data) - if self.config['train_neg_sample_args'].get('dynamic', False): + if self.config["train_neg_sample_args"].get("dynamic", False): train_data.get_model(self.model) valid_step = 0 for epoch_idx in range(self.start_epoch, self.epochs): # train training_start_time = time() - train_loss = self._train_epoch(train_data, epoch_idx, show_progress=show_progress) - self.train_loss_dict[epoch_idx] = sum(train_loss) if isinstance(train_loss, tuple) else train_loss + train_loss = self._train_epoch( + train_data, epoch_idx, show_progress=show_progress + ) + self.train_loss_dict[epoch_idx] = ( + sum(train_loss) if isinstance(train_loss, tuple) else train_loss + ) training_end_time = time() - train_loss_output = \ - self._generate_train_loss_output(epoch_idx, training_start_time, training_end_time, train_loss) + train_loss_output = self._generate_train_loss_output( + epoch_idx, training_start_time, training_end_time, train_loss + ) if verbose: self.logger.info(train_loss_output) self._add_train_loss_to_tensorboard(epoch_idx, train_loss) - self.wandblogger.log_metrics({'epoch': epoch_idx, 'train_loss': train_loss, 'train_step':epoch_idx}, head='train') + self.wandblogger.log_metrics( + {"epoch": epoch_idx, "train_loss": train_loss, "train_step": epoch_idx}, + head="train", + ) # eval if self.eval_step <= 0 or not valid_data: @@ -394,24 +462,40 @@ def fit(self, train_data, valid_data=None, verbose=True, saved=True, show_progre continue if (epoch_idx + 1) % self.eval_step == 0: valid_start_time = time() - valid_score, valid_result = self._valid_epoch(valid_data, show_progress=show_progress) - self.best_valid_score, self.cur_step, stop_flag, update_flag = early_stopping( + valid_score, valid_result = self._valid_epoch( + valid_data, show_progress=show_progress + ) + ( + self.best_valid_score, + self.cur_step, + stop_flag, + update_flag, + ) = early_stopping( valid_score, self.best_valid_score, self.cur_step, max_step=self.stopping_step, - bigger=self.valid_metric_bigger + bigger=self.valid_metric_bigger, ) valid_end_time = time() - valid_score_output = (set_color("epoch %d evaluating", 'green') + " [" + set_color("time", 'blue') - + ": %.2fs, " + set_color("valid_score", 'blue') + ": %f]") % \ - (epoch_idx, valid_end_time - valid_start_time, valid_score) - valid_result_output = set_color('valid result', 'blue') + ': \n' + dict2str(valid_result) + valid_score_output = ( + set_color("epoch %d evaluating", "green") + + " [" + + set_color("time", "blue") + + ": %.2fs, " + + set_color("valid_score", "blue") + + ": %f]" + ) % (epoch_idx, valid_end_time - valid_start_time, valid_score) + valid_result_output = ( + set_color("valid result", "blue") + ": \n" + dict2str(valid_result) + ) if verbose: self.logger.info(valid_score_output) self.logger.info(valid_result_output) - self.tensorboard.add_scalar('Vaild_score', valid_score, epoch_idx) - self.wandblogger.log_metrics({**valid_result, 'valid_step': valid_step}, head='valid') + self.tensorboard.add_scalar("Vaild_score", valid_score, epoch_idx) + self.wandblogger.log_metrics( + {**valid_result, "valid_step": valid_step}, head="valid" + ) if update_flag: if saved: @@ -422,13 +506,14 @@ def fit(self, train_data, valid_data=None, verbose=True, saved=True, show_progre callback_fn(epoch_idx, valid_score) if stop_flag: - stop_output = 'Finished training, best eval result in epoch %d' % \ - (epoch_idx - self.cur_step * self.eval_step) + stop_output = "Finished training, best eval result in epoch %d" % ( + epoch_idx - self.cur_step * self.eval_step + ) if verbose: self.logger.info(stop_output) break - valid_step+=1 + valid_step += 1 self._add_hparam_to_tensorboard(self.best_valid_score) return self.best_valid_score, self.best_valid_result @@ -462,17 +547,21 @@ def _neg_sample_batch_eval(self, batched_data): else: origin_scores = self._spilt_predict(interaction, batch_size) - if self.config['eval_type'] == EvaluatorType.VALUE: + if self.config["eval_type"] == EvaluatorType.VALUE: return interaction, origin_scores, positive_u, positive_i - elif self.config['eval_type'] == EvaluatorType.RANKING: - col_idx = interaction[self.config['ITEM_ID_FIELD']] + elif self.config["eval_type"] == EvaluatorType.RANKING: + col_idx = interaction[self.config["ITEM_ID_FIELD"]] batch_user_num = positive_u[-1] + 1 - scores = torch.full((batch_user_num, self.tot_item_num), -np.inf, device=self.device) + scores = torch.full( + (batch_user_num, self.tot_item_num), -np.inf, device=self.device + ) scores[row_idx, col_idx] = origin_scores return interaction, scores, positive_u, positive_i @torch.no_grad() - def evaluate(self, eval_data, load_best_model=True, model_file=None, show_progress=False): + def evaluate( + self, eval_data, load_best_model=True, model_file=None, show_progress=False + ): r"""Evaluate the model based on the eval data. Args: @@ -491,10 +580,12 @@ def evaluate(self, eval_data, load_best_model=True, model_file=None, show_progre if load_best_model: checkpoint_file = model_file or self.saved_model_file - checkpoint = torch.load(checkpoint_file, map_location= self.device) - self.model.load_state_dict(checkpoint['state_dict']) - self.model.load_other_parameter(checkpoint.get('other_parameter')) - message_output = 'Loading model structure and parameters from {}'.format(checkpoint_file) + checkpoint = torch.load(checkpoint_file, map_location=self.device) + self.model.load_state_dict(checkpoint["state_dict"]) + self.model.load_other_parameter(checkpoint.get("other_parameter")) + message_output = "Loading model structure and parameters from {}".format( + checkpoint_file + ) self.logger.info(message_output) self.model.eval() @@ -505,7 +596,7 @@ def evaluate(self, eval_data, load_best_model=True, model_file=None, show_progre self.item_tensor = eval_data._dataset.get_item_feature().to(self.device) else: eval_func = self._neg_sample_batch_eval - if self.config['eval_type'] == EvaluatorType.RANKING: + if self.config["eval_type"] == EvaluatorType.RANKING: self.tot_item_num = eval_data._dataset.item_num iter_data = ( @@ -513,8 +604,10 @@ def evaluate(self, eval_data, load_best_model=True, model_file=None, show_progre eval_data, total=len(eval_data), ncols=100, - desc=set_color(f"Evaluate ", 'pink'), - ) if show_progress else eval_data + desc=set_color(f"Evaluate ", "pink"), + ) + if show_progress + else eval_data ) num_sample = 0 @@ -522,28 +615,42 @@ def evaluate(self, eval_data, load_best_model=True, model_file=None, show_progre num_sample += len(batched_data) interaction, scores, positive_u, positive_i = eval_func(batched_data) if self.gpu_available and show_progress: - iter_data.set_postfix_str(set_color('GPU RAM: ' + get_gpu_usage(self.device), 'yellow')) - self.eval_collector.eval_batch_collect(scores, interaction, positive_u, positive_i) + iter_data.set_postfix_str( + set_color("GPU RAM: " + get_gpu_usage(self.device), "yellow") + ) + self.eval_collector.eval_batch_collect( + scores, interaction, positive_u, positive_i + ) self.eval_collector.model_collect(self.model) struct = self.eval_collector.get_data_struct() result = self.evaluator.evaluate(struct) - if not self.config['single_spec']: - result = self._map_reduce(result , num_sample) - self.wandblogger.log_eval_metrics(result, head='eval') + if not self.config["single_spec"]: + result = self._map_reduce(result, num_sample) + self.wandblogger.log_eval_metrics(result, head="eval") return result - + def _map_reduce(self, result, num_sample): gather_result = {} - total_sample = [torch.zeros(1).to(self.device) for _ in range(self.config['world_size'])] - torch.distributed.all_gather(total_sample , torch.Tensor([num_sample]).to(self.device)) - total_sample = torch.cat(total_sample , 0) + total_sample = [ + torch.zeros(1).to(self.device) for _ in range(self.config["world_size"]) + ] + torch.distributed.all_gather( + total_sample, torch.Tensor([num_sample]).to(self.device) + ) + total_sample = torch.cat(total_sample, 0) total_sample = torch.sum(total_sample).item() - for key , value in result.items(): + for key, value in result.items(): result[key] = torch.Tensor([value * num_sample]).to(self.device) - gather_result[key] = [torch.zeros_like(result[key]).to(self.device) for _ in range(self.config['world_size'])] - torch.distributed.all_gather(gather_result[key] , result[key]) - gather_result[key] = torch.cat(gather_result[key] , dim = 0) - gather_result[key] = round(torch.sum(gather_result[key]).item() / total_sample, self.config['metric_decimal_place']) + gather_result[key] = [ + torch.zeros_like(result[key]).to(self.device) + for _ in range(self.config["world_size"]) + ] + torch.distributed.all_gather(gather_result[key], result[key]) + gather_result[key] = torch.cat(gather_result[key], dim=0) + gather_result[key] = round( + torch.sum(gather_result[key]).item() / total_sample, + self.config["metric_decimal_place"], + ) return gather_result def _spilt_predict(self, interaction, batch_size): @@ -556,7 +663,9 @@ def _spilt_predict(self, interaction, batch_size): current_interaction = dict() for key, spilt_tensor in spilt_interaction.items(): current_interaction[key] = spilt_tensor[i] - result = self.model.predict(Interaction(current_interaction).to(self.device)) + result = self.model.predict( + Interaction(current_interaction).to(self.device) + ) if len(result.shape) == 0: result = result.unsqueeze(0) result_list.append(result) @@ -572,47 +681,57 @@ class KGTrainer(Trainer): def __init__(self, config, model): super(KGTrainer, self).__init__(config, model) - self.train_rec_step = config['train_rec_step'] - self.train_kg_step = config['train_kg_step'] + self.train_rec_step = config["train_rec_step"] + self.train_kg_step = config["train_kg_step"] def _train_epoch(self, train_data, epoch_idx, loss_func=None, show_progress=False): if self.train_rec_step is None or self.train_kg_step is None: interaction_state = KGDataLoaderState.RSKG - elif epoch_idx % (self.train_rec_step + self.train_kg_step) < self.train_rec_step: + elif ( + epoch_idx % (self.train_rec_step + self.train_kg_step) < self.train_rec_step + ): interaction_state = KGDataLoaderState.RS else: interaction_state = KGDataLoaderState.KG - if not self.config['single_spec']: + if not self.config["single_spec"]: train_data.knowledge_shuffle(epoch_idx) train_data.set_mode(interaction_state) if interaction_state in [KGDataLoaderState.RSKG, KGDataLoaderState.RS]: - return super()._train_epoch(train_data, epoch_idx, show_progress=show_progress) + return super()._train_epoch( + train_data, epoch_idx, show_progress=show_progress + ) elif interaction_state in [KGDataLoaderState.KG]: return super()._train_epoch( - train_data, epoch_idx, loss_func=self.model.calculate_kg_loss, show_progress=show_progress + train_data, + epoch_idx, + loss_func=self.model.calculate_kg_loss, + show_progress=show_progress, ) return None class KGATTrainer(Trainer): - r"""KGATTrainer is designed for KGAT, which is a knowledge-aware recommendation method. - - """ + r"""KGATTrainer is designed for KGAT, which is a knowledge-aware recommendation method.""" def __init__(self, config, model): super(KGATTrainer, self).__init__(config, model) def _train_epoch(self, train_data, epoch_idx, loss_func=None, show_progress=False): # train rs - if not self.config['single_spec']: + if not self.config["single_spec"]: train_data.knowledge_shuffle(epoch_idx) train_data.set_mode(KGDataLoaderState.RS) - rs_total_loss = super()._train_epoch(train_data, epoch_idx, show_progress=show_progress) + rs_total_loss = super()._train_epoch( + train_data, epoch_idx, show_progress=show_progress + ) # train kg train_data.set_mode(KGDataLoaderState.KG) kg_total_loss = super()._train_epoch( - train_data, epoch_idx, loss_func=self.model.calculate_kg_loss, show_progress=show_progress + train_data, + epoch_idx, + loss_func=self.model.calculate_kg_loss, + show_progress=show_progress, ) # update A @@ -630,8 +749,8 @@ class PretrainTrainer(Trainer): def __init__(self, config, model): super(PretrainTrainer, self).__init__(config, model) - self.pretrain_epochs = self.config['pretrain_epochs'] - self.save_step = self.config['save_step'] + self.pretrain_epochs = self.config["pretrain_epochs"] + self.save_step = self.config["save_step"] def save_pretrained_model(self, epoch, saved_model_file): r"""Store the model parameters information and training information. @@ -642,11 +761,11 @@ def save_pretrained_model(self, epoch, saved_model_file): """ state = { - 'config': self.config, - 'epoch': epoch, - 'state_dict': self.model.state_dict(), - 'optimizer': self.optimizer.state_dict(), - 'other_parameter': self.model.other_parameter(), + "config": self.config, + "epoch": epoch, + "state_dict": self.model.state_dict(), + "optimizer": self.optimizer.state_dict(), + "other_parameter": self.model.other_parameter(), } torch.save(state, saved_model_file) @@ -654,11 +773,16 @@ def pretrain(self, train_data, verbose=True, show_progress=False): for epoch_idx in range(self.start_epoch, self.pretrain_epochs): # train training_start_time = time() - train_loss = self._train_epoch(train_data, epoch_idx, show_progress=show_progress) - self.train_loss_dict[epoch_idx] = sum(train_loss) if isinstance(train_loss, tuple) else train_loss + train_loss = self._train_epoch( + train_data, epoch_idx, show_progress=show_progress + ) + self.train_loss_dict[epoch_idx] = ( + sum(train_loss) if isinstance(train_loss, tuple) else train_loss + ) training_end_time = time() - train_loss_output = \ - self._generate_train_loss_output(epoch_idx, training_start_time, training_end_time, train_loss) + train_loss_output = self._generate_train_loss_output( + epoch_idx, training_start_time, training_end_time, train_loss + ) if verbose: self.logger.info(train_loss_output) self._add_train_loss_to_tensorboard(epoch_idx, train_loss) @@ -666,10 +790,14 @@ def pretrain(self, train_data, verbose=True, show_progress=False): if (epoch_idx + 1) % self.save_step == 0: saved_model_file = os.path.join( self.checkpoint_dir, - '{}-{}-{}.pth'.format(self.config['model'], self.config['dataset'], str(epoch_idx + 1)) + "{}-{}-{}.pth".format( + self.config["model"], self.config["dataset"], str(epoch_idx + 1) + ), ) self.save_pretrained_model(epoch_idx, saved_model_file) - update_output = set_color('Saving current', 'blue') + ': %s' % saved_model_file + update_output = ( + set_color("Saving current", "blue") + ": %s" % saved_model_file + ) if verbose: self.logger.info(update_output) @@ -678,56 +806,70 @@ def pretrain(self, train_data, verbose=True, show_progress=False): class S3RecTrainer(PretrainTrainer): r"""S3RecTrainer is designed for S3Rec, which is a self-supervised learning based sequential recommenders. - It includes two training stages: pre-training ang fine-tuning. + It includes two training stages: pre-training ang fine-tuning. - """ + """ def __init__(self, config, model): super(S3RecTrainer, self).__init__(config, model) - def fit(self, train_data, valid_data=None, verbose=True, saved=True, show_progress=False, callback_fn=None): - if self.model.train_stage == 'pretrain': + def fit( + self, + train_data, + valid_data=None, + verbose=True, + saved=True, + show_progress=False, + callback_fn=None, + ): + if self.model.train_stage == "pretrain": return self.pretrain(train_data, verbose, show_progress) - elif self.model.train_stage == 'finetune': - return super().fit(train_data, valid_data, verbose, saved, show_progress, callback_fn) + elif self.model.train_stage == "finetune": + return super().fit( + train_data, valid_data, verbose, saved, show_progress, callback_fn + ) else: - raise ValueError("Please make sure that the 'train_stage' is 'pretrain' or 'finetune'!") + raise ValueError( + "Please make sure that the 'train_stage' is 'pretrain' or 'finetune'!" + ) class MKRTrainer(Trainer): - r"""MKRTrainer is designed for MKR, which is a knowledge-aware recommendation method. - - """ + r"""MKRTrainer is designed for MKR, which is a knowledge-aware recommendation method.""" def __init__(self, config, model): super(MKRTrainer, self).__init__(config, model) - self.kge_interval = config['kge_interval'] + self.kge_interval = config["kge_interval"] def _train_epoch(self, train_data, epoch_idx, loss_func=None, show_progress=False): - rs_total_loss, kg_total_loss = 0., 0. + rs_total_loss, kg_total_loss = 0.0, 0.0 # train rs - self.logger.info('Train RS') + self.logger.info("Train RS") train_data.set_mode(KGDataLoaderState.RS) rs_total_loss = super()._train_epoch( - train_data, epoch_idx, loss_func=self.model.calculate_rs_loss, show_progress=show_progress + train_data, + epoch_idx, + loss_func=self.model.calculate_rs_loss, + show_progress=show_progress, ) # train kg if epoch_idx % self.kge_interval == 0: - self.logger.info('Train KG') + self.logger.info("Train KG") train_data.set_mode(KGDataLoaderState.KG) kg_total_loss = super()._train_epoch( - train_data, epoch_idx, loss_func=self.model.calculate_kg_loss, show_progress=show_progress + train_data, + epoch_idx, + loss_func=self.model.calculate_kg_loss, + show_progress=show_progress, ) return rs_total_loss, kg_total_loss class TraditionalTrainer(Trainer): - r"""TraditionalTrainer is designed for Traditional model(Pop,ItemKNN), which set the epoch to 1 whatever the config. - - """ + r"""TraditionalTrainer is designed for Traditional model(Pop,ItemKNN), which set the epoch to 1 whatever the config.""" def __init__(self, config, model): super(TraditionalTrainer, self).__init__(config, model) @@ -735,40 +877,40 @@ def __init__(self, config, model): class DecisionTreeTrainer(AbstractTrainer): - """DecisionTreeTrainer is designed for DecisionTree model. - - """ + """DecisionTreeTrainer is designed for DecisionTree model.""" def __init__(self, config, model): super(DecisionTreeTrainer, self).__init__(config, model) self.logger = getLogger() self.tensorboard = get_tensorboard(self.logger) - self.label_field = config['LABEL_FIELD'] - self.convert_token_to_onehot = self.config['convert_token_to_onehot'] + self.label_field = config["LABEL_FIELD"] + self.convert_token_to_onehot = self.config["convert_token_to_onehot"] # evaluator - self.eval_type = config['eval_type'] - self.epochs = config['epochs'] - self.eval_step = min(config['eval_step'], self.epochs) - self.valid_metric = config['valid_metric'].lower() + self.eval_type = config["eval_type"] + self.epochs = config["epochs"] + self.eval_step = min(config["eval_step"], self.epochs) + self.valid_metric = config["valid_metric"].lower() self.eval_collector = Collector(config) self.evaluator = Evaluator(config) # model saved - self.checkpoint_dir = config['checkpoint_dir'] + self.checkpoint_dir = config["checkpoint_dir"] ensure_dir(self.checkpoint_dir) - temp_file = '{}-{}-temp.pth'.format(self.config['model'], get_local_time()) + temp_file = "{}-{}-temp.pth".format(self.config["model"], get_local_time()) self.temp_file = os.path.join(self.checkpoint_dir, temp_file) - temp_best_file = '{}-{}-temp-best.pth'.format(self.config['model'], get_local_time()) + temp_best_file = "{}-{}-temp-best.pth".format( + self.config["model"], get_local_time() + ) self.temp_best_file = os.path.join(self.checkpoint_dir, temp_best_file) - saved_model_file = '{}-{}.pth'.format(self.config['model'], get_local_time()) + saved_model_file = "{}-{}.pth".format(self.config["model"], get_local_time()) self.saved_model_file = os.path.join(self.checkpoint_dir, saved_model_file) - self.stopping_step = config['stopping_step'] - self.valid_metric_bigger = config['valid_metric_bigger'] + self.stopping_step = config["stopping_step"] + self.valid_metric_bigger = config["valid_metric_bigger"] self.cur_step = 0 self.best_valid_score = -np.inf if self.valid_metric_bigger else np.inf self.best_valid_result = None @@ -798,6 +940,7 @@ def _interaction_to_sparse(self, dataloader): if self.convert_token_to_onehot: from scipy import sparse from scipy.sparse import dok_matrix + convert_col_list = dataloader._dataset.convert_col_list hash_count = dataloader._dataset.hash_count @@ -845,16 +988,18 @@ def _save_checkpoint(self, epoch): """ state = { - 'config': self.config, - 'epoch': epoch, - 'cur_step': self.cur_step, - 'best_valid_score': self.best_valid_score, - 'state_dict': self.temp_best_file, - 'other_parameter': None + "config": self.config, + "epoch": epoch, + "cur_step": self.cur_step, + "best_valid_score": self.best_valid_score, + "state_dict": self.temp_best_file, + "other_parameter": None, } torch.save(state, self.saved_model_file) - def fit(self, train_data, valid_data=None, verbose=True, saved=True, show_progress=False): + def fit( + self, train_data, valid_data=None, verbose=True, saved=True, show_progress=False + ): for epoch_idx in range(self.epochs): self._train_at_once(train_data, valid_data) @@ -863,23 +1008,35 @@ def fit(self, train_data, valid_data=None, verbose=True, saved=True, show_progre valid_start_time = time() valid_score, valid_result = self._valid_epoch(valid_data) - self.best_valid_score, self.cur_step, stop_flag, update_flag = early_stopping( + ( + self.best_valid_score, + self.cur_step, + stop_flag, + update_flag, + ) = early_stopping( valid_score, self.best_valid_score, self.cur_step, max_step=self.stopping_step, - bigger=self.valid_metric_bigger + bigger=self.valid_metric_bigger, ) valid_end_time = time() - valid_score_output = (set_color("epoch %d evaluating", 'green') + " [" + set_color("time", 'blue') - + ": %.2fs, " + set_color("valid_score", 'blue') + ": %f]") % \ - (epoch_idx, valid_end_time - valid_start_time, valid_score) - valid_result_output = set_color('valid result', 'blue') + ': \n' + dict2str(valid_result) + valid_score_output = ( + set_color("epoch %d evaluating", "green") + + " [" + + set_color("time", "blue") + + ": %.2fs, " + + set_color("valid_score", "blue") + + ": %f]" + ) % (epoch_idx, valid_end_time - valid_start_time, valid_score) + valid_result_output = ( + set_color("valid result", "blue") + ": \n" + dict2str(valid_result) + ) if verbose: self.logger.info(valid_score_output) self.logger.info(valid_result_output) - self.tensorboard.add_scalar('Vaild_score', valid_score, epoch_idx) + self.tensorboard.add_scalar("Vaild_score", valid_score, epoch_idx) if update_flag: if saved: @@ -888,8 +1045,9 @@ def fit(self, train_data, valid_data=None, verbose=True, saved=True, show_progre self.best_valid_result = valid_result if stop_flag: - stop_output = 'Finished training, best eval result in epoch %d' % \ - (epoch_idx - self.cur_step * self.eval_step) + stop_output = "Finished training, best eval result in epoch %d" % ( + epoch_idx - self.cur_step * self.eval_step + ) if self.temp_file: os.remove(self.temp_file) if verbose: @@ -898,7 +1056,9 @@ def fit(self, train_data, valid_data=None, verbose=True, saved=True, show_progre return self.best_valid_score, self.best_valid_result - def evaluate(self, eval_data, load_best_model=True, model_file=None, show_progress=False): + def evaluate( + self, eval_data, load_best_model=True, model_file=None, show_progress=False + ): raise NotImplementedError def _train_at_once(self, train_data, valid_data): @@ -906,25 +1066,23 @@ def _train_at_once(self, train_data, valid_data): class xgboostTrainer(DecisionTreeTrainer): - """xgboostTrainer is designed for XGBOOST. - - """ + """xgboostTrainer is designed for XGBOOST.""" def __init__(self, config, model): super(xgboostTrainer, self).__init__(config, model) - self.xgb = __import__('xgboost') - self.boost_model = config['xgb_model'] - self.silent = config['xgb_silent'] - self.nthread = config['xgb_nthread'] + self.xgb = __import__("xgboost") + self.boost_model = config["xgb_model"] + self.silent = config["xgb_silent"] + self.nthread = config["xgb_nthread"] # train params - self.params = config['xgb_params'] - self.num_boost_round = config['xgb_num_boost_round'] + self.params = config["xgb_params"] + self.num_boost_round = config["xgb_num_boost_round"] self.evals = () - self.early_stopping_rounds = config['xgb_early_stopping_rounds'] + self.early_stopping_rounds = config["xgb_early_stopping_rounds"] self.evals_result = {} - self.verbose_eval = config['xgb_verbose_eval'] + self.verbose_eval = config["xgb_verbose_eval"] self.callbacks = None self.deval = None self.eval_pred = self.eval_true = None @@ -938,7 +1096,9 @@ def _interaction_to_lib_datatype(self, dataloader): DMatrix: Data in the form of 'DMatrix'. """ data, label = self._interaction_to_sparse(dataloader) - return self.xgb.DMatrix(data=data, label=label, silent=self.silent, nthread=self.nthread) + return self.xgb.DMatrix( + data=data, label=label, silent=self.silent, nthread=self.nthread + ) def _train_at_once(self, train_data, valid_data): r""" @@ -949,7 +1109,7 @@ def _train_at_once(self, train_data, valid_data): """ self.dtrain = self._interaction_to_lib_datatype(train_data) self.dvalid = self._interaction_to_lib_datatype(valid_data) - self.evals = [(self.dtrain, 'train'), (self.dvalid, 'valid')] + self.evals = [(self.dtrain, "train"), (self.dvalid, "valid")] self.model = self.xgb.train( self.params, self.dtrain, @@ -959,13 +1119,15 @@ def _train_at_once(self, train_data, valid_data): evals_result=self.evals_result, verbose_eval=self.verbose_eval, xgb_model=self.boost_model, - callbacks=self.callbacks + callbacks=self.callbacks, ) self.model.save_model(self.temp_file) self.boost_model = self.temp_file - def evaluate(self, eval_data, load_best_model=True, model_file=None, show_progress=False): + def evaluate( + self, eval_data, load_best_model=True, model_file=None, show_progress=False + ): if load_best_model: if model_file: checkpoint_file = model_file @@ -983,25 +1145,23 @@ def evaluate(self, eval_data, load_best_model=True, model_file=None, show_progre class lightgbmTrainer(DecisionTreeTrainer): - """lightgbmTrainer is designed for lightgbm. - - """ + """lightgbmTrainer is designed for lightgbm.""" def __init__(self, config, model): super(lightgbmTrainer, self).__init__(config, model) - self.lgb = __import__('lightgbm') - self.boost_model = config['lgb_model'] - self.silent = config['lgb_silent'] + self.lgb = __import__("lightgbm") + self.boost_model = config["lgb_model"] + self.silent = config["lgb_silent"] # train params - self.params = config['lgb_params'] - self.num_boost_round = config['lgb_num_boost_round'] + self.params = config["lgb_params"] + self.num_boost_round = config["lgb_num_boost_round"] self.evals = () - self.early_stopping_rounds = config['lgb_early_stopping_rounds'] + self.early_stopping_rounds = config["lgb_early_stopping_rounds"] self.evals_result = {} - self.verbose_eval = config['lgb_verbose_eval'] - self.learning_rates = config['lgb_learning_rates'] + self.verbose_eval = config["lgb_verbose_eval"] + self.learning_rates = config["lgb_learning_rates"] self.callbacks = None self.deval_data = self.deval_label = None self.eval_pred = self.eval_true = None @@ -1037,13 +1197,15 @@ def _train_at_once(self, train_data, valid_data): verbose_eval=self.verbose_eval, learning_rates=self.learning_rates, init_model=self.boost_model, - callbacks=self.callbacks + callbacks=self.callbacks, ) self.model.save_model(self.temp_file) self.boost_model = self.temp_file - def evaluate(self, eval_data, load_best_model=True, model_file=None, show_progress=False): + def evaluate( + self, eval_data, load_best_model=True, model_file=None, show_progress=False + ): if load_best_model: if model_file: checkpoint_file = model_file @@ -1062,20 +1224,30 @@ def evaluate(self, eval_data, load_best_model=True, model_file=None, show_progre class RaCTTrainer(PretrainTrainer): r"""RaCTTrainer is designed for RaCT, which is an actor-critic reinforcement learning based general recommenders. - It includes three training stages: actor pre-training, critic pre-training and actor-critic training. + It includes three training stages: actor pre-training, critic pre-training and actor-critic training. - """ + """ def __init__(self, config, model): super(RaCTTrainer, self).__init__(config, model) - def fit(self, train_data, valid_data=None, verbose=True, saved=True, show_progress=False, callback_fn=None): - if self.model.train_stage == 'actor_pretrain': + def fit( + self, + train_data, + valid_data=None, + verbose=True, + saved=True, + show_progress=False, + callback_fn=None, + ): + if self.model.train_stage == "actor_pretrain": return self.pretrain(train_data, verbose, show_progress) elif self.model.train_stage == "critic_pretrain": return self.pretrain(train_data, verbose, show_progress) - elif self.model.train_stage == 'finetune': - return super().fit(train_data, valid_data, verbose, saved, show_progress, callback_fn) + elif self.model.train_stage == "finetune": + return super().fit( + train_data, valid_data, verbose, saved, show_progress, callback_fn + ) else: raise ValueError( "Please make sure that the 'train_stage' is " @@ -1084,44 +1256,65 @@ def fit(self, train_data, valid_data=None, verbose=True, saved=True, show_progre class RecVAETrainer(Trainer): - r"""RecVAETrainer is designed for RecVAE, which is a general recommender. - - """ + r"""RecVAETrainer is designed for RecVAE, which is a general recommender.""" def __init__(self, config, model): super(RecVAETrainer, self).__init__(config, model) - self.n_enc_epochs = config['n_enc_epochs'] - self.n_dec_epochs = config['n_dec_epochs'] + self.n_enc_epochs = config["n_enc_epochs"] + self.n_dec_epochs = config["n_dec_epochs"] - self.optimizer_encoder = self._build_optimizer(params=self.model.encoder.parameters()) - self.optimizer_decoder = self._build_optimizer(params=self.model.decoder.parameters()) + self.optimizer_encoder = self._build_optimizer( + params=self.model.encoder.parameters() + ) + self.optimizer_decoder = self._build_optimizer( + params=self.model.decoder.parameters() + ) def _train_epoch(self, train_data, epoch_idx, loss_func=None, show_progress=False): self.optimizer = self.optimizer_encoder - encoder_loss_func = lambda data: self.model.calculate_loss(data, encoder_flag=True) + encoder_loss_func = lambda data: self.model.calculate_loss( + data, encoder_flag=True + ) for epoch in range(self.n_enc_epochs): - super()._train_epoch(train_data, epoch_idx, loss_func=encoder_loss_func, show_progress=show_progress) + super()._train_epoch( + train_data, + epoch_idx, + loss_func=encoder_loss_func, + show_progress=show_progress, + ) self.model.update_prior() loss = 0.0 self.optimizer = self.optimizer_decoder - decoder_loss_func = lambda data: self.model.calculate_loss(data, encoder_flag=False) + decoder_loss_func = lambda data: self.model.calculate_loss( + data, encoder_flag=False + ) for epoch in range(self.n_dec_epochs): loss += super()._train_epoch( - train_data, epoch_idx, loss_func=decoder_loss_func, show_progress=show_progress + train_data, + epoch_idx, + loss_func=decoder_loss_func, + show_progress=show_progress, ) return loss class NCLTrainer(Trainer): - def __init__(self, config, model): super(NCLTrainer, self).__init__(config, model) - self.num_m_step = config['m_step'] + self.num_m_step = config["m_step"] assert self.num_m_step is not None - def fit(self, train_data, valid_data=None, verbose=True, saved=True, show_progress=False, callback_fn=None): + def fit( + self, + train_data, + valid_data=None, + verbose=True, + saved=True, + show_progress=False, + callback_fn=None, + ): r"""Train the model based on the train data and the valid data. Args: @@ -1150,11 +1343,16 @@ def fit(self, train_data, valid_data=None, verbose=True, saved=True, show_progre self.model.e_step() # train training_start_time = time() - train_loss = self._train_epoch(train_data, epoch_idx, show_progress=show_progress) - self.train_loss_dict[epoch_idx] = sum(train_loss) if isinstance(train_loss, tuple) else train_loss + train_loss = self._train_epoch( + train_data, epoch_idx, show_progress=show_progress + ) + self.train_loss_dict[epoch_idx] = ( + sum(train_loss) if isinstance(train_loss, tuple) else train_loss + ) training_end_time = time() - train_loss_output = \ - self._generate_train_loss_output(epoch_idx, training_start_time, training_end_time, train_loss) + train_loss_output = self._generate_train_loss_output( + epoch_idx, training_start_time, training_end_time, train_loss + ) if verbose: self.logger.info(train_loss_output) self._add_train_loss_to_tensorboard(epoch_idx, train_loss) @@ -1163,34 +1361,54 @@ def fit(self, train_data, valid_data=None, verbose=True, saved=True, show_progre if self.eval_step <= 0 or not valid_data: if saved: self._save_checkpoint(epoch_idx) - update_output = set_color('Saving current', 'blue') + ': %s' % self.saved_model_file + update_output = ( + set_color("Saving current", "blue") + + ": %s" % self.saved_model_file + ) if verbose: self.logger.info(update_output) continue if (epoch_idx + 1) % self.eval_step == 0: valid_start_time = time() - valid_score, valid_result = self._valid_epoch(valid_data, show_progress=show_progress) - self.best_valid_score, self.cur_step, stop_flag, update_flag = early_stopping( + valid_score, valid_result = self._valid_epoch( + valid_data, show_progress=show_progress + ) + ( + self.best_valid_score, + self.cur_step, + stop_flag, + update_flag, + ) = early_stopping( valid_score, self.best_valid_score, self.cur_step, max_step=self.stopping_step, - bigger=self.valid_metric_bigger + bigger=self.valid_metric_bigger, ) valid_end_time = time() - valid_score_output = (set_color("epoch %d evaluating", 'green') + " [" + set_color("time", 'blue') - + ": %.2fs, " + set_color("valid_score", 'blue') + ": %f]") % \ - (epoch_idx, valid_end_time - valid_start_time, valid_score) - valid_result_output = set_color('valid result', 'blue') + ': \n' + dict2str(valid_result) + valid_score_output = ( + set_color("epoch %d evaluating", "green") + + " [" + + set_color("time", "blue") + + ": %.2fs, " + + set_color("valid_score", "blue") + + ": %f]" + ) % (epoch_idx, valid_end_time - valid_start_time, valid_score) + valid_result_output = ( + set_color("valid result", "blue") + ": \n" + dict2str(valid_result) + ) if verbose: self.logger.info(valid_score_output) self.logger.info(valid_result_output) - self.tensorboard.add_scalar('Vaild_score', valid_score, epoch_idx) + self.tensorboard.add_scalar("Vaild_score", valid_score, epoch_idx) if update_flag: if saved: self._save_checkpoint(epoch_idx) - update_output = set_color('Saving current best', 'blue') + ': %s' % self.saved_model_file + update_output = ( + set_color("Saving current best", "blue") + + ": %s" % self.saved_model_file + ) if verbose: self.logger.info(update_output) self.best_valid_result = valid_result @@ -1199,8 +1417,9 @@ def fit(self, train_data, valid_data=None, verbose=True, saved=True, show_progre callback_fn(epoch_idx, valid_score) if stop_flag: - stop_output = 'Finished training, best eval result in epoch %d' % \ - (epoch_idx - self.cur_step * self.eval_step) + stop_output = "Finished training, best eval result in epoch %d" % ( + epoch_idx - self.cur_step * self.eval_step + ) if verbose: self.logger.info(stop_output) break @@ -1228,34 +1447,42 @@ def _train_epoch(self, train_data, epoch_idx, loss_func=None, show_progress=Fals train_data, total=len(train_data), ncols=100, - desc=set_color(f"Train {epoch_idx:>5}", 'pink'), - ) if show_progress else train_data + desc=set_color(f"Train {epoch_idx:>5}", "pink"), + ) + if show_progress + else train_data ) scaler = amp.GradScaler(enabled=self.enable_scaler) - if not self.config['single_spec'] and train_data.shuffle: + if not self.config["single_spec"] and train_data.shuffle: train_data.sampler.set_epoch(epoch_idx) - + for batch_idx, interaction in enumerate(iter_data): interaction = interaction.to(self.device) self.optimizer.zero_grad() sync_loss = 0 - if not self.config['single_spec']: + if not self.config["single_spec"]: self.set_reduce_hook() sync_loss = self.sync_grad_loss() - + with amp.autocast(enabled=self.enable_amp): losses = loss_func(interaction) - + if isinstance(losses, tuple): - if epoch_idx < self.config['warm_up_step']: + if epoch_idx < self.config["warm_up_step"]: losses = losses[:-1] loss = sum(losses) loss_tuple = tuple(per_loss.item() for per_loss in losses) - total_loss = loss_tuple if total_loss is None else tuple(map(sum, zip(total_loss, loss_tuple))) + total_loss = ( + loss_tuple + if total_loss is None + else tuple(map(sum, zip(total_loss, loss_tuple))) + ) else: loss = losses - total_loss = losses.item() if total_loss is None else total_loss + losses.item() + total_loss = ( + losses.item() if total_loss is None else total_loss + losses.item() + ) self._check_nan(loss) scaler.scale(loss + sync_loss).backward() @@ -1264,5 +1491,7 @@ def _train_epoch(self, train_data, epoch_idx, loss_func=None, show_progress=Fals scaler.step(self.optimizer) scaler.update() if self.gpu_available and show_progress: - iter_data.set_postfix_str(set_color('GPU RAM: ' + get_gpu_usage(self.device), 'yellow')) + iter_data.set_postfix_str( + set_color("GPU RAM: " + get_gpu_usage(self.device), "yellow") + ) return total_loss diff --git a/recbole/utils/__init__.py b/recbole/utils/__init__.py index 7ee3d2b2c..dac6ec573 100644 --- a/recbole/utils/__init__.py +++ b/recbole/utils/__init__.py @@ -1,13 +1,43 @@ from recbole.utils.logger import init_logger, set_color -from recbole.utils.utils import get_local_time, ensure_dir, get_model, get_trainer, \ - early_stopping, calculate_valid_score, dict2str, init_seed, get_tensorboard, get_gpu_usage +from recbole.utils.utils import ( + get_local_time, + ensure_dir, + get_model, + get_trainer, + early_stopping, + calculate_valid_score, + dict2str, + init_seed, + get_tensorboard, + get_gpu_usage, +) from recbole.utils.enum_type import * from recbole.utils.argument_list import * from recbole.utils.wandblogger import WandbLogger __all__ = [ - 'init_logger', 'get_local_time', 'ensure_dir', 'get_model', 'get_trainer', 'early_stopping', - 'calculate_valid_score', 'dict2str', 'Enum', 'ModelType', 'KGDataLoaderState', 'EvaluatorType', 'InputType', - 'FeatureType', 'FeatureSource', 'init_seed', 'general_arguments', 'training_arguments', 'evaluation_arguments', - 'dataset_arguments', 'get_tensorboard', 'set_color', 'get_gpu_usage', 'WandbLogger' + "init_logger", + "get_local_time", + "ensure_dir", + "get_model", + "get_trainer", + "early_stopping", + "calculate_valid_score", + "dict2str", + "Enum", + "ModelType", + "KGDataLoaderState", + "EvaluatorType", + "InputType", + "FeatureType", + "FeatureSource", + "init_seed", + "general_arguments", + "training_arguments", + "evaluation_arguments", + "dataset_arguments", + "get_tensorboard", + "set_color", + "get_gpu_usage", + "WandbLogger", ] diff --git a/recbole/utils/case_study.py b/recbole/utils/case_study.py index 17af387d8..ddd9a29af 100644 --- a/recbole/utils/case_study.py +++ b/recbole/utils/case_study.py @@ -35,7 +35,7 @@ def full_sort_scores(uid_series, model, test_data, device=None): Returns: torch.Tensor: the scores of all items for each user in uid_series. """ - device = device or torch.device('cpu') + device = device or torch.device("cpu") uid_series = torch.tensor(uid_series) uid_field = test_data.dataset.uid_field dataset = test_data.dataset @@ -44,11 +44,15 @@ def full_sort_scores(uid_series, model, test_data, device=None): if not test_data.is_sequential: input_interaction = dataset.join(Interaction({uid_field: uid_series})) history_item = test_data.uid2history_item[list(uid_series)] - history_row = torch.cat([torch.full_like(hist_iid, i) for i, hist_iid in enumerate(history_item)]) + history_row = torch.cat( + [torch.full_like(hist_iid, i) for i, hist_iid in enumerate(history_item)] + ) history_col = torch.cat(list(history_item)) history_index = history_row, history_col else: - _, index = (dataset.inter_feat[uid_field] == uid_series[:, None]).nonzero(as_tuple=True) + _, index = (dataset.inter_feat[uid_field] == uid_series[:, None]).nonzero( + as_tuple=True + ) input_interaction = dataset[index] history_index = None @@ -58,7 +62,9 @@ def full_sort_scores(uid_series, model, test_data, device=None): scores = model.full_sort_predict(input_interaction) except NotImplementedError: input_interaction = input_interaction.repeat_interleave(dataset.item_num) - input_interaction.update(test_data.dataset.get_item_feature().to(device).repeat(len(uid_series))) + input_interaction.update( + test_data.dataset.get_item_feature().to(device).repeat(len(uid_series)) + ) scores = model.predict(input_interaction) scores = scores.view(-1, dataset.item_num) diff --git a/recbole/utils/enum_type.py b/recbole/utils/enum_type.py index 1b07fc23f..986d7521c 100644 --- a/recbole/utils/enum_type.py +++ b/recbole/utils/enum_type.py @@ -73,10 +73,10 @@ class FeatureType(Enum): - ``FLOAT_SEQ``: Float sequence features like pretrained vector. """ - TOKEN = 'token' - FLOAT = 'float' - TOKEN_SEQ = 'token_seq' - FLOAT_SEQ = 'float_seq' + TOKEN = "token" + FLOAT = "float" + TOKEN_SEQ = "token_seq" + FLOAT_SEQ = "float_seq" class FeatureSource(Enum): @@ -91,10 +91,10 @@ class FeatureSource(Enum): - ``NET``: Features from ``.net``. """ - INTERACTION = 'inter' - USER = 'user' - ITEM = 'item' - USER_ID = 'user_id' - ITEM_ID = 'item_id' - KG = 'kg' - NET = 'net' + INTERACTION = "inter" + USER = "user" + ITEM = "item" + USER_ID = "user_id" + ITEM_ID = "item_id" + KG = "kg" + NET = "net" diff --git a/recbole/utils/logger.py b/recbole/utils/logger.py index 18b850e9c..3cc583db3 100644 --- a/recbole/utils/logger.py +++ b/recbole/utils/logger.py @@ -27,35 +27,34 @@ from colorama import init log_colors_config = { - 'DEBUG': 'cyan', - 'WARNING': 'yellow', - 'ERROR': 'red', - 'CRITICAL': 'red', + "DEBUG": "cyan", + "WARNING": "yellow", + "ERROR": "red", + "CRITICAL": "red", } class RemoveColorFilter(logging.Filter): - def filter(self, record): if record: - ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])') - record.msg = ansi_escape.sub('', str(record.msg)) + ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") + record.msg = ansi_escape.sub("", str(record.msg)) return True def set_color(log, color, highlight=True): - color_set = ['black', 'red', 'green', 'yellow', 'blue', 'pink', 'cyan', 'white'] + color_set = ["black", "red", "green", "yellow", "blue", "pink", "cyan", "white"] try: index = color_set.index(color) except: index = len(color_set) - 1 - prev_log = '\033[' + prev_log = "\033[" if highlight: - prev_log += '1;3' + prev_log += "1;3" else: - prev_log += '0;3' - prev_log += str(index) + 'm' - return prev_log + log + '\033[0m' + prev_log += "0;3" + prev_log += str(index) + "m" + return prev_log + log + "\033[0m" def init_logger(config): @@ -73,14 +72,14 @@ def init_logger(config): >>> logger.info(train_result) """ init(autoreset=True) - LOGROOT = './log/' + LOGROOT = "./log/" dir_name = os.path.dirname(LOGROOT) ensure_dir(dir_name) - model_name = os.path.join(dir_name, config['model']) + model_name = os.path.join(dir_name, config["model"]) ensure_dir(model_name) - config_str = ''.join([str(key) for key in config.final_config_dict.values()]) - md5 = hashlib.md5(config_str.encode(encoding='utf-8')).hexdigest()[:6] - logfilename = '{}/{}-{}.log'.format(config['model'], get_local_time(), md5) + config_str = "".join([str(key) for key in config.final_config_dict.values()]) + md5 = hashlib.md5(config_str.encode(encoding="utf-8")).hexdigest()[:6] + logfilename = "{}/{}-{}.log".format(config["model"], get_local_time(), md5) logfilepath = os.path.join(LOGROOT, logfilename) @@ -91,15 +90,15 @@ def init_logger(config): sfmt = "%(log_color)s%(asctime)-15s %(levelname)s %(message)s" sdatefmt = "%d %b %H:%M" sformatter = colorlog.ColoredFormatter(sfmt, sdatefmt, log_colors=log_colors_config) - if config['state'] is None or config['state'].lower() == 'info': + if config["state"] is None or config["state"].lower() == "info": level = logging.INFO - elif config['state'].lower() == 'debug': + elif config["state"].lower() == "debug": level = logging.DEBUG - elif config['state'].lower() == 'error': + elif config["state"].lower() == "error": level = logging.ERROR - elif config['state'].lower() == 'warning': + elif config["state"].lower() == "warning": level = logging.WARNING - elif config['state'].lower() == 'critical': + elif config["state"].lower() == "critical": level = logging.CRITICAL else: level = logging.INFO diff --git a/recbole/utils/url.py b/recbole/utils/url.py index cd7b99f8b..b2c964611 100644 --- a/recbole/utils/url.py +++ b/recbole/utils/url.py @@ -1,9 +1,9 @@ -''' +""" recbole.utils.url ################################ Reference code: https://github.com/snap-stanford/ogb/blob/master/ogb/utils/url.py -''' +""" import urllib.request as ur import zipfile @@ -19,11 +19,16 @@ def decide_download(url): d = ur.urlopen(url) - size = int(d.info()['Content-Length']) / GBFACTOR + size = int(d.info()["Content-Length"]) / GBFACTOR ### confirm if larger than 1GB if size > 1: - return input('This will download %.2fGB. Will you proceed? (y/N)\n' % (size)).lower() == 'y' + return ( + input( + "This will download %.2fGB. Will you proceed? (y/N)\n" % (size) + ).lower() + == "y" + ) else: return True @@ -37,27 +42,27 @@ def makedirs(path): def download_url(url, folder): - '''Downloads the content of an URL to a specific folder. + """Downloads the content of an URL to a specific folder. Args: url (string): The url. folder (string): The folder. - ''' + """ - filename = url.rpartition('/')[2] + filename = url.rpartition("/")[2] path = osp.join(folder, filename) logger = getLogger() if osp.exists(path) and osp.getsize(path) > 0: # pragma: no cover - logger.info(f'Using exist file {filename}') + logger.info(f"Using exist file {filename}") return path - logger.info(f'Downloading {url}') + logger.info(f"Downloading {url}") makedirs(folder) data = ur.urlopen(url) - size = int(data.info()['Content-Length']) + size = int(data.info()["Content-Length"]) chunk_size = 1024 * 1024 num_iter = int(size / chunk_size) + 2 @@ -65,50 +70,55 @@ def download_url(url, folder): downloaded_size = 0 try: - with open(path, 'wb') as f: + with open(path, "wb") as f: pbar = tqdm(range(num_iter)) for i in pbar: chunk = data.read(chunk_size) downloaded_size += len(chunk) - pbar.set_description('Downloaded {:.2f} GB'.format(float(downloaded_size) / GBFACTOR)) + pbar.set_description( + "Downloaded {:.2f} GB".format(float(downloaded_size) / GBFACTOR) + ) f.write(chunk) except: if os.path.exists(path): os.remove(path) - raise RuntimeError('Stopped downloading due to interruption.') + raise RuntimeError("Stopped downloading due to interruption.") return path def extract_zip(path, folder): - '''Extracts a zip archive to a specific folder. + """Extracts a zip archive to a specific folder. Args: path (string): The path to the tar archive. folder (string): The folder. - ''' + """ logger = getLogger() - logger.info(f'Extracting {path}') - with zipfile.ZipFile(path, 'r') as f: + logger.info(f"Extracting {path}") + with zipfile.ZipFile(path, "r") as f: f.extractall(folder) def rename_atomic_files(folder, old_name, new_name): - '''Rename all atomic files in a given folder. + """Rename all atomic files in a given folder. Args: folder (string): The folder. old_name (string): Old name for atomic files. new_name (string): New name for atomic files. - ''' + """ files = os.listdir(folder) for f in files: base, suf = os.path.splitext(f) if not old_name in base: continue - assert suf in {'.inter', '.user', '.item'} - os.rename(os.path.join(folder, f), os.path.join(folder, base.replace(old_name, new_name) + suf)) + assert suf in {".inter", ".user", ".item"} + os.rename( + os.path.join(folder, f), + os.path.join(folder, base.replace(old_name, new_name) + suf), + ) -if __name__ == '__main__': +if __name__ == "__main__": pass diff --git a/recbole/utils/utils.py b/recbole/utils/utils.py index 212005363..14b7b04fb 100644 --- a/recbole/utils/utils.py +++ b/recbole/utils/utils.py @@ -32,7 +32,7 @@ def get_local_time(): str: current time """ cur = datetime.datetime.now() - cur = cur.strftime('%b-%d-%Y_%H-%M-%S') + cur = cur.strftime("%b-%d-%Y_%H-%M-%S") return cur @@ -58,20 +58,25 @@ def get_model(model_name): Recommender: model class """ model_submodule = [ - 'general_recommender', 'context_aware_recommender', 'sequential_recommender', 'knowledge_aware_recommender', - 'exlib_recommender' + "general_recommender", + "context_aware_recommender", + "sequential_recommender", + "knowledge_aware_recommender", + "exlib_recommender", ] model_file_name = model_name.lower() model_module = None for submodule in model_submodule: - module_path = '.'.join(['recbole.model', submodule, model_file_name]) + module_path = ".".join(["recbole.model", submodule, model_file_name]) if importlib.util.find_spec(module_path, __name__): model_module = importlib.import_module(module_path, __name__) break if model_module is None: - raise ValueError('`model_name` [{}] is not the name of an existing model.'.format(model_name)) + raise ValueError( + "`model_name` [{}] is not the name of an existing model.".format(model_name) + ) model_class = getattr(model_module, model_name) return model_class @@ -87,18 +92,22 @@ def get_trainer(model_type, model_name): Trainer: trainer class """ try: - return getattr(importlib.import_module('recbole.trainer'), model_name + 'Trainer') + return getattr( + importlib.import_module("recbole.trainer"), model_name + "Trainer" + ) except AttributeError: if model_type == ModelType.KNOWLEDGE: - return getattr(importlib.import_module('recbole.trainer'), 'KGTrainer') + return getattr(importlib.import_module("recbole.trainer"), "KGTrainer") elif model_type == ModelType.TRADITIONAL: - return getattr(importlib.import_module('recbole.trainer'), 'TraditionalTrainer') + return getattr( + importlib.import_module("recbole.trainer"), "TraditionalTrainer" + ) else: - return getattr(importlib.import_module('recbole.trainer'), 'Trainer') + return getattr(importlib.import_module("recbole.trainer"), "Trainer") def early_stopping(value, best, cur_step, max_step, bigger=True): - r""" validation-based early stopping + r"""validation-based early stopping Args: value (float): current result @@ -142,7 +151,7 @@ def early_stopping(value, best, cur_step, max_step, bigger=True): def calculate_valid_score(valid_result, valid_metric=None): - r""" return valid score from valid result + r"""return valid score from valid result Args: valid_result (dict): valid result @@ -154,11 +163,11 @@ def calculate_valid_score(valid_result, valid_metric=None): if valid_metric: return valid_result[valid_metric] else: - return valid_result['Recall@10'] + return valid_result["Recall@10"] def dict2str(result_dict): - r""" convert result dict to str + r"""convert result dict to str Args: result_dict (dict): result dict @@ -167,11 +176,13 @@ def dict2str(result_dict): str: result str """ - return ' '.join([str(metric) + ' : ' + str(value) for metric, value in result_dict.items()]) + return " ".join( + [str(metric) + " : " + str(value) for metric, value in result_dict.items()] + ) def init_seed(seed, reproducibility): - r""" init random seed for random functions in numpy, torch, cuda and cudnn + r"""init random seed for random functions in numpy, torch, cuda and cudnn Args: seed (int): random seed @@ -191,7 +202,7 @@ def init_seed(seed, reproducibility): def get_tensorboard(logger): - r""" Creates a SummaryWriter of Tensorboard that can log PyTorch models and metrics into a directory for + r"""Creates a SummaryWriter of Tensorboard that can log PyTorch models and metrics into a directory for visualization within the TensorBoard UI. For the convenience of the user, the naming rule of the SummaryWriter's log_dir is the same as the logger. @@ -202,15 +213,15 @@ def get_tensorboard(logger): Returns: SummaryWriter: it will write out events and summaries to the event file. """ - base_path = 'log_tensorboard' + base_path = "log_tensorboard" dir_name = None for handler in logger.handlers: if hasattr(handler, "baseFilename"): - dir_name = os.path.basename(getattr(handler, 'baseFilename')).split('.')[0] + dir_name = os.path.basename(getattr(handler, "baseFilename")).split(".")[0] break if dir_name is None: - dir_name = '{}-{}'.format('model', get_local_time()) + dir_name = "{}-{}".format("model", get_local_time()) dir_path = os.path.join(base_path, dir_name) writer = SummaryWriter(dir_path) @@ -218,7 +229,7 @@ def get_tensorboard(logger): def get_gpu_usage(device=None): - r""" Return the reserved memory and total memory of given device in a string. + r"""Return the reserved memory and total memory of given device in a string. Args: device: cuda.device. It is the device that the model run on. @@ -226,7 +237,7 @@ def get_gpu_usage(device=None): str: it contains the info about reserved memory and total memory of given device. """ - reserved = torch.cuda.max_memory_reserved(device) / 1024 ** 3 - total = torch.cuda.get_device_properties(device).total_memory / 1024 ** 3 + reserved = torch.cuda.max_memory_reserved(device) / 1024**3 + total = torch.cuda.get_device_properties(device).total_memory / 1024**3 - return '{:.2f} G/{:.2f} G'.format(reserved, total) + return "{:.2f} G/{:.2f} G".format(reserved, total) diff --git a/recbole/utils/wandblogger.py b/recbole/utils/wandblogger.py index 7f2884a46..9b4c0c5bc 100644 --- a/recbole/utils/wandblogger.py +++ b/recbole/utils/wandblogger.py @@ -8,10 +8,10 @@ ################################ """ + class WandbLogger(object): - """WandbLogger to log metrics to Weights and Biases. + """WandbLogger to log metrics to Weights and Biases.""" - """ def __init__(self, config): """ Args: @@ -20,11 +20,12 @@ def __init__(self, config): self.config = config self.log_wandb = config.log_wandb self.setup() - + def setup(self): if self.log_wandb: try: import wandb + self._wandb = wandb except ImportError: raise ImportError( @@ -34,14 +35,11 @@ def setup(self): # Initialize a W&B run if self._wandb.run is None: - self._wandb.init( - project=self.config.wandb_project, - config=self.config - ) + self._wandb.init(project=self.config.wandb_project, config=self.config) self._set_steps() - def log_metrics(self, metrics, head='train', commit=True): + def log_metrics(self, metrics, head="train", commit=True): if self.log_wandb: if head: metrics = self._add_head_to_metrics(metrics, head) @@ -49,23 +47,22 @@ def log_metrics(self, metrics, head='train', commit=True): else: self._wandb.log(metrics, commit=commit) - def log_eval_metrics(self, metrics, head='eval'): + def log_eval_metrics(self, metrics, head="eval"): if self.log_wandb: metrics = self._add_head_to_metrics(metrics, head) for k, v in metrics.items(): self._wandb.run.summary[k] = v def _set_steps(self): - self._wandb.define_metric('train/*', step_metric='train_step') - self._wandb.define_metric('valid/*', step_metric='valid_step') + self._wandb.define_metric("train/*", step_metric="train_step") + self._wandb.define_metric("valid/*", step_metric="valid_step") def _add_head_to_metrics(self, metrics, head): head_metrics = dict() for k, v in metrics.items(): - if '_step' in k: + if "_step" in k: head_metrics[k] = v else: - head_metrics[f'{head}/{k}'] = v + head_metrics[f"{head}/{k}"] = v return head_metrics - \ No newline at end of file diff --git a/run_example/case_study_example.py b/run_example/case_study_example.py index c23ff9b09..d551c1a0b 100644 --- a/run_example/case_study_example.py +++ b/run_example/case_study_example.py @@ -15,22 +15,26 @@ from recbole.quick_start import load_data_and_model -if __name__ == '__main__': +if __name__ == "__main__": config, model, dataset, train_data, valid_data, test_data = load_data_and_model( - model_file='../saved/BPR-Aug-20-2021_03-32-13.pth', + model_file="../saved/BPR-Aug-20-2021_03-32-13.pth", ) # Here you can replace it by your model path. # uid_series = np.array([1, 2]) # internal user id series # or you can use dataset.token2id to transfer external user token to internal user id - uid_series = dataset.token2id(dataset.uid_field, ['196', '186']) + uid_series = dataset.token2id(dataset.uid_field, ["196", "186"]) - topk_score, topk_iid_list = full_sort_topk(uid_series, model, test_data, k=10, device=config['device']) + topk_score, topk_iid_list = full_sort_topk( + uid_series, model, test_data, k=10, device=config["device"] + ) print(topk_score) # scores of top 10 items print(topk_iid_list) # internal id of top 10 items external_item_list = dataset.id2token(dataset.iid_field, topk_iid_list.cpu()) print(external_item_list) # external tokens of top 10 items print() - score = full_sort_scores(uid_series, model, test_data, device=config['device']) + score = full_sort_scores(uid_series, model, test_data, device=config["device"]) print(score) # score of all items - print(score[0, dataset.token2id(dataset.iid_field, ['242', '302'])]) # score of item ['242', '302'] for user '196'. + print( + score[0, dataset.token2id(dataset.iid_field, ["242", "302"])] + ) # score of item ['242', '302'] for user '196'. diff --git a/run_example/save_and_load_example.py b/run_example/save_and_load_example.py index ce4f2c5ac..2132a696f 100644 --- a/run_example/save_and_load_example.py +++ b/run_example/save_and_load_example.py @@ -17,40 +17,40 @@ def save_example(): # configurations initialization config_dict = { - 'checkpoint_dir': '../saved', - 'save_dataset': True, - 'save_dataloaders': True, + "checkpoint_dir": "../saved", + "save_dataset": True, + "save_dataloaders": True, } - run_recbole(model='BPR', dataset='ml-100k', config_dict=config_dict) + run_recbole(model="BPR", dataset="ml-100k", config_dict=config_dict) def load_example(): # Filtered dataset and split dataloaders are created according to 'config'. config, model, dataset, train_data, valid_data, test_data = load_data_and_model( - model_file='../saved/BPR-Aug-20-2021_03-32-13.pth', + model_file="../saved/BPR-Aug-20-2021_03-32-13.pth", ) # Filtered dataset is loaded from file, and split dataloaders are created according to 'config'. config, model, dataset, train_data, valid_data, test_data = load_data_and_model( - model_file='../saved/BPR-Aug-20-2021_03-32-13.pth', - dataset_file='../saved/ml-100k-dataset.pth', + model_file="../saved/BPR-Aug-20-2021_03-32-13.pth", + dataset_file="../saved/ml-100k-dataset.pth", ) # Dataset is neither created nor loaded, and split dataloaders are loaded from file. config, model, dataset, train_data, valid_data, test_data = load_data_and_model( - model_file='../saved/BPR-Aug-20-2021_03-32-13.pth', - dataloader_file='../saved/ml-100k-for-BPR-dataloader.pth', + model_file="../saved/BPR-Aug-20-2021_03-32-13.pth", + dataloader_file="../saved/ml-100k-for-BPR-dataloader.pth", ) assert dataset is None # Filtered dataset and split dataloaders are loaded from file. config, model, dataset, train_data, valid_data, test_data = load_data_and_model( - model_file='../saved/BPR-Aug-20-2021_03-32-13.pth', - dataset_file='../saved/ml-100k-dataset.pth', - dataloader_file='../saved/ml-100k-for-BPR-dataloader.pth', + model_file="../saved/BPR-Aug-20-2021_03-32-13.pth", + dataset_file="../saved/ml-100k-dataset.pth", + dataloader_file="../saved/ml-100k-for-BPR-dataloader.pth", ) -if __name__ == '__main__': +if __name__ == "__main__": save_example() # load_example() diff --git a/run_example/session_based_rec_example.py b/run_example/session_based_rec_example.py index 0b6d05018..1ecbe3e2d 100644 --- a/run_example/session_based_rec_example.py +++ b/run_example/session_based_rec_example.py @@ -26,30 +26,50 @@ def get_args(): parser = argparse.ArgumentParser() - parser.add_argument('--model', '-m', type=str, default='GRU4Rec', help='Model for session-based rec.') - parser.add_argument('--dataset', '-d', type=str, default='diginetica-session', help='Benchmarks for session-based rec.') - parser.add_argument('--validation', action='store_true', help='Whether evaluating on validation set (split from train set), otherwise on test set.') - parser.add_argument('--valid_portion', type=float, default=0.1, help='ratio of validation set.') + parser.add_argument( + "--model", + "-m", + type=str, + default="GRU4Rec", + help="Model for session-based rec.", + ) + parser.add_argument( + "--dataset", + "-d", + type=str, + default="diginetica-session", + help="Benchmarks for session-based rec.", + ) + parser.add_argument( + "--validation", + action="store_true", + help="Whether evaluating on validation set (split from train set), otherwise on test set.", + ) + parser.add_argument( + "--valid_portion", type=float, default=0.1, help="ratio of validation set." + ) return parser.parse_known_args()[0] -if __name__ == '__main__': +if __name__ == "__main__": args = get_args() # configurations initialization config_dict = { - 'USER_ID_FIELD': 'session_id', - 'load_col': None, - 'neg_sampling': None, - 'benchmark_filename': ['train', 'test'], - 'alias_of_item_id': ['item_id_list'], - 'topk': [20], - 'metrics': ['Recall', 'MRR'], - 'valid_metric': 'MRR@20' + "USER_ID_FIELD": "session_id", + "load_col": None, + "neg_sampling": None, + "benchmark_filename": ["train", "test"], + "alias_of_item_id": ["item_id_list"], + "topk": [20], + "metrics": ["Recall", "MRR"], + "valid_metric": "MRR@20", } - config = Config(model=args.model, dataset=f'{args.dataset}', config_dict=config_dict) - init_seed(config['seed'], config['reproducibility']) + config = Config( + model=args.model, dataset=f"{args.dataset}", config_dict=config_dict + ) + init_seed(config["seed"], config["reproducibility"]) # logger initialization init_logger(config) @@ -66,23 +86,33 @@ def get_args(): train_dataset, test_dataset = dataset.build() if args.validation: train_dataset.shuffle() - new_train_dataset, new_test_dataset = train_dataset.split_by_ratio([1 - args.valid_portion, args.valid_portion]) - train_data = get_dataloader(config, 'train')(config, new_train_dataset, None, shuffle=True) - test_data = get_dataloader(config, 'test')(config, new_test_dataset, None, shuffle=False) + new_train_dataset, new_test_dataset = train_dataset.split_by_ratio( + [1 - args.valid_portion, args.valid_portion] + ) + train_data = get_dataloader(config, "train")( + config, new_train_dataset, None, shuffle=True + ) + test_data = get_dataloader(config, "test")( + config, new_test_dataset, None, shuffle=False + ) else: - train_data = get_dataloader(config, 'train')(config, train_dataset, None, shuffle=True) - test_data = get_dataloader(config, 'test')(config, test_dataset, None, shuffle=False) + train_data = get_dataloader(config, "train")( + config, train_dataset, None, shuffle=True + ) + test_data = get_dataloader(config, "test")( + config, test_dataset, None, shuffle=False + ) # model loading and initialization - model = get_model(config['model'])(config, train_data.dataset).to(config['device']) + model = get_model(config["model"])(config, train_data.dataset).to(config["device"]) logger.info(model) # trainer loading and initialization - trainer = get_trainer(config['MODEL_TYPE'], config['model'])(config, model) + trainer = get_trainer(config["MODEL_TYPE"], config["model"])(config, model) # model training and evaluation test_score, test_result = trainer.fit( - train_data, test_data, saved=True, show_progress=config['show_progress'] + train_data, test_data, saved=True, show_progress=config["show_progress"] ) - logger.info(set_color('test result', 'yellow') + f': {test_result}') + logger.info(set_color("test result", "yellow") + f": {test_result}") diff --git a/run_hyper.py b/run_hyper.py index c0e422aae..55fedf905 100644 --- a/run_hyper.py +++ b/run_hyper.py @@ -16,21 +16,31 @@ def main(): parser = argparse.ArgumentParser() - parser.add_argument('--config_files', type=str, default=None, help='fixed config files') - parser.add_argument('--params_file', type=str, default=None, help='parameters file') - parser.add_argument('--output_file', type=str, default='hyper_example.result', help='output file') + parser.add_argument( + "--config_files", type=str, default=None, help="fixed config files" + ) + parser.add_argument("--params_file", type=str, default=None, help="parameters file") + parser.add_argument( + "--output_file", type=str, default="hyper_example.result", help="output file" + ) args, _ = parser.parse_known_args() # plz set algo='exhaustive' to use exhaustive search, in this case, max_evals is auto set - config_file_list = args.config_files.strip().split(' ') if args.config_files else None - hp = HyperTuning(objective_function, algo='exhaustive', - params_file=args.params_file, fixed_config_file_list=config_file_list) + config_file_list = ( + args.config_files.strip().split(" ") if args.config_files else None + ) + hp = HyperTuning( + objective_function, + algo="exhaustive", + params_file=args.params_file, + fixed_config_file_list=config_file_list, + ) hp.run() hp.export_result(output_file=args.output_file) - print('best params: ', hp.best_params) - print('best result: ') + print("best params: ", hp.best_params) + print("best result: ") print(hp.params2result[hp.params2str(hp.best_params)]) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/run_recbole.py b/run_recbole.py index a9a6b189e..3d86e1654 100644 --- a/run_recbole.py +++ b/run_recbole.py @@ -12,28 +12,51 @@ from recbole.quick_start import run_recbole, run_recboles -if __name__ == '__main__': +if __name__ == "__main__": parser = argparse.ArgumentParser() - parser.add_argument('--model', '-m', type=str, default='BPR', help='name of models') - parser.add_argument('--dataset', '-d', type=str, default='ml-100k', help='name of datasets') - parser.add_argument('--config_files', type=str, default=None, help='config files') - parser.add_argument('--nproc', type=int, default=1, help='the number of process in this group') - parser.add_argument('--ip', type=str, default='localhost', help='the ip of master node') - parser.add_argument('--port', type=str, default='5678', help='the port of master node') - parser.add_argument('--world_size', type=int, default=-1, help='total number of jobs') + parser.add_argument("--model", "-m", type=str, default="BPR", help="name of models") + parser.add_argument( + "--dataset", "-d", type=str, default="ml-100k", help="name of datasets" + ) + parser.add_argument("--config_files", type=str, default=None, help="config files") + parser.add_argument( + "--nproc", type=int, default=1, help="the number of process in this group" + ) + parser.add_argument( + "--ip", type=str, default="localhost", help="the ip of master node" + ) + parser.add_argument( + "--port", type=str, default="5678", help="the port of master node" + ) + parser.add_argument( + "--world_size", type=int, default=-1, help="total number of jobs" + ) args, _ = parser.parse_known_args() - config_file_list = args.config_files.strip().split(' ') if args.config_files else None + config_file_list = ( + args.config_files.strip().split(" ") if args.config_files else None + ) if args.nproc == 1: - run_recbole(model=args.model, dataset=args.dataset, config_file_list=config_file_list) + run_recbole( + model=args.model, dataset=args.dataset, config_file_list=config_file_list + ) else: if args.world_size == -1: args.world_size = args.nproc import torch.multiprocessing as mp + mp.spawn( run_recboles, - args=(args.model, args.dataset, config_file_list, args.ip, args.port, args.world_size, args.nproc), - nprocs=args.nproc + args=( + args.model, + args.dataset, + config_file_list, + args.ip, + args.port, + args.world_size, + args.nproc, + ), + nprocs=args.nproc, ) diff --git a/setup.py b/setup.py index ca9cb142c..0e526bbdc 100644 --- a/setup.py +++ b/setup.py @@ -6,48 +6,53 @@ from setuptools import setup, find_packages -install_requires = ['numpy>=1.17.2', 'torch>=1.7.0', 'scipy==1.6.0', 'pandas>=1.0.5', 'tqdm>=4.48.2', - 'colorlog==4.7.2','colorama==0.4.4', - 'scikit_learn>=0.23.2', 'pyyaml>=5.1.0', 'tensorboard>=2.5.0'] +install_requires = [ + "numpy>=1.17.2", + "torch>=1.7.0", + "scipy==1.6.0", + "pandas>=1.0.5", + "tqdm>=4.48.2", + "colorlog==4.7.2", + "colorama==0.4.4", + "scikit_learn>=0.23.2", + "pyyaml>=5.1.0", + "tensorboard>=2.5.0", +] setup_requires = [] -extras_require = { - 'hyperopt': ['hyperopt>=0.2.4'] -} +extras_require = {"hyperopt": ["hyperopt>=0.2.4"]} classifiers = ["License :: OSI Approved :: MIT License"] -long_description = 'RecBole is developed based on Python and PyTorch for ' \ - 'reproducing and developing recommendation algorithms in ' \ - 'a unified, comprehensive and efficient framework for ' \ - 'research purpose. In the first version, our library ' \ - 'includes 53 recommendation algorithms, covering four ' \ - 'major categories: General Recommendation, Sequential ' \ - 'Recommendation, Context-aware Recommendation and ' \ - 'Knowledge-based Recommendation. View RecBole homepage ' \ - 'for more information: https://recbole.io' +long_description = ( + "RecBole is developed based on Python and PyTorch for " + "reproducing and developing recommendation algorithms in " + "a unified, comprehensive and efficient framework for " + "research purpose. In the first version, our library " + "includes 53 recommendation algorithms, covering four " + "major categories: General Recommendation, Sequential " + "Recommendation, Context-aware Recommendation and " + "Knowledge-based Recommendation. View RecBole homepage " + "for more information: https://recbole.io" +) # Readthedocs requires Sphinx extensions to be specified as part of # install_requires in order to build properly. -on_rtd = os.environ.get('READTHEDOCS', None) == 'True' +on_rtd = os.environ.get("READTHEDOCS", None) == "True" if on_rtd: install_requires.extend(setup_requires) setup( - name='recbole', - version= - '1.0.1', # please remember to edit recbole/__init__.py in response, once updating the version - description='A unified, comprehensive and efficient recommendation library', + name="recbole", + version="1.0.1", # please remember to edit recbole/__init__.py in response, once updating the version + description="A unified, comprehensive and efficient recommendation library", long_description=long_description, long_description_content_type="text/markdown", - url='https://github.com/RUCAIBox/RecBole', - author='RecBoleTeam', - author_email='recbole@outlook.com', - packages=[ - package for package in find_packages() - if package.startswith('recbole') - ], + url="https://github.com/RUCAIBox/RecBole", + author="RecBoleTeam", + author_email="recbole@outlook.com", + packages=[package for package in find_packages() if package.startswith("recbole")], include_package_data=True, install_requires=install_requires, setup_requires=setup_requires, diff --git a/tests/config/test_command_line.py b/tests/config/test_command_line.py index d8916f1ee..cfd9ddce7 100644 --- a/tests/config/test_command_line.py +++ b/tests/config/test_command_line.py @@ -11,18 +11,18 @@ from recbole.config import Config -if __name__ == '__main__': +if __name__ == "__main__": - config = Config(model='BPR', dataset='ml-100k') + config = Config(model="BPR", dataset="ml-100k") # command line - assert config['use_gpu'] is False - assert config['valid_metric'] == 'Recall@10' - assert config['metrics'] == ['Recall'] # bug + assert config["use_gpu"] is False + assert config["valid_metric"] == "Recall@10" + assert config["metrics"] == ["Recall"] # bug # priority - assert config['epochs'] == 200 - assert config['learning_rate'] == 0.3 + assert config["epochs"] == 200 + assert config["learning_rate"] == 0.3 - print('------------------------------------------------------------') - print('OK') + print("------------------------------------------------------------") + print("OK") diff --git a/tests/config/test_config.py b/tests/config/test_config.py index 41ac44fe8..e0a3f2f18 100644 --- a/tests/config/test_config.py +++ b/tests/config/test_config.py @@ -14,103 +14,113 @@ parameters_dict = { - 'model': 'SASRec', - 'learning_rate': 0.2, - 'topk': [50, 100], - 'epochs': 100, + "model": "SASRec", + "learning_rate": 0.2, + "topk": [50, 100], + "epochs": 100, } current_path = os.path.dirname(os.path.realpath(__file__)) -config_file_list = [os.path.join(current_path, 'test_config_example.yaml')] +config_file_list = [os.path.join(current_path, "test_config_example.yaml")] class TestConfigClass(unittest.TestCase): - def test_default_settings(self): - config = Config(model='BPR', dataset='ml-100k') - - self.assertEqual(config['model'], 'BPR') - self.assertEqual(config['dataset'], 'ml-100k') - - self.assertIsInstance(config['gpu_id'], str) - self.assertIsInstance(config['worker'], int) - self.assertIsInstance(config['seed'], int) - self.assertIsInstance(config['state'], str) - self.assertIsInstance(config['data_path'], str) - - self.assertIsInstance(config['epochs'], int) - self.assertIsInstance(config['train_batch_size'], int) - self.assertIsInstance(config['learner'], str) - self.assertIsInstance(config['learning_rate'], float) - self.assertIsInstance(config['train_neg_sample_args'], dict) - self.assertIsInstance(config['eval_step'], int) - self.assertIsInstance(config['stopping_step'], int) - self.assertIsInstance(config['checkpoint_dir'], str) - - self.assertIsInstance(config['eval_args'], dict) - self.assertIsInstance(config['metrics'], list) - self.assertIsInstance(config['topk'], list) - self.assertIsInstance(config['valid_metric'], str) - self.assertIsInstance(config['eval_batch_size'], int) + config = Config(model="BPR", dataset="ml-100k") + + self.assertEqual(config["model"], "BPR") + self.assertEqual(config["dataset"], "ml-100k") + + self.assertIsInstance(config["gpu_id"], str) + self.assertIsInstance(config["worker"], int) + self.assertIsInstance(config["seed"], int) + self.assertIsInstance(config["state"], str) + self.assertIsInstance(config["data_path"], str) + + self.assertIsInstance(config["epochs"], int) + self.assertIsInstance(config["train_batch_size"], int) + self.assertIsInstance(config["learner"], str) + self.assertIsInstance(config["learning_rate"], float) + self.assertIsInstance(config["train_neg_sample_args"], dict) + self.assertIsInstance(config["eval_step"], int) + self.assertIsInstance(config["stopping_step"], int) + self.assertIsInstance(config["checkpoint_dir"], str) + + self.assertIsInstance(config["eval_args"], dict) + self.assertIsInstance(config["metrics"], list) + self.assertIsInstance(config["topk"], list) + self.assertIsInstance(config["valid_metric"], str) + self.assertIsInstance(config["eval_batch_size"], int) def test_default_context_settings(self): - config = Config(model='FM', dataset='ml-100k') - - self.assertEqual(config['eval_args']['split'], {'RS': [0.8,0.1,0.1]}) - self.assertEqual(config['eval_args']['order'], 'RO') - self.assertEqual(config['eval_args']['mode'],'labeled') - self.assertEqual(config['eval_args']['group_by'], None) - - self.assertEqual(config['metrics'], ['AUC', 'LogLoss']) - self.assertEqual(config['valid_metric'], 'AUC') - self.assertEqual(config['train_neg_sample_args'], {'distribution': 'none', 'sample_num': 'none', - 'dynamic': False, 'candidate_num': 0}) + config = Config(model="FM", dataset="ml-100k") + + self.assertEqual(config["eval_args"]["split"], {"RS": [0.8, 0.1, 0.1]}) + self.assertEqual(config["eval_args"]["order"], "RO") + self.assertEqual(config["eval_args"]["mode"], "labeled") + self.assertEqual(config["eval_args"]["group_by"], None) + + self.assertEqual(config["metrics"], ["AUC", "LogLoss"]) + self.assertEqual(config["valid_metric"], "AUC") + self.assertEqual( + config["train_neg_sample_args"], + { + "distribution": "none", + "sample_num": "none", + "dynamic": False, + "candidate_num": 0, + }, + ) def test_default_sequential_settings(self): - para_dict = { - 'train_neg_sample_args': None - } - config = Config(model='SASRec', dataset='ml-100k', config_dict=para_dict) - self.assertEqual(config['eval_args']['split'], {'LS': 'valid_and_test'}) - self.assertEqual(config['eval_args']['order'], 'TO') - self.assertEqual(config['eval_args']['mode'],'full') - self.assertEqual(config['eval_args']['group_by'], 'user') - - def test_config_file_list(self): - config = Config(model='BPR', dataset='ml-100k', config_file_list=config_file_list) + para_dict = {"train_neg_sample_args": None} + config = Config(model="SASRec", dataset="ml-100k", config_dict=para_dict) + self.assertEqual(config["eval_args"]["split"], {"LS": "valid_and_test"}) + self.assertEqual(config["eval_args"]["order"], "TO") + self.assertEqual(config["eval_args"]["mode"], "full") + self.assertEqual(config["eval_args"]["group_by"], "user") - self.assertEqual(config['model'], 'BPR') - self.assertEqual(config['learning_rate'], 0.1) - self.assertEqual(config['topk'], [5, 20]) - self.assertEqual(config['eval_args']['split'], {'LS': 'valid_and_test'}) - self.assertEqual(config['eval_args']['order'], 'TO') - self.assertEqual(config['eval_args']['mode'],'full') - self.assertEqual(config['eval_args']['group_by'], 'user') + def test_config_file_list(self): + config = Config( + model="BPR", dataset="ml-100k", config_file_list=config_file_list + ) + + self.assertEqual(config["model"], "BPR") + self.assertEqual(config["learning_rate"], 0.1) + self.assertEqual(config["topk"], [5, 20]) + self.assertEqual(config["eval_args"]["split"], {"LS": "valid_and_test"}) + self.assertEqual(config["eval_args"]["order"], "TO") + self.assertEqual(config["eval_args"]["mode"], "full") + self.assertEqual(config["eval_args"]["group_by"], "user") def test_config_dict(self): - config = Config(model='BPR', dataset='ml-100k', config_dict=parameters_dict) + config = Config(model="BPR", dataset="ml-100k", config_dict=parameters_dict) - self.assertEqual(config['model'], 'BPR') - self.assertEqual(config['learning_rate'], 0.2) - self.assertEqual(config['topk'], [50, 100]) - self.assertEqual(config['eval_args']['split'], {'RS': [0.8, 0.1, 0.1]}) - self.assertEqual(config['eval_args']['order'], 'RO') - self.assertEqual(config['eval_args']['mode'],'full') - self.assertEqual(config['eval_args']['group_by'], 'user') + self.assertEqual(config["model"], "BPR") + self.assertEqual(config["learning_rate"], 0.2) + self.assertEqual(config["topk"], [50, 100]) + self.assertEqual(config["eval_args"]["split"], {"RS": [0.8, 0.1, 0.1]}) + self.assertEqual(config["eval_args"]["order"], "RO") + self.assertEqual(config["eval_args"]["mode"], "full") + self.assertEqual(config["eval_args"]["group_by"], "user") # todo: add command line test examples def test_priority(self): - config = Config(model='BPR', dataset='ml-100k', - config_file_list=config_file_list, config_dict=parameters_dict) - - self.assertEqual(config['learning_rate'], 0.2) # default, file, dict - self.assertEqual(config['topk'], [50, 100]) # default, file, dict - self.assertEqual(config['eval_args']['split'], {'LS': 'valid_and_test'}) - self.assertEqual(config['eval_args']['order'], 'TO') - self.assertEqual(config['eval_args']['mode'],'full') - self.assertEqual(config['eval_args']['group_by'], 'user') - self.assertEqual(config['epochs'], 100) # default, dict - - -if __name__ == '__main__': + config = Config( + model="BPR", + dataset="ml-100k", + config_file_list=config_file_list, + config_dict=parameters_dict, + ) + + self.assertEqual(config["learning_rate"], 0.2) # default, file, dict + self.assertEqual(config["topk"], [50, 100]) # default, file, dict + self.assertEqual(config["eval_args"]["split"], {"LS": "valid_and_test"}) + self.assertEqual(config["eval_args"]["order"], "TO") + self.assertEqual(config["eval_args"]["mode"], "full") + self.assertEqual(config["eval_args"]["group_by"], "user") + self.assertEqual(config["epochs"], 100) # default, dict + + +if __name__ == "__main__": unittest.main() diff --git a/tests/config/test_overall.py b/tests/config/test_overall.py index bd4a2ff22..835bd44d0 100644 --- a/tests/config/test_overall.py +++ b/tests/config/test_overall.py @@ -21,115 +21,141 @@ def run_parms(parm_dict, extra_dict=None): - config_dict = { - 'epochs': 1, - 'state': 'INFO' - } + config_dict = {"epochs": 1, "state": "INFO"} for name, parms in parm_dict.items(): for parm in parms: config_dict[name] = parm if extra_dict is not None: config_dict.update(extra_dict) try: - run_recbole(model='BPR', dataset='ml-100k', config_dict=config_dict) + run_recbole(model="BPR", dataset="ml-100k", config_dict=config_dict) except Exception as e: - print(f'\ntest `{name}`={parm} ... fail.\n') - logging.critical(f'\ntest `{name}`={parm} ... fail.\n') + print(f"\ntest `{name}`={parm} ... fail.\n") + logging.critical(f"\ntest `{name}`={parm} ... fail.\n") return False return True class TestOverallConfig(unittest.TestCase): - def setUp(self): - warnings.simplefilter('ignore', ResourceWarning) + warnings.simplefilter("ignore", ResourceWarning) def test_gpu_id(self): - self.assertTrue(run_parms({'gpu_id': ['0', '-1', '1']})) + self.assertTrue(run_parms({"gpu_id": ["0", "-1", "1"]})) def test_use_gpu(self): - self.assertTrue(run_parms({'use_gpu': [True, False]})) + self.assertTrue(run_parms({"use_gpu": [True, False]})) def test_reproducibility(self): - self.assertTrue(run_parms({'reproducibility': [True, False]})) + self.assertTrue(run_parms({"reproducibility": [True, False]})) def test_seed(self): - self.assertTrue(run_parms({'seed': [2021, 1024]})) + self.assertTrue(run_parms({"seed": [2021, 1024]})) def test_data_path(self): - self.assertTrue(run_parms({'data_path': ['dataset/', './dataset']})) + self.assertTrue(run_parms({"data_path": ["dataset/", "./dataset"]})) def test_epochs(self): - self.assertTrue(run_parms({'epochs': [0, 1, 2]})) + self.assertTrue(run_parms({"epochs": [0, 1, 2]})) def test_train_batch_size(self): - self.assertTrue(run_parms({'train_batch_size': [1, 2048, 200000]})) + self.assertTrue(run_parms({"train_batch_size": [1, 2048, 200000]})) def test_learner(self): - self.assertTrue(run_parms({'learner': ["adam", "sgd", "foo"]})) + self.assertTrue(run_parms({"learner": ["adam", "sgd", "foo"]})) def test_learning_rate(self): - self.assertTrue(run_parms({'learning_rate': [0, 0.001, 1e-5]})) + self.assertTrue(run_parms({"learning_rate": [0, 0.001, 1e-5]})) def test_training_neg_sampling(self): - self.assertTrue(run_parms({'train_neg_sample_args': [{'distribution': 'uniform', 'sample_num': 1}, {'distribution': 'uniform', 'sample_num': 2}, {'distribution': 'uniform', 'sample_num': 3}]})) + self.assertTrue( + run_parms( + { + "train_neg_sample_args": [ + {"distribution": "uniform", "sample_num": 1}, + {"distribution": "uniform", "sample_num": 2}, + {"distribution": "uniform", "sample_num": 3}, + ] + } + ) + ) def test_eval_step(self): - settings = { - 'epochs': 5 - } - self.assertTrue(run_parms({'eval_step': [1, 2]})) + settings = {"epochs": 5} + self.assertTrue(run_parms({"eval_step": [1, 2]})) def test_stopping_step(self): - settings = { - 'epochs': 100 - } - self.assertTrue(run_parms({'stopping_step': [0, 1, 2]})) + settings = {"epochs": 100} + self.assertTrue(run_parms({"stopping_step": [0, 1, 2]})) def test_checkpoint_dir(self): - self.assertTrue(run_parms({'checkpoint_dir': ['saved_1/', './saved_2']})) + self.assertTrue(run_parms({"checkpoint_dir": ["saved_1/", "./saved_2"]})) def test_eval_batch_size(self): - self.assertTrue(run_parms({'eval_batch_size': [1, 100]})) + self.assertTrue(run_parms({"eval_batch_size": [1, 100]})) def test_topk(self): settings = { - 'metrics': ["Recall", "MRR", "NDCG", "Hit", "Precision"], - 'valid_metric': 'Recall@1' + "metrics": ["Recall", "MRR", "NDCG", "Hit", "Precision"], + "valid_metric": "Recall@1", } - self.assertTrue(run_parms({'topk': [1, [1, 3]]}, extra_dict=settings)) + self.assertTrue(run_parms({"topk": [1, [1, 3]]}, extra_dict=settings)) def test_loss(self): settings = { - 'metrics': ["MAE", "RMSE", "LOGLOSS", "AUC"], - 'valid_metric': 'auc', - 'eval_args': {'split': {'RS': [0.8, 0.1, 0.1]}, 'order': 'RO', 'mode': 'uni100'} + "metrics": ["MAE", "RMSE", "LOGLOSS", "AUC"], + "valid_metric": "auc", + "eval_args": { + "split": {"RS": [0.8, 0.1, 0.1]}, + "order": "RO", + "mode": "uni100", + }, } - self.assertTrue(run_parms({'topk': {1, }}, extra_dict=settings)) + self.assertTrue( + run_parms( + { + "topk": { + 1, + } + }, + extra_dict=settings, + ) + ) def test_metric(self): - settings = { - 'topk': 3, - 'valid_metric': 'Recall@3' - } + settings = {"topk": 3, "valid_metric": "Recall@3"} self.assertTrue( - run_parms({'metrics': ["Recall", ["Recall", "MRR", "NDCG", "Hit", "Precision"]]}, extra_dict=settings) + run_parms( + {"metrics": ["Recall", ["Recall", "MRR", "NDCG", "Hit", "Precision"]]}, + extra_dict=settings, + ) ) def test_split_ratio(self): - self.assertTrue(run_parms({'eval_args': [{'split': {'RS': [0.8, 0.1, 0.1]}}, {'split': {'RS': [16, 2, 2]}}]})) + self.assertTrue( + run_parms( + { + "eval_args": [ + {"split": {"RS": [0.8, 0.1, 0.1]}}, + {"split": {"RS": [16, 2, 2]}}, + ] + } + ) + ) def test_group_by_user(self): - self.assertTrue(run_parms({'eval_args': [{'group_by': 'user'}, {'group_by': 'None'}]})) + self.assertTrue( + run_parms({"eval_args": [{"group_by": "user"}, {"group_by": "None"}]}) + ) def test_use_mixed_precision(self): - self.assertTrue(run_parms({'enable_amp': [True, False]})) + self.assertTrue(run_parms({"enable_amp": [True, False]})) def test_use_grad_scaler(self): - self.assertTrue(run_parms({'enable_scaler': [True, False]})) - + self.assertTrue(run_parms({"enable_scaler": [True, False]})) + -if __name__ == '__main__': +if __name__ == "__main__": # suite = unittest.TestSuite() # suite.addTest(TestOverallConfig('test_split_ratio')) # runner = unittest.TextTestRunner(verbosity=2) diff --git a/tests/data/test_dataloader.py b/tests/data/test_dataloader.py index 005304ccf..96c5de16a 100644 --- a/tests/data/test_dataloader.py +++ b/tests/data/test_dataloader.py @@ -22,7 +22,7 @@ def new_dataloader(config_dict=None, config_file_list=None): config = Config(config_dict=config_dict, config_file_list=config_file_list) - init_seed(config['seed'], config['reproducibility']) + init_seed(config["seed"], config["reproducibility"]) logging.basicConfig(level=logging.ERROR) dataset = create_dataset(config) return data_preparation(config, dataset) @@ -33,15 +33,19 @@ def test_general_dataloader(self): train_batch_size = 6 eval_batch_size = 2 config_dict = { - 'model': 'BPR', - 'dataset': 'general_dataloader', - 'data_path': current_path, - 'load_col': None, - 'eval_args': {'split': {'RS': [0.8, 0.1, 0.1]}, 'order': 'TO', 'mode': 'labeled'}, - 'train_neg_sample_args': None, - 'train_batch_size': train_batch_size, - 'eval_batch_size': eval_batch_size, - 'shuffle': False + "model": "BPR", + "dataset": "general_dataloader", + "data_path": current_path, + "load_col": None, + "eval_args": { + "split": {"RS": [0.8, 0.1, 0.1]}, + "order": "TO", + "mode": "labeled", + }, + "train_neg_sample_args": None, + "train_batch_size": train_batch_size, + "eval_batch_size": eval_batch_size, + "shuffle": False, } train_data, valid_data, test_data = new_dataloader(config_dict=config_dict) @@ -49,12 +53,12 @@ def check_dataloader(data, item_list, batch_size, train=False): data.shuffle = False pr = 0 for batch_data in data: - batch_item_list = item_list[pr: pr + batch_size] + batch_item_list = item_list[pr : pr + batch_size] if train: user_df = batch_data else: user_df = batch_data[0] - assert (user_df['item_id'].numpy() == batch_item_list).all() + assert (user_df["item_id"].numpy() == batch_item_list).all() pr += batch_size check_dataloader(train_data, list(range(1, 41)), train_batch_size, True) @@ -65,15 +69,19 @@ def test_general_neg_sample_dataloader_in_pair_wise(self): train_batch_size = 6 eval_batch_size = 100 config_dict = { - 'model': 'BPR', - 'dataset': 'general_dataloader', - 'data_path': current_path, - 'load_col': None, - 'train_neg_sample_args': {'distribution': 'uniform', 'sample_num': 1}, - 'eval_args': {'split': {'RS': [0.8, 0.1, 0.1]}, 'order': 'TO', 'mode': 'full'}, - 'train_batch_size': train_batch_size, - 'eval_batch_size': eval_batch_size, - 'shuffle': False + "model": "BPR", + "dataset": "general_dataloader", + "data_path": current_path, + "load_col": None, + "train_neg_sample_args": {"distribution": "uniform", "sample_num": 1}, + "eval_args": { + "split": {"RS": [0.8, 0.1, 0.1]}, + "order": "TO", + "mode": "full", + }, + "train_batch_size": train_batch_size, + "eval_batch_size": eval_batch_size, + "shuffle": False, } train_data, valid_data, test_data = new_dataloader(config_dict=config_dict) @@ -81,27 +89,31 @@ def test_general_neg_sample_dataloader_in_pair_wise(self): train_item_list = list(range(1, 41)) pr = 0 for batch_data in train_data: - batch_item_list = train_item_list[pr: pr + train_batch_size] - assert (batch_data['item_id'].numpy() == batch_item_list).all() - assert (batch_data['item_id'] == batch_data['price']).all() - assert (40 < batch_data['neg_item_id']).all() - assert (batch_data['neg_item_id'] <= 100).all() - assert (batch_data['neg_item_id'] == batch_data['neg_price']).all() + batch_item_list = train_item_list[pr : pr + train_batch_size] + assert (batch_data["item_id"].numpy() == batch_item_list).all() + assert (batch_data["item_id"] == batch_data["price"]).all() + assert (40 < batch_data["neg_item_id"]).all() + assert (batch_data["neg_item_id"] <= 100).all() + assert (batch_data["neg_item_id"] == batch_data["neg_price"]).all() pr += train_batch_size def test_general_neg_sample_dataloader_in_point_wise(self): train_batch_size = 6 eval_batch_size = 100 config_dict = { - 'model': 'DMF', - 'dataset': 'general_dataloader', - 'data_path': current_path, - 'load_col': None, - 'train_neg_sample_args': {'distribution': 'uniform', 'sample_num': 1}, - 'eval_args': {'split': {'RS': [0.8, 0.1, 0.1]}, 'order': 'TO', 'mode': 'full'}, - 'train_batch_size': train_batch_size, - 'eval_batch_size': eval_batch_size, - 'shuffle': False + "model": "DMF", + "dataset": "general_dataloader", + "data_path": current_path, + "load_col": None, + "train_neg_sample_args": {"distribution": "uniform", "sample_num": 1}, + "eval_args": { + "split": {"RS": [0.8, 0.1, 0.1]}, + "order": "TO", + "mode": "full", + }, + "train_batch_size": train_batch_size, + "eval_batch_size": eval_batch_size, + "shuffle": False, } train_data, valid_data, test_data = new_dataloader(config_dict=config_dict) @@ -110,25 +122,29 @@ def test_general_neg_sample_dataloader_in_point_wise(self): pr = 0 for batch_data in train_data: step = len(batch_data) // 2 - batch_item_list = train_item_list[pr: pr + step] - assert (batch_data['item_id'][: step].numpy() == batch_item_list).all() - assert (40 < batch_data['item_id'][step:]).all() - assert (batch_data['item_id'][step:] <= 100).all() - assert (batch_data['item_id'] == batch_data['price']).all() + batch_item_list = train_item_list[pr : pr + step] + assert (batch_data["item_id"][:step].numpy() == batch_item_list).all() + assert (40 < batch_data["item_id"][step:]).all() + assert (batch_data["item_id"][step:] <= 100).all() + assert (batch_data["item_id"] == batch_data["price"]).all() pr += step def test_general_full_dataloader(self): train_batch_size = 6 eval_batch_size = 100 config_dict = { - 'model': 'BPR', - 'dataset': 'general_full_dataloader', - 'data_path': current_path, - 'load_col': None, - 'train_neg_sample_args': {'distribution': 'uniform', 'sample_num': 1}, - 'eval_args': {'split': {'RS': [0.8, 0.1, 0.1]}, 'order': 'TO', 'mode': 'full'}, - 'train_batch_size': train_batch_size, - 'eval_batch_size': eval_batch_size, + "model": "BPR", + "dataset": "general_full_dataloader", + "data_path": current_path, + "load_col": None, + "train_neg_sample_args": {"distribution": "uniform", "sample_num": 1}, + "eval_args": { + "split": {"RS": [0.8, 0.1, 0.1]}, + "order": "TO", + "mode": "full", + }, + "train_batch_size": train_batch_size, + "eval_batch_size": eval_batch_size, } train_data, valid_data, test_data = new_dataloader(config_dict=config_dict) @@ -137,72 +153,74 @@ def check_result(data, result): for i, batch_data in enumerate(data): user_df, history_index, positive_u, positive_i = batch_data history_row, history_col = history_index - assert len(user_df) == result[i]['len_user_df'] - assert (user_df['user_id'].numpy() == result[i]['user_df_user_id']).all() - assert len(history_row) == len(history_col) == result[i]['history_len'] - assert (history_row.numpy() == result[i]['history_row']).all() - assert (history_col.numpy() == result[i]['history_col']).all() - assert (positive_u.numpy() == result[i]['positive_u']).all() - assert (positive_i.numpy() == result[i]['positive_i']).all() + assert len(user_df) == result[i]["len_user_df"] + assert ( + user_df["user_id"].numpy() == result[i]["user_df_user_id"] + ).all() + assert len(history_row) == len(history_col) == result[i]["history_len"] + assert (history_row.numpy() == result[i]["history_row"]).all() + assert (history_col.numpy() == result[i]["history_col"]).all() + assert (positive_u.numpy() == result[i]["positive_u"]).all() + assert (positive_i.numpy() == result[i]["positive_i"]).all() valid_result = [ { - 'len_user_df': 1, - 'user_df_user_id': [1], - 'history_len': 40, - 'history_row': 0, - 'history_col': list(range(1, 41)), - 'positive_u': [0, 0, 0, 0, 0], - 'positive_i': [41, 42, 43, 44, 45] + "len_user_df": 1, + "user_df_user_id": [1], + "history_len": 40, + "history_row": 0, + "history_col": list(range(1, 41)), + "positive_u": [0, 0, 0, 0, 0], + "positive_i": [41, 42, 43, 44, 45], }, { - 'len_user_df': 1, - 'user_df_user_id': [2], - 'history_len': 37, - 'history_row': 0, - 'history_col': list(range(1, 38)), - 'positive_u': [0, 0, 0, 0, 0], - 'positive_i': [38, 39, 40, 41, 42] + "len_user_df": 1, + "user_df_user_id": [2], + "history_len": 37, + "history_row": 0, + "history_col": list(range(1, 38)), + "positive_u": [0, 0, 0, 0, 0], + "positive_i": [38, 39, 40, 41, 42], }, { - 'len_user_df': 1, - 'user_df_user_id': [3], - 'history_len': 0, - 'history_row': [], - 'history_col': [], - 'positive_u': [0], - 'positive_i': [1] + "len_user_df": 1, + "user_df_user_id": [3], + "history_len": 0, + "history_row": [], + "history_col": [], + "positive_u": [0], + "positive_i": [1], }, ] check_result(valid_data, valid_result) test_result = [ { - 'len_user_df': 1, - 'user_df_user_id': [1], - 'history_len': 45, - 'history_row': 0, - 'history_col': list(range(1, 46)), - 'positive_u': [0, 0, 0, 0, 0], - 'positive_i': [46, 47, 48, 49, 50] + "len_user_df": 1, + "user_df_user_id": [1], + "history_len": 45, + "history_row": 0, + "history_col": list(range(1, 46)), + "positive_u": [0, 0, 0, 0, 0], + "positive_i": [46, 47, 48, 49, 50], }, { - 'len_user_df': 1, - 'user_df_user_id': [2], - 'history_len': 37, - 'history_row': 0, - 'history_col': list(range(1, 36)) + [41, 42], - 'positive_u': [0, 0, 0, 0, 0], - 'positive_i': [36, 37, 38, 39, 40] + "len_user_df": 1, + "user_df_user_id": [2], + "history_len": 37, + "history_row": 0, + "history_col": list(range(1, 36)) + [41, 42], + "positive_u": [0, 0, 0, 0, 0], + "positive_i": [36, 37, 38, 39, 40], }, { - 'len_user_df': 1, - 'user_df_user_id': [3], - 'history_len': 0, - 'history_row': [], - 'history_col': [], - 'positive_u': [0], - 'positive_i': [1] + "len_user_df": 1, + "user_df_user_id": [3], + "history_len": 0, + "history_row": [], + "history_col": [], + "positive_u": [0], + "positive_i": [1], }, ] check_result(test_data, test_result) @@ -211,14 +229,18 @@ def test_general_uni100_dataloader_with_batch_size_in_101(self): train_batch_size = 6 eval_batch_size = 101 config_dict = { - 'model': 'BPR', - 'dataset': 'general_uni100_dataloader', - 'data_path': current_path, - 'load_col': None, - 'train_neg_sample_args': {'distribution': 'uniform', 'sample_num': 1}, - 'eval_args': {'split': {'RS': [0.8, 0.1, 0.1]}, 'order': 'TO', 'mode': 'uni100'}, - 'train_batch_size': train_batch_size, - 'eval_batch_size': eval_batch_size, + "model": "BPR", + "dataset": "general_uni100_dataloader", + "data_path": current_path, + "load_col": None, + "train_neg_sample_args": {"distribution": "uniform", "sample_num": 1}, + "eval_args": { + "split": {"RS": [0.8, 0.1, 0.1]}, + "order": "TO", + "mode": "uni100", + }, + "train_batch_size": train_batch_size, + "eval_batch_size": eval_batch_size, } train_data, valid_data, test_data = new_dataloader(config_dict=config_dict) @@ -227,61 +249,59 @@ def check_result(data, result): assert len(data) == len(result) for i, batch_data in enumerate(data): user_df, row_idx, positive_u, positive_i = batch_data - assert result[i]['item_id_check'](user_df['item_id']) - assert (row_idx.numpy() == result[i]['row_idx']).all() - assert (positive_u.numpy() == result[i]['positive_u']).all() - assert (positive_i.numpy() == result[i]['positive_i']).all() + assert result[i]["item_id_check"](user_df["item_id"]) + assert (row_idx.numpy() == result[i]["row_idx"]).all() + assert (positive_u.numpy() == result[i]["positive_u"]).all() + assert (positive_i.numpy() == result[i]["positive_i"]).all() valid_result = [ { - 'item_id_check': lambda data: data[0] == 9 - and (8 < data[1:]).all() - and (data[1:] <= 100).all(), - 'row_idx': [0] * 101, - 'positive_u': [0], - 'positive_i': [9], + "item_id_check": lambda data: data[0] == 9 + and (8 < data[1:]).all() + and (data[1:] <= 100).all(), + "row_idx": [0] * 101, + "positive_u": [0], + "positive_i": [9], }, { - 'item_id_check': lambda data: data[0] == 1 - and (data[1:] != 1).all(), - 'row_idx': [0] * 101, - 'positive_u': [0], - 'positive_i': [1], + "item_id_check": lambda data: data[0] == 1 and (data[1:] != 1).all(), + "row_idx": [0] * 101, + "positive_u": [0], + "positive_i": [1], }, { - 'item_id_check': lambda data: (data[0: 2].numpy() == [17, 18]).all() - and (16 < data[2:]).all() - and (data[2:] <= 100).all(), - 'row_idx': [0] * 202, - 'positive_u': [0, 0], - 'positive_i': [17, 18], + "item_id_check": lambda data: (data[0:2].numpy() == [17, 18]).all() + and (16 < data[2:]).all() + and (data[2:] <= 100).all(), + "row_idx": [0] * 202, + "positive_u": [0, 0], + "positive_i": [17, 18], }, ] check_result(valid_data, valid_result) test_result = [ { - 'item_id_check': lambda data: data[0] == 10 - and (9 < data[1:]).all() - and (data[1:] <= 100).all(), - 'row_idx': [0] * 101, - 'positive_u': [0], - 'positive_i': [10], + "item_id_check": lambda data: data[0] == 10 + and (9 < data[1:]).all() + and (data[1:] <= 100).all(), + "row_idx": [0] * 101, + "positive_u": [0], + "positive_i": [10], }, { - 'item_id_check': lambda data: data[0] == 1 - and (data[1:] != 1).all(), - 'row_idx': [0] * 101, - 'positive_u': [0], - 'positive_i': [1], + "item_id_check": lambda data: data[0] == 1 and (data[1:] != 1).all(), + "row_idx": [0] * 101, + "positive_u": [0], + "positive_i": [1], }, { - 'item_id_check': lambda data: (data[0: 2].numpy() == [19, 20]).all() - and (18 < data[2:]).all() - and (data[2:] <= 100).all(), - 'row_idx': [0] * 202, - 'positive_u': [0, 0], - 'positive_i': [19, 20], + "item_id_check": lambda data: (data[0:2].numpy() == [19, 20]).all() + and (18 < data[2:]).all() + and (data[2:] <= 100).all(), + "row_idx": [0] * 202, + "positive_u": [0, 0], + "positive_i": [19, 20], }, ] check_result(test_data, test_result) @@ -290,14 +310,18 @@ def test_general_uni100_dataloader_with_batch_size_in_303(self): train_batch_size = 6 eval_batch_size = 303 config_dict = { - 'model': 'BPR', - 'dataset': 'general_uni100_dataloader', - 'data_path': current_path, - 'load_col': None, - 'train_neg_sample_args': {'distribution': 'uniform', 'sample_num': 1}, - 'eval_args': {'split': {'RS': [0.8, 0.1, 0.1]}, 'order': 'TO', 'mode': 'uni100'}, - 'train_batch_size': train_batch_size, - 'eval_batch_size': eval_batch_size, + "model": "BPR", + "dataset": "general_uni100_dataloader", + "data_path": current_path, + "load_col": None, + "train_neg_sample_args": {"distribution": "uniform", "sample_num": 1}, + "eval_args": { + "split": {"RS": [0.8, 0.1, 0.1]}, + "order": "TO", + "mode": "uni100", + }, + "train_batch_size": train_batch_size, + "eval_batch_size": eval_batch_size, } train_data, valid_data, test_data = new_dataloader(config_dict=config_dict) @@ -306,55 +330,55 @@ def check_result(data, result): assert len(data) == len(result) for i, batch_data in enumerate(data): user_df, row_idx, positive_u, positive_i = batch_data - assert result[i]['item_id_check'](user_df['item_id']) - assert (row_idx.numpy() == result[i]['row_idx']).all() - assert (positive_u.numpy() == result[i]['positive_u']).all() - assert (positive_i.numpy() == result[i]['positive_i']).all() + assert result[i]["item_id_check"](user_df["item_id"]) + assert (row_idx.numpy() == result[i]["row_idx"]).all() + assert (positive_u.numpy() == result[i]["positive_u"]).all() + assert (positive_i.numpy() == result[i]["positive_i"]).all() valid_result = [ { - 'item_id_check': lambda data: data[0] == 9 - and (8 < data[1: 101]).all() - and (data[1: 101] <= 100).all() - and data[101] == 1 - and (data[102:202] != 1).all(), - 'row_idx': [0] * 101 + [1] * 101, - 'positive_u': [0, 1], - 'positive_i': [9, 1], + "item_id_check": lambda data: data[0] == 9 + and (8 < data[1:101]).all() + and (data[1:101] <= 100).all() + and data[101] == 1 + and (data[102:202] != 1).all(), + "row_idx": [0] * 101 + [1] * 101, + "positive_u": [0, 1], + "positive_i": [9, 1], }, { - 'item_id_check': lambda data: (data[0: 2].numpy() == [17, 18]).all() - and (16 < data[2:]).all() - and (data[2:] <= 100).all(), - 'row_idx': [0] * 202, - 'positive_u': [0, 0], - 'positive_i': [17, 18], + "item_id_check": lambda data: (data[0:2].numpy() == [17, 18]).all() + and (16 < data[2:]).all() + and (data[2:] <= 100).all(), + "row_idx": [0] * 202, + "positive_u": [0, 0], + "positive_i": [17, 18], }, ] check_result(valid_data, valid_result) test_result = [ { - 'item_id_check': lambda data: data[0] == 10 - and (9 < data[1:101]).all() - and (data[1:101] <= 100).all() - and data[101] == 1 - and (data[102:202] != 1).all(), - 'row_idx': [0] * 101 + [1] * 101, - 'positive_u': [0, 1], - 'positive_i': [10, 1], + "item_id_check": lambda data: data[0] == 10 + and (9 < data[1:101]).all() + and (data[1:101] <= 100).all() + and data[101] == 1 + and (data[102:202] != 1).all(), + "row_idx": [0] * 101 + [1] * 101, + "positive_u": [0, 1], + "positive_i": [10, 1], }, { - 'item_id_check': lambda data: (data[0: 2].numpy() == [19, 20]).all() - and (18 < data[2:]).all() - and (data[2:] <= 100).all(), - 'row_idx': [0] * 202, - 'positive_u': [0, 0], - 'positive_i': [19, 20], + "item_id_check": lambda data: (data[0:2].numpy() == [19, 20]).all() + and (18 < data[2:]).all() + and (data[2:] <= 100).all(), + "row_idx": [0] * 202, + "positive_u": [0, 0], + "positive_i": [19, 20], }, ] check_result(test_data, test_result) -if __name__ == '__main__': +if __name__ == "__main__": pytest.main() diff --git a/tests/data/test_dataset.py b/tests/data/test_dataset.py index 8dcfc5c51..c0137f700 100644 --- a/tests/data/test_dataset.py +++ b/tests/data/test_dataset.py @@ -22,7 +22,7 @@ def new_dataset(config_dict=None, config_file_list=None): config = Config(config_dict=config_dict, config_file_list=config_file_list) - init_seed(config['seed'], config['reproducibility']) + init_seed(config["seed"], config["reproducibility"]) logging.basicConfig(level=logging.ERROR) return create_dataset(config) @@ -35,10 +35,10 @@ def split_dataset(config_dict=None, config_file_list=None): class TestDataset: def test_filter_nan_user_or_item(self): config_dict = { - 'model': 'BPR', - 'dataset': 'filter_nan_user_or_item', - 'data_path': current_path, - 'load_col': None, + "model": "BPR", + "dataset": "filter_nan_user_or_item", + "data_path": current_path, + "load_col": None, } dataset = new_dataset(config_dict=config_dict) assert len(dataset.inter_feat) == 1 @@ -47,34 +47,34 @@ def test_filter_nan_user_or_item(self): def test_remove_duplication_by_first(self): config_dict = { - 'model': 'BPR', - 'dataset': 'remove_duplication', - 'data_path': current_path, - 'load_col': None, - 'rm_dup_inter': 'first', + "model": "BPR", + "dataset": "remove_duplication", + "data_path": current_path, + "load_col": None, + "rm_dup_inter": "first", } dataset = new_dataset(config_dict=config_dict) assert dataset.inter_feat[dataset.time_field][0] == 0 def test_remove_duplication_by_last(self): config_dict = { - 'model': 'BPR', - 'dataset': 'remove_duplication', - 'data_path': current_path, - 'load_col': None, - 'rm_dup_inter': 'last', + "model": "BPR", + "dataset": "remove_duplication", + "data_path": current_path, + "load_col": None, + "rm_dup_inter": "last", } dataset = new_dataset(config_dict=config_dict) assert dataset.inter_feat[dataset.time_field][0] == 2 def test_filter_by_field_value_with_lowest_val(self): config_dict = { - 'model': 'BPR', - 'dataset': 'filter_by_field_value', - 'data_path': current_path, - 'load_col': None, - 'val_interval': { - 'timestamp': "[4,inf)", + "model": "BPR", + "dataset": "filter_by_field_value", + "data_path": current_path, + "load_col": None, + "val_interval": { + "timestamp": "[4,inf)", }, } dataset = new_dataset(config_dict=config_dict) @@ -82,12 +82,12 @@ def test_filter_by_field_value_with_lowest_val(self): def test_filter_by_field_value_with_highest_val(self): config_dict = { - 'model': 'BPR', - 'dataset': 'filter_by_field_value', - 'data_path': current_path, - 'load_col': None, - 'val_interval': { - 'timestamp': "(-inf,4]", + "model": "BPR", + "dataset": "filter_by_field_value", + "data_path": current_path, + "load_col": None, + "val_interval": { + "timestamp": "(-inf,4]", }, } dataset = new_dataset(config_dict=config_dict) @@ -95,12 +95,12 @@ def test_filter_by_field_value_with_highest_val(self): def test_filter_by_field_value_with_equal_val(self): config_dict = { - 'model': 'BPR', - 'dataset': 'filter_by_field_value', - 'data_path': current_path, - 'load_col': None, - 'val_interval': { - 'rating': "[0,0]", + "model": "BPR", + "dataset": "filter_by_field_value", + "data_path": current_path, + "load_col": None, + "val_interval": { + "rating": "[0,0]", }, } dataset = new_dataset(config_dict=config_dict) @@ -108,12 +108,12 @@ def test_filter_by_field_value_with_equal_val(self): def test_filter_by_field_value_with_not_equal_val(self): config_dict = { - 'model': 'BPR', - 'dataset': 'filter_by_field_value', - 'data_path': current_path, - 'load_col': None, - 'val_interval': { - 'rating': "(-inf,4);(4,inf)", + "model": "BPR", + "dataset": "filter_by_field_value", + "data_path": current_path, + "load_col": None, + "val_interval": { + "rating": "(-inf,4);(4,inf)", }, } dataset = new_dataset(config_dict=config_dict) @@ -121,12 +121,12 @@ def test_filter_by_field_value_with_not_equal_val(self): def test_filter_by_field_value_in_same_field(self): config_dict = { - 'model': 'BPR', - 'dataset': 'filter_by_field_value', - 'data_path': current_path, - 'load_col': None, - 'val_interval': { - 'timestamp': "[3,8]", + "model": "BPR", + "dataset": "filter_by_field_value", + "data_path": current_path, + "load_col": None, + "val_interval": { + "timestamp": "[3,8]", }, } dataset = new_dataset(config_dict=config_dict) @@ -134,47 +134,47 @@ def test_filter_by_field_value_in_same_field(self): def test_filter_by_field_value_in_different_field(self): config_dict = { - 'model': 'BPR', - 'dataset': 'filter_by_field_value', - 'data_path': current_path, - 'load_col': None, - 'val_interval': { - 'timestamp': "[3,8]", - 'rating': "(-inf,4);(4,inf)", - } + "model": "BPR", + "dataset": "filter_by_field_value", + "data_path": current_path, + "load_col": None, + "val_interval": { + "timestamp": "[3,8]", + "rating": "(-inf,4);(4,inf)", + }, } dataset = new_dataset(config_dict=config_dict) assert len(dataset.inter_feat) == 5 def test_filter_inter_by_user_or_item_is_true(self): config_dict = { - 'model': 'BPR', - 'dataset': 'filter_inter_by_user_or_item', - 'data_path': current_path, - 'load_col': None, - 'filter_inter_by_user_or_item': True, + "model": "BPR", + "dataset": "filter_inter_by_user_or_item", + "data_path": current_path, + "load_col": None, + "filter_inter_by_user_or_item": True, } dataset = new_dataset(config_dict=config_dict) assert len(dataset.inter_feat) == 1 def test_filter_inter_by_user_or_item_is_false(self): config_dict = { - 'model': 'BPR', - 'dataset': 'filter_inter_by_user_or_item', - 'data_path': current_path, - 'load_col': None, - 'filter_inter_by_user_or_item': False, + "model": "BPR", + "dataset": "filter_inter_by_user_or_item", + "data_path": current_path, + "load_col": None, + "filter_inter_by_user_or_item": False, } dataset = new_dataset(config_dict=config_dict) assert len(dataset.inter_feat) == 2 def test_filter_by_inter_num_in_min_user_inter_num(self): config_dict = { - 'model': 'BPR', - 'dataset': 'filter_by_inter_num', - 'data_path': current_path, - 'load_col': None, - 'user_inter_num_interval': "[2,inf)", + "model": "BPR", + "dataset": "filter_by_inter_num", + "data_path": current_path, + "load_col": None, + "user_inter_num_interval": "[2,inf)", } dataset = new_dataset(config_dict=config_dict) assert dataset.user_num == 6 @@ -182,11 +182,11 @@ def test_filter_by_inter_num_in_min_user_inter_num(self): def test_filter_by_inter_num_in_min_item_inter_num(self): config_dict = { - 'model': 'BPR', - 'dataset': 'filter_by_inter_num', - 'data_path': current_path, - 'load_col': None, - 'item_inter_num_interval': "[2,inf)", + "model": "BPR", + "dataset": "filter_by_inter_num", + "data_path": current_path, + "load_col": None, + "item_inter_num_interval": "[2,inf)", } dataset = new_dataset(config_dict=config_dict) assert dataset.user_num == 7 @@ -194,11 +194,11 @@ def test_filter_by_inter_num_in_min_item_inter_num(self): def test_filter_by_inter_num_in_max_user_inter_num(self): config_dict = { - 'model': 'BPR', - 'dataset': 'filter_by_inter_num', - 'data_path': current_path, - 'load_col': None, - 'user_inter_num_interval': "(-inf,2]", + "model": "BPR", + "dataset": "filter_by_inter_num", + "data_path": current_path, + "load_col": None, + "user_inter_num_interval": "(-inf,2]", } dataset = new_dataset(config_dict=config_dict) assert dataset.user_num == 6 @@ -206,11 +206,11 @@ def test_filter_by_inter_num_in_max_user_inter_num(self): def test_filter_by_inter_num_in_max_item_inter_num(self): config_dict = { - 'model': 'BPR', - 'dataset': 'filter_by_inter_num', - 'data_path': current_path, - 'load_col': None, - 'item_inter_num_interval': "(-inf,2]", + "model": "BPR", + "dataset": "filter_by_inter_num", + "data_path": current_path, + "load_col": None, + "item_inter_num_interval": "(-inf,2]", } dataset = new_dataset(config_dict=config_dict) assert dataset.user_num == 5 @@ -218,12 +218,12 @@ def test_filter_by_inter_num_in_max_item_inter_num(self): def test_filter_by_inter_num_in_min_inter_num(self): config_dict = { - 'model': 'BPR', - 'dataset': 'filter_by_inter_num', - 'data_path': current_path, - 'load_col': None, - 'user_inter_num_interval': "[2,inf)", - 'item_inter_num_interval': "[2,inf)", + "model": "BPR", + "dataset": "filter_by_inter_num", + "data_path": current_path, + "load_col": None, + "user_inter_num_interval": "[2,inf)", + "item_inter_num_interval": "[2,inf)", } dataset = new_dataset(config_dict=config_dict) assert dataset.user_num == 5 @@ -231,12 +231,12 @@ def test_filter_by_inter_num_in_min_inter_num(self): def test_filter_by_inter_num_in_complex_way(self): config_dict = { - 'model': 'BPR', - 'dataset': 'filter_by_inter_num', - 'data_path': current_path, - 'load_col': None, - 'user_inter_num_interval': "[2,3]", - 'item_inter_num_interval': "[2,inf)", + "model": "BPR", + "dataset": "filter_by_inter_num", + "data_path": current_path, + "load_col": None, + "user_inter_num_interval": "[2,3]", + "item_inter_num_interval": "[2,inf)", } dataset = new_dataset(config_dict=config_dict) assert dataset.user_num == 3 @@ -244,13 +244,13 @@ def test_filter_by_inter_num_in_complex_way(self): def test_rm_dup_by_first_and_filter_value(self): config_dict = { - 'model': 'BPR', - 'dataset': 'rm_dup_and_filter_value', - 'data_path': current_path, - 'load_col': None, - 'rm_dup_inter': 'first', - 'val_interval': { - 'rating': "(-inf,4]", + "model": "BPR", + "dataset": "rm_dup_and_filter_value", + "data_path": current_path, + "load_col": None, + "rm_dup_inter": "first", + "val_interval": { + "rating": "(-inf,4]", }, } dataset = new_dataset(config_dict=config_dict) @@ -258,13 +258,13 @@ def test_rm_dup_by_first_and_filter_value(self): def test_rm_dup_by_last_and_filter_value(self): config_dict = { - 'model': 'BPR', - 'dataset': 'rm_dup_and_filter_value', - 'data_path': current_path, - 'load_col': None, - 'rm_dup_inter': 'last', - 'val_interval': { - 'rating': "(-inf,4]", + "model": "BPR", + "dataset": "rm_dup_and_filter_value", + "data_path": current_path, + "load_col": None, + "rm_dup_inter": "last", + "val_interval": { + "rating": "(-inf,4]", }, } dataset = new_dataset(config_dict=config_dict) @@ -272,13 +272,13 @@ def test_rm_dup_by_last_and_filter_value(self): def test_rm_dup_and_filter_by_inter_num(self): config_dict = { - 'model': 'BPR', - 'dataset': 'rm_dup_and_filter_by_inter_num', - 'data_path': current_path, - 'load_col': None, - 'rm_dup_inter': 'first', - 'user_inter_num_interval': "[2,inf)", - 'item_inter_num_interval': "[2,inf)", + "model": "BPR", + "dataset": "rm_dup_and_filter_by_inter_num", + "data_path": current_path, + "load_col": None, + "rm_dup_inter": "first", + "user_inter_num_interval": "[2,inf)", + "item_inter_num_interval": "[2,inf)", } dataset = new_dataset(config_dict=config_dict) assert len(dataset.inter_feat) == 4 @@ -287,15 +287,15 @@ def test_rm_dup_and_filter_by_inter_num(self): def test_filter_value_and_filter_inter_by_ui(self): config_dict = { - 'model': 'BPR', - 'dataset': 'filter_value_and_filter_inter_by_ui', - 'data_path': current_path, - 'load_col': None, - 'val_interval': { - 'age': "(-inf,2]", - 'price': "(-inf,2);(2,inf)", + "model": "BPR", + "dataset": "filter_value_and_filter_inter_by_ui", + "data_path": current_path, + "load_col": None, + "val_interval": { + "age": "(-inf,2]", + "price": "(-inf,2);(2,inf)", }, - 'filter_inter_by_user_or_item': True, + "filter_inter_by_user_or_item": True, } dataset = new_dataset(config_dict=config_dict) assert len(dataset.inter_feat) == 2 @@ -304,17 +304,17 @@ def test_filter_value_and_filter_inter_by_ui(self): def test_filter_value_and_inter_num(self): config_dict = { - 'model': 'BPR', - 'dataset': 'filter_value_and_inter_num', - 'data_path': current_path, - 'load_col': None, - 'val_interval': { - 'rating': "(-inf,0]", - 'age': "(-inf,0]", - 'price': "(-inf,0]", + "model": "BPR", + "dataset": "filter_value_and_inter_num", + "data_path": current_path, + "load_col": None, + "val_interval": { + "rating": "(-inf,0]", + "age": "(-inf,0]", + "price": "(-inf,0]", }, - 'user_inter_num_interval': "[2,inf)", - 'item_inter_num_interval': "[2,inf)", + "user_inter_num_interval": "[2,inf)", + "item_inter_num_interval": "[2,inf)", } dataset = new_dataset(config_dict=config_dict) assert len(dataset.inter_feat) == 4 @@ -323,13 +323,13 @@ def test_filter_value_and_inter_num(self): def test_filter_inter_by_ui_and_inter_num(self): config_dict = { - 'model': 'BPR', - 'dataset': 'filter_inter_by_ui_and_inter_num', - 'data_path': current_path, - 'load_col': None, - 'filter_inter_by_user_or_item': True, - 'user_inter_num_interval': "[2,inf)", - 'item_inter_num_interval': "[2,inf)", + "model": "BPR", + "dataset": "filter_inter_by_ui_and_inter_num", + "data_path": current_path, + "load_col": None, + "filter_inter_by_user_or_item": True, + "user_inter_num_interval": "[2,inf)", + "item_inter_num_interval": "[2,inf)", } dataset = new_dataset(config_dict=config_dict) assert len(dataset.inter_feat) == 4 @@ -338,244 +338,368 @@ def test_filter_inter_by_ui_and_inter_num(self): def test_remap_id(self): config_dict = { - 'model': 'BPR', - 'dataset': 'remap_id', - 'data_path': current_path, - 'load_col': None, + "model": "BPR", + "dataset": "remap_id", + "data_path": current_path, + "load_col": None, } dataset = new_dataset(config_dict=config_dict) - user_list = dataset.token2id('user_id', ['ua', 'ub', 'uc', 'ud']) - item_list = dataset.token2id('item_id', ['ia', 'ib', 'ic', 'id']) + user_list = dataset.token2id("user_id", ["ua", "ub", "uc", "ud"]) + item_list = dataset.token2id("item_id", ["ia", "ib", "ic", "id"]) assert (user_list == [1, 2, 3, 4]).all() assert (item_list == [1, 2, 3, 4]).all() - assert (dataset.inter_feat['user_id'] == [1, 2, 3, 4]).all() - assert (dataset.inter_feat['item_id'] == [1, 2, 3, 4]).all() - assert (dataset.inter_feat['add_user'] == [1, 2, 3, 4]).all() - assert (dataset.inter_feat['add_item'] == [1, 2, 3, 4]).all() - assert (dataset.inter_feat['user_list'][0] == [1, 2]).all() - assert (dataset.inter_feat['user_list'][1] == []).all() - assert (dataset.inter_feat['user_list'][2] == [3, 4, 1]).all() - assert (dataset.inter_feat['user_list'][3] == [5]).all() + assert (dataset.inter_feat["user_id"] == [1, 2, 3, 4]).all() + assert (dataset.inter_feat["item_id"] == [1, 2, 3, 4]).all() + assert (dataset.inter_feat["add_user"] == [1, 2, 3, 4]).all() + assert (dataset.inter_feat["add_item"] == [1, 2, 3, 4]).all() + assert (dataset.inter_feat["user_list"][0] == [1, 2]).all() + assert (dataset.inter_feat["user_list"][1] == []).all() + assert (dataset.inter_feat["user_list"][2] == [3, 4, 1]).all() + assert (dataset.inter_feat["user_list"][3] == [5]).all() def test_remap_id_with_alias(self): config_dict = { - 'model': 'BPR', - 'dataset': 'remap_id', - 'data_path': current_path, - 'load_col': None, - 'alias_of_user_id': ['add_user', 'user_list'], - 'alias_of_item_id': ['add_item'], + "model": "BPR", + "dataset": "remap_id", + "data_path": current_path, + "load_col": None, + "alias_of_user_id": ["add_user", "user_list"], + "alias_of_item_id": ["add_item"], } dataset = new_dataset(config_dict=config_dict) - user_list = dataset.token2id('user_id', ['ua', 'ub', 'uc', 'ud', 'ue', 'uf']) - item_list = dataset.token2id('item_id', ['ia', 'ib', 'ic', 'id', 'ie', 'if']) + user_list = dataset.token2id("user_id", ["ua", "ub", "uc", "ud", "ue", "uf"]) + item_list = dataset.token2id("item_id", ["ia", "ib", "ic", "id", "ie", "if"]) assert (user_list == [1, 2, 3, 4, 5, 6]).all() assert (item_list == [1, 2, 3, 4, 5, 6]).all() - assert (dataset.inter_feat['user_id'] == [1, 2, 3, 4]).all() - assert (dataset.inter_feat['item_id'] == [1, 2, 3, 4]).all() - assert (dataset.inter_feat['add_user'] == [2, 5, 4, 6]).all() - assert (dataset.inter_feat['add_item'] == [5, 3, 6, 1]).all() - assert (dataset.inter_feat['user_list'][0] == [3, 5]).all() - assert (dataset.inter_feat['user_list'][1] == []).all() - assert (dataset.inter_feat['user_list'][2] == [1, 2, 3]).all() - assert (dataset.inter_feat['user_list'][3] == [6]).all() + assert (dataset.inter_feat["user_id"] == [1, 2, 3, 4]).all() + assert (dataset.inter_feat["item_id"] == [1, 2, 3, 4]).all() + assert (dataset.inter_feat["add_user"] == [2, 5, 4, 6]).all() + assert (dataset.inter_feat["add_item"] == [5, 3, 6, 1]).all() + assert (dataset.inter_feat["user_list"][0] == [3, 5]).all() + assert (dataset.inter_feat["user_list"][1] == []).all() + assert (dataset.inter_feat["user_list"][2] == [1, 2, 3]).all() + assert (dataset.inter_feat["user_list"][3] == [6]).all() def test_ui_feat_preparation_and_fill_nan(self): config_dict = { - 'model': 'BPR', - 'dataset': 'ui_feat_preparation_and_fill_nan', - 'data_path': current_path, - 'load_col': None, - 'filter_inter_by_user_or_item': False, - 'normalize_field': None, - 'normalize_all': None, - } - dataset = new_dataset(config_dict=config_dict) - user_token_list = dataset.id2token('user_id', dataset.user_feat['user_id']) - item_token_list = dataset.id2token('item_id', dataset.item_feat['item_id']) - assert (user_token_list == ['[PAD]', 'ua', 'ub', 'uc', 'ud', 'ue']).all() - assert (item_token_list == ['[PAD]', 'ia', 'ib', 'ic', 'id', 'ie']).all() - assert dataset.inter_feat['rating'][3] == 1.0 - assert dataset.user_feat['age'][4] == 1.5 - assert dataset.item_feat['price'][4] == 1.5 - assert (dataset.inter_feat['time_list'][0] == [1., 2., 3.]).all() - assert (dataset.inter_feat['time_list'][1] == [2.]).all() - assert (dataset.inter_feat['time_list'][2] == []).all() - assert (dataset.inter_feat['time_list'][3] == [5, 4]).all() - assert (dataset.user_feat['profile'][0] == []).all() - assert (dataset.user_feat['profile'][1] == [1, 2, 3]).all() - assert (dataset.user_feat['profile'][2] == []).all() - assert (dataset.user_feat['profile'][3] == [3]).all() - assert (dataset.user_feat['profile'][4] == []).all() - assert (dataset.user_feat['profile'][5] == [3, 2]).all() + "model": "BPR", + "dataset": "ui_feat_preparation_and_fill_nan", + "data_path": current_path, + "load_col": None, + "filter_inter_by_user_or_item": False, + "normalize_field": None, + "normalize_all": None, + } + dataset = new_dataset(config_dict=config_dict) + user_token_list = dataset.id2token("user_id", dataset.user_feat["user_id"]) + item_token_list = dataset.id2token("item_id", dataset.item_feat["item_id"]) + assert (user_token_list == ["[PAD]", "ua", "ub", "uc", "ud", "ue"]).all() + assert (item_token_list == ["[PAD]", "ia", "ib", "ic", "id", "ie"]).all() + assert dataset.inter_feat["rating"][3] == 1.0 + assert dataset.user_feat["age"][4] == 1.5 + assert dataset.item_feat["price"][4] == 1.5 + assert (dataset.inter_feat["time_list"][0] == [1.0, 2.0, 3.0]).all() + assert (dataset.inter_feat["time_list"][1] == [2.0]).all() + assert (dataset.inter_feat["time_list"][2] == []).all() + assert (dataset.inter_feat["time_list"][3] == [5, 4]).all() + assert (dataset.user_feat["profile"][0] == []).all() + assert (dataset.user_feat["profile"][1] == [1, 2, 3]).all() + assert (dataset.user_feat["profile"][2] == []).all() + assert (dataset.user_feat["profile"][3] == [3]).all() + assert (dataset.user_feat["profile"][4] == []).all() + assert (dataset.user_feat["profile"][5] == [3, 2]).all() def test_set_label_by_threshold(self): config_dict = { - 'model': 'BPR', - 'dataset': 'set_label_by_threshold', - 'data_path': current_path, - 'load_col': None, - 'threshold': { - 'rating': 4, + "model": "BPR", + "dataset": "set_label_by_threshold", + "data_path": current_path, + "load_col": None, + "threshold": { + "rating": 4, }, - 'normalize_field': None, - 'normalize_all': None, + "normalize_field": None, + "normalize_all": None, } dataset = new_dataset(config_dict=config_dict) - assert (dataset.inter_feat['label'] == [1., 0., 1., 0.]).all() + assert (dataset.inter_feat["label"] == [1.0, 0.0, 1.0, 0.0]).all() def test_normalize_all(self): config_dict = { - 'model': 'BPR', - 'dataset': 'normalize', - 'data_path': current_path, - 'load_col': None, - 'normalize_all': True, + "model": "BPR", + "dataset": "normalize", + "data_path": current_path, + "load_col": None, + "normalize_all": True, } dataset = new_dataset(config_dict=config_dict) - assert (dataset.inter_feat['rating'] == [0., .25, 1., .75, .5]).all() - assert (dataset.inter_feat['star'] == [1., .5, 0., .25, 0.75]).all() + assert (dataset.inter_feat["rating"] == [0.0, 0.25, 1.0, 0.75, 0.5]).all() + assert (dataset.inter_feat["star"] == [1.0, 0.5, 0.0, 0.25, 0.75]).all() def test_normalize_field(self): config_dict = { - 'model': 'BPR', - 'dataset': 'normalize', - 'data_path': current_path, - 'load_col': None, - 'normalize_field': ['rating'], - 'normalize_all': False, + "model": "BPR", + "dataset": "normalize", + "data_path": current_path, + "load_col": None, + "normalize_field": ["rating"], + "normalize_all": False, } dataset = new_dataset(config_dict=config_dict) - assert (dataset.inter_feat['rating'] == [0., .25, 1., .75, .5]).all() - assert (dataset.inter_feat['star'] == [4., 2., 0., 1., 3.]).all() + assert (dataset.inter_feat["rating"] == [0.0, 0.25, 1.0, 0.75, 0.5]).all() + assert (dataset.inter_feat["star"] == [4.0, 2.0, 0.0, 1.0, 3.0]).all() def test_TO_RS_811(self): config_dict = { - 'model': 'BPR', - 'dataset': 'build_dataset', - 'data_path': current_path, - 'load_col': None, - 'eval_args': {'split': {'RS': [0.8, 0.1, 0.1]}, 'order': 'TO', 'mode': 'labeled'} + "model": "BPR", + "dataset": "build_dataset", + "data_path": current_path, + "load_col": None, + "eval_args": { + "split": {"RS": [0.8, 0.1, 0.1]}, + "order": "TO", + "mode": "labeled", + }, } - train_dataset, valid_dataset, test_dataset = split_dataset(config_dict=config_dict) - assert (train_dataset.inter_feat['item_id'].numpy() == list(range(1, 17)) + [1] + [1] + [1] + [1, 2, 3] + - list(range(1, 8)) + list(range(1, 9)) + list(range(1, 10))).all() - assert (valid_dataset.inter_feat['item_id'].numpy() == list(range(17, 19)) + [] + [] + [2] + [4] + - [8] + [9] + [10]).all() - assert (test_dataset.inter_feat['item_id'].numpy() == list(range(19, 21)) + [] + [2] + [3] + [5] + - [9] + [10] + [11]).all() + train_dataset, valid_dataset, test_dataset = split_dataset( + config_dict=config_dict + ) + assert ( + train_dataset.inter_feat["item_id"].numpy() + == list(range(1, 17)) + + [1] + + [1] + + [1] + + [1, 2, 3] + + list(range(1, 8)) + + list(range(1, 9)) + + list(range(1, 10)) + ).all() + assert ( + valid_dataset.inter_feat["item_id"].numpy() + == list(range(17, 19)) + [] + [] + [2] + [4] + [8] + [9] + [10] + ).all() + assert ( + test_dataset.inter_feat["item_id"].numpy() + == list(range(19, 21)) + [] + [2] + [3] + [5] + [9] + [10] + [11] + ).all() def test_TO_RS_820(self): config_dict = { - 'model': 'BPR', - 'dataset': 'build_dataset', - 'data_path': current_path, - 'load_col': None, - 'eval_args': {'split': {'RS': [0.8, 0.2, 0.0]}, 'order': 'TO', 'mode': 'labeled'} - } - train_dataset, valid_dataset, test_dataset = split_dataset(config_dict=config_dict) - assert (train_dataset.inter_feat['item_id'].numpy() == list(range(1, 17)) + [1] + [1] + [1, 2] + [1, 2, 3, 4] + - list(range(1, 9)) + list(range(1, 9)) + list(range(1, 10))).all() - assert (valid_dataset.inter_feat['item_id'].numpy() == list(range(17, 21)) + [] + [2] + [3] + [5] + - [9] + [9, 10] + [10, 11]).all() + "model": "BPR", + "dataset": "build_dataset", + "data_path": current_path, + "load_col": None, + "eval_args": { + "split": {"RS": [0.8, 0.2, 0.0]}, + "order": "TO", + "mode": "labeled", + }, + } + train_dataset, valid_dataset, test_dataset = split_dataset( + config_dict=config_dict + ) + assert ( + train_dataset.inter_feat["item_id"].numpy() + == list(range(1, 17)) + + [1] + + [1] + + [1, 2] + + [1, 2, 3, 4] + + list(range(1, 9)) + + list(range(1, 9)) + + list(range(1, 10)) + ).all() + assert ( + valid_dataset.inter_feat["item_id"].numpy() + == list(range(17, 21)) + [] + [2] + [3] + [5] + [9] + [9, 10] + [10, 11] + ).all() assert len(test_dataset.inter_feat) == 0 def test_TO_RS_802(self): config_dict = { - 'model': 'BPR', - 'dataset': 'build_dataset', - 'data_path': current_path, - 'load_col': None, - 'eval_args': {'split': {'RS': [0.8, 0.0, 0.2]}, 'order': 'TO', 'mode': 'labeled'} + "model": "BPR", + "dataset": "build_dataset", + "data_path": current_path, + "load_col": None, + "eval_args": { + "split": {"RS": [0.8, 0.0, 0.2]}, + "order": "TO", + "mode": "labeled", + }, } - train_dataset, valid_dataset, test_dataset = split_dataset(config_dict=config_dict) - assert (train_dataset.inter_feat['item_id'].numpy() == list(range(1, 17)) + [1] + [1] + [1, 2] + [1, 2, 3, 4] + - list(range(1, 9)) + list(range(1, 9)) + list(range(1, 10))).all() + train_dataset, valid_dataset, test_dataset = split_dataset( + config_dict=config_dict + ) + assert ( + train_dataset.inter_feat["item_id"].numpy() + == list(range(1, 17)) + + [1] + + [1] + + [1, 2] + + [1, 2, 3, 4] + + list(range(1, 9)) + + list(range(1, 9)) + + list(range(1, 10)) + ).all() assert len(valid_dataset.inter_feat) == 0 - assert (test_dataset.inter_feat['item_id'].numpy() == list(range(17, 21)) + [] + [2] + [3] + [5] + - [9] + [9, 10] + [10, 11]).all() + assert ( + test_dataset.inter_feat["item_id"].numpy() + == list(range(17, 21)) + [] + [2] + [3] + [5] + [9] + [9, 10] + [10, 11] + ).all() def test_TO_LS_valid_and_test(self): config_dict = { - 'model': 'BPR', - 'dataset': 'build_dataset', - 'data_path': current_path, - 'load_col': None, - 'eval_args': {'split': {'LS': 'valid_and_test'}, 'order': 'TO', 'mode': 'labeled'} + "model": "BPR", + "dataset": "build_dataset", + "data_path": current_path, + "load_col": None, + "eval_args": { + "split": {"LS": "valid_and_test"}, + "order": "TO", + "mode": "labeled", + }, } - train_dataset, valid_dataset, test_dataset = split_dataset(config_dict=config_dict) - assert (train_dataset.inter_feat['item_id'].numpy() == list(range(1, 19)) + [1] + [1] + [1] + [1, 2, 3] + - list(range(1, 8)) + list(range(1, 9)) + list(range(1, 10))).all() - assert (valid_dataset.inter_feat['item_id'].numpy() == list(range(19, 20)) + [] + [] + [2] + [4] + - [8] + [9] + [10]).all() - assert (test_dataset.inter_feat['item_id'].numpy() == list(range(20, 21)) + [] + [2] + [3] + [5] + - [9] + [10] + [11]).all() + train_dataset, valid_dataset, test_dataset = split_dataset( + config_dict=config_dict + ) + assert ( + train_dataset.inter_feat["item_id"].numpy() + == list(range(1, 19)) + + [1] + + [1] + + [1] + + [1, 2, 3] + + list(range(1, 8)) + + list(range(1, 9)) + + list(range(1, 10)) + ).all() + assert ( + valid_dataset.inter_feat["item_id"].numpy() + == list(range(19, 20)) + [] + [] + [2] + [4] + [8] + [9] + [10] + ).all() + assert ( + test_dataset.inter_feat["item_id"].numpy() + == list(range(20, 21)) + [] + [2] + [3] + [5] + [9] + [10] + [11] + ).all() def test_TO_LS_valid_only(self): config_dict = { - 'model': 'BPR', - 'dataset': 'build_dataset', - 'data_path': current_path, - 'load_col': None, - 'eval_args': {'split': {'LS': 'valid_only'}, 'order': 'TO', 'mode': 'labeled'} - } - train_dataset, valid_dataset, test_dataset = split_dataset(config_dict=config_dict) - assert (train_dataset.inter_feat['item_id'].numpy() == list(range(1, 20)) + [1] + [1] + [1, 2] + [1, 2, 3, 4] + - list(range(1, 9)) + list(range(1, 10)) + list(range(1, 11))).all() - assert (valid_dataset.inter_feat['item_id'].numpy() == list(range(20, 21)) + [] + [2] + [3] + [5] + - [9] + [10] + [11]).all() + "model": "BPR", + "dataset": "build_dataset", + "data_path": current_path, + "load_col": None, + "eval_args": { + "split": {"LS": "valid_only"}, + "order": "TO", + "mode": "labeled", + }, + } + train_dataset, valid_dataset, test_dataset = split_dataset( + config_dict=config_dict + ) + assert ( + train_dataset.inter_feat["item_id"].numpy() + == list(range(1, 20)) + + [1] + + [1] + + [1, 2] + + [1, 2, 3, 4] + + list(range(1, 9)) + + list(range(1, 10)) + + list(range(1, 11)) + ).all() + assert ( + valid_dataset.inter_feat["item_id"].numpy() + == list(range(20, 21)) + [] + [2] + [3] + [5] + [9] + [10] + [11] + ).all() assert len(test_dataset.inter_feat) == 0 def test_TO_LS_test_only(self): config_dict = { - 'model': 'BPR', - 'dataset': 'build_dataset', - 'data_path': current_path, - 'load_col': None, - 'eval_args': {'split': {'LS': 'test_only'}, 'order': 'TO', 'mode': 'labeled'} + "model": "BPR", + "dataset": "build_dataset", + "data_path": current_path, + "load_col": None, + "eval_args": { + "split": {"LS": "test_only"}, + "order": "TO", + "mode": "labeled", + }, } - train_dataset, valid_dataset, test_dataset = split_dataset(config_dict=config_dict) - assert (train_dataset.inter_feat['item_id'].numpy() == list(range(1, 20)) + [1] + [1] + [1, 2] + [1, 2, 3, 4] + - list(range(1, 9)) + list(range(1, 10)) + list(range(1, 11))).all() + train_dataset, valid_dataset, test_dataset = split_dataset( + config_dict=config_dict + ) + assert ( + train_dataset.inter_feat["item_id"].numpy() + == list(range(1, 20)) + + [1] + + [1] + + [1, 2] + + [1, 2, 3, 4] + + list(range(1, 9)) + + list(range(1, 10)) + + list(range(1, 11)) + ).all() assert len(valid_dataset.inter_feat) == 0 - assert (test_dataset.inter_feat['item_id'].numpy() == list(range(20, 21)) + [] + [2] + [3] + [5] + - [9] + [10] + [11]).all() + assert ( + test_dataset.inter_feat["item_id"].numpy() + == list(range(20, 21)) + [] + [2] + [3] + [5] + [9] + [10] + [11] + ).all() def test_RO_RS_811(self): config_dict = { - 'model': 'BPR', - 'dataset': 'build_dataset', - 'data_path': current_path, - 'load_col': None, - 'eval_args': {'split': {'RS': [0.8, 0.1, 0.1]}, 'order': 'RO', 'mode': 'labeled'} + "model": "BPR", + "dataset": "build_dataset", + "data_path": current_path, + "load_col": None, + "eval_args": { + "split": {"RS": [0.8, 0.1, 0.1]}, + "order": "RO", + "mode": "labeled", + }, } - train_dataset, valid_dataset, test_dataset = split_dataset(config_dict=config_dict) + train_dataset, valid_dataset, test_dataset = split_dataset( + config_dict=config_dict + ) assert len(train_dataset.inter_feat) == 16 + 1 + 1 + 1 + 3 + 7 + 8 + 9 assert len(valid_dataset.inter_feat) == 2 + 0 + 0 + 1 + 1 + 1 + 1 + 1 assert len(test_dataset.inter_feat) == 2 + 0 + 1 + 1 + 1 + 1 + 1 + 1 def test_RO_RS_820(self): config_dict = { - 'model': 'BPR', - 'dataset': 'build_dataset', - 'data_path': current_path, - 'load_col': None, - 'eval_args': {'split': {'RS': [0.8, 0.2, 0.0]}, 'order': 'RO', 'mode': 'labeled'} + "model": "BPR", + "dataset": "build_dataset", + "data_path": current_path, + "load_col": None, + "eval_args": { + "split": {"RS": [0.8, 0.2, 0.0]}, + "order": "RO", + "mode": "labeled", + }, } - train_dataset, valid_dataset, test_dataset = split_dataset(config_dict=config_dict) + train_dataset, valid_dataset, test_dataset = split_dataset( + config_dict=config_dict + ) assert len(train_dataset.inter_feat) == 16 + 1 + 1 + 2 + 4 + 8 + 8 + 9 assert len(valid_dataset.inter_feat) == 4 + 0 + 1 + 1 + 1 + 1 + 2 + 2 assert len(test_dataset.inter_feat) == 0 def test_RO_RS_802(self): config_dict = { - 'model': 'BPR', - 'dataset': 'build_dataset', - 'data_path': current_path, - 'load_col': None, - 'eval_args': {'split': {'RS': [0.8, 0.0, 0.2]}, 'order': 'RO', 'mode': 'labeled'} + "model": "BPR", + "dataset": "build_dataset", + "data_path": current_path, + "load_col": None, + "eval_args": { + "split": {"RS": [0.8, 0.0, 0.2]}, + "order": "RO", + "mode": "labeled", + }, } - train_dataset, valid_dataset, test_dataset = split_dataset(config_dict=config_dict) + train_dataset, valid_dataset, test_dataset = split_dataset( + config_dict=config_dict + ) assert len(train_dataset.inter_feat) == 16 + 1 + 1 + 2 + 4 + 8 + 8 + 9 assert len(valid_dataset.inter_feat) == 0 assert len(test_dataset.inter_feat) == 4 + 0 + 1 + 1 + 1 + 1 + 2 + 2 @@ -584,186 +708,287 @@ def test_RO_RS_802(self): class TestSeqDataset: def test_seq_leave_one_out(self): config_dict = { - 'model': 'GRU4Rec', - 'dataset': 'seq_dataset', - 'data_path': current_path, - 'load_col': None, - 'train_neg_sample_args': None - } - train_dataset, valid_dataset, test_dataset = split_dataset(config_dict=config_dict) - assert (train_dataset.inter_feat[train_dataset.uid_field].numpy() == [1, 1, 1, 1, 1, 4, 2, 2, 3]).all() - assert (train_dataset.inter_feat[train_dataset.item_id_list_field][:, :5].numpy() == [ - [1, 0, 0, 0, 0], - [1, 2, 0, 0, 0], - [1, 2, 3, 0, 0], - [1, 2, 3, 4, 0], - [1, 2, 3, 4, 5], - [3, 0, 0, 0, 0], - [4, 0, 0, 0, 0], - [4, 5, 0, 0, 0], - [4, 0, 0, 0, 0]]).all() - assert (train_dataset.inter_feat[train_dataset.iid_field].numpy() == [2, 3, 4, 5, 6, 4, 5, 6, 5]).all() - assert (train_dataset.inter_feat[train_dataset.item_list_length_field].numpy() == [1, 2, 3, 4, 5, 1, 1, 2, - 1]).all() - - assert (valid_dataset.inter_feat[valid_dataset.uid_field].numpy() == [1, 2]).all() - assert (valid_dataset.inter_feat[valid_dataset.item_id_list_field][:, :6].numpy() == [ - [1, 2, 3, 4, 5, 6], - [4, 5, 6, 0, 0, 0]]).all() - assert (valid_dataset.inter_feat[valid_dataset.iid_field].numpy() == [7, 7]).all() - assert (valid_dataset.inter_feat[valid_dataset.item_list_length_field].numpy() == [6, 3]).all() - - assert (test_dataset.inter_feat[test_dataset.uid_field].numpy() == [1, 2, 3]).all() - assert (test_dataset.inter_feat[test_dataset.item_id_list_field][:, :7].numpy() == [ - [1, 2, 3, 4, 5, 6, 7], - [4, 5, 6, 7, 0, 0, 0], - [4, 5, 0, 0, 0, 0, 0]]).all() - assert (test_dataset.inter_feat[test_dataset.iid_field].numpy() == [8, 8, 6]).all() - assert (test_dataset.inter_feat[test_dataset.item_list_length_field].numpy() == [7, 4, 2]).all() - - assert (train_dataset.inter_matrix().toarray() == [ - [0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 1., 1., 1., 1., 1., 1., 0., 0.], - [0., 0., 0., 0., 1., 1., 1., 0., 0.], - [0., 0., 0., 0., 1., 1., 0., 0., 0.], - [0., 0., 0., 1., 1., 0., 0., 0., 0.], - ]).all() - assert (valid_dataset.inter_matrix().toarray() == [ - [0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 1., 0.], - [0., 0., 0., 0., 0., 0., 0., 1., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0.] - ]).all() - assert (test_dataset.inter_matrix().toarray() == [ - [0., 0., 0., 0., 0., 0., 0., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 1.], - [0., 0., 0., 0., 0., 0., 0., 0., 1.], - [0., 0., 0., 0., 0., 0., 1., 0., 0.], - [0., 0., 0., 0., 0., 0., 0., 0., 0.] - ]).all() + "model": "GRU4Rec", + "dataset": "seq_dataset", + "data_path": current_path, + "load_col": None, + "train_neg_sample_args": None, + } + train_dataset, valid_dataset, test_dataset = split_dataset( + config_dict=config_dict + ) + assert ( + train_dataset.inter_feat[train_dataset.uid_field].numpy() + == [1, 1, 1, 1, 1, 4, 2, 2, 3] + ).all() + assert ( + train_dataset.inter_feat[train_dataset.item_id_list_field][:, :5].numpy() + == [ + [1, 0, 0, 0, 0], + [1, 2, 0, 0, 0], + [1, 2, 3, 0, 0], + [1, 2, 3, 4, 0], + [1, 2, 3, 4, 5], + [3, 0, 0, 0, 0], + [4, 0, 0, 0, 0], + [4, 5, 0, 0, 0], + [4, 0, 0, 0, 0], + ] + ).all() + assert ( + train_dataset.inter_feat[train_dataset.iid_field].numpy() + == [2, 3, 4, 5, 6, 4, 5, 6, 5] + ).all() + assert ( + train_dataset.inter_feat[train_dataset.item_list_length_field].numpy() + == [1, 2, 3, 4, 5, 1, 1, 2, 1] + ).all() + + assert ( + valid_dataset.inter_feat[valid_dataset.uid_field].numpy() == [1, 2] + ).all() + assert ( + valid_dataset.inter_feat[valid_dataset.item_id_list_field][:, :6].numpy() + == [[1, 2, 3, 4, 5, 6], [4, 5, 6, 0, 0, 0]] + ).all() + assert ( + valid_dataset.inter_feat[valid_dataset.iid_field].numpy() == [7, 7] + ).all() + assert ( + valid_dataset.inter_feat[valid_dataset.item_list_length_field].numpy() + == [6, 3] + ).all() + + assert ( + test_dataset.inter_feat[test_dataset.uid_field].numpy() == [1, 2, 3] + ).all() + assert ( + test_dataset.inter_feat[test_dataset.item_id_list_field][:, :7].numpy() + == [[1, 2, 3, 4, 5, 6, 7], [4, 5, 6, 7, 0, 0, 0], [4, 5, 0, 0, 0, 0, 0]] + ).all() + assert ( + test_dataset.inter_feat[test_dataset.iid_field].numpy() == [8, 8, 6] + ).all() + assert ( + test_dataset.inter_feat[test_dataset.item_list_length_field].numpy() + == [7, 4, 2] + ).all() + + assert ( + train_dataset.inter_matrix().toarray() + == [ + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + [0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0], + ] + ).all() + assert ( + valid_dataset.inter_matrix().toarray() + == [ + [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.0, 0.0, 0.0, 0.0, 0.0, 0.0, 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], + ] + ).all() + assert ( + test_dataset.inter_matrix().toarray() + == [ + [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.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], + ] + ).all() def test_seq_split_by_ratio(self): config_dict = { - 'model': 'GRU4Rec', - 'dataset': 'seq_dataset', - 'data_path': current_path, - 'load_col': None, - 'train_neg_sample_args': None, - 'eval_args': { - 'split': {'RS': [0.3, 0.3, 0.4]}, - 'order': 'TO' - } - } - train_dataset, valid_dataset, test_dataset = split_dataset(config_dict=config_dict) - assert (train_dataset.inter_feat[train_dataset.uid_field].numpy() == [1, 1, 1, 4, 2, 2, 3]).all() - assert (train_dataset.inter_feat[train_dataset.item_id_list_field][:, :3].numpy() == [ - [1, 0, 0], - [1, 2, 0], - [1, 2, 3], - [3, 0, 0], - [4, 0, 0], - [4, 5, 0], - [4, 0, 0]]).all() - assert (train_dataset.inter_feat[train_dataset.iid_field].numpy() == [2, 3, 4, 4, 5, 6, 5]).all() - assert (train_dataset.inter_feat[train_dataset.item_list_length_field].numpy() == [1, 2, 3, 1, 1, 2, 1]).all() - - assert (valid_dataset.inter_feat[valid_dataset.uid_field].numpy() == [1, 1, 2]).all() - assert (valid_dataset.inter_feat[valid_dataset.item_id_list_field][:, :5].numpy() == [ - [1, 2, 3, 4, 0], - [1, 2, 3, 4, 5], - [4, 5, 6, 0, 0]]).all() - assert (valid_dataset.inter_feat[valid_dataset.iid_field].numpy() == [5, 6, 7]).all() - assert (valid_dataset.inter_feat[valid_dataset.item_list_length_field].numpy() == [4, 5, 3]).all() - - assert (test_dataset.inter_feat[test_dataset.uid_field].numpy() == [1, 1, 2, 3]).all() - assert (test_dataset.inter_feat[test_dataset.item_id_list_field][:, :7].numpy() == [ - [1, 2, 3, 4, 5, 6, 0], - [1, 2, 3, 4, 5, 6, 7], - [4, 5, 6, 7, 0, 0, 0], - [4, 5, 0, 0, 0, 0, 0]]).all() - assert (test_dataset.inter_feat[test_dataset.iid_field].numpy() == [7, 8, 8, 6]).all() - assert (test_dataset.inter_feat[test_dataset.item_list_length_field].numpy() == [6, 7, 4, 2]).all() + "model": "GRU4Rec", + "dataset": "seq_dataset", + "data_path": current_path, + "load_col": None, + "train_neg_sample_args": None, + "eval_args": {"split": {"RS": [0.3, 0.3, 0.4]}, "order": "TO"}, + } + train_dataset, valid_dataset, test_dataset = split_dataset( + config_dict=config_dict + ) + assert ( + train_dataset.inter_feat[train_dataset.uid_field].numpy() + == [1, 1, 1, 4, 2, 2, 3] + ).all() + assert ( + train_dataset.inter_feat[train_dataset.item_id_list_field][:, :3].numpy() + == [ + [1, 0, 0], + [1, 2, 0], + [1, 2, 3], + [3, 0, 0], + [4, 0, 0], + [4, 5, 0], + [4, 0, 0], + ] + ).all() + assert ( + train_dataset.inter_feat[train_dataset.iid_field].numpy() + == [2, 3, 4, 4, 5, 6, 5] + ).all() + assert ( + train_dataset.inter_feat[train_dataset.item_list_length_field].numpy() + == [1, 2, 3, 1, 1, 2, 1] + ).all() + + assert ( + valid_dataset.inter_feat[valid_dataset.uid_field].numpy() == [1, 1, 2] + ).all() + assert ( + valid_dataset.inter_feat[valid_dataset.item_id_list_field][:, :5].numpy() + == [[1, 2, 3, 4, 0], [1, 2, 3, 4, 5], [4, 5, 6, 0, 0]] + ).all() + assert ( + valid_dataset.inter_feat[valid_dataset.iid_field].numpy() == [5, 6, 7] + ).all() + assert ( + valid_dataset.inter_feat[valid_dataset.item_list_length_field].numpy() + == [4, 5, 3] + ).all() + + assert ( + test_dataset.inter_feat[test_dataset.uid_field].numpy() == [1, 1, 2, 3] + ).all() + assert ( + test_dataset.inter_feat[test_dataset.item_id_list_field][:, :7].numpy() + == [ + [1, 2, 3, 4, 5, 6, 0], + [1, 2, 3, 4, 5, 6, 7], + [4, 5, 6, 7, 0, 0, 0], + [4, 5, 0, 0, 0, 0, 0], + ] + ).all() + assert ( + test_dataset.inter_feat[test_dataset.iid_field].numpy() == [7, 8, 8, 6] + ).all() + assert ( + test_dataset.inter_feat[test_dataset.item_list_length_field].numpy() + == [6, 7, 4, 2] + ).all() def test_seq_benchmark(self): config_dict = { - 'model': 'GRU4Rec', - 'dataset': 'seq_benchmark', - 'data_path': current_path, - 'load_col': None, - 'train_neg_sample_args': None, - 'benchmark_filename': ['train', 'valid', 'test'], - 'alias_of_item_id': ['item_id_list'] - } - train_dataset, valid_dataset, test_dataset = split_dataset(config_dict=config_dict) - assert (train_dataset.inter_feat[train_dataset.uid_field].numpy() == [1, 1, 1, 2, 3, 3, 4]).all() - assert (train_dataset.inter_feat[train_dataset.item_id_list_field][:, :3].numpy() == [ - [8, 0, 0], - [8, 1, 0], - [8, 1, 2], - [2, 0, 0], - [3, 0, 0], - [3, 4, 0], - [3, 0, 0]]).all() - assert (train_dataset.inter_feat[train_dataset.iid_field].numpy() == [1, 2, 3, 3, 4, 5, 4]).all() - assert (train_dataset.inter_feat[train_dataset.item_list_length_field].numpy() == [1, 2, 3, 1, 1, 2, 1]).all() - - assert (valid_dataset.inter_feat[valid_dataset.uid_field].numpy() == [1, 1, 3]).all() - assert (valid_dataset.inter_feat[valid_dataset.item_id_list_field][:, :5].numpy() == [ - [8, 1, 2, 3, 0], - [8, 1, 2, 3, 4], - [3, 4, 5, 0, 0]]).all() - assert (valid_dataset.inter_feat[valid_dataset.iid_field].numpy() == [4, 5, 6]).all() - assert (valid_dataset.inter_feat[valid_dataset.item_list_length_field].numpy() == [4, 5, 3]).all() - - assert (test_dataset.inter_feat[test_dataset.uid_field].numpy() == [1, 1, 3, 4]).all() - assert (test_dataset.inter_feat[test_dataset.item_id_list_field][:, :7].numpy() == [ - [8, 1, 2, 3, 4, 5, 0], - [8, 1, 2, 3, 4, 5, 6], - [3, 4, 5, 6, 0, 0, 0], - [3, 4, 0, 0, 0, 0, 0]]).all() - assert (test_dataset.inter_feat[test_dataset.iid_field].numpy() == [6, 7, 7, 5]).all() - assert (test_dataset.inter_feat[test_dataset.item_list_length_field].numpy() == [6, 7, 4, 2]).all() + "model": "GRU4Rec", + "dataset": "seq_benchmark", + "data_path": current_path, + "load_col": None, + "train_neg_sample_args": None, + "benchmark_filename": ["train", "valid", "test"], + "alias_of_item_id": ["item_id_list"], + } + train_dataset, valid_dataset, test_dataset = split_dataset( + config_dict=config_dict + ) + assert ( + train_dataset.inter_feat[train_dataset.uid_field].numpy() + == [1, 1, 1, 2, 3, 3, 4] + ).all() + assert ( + train_dataset.inter_feat[train_dataset.item_id_list_field][:, :3].numpy() + == [ + [8, 0, 0], + [8, 1, 0], + [8, 1, 2], + [2, 0, 0], + [3, 0, 0], + [3, 4, 0], + [3, 0, 0], + ] + ).all() + assert ( + train_dataset.inter_feat[train_dataset.iid_field].numpy() + == [1, 2, 3, 3, 4, 5, 4] + ).all() + assert ( + train_dataset.inter_feat[train_dataset.item_list_length_field].numpy() + == [1, 2, 3, 1, 1, 2, 1] + ).all() + + assert ( + valid_dataset.inter_feat[valid_dataset.uid_field].numpy() == [1, 1, 3] + ).all() + assert ( + valid_dataset.inter_feat[valid_dataset.item_id_list_field][:, :5].numpy() + == [[8, 1, 2, 3, 0], [8, 1, 2, 3, 4], [3, 4, 5, 0, 0]] + ).all() + assert ( + valid_dataset.inter_feat[valid_dataset.iid_field].numpy() == [4, 5, 6] + ).all() + assert ( + valid_dataset.inter_feat[valid_dataset.item_list_length_field].numpy() + == [4, 5, 3] + ).all() + + assert ( + test_dataset.inter_feat[test_dataset.uid_field].numpy() == [1, 1, 3, 4] + ).all() + assert ( + test_dataset.inter_feat[test_dataset.item_id_list_field][:, :7].numpy() + == [ + [8, 1, 2, 3, 4, 5, 0], + [8, 1, 2, 3, 4, 5, 6], + [3, 4, 5, 6, 0, 0, 0], + [3, 4, 0, 0, 0, 0, 0], + ] + ).all() + assert ( + test_dataset.inter_feat[test_dataset.iid_field].numpy() == [6, 7, 7, 5] + ).all() + assert ( + test_dataset.inter_feat[test_dataset.item_list_length_field].numpy() + == [6, 7, 4, 2] + ).all() class TestKGDataset: def test_kg_remap_id(self): config_dict = { - 'model': 'KGAT', - 'dataset': 'kg_remap_id', - 'data_path': current_path, - 'load_col': None, + "model": "KGAT", + "dataset": "kg_remap_id", + "data_path": current_path, + "load_col": None, } dataset = new_dataset(config_dict=config_dict) - item_list = dataset.token2id('item_id', ['ib', 'ic', 'id']) - entity_list = dataset.token2id('entity_id', ['eb', 'ec', 'ed', 'ee', 'ea']) + item_list = dataset.token2id("item_id", ["ib", "ic", "id"]) + entity_list = dataset.token2id("entity_id", ["eb", "ec", "ed", "ee", "ea"]) assert (item_list == [1, 2, 3]).all() assert (entity_list == [1, 2, 3, 4, 5]).all() - assert (dataset.inter_feat['user_id'] == [1, 2, 3]).all() - assert (dataset.inter_feat['item_id'] == [1, 2, 3]).all() - assert (dataset.kg_feat['head_id'] == [1, 2, 3, 4]).all() - assert (dataset.kg_feat['tail_id'] == [5, 1, 2, 3]).all() + assert (dataset.inter_feat["user_id"] == [1, 2, 3]).all() + assert (dataset.inter_feat["item_id"] == [1, 2, 3]).all() + assert (dataset.kg_feat["head_id"] == [1, 2, 3, 4]).all() + assert (dataset.kg_feat["tail_id"] == [5, 1, 2, 3]).all() def test_kg_reverse_r(self): config_dict = { - 'model': 'KGAT', - 'dataset': 'kg_reverse_r', - 'kg_reverse_r': True, - 'data_path': current_path, - 'load_col': None, + "model": "KGAT", + "dataset": "kg_reverse_r", + "kg_reverse_r": True, + "data_path": current_path, + "load_col": None, } dataset = new_dataset(config_dict=config_dict) - relation_list = dataset.token2id('relation_id', ['ra', 'rb', 'ra_r', 'rb_r']) + relation_list = dataset.token2id("relation_id", ["ra", "rb", "ra_r", "rb_r"]) assert (relation_list == [1, 2, 5, 6]).all() assert dataset.relation_num == 10 def test_kg_filter_by_triple_num_in_min_entity_kg_num(self): config_dict = { - 'model': 'KGAT', - 'dataset': 'kg_filter_by_triple_num', - 'data_path': current_path, - 'load_col': None, - 'entity_kg_num_interval': "[2,inf)", + "model": "KGAT", + "dataset": "kg_filter_by_triple_num", + "data_path": current_path, + "load_col": None, + "entity_kg_num_interval": "[2,inf)", } dataset = new_dataset(config_dict=config_dict) assert dataset.entity_num == 6 @@ -771,11 +996,11 @@ def test_kg_filter_by_triple_num_in_min_entity_kg_num(self): def test_kg_filter_by_triple_num_in_min_relation_kg_num(self): config_dict = { - 'model': 'KGAT', - 'dataset': 'kg_filter_by_triple_num', - 'data_path': current_path, - 'load_col': None, - 'relation_kg_num_interval': "[2,inf)", + "model": "KGAT", + "dataset": "kg_filter_by_triple_num", + "data_path": current_path, + "load_col": None, + "relation_kg_num_interval": "[2,inf)", } dataset = new_dataset(config_dict=config_dict) assert dataset.entity_num == 7 @@ -783,11 +1008,11 @@ def test_kg_filter_by_triple_num_in_min_relation_kg_num(self): def test_kg_filter_by_triple_num_in_max_entity_kg_num(self): config_dict = { - 'model': 'KGAT', - 'dataset': 'kg_filter_by_triple_num', - 'data_path': current_path, - 'load_col': None, - 'entity_kg_num_interval': "(-inf,3]", + "model": "KGAT", + "dataset": "kg_filter_by_triple_num", + "data_path": current_path, + "load_col": None, + "entity_kg_num_interval": "(-inf,3]", } dataset = new_dataset(config_dict=config_dict) assert dataset.entity_num == 3 @@ -795,11 +1020,11 @@ def test_kg_filter_by_triple_num_in_max_entity_kg_num(self): def test_kg_filter_by_triple_num_in_max_relation_kg_num(self): config_dict = { - 'model': 'KGAT', - 'dataset': 'kg_filter_by_triple_num', - 'data_path': current_path, - 'load_col': None, - 'relation_kg_num_interval': "(-inf,2]", + "model": "KGAT", + "dataset": "kg_filter_by_triple_num", + "data_path": current_path, + "load_col": None, + "relation_kg_num_interval": "(-inf,2]", } dataset = new_dataset(config_dict=config_dict) assert dataset.entity_num == 6 @@ -807,12 +1032,12 @@ def test_kg_filter_by_triple_num_in_max_relation_kg_num(self): def test_kg_filter_by_triple_num_in_min_kg_num(self): config_dict = { - 'model': 'KGAT', - 'dataset': 'kg_filter_by_triple_num', - 'data_path': current_path, - 'load_col': None, - 'entity_kg_num_interval': "[1,inf)", - 'relation_kg_num_interval': "[2,inf)", + "model": "KGAT", + "dataset": "kg_filter_by_triple_num", + "data_path": current_path, + "load_col": None, + "entity_kg_num_interval": "[1,inf)", + "relation_kg_num_interval": "[2,inf)", } dataset = new_dataset(config_dict=config_dict) assert dataset.entity_num == 7 @@ -820,12 +1045,12 @@ def test_kg_filter_by_triple_num_in_min_kg_num(self): def test_kg_filter_by_triple_num_in_complex_way(self): config_dict = { - 'model': 'KGAT', - 'dataset': 'kg_filter_by_triple_num', - 'data_path': current_path, - 'load_col': None, - 'entity_kg_num_interval': "[1,4]", - 'relation_kg_num_interval': "[2,inf)", + "model": "KGAT", + "dataset": "kg_filter_by_triple_num", + "data_path": current_path, + "load_col": None, + "entity_kg_num_interval": "[1,4]", + "relation_kg_num_interval": "[2,inf)", } dataset = new_dataset(config_dict=config_dict) assert dataset.entity_num == 7 diff --git a/tests/evaluation_setting/test_evaluation_setting.py b/tests/evaluation_setting/test_evaluation_setting.py index 19d240d14..56a160191 100644 --- a/tests/evaluation_setting/test_evaluation_setting.py +++ b/tests/evaluation_setting/test_evaluation_setting.py @@ -13,91 +13,133 @@ from recbole.quick_start import objective_function current_path = os.path.dirname(os.path.realpath(__file__)) -config_file_list = [os.path.join(current_path, '../model/test_model.yaml')] +config_file_list = [os.path.join(current_path, "../model/test_model.yaml")] class TestGeneralRecommender(unittest.TestCase): - def test_rols_full(self): config_dict = { - 'eval_args': {'split': {'LS': 'valid_and_test'}, 'order': 'RO', 'mode': 'full'}, - 'model': 'BPR', + "eval_args": { + "split": {"LS": "valid_and_test"}, + "order": "RO", + "mode": "full", + }, + "model": "BPR", } - objective_function(config_dict=config_dict, - config_file_list=config_file_list, saved=False) + objective_function( + config_dict=config_dict, config_file_list=config_file_list, saved=False + ) def test_tols_full(self): config_dict = { - 'eval_args': {'split': {'LS': 'valid_and_test'}, 'order': 'TO', 'mode': 'full'}, - 'model': 'BPR', + "eval_args": { + "split": {"LS": "valid_and_test"}, + "order": "TO", + "mode": "full", + }, + "model": "BPR", } - objective_function(config_dict=config_dict, - config_file_list=config_file_list, saved=False) + objective_function( + config_dict=config_dict, config_file_list=config_file_list, saved=False + ) def test_tors_full(self): config_dict = { - 'eval_args': {'split': {'RS': [0.8, 0.1, 0.1]}, 'order': 'RO', 'mode': 'full'}, - 'model': 'BPR', + "eval_args": { + "split": {"RS": [0.8, 0.1, 0.1]}, + "order": "RO", + "mode": "full", + }, + "model": "BPR", } - objective_function(config_dict=config_dict, - config_file_list=config_file_list, saved=False) + objective_function( + config_dict=config_dict, config_file_list=config_file_list, saved=False + ) def test_rors_uni100(self): config_dict = { - 'eval_args': {'split': {'RS': [0.8, 0.1, 0.1]}, 'order': 'RO', 'mode': 'uni100'}, - 'model': 'BPR', + "eval_args": { + "split": {"RS": [0.8, 0.1, 0.1]}, + "order": "RO", + "mode": "uni100", + }, + "model": "BPR", } - objective_function(config_dict=config_dict, - config_file_list=config_file_list, saved=False) + objective_function( + config_dict=config_dict, config_file_list=config_file_list, saved=False + ) def test_tols_uni100(self): config_dict = { - 'eval_setting': 'TO_LS,uni100', - 'eval_args': {'split': {'LS': 'valid_and_test'}, 'order': 'TO', 'mode': 'full'}, - 'model': 'BPR', + "eval_setting": "TO_LS,uni100", + "eval_args": { + "split": {"LS": "valid_and_test"}, + "order": "TO", + "mode": "full", + }, + "model": "BPR", } - objective_function(config_dict=config_dict, - config_file_list=config_file_list, saved=False) + objective_function( + config_dict=config_dict, config_file_list=config_file_list, saved=False + ) def test_rols_uni100(self): config_dict = { - 'eval_args': {'split': {'LS': 'valid_and_test'}, 'order': 'RO', 'mode': 'uni100'}, - 'model': 'BPR', + "eval_args": { + "split": {"LS": "valid_and_test"}, + "order": "RO", + "mode": "uni100", + }, + "model": "BPR", } - objective_function(config_dict=config_dict, - config_file_list=config_file_list, saved=False) + objective_function( + config_dict=config_dict, config_file_list=config_file_list, saved=False + ) def test_tors_uni100(self): config_dict = { - 'eval_args': {'split': {'RS': [0.8, 0.1, 0.1]}, 'order': 'TO', 'mode': 'uni100'}, - 'model': 'BPR', + "eval_args": { + "split": {"RS": [0.8, 0.1, 0.1]}, + "order": "TO", + "mode": "uni100", + }, + "model": "BPR", } - objective_function(config_dict=config_dict, - config_file_list=config_file_list, saved=False) + objective_function( + config_dict=config_dict, config_file_list=config_file_list, saved=False + ) class TestContextRecommender(unittest.TestCase): - def test_tors(self): config_dict = { - 'eval_args': {'split': {'RS': [0.8, 0.1, 0.1]}, 'order': 'TO', 'mode': 'labeled'}, - 'threshold': {'rating': 4}, - 'model': 'FM', + "eval_args": { + "split": {"RS": [0.8, 0.1, 0.1]}, + "order": "TO", + "mode": "labeled", + }, + "threshold": {"rating": 4}, + "model": "FM", } - objective_function(config_dict=config_dict, - config_file_list=config_file_list, saved=False) + objective_function( + config_dict=config_dict, config_file_list=config_file_list, saved=False + ) class TestSequentialRecommender(unittest.TestCase): - def test_tols_uni100(self): config_dict = { - 'eval_args': {'split': {'LS': 'valid_and_test'}, 'order': 'TO', 'mode': 'uni100'}, - 'model': 'FPMC', + "eval_args": { + "split": {"LS": "valid_and_test"}, + "order": "TO", + "mode": "uni100", + }, + "model": "FPMC", } - objective_function(config_dict=config_dict, - config_file_list=config_file_list, saved=False) + objective_function( + config_dict=config_dict, config_file_list=config_file_list, saved=False + ) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/metrics/test_loss_metrics.py b/tests/metrics/test_loss_metrics.py index c6b99fadf..e91d9dbd2 100644 --- a/tests/metrics/test_loss_metrics.py +++ b/tests/metrics/test_loss_metrics.py @@ -14,10 +14,10 @@ from recbole.evaluator.register import metrics_dict parameters_dict = { - 'metric_decimal_place': 4, + "metric_decimal_place": 4, } -config = Config('BPR', 'ml-1m', config_dict=parameters_dict) +config = Config("BPR", "ml-1m", config_dict=parameters_dict) class TestCases(object): @@ -31,34 +31,40 @@ class TestCases(object): def get_result(name, case=0): Metric = metrics_dict[name](config) return Metric.metric_info( - getattr(TestCases, f'preds_{case}'), - getattr(TestCases, f'trues_{case}')) + getattr(TestCases, f"preds_{case}"), getattr(TestCases, f"trues_{case}") + ) class TestLossMetrics(unittest.TestCase): def test_auc(self): - name = 'auc' + name = "auc" self.assertEqual(get_result(name, case=0), 0) self.assertEqual(get_result(name, case=1), 2 / (2 * 2)) def test_rmse(self): - name = 'rmse' - self.assertEqual(get_result(name, case=0), - np.sqrt((0.9 ** 2 + 0.9 ** 2 + 0.8 ** 2 + 0.7 ** 2) / 4)) - self.assertEqual(get_result(name, case=1), - np.sqrt((0.7 ** 2 + 0.5 ** 2 + 0.4 ** 2 + 0.2 ** 2) / 4)) + name = "rmse" + self.assertEqual( + get_result(name, case=0), + np.sqrt((0.9**2 + 0.9**2 + 0.8**2 + 0.7**2) / 4), + ) + self.assertEqual( + get_result(name, case=1), + np.sqrt((0.7**2 + 0.5**2 + 0.4**2 + 0.2**2) / 4), + ) def test_logloss(self): - name = 'logloss' + name = "logloss" self.assertAlmostEqual( get_result(name, case=0), - (-np.log(0.1) - np.log(0.2) - np.log(0.3) - np.log(0.1)) / 4) + (-np.log(0.1) - np.log(0.2) - np.log(0.3) - np.log(0.1)) / 4, + ) self.assertAlmostEqual( get_result(name, case=1), - (-np.log(0.5) - np.log(0.6) - np.log(0.3) - np.log(0.8)) / 4) + (-np.log(0.5) - np.log(0.6) - np.log(0.3) - np.log(0.8)) / 4, + ) def test_mae(self): - name = 'mae' + name = "mae" self.assertEqual(get_result(name, case=0), (0.9 + 0.9 + 0.8 + 0.7) / 4) self.assertEqual(get_result(name, case=1), (0.7 + 0.5 + 0.4 + 0.2) / 4) diff --git a/tests/metrics/test_rank_metrics.py b/tests/metrics/test_rank_metrics.py index 74851a64d..a6eba9791 100644 --- a/tests/metrics/test_rank_metrics.py +++ b/tests/metrics/test_rank_metrics.py @@ -18,12 +18,12 @@ from recbole.evaluator import metrics_dict, Collector parameters_dict = { - 'model': 'BPR', - 'eval_args': {'split':{'RS':[0.8,0.1,0.1]}, 'order': 'RO', 'mode': 'uni100'}, - 'metric_decimal_place': 4, + "model": "BPR", + "eval_args": {"split": {"RS": [0.8, 0.1, 0.1]}, "order": "RO", "mode": "uni100"}, + "metric_decimal_place": 4, } -config = Config('BPR', 'ml-1m', config_dict=parameters_dict) +config = Config("BPR", "ml-1m", config_dict=parameters_dict) class MetricsTestCases(object): @@ -39,18 +39,24 @@ class MetricsTestCases(object): def get_metric_result(name, case=0): Metric = metrics_dict[name](config) return Metric.metric_info( - getattr(MetricsTestCases, f'pos_rank_sum{case}'), - getattr(MetricsTestCases, f'user_len_list{case}'), - getattr(MetricsTestCases, f'pos_len_list{case}')) + getattr(MetricsTestCases, f"pos_rank_sum{case}"), + getattr(MetricsTestCases, f"user_len_list{case}"), + getattr(MetricsTestCases, f"pos_len_list{case}"), + ) class TestRankMetrics(unittest.TestCase): def test_gauc(self): - name = 'gauc' - self.assertEqual(get_metric_result(name, case=0), (1 * ((2 - (1 - 1) / 2 - 1 / 1) / (2 - 1)) + - 2 * ((3 - (2 - 1) / 2 - 4 / 2) / (3 - 2)) + - 3 * ((5 - (3 - 1) / 2 - 9 / 3) / (5 - 3))) - / (1 + 2 + 3)) + name = "gauc" + self.assertEqual( + get_metric_result(name, case=0), + ( + 1 * ((2 - (1 - 1) / 2 - 1 / 1) / (2 - 1)) + + 2 * ((3 - (2 - 1) / 2 - 4 / 2) / (3 - 2)) + + 3 * ((5 - (3 - 1) / 2 - 9 / 3) / (5 - 3)) + ) + / (1 + 2 + 3), + ) self.assertEqual(get_metric_result(name, case=1), (3 - 0 - 3 / 1) / (3 - 1)) diff --git a/tests/metrics/test_topk_metrics.py b/tests/metrics/test_topk_metrics.py index b58fb628c..ee9169396 100644 --- a/tests/metrics/test_topk_metrics.py +++ b/tests/metrics/test_topk_metrics.py @@ -18,133 +18,165 @@ from recbole.evaluator.register import metrics_dict parameters_dict = { - 'topk': [10], - 'metric_decimal_place': 4, + "topk": [10], + "metric_decimal_place": 4, } -config = Config('BPR', 'ml-1m', config_dict=parameters_dict) -pos_idx = np.array([ - [0, 0, 0], - [1, 1, 1], - [1, 0, 1], - [0, 0, 1], -]) +config = Config("BPR", "ml-1m", config_dict=parameters_dict) +pos_idx = np.array( + [ + [0, 0, 0], + [1, 1, 1], + [1, 0, 1], + [0, 0, 1], + ] +) pos_len = np.array([1, 3, 4, 2]) -item_matrix = np.array([ - [5, 7, 3], - [4, 5, 2], - [2, 3, 5], - [1, 4, 6], - [5, 3, 7] -]) +item_matrix = np.array([[5, 7, 3], [4, 5, 2], [2, 3, 5], [1, 4, 6], [5, 3, 7]]) num_items = 8 -item_count = {1: 0, - 2: 1, - 3: 2, - 4: 3, - 5: 4, - 6: 5} +item_count = {1: 0, 2: 1, 3: 2, 4: 3, 5: 4, 6: 5} class TestTopKMetrics(unittest.TestCase): def test_hit(self): - name = 'hit' + name = "hit" Metric = metrics_dict[name](config) self.assertEqual( Metric.metric_info(pos_idx).tolist(), - np.array([[0, 0, 0], [1, 1, 1], [1, 1, 1], [0, 0, 1]]).tolist()) + np.array([[0, 0, 0], [1, 1, 1], [1, 1, 1], [0, 0, 1]]).tolist(), + ) def test_ndcg(self): - name = 'ndcg' + name = "ndcg" Metric = metrics_dict[name](config) self.assertEqual( Metric.metric_info(pos_idx, pos_len).tolist(), - np.array([[0, 0, 0], [1, 1, 1], - [ - 1, - (1 / np.log2(2) / (1 / np.log2(2) + 1 / np.log2(3))), - ((1 / np.log2(2) + 1 / np.log2(4)) / (1 / np.log2(2) + 1 / np.log2(3) + 1 / np.log2(4))) - ], - [ - 0, - 0, - (1 / np.log2(4) / (1 / np.log2(2) + 1 / np.log2(3))) - ]]).tolist()) + np.array( + [ + [0, 0, 0], + [1, 1, 1], + [ + 1, + (1 / np.log2(2) / (1 / np.log2(2) + 1 / np.log2(3))), + ( + (1 / np.log2(2) + 1 / np.log2(4)) + / (1 / np.log2(2) + 1 / np.log2(3) + 1 / np.log2(4)) + ), + ], + [0, 0, (1 / np.log2(4) / (1 / np.log2(2) + 1 / np.log2(3)))], + ] + ).tolist(), + ) def test_mrr(self): - name = 'mrr' + name = "mrr" Metric = metrics_dict[name](config) self.assertEqual( Metric.metric_info(pos_idx).tolist(), - np.array([[0, 0, 0], [1, 1, 1], [1, 1, 1], [0, 0, - 1 / 3]]).tolist()) + np.array([[0, 0, 0], [1, 1, 1], [1, 1, 1], [0, 0, 1 / 3]]).tolist(), + ) def test_map(self): - name = 'map' + name = "map" Metric = metrics_dict[name](config) self.assertEqual( Metric.metric_info(pos_idx, pos_len).tolist(), - np.array([[0, 0, 0], [1, 1, 1], - [1, (1 / 2), (1 / 3) * ((1 / 1) + (2 / 3))], - [0, 0, (1 / 3) * (1 / 2)]]).tolist()) + np.array( + [ + [0, 0, 0], + [1, 1, 1], + [1, (1 / 2), (1 / 3) * ((1 / 1) + (2 / 3))], + [0, 0, (1 / 3) * (1 / 2)], + ] + ).tolist(), + ) def test_recall(self): - name = 'recall' + name = "recall" Metric = metrics_dict[name](config) self.assertEqual( Metric.metric_info(pos_idx, pos_len).tolist(), - np.array([[0, 0, 0], [1 / 3, 2 / 3, 3 / 3], [1 / 4, 1 / 4, 2 / 4], - [0, 0, 1 / 2]]).tolist()) + np.array( + [[0, 0, 0], [1 / 3, 2 / 3, 3 / 3], [1 / 4, 1 / 4, 2 / 4], [0, 0, 1 / 2]] + ).tolist(), + ) def test_precision(self): - name = 'precision' + name = "precision" Metric = metrics_dict[name](config) self.assertEqual( Metric.metric_info(pos_idx).tolist(), - np.array([[0, 0, 0], [1 / 1, 2 / 2, 3 / 3], [1 / 1, 1 / 2, 2 / 3], - [0, 0, 1 / 3]]).tolist()) + np.array( + [[0, 0, 0], [1 / 1, 2 / 2, 3 / 3], [1 / 1, 1 / 2, 2 / 3], [0, 0, 1 / 3]] + ).tolist(), + ) def test_itemcoverage(self): - name = 'itemcoverage' + name = "itemcoverage" Metric = metrics_dict[name](config) - self.assertEqual( - Metric.get_coverage(item_matrix, num_items), - 7 / 8) + self.assertEqual(Metric.get_coverage(item_matrix, num_items), 7 / 8) def test_averagepopularity(self): - name = 'averagepopularity' + name = "averagepopularity" Metric = metrics_dict[name](config) self.assertEqual( Metric.metric_info(Metric.get_pop(item_matrix, item_count)).tolist(), - np.array([[4/1, 4/2, 6/3], [3/1, 7/2, 8/3], [1/1, 3/2, 7/3], [0/1, 3/2, 8/3], - [4/1, 6/2, 6/3]]).tolist()) + np.array( + [ + [4 / 1, 4 / 2, 6 / 3], + [3 / 1, 7 / 2, 8 / 3], + [1 / 1, 3 / 2, 7 / 3], + [0 / 1, 3 / 2, 8 / 3], + [4 / 1, 6 / 2, 6 / 3], + ] + ).tolist(), + ) def test_giniindex(self): - name = 'giniindex' + name = "giniindex" Metric = metrics_dict[name](config) self.assertEqual( Metric.get_gini(item_matrix, num_items), ((-7) * 0 + (-5) * 1 + (-3) * 1 + (-1) * 2 + 1 * 2 + 3 * 2 + 5 * 3 + 7 * 4) - / (8 * (3 * 5))) + / (8 * (3 * 5)), + ) def test_shannonentropy(self): - name = 'shannonentropy' + name = "shannonentropy" Metric = metrics_dict[name](config) self.assertEqual( Metric.get_entropy(item_matrix), - -np.mean([1/15*np.log(1/15), 2/15*np.log(2/15), 3/15*np.log(3/15), 2/15*np.log(2/15), - 4/15*np.log(4/15), 1/15*np.log(1/15), 2/15*np.log(2/15)])) + -np.mean( + [ + 1 / 15 * np.log(1 / 15), + 2 / 15 * np.log(2 / 15), + 3 / 15 * np.log(3 / 15), + 2 / 15 * np.log(2 / 15), + 4 / 15 * np.log(4 / 15), + 1 / 15 * np.log(1 / 15), + 2 / 15 * np.log(2 / 15), + ] + ), + ) def test_tailpercentage(self): - name = 'tailpercentage' + name = "tailpercentage" Metric = metrics_dict[name](config) self.assertEqual( Metric.metric_info(Metric.get_tail(item_matrix, item_count)).tolist(), - np.array([[0 / 1, 0 / 2, 0 / 3], [0 / 1, 0 / 2, 0 / 3], [0 / 1, 0 / 2, 0 / 3], [1 / 1, 1 / 2, 1 / 3], - [0 / 1, 0 / 2, 0 / 3]]).tolist()) + np.array( + [ + [0 / 1, 0 / 2, 0 / 3], + [0 / 1, 0 / 2, 0 / 3], + [0 / 1, 0 / 2, 0 / 3], + [1 / 1, 1 / 2, 1 / 3], + [0 / 1, 0 / 2, 0 / 3], + ] + ).tolist(), + ) if __name__ == "__main__": diff --git a/tests/model/test_model_auto.py b/tests/model/test_model_auto.py index c6aa85d95..198a54663 100644 --- a/tests/model/test_model_auto.py +++ b/tests/model/test_model_auto.py @@ -14,248 +14,222 @@ from recbole.quick_start import objective_function current_path = os.path.dirname(os.path.realpath(__file__)) -config_file_list = [os.path.join(current_path, 'test_model.yaml')] +config_file_list = [os.path.join(current_path, "test_model.yaml")] def quick_test(config_dict): - objective_function(config_dict=config_dict, config_file_list=config_file_list, saved=False) + objective_function( + config_dict=config_dict, config_file_list=config_file_list, saved=False + ) class TestGeneralRecommender(unittest.TestCase): - def test_pop(self): config_dict = { - 'model': 'Pop', + "model": "Pop", } quick_test(config_dict) def test_itemknn(self): config_dict = { - 'model': 'ItemKNN', + "model": "ItemKNN", } quick_test(config_dict) def test_bpr(self): config_dict = { - 'model': 'BPR', + "model": "BPR", } quick_test(config_dict) def test_bpr_with_dns(self): config_dict = { - 'model': 'BPR', - 'train_neg_sample_args': { - 'distribution': 'uniform', - 'sample_num': 1, - 'dynamic': True, - 'candidate_num': 2 - } + "model": "BPR", + "train_neg_sample_args": { + "distribution": "uniform", + "sample_num": 1, + "dynamic": True, + "candidate_num": 2, + }, } quick_test(config_dict) def test_neumf(self): config_dict = { - 'model': 'NeuMF', + "model": "NeuMF", } quick_test(config_dict) def test_convncf(self): config_dict = { - 'model': 'ConvNCF', + "model": "ConvNCF", } quick_test(config_dict) def test_dmf(self): config_dict = { - 'model': 'DMF', + "model": "DMF", } quick_test(config_dict) def test_dmf_with_rating(self): config_dict = { - 'model': 'DMF', - 'inter_matrix_type': 'rating', + "model": "DMF", + "inter_matrix_type": "rating", } quick_test(config_dict) def test_fism(self): config_dict = { - 'model': 'FISM', + "model": "FISM", } quick_test(config_dict) def test_fism_with_split_to_and_alpha(self): config_dict = { - 'model': 'FISM', - 'split_to': 10, - 'alpha': 0.5, + "model": "FISM", + "split_to": 10, + "alpha": 0.5, } quick_test(config_dict) def test_nais(self): config_dict = { - 'model': 'NAIS', + "model": "NAIS", } quick_test(config_dict) def test_nais_with_concat(self): config_dict = { - 'model': 'NAIS', - 'algorithm': 'concat', - 'split_to': 10, - 'alpha': 0.5, - 'beta': 0.1, + "model": "NAIS", + "algorithm": "concat", + "split_to": 10, + "alpha": 0.5, + "beta": 0.1, } quick_test(config_dict) def test_spectralcf(self): config_dict = { - 'model': 'SpectralCF', + "model": "SpectralCF", } quick_test(config_dict) def test_gcmc(self): config_dict = { - 'model': 'GCMC', + "model": "GCMC", } quick_test(config_dict) def test_gcmc_with_stack(self): config_dict = { - 'model': 'GCMC', - 'accum': 'stack', - 'sparse_feature': False, + "model": "GCMC", + "accum": "stack", + "sparse_feature": False, } quick_test(config_dict) def test_ngcf(self): config_dict = { - 'model': 'NGCF', + "model": "NGCF", } quick_test(config_dict) def test_lightgcn(self): config_dict = { - 'model': 'LightGCN', + "model": "LightGCN", } quick_test(config_dict) def test_dgcf(self): config_dict = { - 'model': 'DGCF', + "model": "DGCF", } quick_test(config_dict) def test_line(self): config_dict = { - 'model': 'LINE', + "model": "LINE", } quick_test(config_dict) def test_ease(self): config_dict = { - 'model': 'EASE', + "model": "EASE", } quick_test(config_dict) def test_MultiDAE(self): - config_dict = { - 'model': 'MultiDAE', - 'train_neg_sample_args': None - } + config_dict = {"model": "MultiDAE", "train_neg_sample_args": None} quick_test(config_dict) def test_MultiVAE(self): - config_dict = { - 'model': 'MultiVAE', - 'train_neg_sample_args': None - } + config_dict = {"model": "MultiVAE", "train_neg_sample_args": None} quick_test(config_dict) def test_enmf(self): config_dict = { - 'model': 'ENMF', - 'train_neg_sample_args': None, + "model": "ENMF", + "train_neg_sample_args": None, } quick_test(config_dict) def test_MacridVAE(self): - config_dict = { - 'model': 'MacridVAE', - 'train_neg_sample_args': None - } + config_dict = {"model": "MacridVAE", "train_neg_sample_args": None} quick_test(config_dict) def test_CDAE(self): - config_dict = { - 'model': 'CDAE', - 'train_neg_sample_args': None - } + config_dict = {"model": "CDAE", "train_neg_sample_args": None} quick_test(config_dict) def test_NNCF(self): config_dict = { - 'model': 'NNCF', + "model": "NNCF", } quick_test(config_dict) def test_RecVAE(self): - config_dict = { - 'model': 'RecVAE', - 'train_neg_sample_args': None - } + config_dict = {"model": "RecVAE", "train_neg_sample_args": None} quick_test(config_dict) def test_slimelastic(self): config_dict = { - 'model': 'SLIMElastic', + "model": "SLIMElastic", } quick_test(config_dict) - + def test_SGL(self): config_dict = { - 'model': 'SGL', + "model": "SGL", } quick_test(config_dict) - + def test_ADMMSLIM(self): config_dict = { - 'model': 'ADMMSLIM', + "model": "ADMMSLIM", } quick_test(config_dict) def test_SimpleX_with_mean(self): - config_dict = { - 'model': 'SimpleX', - 'aggregator': 'mean' - } + config_dict = {"model": "SimpleX", "aggregator": "mean"} quick_test(config_dict) def test_SimpleX_with_user_attention(self): - config_dict = { - 'model': 'SimpleX', - 'aggregator': 'user_attention' - } + config_dict = {"model": "SimpleX", "aggregator": "user_attention"} quick_test(config_dict) def test_SimpleX_with_self_attention(self): - config_dict = { - 'model': 'SimpleX', - 'aggregator': 'self_attention' - } + config_dict = {"model": "SimpleX", "aggregator": "self_attention"} quick_test(config_dict) def test_NCEPLRec(self): config_dict = { - 'model': 'NCEPLRec', + "model": "NCEPLRec", } quick_test(config_dict) def test_NCL(self): - config_dict = { - 'model': 'NCL', - 'num_clusters': 100 - } + config_dict = {"model": "NCL", "num_clusters": 100} quick_test(config_dict) @@ -264,515 +238,457 @@ class TestContextRecommender(unittest.TestCase): def test_lr(self): config_dict = { - 'model': 'LR', - 'threshold': {'rating': 4}, + "model": "LR", + "threshold": {"rating": 4}, } quick_test(config_dict) def test_fm(self): config_dict = { - 'model': 'FM', - 'threshold': {'rating': 4}, + "model": "FM", + "threshold": {"rating": 4}, } quick_test(config_dict) def test_nfm(self): config_dict = { - 'model': 'NFM', - 'threshold': {'rating': 4}, + "model": "NFM", + "threshold": {"rating": 4}, } quick_test(config_dict) def test_deepfm(self): config_dict = { - 'model': 'DeepFM', - 'threshold': {'rating': 4}, + "model": "DeepFM", + "threshold": {"rating": 4}, } quick_test(config_dict) def test_xdeepfm(self): config_dict = { - 'model': 'xDeepFM', - 'threshold': {'rating': 4}, + "model": "xDeepFM", + "threshold": {"rating": 4}, } quick_test(config_dict) def test_xdeepfm_with_direct(self): config_dict = { - 'model': 'xDeepFM', - 'threshold': {'rating': 4}, - 'direct': True, + "model": "xDeepFM", + "threshold": {"rating": 4}, + "direct": True, } quick_test(config_dict) def test_afm(self): config_dict = { - 'model': 'AFM', - 'threshold': {'rating': 4}, + "model": "AFM", + "threshold": {"rating": 4}, } quick_test(config_dict) def test_fnn(self): config_dict = { - 'model': 'FNN', - 'threshold': {'rating': 4}, + "model": "FNN", + "threshold": {"rating": 4}, } quick_test(config_dict) def test_pnn(self): config_dict = { - 'model': 'PNN', - 'threshold': {'rating': 4}, + "model": "PNN", + "threshold": {"rating": 4}, } quick_test(config_dict) def test_pnn_with_use_inner_and_use_outer(self): config_dict = { - 'model': 'PNN', - 'threshold': {'rating': 4}, - 'use_inner': True, - 'use_outer': True, + "model": "PNN", + "threshold": {"rating": 4}, + "use_inner": True, + "use_outer": True, } quick_test(config_dict) def test_pnn_without_use_inner_and_use_outer(self): config_dict = { - 'model': 'PNN', - 'threshold': {'rating': 4}, - 'use_inner': False, - 'use_outer': False, + "model": "PNN", + "threshold": {"rating": 4}, + "use_inner": False, + "use_outer": False, } quick_test(config_dict) def test_dssm(self): config_dict = { - 'model': 'DSSM', - 'threshold': {'rating': 4}, + "model": "DSSM", + "threshold": {"rating": 4}, } quick_test(config_dict) def test_widedeep(self): config_dict = { - 'model': 'WideDeep', - 'threshold': {'rating': 4}, + "model": "WideDeep", + "threshold": {"rating": 4}, } quick_test(config_dict) def test_autoint(self): config_dict = { - 'model': 'AutoInt', - 'threshold': {'rating': 4}, + "model": "AutoInt", + "threshold": {"rating": 4}, } quick_test(config_dict) def test_ffm(self): config_dict = { - 'model': 'FFM', - 'threshold': {'rating': 4}, + "model": "FFM", + "threshold": {"rating": 4}, } quick_test(config_dict) def test_fwfm(self): config_dict = { - 'model': 'FwFM', - 'threshold': {'rating': 4}, + "model": "FwFM", + "threshold": {"rating": 4}, } quick_test(config_dict) def test_dcn(self): config_dict = { - 'model': 'DCN', - 'threshold': {'rating': 4}, + "model": "DCN", + "threshold": {"rating": 4}, } quick_test(config_dict) def test_xgboost(self): config_dict = { - 'model': 'xgboost', - 'threshold': {'rating': 4}, - 'xgb_params': { - 'booster': 'gbtree', - 'objective': 'binary:logistic', - 'eval_metric': ['auc', 'logloss'] + "model": "xgboost", + "threshold": {"rating": 4}, + "xgb_params": { + "booster": "gbtree", + "objective": "binary:logistic", + "eval_metric": ["auc", "logloss"], }, - 'xgb_num_boost_round': 1, + "xgb_num_boost_round": 1, } quick_test(config_dict) def test_lightgbm(self): config_dict = { - 'model': 'lightgbm', - 'threshold': {'rating': 4}, - 'lgb_params': { - 'boosting': 'gbdt', - 'objective': 'binary', - 'metric': ['auc', 'binary_logloss'] + "model": "lightgbm", + "threshold": {"rating": 4}, + "lgb_params": { + "boosting": "gbdt", + "objective": "binary", + "metric": ["auc", "binary_logloss"], }, - 'lgb_num_boost_round': 1, + "lgb_num_boost_round": 1, } quick_test(config_dict) class TestSequentialRecommender(unittest.TestCase): - def test_din(self): config_dict = { - 'model': 'DIN', + "model": "DIN", } quick_test(config_dict) def test_dien(self): config_dict = { - 'model': 'DIEN', + "model": "DIEN", } quick_test(config_dict) def test_fpmc(self): config_dict = { - 'model': 'FPMC', + "model": "FPMC", } quick_test(config_dict) def test_gru4rec(self): - config_dict = { - 'model': 'GRU4Rec', - 'train_neg_sample_args': None - } + config_dict = {"model": "GRU4Rec", "train_neg_sample_args": None} quick_test(config_dict) def test_gru4rec_with_BPR_loss(self): config_dict = { - 'model': 'GRU4Rec', - 'loss_type': 'BPR', + "model": "GRU4Rec", + "loss_type": "BPR", } quick_test(config_dict) def test_narm(self): - config_dict = { - 'model': 'NARM', - 'train_neg_sample_args': None - } + config_dict = {"model": "NARM", "train_neg_sample_args": None} quick_test(config_dict) def test_narm_with_BPR_loss(self): config_dict = { - 'model': 'NARM', - 'loss_type': 'BPR', + "model": "NARM", + "loss_type": "BPR", } quick_test(config_dict) def test_stamp(self): - config_dict = { - 'model': 'STAMP', - 'train_neg_sample_args': None - } + config_dict = {"model": "STAMP", "train_neg_sample_args": None} quick_test(config_dict) def test_stamp_with_BPR_loss(self): config_dict = { - 'model': 'STAMP', - 'loss_type': 'BPR', + "model": "STAMP", + "loss_type": "BPR", } quick_test(config_dict) def test_caser(self): config_dict = { - 'model': 'Caser', - 'MAX_ITEM_LIST_LENGTH': 10, - 'reproducibility': False, - 'train_neg_sample_args': None + "model": "Caser", + "MAX_ITEM_LIST_LENGTH": 10, + "reproducibility": False, + "train_neg_sample_args": None, } quick_test(config_dict) def test_caser_with_BPR_loss(self): config_dict = { - 'model': 'Caser', - 'loss_type': 'BPR', - 'MAX_ITEM_LIST_LENGTH': 10, - 'reproducibility': False, + "model": "Caser", + "loss_type": "BPR", + "MAX_ITEM_LIST_LENGTH": 10, + "reproducibility": False, } quick_test(config_dict) def test_nextitnet(self): config_dict = { - 'model': 'NextItNet', - 'reproducibility': False, - 'train_neg_sample_args': None + "model": "NextItNet", + "reproducibility": False, + "train_neg_sample_args": None, } quick_test(config_dict) def test_nextitnet_with_BPR_loss(self): config_dict = { - 'model': 'NextItNet', - 'loss_type': 'BPR', - 'reproducibility': False, + "model": "NextItNet", + "loss_type": "BPR", + "reproducibility": False, } quick_test(config_dict) def test_transrec(self): config_dict = { - 'model': 'TransRec', + "model": "TransRec", } quick_test(config_dict) def test_sasrec(self): - config_dict = { - 'model': 'SASRec', - 'train_neg_sample_args': None - } + config_dict = {"model": "SASRec", "train_neg_sample_args": None} quick_test(config_dict) def test_sasrec_with_BPR_loss_and_relu(self): - config_dict = { - 'model': 'SASRec', - 'loss_type': 'BPR', - 'hidden_act': 'relu' - } + config_dict = {"model": "SASRec", "loss_type": "BPR", "hidden_act": "relu"} quick_test(config_dict) def test_sasrec_with_BPR_loss_and_sigmoid(self): - config_dict = { - 'model': 'SASRec', - 'loss_type': 'BPR', - 'hidden_act': 'sigmoid' - } + config_dict = {"model": "SASRec", "loss_type": "BPR", "hidden_act": "sigmoid"} quick_test(config_dict) def test_srgnn(self): config_dict = { - 'model': 'SRGNN', - 'MAX_ITEM_LIST_LENGTH': 3, - 'train_neg_sample_args': None + "model": "SRGNN", + "MAX_ITEM_LIST_LENGTH": 3, + "train_neg_sample_args": None, } quick_test(config_dict) def test_srgnn_with_BPR_loss(self): config_dict = { - 'model': 'SRGNN', - 'loss_type': 'BPR', - 'MAX_ITEM_LIST_LENGTH': 3, + "model": "SRGNN", + "loss_type": "BPR", + "MAX_ITEM_LIST_LENGTH": 3, } quick_test(config_dict) def test_gcsan(self): config_dict = { - 'model': 'GCSAN', - 'MAX_ITEM_LIST_LENGTH': 3, - 'train_neg_sample_args': None + "model": "GCSAN", + "MAX_ITEM_LIST_LENGTH": 3, + "train_neg_sample_args": None, } quick_test(config_dict) def test_gcsan_with_BPR_loss_and_tanh(self): config_dict = { - 'model': 'GCSAN', - 'loss_type': 'BPR', - 'hidden_act': 'tanh', - 'MAX_ITEM_LIST_LENGTH': 3, + "model": "GCSAN", + "loss_type": "BPR", + "hidden_act": "tanh", + "MAX_ITEM_LIST_LENGTH": 3, } quick_test(config_dict) def test_gru4recf(self): - config_dict = { - 'model': 'GRU4RecF', - 'train_neg_sample_args': None - } + config_dict = {"model": "GRU4RecF", "train_neg_sample_args": None} quick_test(config_dict) def test_gru4recf_with_max_pooling(self): config_dict = { - 'model': 'GRU4RecF', - 'pooling_mode': 'max', - 'train_neg_sample_args': None + "model": "GRU4RecF", + "pooling_mode": "max", + "train_neg_sample_args": None, } quick_test(config_dict) def test_gru4recf_with_sum_pooling(self): config_dict = { - 'model': 'GRU4RecF', - 'pooling_mode': 'sum', - 'train_neg_sample_args': None + "model": "GRU4RecF", + "pooling_mode": "sum", + "train_neg_sample_args": None, } quick_test(config_dict) def test_sasrecf(self): - config_dict = { - 'model': 'SASRecF', - 'train_neg_sample_args': None - } + config_dict = {"model": "SASRecF", "train_neg_sample_args": None} quick_test(config_dict) def test_sasrecf_with_max_pooling(self): config_dict = { - 'model': 'SASRecF', - 'pooling_mode': 'max', - 'train_neg_sample_args': None + "model": "SASRecF", + "pooling_mode": "max", + "train_neg_sample_args": None, } quick_test(config_dict) def test_sasrecf_with_sum_pooling(self): config_dict = { - 'model': 'SASRecF', - 'pooling_mode': 'sum', - 'train_neg_sample_args': None + "model": "SASRecF", + "pooling_mode": "sum", + "train_neg_sample_args": None, } quick_test(config_dict) def test_hrm(self): - config_dict = { - 'model': 'HRM', - 'train_neg_sample_args': None - } + config_dict = {"model": "HRM", "train_neg_sample_args": None} quick_test(config_dict) def test_hrm_with_BPR_loss(self): config_dict = { - 'model': 'HRM', - 'loss_type': 'BPR', + "model": "HRM", + "loss_type": "BPR", } quick_test(config_dict) def test_npe(self): - config_dict = { - 'model': 'NPE', - 'train_neg_sample_args': None - } + config_dict = {"model": "NPE", "train_neg_sample_args": None} quick_test(config_dict) def test_npe_with_BPR_loss(self): config_dict = { - 'model': 'NPE', - 'loss_type': 'BPR', + "model": "NPE", + "loss_type": "BPR", } quick_test(config_dict) def test_shan(self): - config_dict = { - 'model': 'SHAN', - 'train_neg_sample_args': None - } + config_dict = {"model": "SHAN", "train_neg_sample_args": None} quick_test(config_dict) def test_shan_with_BPR_loss(self): config_dict = { - 'model': 'SHAN', - 'loss_type': 'BPR', + "model": "SHAN", + "loss_type": "BPR", } quick_test(config_dict) def test_hgn(self): - config_dict = { - 'model': 'HGN', - 'train_neg_sample_args': None - } + config_dict = {"model": "HGN", "train_neg_sample_args": None} quick_test(config_dict) def test_hgn_with_BPR_loss(self): config_dict = { - 'model': 'HGN', - 'loss_type': 'BPR', + "model": "HGN", + "loss_type": "BPR", } quick_test(config_dict) def test_fossil(self): - config_dict = { - 'model': 'FOSSIL', - 'train_neg_sample_args': None - } + config_dict = {"model": "FOSSIL", "train_neg_sample_args": None} quick_test(config_dict) def test_repeat_net(self): config_dict = { - 'model': 'RepeatNet', + "model": "RepeatNet", } quick_test(config_dict) def test_fdsa(self): - config_dict = { - 'model': 'FDSA', - 'train_neg_sample_args': None - } + config_dict = {"model": "FDSA", "train_neg_sample_args": None} quick_test(config_dict) def test_fdsa_with_max_pooling(self): config_dict = { - 'model': 'FDSA', - 'pooling_mode': 'max', - 'train_neg_sample_args': None + "model": "FDSA", + "pooling_mode": "max", + "train_neg_sample_args": None, } quick_test(config_dict) def test_fdsa_with_sum_pooling(self): config_dict = { - 'model': 'FDSA', - 'pooling_mode': 'sum', - 'train_neg_sample_args': None + "model": "FDSA", + "pooling_mode": "sum", + "train_neg_sample_args": None, } quick_test(config_dict) def test_bert4rec(self): - config_dict = { - 'model': 'BERT4Rec', - 'train_neg_sample_args': None - } + config_dict = {"model": "BERT4Rec", "train_neg_sample_args": None} quick_test(config_dict) def test_bert4rec_with_BPR_loss_and_swish(self): - config_dict = { - 'model': 'BERT4Rec', - 'loss_type': 'BPR', - 'hidden_act': 'swish' - } + config_dict = {"model": "BERT4Rec", "loss_type": "BPR", "hidden_act": "swish"} quick_test(config_dict) def test_lightsans(self): - config_dict = { - 'model': 'LightSANs', - 'train_neg_sample_args': None - } + config_dict = {"model": "LightSANs", "train_neg_sample_args": None} quick_test(config_dict) def test_lightsans_with_BPR_loss(self): config_dict = { - 'model': 'LightSANs', - 'loss_type': 'BPR', + "model": "LightSANs", + "loss_type": "BPR", } quick_test(config_dict) def test_sine(self): - config_dict = { - 'model': 'SINE', - 'train_neg_sample_args': None - } + config_dict = {"model": "SINE", "train_neg_sample_args": None} quick_test(config_dict) def test_sine_with_BPR_loss(self): config_dict = { - 'model': 'SINE', - 'loss_type': 'BPR', + "model": "SINE", + "loss_type": "BPR", } quick_test(config_dict) def test_sine_with_NLL_loss(self): config_dict = { - 'model': 'SINE', - 'train_neg_sample_args': None, - 'loss_type': 'NLL', + "model": "SINE", + "train_neg_sample_args": None, + "loss_type": "NLL", } quick_test(config_dict) def test_core_trm(self): config_dict = { - 'model': 'CORE', - 'train_neg_sample_args': None, - 'dnn_type': 'trm' + "model": "CORE", + "train_neg_sample_args": None, + "dnn_type": "trm", } quick_test(config_dict) def test_core_ave(self): config_dict = { - 'model': 'CORE', - 'train_neg_sample_args': None, - 'dnn_type': 'ave' + "model": "CORE", + "train_neg_sample_args": None, + "dnn_type": "ave", } quick_test(config_dict) @@ -799,122 +715,121 @@ def test_core_ave(self): class TestKnowledgeRecommender(unittest.TestCase): - def test_cke(self): config_dict = { - 'model': 'CKE', + "model": "CKE", } quick_test(config_dict) def test_cfkg(self): config_dict = { - 'model': 'CFKG', + "model": "CFKG", } quick_test(config_dict) def test_cfkg_with_transe(self): config_dict = { - 'model': 'CFKG', - 'loss_function': 'transe', + "model": "CFKG", + "loss_function": "transe", } quick_test(config_dict) def test_ktup(self): config_dict = { - 'model': 'KTUP', - 'train_rec_step': 1, - 'train_kg_step': 1, - 'epochs': 2, + "model": "KTUP", + "train_rec_step": 1, + "train_kg_step": 1, + "epochs": 2, } quick_test(config_dict) def test_ktup_with_L1_flag(self): config_dict = { - 'model': 'KTUP', - 'use_st_gumbel': False, - 'L1_flag': True, + "model": "KTUP", + "use_st_gumbel": False, + "L1_flag": True, } quick_test(config_dict) def test_kgat(self): config_dict = { - 'model': 'KGAT', + "model": "KGAT", } quick_test(config_dict) def test_kgat_with_gcn(self): config_dict = { - 'model': 'KGAT', - 'aggregator_type': 'gcn', + "model": "KGAT", + "aggregator_type": "gcn", } quick_test(config_dict) def test_kgat_with_graphsage(self): config_dict = { - 'model': 'KGAT', - 'aggregator_type': 'graphsage', + "model": "KGAT", + "aggregator_type": "graphsage", } quick_test(config_dict) def test_ripplenet(self): config_dict = { - 'model': 'RippleNet', + "model": "RippleNet", } quick_test(config_dict) def test_mkr(self): config_dict = { - 'model': 'MKR', + "model": "MKR", } quick_test(config_dict) def test_mkr_without_use_inner_product(self): config_dict = { - 'model': 'MKR', - 'use_inner_product': False, + "model": "MKR", + "use_inner_product": False, } quick_test(config_dict) def test_kgcn(self): config_dict = { - 'model': 'KGCN', + "model": "KGCN", } quick_test(config_dict) def test_kgcn_with_neighbor(self): config_dict = { - 'model': 'KGCN', - 'aggregator': 'neighbor', + "model": "KGCN", + "aggregator": "neighbor", } quick_test(config_dict) def test_kgcn_with_concat(self): config_dict = { - 'model': 'KGCN', - 'aggregator': 'concat', + "model": "KGCN", + "aggregator": "concat", } quick_test(config_dict) def test_kgnnls(self): config_dict = { - 'model': 'KGNNLS', + "model": "KGNNLS", } quick_test(config_dict) def test_kgnnls_with_neighbor(self): config_dict = { - 'model': 'KGNNLS', - 'aggregator': 'neighbor', + "model": "KGNNLS", + "aggregator": "neighbor", } quick_test(config_dict) def test_kgnnls_with_concat(self): config_dict = { - 'model': 'KGNNLS', - 'aggregator': 'concat', + "model": "KGNNLS", + "aggregator": "concat", } quick_test(config_dict) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() diff --git a/tests/model/test_model_manual.py b/tests/model/test_model_manual.py index 4ace05064..e1606e2a2 100644 --- a/tests/model/test_model_manual.py +++ b/tests/model/test_model_manual.py @@ -10,11 +10,13 @@ from recbole.quick_start import objective_function current_path = os.path.dirname(os.path.realpath(__file__)) -config_file_list = [os.path.join(current_path, 'test_model.yaml')] +config_file_list = [os.path.join(current_path, "test_model.yaml")] def quick_test(config_dict): - objective_function(config_dict=config_dict, config_file_list=config_file_list, saved=False) + objective_function( + config_dict=config_dict, config_file_list=config_file_list, saved=False + ) class TestSequentialRecommender(unittest.TestCase): @@ -27,21 +29,21 @@ class TestSequentialRecommender(unittest.TestCase): def test_s3rec(self): config_dict = { - 'model': 'S3Rec', - 'train_stage': 'pretrain', - 'save_step': 1, - 'train_neg_sample_args': None + "model": "S3Rec", + "train_stage": "pretrain", + "save_step": 1, + "train_neg_sample_args": None, } quick_test(config_dict) config_dict = { - 'model': 'S3Rec', - 'train_stage': 'finetune', - 'pre_model_path': './saved/S3Rec-test-1.pth', - 'train_neg_sample_args': None + "model": "S3Rec", + "train_stage": "finetune", + "pre_model_path": "./saved/S3Rec-test-1.pth", + "train_neg_sample_args": None, } quick_test(config_dict) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main() From 1c604bff16d2ea23d852d31c4ee50bb4d77bdb00 Mon Sep 17 00:00:00 2001 From: Sherry-XLL Date: Thu, 14 Jul 2022 10:46:27 +0000 Subject: [PATCH 3/4] Format Python code according to PEP8 --- recbole/quick_start/quick_start.py | 15 +++++++++++---- recbole/utils/utils.py | 26 +++++++++++++++----------- 2 files changed, 26 insertions(+), 15 deletions(-) diff --git a/recbole/quick_start/quick_start.py b/recbole/quick_start/quick_start.py index 9c9111b21..487391f26 100644 --- a/recbole/quick_start/quick_start.py +++ b/recbole/quick_start/quick_start.py @@ -26,7 +26,14 @@ save_split_dataloaders, load_split_dataloaders, ) -from recbole.utils import init_logger, get_model, get_trainer, init_seed, set_color, get_flops +from recbole.utils import ( + init_logger, + get_model, + get_trainer, + init_seed, + set_color, + get_flops, +) def run_recbole( @@ -67,9 +74,9 @@ def run_recbole( init_seed(config["seed"] + config["local_rank"], config["reproducibility"]) model = get_model(config["model"])(config, train_data._dataset).to(config["device"]) logger.info(model) - flops = get_flops(model,dataset,config['device']) - logger.info(set_color('FLOPs', 'blue') + f': {flops}') - + flops = get_flops(model, dataset, config["device"]) + logger.info(set_color("FLOPs", "blue") + f": {flops}") + # trainer loading and initialization trainer = get_trainer(config["MODEL_TYPE"], config["model"])(config, model) diff --git a/recbole/utils/utils.py b/recbole/utils/utils.py index 7b99c30f4..66241eb9e 100644 --- a/recbole/utils/utils.py +++ b/recbole/utils/utils.py @@ -241,9 +241,12 @@ def get_gpu_usage(device=None): reserved = torch.cuda.max_memory_reserved(device) / 1024**3 total = torch.cuda.get_device_properties(device).total_memory / 1024**3 - return '{:.2f} G/{:.2f} G'.format(reserved, total) + return "{:.2f} G/{:.2f} G".format(reserved, total) -def get_flops(model, dataset, device, uncalled_warnings=False, unsupported_warnings=False): + +def get_flops( + model, dataset, device, uncalled_warnings=False, unsupported_warnings=False +): r"""Given a model and dataset to the model, compute the per-operator flops of the given model. Args: @@ -271,9 +274,9 @@ def get_shape(val): else: return None - def binary_ops_jit(inputs,outputs): + def binary_ops_jit(inputs, outputs): r""" - Count flops for binary operator such as addition, subtraction, multiplication + Count flops for binary operator such as addition, subtraction, multiplication and division. """ input_shapes = [get_shape(v) for v in inputs] @@ -281,22 +284,22 @@ def binary_ops_jit(inputs,outputs): flop = np.prod(input_shapes[0]) return flop - def sum_ops_jit(inputs,outputs): + def sum_ops_jit(inputs, outputs): r""" Count flops for sum. """ input_shapes = [get_shape(v) for v in inputs] assert input_shapes[0] - flop = np.prod(input_shapes[0])-1 + flop = np.prod(input_shapes[0]) - 1 return flop - def sigmoid_ops_jit(inputs,outputs): + def sigmoid_ops_jit(inputs, outputs): r""" Count flops for sigmoid. """ input_shapes = [get_shape(v) for v in inputs] assert input_shapes[0] - flop = np.prod(input_shapes)*4 + flop = np.prod(input_shapes) * 4 return flop custom_ops = { @@ -310,16 +313,17 @@ def sigmoid_ops_jit(inputs,outputs): inter = dataset[torch.tensor([1])].to(device) class TracingAdapter(torch.nn.Module): - def __init__(self, rec_model): super().__init__() self.model = rec_model - def forward(self,interaction): + def forward(self, interaction): return self.model.predict(interaction) wrapper = TracingAdapter(model) - flop_counter = FlopCountAnalysis(wrapper, (inter.interaction,)).set_op_handle(**custom_ops) + flop_counter = FlopCountAnalysis(wrapper, (inter.interaction,)).set_op_handle( + **custom_ops + ) flop_counter.unsupported_ops_warnings(unsupported_warnings) flop_counter.uncalled_modules_warnings(uncalled_warnings) flop_counter.tracer_warnings("none") From 2fbf64824257c2e06846bc6f5d5bad0756cc82fa Mon Sep 17 00:00:00 2001 From: Lanling Xu Date: Thu, 14 Jul 2022 19:02:51 +0800 Subject: [PATCH 4/4] Remove useless url --- .github/workflows/python-package.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 207fafeb2..923a5f314 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -69,7 +69,6 @@ jobs: # Use black to test code format # Reference code: # https://black.readthedocs.io/en/stable/integrations/github_actions.html - # https://github.com/marketplace/actions/run-black-formatter lint: runs-on: ubuntu-latest steps: