Skip to content

Commit 5597494

Browse files
authored
Merge pull request #27 from DataboyUsen/main
feat/fix/doc: Add separate constraints + ReHLoss() for plqMF_Ridge + NMF example (#7)
2 parents c9aa347 + 1ed1a91 commit 5597494

File tree

8 files changed

+1018
-467
lines changed

8 files changed

+1018
-467
lines changed

doc/source/example.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Example Gallery
1616
examples/Path_solution.ipynb
1717
examples/Warm_start.ipynb
1818
examples/Sklearn_Mixin.ipynb
19-
examples/MF.ipynb
19+
examples/NMF.ipynb
2020

2121
List of Examples
2222
----------------
@@ -32,4 +32,4 @@ List of Examples
3232
examples/Path_solution.ipynb
3333
examples/Warm_start.ipynb
3434
examples/Sklearn_Mixin.ipynb
35-
examples/MF.ipynb
35+
examples/NMF.ipynb

doc/source/examples/MF.ipynb

Lines changed: 0 additions & 407 deletions
This file was deleted.

doc/source/examples/NMF.ipynb

Lines changed: 965 additions & 0 deletions
Large diffs are not rendered by default.

doc/source/tutorials/ReHLine_MF.rst

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,20 @@ Considering a User-Item-Rating triplet dataset :math:`(u, i, r_{ui})` derived fr
2626
2727
.. math::
2828
\ \text{ s.t. } \
29-
\mathbf{A} \begin{pmatrix} \alpha_u \\ \mathbf{p}_u \end{pmatrix} + \mathbf{b} \geq \mathbf{0},\ u = 1,\dots,n
29+
\mathbf{A}_{\text{user}} \begin{pmatrix} \alpha_u \\ \mathbf{p}_u \end{pmatrix} + \mathbf{b}_{\text{user}} \geq \mathbf{0},\ u = 1,\dots,n
3030
\quad \text{and} \quad
31-
\mathbf{A} \begin{pmatrix} \beta_i \\ \mathbf{q}_i \end{pmatrix} + \mathbf{b} \geq \mathbf{0},\ i = 1,\dots,m
31+
\mathbf{A}_{\text{item}} \begin{pmatrix} \beta_i \\ \mathbf{q}_i \end{pmatrix} + \mathbf{b}_{\text{item}} \geq \mathbf{0},\ i = 1,\dots,m
3232
3333
where
3434

3535
- :math:`\text{PLQ}(\cdot , \cdot)`
3636
is a convex piecewise linear-quadratic loss function. You can find built-in loss functions in the `Loss <./loss.rst>`_ section.
3737

38-
- :math:`\mathbf{A}` is a :math:`d \times (k+1)` matrix and :math:`\mathbf{b}` is a :math:`d`-dimensional vector
39-
representing :math:`d` linear constraints. See `Constraints <./constraint.rst>`_ for more details.
38+
- :math:`\mathbf{A}_{\text{user}}` is a :math:`d \times (k+1)` matrix and :math:`\mathbf{b}_{\text{user}}` is a :math:`d`-dimensional vector
39+
representing :math:`d` linear constraints to user side parameters. See `Constraints <./constraint.rst>`_ for more details.
40+
41+
- :math:`\mathbf{A}_{\text{item}}` is a :math:`d \times (k+1)` matrix and :math:`\mathbf{b}_{\text{item}}` is a :math:`d`-dimensional vector
42+
representing :math:`d` linear constraints to item side parameters. See `Constraints <./constraint.rst>`_ for more details.
4043

4144
- :math:`\Omega`
4245
is a user-item collection that records all training data
@@ -93,11 +96,11 @@ Basic Usage
9396
9497
# 3. Model Construction
9598
clf = plqMF_Ridge(
96-
C=0.001, ## Regularization strength
97-
rank=6, ## Latent factor dimension
98-
loss={'name': 'mae'}, ## Use absolute loss
99-
n_users=user_num, ## Number of users
100-
n_items=item_num, ## Number of items
99+
C=0.001, ## Regularization strength
100+
rank=6, ## Latent factor dimension
101+
loss={'name': 'mae'}, ## Use absolute loss
102+
n_users=user_num, ## Number of users
103+
n_items=item_num, ## Number of items
101104
)
102105
clf.fit(X_train, y_train)
103106
@@ -118,19 +121,19 @@ Choosing different `loss functions <./loss.rst>`_ through :code:`loss`:
118121
clf_mse = plqMF_Ridge(
119122
C=0.001,
120123
rank=6,
121-
loss={'name': 'mse'}, ## Choose square loss
124+
loss={'name': 'mse'}, ## Choose square loss
122125
n_users=user_num,
123126
n_items=item_num)
124127
125128
# Hinge loss (suitable for binary data)
126129
clf_hinge = plqMF_Ridge(
127130
C=0.001,
128131
rank=6,
129-
loss={'name': 'hinge'}, ## Choose hinge loss
132+
loss={'name': 'hinge'}, ## Choose hinge loss
130133
n_users=user_num,
131134
n_items=item_num)
132135
133-
`Linear constraints <./constraint.rst>`_ can be applied via :code:`constraint`:
136+
`Linear constraints <./constraint.rst>`_ can be applied via :code:`constraint_user` and :code:`constraint_item`:
134137

135138
.. code-block:: python
136139
@@ -141,7 +144,8 @@ Choosing different `loss functions <./loss.rst>`_ through :code:`loss`:
141144
loss={'name': 'mae'},
142145
n_users=user_num,
143146
n_items=item_num,
144-
constraint=[{'name': '>=0'}] ## Use nonnegative constraint
147+
constraint_user=[{'name': '>=0'}], ## Use nonnegative constraint
148+
constraint_item=[{'name': '>=0'}]
145149
)
146150
147151
The algorithm includes bias terms :math:`\mathbf{\alpha}` and :math:`\mathbf{\beta}` by default. To disable them, that is, :math:`\mathbf{\alpha} = \mathbf{0}` and :math:`\mathbf{\beta} = \mathbf{0}`, set: :code:`biased=False`:
@@ -155,7 +159,7 @@ The algorithm includes bias terms :math:`\mathbf{\alpha}` and :math:`\mathbf{\be
155159
loss={'name': 'mae'},
156160
n_users=user_num,
157161
n_items=item_num,
158-
biased=False ## Disable bias terms
162+
biased=False ## Disable bias terms
159163
)
160164
161165
Imposing different strengths of regularization on items/users through :code:`rho`:
@@ -169,7 +173,7 @@ Imposing different strengths of regularization on items/users through :code:`rho
169173
loss={'name': 'mae'},
170174
n_users=user_num,
171175
n_items=item_num,
172-
rho=0.7 ## Add heavier penalties for user parameters
176+
rho=0.7 ## Add heavier penalties for user parameters
173177
)
174178
175179
Parameter Tuning
@@ -182,7 +186,7 @@ The model complexity is mainly controlled by :code:`C` and :code:`rank`.
182186
183187
for C_value in [0.0002, 0.001, 0.005]:
184188
clf = plqMF_Ridge(
185-
C=C_value, ## Try different regularization strengths
189+
C=C_value, ## Try different regularization strengths
186190
rank=6,
187191
loss={'name': 'mae'},
188192
n_users=user_num,
@@ -197,7 +201,7 @@ The model complexity is mainly controlled by :code:`C` and :code:`rank`.
197201
for rank_value in [4, 8, 12]:
198202
clf = plqMF_Ridge(
199203
C=0.001,
200-
rank=rank_value, ## Try different latent factor dimensions
204+
rank=rank_value, ## Try different latent factor dimensions
201205
loss={'name': 'mae'},
202206
n_users=user_num,
203207
n_items=item_num
@@ -221,4 +225,4 @@ Example
221225
:caption: Empirical Risk Minimization
222226
:name: rst-link-gallery
223227

224-
../examples/MF.ipynb
228+
../examples/NMF.ipynb

doc/source/tutorials/constraint.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,4 @@ Related Examples
4646
:name: rst-link-gallery
4747

4848
../examples/FairSVM.ipynb
49+
../examples/NMF.ipynb

rehline/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from ._sklearn_mixin import plq_Ridge_Classifier, plq_Ridge_Regressor
88
from ._mf_class import plqMF_Ridge
99
from ._data import make_mf_dataset
10+
from ._loss import ReHLoss
1011

1112
__all__ = ("_BaseReHLine",
1213
"ReHLine_solver",

rehline/_mf_class.py

Lines changed: 23 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from sklearn.base import BaseEstimator
77
from sklearn.utils.validation import _check_sample_weight
88
from sklearn.exceptions import ConvergenceWarning
9+
from ._loss import ReHLoss
910
from ._base import (_BaseReHLine, ReHLine_solver,
1011
_make_loss_rehline_param, _make_constraint_rehline_param,
1112
_cast_sample_bias, _cast_sample_weight)
@@ -32,9 +33,9 @@ class plqMF_Ridge(_BaseReHLine, BaseEstimator):
3233
3334
.. math::
3435
\ \text{ s.t. } \
35-
\mathbf{A} \begin{pmatrix} \alpha_u \\ \mathbf{p}_u \end{pmatrix} + \mathbf{b} \geq \mathbf{0},\ u = 1,\dots,n
36+
\mathbf{A}_{\text{user}} \begin{pmatrix} \alpha_u \\ \mathbf{p}_u \end{pmatrix} + \mathbf{b}_{\text{user}} \geq \mathbf{0},\ u = 1,\dots,n
3637
\quad \text{and} \quad
37-
\mathbf{A} \begin{pmatrix} \beta_i \\ \mathbf{q}_i \end{pmatrix} + \mathbf{b} \geq \mathbf{0},\ i = 1,\dots,m
38+
\mathbf{A}_{\text{item}} \begin{pmatrix} \beta_i \\ \mathbf{q}_i \end{pmatrix} + \mathbf{b}_{\text{item}} \geq \mathbf{0},\ i = 1,\dots,m
3839
3940
The function supports various loss functions, including:
4041
- 'hinge', 'svm' or 'SVM'
@@ -58,8 +59,12 @@ class plqMF_Ridge(_BaseReHLine, BaseEstimator):
5859
loss : dict
5960
A dictionary specifying the loss function parameters.
6061
61-
constraint : list of dict
62-
A list of dictionaries, where each dictionary represents a constraint.
62+
constraint_user : list of dict
63+
A list of dictionaries, where each dictionary represents a constraint to user side parameters.
64+
Each dictionary must contain a 'name' key, which specifies the type of constraint.
65+
66+
constraint_item : list of dict
67+
A list of dictionaries, where each dictionary represents a constraint to item side parameters.
6368
Each dictionary must contain a 'name' key, which specifies the type of constraint.
6469
6570
biased : bool, default=True
@@ -156,7 +161,7 @@ class plqMF_Ridge(_BaseReHLine, BaseEstimator):
156161
decision_function(X)
157162
The decision function evaluated on the given dataset.
158163
159-
obj(X, y, loss))
164+
obj(X, y)
160165
Compute the values of loss term and objective function.
161166
162167
Notes
@@ -165,7 +170,8 @@ class plqMF_Ridge(_BaseReHLine, BaseEstimator):
165170
166171
"""
167172

168-
def __init__(self, n_users, n_items, loss, constraint=[], biased=True,
173+
def __init__(self, n_users, n_items, loss, biased=True,
174+
constraint_user=[], constraint_item=[],
169175
rank=10, C=1.0, rho=0.5,
170176
init_mean=0.0, init_sd=0.1, random_state=None,
171177
max_iter=10000, tol=1e-4, shrink=1, trace_freq=100,
@@ -189,7 +195,8 @@ def __init__(self, n_users, n_items, loss, constraint=[], biased=True,
189195
self.n_users = n_users
190196
self.n_items = n_items
191197
self.loss = loss
192-
self.constraint = constraint
198+
self.constraint_user = constraint_user
199+
self.constraint_item = constraint_item
193200
self.biased = biased
194201
## -----------------------------hyper perameters-----------------------------
195202
self.rank = rank
@@ -259,7 +266,7 @@ def fit(self, X, y, sample_weight=None):
259266

260267

261268
# CD algorithm
262-
self.history[0] = self.obj(X, y, loss=self.loss)
269+
self.history[0] = self.obj(X, y)
263270
for l in range(self.max_iter_CD):
264271
## User side update
265272
for user in range(self.n_users):
@@ -286,7 +293,7 @@ def fit(self, X, y, sample_weight=None):
286293
U, V, Tau, S, T = _make_loss_rehline_param(loss=self.loss, X=Q_tmp, y=y_tmp)
287294
U_bias, V_bias, Tau_bias, S_bias, T_bias = _cast_sample_bias(U, V, Tau, S, T, sample_bias=bias_tmp)
288295
U_weight, V_weight, Tau_weight, S_weight, T_weight = _cast_sample_weight(U_bias, V_bias, Tau_bias, S_bias, T_bias, C=C_user, sample_weight=weight_tmp)
289-
A, b = _make_constraint_rehline_param(constraint=self.constraint, X=Q_tmp, y=y_tmp)
296+
A, b = _make_constraint_rehline_param(constraint=self.constraint_user, X=Q_tmp, y=y_tmp)
290297

291298
### solve and update
292299
result_tmp = ReHLine_solver(X=Q_tmp,
@@ -337,7 +344,7 @@ def fit(self, X, y, sample_weight=None):
337344
U, V, Tau, S, T = _make_loss_rehline_param(loss=self.loss, X=P_tmp, y=y_tmp)
338345
U_bias, V_bias, Tau_bias, S_bias, T_bias = _cast_sample_bias(U, V, Tau, S, T, sample_bias=bias_tmp)
339346
U_weight, V_weight, Tau_weight, S_weight, T_weight = _cast_sample_weight(U_bias, V_bias, Tau_bias, S_bias, T_bias, C=C_item, sample_weight=weight_tmp)
340-
A, b = _make_constraint_rehline_param(constraint=self.constraint, X=P_tmp, y=y_tmp)
347+
A, b = _make_constraint_rehline_param(constraint=self.constraint_item, X=P_tmp, y=y_tmp)
341348

342349
### solve and update
343350
result_tmp = ReHLine_solver(X=P_tmp,
@@ -364,7 +371,7 @@ def fit(self, X, y, sample_weight=None):
364371

365372

366373
## Check convergence
367-
self.history[l+1] = self.obj(X, y, loss=self.loss)
374+
self.history[l+1] = self.obj(X, y)
368375
obj_diff = (self.history[l] - self.history[l+1])[1]
369376

370377

@@ -407,7 +414,7 @@ def decision_function(self, X):
407414

408415

409416

410-
def obj(self, X, y, loss):
417+
def obj(self, X, y):
411418
"""
412419
Compute the values of loss term and objective function.
413420
@@ -418,9 +425,6 @@ def obj(self, X, y, loss):
418425
419426
y : array-like of shape (n_ratings,)
420427
Actual rating values.
421-
422-
loss : dict
423-
A dictionary specifying the loss function parameters.
424428
425429
Returns
426430
-------
@@ -441,28 +445,9 @@ def obj(self, X, y, loss):
441445
item_penalty = np.sum(self.Q ** 2) * (1 - self.rho) / self.n_items
442446
penalty = user_penalty + item_penalty
443447

444-
if (loss['name'] == 'mae') \
445-
or (loss['name'] == 'MAE') \
446-
or (loss['name'] == 'mean absolute error'):
447-
loss_term = np.sum( np.abs(self.decision_function(X) - y) )
448-
449-
elif (loss['name'] == 'MSE') \
450-
or (loss['name'] == 'mse') \
451-
or (loss['name'] == 'mean squared error'):
452-
loss_term = np.sum( (self.decision_function(X) - y) ** 2 )
453-
454-
elif (loss['name'] == 'hinge') \
455-
or (loss['name'] == 'svm') \
456-
or (loss['name'] == 'SVM'):
457-
loss_term = np.sum( np.maximum(0, 1 - y * self.decision_function(X)) )
458-
459-
elif (loss['name'] == 'squared hinge') \
460-
or (loss['name'] == 'squared svm') \
461-
or (loss['name'] == 'squared SVM'):
462-
loss_term = np.sum( np.maximum(0, 1 - y * self.decision_function(X)) ** 2 )
463-
464-
else:
465-
raise ValueError(f"Unsupported loss function: {loss['name']}. "
466-
f"Supported losses are: 'mae', 'mse', 'hinge', 'squared hinge'")
448+
y_pred = self.decision_function(X)
449+
U, V, Tau, S, T = _make_loss_rehline_param(loss=self.loss, X=X, y=y)
450+
loss = ReHLoss(U, V, S, T, Tau)
451+
loss_term = loss(y_pred)
467452

468453
return loss_term, self.C * loss_term + penalty

tests/_test_mf.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ def evaluate_single_params(params):
7777
y_pred = model.decision_function(X_val)
7878
y_pred_classes = np.where(y_pred > 0, 1, -1)
7979
accuracy = accuracy_score(y_val, y_pred_classes)
80-
score = model.obj(X_val, y_val, loss=model.loss)[0] / len(y_val)
80+
score = model.obj(X_val, y_val)[0] / len(y_val)
8181
return {'params': param_dict, 'score': score, 'accuracy':accuracy}
8282

8383

@@ -94,11 +94,13 @@ def evaluate_single_params(params):
9494

9595
## Parameters to be selected
9696
param_grid = {
97-
'constraint': [[], [{'name': '>=0'}]],
97+
'constraint_user': [[], [{'name': '>=0'}]],
98+
'constraint_item': [[], [{'name': '>=0'}]],
9899
'biased': [True, False],
99100
'rank': [5, 10],
100101
'C': [0.0006, 0.0002],
101102
'rho': [0.3, 0.6],
103+
'tol': [0.01]
102104
}
103105

104106

0 commit comments

Comments
 (0)