Skip to content

Commit cfdb3fd

Browse files
committed
Change graph_lasso to use block diagonal structure
The block diagonal components of the graphical lasso solution can be identified by thresholding the same covariance matrix; the solution can then be found by solving a subproblem corresponding to each component, which can be much faster if the graph is very sparse. See http://faculty.washington.edu/dwitten/Papers/jcgs.2011.pdf for details.
1 parent 90cfb48 commit cfdb3fd

File tree

1 file changed

+54
-37
lines changed

1 file changed

+54
-37
lines changed

sklearn/covariance/graph_lasso_.py

+54-37
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

1313
import numpy as np
1414
from scipy import linalg
15+
from scipy.sparse.csr import csr_matrix
16+
from scipy.sparse.csgraph import connected_components
1517

1618
from .empirical_covariance_ import (empirical_covariance, EmpiricalCovariance,
1719
log_likelihood)
@@ -190,55 +192,70 @@ def graph_lasso(emp_cov, alpha, cov_init=None, mode='cd', tol=1e-4,
190192
covariance_ *= 0.95
191193
diagonal = emp_cov.flat[::n_features + 1]
192194
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
193200
precision_ = linalg.pinvh(covariance_)
194201

195-
indices = np.arange(n_features)
196202
costs = list()
197203
# The different l1 regression solver have different numerical errors
198204
if mode == 'cd':
199205
errors = dict(over='raise', invalid='ignore')
200206
else:
201207
errors = dict(invalid='raise')
202208
try:
209+
cost_by_comp = np.inf * np.ones(n_comp)
203210
# be robust to the max_iter=0 edge case, see:
204211
# 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)
206213
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()
238256
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))
242259
if return_costs:
243260
costs.append((cost, d_gap))
244261
if np.abs(d_gap) < tol:
@@ -248,7 +265,7 @@ def graph_lasso(emp_cov, alpha, cov_init=None, mode='cd', tol=1e-4,
248265
'too ill-conditioned for this solver')
249266
else:
250267
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()),
252269
ConvergenceWarning)
253270
except FloatingPointError as e:
254271
e.args = (e.args[0]

0 commit comments

Comments
 (0)