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

1r"""EASE 

2################################################ 

3Reference: 

4 Harald Steck. "Embarrassingly Shallow Autoencoders for Sparse Data" in WWW 2019. 

5""" 

6 

7import numpy as np 

8import scipy.sparse as sp 

9import torch 

10 

11from hopwise.model.abstract_recommender import GeneralRecommender 

12from hopwise.utils import InputType, ModelType 

13 

14 

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. 

18 

19 """ 

20 

21 input_type = InputType.POINTWISE 

22 type = ModelType.TRADITIONAL 

23 

24 def __init__(self, config, dataset): 

25 super().__init__(config, dataset) 

26 

27 # load parameters info 

28 reg_weight = config["reg_weight"] 

29 

30 # need at least one param 

31 self.dummy_param = torch.nn.Parameter(torch.zeros(1)) 

32 

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) 

36 

37 # gram matrix 

38 G = X.T @ X 

39 

40 # add reg to diagonal 

41 G += reg_weight * sp.identity(G.shape[0]).astype(np.float32) 

42 

43 # convert to dense because inverse will be dense 

44 G = G.todense() 

45 

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) 

51 

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) 

59 

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 

66 

67 def forward(self): 

68 pass 

69 

70 def calculate_loss(self, interaction): 

71 return torch.nn.Parameter(torch.zeros(1)) 

72 

73 def predict(self, interaction): 

74 user = interaction[self.USER_ID].cpu().numpy() 

75 item = interaction[self.ITEM_ID].cpu().numpy() 

76 

77 return torch.from_numpy( 

78 (self.interaction_matrix[user, :].multiply(self.item_similarity[:, item].T)).sum(axis=1).getA1() 

79 ).to(self.device) 

80 

81 def full_sort_predict(self, interaction): 

82 user = interaction[self.USER_ID].cpu().numpy() 

83 

84 r = self.interaction_matrix[user, :] @ self.item_similarity 

85 return torch.from_numpy(r.flatten())