11from __future__ import annotations
22
33from typing import Optional , Tuple , Union
4+ import warnings
45
56import numpy as np
67from numpy .typing import NDArray
@@ -55,9 +56,9 @@ class SoftImpute(BaseEstimator, TransformerMixin):
5556 def __init__ (
5657 self ,
5758 period : int = 1 ,
58- rank : int = 2 ,
59+ rank : Optional [ int ] = None ,
5960 tolerance : float = 1e-05 ,
60- tau : float = 0 ,
61+ tau : Optional [ float ] = None ,
6162 max_iterations : int = 100 ,
6263 random_state : Union [None , int , np .random .RandomState ] = None ,
6364 verbose : bool = False ,
@@ -70,86 +71,38 @@ def __init__(
7071 self .random_state = sku .check_random_state (random_state )
7172 self .verbose = verbose
7273
73- # def decompose (self, X: NDArray, Omega: NDArray) -> Tuple[NDArray, NDArray] :
74- # """
75- # Compute the Soft Impute decomposition
74+ def get_params_scale (self , X : NDArray ) :
75+ """
76+ Get parameters for scaling in Soft Impute based on the input data.
7677
77- # Parameters
78- # ----------
79- # D : NDArray
80- # Matrix of the observations
81- # Omega: NDArray
82- # Matrix of missingness, with boolean data
78+ Parameters
79+ ----------
80+ X : np.ndarray
81+ Input data matrix of shape (m, n).
8382
84- # Returns
85- # -------
86- # M: NDArray
87- # Low-rank signal
88- # A: NDArray
89- # Anomalies
90- # """
91- # print()
92- # print()
93- # print(X.shape)
94- # print()
95- # X = utils.linear_interpolation(X)
96-
97- # n, m = X.shape
98- # V = np.zeros((m, self.rank))
99- # U = self.random_state.normal(0.0, 1.0, (n, self.rank))
100- # U, _, _ = np.linalg.svd(U, full_matrices=False)
101- # D2 = np.ones((self.rank, 1))
102- # col_means = np.nanmean(X, axis=0)
103- # np.copyto(X, col_means, where=~Omega)
104- # if self.rank is None:
105- # self.rank = rpca_utils.approx_rank(X)
106- # for iter_ in range(self.max_iterations):
107- # U_old = U
108- # V_old = V
109- # D2_old = D2
110-
111- # BDt = U.T @ X
112- # if self.tau > 0:
113- # BDt *= D2 / (D2**2 + self.tau)
114- # Vtilde, D2tilde, Rt = np.linalg.svd(BDt.T, full_matrices=False)
115- # V = Vtilde
116- # D2 = D2tilde.reshape(-1, 1)
117- # U = U @ Rt
118- # X_hat = U @ (D2 * V.T)
119- # X[~Omega] = X_hat[~Omega]
120-
121- # A = (X @ V).T
122- # if self.tau > 0:
123- # A *= D2 / (D2 + self.tau)
124- # Lsvd = np.linalg.svd(A.T, full_matrices=False)
125- # U = Lsvd[0]
126- # D2 = Lsvd[1][:, np.newaxis]
127- # V = V @ Lsvd[2]
128- # X_hat = U @ (D2 * V.T)
129- # X[~Omega] = X_hat[~Omega]
130-
131- # ratio = self._check_convergence(U_old, D2_old, V_old, U, D2, V)
132- # if self.verbose:
133- # print(f"iter {iter_}: ratio = {round(ratio, 4)}")
134- # if ratio < self.tolerance:
135- # break
136-
137- # u = U[:, : self.rank]
138- # d = D2[: self.rank]
139- # v = V[:, : self.rank]
140-
141- # M = u @ np.diag(d.T[0]) @ (v).T
142- # A = X - M
143-
144- # return M, A
83+ Returns
84+ -------
85+ dict
86+ A dictionary containing the following parameters:
87+ - "rank" : float
88+ Rank estimate for low-rank matrix decomposition.
89+ - "tau" : float
90+ Parameter for the nuclear norm penality
91+
92+ """
93+ X = utils .linear_interpolation (X )
94+ rank = rpca_utils .approx_rank (X )
95+ tau = 1 / np .sqrt (np .max (X .shape ))
96+ dict_params = {"rank" : rank , "tau" : tau }
97+ return dict_params
14598
14699 def decompose (self , X : NDArray , Omega : NDArray ) -> Tuple [NDArray , NDArray ]:
147100 """
148101 Compute the Soft Impute decomposition
149102
150103 Parameters
151104 ----------
152- D : NDArray
105+ X : NDArray
153106 Matrix of the observations
154107 Omega: NDArray
155108 Matrix of missingness, with boolean data
@@ -161,29 +114,29 @@ def decompose(self, X: NDArray, Omega: NDArray) -> Tuple[NDArray, NDArray]:
161114 A: NDArray
162115 Anomalies
163116 """
164- assert self .tau > 0
165- if self .rank is None :
166- self .rank = rpca_utils . approx_rank ( X )
167- # X = utils.linear_interpolation(X)
117+ params_scale = self .get_params_scale ( X )
118+ rank = params_scale [ "rank" ] if self .rank is None else self . rank
119+ tau = params_scale [ "tau" ] if self .tau is None else self . tau
120+ assert tau > 0
168121
169122 # Step 1 : Initializing
170123 n , m = X .shape
171- V = np .zeros ((m , self . rank ))
172- U = self .random_state .normal (0.0 , 1.0 , (n , self . rank ))
124+ V = np .zeros ((m , rank ))
125+ U = self .random_state .normal (0.0 , 1.0 , (n , rank ))
173126 U , _ , _ = np .linalg .svd (U , full_matrices = False )
174- D = np .ones ((1 , self .rank ))
175- # col_means = np.nanmean(X, axis=0)
176- # np.copyto(X, col_means, where=~Omega)
127+ D = np .ones ((1 , rank ))
177128
178129 A = U * D
179130 B = V * D
131+ M = A @ B .T
132+ cost_start = self .cost_function (X , M , A , Omega , tau )
180133 for iter_ in range (self .max_iterations ):
181134 U_old = U
182135 V_old = V
183136 D_old = D
184137
185138 # Step 2 : Upate on B
186- D2_invreg = (D ** 2 + self . tau ) ** (- 1 )
139+ D2_invreg = (D ** 2 + tau ) ** (- 1 )
187140 Btilde = ((U * D ).T @ np .where (Omega , X - A @ B .T , 0 ) + (B * D ** 2 ).T ).T
188141 Btilde = Btilde * D2_invreg
189142
@@ -193,8 +146,8 @@ def decompose(self, X: NDArray, Omega: NDArray) -> Tuple[NDArray, NDArray]:
193146 B = V * D
194147
195148 # Step 3 : Upate on A
196- D2_invreg = (D ** 2 + self . tau ) ** (- 1 )
197- Atilde = ((V * D ).T @ np .where (Omega , X . T - B @ A .T , 0 ) + (A * D ** 2 ).T ).T
149+ D2_invreg = (D ** 2 + tau ) ** (- 1 )
150+ Atilde = ((V * D ).T @ np .where (Omega , X - A @ B .T , 0 ). T + (A * D ** 2 ).T ).T
198151 Atilde = Atilde * D2_invreg
199152
200153 Utilde , D2tilde , _ = np .linalg .svd (Atilde * D , full_matrices = False )
@@ -213,85 +166,19 @@ def decompose(self, X: NDArray, Omega: NDArray) -> Tuple[NDArray, NDArray]:
213166 Xstar = np .where (Omega , X - A @ B .T , 0 ) + A @ B .T
214167 M = Xstar @ V
215168 U , D , Rt = np .linalg .svd (M , full_matrices = False )
216- D = rpca_utils .soft_thresholding (D , self . tau )
169+ D = rpca_utils .soft_thresholding (D , tau )
217170 M = (U * D ) @ Rt @ V .T
218171
219172 A = np .where (Omega , X - M , 0 )
220173
221- return M , A
222-
223- # def fit(self, D: NDArray, y=None) -> SoftImpute:
224- # """Fit the imputer on D.
174+ cost_end = self .cost_function (X , M , A , Omega , tau )
175+ if self .verbose and (cost_end > cost_start + 1e-9 ):
176+ warnings .warn (
177+ f"Convergence failed: cost function increased from"
178+ f" { cost_start } to { cost_end } instead of decreasing!" .format ("%.2f" )
179+ )
225180
226- # Parameters
227- # ----------
228- # D : NDArray
229- # Input data
230-
231- # y : Ignored
232- # Not used, present here for API consistency by convention.
233-
234- # Returns
235- # -------
236- # self : object
237- # The fitted `SoftImpute` class instance.
238- # """
239- # D = D.copy()
240- # D = utils.prepare_data(D, self.period)
241-
242- # if not isinstance(D, np.ndarray):
243- # raise AssertionError("Invalid type. D must be a NDArray.")
244-
245- # n, m = D.shape
246- # mask = np.isnan(D)
247- # V = np.zeros((m, self.rank))
248- # U = self.random_state.normal(0.0, 1.0, (n, self.rank))
249- # U, _, _ = np.linalg.svd(U, full_matrices=False)
250- # Dsq = np.ones((self.rank, 1))
251- # col_means = np.nanmean(D, axis=0)
252- # np.copyto(D, col_means, where=np.isnan(D))
253- # if self.rank is None:
254- # self.rank = rpca_utils.approx_rank(D)
255- # for iter_ in range(self.max_iterations):
256- # U_old = U
257- # V_old = V
258- # Dsq_old = Dsq
259-
260- # Q = U.T @ D
261- # if self.tau > 0:
262- # tmp = Dsq / (Dsq + self.tau)
263- # Q = Q * tmp
264- # Bsvd = np.linalg.svd(Q.T, full_matrices=False)
265- # V = Bsvd[0]
266- # Dsq = Bsvd[1][:, np.newaxis]
267- # U = U @ Bsvd[2]
268- # tmp = Dsq * V.T
269- # D_hat = U @ tmp
270- # D[mask] = D_hat[mask]
271-
272- # L = (D @ V).T
273- # if self.tau > 0:
274- # tmp = Dsq / (Dsq + self.tau)
275- # L = L * tmp
276- # Lsvd = np.linalg.svd(L.T, full_matrices=False)
277- # U = Lsvd[0]
278- # Dsq = Lsvd[1][:, np.newaxis]
279- # V = V @ Lsvd[2]
280- # tmp = Dsq * V.T
281- # D_hat = U @ tmp
282- # D[mask] = D_hat[mask]
283-
284- # ratio = self._check_convergence(U_old, Dsq_old, V_old, U, Dsq, V)
285- # if self.verbose:
286- # print(f"iter {iter_}: ratio = {round(ratio, 4)}")
287- # if ratio < self.tolerance:
288- # break
289-
290- # self.u = U[:, : self.rank]
291- # self.d = Dsq[: self.rank]
292- # self.v = V[:, : self.rank]
293-
294- # return self
181+ return M , A
295182
296183 def _check_convergence (
297184 self ,
@@ -362,3 +249,36 @@ def _check_convergence(
362249 # raise AssertionError("Result contains NaN. This is a bug.")
363250
364251 # return D_transformed
252+
253+ @staticmethod
254+ def cost_function (
255+ X : NDArray ,
256+ M : NDArray ,
257+ A : NDArray ,
258+ Omega : NDArray ,
259+ tau : float ,
260+ ):
261+ """
262+ Compute cost function for different RPCA algorithm
263+
264+ Parameters
265+ ----------
266+ X : NDArray
267+ Matrix of observations
268+ M : NDArray
269+ Low-rank signal
270+ A : NDArray
271+ Anomalies
272+ Omega : NDArray
273+ Mask for observations
274+ tau: Optional[float]
275+ penalizing parameter for the nuclear norm
276+
277+ Returns
278+ -------
279+ float
280+ Value of the cost function minimized by the Soft Impute algorithm
281+ """
282+ norm_frobenius = np .sum (np .where (Omega , X - M , 0 ) ** 2 )
283+ norm_nuclear = np .linalg .norm (M , "nuc" )
284+ return norm_frobenius + tau * norm_nuclear
0 commit comments