|
6 | 6 | @author: mike_ubuntu |
7 | 7 | """ |
8 | 8 |
|
9 | | -from typing import Union, Callable |
10 | 9 | import numpy as np |
11 | | -from sklearn.linear_model import Lasso, LassoLars, OrthogonalMatchingPursuit |
12 | | -# from pysindy import STLSQ |
13 | | - |
| 10 | +from sklearn.linear_model import Lasso, LassoLars, OrthogonalMatchingPursuit, Ridge, ElasticNet, SGDRegressor |
| 11 | +# from cuml.linear_model import Ridge |
| 12 | +# from pysindy import STLSQ, SR3 |
| 13 | +from scipy.linalg import lstsq |
14 | 14 | import epde.globals as global_var |
15 | 15 | from epde.operators.utils.template import CompoundOperator |
16 | 16 | from epde.structure.main_structures import Equation |
| 17 | +import time |
| 18 | +from sklearn.base import BaseEstimator, RegressorMixin |
| 19 | +from sklearn.utils.validation import check_X_y, check_array, check_is_fitted |
| 20 | + |
| 21 | + |
| 22 | +class CustomPhysicsLasso(BaseEstimator, RegressorMixin): |
| 23 | + def __init__(self, max_iter=100, tol=1e-4): |
| 24 | + self.max_iter = max_iter |
| 25 | + self.tol = tol |
| 26 | + |
| 27 | + def _soft_threshold(self, x, lambda_): |
| 28 | + return np.sign(x) * np.maximum(np.abs(x) - lambda_, 0) |
| 29 | + |
| 30 | + def get_cv(self, weights): |
| 31 | + std = np.array(weights).std(axis=0, ddof=1) |
| 32 | + mu = np.array(weights).mean(axis=0) |
| 33 | + # cv = std ** 2 / (std ** 2 + mu ** 2) |
| 34 | + cv = std ** 2 / (std ** 2 + mu ** 2) |
| 35 | + return cv |
| 36 | + |
| 37 | + def calculate_weights(self, X, y): |
| 38 | + X_aug = np.column_stack([X, np.ones(self.n_samples)]) |
| 39 | + weights = [] |
| 40 | + for _ in range(30): |
| 41 | + idx = np.random.choice(self.n_samples, self.batch_size, replace=False) |
| 42 | + X_batch = X_aug[idx] |
| 43 | + y_batch = y[idx] |
| 44 | + w_full, _, _, _ = np.linalg.lstsq(X_batch, y_batch, rcond=None) |
| 45 | + weights.append(w_full) |
| 46 | + |
| 47 | + return weights |
| 48 | + |
| 49 | + def fit(self, X, y): |
| 50 | + X, y = check_X_y(X, y, dtype=np.float64) |
| 51 | + self.n_samples, self.n_features = X.shape |
| 52 | + self.batch_size = int(self.n_samples * 0.5) # 50% of data |
| 53 | + |
| 54 | + # --- 1. Initialization --- |
| 55 | + # Add column of 1s to solve for intercept correctly via OLS |
| 56 | + weights = self.calculate_weights(X, y) |
| 57 | + cv = self.get_cv(weights) |
| 58 | + |
| 59 | + self.coef_ = np.array(weights).mean(axis=0)[:-1] |
| 60 | + self.intercept_ = np.array(weights).mean(axis=0)[-1] |
| 61 | + |
| 62 | + # Pre-compute norms of features (optimization) |
| 63 | + # These are constant throughout the loop |
| 64 | + norm_sq_features = np.sum(X ** 2, axis=0) |
| 65 | + |
| 66 | + # Pre-compute initial residual: r = y - (Xw + b) |
| 67 | + y_pred = X @ self.coef_ + self.intercept_ |
| 68 | + residual = y - y_pred |
| 69 | + |
| 70 | + max_change_old = 0.0 |
| 71 | + |
| 72 | + # --- 2. Coordinate Descent Loop --- |
| 73 | + for iteration in range(self.max_iter): |
| 74 | + max_change = 0.0 |
| 75 | + |
| 76 | + # A. Update Intercept (Unpenalized) |
| 77 | + # The optimal intercept shift is simply the mean of the residuals |
| 78 | + # because we want mean(y - Xw - b_new) = 0 |
| 79 | + intercept_shift = np.mean(residual) |
| 80 | + self.intercept_ += intercept_shift |
| 81 | + residual -= intercept_shift |
| 82 | + |
| 83 | + # B. Update Coefficients |
| 84 | + for j in range(self.n_features): |
| 85 | + if self.coef_[j] == 0: |
| 86 | + continue |
| 87 | + |
| 88 | + old_coef = self.coef_[j] |
| 89 | + norm_sq = norm_sq_features[j] |
| 90 | + |
| 91 | + # Skip constant columns to avoid division by zero |
| 92 | + # if norm_sq == 0: |
| 93 | + # continue |
| 94 | + |
| 95 | + # 1. Calculate partial residual correlation |
| 96 | + # This represents the correlation between feature j and the target |
| 97 | + # if feature j were removed from the model. |
| 98 | + # rho = dot(X_j, residual + old_coef * X_j) |
| 99 | + rho = np.dot(X[:, j], residual) + old_coef * norm_sq |
| 100 | + |
| 101 | + # 2. Soft Thresholding |
| 102 | + # Threshold is N * alpha |
| 103 | + threshold = cv[j] * sum(y ** 2) |
| 104 | + new_coef = self._soft_threshold(rho, threshold) / norm_sq |
| 105 | + |
| 106 | + # 3. Update State |
| 107 | + self.coef_[j] = new_coef |
| 108 | + # Update residual vector efficiently |
| 109 | + # r_new = r_old - (w_new - w_old) * X_j |
| 110 | + residual -= (new_coef - old_coef) * X[:, j] |
| 111 | + max_change = max(max_change, abs((new_coef - old_coef) / old_coef)) |
| 112 | + |
| 113 | + if abs(max_change - max_change_old) < self.tol: |
| 114 | + break |
| 115 | + |
| 116 | + max_change_old = max_change |
| 117 | + |
| 118 | + self.n_iter_ = iteration + 1 |
| 119 | + # print("-------") |
| 120 | + # print(self.n_iter_) |
| 121 | + # print(np.mean(cv[:-1])) |
| 122 | + # print(sum(abs(y - X @ self.coef_ - self.intercept_)) / sum(abs(y))) |
| 123 | + # print(self.coef_, self.intercept_) |
| 124 | + return self |
| 125 | + |
| 126 | + def predict(self, X): |
| 127 | + check_is_fitted(self) |
| 128 | + X = check_array(X) |
| 129 | + return X @ self.coef_ + self.intercept_ |
17 | 130 |
|
18 | 131 |
|
19 | 132 | class LASSOSparsity(CompoundOperator): |
@@ -61,18 +174,71 @@ def apply(self, objective : Equation, arguments : dict): |
61 | 174 | # print(f'Metaparameter: {objective.metaparameters}, objective.metaparameters[("sparsity", objective.main_var_to_explain)]') |
62 | 175 | self_args, subop_args = self.parse_suboperator_args(arguments = arguments) |
63 | 176 |
|
64 | | - estimator = Lasso(alpha = objective.metaparameters[('sparsity', objective.main_var_to_explain)]['value'], |
65 | | - copy_X=True, fit_intercept=True, max_iter=1000, |
66 | | - positive=False, precompute=False, random_state=None, |
67 | | - selection='random', tol=0.0001, warm_start=False) |
| 177 | + # estimator = Lasso(alpha = objective.metaparameters[('sparsity', objective.main_var_to_explain)]['value'], |
| 178 | + # copy_X=True, fit_intercept=True, max_iter=1000, |
| 179 | + # positive=False, precompute=False, random_state=None, |
| 180 | + # selection='random', tol=0.0001, warm_start=True) |
| 181 | + # estimator = SGDRegressor(alpha=objective.metaparameters[('sparsity', objective.main_var_to_explain)]['value'], |
| 182 | + # penalty='l1', fit_intercept=True, max_iter=1000, |
| 183 | + # random_state=None, tol=0.0001, warm_start=False) |
| 184 | + # estimator = Ridge(alpha=objective.metaparameters[('sparsity', objective.main_var_to_explain)]['value'], |
| 185 | + # copy_X=True, fit_intercept=True, |
| 186 | + # positive=False, random_state=None, |
| 187 | + # tol=0.0001, solver='cholesky') |
| 188 | + estimator = CustomPhysicsLasso() |
| 189 | + # estimator = ElasticNet(alpha=objective.metaparameters[('sparsity', objective.main_var_to_explain)]['value'], |
| 190 | + # l1_ratio=objective.metaparameters[('threshold', objective.main_var_to_explain)]['value'], |
| 191 | + # copy_X=True, fit_intercept=True, max_iter=1000, |
| 192 | + # positive=False, precompute=False, random_state=None, |
| 193 | + # selection='random', tol=0.0001, warm_start=False |
| 194 | + # ) |
| 195 | + # estimator = OrthogonalMatchingPursuit(n_nonzero_coefs=objective.metaparameters[('nonzero_terms', objective.main_var_to_explain)]['value'], fit_intercept=True) |
68 | 196 | # estimator = STLSQ(threshold=objective.metaparameters[('sparsity', objective.main_var_to_explain)]['value'], |
69 | | - # copy_X=True, unbias=True, max_iter=20, alpha=1e-5, ridge_kw={"tol": 1e-10}) |
| 197 | + # copy_X=True, unbias=True, max_iter=20, alpha=objective.metaparameters[('threshold', objective.main_var_to_explain)]['value'], ridge_kw={"tol": 1e-10}) |
| 198 | + # estimator = SR3(reg_weight_lam=objective.metaparameters[('sparsity', objective.main_var_to_explain)]['value'], |
| 199 | + # regularizer='L2', relax_coeff_nu=objective.metaparameters[('nu', objective.main_var_to_explain)]['value'], |
| 200 | + # copy_X=True, unbias=True) |
| 201 | + |
| 202 | + start_time = time.time() # record start time |
70 | 203 | _, target, features = objective.evaluate(normalize = True, return_val = False) |
71 | | - self.g_fun_vals = global_var.grid_cache.g_func.reshape(-1) |
| 204 | + end_time = time.time() # record end time |
| 205 | + elapsed_time = end_time - start_time |
| 206 | + # print(f"Elapsed time for evaluating: {elapsed_time / len(features.reshape(-1)):.12f} seconds") |
| 207 | + |
| 208 | + self.g_fun_vals = global_var.grid_cache.g_func[global_var.grid_cache.g_func != 0] |
| 209 | + |
| 210 | + # fraction = 0.1 |
| 211 | + # num_subsample = int(len(target) * fraction) |
| 212 | + # |
| 213 | + # probabilities = self.g_fun_vals / np.sum(self.g_fun_vals) |
| 214 | + # indices = np.random.choice( |
| 215 | + # a=np.arange(len(target)), |
| 216 | + # size=num_subsample, |
| 217 | + # replace=False, |
| 218 | + # p=probabilities |
| 219 | + # ) |
| 220 | + # |
| 221 | + # features_subsampled = features[indices] |
| 222 | + # target_subsampled = target[indices] |
| 223 | + # weights_subsampled = self.g_fun_vals[indices] |
| 224 | + |
| 225 | + start_time = time.time() # record start time |
| 226 | + # estimator.fit(features, target, sample_weight = self.g_fun_vals) |
| 227 | + end_time = time.time() # record end time |
| 228 | + elapsed_time = end_time - start_time |
| 229 | + # print(f"Elapsed time for fitting: {elapsed_time / len(features.reshape(-1)):.12f} seconds") |
72 | 230 |
|
73 | | - estimator.fit(features, target, sample_weight = self.g_fun_vals) |
| 231 | + # estimator.fit(features_subsampled, target_subsampled, sample_weight=weights_subsampled) |
| 232 | + estimator.fit(features, target) |
74 | 233 | objective.weights_internal = estimator.coef_ |
| 234 | + # print(estimator.coef_) |
| 235 | + # objective.weights_internal = np.where( |
| 236 | + # np.abs(estimator.w) < objective.metaparameters[('threshold', objective.main_var_to_explain)]['value'], |
| 237 | + # 0, estimator.w) |
| 238 | + # objective.weights_internal = estimator.coef_ |
| 239 | + # objective.weights_internal = np.where(np.abs(estimator.coef_) < objective.metaparameters[('threshold', objective.main_var_to_explain)]['value'], 0, estimator.coef_) |
75 | 240 | # objective.weights_internal = estimator.coef_[0] |
| 241 | + # objective.weights_internal = np.where(np.abs(estimator.coef_[0]) < objective.metaparameters[('threshold', objective.main_var_to_explain)]['value'], 0, estimator.coef_[0]) |
76 | 242 | objective.weights_internal_evald = True |
77 | 243 |
|
78 | 244 | def use_default_tags(self): |
|
0 commit comments