oikan 0.0.3.1__tar.gz → 0.0.3.3__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oikan
3
- Version: 0.0.3.1
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
- *Will be updated soon..*
213
+ ![OIKAN v0.0.3(1) Architecture](https://raw.githubusercontent.com/silvermete0r/oikan/main/docs/media/oikan-v0.0.3(1)-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: Optimized Interpretable Kolmogorov-Arnold Networks},
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}
@@ -39,7 +39,7 @@ OIKAN implements a modern interpretation of the Kolmogorov-Arnold Representation
39
39
 
40
40
  2. **Neural Implementation**: OIKAN uses a specialized architecture combining:
41
41
  - Feature transformation layers with interpretable basis functions
42
- - Symbolic regression for formula extraction
42
+ - Symbolic regression for formula extraction (ElasticNet-based)
43
43
  - Automatic pruning of insignificant terms
44
44
 
45
45
  ```python
@@ -58,15 +58,19 @@ OIKAN implements a modern interpretation of the Kolmogorov-Arnold Representation
58
58
  SYMBOLIC_FUNCTIONS = {
59
59
  'linear': 'x', # Direct relationships
60
60
  'quadratic': 'x^2', # Non-linear patterns
61
+ 'cubic': 'x^3', # Higher-order relationships
61
62
  'interaction': 'x_i x_j', # Feature interactions
62
- 'higher_order': 'x^n' # Polynomial terms
63
+ 'higher_order': 'x^n', # Polynomial terms
64
+ 'trigonometric': 'sin(x)', # Trigonometric functions
65
+ 'exponential': 'exp(x)', # Exponential growth
66
+ 'logarithmic': 'log(x)' # Logarithmic relationships
63
67
  }
64
68
  ```
65
69
 
66
70
  4. **Formula Extraction Process**:
67
71
  - Train neural network on raw data
68
72
  - Generate augmented samples for better coverage
69
- - Perform L1-regularized symbolic regression
73
+ - Perform L1-regularized symbolic regression (alpha)
70
74
  - Prune terms with coefficients below threshold
71
75
  - Export human-readable mathematical expressions
72
76
 
@@ -97,12 +101,14 @@ model = OIKANRegressor(
97
101
  activation='relu', # Activation function (other options: 'tanh', 'leaky_relu', 'elu', 'swish', 'gelu')
98
102
  augmentation_factor=5, # Augmentation factor for data generation
99
103
  polynomial_degree=2, # Degree of polynomial basis functions
100
- alpha=0.1, # L1 regularization strength
104
+ alpha=0.1, # L1 regularization strength (Symbolic regression)
101
105
  sigma=0.1, # Standard deviation of Gaussian noise for data augmentation
106
+ top_k=5, # Number of top features to select (Symbolic regression)
102
107
  epochs=100, # Number of training epochs
103
108
  lr=0.001, # Learning rate
104
109
  batch_size=32, # Batch size for training
105
- verbose=True # Verbose output during training
110
+ verbose=True, # Verbose output during training
111
+ evaluate_nn=True # Validate neural network performance before full process
106
112
  )
107
113
 
108
114
  # Fit the model
@@ -145,12 +151,14 @@ model = OIKANClassifier(
145
151
  activation='relu', # Activation function (other options: 'tanh', 'leaky_relu', 'elu', 'swish', 'gelu')
146
152
  augmentation_factor=10, # Augmentation factor for data generation
147
153
  polynomial_degree=2, # Degree of polynomial basis functions
148
- alpha=0.1, # L1 regularization strength
154
+ alpha=0.1, # L1 regularization strength (Symbolic regression)
149
155
  sigma=0.1, # Standard deviation of Gaussian noise for data augmentation
156
+ top_k=5, # Number of top features to select (Symbolic regression)
150
157
  epochs=100, # # Number of training epochs
151
158
  lr=0.001, # Learning rate
152
159
  batch_size=32, # Batch size for training
153
- verbose=True # Verbose output during training
160
+ verbose=True, # Verbose output during training
161
+ evaluate_nn=True # Validate neural network performance before full process
154
162
  )
155
163
 
156
164
  # Fit the model
@@ -184,7 +192,7 @@ loaded_model.load("outputs/model.json")
184
192
 
185
193
  ### Architecture Diagram
186
194
 
