12
12
13
13
import numpy as np
14
14
from scipy import linalg
15
+ from scipy .sparse .csr import csr_matrix
16
+ from scipy .sparse .csgraph import connected_components
15
17
16
18
from .empirical_covariance_ import (empirical_covariance , EmpiricalCovariance ,
17
19
log_likelihood )
@@ -190,55 +192,70 @@ def graph_lasso(emp_cov, alpha, cov_init=None, mode='cd', tol=1e-4,
190
192
covariance_ *= 0.95
191
193
diagonal = emp_cov .flat [::n_features + 1 ]
192
194
covariance_ .flat [::n_features + 1 ] = diagonal
195
+ # Fit each connected component of the thresholded sample covariance;
196
+ # see http://faculty.washington.edu/dwitten/Papers/jcgs.2011.pdf
197
+ cov_thresh = csr_matrix (np .abs (emp_cov ) > alpha ) # workaround for scipy#3819
198
+ n_comp , comp_labels = connected_components (cov_thresh )
199
+ covariance_ [np .not_equal .outer (comp_labels , comp_labels )] = 0.0
193
200
precision_ = linalg .pinvh (covariance_ )
194
201
195
- indices = np .arange (n_features )
196
202
costs = list ()
197
203
# The different l1 regression solver have different numerical errors
198
204
if mode == 'cd' :
199
205
errors = dict (over = 'raise' , invalid = 'ignore' )
200
206
else :
201
207
errors = dict (invalid = 'raise' )
202
208
try :
209
+ cost_by_comp = np .inf * np .ones (n_comp )
203
210
# be robust to the max_iter=0 edge case, see:
204
211
# https://github.com/scikit-learn/scikit-learn/issues/4134
205
- d_gap = np .inf
212
+ d_gap_by_comp = np .inf * np . ones ( n_comp )
206
213
for i in range (max_iter ):
207
- for idx in range (n_features ):
208
- sub_covariance = np .ascontiguousarray (
209
- covariance_ [indices != idx ].T [indices != idx ])
210
- row = emp_cov [idx , indices != idx ]
211
- with np .errstate (** errors ):
212
- if mode == 'cd' :
213
- # Use coordinate descent
214
- coefs = - (precision_ [indices != idx , idx ]
215
- / (precision_ [idx , idx ] + 1000 * eps ))
216
- coefs , _ , _ , _ = cd_fast .enet_coordinate_descent_gram (
217
- coefs , alpha , 0 , sub_covariance , row , row ,
218
- max_iter , enet_tol , check_random_state (None ), False )
219
- else :
220
- # Use LARS
221
- _ , _ , coefs = lars_path (
222
- sub_covariance , row , Xy = row , Gram = sub_covariance ,
223
- alpha_min = alpha / (n_features - 1 ), copy_Gram = True ,
224
- method = 'lars' , return_path = False )
225
- # Update the precision matrix
226
- precision_ [idx , idx ] = (
227
- 1. / (covariance_ [idx , idx ]
228
- - np .dot (covariance_ [indices != idx , idx ], coefs )))
229
- precision_ [indices != idx , idx ] = (- precision_ [idx , idx ]
230
- * coefs )
231
- precision_ [idx , indices != idx ] = (- precision_ [idx , idx ]
232
- * coefs )
233
- coefs = np .dot (sub_covariance , coefs )
234
- covariance_ [idx , indices != idx ] = coefs
235
- covariance_ [indices != idx , idx ] = coefs
236
- d_gap = _dual_gap (emp_cov , precision_ , alpha )
237
- cost = _objective (emp_cov , precision_ , alpha )
214
+ for comp_idx in range (n_comp ):
215
+ indices = np .where (comp_labels == comp_idx )[0 ]
216
+ comp_size = len (indices )
217
+ # closed-form solution for disconnected node
218
+ if comp_size == 1 :
219
+ covariance_ [indices , indices ] = emp_cov [indices , indices ]
220
+ precision_ [indices , indices ] = 1. / emp_cov [indices , indices ]
221
+ else :
222
+ for idx in indices :
223
+ other_inds = indices [indices != idx ]
224
+ sub_covariance = np .ascontiguousarray (
225
+ covariance_ [other_inds ].T [other_inds ])
226
+ row = emp_cov [idx , other_inds ]
227
+ with np .errstate (** errors ):
228
+ if mode == 'cd' :
229
+ # Use coordinate descent
230
+ coefs = - (precision_ [other_inds , idx ] /
231
+ (precision_ [idx , idx ] + 1000 * eps ))
232
+ coefs , _ , _ , _ = cd_fast .enet_coordinate_descent_gram (
233
+ coefs , alpha , 0 , sub_covariance , row , row ,
234
+ max_iter , enet_tol , check_random_state (None ), False )
235
+ else :
236
+ # Use LARS
237
+ _ , _ , coefs = lars_path (
238
+ sub_covariance , row , Xy = row , Gram = sub_covariance ,
239
+ alpha_min = alpha / (comp_size - 1 ), copy_Gram = True ,
240
+ method = 'lars' , return_path = False )
241
+ # Update the precision matrix
242
+ precision_ [idx , idx ] = (
243
+ 1. / (covariance_ [idx , idx ]
244
+ - np .dot (covariance_ [other_inds , idx ], coefs )))
245
+ precision_ [other_inds , idx ] = - precision_ [idx , idx ] * coefs
246
+ precision_ [idx , other_inds ] = - precision_ [idx , idx ] * coefs
247
+ coefs = np .dot (sub_covariance , coefs )
248
+ covariance_ [idx , other_inds ] = coefs
249
+ covariance_ [other_inds , idx ] = coefs
250
+ cost_by_comp [comp_idx ] = _objective (emp_cov [indices ].T [indices ],
251
+ precision_ [indices ].T [indices ], alpha )
252
+ d_gap_by_comp [comp_idx ] = _dual_gap (emp_cov [indices ].T [indices ],
253
+ precision_ [indices ].T [indices ], alpha )
254
+ cost = cost_by_comp .sum ()
255
+ d_gap = d_gap_by_comp .sum ()
238
256
if verbose :
239
- print (
240
- '[graph_lasso] Iteration % 3i, cost % 3.2e, dual gap %.3e'
241
- % (i , cost , d_gap ))
257
+ print ('[graph_lasso] Iteration % 3i, cost % 3.2e, dual gap %.3e'
258
+ % (i , cost , d_gap ))
242
259
if return_costs :
243
260
costs .append ((cost , d_gap ))
244
261
if np .abs (d_gap ) < tol :
@@ -248,7 +265,7 @@ def graph_lasso(emp_cov, alpha, cov_init=None, mode='cd', tol=1e-4,
248
265
'too ill-conditioned for this solver' )
249
266
else :
250
267
warnings .warn ('graph_lasso: did not converge after %i iteration:'
251
- ' dual gap: %.3e' % (max_iter , d_gap ),
268
+ ' dual gap: %.3e' % (max_iter , d_gap_by_comp . sum () ),
252
269
ConvergenceWarning )
253
270
except FloatingPointError as e :
254
271
e .args = (e .args [0 ]
0 commit comments