Coverage for hopwise/model/general_recommender/ease.py: 89%
35 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-09-30 13:25 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-09-30 13:25 +0000
1r"""EASE
2################################################
3Reference:
4 Harald Steck. "Embarrassingly Shallow Autoencoders for Sparse Data" in WWW 2019.
5"""
7import numpy as np
8import scipy.sparse as sp
9import torch
11from hopwise.model.abstract_recommender import GeneralRecommender
12from hopwise.utils import InputType, ModelType
15class EASE(GeneralRecommender):
16 r"""EASE is a linear model for collaborative filtering, which combines the
17 strengths of auto-encoders and neighborhood-based approaches.
19 """
21 input_type = InputType.POINTWISE
22 type = ModelType.TRADITIONAL
24 def __init__(self, config, dataset):
25 super().__init__(config, dataset)
27 # load parameters info
28 reg_weight = config["reg_weight"]
30 # need at least one param
31 self.dummy_param = torch.nn.Parameter(torch.zeros(1))
33 X = dataset.inter_matrix(form="csr").astype(np.float32)
34 # just directly calculate the entire score matrix in init
35 # (can't be done incrementally)
37 # gram matrix
38 G = X.T @ X
40 # add reg to diagonal
41 G += reg_weight * sp.identity(G.shape[0]).astype(np.float32)
43 # convert to dense because inverse will be dense
44 G = G.todense()
46 # invert. this takes most of the time
47 P = np.linalg.inv(G)
48 B = P / (-np.diag(P))
49 # zero out diag
50 np.fill_diagonal(B, 0.0)
52 # instead of computing and storing the entire score matrix,
53 # just store B and compute the scores on demand
54 # more memory efficient for a larger number of users
55 # but if there's a large number of items not much one can do:
56 # still have to compute B all at once
57 # S = X @ B
58 # self.score_matrix = torch.from_numpy(S).to(self.device)
60 # torch doesn't support sparse tensor slicing,
61 # so will do everything with np/scipy
62 self.item_similarity = B
63 self.interaction_matrix = X
64 self.other_parameter_name = ["interaction_matrix", "item_similarity"]
65 self.device = config.device
67 def forward(self):
68 pass
70 def calculate_loss(self, interaction):
71 return torch.nn.Parameter(torch.zeros(1))
73 def predict(self, interaction):
74 user = interaction[self.USER_ID].cpu().numpy()
75 item = interaction[self.ITEM_ID].cpu().numpy()
77 return torch.from_numpy(
78 (self.interaction_matrix[user, :].multiply(self.item_similarity[:, item].T)).sum(axis=1).getA1()
79 ).to(self.device)
81 def full_sort_predict(self, interaction):
82 user = interaction[self.USER_ID].cpu().numpy()
84 r = self.interaction_matrix[user, :] @ self.item_similarity
85 return torch.from_numpy(r.flatten())