Skip to content

Commit 014bb8d

Browse files
authored
Merge pull request ITMO-NSS-team#70 from Gromwud/main
Regularizer optimization
2 parents f649353 + 2aeefac commit 014bb8d

1 file changed

Lines changed: 40 additions & 10 deletions

File tree

epde/operators/common/sparsity.py

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -226,11 +226,19 @@ def get_cv(self, weights):
226226
mu = weights_arr.mean(axis=0)
227227

228228
with np.errstate(divide='ignore', invalid='ignore'):
229-
cv = (std ** 2) / (mu ** 2)
230-
cv[mu == 0] = 0.0
229+
cv = (std ** 2) / (mu ** 2 + std ** 2)
231230

232231
return np.nan_to_num(cv)
233232

233+
# def get_cv(self, weights):
234+
# weights_arr = np.asarray(weights)
235+
# q1, q3 = np.percentile(weights_arr, [25, 75], axis=0)
236+
# spread = (q3 - q1) / 1.349 # IQR/1.349 ≈ σ for Gaussian
237+
# center = np.median(weights_arr, axis=0)
238+
# with np.errstate(divide='ignore', invalid='ignore'):
239+
# cv = spread ** 2 / (center ** 2 + spread ** 2)
240+
# return np.nan_to_num(cv)
241+
234242
def fit(self, X, y, sample_weights=None):
235243
n_samples, n_features = X.shape
236244

@@ -244,8 +252,7 @@ def fit(self, X, y, sample_weights=None):
244252

245253
# Precompute static operations for speed
246254
norm_sq_features = np.sum(X_aug ** 2, axis=0)
247-
X_T_y = X_aug.T @ y
248-
max_corr = np.max(np.abs(X_T_y)) # Global max correlation anchors the penalty
255+
X_T_y = X_aug.T @ y # Cached once; slice by active_mask each outer iter.
249256

250257
outer_iteration = 0
251258
max_outer_iters = total_features # Max possible eliminations
@@ -267,27 +274,36 @@ def fit(self, X, y, sample_weights=None):
267274
grid_shape=self.grid_shape,
268275
fit_intercept=intercept_is_active
269276
)
270-
self.cached_weights_ = weights
277+
278+
# Slice data for the CD run
279+
X_active = X_aug[:, active_mask]
280+
norm_sq_active = norm_sq_features[active_mask]
281+
282+
# Anchor the penalty to the max correlation on the SURVIVING subspace
283+
# so threshold scale tracks the current problem as features drop.
284+
max_corr = np.max(np.abs(X_T_y[active_mask]))
271285

272286
# 3. CV performs as adaptive alpha
273287
active_cv = self.get_cv(weights)
288+
# Tackle the most physically unstable feature first so unstable
289+
# terms get shrunk to zero before they pollute the residual.
290+
cv_order = np.argsort(active_cv)[::-1]
274291
active_thresholds = active_cv * max_corr
275292

276-
# Initialize coefficients and slice data for the CD run
293+
# Initialize coefficients
277294
active_coef = weights.mean(axis=0)
278-
X_active = X_aug[:, active_mask]
279-
norm_sq_active = norm_sq_features[active_mask]
280295

281296
residual = y - (X_active @ active_coef)
282297

283298
# =================================================================
284299
# INNER LOOP: Pure Coordinate Descent on the Stabilized Library
285300
# =================================================================
286301
cd_iteration = 0
302+
killed_feature = False
287303
while cd_iteration < self.max_iter:
288304
max_change = 0.0
289305

290-
for j in range(len(active_coef)):
306+
for j in cv_order:
291307
old_coef = active_coef[j]
292308
norm_sq = norm_sq_active[j]
293309

@@ -301,13 +317,23 @@ def fit(self, X, y, sample_weights=None):
301317
residual -= (new_coef - old_coef) * X_active[:, j]
302318
active_coef[j] = new_coef
303319

320+
if new_coef == 0 and old_coef != 0:
321+
# A feature just died — hand control back to the outer
322+
# loop so CVs/anchor/thresholds get recomputed on the
323+
# smaller library before doing any more CD work.
324+
killed_feature = True
325+
break
326+
304327
with np.errstate(divide='ignore', invalid='ignore'):
305328
change = abs(new_coef - old_coef)
306329
if old_coef != 0:
307330
change /= abs(old_coef)
308331
if change > max_change:
309332
max_change = change
310333

334+
if killed_feature:
335+
break
336+
311337
# Inner loop convergence check
312338
if max_change <= self.tol:
313339
# You can add your Dual Gap check here if desired,
@@ -334,10 +360,14 @@ def fit(self, X, y, sample_weights=None):
334360
active_mask = new_active_mask
335361
outer_iteration += 1
336362

337-
# Emergency break if everything died
363+
# Emergency break if everything died. `weights` still references
364+
# the prior (now-stale) mask, so drop it instead of caching.
338365
if not np.any(active_mask):
366+
weights = None
339367
break
340368

369+
self.cached_weights_ = weights
370+
341371
# Map back to standard sklearn attributes
342372
self.coef_ = self.full_coef_[:-1]
343373
self.intercept_ = self.full_coef_[-1]

0 commit comments

Comments
 (0)