From 0a78a34f1a147db69ac779cd5e184e95e2f0d6e9 Mon Sep 17 00:00:00 2001 From: qyzl7 Date: Sat, 9 Aug 2025 14:34:47 +0800 Subject: [PATCH 1/2] ci: Add Ruff workflow for code linting and configuration - Add a GitHub Actions workflow to automatically run Ruff checks on every push and pull request. - Create a `pyproject.toml` file to define project metadata, dependencies, and Ruff's linting rules. - Configure the Ruff rule set (including pycodestyle, flake8, etc.) and ignore specific checks like line length and function complexity. This change ensures that all future code submissions will trigger automated style checks. Violations will cause the CI pipeline to fail, requiring code to be fixed to meet the defined standards. --- .github/workflows/ruff.yml | 18 ++++++++++++++++++ pyproject.toml | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) create mode 100644 .github/workflows/ruff.yml create mode 100644 pyproject.toml diff --git a/.github/workflows/ruff.yml b/.github/workflows/ruff.yml new file mode 100644 index 0000000..b73ce97 --- /dev/null +++ b/.github/workflows/ruff.yml @@ -0,0 +1,18 @@ +name: Ruff +on: [push, pull_request] + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Install Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install ruff + - name: Run Ruff + run: ruff check --output-format=github . \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..3aaefcd --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,37 @@ +[project] +name = "coldrec" +version = "0.1.0" +description = "" +readme = "README.md" +requires-python = ">=3.10" +dependencies = [ + "torch >= 1.11.0", + "faiss-gpu", + "pandas >= 2.0.3", + "numpy < 2.0", + "scikit-learn >= 1.3.2", + "optuna >= 3.6.1", + "rich>=14.1.0", +] + +[tool.ruff] +target-version = "py310" +line-length = 120 + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "C", # flake8-comprehensions + "B", # flake8-bugbear + "UP", # pyupgrade +] +ignore = [ + "E501", # line too long + "B008", # do not perform function calls in argument defaults + "C901", # too complex + "W191", # indentation contains tabs + "UP008", # Use `super()` instead of `super(__class__, self)` +] From 55ef9694bb0067d4128994e4e39c012791f80e74 Mon Sep 17 00:00:00 2001 From: qyzl7 Date: Sat, 9 Aug 2025 15:04:31 +0800 Subject: [PATCH 2/2] style: Format code in `model.py` and `parma_search.py` --- main.py | 184 ++++++++++++++++++++++++++++-------------------- param_search.py | 14 ++-- 2 files changed, 115 insertions(+), 83 deletions(-) diff --git a/main.py b/main.py index c7c3419..2f69fd1 100644 --- a/main.py +++ b/main.py @@ -1,68 +1,89 @@ import argparse -import torch -import numpy as np import pickle -from util.loader import DataLoader -from util.utils import set_seed + +import numpy as np +import torch + from config.model_param import model_specific_param from model import AVAILABLE_MODELS from util.databuilder import ColdStartDataBuilder +from util.loader import DataLoader +from util.utils import set_seed + class Config: """ Configuration class that encapsulates all model and training parameters. - + This class centralizes all configuration data, making it easier to pass to model constructors and maintain consistency across the codebase. """ - + def __init__(self, args: argparse.Namespace): self.args = args - self.device = torch.device("cuda:%d" % (args.gpu_id) if (torch.cuda.is_available() and args.use_gpu) else "cpu") - + self.device = torch.device(f"cuda:{args.gpu_id}") if (args.use_gpu and torch.cuda.is_available()) else torch.device("cpu") + # Load data - training_data = DataLoader.load_data_set(f'./data/{args.dataset}/cold_{args.cold_object}/warm_train.csv') - all_valid_data = DataLoader.load_data_set(f'./data/{args.dataset}/cold_{args.cold_object}/overall_val.csv') - warm_valid_data = DataLoader.load_data_set(f'./data/{args.dataset}/cold_{args.cold_object}/warm_val.csv') - cold_valid_data = DataLoader.load_data_set(f'./data/{args.dataset}/cold_{args.cold_object}/cold_{args.cold_object}_val.csv') - all_test_data = DataLoader.load_data_set(f'./data/{args.dataset}/cold_{args.cold_object}/overall_test.csv') - warm_test_data = DataLoader.load_data_set(f'./data/{args.dataset}/cold_{args.cold_object}/warm_test.csv') - cold_test_data = DataLoader.load_data_set(f'./data/{args.dataset}/cold_{args.cold_object}/cold_{args.cold_object}_test.csv') + training_data = DataLoader.load_data_set(f"./data/{args.dataset}/cold_{args.cold_object}/warm_train.csv") + all_valid_data = DataLoader.load_data_set(f"./data/{args.dataset}/cold_{args.cold_object}/overall_val.csv") + warm_valid_data = DataLoader.load_data_set(f"./data/{args.dataset}/cold_{args.cold_object}/warm_val.csv") + cold_valid_data = DataLoader.load_data_set( + f"./data/{args.dataset}/cold_{args.cold_object}/cold_{args.cold_object}_val.csv" + ) + all_test_data = DataLoader.load_data_set(f"./data/{args.dataset}/cold_{args.cold_object}/overall_test.csv") + warm_test_data = DataLoader.load_data_set(f"./data/{args.dataset}/cold_{args.cold_object}/warm_test.csv") + cold_test_data = DataLoader.load_data_set( + f"./data/{args.dataset}/cold_{args.cold_object}/cold_{args.cold_object}_test.csv" + ) # Dataset information - data_info_dict = pickle.load(open(f'./data/{args.dataset}/cold_{args.cold_object}/info_dict.pkl', 'rb')) - user_num = data_info_dict['user_num'] - item_num = data_info_dict['item_num'] - warm_user_idx = data_info_dict['warm_user'] - warm_item_idx = data_info_dict['warm_item'] - cold_user_idx = data_info_dict['cold_user'] - cold_item_idx = data_info_dict['cold_item'] + data_info_dict = pickle.load(open(f"./data/{args.dataset}/cold_{args.cold_object}/info_dict.pkl", "rb")) + user_num = data_info_dict["user_num"] + item_num = data_info_dict["item_num"] + warm_user_idx = data_info_dict["warm_user"] + warm_item_idx = data_info_dict["warm_item"] + cold_user_idx = data_info_dict["cold_user"] + cold_item_idx = data_info_dict["cold_item"] print(f"Dataset: {args.dataset}, User num: {user_num}, Item num: {item_num}.") # Content obtaining user_content, item_content = None, None - if args.cold_object == 'user': - user_content = np.load(f'./data/{args.dataset}/{args.dataset}_{args.cold_object}_content.npy') - print(f'user content shape: {user_content.shape}') - if args.cold_object == 'item': - item_content = np.load(f'./data/{args.dataset}/{args.dataset}_{args.cold_object}_content.npy') - print(f'item content shape: {item_content.shape}') - - self.data = ColdStartDataBuilder(training_data, warm_valid_data, cold_valid_data, all_valid_data, - warm_test_data, cold_test_data, all_test_data, user_num, item_num, - warm_user_idx, warm_item_idx, cold_user_idx, cold_item_idx, - user_content, item_content) + if args.cold_object == "user": + user_content = np.load(f"./data/{args.dataset}/{args.dataset}_{args.cold_object}_content.npy") + print(f"user content shape: {user_content.shape}") + if args.cold_object == "item": + item_content = np.load(f"./data/{args.dataset}/{args.dataset}_{args.cold_object}_content.npy") + print(f"item content shape: {item_content.shape}") + + self.data = ColdStartDataBuilder( + training_data, + warm_valid_data, + cold_valid_data, + all_valid_data, + warm_test_data, + cold_test_data, + all_test_data, + user_num, + item_num, + warm_user_idx, + warm_item_idx, + cold_user_idx, + cold_item_idx, + user_content, + item_content, + ) + def model_factory(config: Config): """ Factory function to create model instances based on configuration. - + Args: config: Configuration object containing all necessary parameters - + Returns: Model instance implementing BaseColdStartTrainer - + Raises: ValueError: If the model name is not in the available models list """ @@ -70,8 +91,7 @@ def model_factory(config: Config): model_class = AVAILABLE_MODELS.get(model_name) if model_class is None: - raise ValueError(f"Invalid model name: {model_name}. " - f"Available models: {list(AVAILABLE_MODELS.keys())}") + raise ValueError(f"Invalid model name: {model_name}. Available models: {list(AVAILABLE_MODELS.keys())}") return model_class(config) @@ -79,42 +99,47 @@ def model_factory(config: Config): def parse_args() -> argparse.Namespace: """ Parse command line arguments and return a namespace object. - + Returns: Parsed arguments namespace """ parser = argparse.ArgumentParser() - parser.add_argument('--dataset', default='citeulike') - parser.add_argument('--model', default='MF') - parser.add_argument('--epochs', type=int, default=500) - parser.add_argument('--layers', type=int, default=2) - parser.add_argument('--topN', default='10,20') - parser.add_argument('--bs', type=int, default=2048, help='training batch size') - parser.add_argument('--emb_size', type=int, default=64) - parser.add_argument('--lr', type=float, default=0.001) - parser.add_argument('--reg', type=float, default=0.0001) - parser.add_argument('--runs', type=int, default=1, help='model runs') - parser.add_argument('--seed', type=int, default=2024) - parser.add_argument('--use_gpu', default=True, help='Whether to use CUDA') - parser.add_argument('--save_emb', default=True, help='Whether to save the user/item embeddings') - parser.add_argument('--gpu_id', type=int, default=0, help='CUDA id') - parser.add_argument('--cold_object', default='item', type=str, choices=['user', 'item']) - parser.add_argument('--backbone', default='MF') - parser.add_argument('--early_stop', type=int, default=10, help='Early stopping patience. If set to 0, early stopping is disabled.') + parser.add_argument("--dataset", default="citeulike") + parser.add_argument("--model", default="MF") + parser.add_argument("--epochs", type=int, default=500) + parser.add_argument("--layers", type=int, default=2) + parser.add_argument("--topN", default="10,20") + parser.add_argument("--bs", type=int, default=2048, help="training batch size") + parser.add_argument("--emb_size", type=int, default=64) + parser.add_argument("--lr", type=float, default=0.001) + parser.add_argument("--reg", type=float, default=0.0001) + parser.add_argument("--runs", type=int, default=1, help="model runs") + parser.add_argument("--seed", type=int, default=2024) + parser.add_argument("--use_gpu", default=True, help="Whether to use CUDA") + parser.add_argument("--save_emb", default=True, help="Whether to save the user/item embeddings") + parser.add_argument("--gpu_id", type=int, default=0, help="CUDA id") + parser.add_argument("--cold_object", default="item", type=str, choices=["user", "item"]) + parser.add_argument("--backbone", default="MF") + parser.add_argument( + "--early_stop", type=int, default=10, help="Early stopping patience. If set to 0, early stopping is disabled." + ) args, _ = parser.parse_known_args() parser = model_specific_param(args.model, parser, AVAILABLE_MODELS) return parser.parse_args() -if __name__ == '__main__': +if __name__ == "__main__": args = parse_args() print(args) - + config = Config(args) - top_Ns = args.topN.split(',') - results = {setting: {metric: [[] for _ in top_Ns] for metric in ['hit', 'precision', 'recall', 'ndcg']} for setting in ['all', 'cold', 'warm']} - + top_Ns = args.topN.split(",") + results = { + setting: {metric: [[] for _ in top_Ns] for metric in ["hit", "precision", "recall", "ndcg"]} + for setting in ["all", "cold", "warm"] + } + time_results = [] for round_num in range(args.runs): @@ -122,7 +147,7 @@ def parse_args() -> argparse.Namespace: seed = args.seed if args.runs == 1 else round_num set_seed(seed, args.use_gpu) - + model = model_factory(config) print(f"Registered model: {args.model}.") @@ -130,28 +155,35 @@ def parse_args() -> argparse.Namespace: # Results recording for i in range(len(top_Ns)): - for setting, test_results in [('all', model.overall_test_results), ('cold', model.cold_test_results), ('warm', model.warm_test_results)]: - results[setting]['hit'][i].append(test_results[i][0]) - results[setting]['precision'][i].append(test_results[i][1]) - results[setting]['recall'][i].append(test_results[i][2]) - results[setting]['ndcg'][i].append(test_results[i][3]) + for setting, test_results in [ + ("all", model.overall_test_results), + ("cold", model.cold_test_results), + ("warm", model.warm_test_results), + ]: + results[setting]["hit"][i].append(test_results[i][0]) + results[setting]["precision"][i].append(test_results[i][1]) + results[setting]["recall"][i].append(test_results[i][2]) + results[setting]["ndcg"][i].append(test_results[i][3]) time_results.append((model.train_end_time - model.train_start_time) / args.epochs) for i, top_n in enumerate(top_Ns): print("*" * 80) - for setting_name, setting_key in [('Overall', 'all'), ('Cold-Start', 'cold'), ('Warm-Start', 'warm')]: + for setting_name, setting_key in [("Overall", "all"), ("Cold-Start", "cold"), ("Warm-Start", "warm")]: print(f"Top-{top_n} {setting_name} Test Performance:") - + metrics = { - 'Hit': (np.mean(results[setting_key]['hit'][i]), np.std(results[setting_key]['hit'][i])), - 'Precision': (np.mean(results[setting_key]['precision'][i]), np.std(results[setting_key]['precision'][i])), - 'Recall': (np.mean(results[setting_key]['recall'][i]), np.std(results[setting_key]['recall'][i])), - 'NDCG': (np.mean(results[setting_key]['ndcg'][i]), np.std(results[setting_key]['ndcg'][i])) + "Hit": (np.mean(results[setting_key]["hit"][i]), np.std(results[setting_key]["hit"][i])), + "Precision": ( + np.mean(results[setting_key]["precision"][i]), + np.std(results[setting_key]["precision"][i]), + ), + "Recall": (np.mean(results[setting_key]["recall"][i]), np.std(results[setting_key]["recall"][i])), + "NDCG": (np.mean(results[setting_key]["ndcg"][i]), np.std(results[setting_key]["ndcg"][i])), } - - print(', '.join([f"{name}@{top_n}: {mean:.4f}±{std:.4f}" for name, (mean, std) in metrics.items()])) - print(f"Efficiency Performance:") + print(", ".join([f"{name}@{top_n}: {mean:.4f}±{std:.4f}" for name, (mean, std) in metrics.items()])) + + print("Efficiency Performance:") mean_time, std_time = np.mean(time_results), np.std(time_results) print(f"Time: {mean_time:.4f}±{std_time:.4f} seconds per epoch.") diff --git a/param_search.py b/param_search.py index 11ba797..3f2fc17 100644 --- a/param_search.py +++ b/param_search.py @@ -1,13 +1,12 @@ import argparse -import torch -import numpy as np -import random import pickle -import os + +import numpy as np import optuna -from util.loader import DataLoader +import torch + from config.model_param import model_specific_param -from model_imports import * +from util.loader import DataLoader def objective(trial): @@ -114,7 +113,8 @@ def objective(trial): args = parser.parse_args() print(args) - device = torch.device("cuda:%d" % (args.gpu_id) if (torch.cuda.is_available() and args.use_gpu) else "cpu") + device = torch.device(f"cuda:{args.gpu_id}") if (args.use_gpu and torch.cuda.is_available()) else torch.device("cpu") + # data loader training_data = DataLoader.load_data_set(f'./data/{args.dataset}/cold_{args.cold_object}/warm_train.csv') # following the widely used setting in previous works, the 'all' set is used for validation.