oikan 0.0.3.7__py3-none-any.whl → 0.0.3.9__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/__init__.py CHANGED
@@ -9,6 +9,8 @@ Docs: https://silvermete0r.github.io/oikan/
9
9
  '''
10
10
 
11
11
  from .model import OIKAN, OIKANClassifier, OIKANRegressor
12
+ from .neural import TabularNet
13
+ from .elasticnet import ElasticNet
12
14
 
13
- __all__ = ['OIKAN', 'OIKANClassifier', 'OIKANRegressor']
15
+ __all__ = ['OIKAN', 'OIKANClassifier', 'OIKANRegressor', 'TabularNet', 'ElasticNet']
14
16
  __version__ = '0.0.3'
oikan/elasticnet.py ADDED
@@ -0,0 +1,71 @@
1
+ import torch.nn as nn
2
+ import torch
3
+ import numpy as np
4
+
5
+ class ElasticNet(nn.Module):
6
+ def __init__(self, alpha=1.0, l1_ratio=0.5, fit_intercept=False, max_iter=1000, tol=1e-4, random_state=None):
7
+ super().__init__()
8
+ self.alpha = alpha
9
+ self.l1_ratio = l1_ratio
10
+ self.fit_intercept = fit_intercept
11
+ self.max_iter = max_iter
12
+ self.tol = tol
13
+ self.random_state = random_state
14
+ self.coef_ = None
15
+ self.intercept_ = None
16
+
17
+ def fit(self, X, y):
18
+ X = np.asarray(X, dtype=np.float32)
19
+ y = np.asarray(y, dtype=np.float32)
20
+ n_samples, n_features = X.shape
21
+ if y.ndim == 1:
22
+ y = y.reshape(-1, 1)
23
+ n_targets = y.shape[1]
24
+
25
+ if self.random_state is not None:
26
+ torch.manual_seed(self.random_state)
27
+ np.random.seed(self.random_state)
28
+
29
+ X_tensor = torch.tensor(X, dtype=torch.float32)
30
+ y_tensor = torch.tensor(y, dtype=torch.float32)
31
+
32
+ W = torch.zeros((n_features, n_targets), requires_grad=True, dtype=torch.float32)
33
+ if self.fit_intercept:
34
+ b = torch.zeros(n_targets, requires_grad=True, dtype=torch.float32)
35
+ else:
36
+ b = None
37
+
38
+ optimizer = torch.optim.Adam([W] + ([b] if b is not None else []), lr=0.05)
39
+
40
+ prev_loss = None
41
+ for _ in range(self.max_iter):
42
+ optimizer.zero_grad()
43
+ pred = X_tensor @ W
44
+ if b is not None:
45
+ pred = pred + b
46
+ mse = torch.mean((pred - y_tensor) ** 2)
47
+ l1 = torch.sum(torch.abs(W))
48
+ l2 = torch.sum(W ** 2)
49
+ loss = mse + self.alpha * (self.l1_ratio * l1 + (1 - self.l1_ratio) * l2)
50
+ loss.backward()
51
+ optimizer.step()
52
+ if prev_loss is not None and abs(prev_loss - loss.item()) < self.tol:
53
+ break
54
+ prev_loss = loss.item()
55
+
56
+ self.coef_ = W.detach().cpu().numpy().T if n_targets > 1 else W.detach().cpu().numpy().flatten()
57
+ if b is not None:
58
+ self.intercept_ = b.detach().cpu().numpy()
59
+ else:
60
+ self.intercept_ = np.zeros(n_targets) if n_targets > 1 else 0.0
61
+ return self
62
+
63
+ def predict(self, X):
64
+ X = np.asarray(X, dtype=np.float32)
65
+ if self.coef_ is None:
66
+ raise RuntimeError("Model not fitted yet.")
67
+ W = self.coef_.T if self.coef_.ndim == 2 else self.coef_
68
+ y_pred = X @ W
69
+ if self.intercept_ is not None:
70
+ y_pred += self.intercept_
71
+ return y_pred
oikan/model.py CHANGED
@@ -3,15 +3,16 @@ 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 ElasticNet
7
6
  from abc import ABC, abstractmethod
8
7
  import json
8
+ from .elasticnet import ElasticNet
9
9
  from .neural import TabularNet
10
10
  from .utils import evaluate_basis_functions, get_features_involved, sympify_formula, get_latex_formula
11
11
  from sklearn.model_selection import train_test_split
12
12
  from sklearn.metrics import r2_score, accuracy_score
13
13
  from .exceptions import *
14
14
  import sys
15
+ from tqdm import tqdm
15
16
 
16
17
  class OIKAN(ABC):
17
18
  """
@@ -25,9 +26,12 @@ class OIKAN(ABC):
25
26
  Activation function for the neural network ('relu', 'tanh', 'leaky_relu', 'elu', 'swish', 'gelu').
26
27
  augmentation_factor : int, optional (default=10)
27
28
  Number of augmented samples per original sample.
28
- alpha : float, optional (default=0.1)
29
- L1 regularization strength for Lasso in symbolic regression.
30
- sigma : float, optional (default=0.1)
29
+ alpha : float, optional (default=1.0)
30
+ ElasticNet regularization strength.
31
+ l1_ratio: float, optional (default=0.5)
32
+ ElasticNet mixing parameter (0 <= l1_ratio <= 1).
33
+ 0 is equivalent to Ridge regression, 1 is equivalent to Lasso.
34
+ sigma : float, optional (default=5.0)
31
35
  Standard deviation of Gaussian noise for data augmentation.
32
36
  top_k : int, optional (default=5)
33
37
  Number of top features to select in hierarchical symbolic regression.
@@ -45,7 +49,7 @@ class OIKAN(ABC):
45
49
  Random seed for reproducibility.
46
50
  """
47
51
  def __init__(self, hidden_sizes=[64, 64], activation='relu', augmentation_factor=10,
48
- alpha=0.1, sigma=0.1, epochs=100, lr=0.001, batch_size=32,
52
+ alpha=1.0, l1_ratio=0.5, sigma=5.0, epochs=100, lr=0.001, batch_size=32,
49
53
  verbose=False, evaluate_nn=False, top_k=5, random_state=None):
50
54
  if not isinstance(hidden_sizes, list) or not all(isinstance(x, int) and x > 0 for x in hidden_sizes):
51
55
  raise InvalidParameterError("hidden_sizes must be a list of positive integers")
@@ -63,6 +67,8 @@ class OIKAN(ABC):
63
67
  raise InvalidParameterError("epochs must be a positive integer")
64
68
  if not 0 <= alpha <= 1:
65
69
  raise InvalidParameterError("alpha must be between 0 and 1")
70
+ if not 0 <= l1_ratio <= 1:
71
+ raise InvalidParameterError("l1_ratio must be between 0 and 1")
66
72
  if sigma <= 0:
67
73
  raise InvalidParameterError("sigma must be positive")
68
74
 
@@ -70,6 +76,7 @@ class OIKAN(ABC):
70
76
  self.activation = activation
71
77
  self.augmentation_factor = augmentation_factor
72
78
  self.alpha = alpha
79
+ self.l1_ratio = l1_ratio
73
80
  self.sigma = sigma
74
81
  self.epochs = epochs
75
82
  self.lr = lr
@@ -353,14 +360,23 @@ class OIKAN(ABC):
353
360
  if np.any(np.isinf(X)) or np.any(np.isinf(y)):
354
361
  raise NumericalInstabilityError("Input data contains infinite values")
355
362
 
356
- # Stage 1: Coarse Model
363
+ if self.verbose:
364
+ print("\nStage 1: Coarse Model Fitting")
365
+
357
366
  coarse_degree = 2 # Fixed low degree for coarse model
358
367
  poly_coarse = PolynomialFeatures(degree=coarse_degree, include_bias=True)
368
+
369
+ if self.verbose:
370
+ print("Generating polynomial features...")
359
371
  X_poly_coarse = poly_coarse.fit_transform(X)
360
- model_coarse = ElasticNet(alpha=self.alpha, fit_intercept=False)
372
+
373
+ if self.verbose:
374
+ print("Fitting coarse elastic net model...")
375
+ model_coarse = ElasticNet(alpha=self.alpha, l1_ratio=self.l1_ratio, fit_intercept=False, random_state=self.random_state)
361
376
  model_coarse.fit(X_poly_coarse, y)
362
377
 
363
- # Compute feature importances for original features
378
+ if self.verbose:
379
+ print("Computing feature importances...")
364
380
  basis_functions_coarse = poly_coarse.get_feature_names_out()
365
381
  if len(y.shape) == 1 or y.shape[1] == 1:
366
382
  coef_coarse = model_coarse.coef_.flatten()
@@ -368,7 +384,7 @@ class OIKAN(ABC):
368
384
  coef_coarse = np.sum(np.abs(model_coarse.coef_), axis=0)
369
385
 
370
386
  importances = np.zeros(X.shape[1])
371
- for i, func in enumerate(basis_functions_coarse):
387
+ for i, func in enumerate(tqdm(basis_functions_coarse, disable=not self.verbose, desc="Analyzing features")):
372
388
  features_involved = get_features_involved(func)
373
389
  for idx in features_involved:
374
390
  importances[idx] += np.abs(coef_coarse[i])
@@ -379,11 +395,13 @@ class OIKAN(ABC):
379
395
  # Select top K features
380
396
  top_k_indices = np.argsort(importances)[::-1][:self.top_k]
381
397
 
382
- # Stage 2: Refined Model
383
- # ~ generate additional non-linear features for top K features
398
+ if self.verbose:
399
+ print(f"\nStage 2: Refined Model with top {self.top_k} features")
400
+ print("Generating additional non-linear features...")
401
+
384
402
  additional_features = []
385
403
  additional_names = []
386
- for i in top_k_indices:
404
+ for i in tqdm(top_k_indices, disable=not self.verbose, desc="Generating features"):
387
405
  # Higher-degree polynomial
388
406
  additional_features.append(X[:, i]**3)
389
407
  additional_names.append(f'x{i}^3')
@@ -395,15 +413,18 @@ class OIKAN(ABC):
395
413
  additional_features.append(np.sin(X[:, i]))
396
414
  additional_names.append(f'sin_x{i}')
397
415
 
398
- # Combine features
416
+ if self.verbose:
417
+ print("Combining features and fitting final model...")
399
418
  X_additional = np.column_stack(additional_features)
400
419
  X_refined = np.hstack([X_poly_coarse, X_additional])
401
420
  basis_functions_refined = list(basis_functions_coarse) + additional_names
402
421
 
403
- # Fit refined model
404
- model_refined = ElasticNet(alpha=self.alpha, fit_intercept=False)
422
+ model_refined = ElasticNet(alpha=self.alpha, l1_ratio=self.l1_ratio, fit_intercept=False, random_state=self.random_state)
405
423
  model_refined.fit(X_refined, y)
406
424
 
425
+ if self.verbose:
426
+ print("Building final symbolic model...")
427
+
407
428
  # Store symbolic model
408
429
  if len(y.shape) == 1 or y.shape[1] == 1:
409
430
  # Regression
@@ -418,7 +439,7 @@ class OIKAN(ABC):
418
439
  # Classification
419
440
  coefficients_list = []
420
441
  selected_indices = set()
421
- for c in range(y.shape[1]):
442
+ for c in tqdm(range(y.shape[1]), disable=not self.verbose, desc="Processing classes"):
422
443
  coef = model_refined.coef_[c]
423
444
  indices = np.where(np.abs(coef) > 1e-6)[0]
424
445
  selected_indices.update(indices)
@@ -454,7 +475,7 @@ class OIKANRegressor(OIKAN):
454
475
  self._train_neural_net(X, y, output_size=1, loss_fn=nn.MSELoss())
455
476
 
456
477
  if self.verbose:
457
- print(f"Original data: features shape: {X.shape} | target shape: {y.shape}")
478
+ print(f"Original data: features shape: {X.shape} | target shape: {y.shape} | size: {X.nbytes / (1024 * 1024):.2f} MB")
458
479
 
459
480
  X_aug = self._generate_augmented_data(X)
460
481
 
@@ -463,13 +484,14 @@ class OIKANRegressor(OIKAN):
463
484
  y_aug = self.neural_net(torch.tensor(X_aug, dtype=torch.float32)).detach().numpy()
464
485
 
465
486
  if self.verbose:
466
- print(f"Augmented data: features shape: {X_aug.shape} | target shape: {y_aug.shape}")
487
+ print(f"Augmented data: features shape: {X_aug.shape} | target shape: {y_aug.shape} | size: {X_aug.nbytes / (1024 * 1024):.2f} MB")
467
488
 
468
489
  X_combined = np.vstack([X, X_aug])
469
490
  y_combined = np.vstack([y, y_aug])
470
491
  else:
471
492
  if self.verbose:
472
493
  print("Skipping neural network training (augmentation_factor=1)")
494
+ print(f"Data: features shape: {X.shape} | target shape: {y.shape} | size: {X.nbytes / (1024 * 1024):.2f} MB")
473
495
  X_combined = X
474
496
  y_combined = y
475
497
 
@@ -523,7 +545,7 @@ class OIKANClassifier(OIKAN):
523
545
  self._train_neural_net(X, y_onehot, output_size=n_classes, loss_fn=nn.CrossEntropyLoss())
524
546
 
525
547
  if self.verbose:
526
- print(f"Original data: features shape: {X.shape} | target shape: {y.shape}")
548
+ print(f"Original data: features shape: {X.shape} | target shape: {y.shape} | size: {X.nbytes / (1024 * 1024):.2f} MB")
527
549
 
528
550
  X_aug = self._generate_augmented_data(X)
529
551
 
@@ -532,13 +554,14 @@ class OIKANClassifier(OIKAN):
532
554
  logits_aug = self.neural_net(torch.tensor(X_aug, dtype=torch.float32)).detach().numpy()
533
555
 
534
556
  if self.verbose:
535
- print(f"Augmented data: features shape: {X_aug.shape} | target shape: {logits_aug.shape}")
557
+ print(f"Augmented data: features shape: {X_aug.shape} | target shape: {logits_aug.shape} | size: {X_aug.nbytes / (1024 * 1024):.2f} MB")
536
558
 
537
559
  X_combined = np.vstack([X, X_aug])
538
560
  y_combined = np.vstack([y_onehot.numpy(), logits_aug])
539
561
  else:
540
562
  if self.verbose:
541
563
  print("Skipping neural network training (augmentation_factor=1)")
564
+ print(f"Data: features shape: {X.shape} | target shape: {y.shape} | size: {X.nbytes / (1024 * 1024):.2f} MB")
542
565
  X_combined = X
543
566
  y_combined = y_onehot.numpy()
544
567
 
@@ -1,9 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: oikan
3
- Version: 0.0.3.7
3
+ Version: 0.0.3.9
4
4
  Summary: OIKAN: Neuro-Symbolic ML for Scientific Discovery
5
5
  Author: Arman Zhalgasbayev
6
6
  License: MIT
7
+ Project-URL: Homepage, https://github.com/silvermete0r/oikan
8
+ Project-URL: Bug Tracker, https://github.com/silvermete0r/oikan/issues
7
9
  Classifier: Programming Language :: Python :: 3
8
10
  Classifier: License :: OSI Approved :: MIT License
9
11
  Classifier: Operating System :: OS Independent
@@ -49,19 +51,9 @@ OIKAN is a neuro-symbolic machine learning framework inspired by Kolmogorov-Arno
49
51
  - 🔬 **Research-Focused**: Designed for academic exploration and experimentation
50
52
  - 📈 **Multi-Task**: Supports both regression and classification problems
51
53
 
52
- ## Scientific Foundation
54
+ ## Key Aspects
53
55
 
54
- OIKAN implements a modern interpretation of the Kolmogorov-Arnold Representation Theorem through a hybrid neural architecture:
55
-
56
- 1. **Theoretical Foundation**: The Kolmogorov-Arnold theorem states that any continuous n-dimensional function can be decomposed into a combination of single-variable functions:
57
-
58
- ```
59
- f(x₁,...,xₙ) = ∑(j=0 to 2n){ φⱼ( ∑(i=1 to n) ψᵢⱼ(xᵢ) ) }
60
- ```
61
-
62
- where φⱼ and ψᵢⱼ are continuous univariate functions.
63
-
64
- 2. **Neural Implementation**: OIKAN uses a specialized architecture combining:
56
+ 1. **Neural Implementation**: OIKAN uses a specialized architecture combining:
65
57
  - Feature transformation layers with interpretable basis functions
66
58
  - Symbolic regression for formula extraction (ElasticNet-based)
67
59
  - Automatic pruning of insignificant terms
@@ -78,7 +70,7 @@ OIKAN implements a modern interpretation of the Kolmogorov-Arnold Representation
78
70
  self.symbolic_regression = SymbolicRegression(alpha=alpha)
79
71
  ```
80
72
 
81
- 3. **Basis Functions**: Core set of interpretable transformations:
73
+ 2. **Basis Functions**: Core set of interpretable transformations:
82
74
  ```python
83
75
  SYMBOLIC_FUNCTIONS = {
84
76
  'linear': 'x', # Direct relationships
@@ -92,10 +84,10 @@ OIKAN implements a modern interpretation of the Kolmogorov-Arnold Representation
92
84
  }
93
85
  ```
94
86
 
95
- 4. **Formula Extraction Process**:
87
+ 3. **Formula Extraction Process**:
96
88
  - Train neural network on raw data
97
89
  - Generate augmented samples for better coverage
98
- - Perform L1-regularized symbolic regression (alpha)
90
+ - Perform ElasticNet-regularization
99
91
  - Prune terms with coefficients below threshold
100
92
  - Export human-readable mathematical expressions
101
93
 
@@ -127,6 +119,9 @@ pip install -e . # Install in development mode
127
119
  | Dependencies | torch, numpy, scikit-learn, sympy, tqdm |
128
120
 
129
121
  ### Regression Example
122
+
123
+ > **Suggestion:** Please ensure that the data is normalized using standard scaling (or another suitable normalization method), as Elastic Net assumes that the model intercept has already been accounted for.
124
+
130
125
  ```python
131
126
  from oikan import OIKANRegressor
132
127
  from sklearn.metrics import mean_squared_error
@@ -136,8 +131,9 @@ model = OIKANRegressor(
136
131
  hidden_sizes=[32, 32], # Hidden layer sizes
137
132
  activation='relu', # Activation function (other options: 'tanh', 'leaky_relu', 'elu', 'swish', 'gelu')
138
133
  augmentation_factor=5, # Augmentation factor for data generation
139
- alpha=0.1, # L1 regularization strength (Symbolic regression)
140
- sigma=0.1, # Standard deviation of Gaussian noise for data augmentation
134
+ alpha=1.0, # ElasticNet regularization strength (Symbolic regression)
135
+ l1_rate=0.5, # ElasticNet mixing parameter (0 <= l1_ratio <= 1). 0 is equivalent to Ridge regression, 1 is equivalent to Lasso (Symbolic regression)
136
+ sigma=5, # Standard deviation of Gaussian noise for data augmentation
141
137
  top_k=5, # Number of top features to select (Symbolic regression)
142
138
  epochs=100, # Number of training epochs
143
139
  lr=0.001, # Learning rate
@@ -177,6 +173,9 @@ loaded_model.load("outputs/model.json")
177
173
 
178
174
 
179
175
  ### Classification Example
176
+
177
+ > **Suggestion:** Please ensure that the data is normalized using standard scaling (or another suitable normalization method), as Elastic Net assumes that the model intercept has already been accounted for.
178
+
180
179
  ```python
181
180
  from oikan import OIKANClassifier
182
181
  from sklearn.metrics import accuracy_score
@@ -186,8 +185,9 @@ model = OIKANClassifier(
186
185
  hidden_sizes=[32, 32], # Hidden layer sizes
187
186
  activation='relu', # Activation function (other options: 'tanh', 'leaky_relu', 'elu', 'swish', 'gelu')
188
187
  augmentation_factor=10, # Augmentation factor for data generation
189
- alpha=0.1, # L1 regularization strength (Symbolic regression)
190
- sigma=0.1, # Standard deviation of Gaussian noise for data augmentation
188
+ alpha=1.0, # ElasticNet regularization strength (Symbolic regression)
189
+ l1_rate=0.5, # ElasticNet mixing parameter (0 <= l1_ratio <= 1). 0 is equivalent to Ridge regression, 1 is equivalent to Lasso (Symbolic regression)
190
+ sigma=5, # Standard deviation of Gaussian noise for data augmentation
191
191
  top_k=5, # Number of top features to select (Symbolic regression)
192
192
  epochs=100, # # Number of training epochs
193
193
  lr=0.001, # Learning rate
@@ -0,0 +1,11 @@
1
+ oikan/__init__.py,sha256=Dh1Rf9ONRdm75B6tFiv9Y9P6NNiHAiKPCGDMuag6TTE,724
2
+ oikan/elasticnet.py,sha256=yByuG9KCFQ4PpT2ze6oTSDy0DxvdF5MAJoegUGEipSA,2614
3
+ oikan/exceptions.py,sha256=GhHWqy2Q5LVBcteTy4ngnqxr7FOoLNyD8dNt1kfRXyw,901
4
+ oikan/model.py,sha256=UAjRYwb-kEap4AJkJ3OVmpNWpun0qgcad5m6x-mUbN8,26237
5
+ oikan/neural.py,sha256=PZjaffSuABuCNxu-7PinU1GR6ji0Y6xRgSQ3n5HRDxI,1572
6
+ oikan/utils.py,sha256=7UCm9obO-8Q2zhetdAkukMDOZvGSBWUL_dSF04XqM7k,8808
7
+ oikan-0.0.3.9.dist-info/licenses/LICENSE,sha256=75ASVmU-XIpN-M4LbVmJ_ibgbzbvRLVti8FhnR0BTf8,1096
8
+ oikan-0.0.3.9.dist-info/METADATA,sha256=KCUIcXDdneq4MTgnIKmOHf0OPjWUzmQSm21ayOIYQZs,13124
9
+ oikan-0.0.3.9.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
10
+ oikan-0.0.3.9.dist-info/top_level.txt,sha256=XwnwKwTJddZwIvtrUsAz-l-58BJRj6HjAGWrfYi_3QY,6
11
+ oikan-0.0.3.9.dist-info/RECORD,,
@@ -1,10 +0,0 @@
1
- oikan/__init__.py,sha256=zEzhm1GYLT4vNaIQ4CgZcNpUk3uo8SWnoaHYtHW_XSQ,628
2
- oikan/exceptions.py,sha256=GhHWqy2Q5LVBcteTy4ngnqxr7FOoLNyD8dNt1kfRXyw,901
3
- oikan/model.py,sha256=TC2-R00GOjFb7ePzKTqeYkOiVlqUK7KP0mXsnJhg9ik,24736
4
- oikan/neural.py,sha256=PZjaffSuABuCNxu-7PinU1GR6ji0Y6xRgSQ3n5HRDxI,1572
5
- oikan/utils.py,sha256=7UCm9obO-8Q2zhetdAkukMDOZvGSBWUL_dSF04XqM7k,8808
6
- oikan-0.0.3.7.dist-info/licenses/LICENSE,sha256=75ASVmU-XIpN-M4LbVmJ_ibgbzbvRLVti8FhnR0BTf8,1096
7
- oikan-0.0.3.7.dist-info/METADATA,sha256=nrel6O7TXdbtJHSNCzvqPq_IELeQWx0azfrU4Jq6sps,12749
8
- oikan-0.0.3.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
- oikan-0.0.3.7.dist-info/top_level.txt,sha256=XwnwKwTJddZwIvtrUsAz-l-58BJRj6HjAGWrfYi_3QY,6
10
- oikan-0.0.3.7.dist-info/RECORD,,