oikan 0.0.3.1__py3-none-any.whl → 0.0.3.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- oikan/exceptions.py +25 -1
- oikan/model.py +217 -53
- oikan/utils.py +33 -14
- {oikan-0.0.3.1.dist-info → oikan-0.0.3.3.dist-info}/METADATA +18 -10
- oikan-0.0.3.3.dist-info/RECORD +10 -0
- {oikan-0.0.3.1.dist-info → oikan-0.0.3.3.dist-info}/WHEEL +1 -1
- oikan/symbolic.py +0 -55
- oikan-0.0.3.1.dist-info/RECORD +0 -11
- {oikan-0.0.3.1.dist-info → oikan-0.0.3.3.dist-info}/licenses/LICENSE +0 -0
- {oikan-0.0.3.1.dist-info → oikan-0.0.3.3.dist-info}/top_level.txt +0 -0
oikan/exceptions.py
CHANGED
@@ -4,4 +4,28 @@ class OIKANError(Exception):
|
|
4
4
|
|
5
5
|
class ModelNotFittedError(OIKANError):
|
6
6
|
"""Raised when a method requires a fitted model."""
|
7
|
-
pass
|
7
|
+
pass
|
8
|
+
|
9
|
+
class InvalidParameterError(OIKANError):
|
10
|
+
"""Raised when an invalid parameter value is provided."""
|
11
|
+
pass
|
12
|
+
|
13
|
+
class DataDimensionError(OIKANError):
|
14
|
+
"""Raised when input data has incorrect dimensions."""
|
15
|
+
pass
|
16
|
+
|
17
|
+
class NumericalInstabilityError(OIKANError):
|
18
|
+
"""Raised when numerical computations become unstable."""
|
19
|
+
pass
|
20
|
+
|
21
|
+
class FeatureExtractionError(OIKANError):
|
22
|
+
"""Raised when feature extraction or transformation fails."""
|
23
|
+
pass
|
24
|
+
|
25
|
+
class ModelSerializationError(OIKANError):
|
26
|
+
"""Raised when model saving/loading operations fail."""
|
27
|
+
pass
|
28
|
+
|
29
|
+
class ConvergenceError(OIKANError):
|
30
|
+
"""Raised when the model fails to converge during training."""
|
31
|
+
pass
|
oikan/model.py
CHANGED
@@ -3,11 +3,15 @@ import torch
|
|
3
3
|
import torch.nn as nn
|
4
4
|
import torch.optim as optim
|
5
5
|
from sklearn.preprocessing import PolynomialFeatures
|
6
|
-
from sklearn.linear_model import
|
6
|
+
from sklearn.linear_model import ElasticNet
|
7
7
|
from abc import ABC, abstractmethod
|
8
8
|
import json
|
9
9
|
from .neural import TabularNet
|
10
10
|
from .utils import evaluate_basis_functions, get_features_involved
|
11
|
+
from sklearn.model_selection import train_test_split
|
12
|
+
from sklearn.metrics import r2_score, accuracy_score
|
13
|
+
from .exceptions import *
|
14
|
+
import sys
|
11
15
|
|
12
16
|
class OIKAN(ABC):
|
13
17
|
"""
|
@@ -18,7 +22,7 @@ class OIKAN(ABC):
|
|
18
22
|
hidden_sizes : list, optional (default=[64, 64])
|
19
23
|
List of hidden layer sizes for the neural network.
|
20
24
|
activation : str, optional (default='relu')
|
21
|
-
Activation function for the neural network ('relu'
|
25
|
+
Activation function for the neural network ('relu', 'tanh', 'leaky_relu', 'elu', 'swish', 'gelu').
|
22
26
|
augmentation_factor : int, optional (default=10)
|
23
27
|
Number of augmented samples per original sample.
|
24
28
|
polynomial_degree : int, optional (default=2)
|
@@ -27,6 +31,8 @@ class OIKAN(ABC):
|
|
27
31
|
L1 regularization strength for Lasso in symbolic regression.
|
28
32
|
sigma : float, optional (default=0.1)
|
29
33
|
Standard deviation of Gaussian noise for data augmentation.
|
34
|
+
top_k : int, optional (default=5)
|
35
|
+
Number of top features to select in hierarchical symbolic regression.
|
30
36
|
epochs : int, optional (default=100)
|
31
37
|
Number of epochs for neural network training.
|
32
38
|
lr : float, optional (default=0.001)
|
@@ -35,10 +41,33 @@ class OIKAN(ABC):
|
|
35
41
|
Batch size for neural network training.
|
36
42
|
verbose : bool, optional (default=False)
|
37
43
|
Whether to display training progress.
|
44
|
+
evaluate_nn : bool, optional (default=False)
|
45
|
+
Whether to evaluate neural network performance before full training.
|
38
46
|
"""
|
39
47
|
def __init__(self, hidden_sizes=[64, 64], activation='relu', augmentation_factor=10,
|
40
48
|
polynomial_degree=2, alpha=0.1, sigma=0.1, epochs=100, lr=0.001, batch_size=32,
|
41
|
-
verbose=False):
|
49
|
+
verbose=False, evaluate_nn=False, top_k=5):
|
50
|
+
if not isinstance(hidden_sizes, list) or not all(isinstance(x, int) and x > 0 for x in hidden_sizes):
|
51
|
+
raise InvalidParameterError("hidden_sizes must be a list of positive integers")
|
52
|
+
if activation not in ['relu', 'tanh', 'leaky_relu', 'elu', 'swish', 'gelu']:
|
53
|
+
raise InvalidParameterError(f"Unsupported activation function: {activation}")
|
54
|
+
if not isinstance(augmentation_factor, int) or augmentation_factor < 1:
|
55
|
+
raise InvalidParameterError("augmentation_factor must be a positive integer")
|
56
|
+
if not isinstance(polynomial_degree, int) or polynomial_degree < 1:
|
57
|
+
raise InvalidParameterError("polynomial_degree must be a positive integer")
|
58
|
+
if not isinstance(top_k, int) or top_k < 1:
|
59
|
+
raise InvalidParameterError("top_k must be a positive integer")
|
60
|
+
if not 0 < lr < 1:
|
61
|
+
raise InvalidParameterError("Learning rate must be between 0 and 1")
|
62
|
+
if not isinstance(batch_size, int) or batch_size < 1:
|
63
|
+
raise InvalidParameterError("batch_size must be a positive integer")
|
64
|
+
if not isinstance(epochs, int) or epochs < 1:
|
65
|
+
raise InvalidParameterError("epochs must be a positive integer")
|
66
|
+
if not 0 <= alpha <= 1:
|
67
|
+
raise InvalidParameterError("alpha must be between 0 and 1")
|
68
|
+
if sigma <= 0:
|
69
|
+
raise InvalidParameterError("sigma must be positive")
|
70
|
+
|
42
71
|
self.hidden_sizes = hidden_sizes
|
43
72
|
self.activation = activation
|
44
73
|
self.augmentation_factor = augmentation_factor
|
@@ -49,8 +78,11 @@ class OIKAN(ABC):
|
|
49
78
|
self.lr = lr
|
50
79
|
self.batch_size = batch_size
|
51
80
|
self.verbose = verbose
|
81
|
+
self.evaluate_nn = evaluate_nn
|
82
|
+
self.top_k = top_k
|
52
83
|
self.neural_net = None
|
53
84
|
self.symbolic_model = None
|
85
|
+
self.evaluation_done = False
|
54
86
|
|
55
87
|
@abstractmethod
|
56
88
|
def fit(self, X, y):
|
@@ -61,19 +93,19 @@ class OIKAN(ABC):
|
|
61
93
|
pass
|
62
94
|
|
63
95
|
def get_formula(self):
|
64
|
-
"""Returns the symbolic formula(s) as a string or list of strings."""
|
96
|
+
"""Returns the symbolic formula(s) as a string (regression) or list of strings (classification)."""
|
65
97
|
if self.symbolic_model is None:
|
66
98
|
raise ValueError("Model not fitted yet.")
|
67
99
|
basis_functions = self.symbolic_model['basis_functions']
|
68
100
|
if 'coefficients' in self.symbolic_model:
|
69
101
|
coefficients = self.symbolic_model['coefficients']
|
70
|
-
formula = " + ".join([f"{coefficients[i]:.
|
102
|
+
formula = " + ".join([f"{coefficients[i]:.5f}*{basis_functions[i]}"
|
71
103
|
for i in range(len(coefficients)) if coefficients[i] != 0])
|
72
104
|
return formula if formula else "0"
|
73
105
|
else:
|
74
106
|
formulas = []
|
75
107
|
for c, coef in enumerate(self.symbolic_model['coefficients_list']):
|
76
|
-
formula = " + ".join([f"{coef[i]:.
|
108
|
+
formula = " + ".join([f"{coef[i]:.5f}*{basis_functions[i]}"
|
77
109
|
for i in range(len(coef)) if coef[i] != 0])
|
78
110
|
formulas.append(f"Class {self.classes_[c]}: {formula if formula else '0'}")
|
79
111
|
return formulas
|
@@ -122,27 +154,33 @@ class OIKAN(ABC):
|
|
122
154
|
File path to save the model. Should end with .json
|
123
155
|
"""
|
124
156
|
if self.symbolic_model is None:
|
125
|
-
raise
|
126
|
-
|
157
|
+
raise ModelNotFittedError("Model must be fitted before saving")
|
158
|
+
|
127
159
|
if not path.endswith('.json'):
|
128
160
|
path = path + '.json'
|
129
|
-
|
130
|
-
# Convert numpy arrays and other non-serializable types to lists
|
131
|
-
model_data = {
|
132
|
-
'n_features': self.symbolic_model['n_features'],
|
133
|
-
'degree': self.symbolic_model['degree'],
|
134
|
-
'basis_functions': self.symbolic_model['basis_functions']
|
135
|
-
}
|
136
161
|
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
162
|
+
try:
|
163
|
+
# Convert numpy arrays and other non-serializable types to lists
|
164
|
+
model_data = {
|
165
|
+
'n_features': self.symbolic_model['n_features'],
|
166
|
+
'degree': self.symbolic_model['degree'],
|
167
|
+
'basis_functions': self.symbolic_model['basis_functions']
|
168
|
+
}
|
169
|
+
|
170
|
+
if 'coefficients' in self.symbolic_model:
|
171
|
+
model_data['coefficients'] = self.symbolic_model['coefficients']
|
172
|
+
else:
|
173
|
+
model_data['coefficients_list'] = [coef for coef in self.symbolic_model['coefficients_list']]
|
174
|
+
if hasattr(self, 'classes_'):
|
175
|
+
model_data['classes'] = self.classes_.tolist()
|
176
|
+
|
177
|
+
with open(path, 'w') as f:
|
178
|
+
json.dump(model_data, f, indent=2)
|
179
|
+
except Exception as e:
|
180
|
+
raise ModelSerializationError(f"Failed to save model: {str(e)}")
|
143
181
|
|
144
|
-
|
145
|
-
|
182
|
+
if self.verbose:
|
183
|
+
print(f"Model saved to {path}")
|
146
184
|
|
147
185
|
def load(self, path):
|
148
186
|
"""
|
@@ -155,27 +193,76 @@ class OIKAN(ABC):
|
|
155
193
|
"""
|
156
194
|
if not path.endswith('.json'):
|
157
195
|
path = path + '.json'
|
196
|
+
|
197
|
+
try:
|
198
|
+
with open(path, 'r') as f:
|
199
|
+
model_data = json.load(f)
|
200
|
+
|
201
|
+
self.symbolic_model = {
|
202
|
+
'n_features': model_data['n_features'],
|
203
|
+
'degree': model_data['degree'],
|
204
|
+
'basis_functions': model_data['basis_functions']
|
205
|
+
}
|
158
206
|
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
207
|
+
if 'coefficients' in model_data:
|
208
|
+
self.symbolic_model['coefficients'] = model_data['coefficients']
|
209
|
+
else:
|
210
|
+
self.symbolic_model['coefficients_list'] = model_data['coefficients_list']
|
211
|
+
if 'classes' in model_data:
|
212
|
+
self.classes_ = np.array(model_data['classes'])
|
213
|
+
except Exception as e:
|
214
|
+
raise ModelSerializationError(f"Failed to load model: {str(e)}")
|
167
215
|
|
168
|
-
if
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
216
|
+
if self.verbose:
|
217
|
+
print(f"Model loaded from {path}")
|
218
|
+
|
219
|
+
def _evaluate_neural_net(self, X, y, output_size, loss_fn):
|
220
|
+
"""Evaluates neural network performance on train-test split."""
|
221
|
+
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
|
222
|
+
|
223
|
+
input_size = X.shape[1]
|
224
|
+
self.neural_net = TabularNet(input_size, self.hidden_sizes, output_size, self.activation)
|
225
|
+
optimizer = optim.Adam(self.neural_net.parameters(), lr=self.lr)
|
226
|
+
|
227
|
+
# Train on the training set
|
228
|
+
self._train_neural_net(X_train, y_train, output_size, loss_fn)
|
229
|
+
|
230
|
+
# Evaluate on test set
|
231
|
+
self.neural_net.eval()
|
232
|
+
with torch.no_grad():
|
233
|
+
y_pred = self.neural_net(torch.tensor(X_test, dtype=torch.float32))
|
234
|
+
if output_size == 1: # Regression
|
235
|
+
y_pred = y_pred.numpy()
|
236
|
+
score = r2_score(y_test, y_pred)
|
237
|
+
metric_name = "R² Score"
|
238
|
+
else: # Classification
|
239
|
+
y_pred = torch.argmax(y_pred, dim=1).numpy()
|
240
|
+
y_test = torch.argmax(y_test, dim=1).numpy()
|
241
|
+
score = accuracy_score(y_test, y_pred)
|
242
|
+
metric_name = "Accuracy"
|
243
|
+
|
244
|
+
print(f"\nNeural Network Evaluation:")
|
245
|
+
print(f"Train size: {len(X_train)}, Test size: {len(X_test)}")
|
246
|
+
print(f"{metric_name}: {score:.4f}")
|
247
|
+
|
248
|
+
# Ask user for confirmation
|
249
|
+
response = input("\nProceed with full training and symbolic regression? [Y/n]: ").lower()
|
250
|
+
if response not in ['y', 'yes']:
|
251
|
+
sys.exit("Training cancelled by user.")
|
252
|
+
|
253
|
+
# Retrain on full dataset
|
254
|
+
self._train_neural_net(X, y, output_size, loss_fn)
|
174
255
|
|
175
256
|
def _train_neural_net(self, X, y, output_size, loss_fn):
|
176
257
|
"""Trains the neural network on the input data."""
|
258
|
+
if self.evaluate_nn and not self.evaluation_done:
|
259
|
+
self.evaluation_done = True
|
260
|
+
self._evaluate_neural_net(X, y, output_size, loss_fn)
|
261
|
+
return
|
262
|
+
|
177
263
|
input_size = X.shape[1]
|
178
|
-
self.neural_net
|
264
|
+
if self.neural_net is None:
|
265
|
+
self.neural_net = TabularNet(input_size, self.hidden_sizes, output_size, self.activation)
|
179
266
|
optimizer = optim.Adam(self.neural_net.parameters(), lr=self.lr)
|
180
267
|
dataset = torch.utils.data.TensorDataset(torch.tensor(X, dtype=torch.float32),
|
181
268
|
torch.tensor(y, dtype=torch.float32))
|
@@ -203,7 +290,6 @@ class OIKAN(ABC):
|
|
203
290
|
|
204
291
|
def _generate_augmented_data(self, X):
|
205
292
|
"""Generates augmented data by adding Gaussian noise."""
|
206
|
-
n_samples = X.shape[0]
|
207
293
|
X_aug = []
|
208
294
|
for _ in range(self.augmentation_factor):
|
209
295
|
noise = np.random.normal(0, self.sigma, X.shape)
|
@@ -212,32 +298,102 @@ class OIKAN(ABC):
|
|
212
298
|
return np.vstack(X_aug)
|
213
299
|
|
214
300
|
def _perform_symbolic_regression(self, X, y):
|
215
|
-
"""
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
301
|
+
"""
|
302
|
+
Performs hierarchical symbolic regression using a two-stage approach.
|
303
|
+
|
304
|
+
Parameters:
|
305
|
+
-----------
|
306
|
+
X : array-like of shape (n_samples, n_features)
|
307
|
+
Input data.
|
308
|
+
y : array-like of shape (n_samples,) or (n_samples, n_classes)
|
309
|
+
Target values or logits.
|
310
|
+
"""
|
311
|
+
n_features = X.shape[1]
|
312
|
+
self.top_k = min(self.top_k, n_features)
|
313
|
+
|
314
|
+
if self.top_k < 1:
|
315
|
+
raise InvalidParameterError("top_k must be at least 1")
|
316
|
+
|
317
|
+
if np.any(np.isnan(X)) or np.any(np.isnan(y)):
|
318
|
+
raise NumericalInstabilityError("Input data contains NaN values")
|
319
|
+
|
320
|
+
if np.any(np.isinf(X)) or np.any(np.isinf(y)):
|
321
|
+
raise NumericalInstabilityError("Input data contains infinite values")
|
322
|
+
|
323
|
+
# Stage 1: Coarse Model
|
324
|
+
coarse_degree = 2 # Fixed low degree for coarse model
|
325
|
+
poly_coarse = PolynomialFeatures(degree=coarse_degree, include_bias=True)
|
326
|
+
X_poly_coarse = poly_coarse.fit_transform(X)
|
327
|
+
model_coarse = ElasticNet(alpha=self.alpha, fit_intercept=False)
|
328
|
+
model_coarse.fit(X_poly_coarse, y)
|
329
|
+
|
330
|
+
# Compute feature importances for original features
|
331
|
+
basis_functions_coarse = poly_coarse.get_feature_names_out()
|
220
332
|
if len(y.shape) == 1 or y.shape[1] == 1:
|
221
|
-
|
222
|
-
|
333
|
+
coef_coarse = model_coarse.coef_.flatten()
|
334
|
+
else:
|
335
|
+
coef_coarse = np.sum(np.abs(model_coarse.coef_), axis=0)
|
336
|
+
|
337
|
+
importances = np.zeros(X.shape[1])
|
338
|
+
for i, func in enumerate(basis_functions_coarse):
|
339
|
+
features_involved = get_features_involved(func)
|
340
|
+
for idx in features_involved:
|
341
|
+
importances[idx] += np.abs(coef_coarse[i])
|
342
|
+
|
343
|
+
if np.all(importances == 0):
|
344
|
+
raise FeatureExtractionError("Failed to compute feature importances - all values are zero")
|
345
|
+
|
346
|
+
# Select top K features
|
347
|
+
top_k_indices = np.argsort(importances)[::-1][:self.top_k]
|
348
|
+
|
349
|
+
# Stage 2: Refined Model
|
350
|
+
# ~ generate additional non-linear features for top K features
|
351
|
+
additional_features = []
|
352
|
+
additional_names = []
|
353
|
+
for i in top_k_indices:
|
354
|
+
# Higher-degree polynomial
|
355
|
+
additional_features.append(X[:, i]**3)
|
356
|
+
additional_names.append(f'x{i}^3')
|
357
|
+
# Non-linear transformations
|
358
|
+
additional_features.append(np.log1p(np.abs(X[:, i])))
|
359
|
+
additional_names.append(f'log1p_x{i}')
|
360
|
+
additional_features.append(np.exp(np.clip(X[:, i], -10, 10)))
|
361
|
+
additional_names.append(f'exp_x{i}')
|
362
|
+
additional_features.append(np.sin(X[:, i]))
|
363
|
+
additional_names.append(f'sin_x{i}')
|
364
|
+
|
365
|
+
# Combine features
|
366
|
+
X_additional = np.column_stack(additional_features)
|
367
|
+
X_refined = np.hstack([X_poly_coarse, X_additional])
|
368
|
+
basis_functions_refined = list(basis_functions_coarse) + additional_names
|
369
|
+
|
370
|
+
# Fit refined model
|
371
|
+
model_refined = ElasticNet(alpha=self.alpha, fit_intercept=False)
|
372
|
+
model_refined.fit(X_refined, y)
|
373
|
+
|
374
|
+
# Store symbolic model
|
375
|
+
if len(y.shape) == 1 or y.shape[1] == 1:
|
376
|
+
# Regression
|
377
|
+
coef_refined = model_refined.coef_.flatten()
|
378
|
+
selected_indices = np.where(np.abs(coef_refined) > 1e-6)[0]
|
223
379
|
self.symbolic_model = {
|
224
380
|
'n_features': X.shape[1],
|
225
|
-
'degree': self.polynomial_degree,
|
226
|
-
'basis_functions':
|
227
|
-
'coefficients':
|
381
|
+
'degree': self.polynomial_degree,
|
382
|
+
'basis_functions': [basis_functions_refined[i] for i in selected_indices],
|
383
|
+
'coefficients': coef_refined[selected_indices].tolist()
|
228
384
|
}
|
229
385
|
else:
|
386
|
+
# Classification
|
230
387
|
coefficients_list = []
|
231
|
-
# Note: Using the same basis functions across classes for simplicity
|
232
388
|
selected_indices = set()
|
233
389
|
for c in range(y.shape[1]):
|
234
|
-
coef =
|
390
|
+
coef = model_refined.coef_[c]
|
235
391
|
indices = np.where(np.abs(coef) > 1e-6)[0]
|
236
392
|
selected_indices.update(indices)
|
237
393
|
selected_indices = list(selected_indices)
|
238
|
-
basis_functions =
|
394
|
+
basis_functions = [basis_functions_refined[i] for i in selected_indices]
|
239
395
|
for c in range(y.shape[1]):
|
240
|
-
coef =
|
396
|
+
coef = model_refined.coef_[c]
|
241
397
|
coef_selected = coef[selected_indices].tolist()
|
242
398
|
coefficients_list.append(coef_selected)
|
243
399
|
self.symbolic_model = {
|
@@ -263,10 +419,14 @@ class OIKANRegressor(OIKAN):
|
|
263
419
|
X = np.asarray(X)
|
264
420
|
y = np.asarray(y).reshape(-1, 1)
|
265
421
|
self._train_neural_net(X, y, output_size=1, loss_fn=nn.MSELoss())
|
422
|
+
if self.verbose:
|
423
|
+
print(f"Original data: features shape: {X.shape} | target shape: {y.shape}")
|
266
424
|
X_aug = self._generate_augmented_data(X)
|
267
425
|
self.neural_net.eval()
|
268
426
|
with torch.no_grad():
|
269
427
|
y_aug = self.neural_net(torch.tensor(X_aug, dtype=torch.float32)).detach().numpy()
|
428
|
+
if self.verbose:
|
429
|
+
print(f"Augmented data: features shape: {X_aug.shape} | target shape: {y_aug.shape}")
|
270
430
|
self._perform_symbolic_regression(X_aug, y_aug)
|
271
431
|
|
272
432
|
def predict(self, X):
|
@@ -311,10 +471,14 @@ class OIKANClassifier(OIKAN):
|
|
311
471
|
n_classes = len(self.classes_)
|
312
472
|
y_onehot = nn.functional.one_hot(torch.tensor(y_encoded), num_classes=n_classes).float()
|
313
473
|
self._train_neural_net(X, y_onehot, output_size=n_classes, loss_fn=nn.CrossEntropyLoss())
|
474
|
+
if self.verbose:
|
475
|
+
print(f"Original data: features shape: {X.shape} | target shape: {y.shape}")
|
314
476
|
X_aug = self._generate_augmented_data(X)
|
315
477
|
self.neural_net.eval()
|
316
478
|
with torch.no_grad():
|
317
479
|
logits_aug = self.neural_net(torch.tensor(X_aug, dtype=torch.float32)).detach().numpy()
|
480
|
+
if self.verbose:
|
481
|
+
print(f"Augmented data: features shape: {X_aug.shape} | target shape: {logits_aug.shape}")
|
318
482
|
self._perform_symbolic_regression(X_aug, logits_aug)
|
319
483
|
|
320
484
|
def predict(self, X):
|
oikan/utils.py
CHANGED
@@ -9,7 +9,7 @@ def evaluate_basis_functions(X, basis_functions, n_features):
|
|
9
9
|
X : array-like of shape (n_samples, n_features)
|
10
10
|
Input data.
|
11
11
|
basis_functions : list
|
12
|
-
List of basis function strings (e.g., '1', 'x0', 'x0^2', 'x0 x1').
|
12
|
+
List of basis function strings (e.g., '1', 'x0', 'x0^2', 'x0 x1', 'log1p_x0').
|
13
13
|
n_features : int
|
14
14
|
Number of input features.
|
15
15
|
|
@@ -22,15 +22,26 @@ def evaluate_basis_functions(X, basis_functions, n_features):
|
|
22
22
|
for i, func in enumerate(basis_functions):
|
23
23
|
if func == '1':
|
24
24
|
X_transformed[:, i] = 1
|
25
|
+
elif func.startswith('log1p_x'):
|
26
|
+
idx = int(func.split('_')[1][1:])
|
27
|
+
X_transformed[:, i] = np.log1p(np.abs(X[:, idx]))
|
28
|
+
elif func.startswith('exp_x'):
|
29
|
+
idx = int(func.split('_')[1][1:])
|
30
|
+
X_transformed[:, i] = np.exp(np.clip(X[:, idx], -10, 10))
|
31
|
+
elif func.startswith('sin_x'):
|
32
|
+
idx = int(func.split('_')[1][1:])
|
33
|
+
X_transformed[:, i] = np.sin(X[:, idx])
|
25
34
|
elif '^' in func:
|
26
35
|
var, power = func.split('^')
|
27
36
|
idx = int(var[1:])
|
28
37
|
X_transformed[:, i] = X[:, idx] ** int(power)
|
29
38
|
elif ' ' in func:
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
39
|
+
vars = func.split(' ')
|
40
|
+
result = np.ones(X.shape[0])
|
41
|
+
for var in vars:
|
42
|
+
idx = int(var[1:])
|
43
|
+
result *= X[:, idx]
|
44
|
+
X_transformed[:, i] = result
|
34
45
|
else:
|
35
46
|
idx = int(func[1:])
|
36
47
|
X_transformed[:, i] = X[:, idx]
|
@@ -43,21 +54,29 @@ def get_features_involved(basis_function):
|
|
43
54
|
Parameters:
|
44
55
|
-----------
|
45
56
|
basis_function : str
|
46
|
-
String representation of the basis function, e.g., 'x0', 'x0^2', 'x0 x1'.
|
57
|
+
String representation of the basis function, e.g., 'x0', 'x0^2', 'x0 x1', 'log1p_x0'.
|
47
58
|
|
48
59
|
Returns:
|
49
60
|
--------
|
50
61
|
set : Set of feature indices involved.
|
51
62
|
"""
|
52
|
-
if basis_function == '1':
|
63
|
+
if basis_function == '1':
|
53
64
|
return set()
|
54
65
|
features = set()
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
else:
|
60
|
-
var = part # Take 'x0' as is
|
61
|
-
idx = int(var[1:]) # Extract index, e.g., 0
|
66
|
+
if '_' in basis_function: # Handle non-linear functions like 'log1p_x0'
|
67
|
+
parts = basis_function.split('_')
|
68
|
+
if len(parts) == 2 and parts[1].startswith('x'):
|
69
|
+
idx = int(parts[1][1:])
|
62
70
|
features.add(idx)
|
71
|
+
elif '^' in basis_function: # Handle powers, e.g., 'x0^2'
|
72
|
+
var = basis_function.split('^')[0]
|
73
|
+
idx = int(var[1:])
|
74
|
+
features.add(idx)
|
75
|
+
elif ' ' in basis_function: # Handle interactions, e.g., 'x0 x1'
|
76
|
+
for part in basis_function.split():
|
77
|
+
idx = int(part[1:])
|
78
|
+
features.add(idx)
|
79
|
+
elif basis_function.startswith('x'):
|
80
|
+
idx = int(basis_function[1:])
|
81
|
+
features.add(idx)
|
63
82
|
return features
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: oikan
|
3
|
-
Version: 0.0.3.
|
3
|
+
Version: 0.0.3.3
|
4
4
|
Summary: OIKAN: Neuro-Symbolic ML for Scientific Discovery
|
5
5
|
Author: Arman Zhalgasbayev
|
6
6
|
License: MIT
|
@@ -57,7 +57,7 @@ OIKAN implements a modern interpretation of the Kolmogorov-Arnold Representation
|
|
57
57
|
|
58
58
|
2. **Neural Implementation**: OIKAN uses a specialized architecture combining:
|
59
59
|
- Feature transformation layers with interpretable basis functions
|
60
|
-
- Symbolic regression for formula extraction
|
60
|
+
- Symbolic regression for formula extraction (ElasticNet-based)
|
61
61
|
- Automatic pruning of insignificant terms
|
62
62
|
|
63
63
|
```python
|
@@ -76,15 +76,19 @@ OIKAN implements a modern interpretation of the Kolmogorov-Arnold Representation
|
|
76
76
|
SYMBOLIC_FUNCTIONS = {
|
77
77
|
'linear': 'x', # Direct relationships
|
78
78
|
'quadratic': 'x^2', # Non-linear patterns
|
79
|
+
'cubic': 'x^3', # Higher-order relationships
|
79
80
|
'interaction': 'x_i x_j', # Feature interactions
|
80
|
-
'higher_order': 'x^n' # Polynomial terms
|
81
|
+
'higher_order': 'x^n', # Polynomial terms
|
82
|
+
'trigonometric': 'sin(x)', # Trigonometric functions
|
83
|
+
'exponential': 'exp(x)', # Exponential growth
|
84
|
+
'logarithmic': 'log(x)' # Logarithmic relationships
|
81
85
|
}
|
82
86
|
```
|
83
87
|
|
84
88
|
4. **Formula Extraction Process**:
|
85
89
|
- Train neural network on raw data
|
86
90
|
- Generate augmented samples for better coverage
|
87
|
-
- Perform L1-regularized symbolic regression
|
91
|
+
- Perform L1-regularized symbolic regression (alpha)
|
88
92
|
- Prune terms with coefficients below threshold
|
89
93
|
- Export human-readable mathematical expressions
|
90
94
|
|
@@ -115,12 +119,14 @@ model = OIKANRegressor(
|
|
115
119
|
activation='relu', # Activation function (other options: 'tanh', 'leaky_relu', 'elu', 'swish', 'gelu')
|
116
120
|
augmentation_factor=5, # Augmentation factor for data generation
|
117
121
|
polynomial_degree=2, # Degree of polynomial basis functions
|
118
|
-
alpha=0.1, # L1 regularization strength
|
122
|
+
alpha=0.1, # L1 regularization strength (Symbolic regression)
|
119
123
|
sigma=0.1, # Standard deviation of Gaussian noise for data augmentation
|
124
|
+
top_k=5, # Number of top features to select (Symbolic regression)
|
120
125
|
epochs=100, # Number of training epochs
|
121
126
|
lr=0.001, # Learning rate
|
122
127
|
batch_size=32, # Batch size for training
|
123
|
-
verbose=True # Verbose output during training
|
128
|
+
verbose=True, # Verbose output during training
|
129
|
+
evaluate_nn=True # Validate neural network performance before full process
|
124
130
|
)
|
125
131
|
|
126
132
|
# Fit the model
|
@@ -163,12 +169,14 @@ model = OIKANClassifier(
|
|
163
169
|
activation='relu', # Activation function (other options: 'tanh', 'leaky_relu', 'elu', 'swish', 'gelu')
|
164
170
|
augmentation_factor=10, # Augmentation factor for data generation
|
165
171
|
polynomial_degree=2, # Degree of polynomial basis functions
|
166
|
-
alpha=0.1, # L1 regularization strength
|
172
|
+
alpha=0.1, # L1 regularization strength (Symbolic regression)
|
167
173
|
sigma=0.1, # Standard deviation of Gaussian noise for data augmentation
|
174
|
+
top_k=5, # Number of top features to select (Symbolic regression)
|
168
175
|
epochs=100, # # Number of training epochs
|
169
176
|
lr=0.001, # Learning rate
|
170
177
|
batch_size=32, # Batch size for training
|
171
|
-
verbose=True # Verbose output during training
|
178
|
+
verbose=True, # Verbose output during training
|
179
|
+
evaluate_nn=True # Validate neural network performance before full process
|
172
180
|
)
|
173
181
|
|
174
182
|
# Fit the model
|
@@ -202,7 +210,7 @@ loaded_model.load("outputs/model.json")
|
|
202
210
|
|
203
211
|
### Architecture Diagram
|
204
212
|
|
205
|
-
|
213
|
+
-architecture-oop.png)
|
206
214
|
|
207
215
|
## Contributing
|
208
216
|
|
@@ -222,7 +230,7 @@ If you use OIKAN in your research, please cite:
|
|
222
230
|
|
223
231
|
```bibtex
|
224
232
|
@software{oikan2025,
|
225
|
-
title = {OIKAN:
|
233
|
+
title = {OIKAN: Neuro-Symbolic ML for Scientific Discovery},
|
226
234
|
author = {Zhalgasbayev, Arman},
|
227
235
|
year = {2025},
|
228
236
|
url = {https://github.com/silvermete0r/OIKAN}
|
@@ -0,0 +1,10 @@
|
|
1
|
+
oikan/__init__.py,sha256=zEzhm1GYLT4vNaIQ4CgZcNpUk3uo8SWnoaHYtHW_XSQ,628
|
2
|
+
oikan/exceptions.py,sha256=GhHWqy2Q5LVBcteTy4ngnqxr7FOoLNyD8dNt1kfRXyw,901
|
3
|
+
oikan/model.py,sha256=wvF_g1RcpYcQin_wOUiWEUeKJcQ8HyPtEm_5YrCeXFs,21946
|
4
|
+
oikan/neural.py,sha256=wxmGgzmtpwJ3lvH6u6D4i4BiAzg018czrIdw49phSCY,1558
|
5
|
+
oikan/utils.py,sha256=_FNhB_sIQfY-KsKRqvuKKVXNVZaAdpI5w8zPY_j_xJU,2898
|
6
|
+
oikan-0.0.3.3.dist-info/licenses/LICENSE,sha256=75ASVmU-XIpN-M4LbVmJ_ibgbzbvRLVti8FhnR0BTf8,1096
|
7
|
+
oikan-0.0.3.3.dist-info/METADATA,sha256=moaO5H0kXU-Gf_sV7tpt4VUgmTEys6dINlzr0yfDSUc,9055
|
8
|
+
oikan-0.0.3.3.dist-info/WHEEL,sha256=DnLRTWE75wApRYVsjgc6wsVswC54sMSJhAEd4xhDpBk,91
|
9
|
+
oikan-0.0.3.3.dist-info/top_level.txt,sha256=XwnwKwTJddZwIvtrUsAz-l-58BJRj6HjAGWrfYi_3QY,6
|
10
|
+
oikan-0.0.3.3.dist-info/RECORD,,
|
oikan/symbolic.py
DELETED
@@ -1,55 +0,0 @@
|
|
1
|
-
import numpy as np
|
2
|
-
from sklearn.preprocessing import PolynomialFeatures
|
3
|
-
from sklearn.linear_model import Lasso
|
4
|
-
|
5
|
-
def symbolic_regression(X, y, degree=2, alpha=0.1):
|
6
|
-
"""
|
7
|
-
Performs symbolic regression on the input data.
|
8
|
-
|
9
|
-
Parameters:
|
10
|
-
-----------
|
11
|
-
X : array-like of shape (n_samples, n_features)
|
12
|
-
Input data.
|
13
|
-
y : array-like of shape (n_samples,) or (n_samples, n_targets)
|
14
|
-
Target values.
|
15
|
-
degree : int, optional (default=2)
|
16
|
-
Maximum polynomial degree.
|
17
|
-
alpha : float, optional (default=0.1)
|
18
|
-
L1 regularization strength.
|
19
|
-
|
20
|
-
Returns:
|
21
|
-
--------
|
22
|
-
dict : Contains 'basis_functions', 'coefficients' (or 'coefficients_list'), 'n_features', 'degree'
|
23
|
-
"""
|
24
|
-
poly = PolynomialFeatures(degree=degree, include_bias=True)
|
25
|
-
X_poly = poly.fit_transform(X)
|
26
|
-
model = Lasso(alpha=alpha, fit_intercept=False)
|
27
|
-
model.fit(X_poly, y)
|
28
|
-
if len(y.shape) == 1 or y.shape[1] == 1:
|
29
|
-
coef = model.coef_.flatten()
|
30
|
-
selected_indices = np.where(np.abs(coef) > 1e-6)[0]
|
31
|
-
return {
|
32
|
-
'n_features': X.shape[1],
|
33
|
-
'degree': degree,
|
34
|
-
'basis_functions': poly.get_feature_names_out()[selected_indices].tolist(),
|
35
|
-
'coefficients': coef[selected_indices].tolist()
|
36
|
-
}
|
37
|
-
else:
|
38
|
-
coefficients_list = []
|
39
|
-
selected_indices = set()
|
40
|
-
for c in range(y.shape[1]):
|
41
|
-
coef = model.coef_[c]
|
42
|
-
indices = np.where(np.abs(coef) > 1e-6)[0]
|
43
|
-
selected_indices.update(indices)
|
44
|
-
selected_indices = list(selected_indices)
|
45
|
-
basis_functions = poly.get_feature_names_out()[selected_indices].tolist()
|
46
|
-
for c in range(y.shape[1]):
|
47
|
-
coef = model.coef_[c]
|
48
|
-
coef_selected = coef[selected_indices].tolist()
|
49
|
-
coefficients_list.append(coef_selected)
|
50
|
-
return {
|
51
|
-
'n_features': X.shape[1],
|
52
|
-
'degree': degree,
|
53
|
-
'basis_functions': basis_functions,
|
54
|
-
'coefficients_list': coefficients_list
|
55
|
-
}
|
oikan-0.0.3.1.dist-info/RECORD
DELETED
@@ -1,11 +0,0 @@
|
|
1
|
-
oikan/__init__.py,sha256=zEzhm1GYLT4vNaIQ4CgZcNpUk3uo8SWnoaHYtHW_XSQ,628
|
2
|
-
oikan/exceptions.py,sha256=Is0jG4apxO8QJQREIiJQYMjANYWibWeS-103q9KWbfg,192
|
3
|
-
oikan/model.py,sha256=-LuvcljM5fqQsqwmhfol_e-_zVQzTAfq8SedQ3HYQQQ,14032
|
4
|
-
oikan/neural.py,sha256=wxmGgzmtpwJ3lvH6u6D4i4BiAzg018czrIdw49phSCY,1558
|
5
|
-
oikan/symbolic.py,sha256=3gtBndqFFC9ny2-PekKkUgr_t1HEpfkbk68e94yPpbI,2083
|
6
|
-
oikan/utils.py,sha256=xMGRa1qhn8BWn9UxpVeJIuGb-UvQmbjiFSsvAdF0bMU,2095
|
7
|
-
oikan-0.0.3.1.dist-info/licenses/LICENSE,sha256=75ASVmU-XIpN-M4LbVmJ_ibgbzbvRLVti8FhnR0BTf8,1096
|
8
|
-
oikan-0.0.3.1.dist-info/METADATA,sha256=BAYWIvUqQ-al4TPraOnx0tx6eGSFUOvl4_Mxfxo61Qw,8335
|
9
|
-
oikan-0.0.3.1.dist-info/WHEEL,sha256=0CuiUZ_p9E4cD6NyLD6UG80LBXYyiSYZOKDm5lp32xk,91
|
10
|
-
oikan-0.0.3.1.dist-info/top_level.txt,sha256=XwnwKwTJddZwIvtrUsAz-l-58BJRj6HjAGWrfYi_3QY,6
|
11
|
-
oikan-0.0.3.1.dist-info/RECORD,,
|
File without changes
|
File without changes
|