-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathFunkSVD.py
106 lines (82 loc) · 4.78 KB
/
FunkSVD.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
import numpy as np
import sys
from AdamOptimizer import AdamOptimizer
class FunkSVD:
def __init__(self, dataframe):
self.dataframe = dataframe.copy()
self.m = dataframe['UserId'].nunique()
self.n = dataframe['ItemId'].nunique()
# Creating bias factors
self.global_mean = dataframe['Rating'].mean()
self.bu_vector = np.zeros(self.m)
self.bi_vector = np.zeros(self.n)
# Create a mapping for userId and itemId
self.user_to_index = {user: idx for idx, user in enumerate(dataframe['UserId'].unique())}
self.item_to_index = {item: idx for idx, item in enumerate(dataframe['ItemId'].unique())}
# Add new columns for pre-mapped user and item indices
self.dataframe.loc[:,'user_idx'] = self.dataframe['UserId'].map(self.user_to_index)
self.dataframe.loc[:,'item_idx'] = self.dataframe['ItemId'].map(self.item_to_index)
def _initializePQ(self, k):
self.P = (np.sqrt(5/k)) * np.random.rand(self.m, k)
self.Q = (np.sqrt(5/k)) * np.random.rand(k, self.n)
def _getMiniBatch(self, batch_size):
"""Generate a mini-batch from the DataFrame."""
shuffled_df = self.dataframe.sample(frac=1).reset_index(drop=True) # Shuffle data
for start in range(0, len(shuffled_df), batch_size):
yield shuffled_df.iloc[start:start + batch_size]
def _MiniBatchGradientDescent(self, k=100, batch_size=10, lr=0.01, lamda=0.02, epochs=20, momentum=0.9):
# Initialize the P and Q matrices
self._initializePQ(k)
adam = AdamOptimizer()
# Initialize velocities (momentum terms) for P, Q, bu, and bi
vP = np.zeros_like(self.P)
mP = np.zeros_like(self.P)
vQ = np.zeros_like(self.Q)
mQ = np.zeros_like(self.Q)
vbu = np.zeros_like(self.bu_vector)
mbu = np.zeros_like(self.bu_vector)
vbi = np.zeros_like(self.bi_vector)
mbi = np.zeros_like(self.bi_vector)
# Iterate over epochs
for epoch in range(epochs):
total_loss = 0
# Iterate over mini-batches
for batch in self._getMiniBatch(batch_size):
users_idx = batch['user_idx'].to_numpy()
items_idx = batch['item_idx'].to_numpy()
# Make predictions for the batch
predictions = np.sum(self.P[users_idx, :] * self.Q[:, items_idx].T, axis=1) + self.global_mean + self.bi_vector[items_idx] + self.bu_vector[users_idx]
error = batch['Rating'].to_numpy() - predictions
# Reshape error for broadcasting
error = error[:, np.newaxis] # Shape (batch_size, 1)
# Update P and Q matrices and b_u and b_i vectors
P_grad = -((error * self.Q[:, items_idx].T) - (lamda * self.P[users_idx, :]))
Q_grad = -(error * self.P[users_idx, :] - lamda * self.Q[:, items_idx].T)
bu_grad = -(error.squeeze() - lamda * self.bu_vector[users_idx])
bi_grad = -(error.squeeze() - lamda * self.bi_vector[items_idx])
updateP,mP[users_idx,:],vP[users_idx,:] = adam.step(mP[users_idx,:],vP[users_idx,:],P_grad)
updateQ,mQ[:,items_idx],vQ[:,items_idx] = adam.step(mQ[:,items_idx],vQ[:,items_idx],Q_grad.T)
updatebu,mbu[users_idx],vbu[users_idx] = adam.step(mbu[users_idx],vbu[users_idx],bu_grad)
updatebi,mbi[items_idx],vbi[items_idx] = adam.step(mbi[items_idx],vbi[items_idx],bi_grad)
P_new = self.P[users_idx, :] + updateP
Q_new = self.Q[:,items_idx] + updateQ
self.bu_vector[users_idx] = self.bu_vector[users_idx] + updatebu
self.bi_vector[items_idx] = self.bi_vector[items_idx] + updatebi
self.P[users_idx, :] = P_new
self.Q[:, items_idx] = Q_new
# Accumulate the squared error for loss calculation
total_loss += np.sum(error**2)
# Calculate total RMSE after each epoch
avg_loss = np.sqrt(total_loss / len(self.dataframe))
if sys.stdout.isatty():
print(f"Epoch {epoch+1}/{epochs}, Loss (RMSE): {avg_loss}")
def train(self, k=100, batch_size=10, lr=0.01, lamda=0.02, epochs=30):
self._MiniBatchGradientDescent(k, batch_size, lr, lamda, epochs)
def prediction(self, userId, itemId, item_mean):
if userId not in self.user_to_index or itemId not in self.item_to_index:
return item_mean[itemId]
else:
user_idx = self.user_to_index[userId]
item_idx = self.item_to_index[itemId]
prediction = self.global_mean + self.bu_vector[user_idx] + self.bi_vector[item_idx] + self.P[user_idx, :] @ self.Q[:, item_idx]
return np.clip(prediction,1,5)