|
7 | 7 | """ |
8 | 8 |
|
9 | 9 | import numpy as np |
10 | | -from sklearn.linear_model import LinearRegression, Ridge |
| 10 | +from sklearn.linear_model import LinearRegression |
11 | 11 |
|
12 | 12 | import epde.globals as global_var |
13 | 13 | from epde.operators.utils.template import CompoundOperator |
14 | 14 | from epde.structure.main_structures import Equation |
15 | 15 |
|
| 16 | + |
| 17 | +# Marker attribute set by ``LASSOSparsity.apply`` on the equation |
| 18 | +# instance to indicate that the legacy LASSO post-processing refit |
| 19 | +# (LinearRegression on the LASSO survivors, un-normalised features) |
| 20 | +# should run on this equation. ``VWSRSparsity`` does NOT set this |
| 21 | +# marker; its PhysicsInformedLasso output is already on the physical |
| 22 | +# scale and would be corrupted by the refit. Gating on the equation |
| 23 | +# (rather than on the operator) keeps the strategy wiring untouched. |
| 24 | +LEGACY_REFIT_MARKER = '_legacy_refit_pending' |
| 25 | + |
| 26 | + |
16 | 27 | class LinRegBasedCoeffsEquation(CompoundOperator): |
17 | 28 | ''' |
18 | | - |
19 | | - The operatror, dedicated to the calculation of the weights of the equation (for the free coefficient and |
20 | | - each of its terms except the target one). |
21 | | -
|
22 | | - Attributes: |
23 | | - _tags (`set`): |
24 | | - g_fun_vals (`numpy.ndarray`): |
25 | | - |
26 | | - Methods: |
27 | | - apply(equation) |
28 | | - Calculate the coefficients of the equation, using the linear regression. The result is stored in the |
29 | | - equation.weights_final attribute |
| 29 | + Refit the LASSO survivors with ``LinearRegression`` on |
| 30 | + *un-normalised* features, replacing ``weights_final`` with |
| 31 | + physically-scaled coefficients. |
| 32 | +
|
| 33 | + Restores the legacy two-step pipeline: |
| 34 | + 1. LASSOSparsity fits Lasso on min-max-normalised features and |
| 35 | + identifies the surviving (non-zero) terms. |
| 36 | + 2. This operator re-fits those survivors with ordinary least |
| 37 | + squares on the un-normalised features to recover physical |
| 38 | + coefficient magnitudes (LASSO coefficients are biased by |
| 39 | + both L1 shrinkage and the upstream normalisation). |
| 40 | +
|
| 41 | + Gated by the per-equation marker ``LEGACY_REFIT_MARKER`` that |
| 42 | + ``LASSOSparsity`` sets and ``VWSRSparsity`` leaves unset, so this |
| 43 | + operator can be wired into both pipelines without a strategy flag. |
| 44 | +
|
| 45 | + Output shape matches the upstream sparsity convention: |
| 46 | + ``np.append(coef_, intercept)`` -- one entry per surviving non-zero |
| 47 | + feature plus a trailing intercept slot. Downstream consumers |
| 48 | + (``L2Fitness.apply``, ``L2LRFitness.apply``) need no change. |
30 | 49 | ''' |
31 | 50 | key = 'LinRegCoeffCalc' |
32 | | - |
| 51 | + |
| 52 | + @staticmethod |
| 53 | + def _legacy_evaluate_nonzero(objective: Equation): |
| 54 | + """Build target + un-normalised feature matrix from the LASSO |
| 55 | + survivors, independent of ``Equation.evaluate``. |
| 56 | +
|
| 57 | + Mirrors the pre-aaea0f4 legacy feature builder: iterate the |
| 58 | + structure, skip the target, emit columns only for terms whose |
| 59 | + ``weights_internal`` slot is non-zero. Returns |
| 60 | + ``(target, features)`` with ``features=None`` when every |
| 61 | + non-target slot was filtered to zero. |
| 62 | + """ |
| 63 | + target = objective.structure[objective.target_idx].evaluate(False) |
| 64 | + feats = [] |
| 65 | + for term_idx, term in enumerate(objective.structure): |
| 66 | + if term_idx == objective.target_idx: |
| 67 | + continue |
| 68 | + wi_pos = (term_idx if term_idx < objective.target_idx |
| 69 | + else term_idx - 1) |
| 70 | + if objective.weights_internal[wi_pos] != 0: |
| 71 | + feats.append(term.evaluate(False)) |
| 72 | + if not feats: |
| 73 | + return target, None |
| 74 | + features = np.vstack(feats) |
| 75 | + if features.ndim == 1: |
| 76 | + features = np.expand_dims(features, 1).T |
| 77 | + features = np.transpose(features) |
| 78 | + return target, features |
| 79 | + |
33 | 80 | def apply(self, objective : Equation, arguments : dict = None): |
| 81 | + """Refit LASSO survivors with un-normalised LinearRegression. |
| 82 | +
|
| 83 | + Skipped unless ``LASSOSparsity`` set ``LEGACY_REFIT_MARKER`` on |
| 84 | + the equation. The marker is cleared after the refit so a stale |
| 85 | + marker from a previous run doesn't double-trigger work. |
34 | 86 | """ |
35 | | - Calculate the coefficients of the equation, using the linear regression.The result is stored in the |
36 | | - objective.weights_final attribute |
37 | | -
|
38 | | - Args: |
39 | | - objective (`Equation`): the equation object, to that the fitness function is obtained. |
40 | | - arguments (`dict`): |
41 | | - |
42 | | - Returns: |
43 | | - None |
44 | | - """ |
45 | | - # self_args, subop_args = self.parse_suboperator_args(arguments = arguments) |
46 | | - |
47 | | - assert objective.weights_internal_evald, 'Trying to calculate final weights before evaluating intermeidate ones (no sparsity).' |
48 | | - # target = objective.structure[objective.target_idx] |
49 | | - # |
50 | | - # target_vals = target.evaluate(False) |
51 | | - # features_vals = [] |
52 | | - # nonzero_features_indexes = [] |
53 | | - # for i in range(len(objective.structure)): |
54 | | - # if i == objective.target_idx: |
55 | | - # continue |
56 | | - # idx = i if i < objective.target_idx else i-1 |
57 | | - # if objective.weights_internal[idx] != 0: |
58 | | - # features_vals.append(objective.structure[i].evaluate(False)) |
59 | | - # nonzero_features_indexes.append(idx) |
60 | | - # |
61 | | - # if len(features_vals) == 0: |
62 | | - # objective.weights_final = np.zeros(len(objective.structure)) |
63 | | - # else: |
64 | | - # features = features_vals[0] |
65 | | - # if len(features_vals) > 1: |
66 | | - # for i in range(1, len(features_vals)): |
67 | | - # features = np.vstack([features, features_vals[i]]) |
68 | | - # features = np.vstack([features, np.ones(features_vals[0].shape)]) # Добавляем константную фичу |
69 | | - # features = np.transpose(features) |
70 | | - # estimator = LinearRegression(copy_X=True, fit_intercept=False, n_jobs=-1, |
71 | | - # positive=False, tol=0.0001) |
72 | | - # # estimator = LinearRegression(fit_intercept=False) |
73 | | - # if features.ndim == 1: |
74 | | - # features = features.reshape(-1, 1) |
75 | | - # try: |
76 | | - # self.g_fun_vals = global_var.grid_cache.g_func[global_var.grid_cache.g_func != 0] |
77 | | - # except AttributeError: |
78 | | - # self.g_fun_vals = None |
79 | | - # estimator.fit(features, target_vals, sample_weight = self.g_fun_vals) |
80 | | - # |
81 | | - # valuable_weights = estimator.coef_ |
82 | | - # weights = np.zeros(len(objective.structure)) |
83 | | - # for weight_idx in range(len(weights)-1): |
84 | | - # if weight_idx in nonzero_features_indexes: |
85 | | - # weights[weight_idx] = valuable_weights[nonzero_features_indexes.index(weight_idx)] |
86 | | - # weights[-1] = valuable_weights[-1] |
87 | | - # objective.weights_final = weights |
88 | | - # _, target, features = objective.evaluate(normalize=False, return_val=False) |
89 | | - # self.g_fun_vals = global_var.grid_cache.g_func[global_var.grid_cache.g_func != 0] |
90 | | - # estimator = LinearRegression(copy_X=True, fit_intercept=True, n_jobs=-1, positive=False, tol=0.0001) |
91 | | - # estimator.fit(features, target, sample_weight=self.g_fun_vals) |
92 | | - # valuable_weights = estimator.coef_ |
93 | | - # objective.weights_final = np.append(valuable_weights, estimator.intercept_) |
94 | | - # objective.weights_final_evald = True |
95 | | - |
| 87 | + assert objective.weights_internal_evald, ( |
| 88 | + 'Trying to calculate final weights before evaluating ' |
| 89 | + 'intermediate ones (no sparsity).' |
| 90 | + ) |
| 91 | + if not getattr(objective, LEGACY_REFIT_MARKER, False): |
| 92 | + return |
| 93 | + |
| 94 | + target, features = self._legacy_evaluate_nonzero(objective) |
| 95 | + if features is None: |
| 96 | + # No non-zero terms to refit -- leave ``weights_final`` as |
| 97 | + # set by the upstream sparsity step (just the intercept). |
| 98 | + objective.weights_final_evald = True |
| 99 | + setattr(objective, LEGACY_REFIT_MARKER, False) |
| 100 | + return |
| 101 | + |
| 102 | + self.g_fun_vals = global_var.grid_cache.g_func[global_var.grid_cache.g_func_mask] |
| 103 | + estimator = LinearRegression(copy_X=True, fit_intercept=True, n_jobs=-1) |
| 104 | + estimator.fit(features, target, sample_weight=self.g_fun_vals) |
| 105 | + objective.weights_final = np.append(estimator.coef_, estimator.intercept_) |
| 106 | + objective.weights_final_evald = True |
| 107 | + setattr(objective, LEGACY_REFIT_MARKER, False) |
| 108 | + |
96 | 109 | def use_default_tags(self): |
97 | 110 | self._tags = {'coefficient calculation', 'gene level', 'no suboperators', 'inplace'} |
0 commit comments