Source code for cornac.models.beacon.recom_beacon

# Copyright 2023 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.
# ============================================================================

import os
from collections import Counter

import numpy as np
from scipy.sparse import csc_matrix, csr_matrix, diags
from tqdm.auto import trange

from ..recommender import NextBasketRecommender


[docs] class Beacon(NextBasketRecommender): """Correlation-Sensitive Next-Basket Recommendation Parameters ---------- name: string, default: 'Beacon' The name of the recommender model. emb_dim: int, optional, default: 2 Embedding dimension rnn_unit: int, optional, default: 4 Number of dimension in a rnn unit. alpha: float, optional, default: 0.5 Hyperparameter to control the balance between correlative and sequential associations. rnn_cell_type: str, optional, default: 'LSTM' RNN cell type, options including ['LSTM', 'GRU', None] If None, BasicRNNCell will be used. dropout_rate: float, optional, default: 0.5 Dropout rate of neural network dense layers nb_hop: int, optional, default: 1 Number of hops for constructing correlation matrix. If 0, zeros matrix will be used. max_seq_length: int, optional, default: None Maximum basket sequence length. If None, it is the maximum number of basket in training sequences. n_epochs: int, optional, default: 15 Number of training epochs batch_size: int, optional, default: 32 Batch size lr: float, optional, default: 0.001 Initial value of learning rate for the optimizer. verbose: boolean, optional, default: False When True, running logs are displayed. seed: int, optional, default: None Random seed References ---------- LE, Duc Trong, Hady Wirawan LAUW, and Yuan Fang. Correlation-sensitive next-basket recommendation. International Joint Conferences on Artificial Intelligence, 2019. """ def __init__( self, name="Beacon", emb_dim=2, rnn_unit=4, alpha=0.5, rnn_cell_type="LSTM", dropout_rate=0.5, nb_hop=1, max_seq_length=None, n_epochs=15, batch_size=32, lr=0.001, trainable=True, verbose=False, seed=None, ): super().__init__(name=name, trainable=trainable, verbose=verbose) self.n_epochs = n_epochs self.batch_size = batch_size self.nb_hop = nb_hop self.emb_dim = emb_dim self.rnn_unit = rnn_unit self.alpha = alpha self.rnn_cell_type = rnn_cell_type self.dropout_rate = dropout_rate self.max_seq_length = max_seq_length self.seed = seed self.lr = lr
[docs] def fit(self, train_set, val_set=None): super().fit(train_set=train_set, val_set=val_set) import tensorflow.compat.v1 as tf from .beacon_tf import BeaconModel tf.disable_eager_execution() # less verbose TF os.environ["TF_CPP_MIN_LOG_LEVEL"] = "3" tf.logging.set_verbosity(tf.logging.ERROR) # max sequence length self.max_seq_length = ( max([len(bids) for bids in train_set.user_basket_data.values()]) if self.max_seq_length is None # init max_seq_length else self.max_seq_length ) self.correlation_matrix = self._build_correlation_matrix( train_set=train_set, val_set=val_set, n_items=self.total_items ) self.item_probs = self._compute_item_probs( train_set=train_set, val_set=val_set, n_items=self.total_items ) config = tf.ConfigProto() config.gpu_options.allow_growth = True config.log_device_placement = False sess = tf.Session(config=config) self.model = BeaconModel( sess, self.emb_dim, self.rnn_unit, self.alpha, self.max_seq_length, self.total_items, self.item_probs, self.correlation_matrix, self.rnn_cell_type, self.dropout_rate, self.seed, self.lr, ) sess.run(tf.global_variables_initializer()) # init variable last_loss = np.inf last_val_loss = np.inf loop = trange(self.n_epochs, disable=not self.verbose) loop.set_postfix( loss=last_loss, val_loss=last_val_loss, ) train_pool = [] validation_pool = [] for _ in loop: train_loss = 0.0 trained_cnt = 0 for batch_basket_items in self._data_iter( train_set, shuffle=True, current_pool=train_pool ): s, s_length, y = self._transform_data( batch_basket_items, self.total_items ) loss = self.model.train_batch(s, s_length, y) current_batch_size = len(batch_basket_items) trained_cnt += current_batch_size train_loss += loss * current_batch_size last_loss = train_loss / trained_cnt loop.set_postfix( loss=last_loss, val_loss=last_val_loss, ) if val_set is not None: val_loss = 0.0 val_cnt = 0 for batch_basket_items in self._data_iter( val_set, shuffle=False, current_pool=validation_pool ): s, s_length, y = self._transform_data( batch_basket_items, self.total_items ) loss = self.model.validate_batch(s, s_length, y) current_batch_size = len(batch_basket_items) val_cnt += current_batch_size val_loss += loss * current_batch_size last_val_loss = val_loss / val_cnt loop.set_postfix( loss=last_loss, val_loss=last_val_loss, ) return self
def _data_iter(self, data_set, shuffle=False, current_pool=[]): """This iterator ensure each batch has same size, the remaining data will be preceded in the next epoch""" for _, _, batch_basket_items in data_set.ubi_iter( batch_size=self.batch_size, shuffle=shuffle ): current_pool += batch_basket_items if len(current_pool) >= self.batch_size: yield current_pool[: self.batch_size] del current_pool[self.batch_size :] def _transform_data(self, batch_basket_items, n_items): assert len(batch_basket_items) == self.batch_size s = [basket_items[:-1] for basket_items in batch_basket_items] s_length = [len(b) for b in s] y = np.zeros((self.batch_size, n_items), dtype="int32") for inc, basket_items in enumerate(batch_basket_items): y[inc, basket_items[-1]] = 1 return s, s_length, y def _build_correlation_matrix(self, train_set, val_set, n_items): if self.nb_hop == 0: return csr_matrix((n_items, n_items), dtype="float32") pairs_cnt = Counter() for _, _, [basket_items] in train_set.ubi_iter(1, shuffle=False): for items in basket_items: current_items = np.unique(items) for i in range(len(current_items) - 1): for j in range(i + 1, len(current_items)): pairs_cnt[(current_items[i], current_items[j])] += 1 if val_set is not None: for _, _, [basket_items] in val_set.ubi_iter(1, shuffle=False): for items in basket_items: current_items = np.unique(items) for i in range(len(current_items) - 1): for j in range(i + 1, len(current_items)): pairs_cnt[(current_items[i], current_items[j])] += 1 data, row, col = [], [], [] for pair, cnt in pairs_cnt.most_common(): data.append(cnt) row.append(pair[0]) col.append(pair[1]) correlation_matrix = csc_matrix( (data, (row, col)), shape=(n_items, n_items), dtype="float32" ) correlation_matrix = self._normalize(correlation_matrix) w_mul = correlation_matrix coeff = 1.0 for _ in range(1, self.nb_hop): coeff *= 0.85 w_mul *= correlation_matrix w_mul = self._remove_diag(w_mul) w_adj_matrix = self._normalize(w_mul) correlation_matrix += coeff * w_adj_matrix return correlation_matrix def _remove_diag(self, adj_matrix): new_adj_matrix = csr_matrix(adj_matrix) new_adj_matrix.setdiag(0.0) new_adj_matrix.eliminate_zeros() return new_adj_matrix def _normalize(self, adj_matrix: csr_matrix): """Symmetrically normalize adjacency matrix.""" row_sum = adj_matrix.sum(1).A.squeeze() d_inv_sqrt = np.power( row_sum, -0.5, out=np.zeros_like(row_sum, dtype="float32"), where=row_sum != 0, ) d_mat_inv_sqrt = diags(d_inv_sqrt) normalized_matrix = ( adj_matrix.dot(d_mat_inv_sqrt).transpose().dot(d_mat_inv_sqrt) ) return normalized_matrix.tocsr() def _compute_item_probs(self, train_set, val_set, n_items): item_freq = Counter(train_set.uir_tuple[1]) if val_set is not None: item_freq += Counter(val_set.uir_tuple[1]) item_probs = np.zeros(n_items, dtype="float32") total_cnt = len(train_set.uir_tuple[1]) + len(val_set.uir_tuple[1]) for iid, cnt in item_freq.items(): item_probs[iid] = cnt / total_cnt return item_probs
[docs] def score(self, user_idx, history_baskets, **kwargs): s = [history_baskets] s_length = [len(history_baskets)] return self.model.predict(s, s_length)