|
| 1 | +""" |
| 2 | +======================================================================= |
| 3 | +Regularization paths for the Graphical Lasso and its Adaptive variation |
| 4 | +======================================================================= |
| 5 | +This example demonstrates how non-convex penalties in the Adaptive Graphical Lasso |
| 6 | +can achieve superior sparsity recovery compared to the standard L1 penalty. |
| 7 | +
|
| 8 | +The Adaptive Graphical Lasso uses iterative reweighting to approximate non-convex |
| 9 | +penalties, following Candès et al. (2007). Non-convex penalties often produce |
| 10 | +better sparsity patterns by more aggressively shrinking small coefficients while |
| 11 | +preserving large ones. |
| 12 | +
|
| 13 | +We compare three approaches: |
| 14 | + - **L1**: Standard Graphical Lasso with L1 penalty |
| 15 | + - **Log**: Adaptive approach with logarithmic penalty |
| 16 | + - **L0.5**: Adaptive approach with L0.5 penalty |
| 17 | +
|
| 18 | +The plots show normalized mean square error (NMSE) for reconstruction accuracy |
| 19 | +and F1 score for sparsity pattern recovery across different regularization levels. |
| 20 | +""" |
| 21 | + |
| 22 | +# Authors: Can Pouliquen |
| 23 | +# Mathurin Massias |
| 24 | +# Florian Kozikowski |
| 25 | + |
| 26 | +import numpy as np |
| 27 | +from numpy.linalg import norm |
| 28 | +import matplotlib.pyplot as plt |
| 29 | +from sklearn.metrics import f1_score |
| 30 | + |
| 31 | +from skglm.covariance import GraphicalLasso, AdaptiveGraphicalLasso |
| 32 | +from skglm.penalties.separable import LogSumPenalty, L0_5 |
| 33 | +from skglm.utils.data import make_dummy_covariance_data |
| 34 | + |
| 35 | +# %% |
| 36 | +# Generate synthetic sparse precision matrix data |
| 37 | +# =============================================== |
| 38 | + |
| 39 | +p = 100 |
| 40 | +n = 1000 |
| 41 | +S, _, Theta_true, alpha_max = make_dummy_covariance_data(n, p) |
| 42 | +alphas = alpha_max*np.geomspace(1, 1e-4, num=10) |
| 43 | + |
| 44 | +# %% |
| 45 | +# Setup models with different penalty functions |
| 46 | +# ============================================ |
| 47 | + |
| 48 | +penalties = ["L1", "Log", "L0.5"] |
| 49 | +n_reweights = 5 # Number of adaptive reweighting iterations |
| 50 | +models_tol = 1e-4 |
| 51 | + |
| 52 | +models = [ |
| 53 | + # Standard Graphical Lasso with L1 penalty |
| 54 | + GraphicalLasso(algo="primal", warm_start=True, tol=models_tol), |
| 55 | + |
| 56 | + # Adaptive Graphical Lasso with logarithmic penalty |
| 57 | + AdaptiveGraphicalLasso(warm_start=True, |
| 58 | + penalty=LogSumPenalty(alpha=1.0, eps=1e-10), |
| 59 | + n_reweights=n_reweights, |
| 60 | + tol=models_tol), |
| 61 | + |
| 62 | + # Adaptive Graphical Lasso with L0.5 penalty |
| 63 | + AdaptiveGraphicalLasso(warm_start=True, |
| 64 | + penalty=L0_5(alpha=1.0), |
| 65 | + n_reweights=n_reweights, |
| 66 | + tol=models_tol), |
| 67 | +] |
| 68 | + |
| 69 | +# %% |
| 70 | +# Compute regularization paths |
| 71 | +# ============================ |
| 72 | + |
| 73 | +nmse_results = {penalty: [] for penalty in penalties} |
| 74 | +f1_results = {penalty: [] for penalty in penalties} |
| 75 | + |
| 76 | + |
| 77 | +# Fit models across regularization path |
| 78 | +for i, (penalty, model) in enumerate(zip(penalties, models)): |
| 79 | + print(f"Fitting {penalty} penalty across {len(alphas)} regularization values...") |
| 80 | + for alpha_idx, alpha in enumerate(alphas): |
| 81 | + print( |
| 82 | + f" alpha {alpha_idx+1}/{len(alphas)}: " |
| 83 | + f"lambda/lambda_max = {alpha/alpha_max:.1e}", |
| 84 | + end="") |
| 85 | + |
| 86 | + model.alpha = alpha |
| 87 | + model.fit(S, mode='precomputed') |
| 88 | + |
| 89 | + Theta_est = model.precision_ |
| 90 | + nmse = norm(Theta_est - Theta_true)**2 / norm(Theta_true)**2 |
| 91 | + f1_val = f1_score(Theta_est.flatten() != 0., Theta_true.flatten() != 0.) |
| 92 | + |
| 93 | + nmse_results[penalty].append(nmse) |
| 94 | + f1_results[penalty].append(f1_val) |
| 95 | + |
| 96 | + print(f"NMSE: {nmse:.3f}, F1: {f1_val:.3f}") |
| 97 | + print(f"{penalty} penalty complete!\n") |
| 98 | + |
| 99 | + |
| 100 | +# %% |
| 101 | +# Plot results |
| 102 | +# ============ |
| 103 | +fig, axarr = plt.subplots(2, 1, sharex=True, figsize=([6.11, 3.91]), |
| 104 | + layout="constrained") |
| 105 | +cmap = plt.get_cmap("tab10") |
| 106 | +for i, penalty in enumerate(penalties): |
| 107 | + |
| 108 | + for j, ax in enumerate(axarr): |
| 109 | + |
| 110 | + if j == 0: |
| 111 | + metric = nmse_results |
| 112 | + best_idx = np.argmin(metric[penalty]) |
| 113 | + ystop = np.min(metric[penalty]) |
| 114 | + else: |
| 115 | + metric = f1_results |
| 116 | + best_idx = np.argmax(metric[penalty]) |
| 117 | + ystop = np.max(metric[penalty]) |
| 118 | + |
| 119 | + ax.semilogx(alphas/alpha_max, |
| 120 | + metric[penalty], |
| 121 | + color=cmap(i), |
| 122 | + linewidth=2., |
| 123 | + label=penalty) |
| 124 | + |
| 125 | + ax.vlines( |
| 126 | + x=alphas[best_idx] / alphas[0], |
| 127 | + ymin=0, |
| 128 | + ymax=ystop, |
| 129 | + linestyle='--', |
| 130 | + color=cmap(i)) |
| 131 | + line = ax.plot( |
| 132 | + [alphas[best_idx] / alphas[0]], |
| 133 | + 0, |
| 134 | + clip_on=False, |
| 135 | + marker='X', |
| 136 | + color=cmap(i), |
| 137 | + markersize=12) |
| 138 | + |
| 139 | + ax.grid(which='both', alpha=0.9) |
| 140 | + |
| 141 | +axarr[0].legend(fontsize=14) |
| 142 | +axarr[0].set_title(f"{p=},{n=}", fontsize=18) |
| 143 | +axarr[0].set_ylabel("NMSE", fontsize=18) |
| 144 | +axarr[1].set_ylabel("F1 score", fontsize=18) |
| 145 | +_ = axarr[1].set_xlabel(r"$\lambda / \lambda_\mathrm{{max}}$", fontsize=18) |
| 146 | +# %% |
| 147 | +# Results summary |
| 148 | +# =============== |
| 149 | + |
| 150 | +print("Performance at optimal regularization:") |
| 151 | +print("-" * 50) |
| 152 | + |
| 153 | +for penalty in penalties: |
| 154 | + best_nmse = min(nmse_results[penalty]) |
| 155 | + best_f1 = max(f1_results[penalty]) |
| 156 | + print(f"{penalty:>4}: NMSE = {best_nmse:.3f}, F1 = {best_f1:.3f}") |
| 157 | + |
| 158 | +# %% [markdown] |
| 159 | +# |
| 160 | +# **Metrics explanation:** |
| 161 | +# |
| 162 | +# * **NMSE (Normalized Mean Square Error)**: Measures reconstruction accuracy |
| 163 | +# of the precision matrix. Lower values = better reconstruction. |
| 164 | +# * **F1 Score**: Measures sparsity pattern recovery (correctly identifying |
| 165 | +# which entries are zero/non-zero). Higher values = better sparsity. |
| 166 | +# |
| 167 | +# **Key finding**: Non-convex penalties achieve significantly |
| 168 | +# better sparsity recovery (F1 score) while maintaining |
| 169 | +# competitive reconstruction accuracy (NMSE). |
0 commit comments