FranKGraphBench 0.0.1a0__py2.py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- FranKGraphBench-0.0.1a0.dist-info/LICENSE +201 -0
- FranKGraphBench-0.0.1a0.dist-info/METADATA +189 -0
- FranKGraphBench-0.0.1a0.dist-info/RECORD +70 -0
- FranKGraphBench-0.0.1a0.dist-info/WHEEL +6 -0
- FranKGraphBench-0.0.1a0.dist-info/entry_points.txt +3 -0
- FranKGraphBench-0.0.1a0.dist-info/top_level.txt +2 -0
- data_integration/__init__.py +47 -0
- data_integration/__main__.py +4 -0
- data_integration/dataset.py +270 -0
- data_integration/dataset2class.py +34 -0
- data_integration/datasets/__init__.py +0 -0
- data_integration/datasets/amazon.py +140 -0
- data_integration/datasets/book-crossing.py +98 -0
- data_integration/datasets/douban-movie.py +75 -0
- data_integration/datasets/lastfm.py +215 -0
- data_integration/datasets/movielens.py +375 -0
- data_integration/datasets/steam.py +98 -0
- data_integration/datasets/worker.py +31 -0
- data_integration/datasets/yelp.py +156 -0
- framework/__init__.py +17 -0
- framework/__main__.py +4 -0
- framework/dataloader/__init__.py +0 -0
- framework/dataloader/dataloader.py +52 -0
- framework/dataloader/dataset/__init__.py +0 -0
- framework/dataloader/dataset/dataset.py +38 -0
- framework/dataloader/edge_splitter/__init__.py +0 -0
- framework/dataloader/edge_splitter/edge_splitter.py +163 -0
- framework/dataloader/graph/__init__.py +0 -0
- framework/dataloader/graph/graph.py +314 -0
- framework/dataloader/graph/node.py +80 -0
- framework/dataloader/preprocess/__init__.py +0 -0
- framework/dataloader/preprocess/methods.py +47 -0
- framework/evaluator/__init__.py +0 -0
- framework/evaluator/metric.py +39 -0
- framework/evaluator/metric2class.py +10 -0
- framework/evaluator/metrics/__init__.py +0 -0
- framework/evaluator/metrics/map.py +33 -0
- framework/evaluator/metrics/ndcg.py +49 -0
- framework/experiment.py +120 -0
- framework/recommender/__init__.py +0 -0
- framework/recommender/model2class.py +46 -0
- framework/recommender/models/__init__.py +0 -0
- framework/recommender/models/complEx/__init__.py +0 -0
- framework/recommender/models/complEx/model.py +193 -0
- framework/recommender/models/deep_walk_based/__init__.py +0 -0
- framework/recommender/models/deep_walk_based/model.py +149 -0
- framework/recommender/models/distMult/__init__.py +0 -0
- framework/recommender/models/distMult/model.py +187 -0
- framework/recommender/models/ePHEN/__init__.py +0 -0
- framework/recommender/models/ePHEN/model.py +159 -0
- framework/recommender/models/rESCAL/__init__.py +0 -0
- framework/recommender/models/rESCAL/model.py +184 -0
- framework/recommender/models/rotatE/__init__.py +0 -0
- framework/recommender/models/rotatE/model.py +195 -0
- framework/recommender/models/transD/__init__.py +0 -0
- framework/recommender/models/transD/model.py +190 -0
- framework/recommender/models/transE/__init__.py +0 -0
- framework/recommender/models/transE/model.py +194 -0
- framework/recommender/models/transH/__init__.py +0 -0
- framework/recommender/models/transH/model.py +193 -0
- framework/recommender/models/transR/__init__.py +0 -0
- framework/recommender/models/transR/model.py +190 -0
- framework/recommender/models/tuckER/__init__.py +0 -0
- framework/recommender/models/tuckER/model.py +199 -0
- framework/recommender/recommender.py +32 -0
- framework/recommender/utils/__init__.py +0 -0
- framework/recommender/utils/walker.py +185 -0
- framework/reporter/__init__.py +0 -0
- framework/reporter/report.py +101 -0
- framework/utils.py +10 -0
framework/experiment.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import yaml
|
|
2
|
+
import json
|
|
3
|
+
import importlib
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
from copy import deepcopy
|
|
6
|
+
|
|
7
|
+
from .dataloader.dataloader import load, preprocess, split
|
|
8
|
+
from .recommender.model2class import model2class
|
|
9
|
+
from .evaluator.metric2class import metric2class
|
|
10
|
+
from .reporter.report import Reporter
|
|
11
|
+
from .reporter.report import ExecutionTimesReporter
|
|
12
|
+
|
|
13
|
+
import numpy as np
|
|
14
|
+
import time
|
|
15
|
+
import cpuinfo
|
|
16
|
+
import psutil
|
|
17
|
+
import GPUtil
|
|
18
|
+
|
|
19
|
+
def _load_model_class(name):
|
|
20
|
+
# relative path from root
|
|
21
|
+
module_name = f'framework.recommender.models.{model2class[name]["submodule"]}'
|
|
22
|
+
class_name = model2class[name]['class']
|
|
23
|
+
return module_name, class_name
|
|
24
|
+
|
|
25
|
+
def _load_metric_class(name):
|
|
26
|
+
# relative path from root
|
|
27
|
+
module_name = f'framework.evaluator.{metric2class[name]["submodule"]}'
|
|
28
|
+
class_name = metric2class[name]['class']
|
|
29
|
+
return module_name, class_name
|
|
30
|
+
|
|
31
|
+
def run(config_path):
|
|
32
|
+
config = None
|
|
33
|
+
with open(config_path, 'r') as f:
|
|
34
|
+
config = yaml.safe_load(f)
|
|
35
|
+
|
|
36
|
+
experiment = config['experiment']
|
|
37
|
+
|
|
38
|
+
G = load(**experiment['dataset'])
|
|
39
|
+
preprocess(G, experiment['preprocess'])
|
|
40
|
+
print(f'Final graph: {G.info()}')
|
|
41
|
+
|
|
42
|
+
# Extracting evaluation metrics
|
|
43
|
+
evaluation_config = experiment['evaluation']
|
|
44
|
+
eval_metrics = []
|
|
45
|
+
for metric in evaluation_config['metrics']:
|
|
46
|
+
module_name, class_name = _load_metric_class(metric)
|
|
47
|
+
metric_class = getattr(importlib.import_module(module_name), class_name)
|
|
48
|
+
k = evaluation_config['k']
|
|
49
|
+
relevance_threshold = evaluation_config['relevance_threshold']
|
|
50
|
+
metric_instance = metric_class(k, relevance_threshold)
|
|
51
|
+
eval_metrics.append(metric_instance)
|
|
52
|
+
|
|
53
|
+
# Loop over dataset (specially if its a k-fold split)
|
|
54
|
+
model_metrics = defaultdict(list)
|
|
55
|
+
model_execution_times = defaultdict(list)
|
|
56
|
+
for fold, dataset in enumerate(split(G, **experiment['split'])):
|
|
57
|
+
print(f'{fold+1}-Fold: evaluating model ...')
|
|
58
|
+
print(G.info())
|
|
59
|
+
|
|
60
|
+
for model_config in experiment['models']:
|
|
61
|
+
start_time = time.time()
|
|
62
|
+
# Loading model dinamically
|
|
63
|
+
module_name, class_name = _load_model_class(model_config['name'])
|
|
64
|
+
model = getattr(importlib.import_module(module_name), class_name)
|
|
65
|
+
model = model(model_config['config'], **model_config['parameters'])
|
|
66
|
+
|
|
67
|
+
# Training model
|
|
68
|
+
G_train, ratings_train = dataset.get_train_data()
|
|
69
|
+
print(f'Training model: {model.name()}...')
|
|
70
|
+
|
|
71
|
+
model.train(G_train, ratings_train)
|
|
72
|
+
# Getting recomendations
|
|
73
|
+
# From this data I can evaluate the result for validation and test data
|
|
74
|
+
recs = model.get_recommendations(k=k)
|
|
75
|
+
|
|
76
|
+
ratings_test = dataset.get_test_data()
|
|
77
|
+
|
|
78
|
+
model_execution_times[model.name()].append(time.time() - start_time)
|
|
79
|
+
|
|
80
|
+
metric_vals = []
|
|
81
|
+
for metric in eval_metrics:
|
|
82
|
+
metric_val = metric.eval(ratings_test, recs)
|
|
83
|
+
print(f'{metric.name()}: {metric_val}')
|
|
84
|
+
metric_vals.append(metric_val)
|
|
85
|
+
|
|
86
|
+
print(f'Execution time: {model_execution_times[model.name()][-1]}s')
|
|
87
|
+
|
|
88
|
+
model_metrics[model.name()].append(metric_vals)
|
|
89
|
+
|
|
90
|
+
print('----'*20)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
print(f'Metrics final result')
|
|
94
|
+
for model, metrics in model_metrics.items():
|
|
95
|
+
print(f'{model}')
|
|
96
|
+
metrics_mean = np.array(metrics).mean(axis=0)
|
|
97
|
+
metrics_std = np.array(metrics).std(axis=0)
|
|
98
|
+
for idx, (metric_mean, metric_std) in enumerate(zip(metrics_mean, metrics_std)):
|
|
99
|
+
print(f'\t{eval_metrics[idx].name()} mean: {metric_mean} +- {metric_std}')
|
|
100
|
+
print('')
|
|
101
|
+
print(f'Execution times final result')
|
|
102
|
+
for model, execution_times in model_execution_times.items():
|
|
103
|
+
print(f'model: {model}; CPU: {cpuinfo.get_cpu_info()["brand_raw"]}; RAM: {round(psutil.virtual_memory().total/(1024**3))}GB; GPUs: {[gpu.name for gpu in GPUtil.getGPUs()]}')
|
|
104
|
+
execution_times_mean = np.array(execution_times).mean(axis=0)
|
|
105
|
+
execution_times_std = np.array(execution_times).std(axis=0)
|
|
106
|
+
print(f'\tmean: {execution_times_mean}s +- {execution_times_std}s')
|
|
107
|
+
|
|
108
|
+
reporter = Reporter(experiment['report']['file'], eval_metrics)
|
|
109
|
+
reporter.report(model_metrics)
|
|
110
|
+
|
|
111
|
+
if 'execution_times' in experiment['report']:
|
|
112
|
+
et_reporter = ExecutionTimesReporter(experiment['report']['execution_times']['file'])
|
|
113
|
+
et_reporter.report(model_execution_times)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
|
|
File without changes
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
model2class = {
|
|
2
|
+
'deepwalk_based': {
|
|
3
|
+
'submodule': 'deep_walk_based.model',
|
|
4
|
+
'class': 'DeepWalkBased'
|
|
5
|
+
},
|
|
6
|
+
'transE': {
|
|
7
|
+
'submodule': 'transE.model',
|
|
8
|
+
'class': 'TransE'
|
|
9
|
+
},
|
|
10
|
+
'transH': {
|
|
11
|
+
'submodule': 'transH.model',
|
|
12
|
+
'class': 'TransH'
|
|
13
|
+
},
|
|
14
|
+
'transR': {
|
|
15
|
+
'submodule': 'transR.model',
|
|
16
|
+
'class': 'TransR'
|
|
17
|
+
},
|
|
18
|
+
'transD': {
|
|
19
|
+
'submodule': 'transD.model',
|
|
20
|
+
'class': 'TransD'
|
|
21
|
+
},
|
|
22
|
+
'rotatE': {
|
|
23
|
+
'submodule': 'rotatE.model',
|
|
24
|
+
'class': 'RotatE'
|
|
25
|
+
},
|
|
26
|
+
'tuckER': {
|
|
27
|
+
'submodule': 'tuckER.model',
|
|
28
|
+
'class': 'TuckER'
|
|
29
|
+
},
|
|
30
|
+
'distMult': {
|
|
31
|
+
'submodule': 'distMult.model',
|
|
32
|
+
'class': 'DistMult'
|
|
33
|
+
},
|
|
34
|
+
'rESCAL': {
|
|
35
|
+
'submodule': 'rESCAL.model',
|
|
36
|
+
'class': 'RESCAL'
|
|
37
|
+
},
|
|
38
|
+
'complEx': {
|
|
39
|
+
'submodule': 'complEx.model',
|
|
40
|
+
'class': 'ComplEx'
|
|
41
|
+
},
|
|
42
|
+
'ePHEN': {
|
|
43
|
+
'submodule': 'ePHEN.model',
|
|
44
|
+
'class': 'EPHEN'
|
|
45
|
+
}
|
|
46
|
+
}
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
from framework.dataloader.graph.node import UserNode, ItemNode
|
|
2
|
+
from ...recommender import Recommender
|
|
3
|
+
|
|
4
|
+
from pykeen.pipeline import pipeline
|
|
5
|
+
from pykeen.triples import TriplesFactory
|
|
6
|
+
from pykeen.datasets import Dataset
|
|
7
|
+
from sklearn.neighbors import NearestNeighbors
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
import torch
|
|
12
|
+
import typing as t
|
|
13
|
+
|
|
14
|
+
"""
|
|
15
|
+
Recommender base class
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ComplEx(Recommender):
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
config: dict,
|
|
23
|
+
embedding_dim: int = 200,
|
|
24
|
+
entity_initializer=None,
|
|
25
|
+
relation_initializer=None,
|
|
26
|
+
regularizer=None,
|
|
27
|
+
regularizer_kwargs=None,
|
|
28
|
+
epochs: int = 5,
|
|
29
|
+
seed: int = 42,
|
|
30
|
+
evaluation_fallback: bool = True,
|
|
31
|
+
all_recs: bool = False,
|
|
32
|
+
triples: str = "all",
|
|
33
|
+
):
|
|
34
|
+
super().__init__(config)
|
|
35
|
+
self.embedding_dim = embedding_dim
|
|
36
|
+
self.entity_initializer = entity_initializer
|
|
37
|
+
self.relation_initializer = relation_initializer
|
|
38
|
+
self.regularizer = regularizer
|
|
39
|
+
self.regularizer_kwargs = regularizer_kwargs
|
|
40
|
+
self.epochs = epochs
|
|
41
|
+
self.seed = seed
|
|
42
|
+
self.evaluation_fallback = evaluation_fallback
|
|
43
|
+
self.all_recs = all_recs
|
|
44
|
+
self.triples = triples
|
|
45
|
+
self._triples = None
|
|
46
|
+
self._embedding = {}
|
|
47
|
+
|
|
48
|
+
def name(self):
|
|
49
|
+
text = "ComplEx based model + cosine similarity"
|
|
50
|
+
text += f";embedding_dim={self.embedding_dim};entity_initializer={self.entity_initializer};relation_initializer={self.relation_initializer};regularizer={self.regularizer};epochs={self.epochs};seed={self.seed}"
|
|
51
|
+
return text
|
|
52
|
+
|
|
53
|
+
def train(self, G_train, ratings_train):
|
|
54
|
+
self.G_train = G_train
|
|
55
|
+
if self.triples == "all":
|
|
56
|
+
self._triples = G_train.get_all_triples()
|
|
57
|
+
elif self.triples == "ratings":
|
|
58
|
+
self._triples = G_train.get_ratings_triples()
|
|
59
|
+
self.fit()
|
|
60
|
+
|
|
61
|
+
def get_recommendations(self, k: int = 5) -> t.Dict[UserNode, t.List[ItemNode]]:
|
|
62
|
+
"""
|
|
63
|
+
:param k: cutoff recommendation (int)
|
|
64
|
+
:return dict of recommendations for each user
|
|
65
|
+
of type {user1: [item1, item2]}
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
users = list(self.G_train.get_user_nodes())
|
|
69
|
+
items = list(self.G_train.get_item_nodes())
|
|
70
|
+
|
|
71
|
+
users_embeddings = np.array([self._embedding[user] for user in users])
|
|
72
|
+
items_embeddings = np.array([self._embedding[item] for item in items])
|
|
73
|
+
|
|
74
|
+
n_neighbors = self._get_n_neighbors(users, items, k)
|
|
75
|
+
knn = NearestNeighbors(n_neighbors=n_neighbors, metric="cosine")
|
|
76
|
+
knn.fit(items_embeddings)
|
|
77
|
+
rec_indices = knn.kneighbors(users_embeddings, return_distance=False)
|
|
78
|
+
|
|
79
|
+
recommendations = {}
|
|
80
|
+
for user_idx, user in enumerate(users):
|
|
81
|
+
rated_items = self.G_train.get_user_rated_items(user)
|
|
82
|
+
recs = []
|
|
83
|
+
for item_idx in rec_indices[user_idx]:
|
|
84
|
+
item = items[item_idx]
|
|
85
|
+
if item not in rated_items:
|
|
86
|
+
recs.append(item)
|
|
87
|
+
if not self.all_recs and len(recs) == k:
|
|
88
|
+
break
|
|
89
|
+
|
|
90
|
+
recommendations[user] = recs
|
|
91
|
+
|
|
92
|
+
return recommendations
|
|
93
|
+
|
|
94
|
+
def get_user_recommendation(self, user: UserNode, k: int = 5):
|
|
95
|
+
raise NotImplementedError(
|
|
96
|
+
"Override get_user_recommendation() for your model subclass."
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
def fit(self):
|
|
100
|
+
triples = TriplesFactory.from_labeled_triples(
|
|
101
|
+
self._triples[["head", "relation", "tail"]].values,
|
|
102
|
+
create_inverse_triples=False,
|
|
103
|
+
entity_to_id=None,
|
|
104
|
+
relation_to_id=None,
|
|
105
|
+
compact_id=False,
|
|
106
|
+
filter_out_candidate_inverse_relations=True,
|
|
107
|
+
metadata=None,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
dataset = Dataset.from_tf(triples, ratios=[0.95, 0.05, 0.0])
|
|
111
|
+
entity_to_id = dataset.entity_to_id
|
|
112
|
+
|
|
113
|
+
result = pipeline(
|
|
114
|
+
dataset=dataset,
|
|
115
|
+
model="ComplEx",
|
|
116
|
+
model_kwargs=dict(
|
|
117
|
+
embedding_dim=self.embedding_dim,
|
|
118
|
+
entity_initializer=self.entity_initializer,
|
|
119
|
+
relation_initializer=self.relation_initializer,
|
|
120
|
+
regularizer=self.regularizer,
|
|
121
|
+
regularizer_kwargs=self.regularizer_kwargs,
|
|
122
|
+
),
|
|
123
|
+
epochs = self.epochs,
|
|
124
|
+
random_seed = self.seed,
|
|
125
|
+
evaluation_fallback= self.evaluation_fallback,
|
|
126
|
+
)
|
|
127
|
+
model = result.model
|
|
128
|
+
|
|
129
|
+
users_iter = self.G_train.get_user_nodes()
|
|
130
|
+
items_iter = self.G_train.get_item_nodes()
|
|
131
|
+
|
|
132
|
+
users = list(users_iter)
|
|
133
|
+
items = list(items_iter)
|
|
134
|
+
|
|
135
|
+
# deal with entities that might have not been embedded
|
|
136
|
+
users_indices, users_no_embedding = [], []
|
|
137
|
+
for user in users_iter:
|
|
138
|
+
try:
|
|
139
|
+
users_indices.append(entity_to_id[user.__str__()])
|
|
140
|
+
except:
|
|
141
|
+
users_no_embedding.append(user)
|
|
142
|
+
users.remove(user)
|
|
143
|
+
continue
|
|
144
|
+
users_indices = torch.LongTensor(users_indices)
|
|
145
|
+
|
|
146
|
+
items_indices, items_no_embedding = [], []
|
|
147
|
+
for item in items_iter:
|
|
148
|
+
try:
|
|
149
|
+
items_indices.append(entity_to_id[item.__str__()])
|
|
150
|
+
except:
|
|
151
|
+
items_no_embedding.append(item)
|
|
152
|
+
items.remove(item)
|
|
153
|
+
continue
|
|
154
|
+
items_indices = torch.LongTensor(items_indices)
|
|
155
|
+
|
|
156
|
+
users_embeddings = (model.entity_representations[0](indices=users_indices).detach().cpu().numpy())
|
|
157
|
+
items_embeddings = (model.entity_representations[0](indices=items_indices).detach().cpu().numpy())
|
|
158
|
+
|
|
159
|
+
concat_users_embeddings, concat_items_embeddings = np.zeros((len(users_embeddings), self.embedding_dim*2)), np.zeros((len(items_embeddings), self.embedding_dim*2))
|
|
160
|
+
|
|
161
|
+
# complex data not supported by sklearn
|
|
162
|
+
for index, ue in enumerate(users_embeddings):
|
|
163
|
+
concat_users_embeddings[index] = np.concatenate((np.real(ue), np.imag(ue)))
|
|
164
|
+
for index, ie in enumerate(items_embeddings):
|
|
165
|
+
concat_items_embeddings[index] = np.concatenate((np.real(ie), np.imag(ie)))
|
|
166
|
+
|
|
167
|
+
if len(users_no_embedding) > 0:
|
|
168
|
+
concat_users_embeddings = np.concatenate((concat_users_embeddings, np.zeros((len(users_no_embedding), self.embedding_dim*2), dtype=concat_users_embeddings.dtype)), axis=0)
|
|
169
|
+
users = users + users_no_embedding
|
|
170
|
+
|
|
171
|
+
if len(items_no_embedding) > 0:
|
|
172
|
+
concat_items_embeddings = np.concatenate((concat_items_embeddings, np.zeros((len(items_no_embedding), self.embedding_dim*2), dtype=concat_items_embeddings.dtype)), axis=0)
|
|
173
|
+
items = items + items_no_embedding
|
|
174
|
+
|
|
175
|
+
for index, user in enumerate(users):
|
|
176
|
+
self._embedding[user] = concat_users_embeddings[index]
|
|
177
|
+
|
|
178
|
+
for index, item in enumerate(items):
|
|
179
|
+
self._embedding[item] = concat_items_embeddings[index]
|
|
180
|
+
|
|
181
|
+
def _get_n_neighbors(self, users, items, top_k):
|
|
182
|
+
n_neighbors = 0
|
|
183
|
+
if self.all_recs:
|
|
184
|
+
n_neighbors = len(items)
|
|
185
|
+
else:
|
|
186
|
+
max_recs = 0
|
|
187
|
+
for user in users:
|
|
188
|
+
rated_items = self.G_train.get_user_rated_items(user)
|
|
189
|
+
if len(rated_items) > max_recs:
|
|
190
|
+
max_recs = len(rated_items)
|
|
191
|
+
n_neighbors = min(max_recs + top_k, len(items))
|
|
192
|
+
|
|
193
|
+
return n_neighbors
|
|
File without changes
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
from framework.dataloader.graph.node import UserNode
|
|
2
|
+
from ...recommender import Recommender
|
|
3
|
+
from ...utils.walker import BiasedRandomWalker
|
|
4
|
+
|
|
5
|
+
import walker
|
|
6
|
+
import numpy as np
|
|
7
|
+
import networkx as nx
|
|
8
|
+
from gensim.models.word2vec import Word2Vec
|
|
9
|
+
from sklearn.neighbors import NearestNeighbors
|
|
10
|
+
from copy import deepcopy
|
|
11
|
+
|
|
12
|
+
from ....dataloader.graph.node import ItemNode
|
|
13
|
+
|
|
14
|
+
class DeepWalkBased(Recommender):
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
config : dict,
|
|
18
|
+
walk_len : int,
|
|
19
|
+
n_walks : int,
|
|
20
|
+
p : float = 1.0,
|
|
21
|
+
q : float = 1.0,
|
|
22
|
+
embedding_size : int = 64,
|
|
23
|
+
workers : int = 4,
|
|
24
|
+
window_size : int = 3,
|
|
25
|
+
epochs: int = 1,
|
|
26
|
+
learning_rate : float = 0.05,
|
|
27
|
+
min_count : int = 1,
|
|
28
|
+
seed : int = 42,
|
|
29
|
+
all_recs : bool = False
|
|
30
|
+
):
|
|
31
|
+
super().__init__(config)
|
|
32
|
+
self.walk_len = walk_len
|
|
33
|
+
self.n_walks = n_walks
|
|
34
|
+
self.embedding_size = embedding_size
|
|
35
|
+
self.p, self.q = p, q
|
|
36
|
+
self.workers = workers
|
|
37
|
+
self.window_size = window_size
|
|
38
|
+
self.epochs = epochs
|
|
39
|
+
self.learning_rate = learning_rate
|
|
40
|
+
self.min_count = min_count
|
|
41
|
+
self.seed = seed
|
|
42
|
+
self.all_recs = all_recs
|
|
43
|
+
self._embedding = {}
|
|
44
|
+
|
|
45
|
+
def name(self):
|
|
46
|
+
text = 'Node2Vec based model + cosine similarity'
|
|
47
|
+
text += f';q={self.q};p={self.p};embedding_size={self.embedding_size}'
|
|
48
|
+
return text
|
|
49
|
+
|
|
50
|
+
def train(self, G_train, ratings_train):
|
|
51
|
+
self.G_train = G_train
|
|
52
|
+
self.fit()
|
|
53
|
+
|
|
54
|
+
def get_recommendations(self, k: int = 5):
|
|
55
|
+
# Set doesnt guarantee the order of set elements
|
|
56
|
+
users = list(self.G_train.get_user_nodes())
|
|
57
|
+
items = list(self.G_train.get_item_nodes())
|
|
58
|
+
|
|
59
|
+
users_embeddings = np.array([self._embedding[user] for user in users])
|
|
60
|
+
items_embeddings = np.array([self._embedding[item] for item in items])
|
|
61
|
+
|
|
62
|
+
n_neighbors = self._get_n_neighbors(users, items, k)
|
|
63
|
+
knn = NearestNeighbors(n_neighbors=n_neighbors, metric='cosine')
|
|
64
|
+
knn.fit(items_embeddings)
|
|
65
|
+
rec_indices = knn.kneighbors(users_embeddings, return_distance=False)
|
|
66
|
+
|
|
67
|
+
recommendations = {}
|
|
68
|
+
for user_idx, user in enumerate(users):
|
|
69
|
+
rated_items = self.G_train.get_user_rated_items(user)
|
|
70
|
+
recs = []
|
|
71
|
+
for item_idx in rec_indices[user_idx]:
|
|
72
|
+
item = items[item_idx]
|
|
73
|
+
if item not in rated_items:
|
|
74
|
+
recs.append(item)
|
|
75
|
+
if not self.all_recs and len(recs) == k:
|
|
76
|
+
break
|
|
77
|
+
|
|
78
|
+
recommendations[user] = recs
|
|
79
|
+
|
|
80
|
+
return recommendations
|
|
81
|
+
|
|
82
|
+
def get_user_recommendation(self, user: UserNode, k: int = 5):
|
|
83
|
+
items = self.G_train.get_item_nodes()
|
|
84
|
+
rated_items = self.G_train.get_user_rated_items(user)
|
|
85
|
+
unrated_items = list(items - rated_items)
|
|
86
|
+
user_embedding = self._embedding[user].reshape(1, -1)
|
|
87
|
+
items_embeddings = np.array([self._embedding[item] for item in unrated_items])
|
|
88
|
+
n_neighbors = k if not self.all_recs else len(unrated_items)
|
|
89
|
+
knn = NearestNeighbors(n_neighbors=n_neighbors, metric='cosine')
|
|
90
|
+
knn.fit(items_embeddings)
|
|
91
|
+
rec_indices = knn.kneighbors(user_embedding, return_distance=False)
|
|
92
|
+
|
|
93
|
+
recs = []
|
|
94
|
+
for idx in rec_indices[0]:
|
|
95
|
+
recs.append(unrated_items[idx])
|
|
96
|
+
|
|
97
|
+
return recs
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def fit(self):
|
|
101
|
+
graph = self.G_train.convert_node_labels_to_integer()
|
|
102
|
+
self.walks = walker.random_walks(
|
|
103
|
+
graph, n_walks=self.n_walks, walk_len=self.walk_len
|
|
104
|
+
)
|
|
105
|
+
print(f'Random walk shape: {self.walks.shape}')
|
|
106
|
+
self.walks = self.walks.tolist()
|
|
107
|
+
|
|
108
|
+
model = Word2Vec(
|
|
109
|
+
self.walks,
|
|
110
|
+
sg=1,
|
|
111
|
+
hs=1,
|
|
112
|
+
alpha=self.learning_rate,
|
|
113
|
+
epochs=self.epochs,
|
|
114
|
+
vector_size=self.embedding_size,
|
|
115
|
+
window=self.window_size,
|
|
116
|
+
min_count=self.min_count,
|
|
117
|
+
workers=self.workers,
|
|
118
|
+
seed=self.seed
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
for node in graph.nodes():
|
|
122
|
+
self._embedding[graph.nodes[node]['old_label']] = model.wv[node]
|
|
123
|
+
|
|
124
|
+
# self.G_train.convert_back()
|
|
125
|
+
|
|
126
|
+
# def _convert_node_labels_to_integer(self, G):
|
|
127
|
+
# N = G.number_of_nodes()
|
|
128
|
+
# mapping = dict(zip(G.nodes(), range(0, N)))
|
|
129
|
+
# nx.relabel_nodes(G, mapping, copy=False)
|
|
130
|
+
# nx.set_node_attributes(G, {v: k for k, v in mapping.items()}, 'old_label')
|
|
131
|
+
|
|
132
|
+
def _convert_back(self, G):
|
|
133
|
+
mapping = {node: G.nodes[node]['old_label'] for node in G.nodes()}
|
|
134
|
+
nx.relabel_nodes(G, mapping, copy=False)
|
|
135
|
+
nx.set_node_attributes(G, {v: k for k, v in mapping.items()}, 'old_label')
|
|
136
|
+
|
|
137
|
+
def _get_n_neighbors(self, users, items, top_k):
|
|
138
|
+
n_neighbors = 0
|
|
139
|
+
if self.all_recs:
|
|
140
|
+
n_neighbors = len(items)
|
|
141
|
+
else:
|
|
142
|
+
max_recs = 0
|
|
143
|
+
for user in users:
|
|
144
|
+
rated_items = self.G_train.get_user_rated_items(user)
|
|
145
|
+
if len(rated_items) > max_recs:
|
|
146
|
+
max_recs = len(rated_items)
|
|
147
|
+
n_neighbors = min(max_recs + top_k, len(items))
|
|
148
|
+
|
|
149
|
+
return n_neighbors
|
|
File without changes
|