Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,8 @@ The table below lists the recommendation models/algorithms featured in Cornac. E
| :--: | --------------- | :--: | :---------: | :-----: |
| 2024 | [Comparative Aspects and Opinions Ranking for Recommendation Explanations (Companion)](cornac/models/companion), [docs](https://cornac.readthedocs.io/en/stable/api_ref/models.html#module-cornac.models.companion.recom_companion), [paper](https://lthoang.com/assets/publications/mlj24.pdf) | Hybrid / Sentiment / Explainable | CPU | [quick-start](examples/companion_example.py)
| | [Hypergraphs with Attention on Reviews (HypAR)](cornac/models/hypar), [docs](https://cornac.readthedocs.io/en/stable/api_ref/models.html#module-cornac.models.hypar.recom_hypar), [paper](https://doi.org/10.1007/978-3-031-56027-9_14)| Hybrid / Sentiment / Explainable | [requirements](cornac/models/hypar/requirements_cu116.txt), CPU / GPU | [quick-start](https://github.com/PreferredAI/HypAR)
| 2023 | [Scalable Approximate NonSymmetric Autoencoder (SANSA)](cornac/models/sansa), [docs](https://cornac.readthedocs.io/en/stable/api_ref/models.html#module-cornac.models.sansa.recom_sansa), [paper](https://dl.acm.org/doi/10.1145/3604915.3608827) | Collaborative Filtering | [requirements](cornac/models/sansa/requirements.txt), CPU | [quick-start](examples/sansa_movielens.py), [150k-items](examples/sansa_tradesy.py)
| 2023 | [Recommender Systems with Generative Retrieval (TIGER)](cornac/models/tiger), [docs](https://cornac.readthedocs.io/en/stable/api_ref/models.html#module-cornac.models.tiger.recom_tiger), [paper](https://arxiv.org/pdf/2305.05065.pdf) | Next-Item / Content-Based | [requirements](cornac/models/tiger/requirements.txt), CPU / GPU | [quick-start](examples/tiger_example.py)
| | [Scalable Approximate NonSymmetric Autoencoder (SANSA)](cornac/models/sansa), [docs](https://cornac.readthedocs.io/en/stable/api_ref/models.html#module-cornac.models.sansa.recom_sansa), [paper](https://dl.acm.org/doi/10.1145/3604915.3608827) | Collaborative Filtering | [requirements](cornac/models/sansa/requirements.txt), CPU | [quick-start](examples/sansa_movielens.py), [150k-items](examples/sansa_tradesy.py)
| 2022 | [Disentangled Multimodal Representation Learning for Recommendation (DMRL)](cornac/models/dmrl), [docs](https://cornac.readthedocs.io/en/stable/api_ref/models.html#module-cornac.models.dmrl.recom_dmrl), [paper](https://arxiv.org/pdf/2203.05406.pdf) | Content-Based / Text & Image | [requirements](cornac/models/dmrl/requirements.txt), CPU / GPU | [quick-start](examples/dmrl_example.py)
| 2021 | [Bilateral Variational Autoencoder for Collaborative Filtering (BiVAECF)](cornac/models/bivaecf), [docs](https://cornac.readthedocs.io/en/stable/api_ref/models.html#module-cornac.models.bivaecf.recom_bivaecf), [paper](https://dl.acm.org/doi/pdf/10.1145/3437963.3441759) | Collaborative Filtering / Content-Based | [requirements](cornac/models/bivaecf/requirements.txt), CPU / GPU | [quick-start](https://github.com/PreferredAI/bi-vae), [deep-dive](https://github.com/recommenders-team/recommenders/blob/main/examples/02_model_collaborative_filtering/cornac_bivae_deep_dive.ipynb)
| | [Transformers4Rec-style Unified Transformer Recommender (TransformerRec)](cornac/models/transformer_rec), [docs](https://cornac.readthedocs.io/en/stable/api_ref/models.html#module-cornac.models.transformer_rec.recom_transformer_rec), [paper](https://dl.acm.org/doi/10.1145/3460231.3474255) | Next-Item | [requirements](cornac/models/transformer_rec/requirements.txt), CPU / GPU | [quick-start](examples/transformer_rec_diginetica.py)
Expand Down
9 changes: 8 additions & 1 deletion cornac/datasets/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,10 +296,17 @@ Each user's reviews form one chronologically-ordered sequence. Interactions are
| Amazon Sports (`sports`) | 35,598 | 18,357 | 296,337 | INT [1,5] |
| Amazon Toys (`toys`) | 19,412 | 11,924 | 167,597 | INT [1,5] |

Item content text (title, price, brand, categories -- the features embedded with Sentence-T5 in the TIGER paper) is available via `amazon_review.load_text(category=...)`, built from the public product metadata and covering exactly the 5-core items. Passing `include_description=True` appends each item's product description to its text (cached separately); Paischer et al. (arXiv:2412.08604) found this beneficial for the Toys dataset, while attribute-only text works better for Beauty/Sports.

```Python
from cornac.data import FeatureModality
from cornac.datasets import amazon_review
from cornac.eval_methods import NextItemEvaluation

data = amazon_review.load_feedback(category="beauty") # UIRT tuples, chronological per user
eval_method = NextItemEvaluation.leave_last_out(data, fmt="UIRT")
texts, item_ids = amazon_review.load_text(category="beauty") # item content text, aligned ids
features = ... # embed texts, e.g. SentenceTransformer("sentence-t5-base").encode(texts)
eval_method = NextItemEvaluation.leave_last_out(
data, fmt="UIRT", item_feature=FeatureModality(features=features, ids=item_ids)
)
```
149 changes: 136 additions & 13 deletions cornac/datasets/amazon_review.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
Source: https://cseweb.ucsd.edu/~jmcauley/datasets/amazon/links.html
"""

import ast
import csv
import gzip
import json
import os
Expand Down Expand Up @@ -65,6 +67,138 @@ def _preprocess(gz_path: str, csv_path: str) -> None:
f.write(f"{user},{item},{rating},{timestamp}\n")


def _validate(category: str, version: str) -> None:
if category not in _CATEGORY_FILES:
raise ValueError(f"category='{category}' not supported; " f"choose one of {sorted(_CATEGORY_FILES)}")
if version != "2014":
raise ValueError(f"version='{version}' not supported; only '2014' (McAuley 5-core) " "is available")


def _reviews_csv(category: str, version: str) -> str:
stem = _CATEGORY_FILES[category]
gz_path = cache(
url=f"{_BASE_URL}/reviews_{stem}_5.json.gz",
relative_path=f"amazon_review/{category}_{version}.json.gz",
)
csv_path = f"{gz_path[:-len('.json.gz')]}.csv"
if not os.path.exists(csv_path):
_preprocess(gz_path, csv_path)
return csv_path


def _item_text(meta: dict, include_description: bool = False) -> str:
"""Flatten item metadata into one text string (title, price, brand,
categories -- the content features used by TIGER).

When ``include_description`` is set, the item's ``description`` field (if
present and non-empty) is appended as a last ``Description: ...`` part.
"""
parts = []
title = meta.get("title")
if title:
parts.append(f"Title: {title}")
price = meta.get("price")
if price is not None:
parts.append(f"Price: {price}")
brand = meta.get("brand")
if brand:
parts.append(f"Brand: {brand}")
categories = meta.get("categories")
if categories:
flat = []
for path in categories:
for cat in path:
if cat not in flat:
flat.append(cat)
parts.append("Categories: " + ", ".join(flat))
if include_description:
description = meta.get("description")
if description:
parts.append(f"Description: {description}")
return ". ".join(" ".join(part.split()) for part in parts)


def _preprocess_meta(meta_gz_path: str, reviews_csv_path: str, out_path: str, include_description: bool = False) -> None:
"""Extract one text string per 5-core item from the raw category metadata.

The 2014 metadata files contain Python dict literals (not valid JSON),
hence ``ast.literal_eval``. Items without a metadata entry get an empty
string so the output covers every item in the reviews file.
"""
keep = {} # item id -> insertion order (dict preserves order)
with open(reviews_csv_path) as f:
for line in f:
item = line.split(",")[1]
if item not in keep:
keep[item] = len(keep)

texts = {}
with gzip.open(meta_gz_path, "rt", encoding="utf-8") as f:
for line in f:
meta = ast.literal_eval(line)
asin = meta.get("asin")
if asin in keep:
texts[asin] = _item_text(meta, include_description)

with open(out_path, "w", newline="") as f:
writer = csv.writer(f)
for item in keep:
writer.writerow([item, texts.get(item, "")])


def load_text(category: str, version: str = "2014", include_description: bool = False) -> (List, List):
"""Load item content text (title, price, brand, categories) per item.

Texts are built from the public product metadata of the same corpus as
:func:`load_feedback` and cover exactly the items appearing in the 5-core
reviews (items without a metadata entry get an empty string). These are
the content features embedded with Sentence-T5 in the TIGER paper.

Parameters
----------
category: str, required
One of ``'beauty'``, ``'sports'``, ``'toys'``.

version: str, default: '2014'
Dataset version. Only ``'2014'`` is currently supported.

include_description: bool, default: False
If True, append each item's ``description`` field as a last
``Description: ...`` part of its text. Paischer et al.
(arXiv:2412.08604) found this beneficial for the Toys dataset, while
attribute-only text works better for Beauty/Sports. Descriptions are
kept in full (downstream sentence encoders truncate as needed). The
description variant is cached separately (``*_text_desc.csv``) so it
never overwrites the attribute-only cache.

Returns
-------
texts: List
List of text documents, one per item.

ids: List
List of item ids aligned with indices in `texts`.
"""
_validate(category, version)
reviews_csv_path = _reviews_csv(category, version)
suffix = "_text_desc" if include_description else "_text"
text_path = f"{reviews_csv_path[:-len('.csv')]}{suffix}.csv"
if not os.path.exists(text_path):
stem = _CATEGORY_FILES[category]
meta_gz_path = cache(
url=f"{_BASE_URL}/meta_{stem}.json.gz",
relative_path=f"amazon_review/meta_{category}_{version}.json.gz",
)
_preprocess_meta(meta_gz_path, reviews_csv_path, text_path, include_description)

texts, ids = [], []
with open(text_path, newline="") as f:
for item, text in csv.reader(f):
ids.append(item)
texts.append(text)
return texts, ids


def load_feedback(category: str, version: str = "2014", fmt: str = "UIRT", reader: Reader = None) -> List:
"""Load the user-item review feedback, chronologically ordered per user.

Expand All @@ -90,19 +224,8 @@ def load_feedback(category: str, version: str = "2014", fmt: str = "UIRT", reade
data: array-like
Data in the form of a list of tuples (user, item, rating, timestamp).
"""
if category not in _CATEGORY_FILES:
raise ValueError(f"category='{category}' not supported; " f"choose one of {sorted(_CATEGORY_FILES)}")
if version != "2014":
raise ValueError(f"version='{version}' not supported; only '2014' (McAuley 5-core) " "is available")

stem = _CATEGORY_FILES[category]
gz_path = cache(
url=f"{_BASE_URL}/reviews_{stem}_5.json.gz",
relative_path=f"amazon_review/{category}_{version}.json.gz",
)
csv_path = f"{gz_path[:-len('.json.gz')]}.csv"
if not os.path.exists(csv_path):
_preprocess(gz_path, csv_path)
_validate(category, version)
csv_path = _reviews_csv(category, version)

reader = Reader() if reader is None else reader
return reader.read(csv_path, fmt=fmt, sep=",")
1 change: 1 addition & 0 deletions cornac/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
from .spop import SPop
from .svd import SVD
from .tifuknn import TIFUKNN
from .tiger import TIGER
from .transformer_rec import TransformerRec
from .trirank import TriRank
from .upcf import UPCF
Expand Down
18 changes: 18 additions & 0 deletions cornac/models/tiger/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Copyright 2026 The Cornac Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ============================================================================

from .grid_config import GRID_CONFIG
from .paischer_config import PAISCHER_CONFIG
from .recom_tiger import TIGER
52 changes: 52 additions & 0 deletions cornac/models/tiger/grid_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Copyright 2026 The Cornac Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ============================================================================
"""GRID handbook recipe for TIGER (Ju et al., 2025, arXiv 2507.22224).

Trades some accuracy for much faster training: the residual k-means tokenizer
needs no gradient training at all (GRID Table 1 finds it *outperforms* RQ-VAE
anyway), and the seq2seq stage runs a short epoch budget with best-on-val
checkpoint selection instead of a long fixed schedule.

Usage::

from cornac.models.tiger import GRID_CONFIG, TIGER

model = TIGER(**{**GRID_CONFIG, "seed": 123})

Verbatim from the GRID paper: optimizer settings (Adam lr 5e-4, weight decay
1e-6, batch 256, constant lr), NDCG@10 validation, rkmeans tokenizer with the
paper's (3 levels, 256 codes) shape and the paper's 4+4-layer transformer
(both cornac defaults, so not repeated here). Adapted to cornac's epoch-based
trainer: GRID counts optimizer *steps* (one step = one minibatch update, i.e.
``n_steps = n_epochs * ceil(n_train_samples / batch_size)``, where a session
of length T contributes T-1 samples) and early-stops on step-based validation
intervals (every 100 steps, patience 10); cornac instead trains ``n_epochs``
and keeps the best-on-val checkpoint, so ``n_epochs``/``val_eval_every``/
``val_sample`` here are pragmatic equivalents, not GRID values.
"""

GRID_CONFIG = dict(
tokenizer="rkmeans",
learning_rate=5e-4,
weight_decay=1e-6,
batch_size=256,
lr_schedule="constant",
model_selection="best",
val_metric="ndcg",
val_k=10,
n_epochs=50,
val_eval_every=1,
val_sample=2000,
)
67 changes: 67 additions & 0 deletions cornac/models/tiger/paischer_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Copyright 2026 The Cornac Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ============================================================================
"""Paischer et al. recipe for TIGER (arXiv 2412.08604) - the best documented
reproduction of the original paper's numbers (matches Beauty/Sports).

Taken from ``configs/setting/MenderTok_Beauty.yaml`` in
https://github.com/facebookresearch/preference_discerning (their TIGER
baseline shares the trainer). Slower than :data:`GRID_CONFIG` but tuned for
accuracy. Their item embeddings are Sentence-T5-XXL on title/price/brand/
categories text (descriptions instead for Toys) - the embedding side lives in
the experiment script, not here.

Usage::

from cornac.models.tiger import PAISCHER_CONFIG, TIGER

model = TIGER(**{**PAISCHER_CONFIG, "seed": 123})

Verbatim from their config: RQ-VAE (hidden 768/512/256, latent 128, AdamW wd
0.1, lr 1e-3, batch 2048, 8000 epochs, standardized inputs), seq2seq (6+6
layers, dropout 0.2, lr 3e-4 cosine with 10k warmup steps, weight decay
0.035, batch 64), beam width 30. Adapted to cornac's epoch-based trainer:
they cap training at 200k optimizer *steps* (one step = one minibatch update,
so ``n_steps = n_epochs * ceil(n_train_samples / batch_size)``, where a
session of length T contributes T-1 samples) with step-based early stopping
(patience 15). At batch 64 on Amazon-Beauty-scale data (~128k training
samples, ~2k steps/epoch), ``n_epochs=100`` lands near their 200k-step cap,
with best-on-val checkpoint selection instead of early stopping
(``val_eval_every``/``val_sample`` are pragmatic choices, not theirs).
"""

PAISCHER_CONFIG = dict(
tokenizer="rqvae",
feature_standardize=True,
rqvae_hidden_dims=(768, 512, 256),
rqvae_latent_dim=128,
rqvae_n_epochs=8000,
rqvae_batch_size=2048,
rqvae_weight_decay=0.1,
num_enc_layers=6,
num_dec_layers=6,
dropout=0.2,
learning_rate=3e-4,
lr_schedule="cosine",
warmup_steps=10000,
weight_decay=0.035,
batch_size=64,
n_beams=30,
model_selection="best",
val_metric="ndcg",
val_k=10,
n_epochs=100,
val_eval_every=5,
val_sample=2000,
)
Loading
Loading