187
- *Will be updated soon..*
195
+ ![OIKAN v0.0.3(1) Architecture](https://raw.githubusercontent.com/silvermete0r/oikan/main/docs/media/oikan-v0.0.3(1)-architecture-oop.png)
188
196
 
189
197
  ## Contributing
190
198
 
@@ -204,7 +212,7 @@ If you use OIKAN in your research, please cite:
204
212
 
205
213
  ```bibtex
206
214
  @software{oikan2025,
207
- title = {OIKAN: Optimized Interpretable Kolmogorov-Arnold Networks},
215
+ title = {OIKAN: Neuro-Symbolic ML for Scientific Discovery},
208
216
  author = {Zhalgasbayev, Arman},
209
217
  year = {2025},
210
218
  url = {https://github.com/silvermete0r/OIKAN}
@@ -0,0 +1,31 @@
1
+ class OIKANError(Exception):
2
+ """Base exception for OIKAN library."""
3
+ pass
4
+
5
+ class ModelNotFittedError(OIKANError):
6
+ """Raised when a method requires a fitted model."""
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
@@ -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 Lasso
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' or 'tanh').
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]:.3f}*{basis_functions[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]:.3f}*{basis_functions[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 ValueError("Model not fitted yet.")
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
- if 'coefficients' in self.symbolic_model:
138
- model_data['coefficients'] = self.symbolic_model['coefficients']
139
- else:
140
- model_data['coefficients_list'] = [coef for coef in self.symbolic_model['coefficients_list']]
141
- if hasattr(self, 'classes_'):
142
- model_data['classes'] = self.classes_.tolist()
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
- with open(path, 'w') as f:
145
- json.dump(model_data, f, indent=2)
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
- with open(path, 'r') as f:
160
- model_data = json.load(f)
161
-
162
- self.symbolic_model = {
163
- 'n_features': model_data['n_features'],
164
- 'degree': model_data['degree'],
165
- 'basis_functions': model_data['basis_functions']
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 'coefficients' in model_data:
169
- self.symbolic_model['coefficients'] = model_data['coefficients']
170
- else:
171
- self.symbolic_model['coefficients_list'] = model_data['coefficients_list']
172
- if 'classes' in model_data:
173
- self.classes_ = np.array(model_data['classes'])
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 = TabularNet(input_size, self.hidden_sizes, output_size, self.activation)
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
- """Performs symbolic regression using polynomial features and Lasso."""
216
- poly = PolynomialFeatures(degree=self.polynomial_degree, include_bias=True)
217
- X_poly = poly.fit_transform(X)
218
- model = Lasso(alpha=self.alpha, fit_intercept=False)
219
- model.fit(X_poly, y)
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
- coef = model.coef_.flatten()
222
- selected_indices = np.where(np.abs(coef) > 1e-6)[0]
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': poly.get_feature_names_out()[selected_indices].tolist(),
227
- 'coefficients': coef[selected_indices].tolist()
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 = model.coef_[c]
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 = poly.get_feature_names_out()[selected_indices].tolist()
394
+ basis_functions = [basis_functions_refined[i] for i in selected_indices]
239
395
  for c in range(y.shape[1]):
240
- coef = model.coef_[c]
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):
@@ -0,0 +1,82 @@
1
+ import numpy as np
2
+
3
+ def evaluate_basis_functions(X, basis_functions, n_features):
4
+ """
5
+ Evaluates basis functions on the input data.
6
+
7
+ Parameters:
8
+ -----------
9
+ X : array-like of shape (n_samples, n_features)
10
+ Input data.
11
+ basis_functions : list
12
+ List of basis function strings (e.g., '1', 'x0', 'x0^2', 'x0 x1', 'log1p_x0').
13
+ n_features : int
14
+ Number of input features.
15
+
16
+ Returns:
17
+ --------
18
+ X_transformed : ndarray of shape (n_samples, n_basis_functions)
19
+ Transformed data matrix.
20
+ """
21
+ X_transformed = np.zeros((X.shape[0], len(basis_functions)))
22
+ for i, func in enumerate(basis_functions):
23
+ if func == '1':
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])
34
+ elif '^' in func:
35
+ var, power = func.split('^')
36
+ idx = int(var[1:])
37
+ X_transformed[:, i] = X[:, idx] ** int(power)
38
+ elif ' ' in func:
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
45
+ else:
46
+ idx = int(func[1:])
47
+ X_transformed[:, i] = X[:, idx]
48
+ return X_transformed
49
+
50
+ def get_features_involved(basis_function):
51
+ """
52
+ Extracts the feature indices involved in a basis function string.
53
+
54
+ Parameters:
55
+ -----------
56
+ basis_function : str
57
+ String representation of the basis function, e.g., 'x0', 'x0^2', 'x0 x1', 'log1p_x0'.
58
+
59
+ Returns:
60
+ --------
61
+ set : Set of feature indices involved.
62
+ """
63
+ if basis_function == '1':
64
+ return set()
65
+ features = set()
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:])
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)
82
+ return features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oikan
3
- Version: 0.0.3.1
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
- *Will be updated soon..*
213
+ ![OIKAN v0.0.3(1) Architecture](https://raw.githubusercontent.com/silvermete0r/oikan/main/docs/media/oikan-v0.0.3(1)-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: Optimized Interpretable Kolmogorov-Arnold Networks},
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}
@@ -6,7 +6,6 @@ oikan/__init__.py
6
6
  oikan/exceptions.py
7
7
  oikan/model.py
8
8
  oikan/neural.py
9
- oikan/symbolic.py
10
9
  oikan/utils.py
11
10
  oikan.egg-info/PKG-INFO
12
11
  oikan.egg-info/SOURCES.txt
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "oikan"
7
- version = "0.0.3.1"
7
+ version = "0.0.3.3"
8
8
  description = "OIKAN: Neuro-Symbolic ML for Scientific Discovery"
9
9
  readme = "README.md"
10
10
  authors = [{name = "Arman Zhalgasbayev"}]
@@ -1,7 +0,0 @@
1
- class OIKANError(Exception):
2
- """Base exception for OIKAN library."""
3
- pass
4
-
5
- class ModelNotFittedError(OIKANError):
6
- """Raised when a method requires a fitted model."""
7
- pass
@@ -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
- }
@@ -1,63 +0,0 @@
1
- import numpy as np
2
-
3
- def evaluate_basis_functions(X, basis_functions, n_features):
4
- """
5
- Evaluates basis functions on the input data.
6
-
7
- Parameters:
8
- -----------
9
- X : array-like of shape (n_samples, n_features)
10
- Input data.
11
- basis_functions : list
12
- List of basis function strings (e.g., '1', 'x0', 'x0^2', 'x0 x1').
13
- n_features : int
14
- Number of input features.
15
-
16
- Returns:
17
- --------
18
- X_transformed : ndarray of shape (n_samples, n_basis_functions)
19
- Transformed data matrix.
20
- """
21
- X_transformed = np.zeros((X.shape[0], len(basis_functions)))
22
- for i, func in enumerate(basis_functions):
23
- if func == '1':
24
- X_transformed[:, i] = 1
25
- elif '^' in func:
26
- var, power = func.split('^')
27
- idx = int(var[1:])
28
- X_transformed[:, i] = X[:, idx] ** int(power)
29
- elif ' ' in func:
30
- var1, var2 = func.split(' ')
31
- idx1 = int(var1[1:])
32
- idx2 = int(var2[1:])
33
- X_transformed[:, i] = X[:, idx1] * X[:, idx2]
34
- else:
35
- idx = int(func[1:])
36
- X_transformed[:, i] = X[:, idx]
37
- return X_transformed
38
-
39
- def get_features_involved(basis_function):
40
- """
41
- Extracts the feature indices involved in a basis function string.
42
-
43
- Parameters:
44
- -----------
45
- basis_function : str
46
- String representation of the basis function, e.g., 'x0', 'x0^2', 'x0 x1'.
47
-
48
- Returns:
49
- --------
50
- set : Set of feature indices involved.
51
- """
52
- if basis_function == '1': # Constant term involves no features
53
- return set()
54
- features = set()
55
- for part in basis_function.split(): # Split by space for interaction terms
56
- if part.startswith('x'):
57
- if '^' in part: # Handle powers, e.g., 'x0^2'
58
- var = part.split('^')[0] # Take 'x0'
59
- else:
60
- var = part # Take 'x0' as is
61
- idx = int(var[1:]) # Extract index, e.g., 0
62
- features.add(idx)
63
- return features
File without changes
File without changes
File without changes
File without changes
File without changes