oikan 0.0.2.5__py3-none-any.whl → 0.0.3.1__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 +14 -0
- oikan/exceptions.py +5 -13
- oikan/model.py +303 -443
- oikan/neural.py +43 -0
- oikan/symbolic.py +55 -0
- oikan/utils.py +59 -49
- oikan-0.0.3.1.dist-info/METADATA +233 -0
- oikan-0.0.3.1.dist-info/RECORD +11 -0
- {oikan-0.0.2.5.dist-info → oikan-0.0.3.1.dist-info}/WHEEL +1 -1
- oikan-0.0.2.5.dist-info/METADATA +0 -195
- oikan-0.0.2.5.dist-info/RECORD +0 -9
- {oikan-0.0.2.5.dist-info → oikan-0.0.3.1.dist-info}/licenses/LICENSE +0 -0
- {oikan-0.0.2.5.dist-info → oikan-0.0.3.1.dist-info}/top_level.txt +0 -0
oikan/model.py
CHANGED
@@ -1,481 +1,341 @@
|
|
1
|
+
import numpy as np
|
1
2
|
import torch
|
2
3
|
import torch.nn as nn
|
3
|
-
import
|
4
|
-
from sklearn.
|
5
|
-
from .
|
6
|
-
from
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
"""Edge-based activation function learner"""
|
11
|
-
def __init__(self):
|
12
|
-
super().__init__()
|
13
|
-
self.activation = EdgeActivation()
|
14
|
-
|
15
|
-
def forward(self, x):
|
16
|
-
return self.activation(x)
|
17
|
-
|
18
|
-
def get_symbolic_repr(self, threshold=1e-4):
|
19
|
-
return self.activation.get_symbolic_repr(threshold)
|
4
|
+
import torch.optim as optim
|
5
|
+
from sklearn.preprocessing import PolynomialFeatures
|
6
|
+
from sklearn.linear_model import Lasso
|
7
|
+
from abc import ABC, abstractmethod
|
8
|
+
import json
|
9
|
+
from .neural import TabularNet
|
10
|
+
from .utils import evaluate_basis_functions, get_features_involved
|
20
11
|
|
21
|
-
class
|
22
|
-
"""
|
23
|
-
|
24
|
-
super().__init__()
|
25
|
-
self.input_dim = input_dim
|
26
|
-
self.output_dim = output_dim
|
27
|
-
|
28
|
-
self.edges = nn.ModuleList([
|
29
|
-
nn.ModuleList([SymbolicEdge() for _ in range(output_dim)])
|
30
|
-
for _ in range(input_dim)
|
31
|
-
])
|
32
|
-
|
33
|
-
# Updated initialization using Xavier uniform initialization
|
34
|
-
self.combination_weights = nn.Parameter(
|
35
|
-
nn.init.xavier_uniform_(torch.empty(input_dim, output_dim))
|
36
|
-
)
|
37
|
-
|
38
|
-
def forward(self, x):
|
39
|
-
x_split = x.split(1, dim=1) # list of (batch, 1) tensors for each input feature
|
40
|
-
edge_outputs = torch.stack([
|
41
|
-
torch.stack([edge(x_i).squeeze() for edge in edge_list], dim=1)
|
42
|
-
for x_i, edge_list in zip(x_split, self.edges)
|
43
|
-
], dim=1) # shape: (batch, input_dim, output_dim)
|
44
|
-
combined = edge_outputs * self.combination_weights.unsqueeze(0)
|
45
|
-
return combined.sum(dim=1)
|
12
|
+
class OIKAN(ABC):
|
13
|
+
"""
|
14
|
+
Base class for the OIKAN neuro-symbolic framework.
|
46
15
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
for
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
16
|
+
Parameters:
|
17
|
+
-----------
|
18
|
+
hidden_sizes : list, optional (default=[64, 64])
|
19
|
+
List of hidden layer sizes for the neural network.
|
20
|
+
activation : str, optional (default='relu')
|
21
|
+
Activation function for the neural network ('relu' or 'tanh').
|
22
|
+
augmentation_factor : int, optional (default=10)
|
23
|
+
Number of augmented samples per original sample.
|
24
|
+
polynomial_degree : int, optional (default=2)
|
25
|
+
Maximum degree of polynomial features for symbolic regression.
|
26
|
+
alpha : float, optional (default=0.1)
|
27
|
+
L1 regularization strength for Lasso in symbolic regression.
|
28
|
+
sigma : float, optional (default=0.1)
|
29
|
+
Standard deviation of Gaussian noise for data augmentation.
|
30
|
+
epochs : int, optional (default=100)
|
31
|
+
Number of epochs for neural network training.
|
32
|
+
lr : float, optional (default=0.001)
|
33
|
+
Learning rate for neural network optimization.
|
34
|
+
batch_size : int, optional (default=32)
|
35
|
+
Batch size for neural network training.
|
36
|
+
verbose : bool, optional (default=False)
|
37
|
+
Whether to display training progress.
|
38
|
+
"""
|
39
|
+
def __init__(self, hidden_sizes=[64, 64], activation='relu', augmentation_factor=10,
|
40
|
+
polynomial_degree=2, alpha=0.1, sigma=0.1, epochs=100, lr=0.001, batch_size=32,
|
41
|
+
verbose=False):
|
42
|
+
self.hidden_sizes = hidden_sizes
|
43
|
+
self.activation = activation
|
44
|
+
self.augmentation_factor = augmentation_factor
|
45
|
+
self.polynomial_degree = polynomial_degree
|
46
|
+
self.alpha = alpha
|
47
|
+
self.sigma = sigma
|
48
|
+
self.epochs = epochs
|
49
|
+
self.lr = lr
|
50
|
+
self.batch_size = batch_size
|
51
|
+
self.verbose = verbose
|
52
|
+
self.neural_net = None
|
53
|
+
self.symbolic_model = None
|
61
54
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
self.hidden_dims = hidden_dims
|
66
|
-
self.dropout = dropout # Dropout probability for uncertainty quantification
|
67
|
-
self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') # Auto device chooser
|
68
|
-
self.model = None
|
69
|
-
self._is_fitted = False
|
70
|
-
self.__name = "OIKAN v0.0.2" # Manual configured version
|
71
|
-
self.loss_history = [] # <-- new attribute to store loss values
|
72
|
-
|
73
|
-
def _build_network(self, input_dim, output_dim):
|
74
|
-
layers = []
|
75
|
-
prev_dim = input_dim
|
76
|
-
for hidden_dim in self.hidden_dims:
|
77
|
-
layers.append(KANLayer(prev_dim, hidden_dim))
|
78
|
-
layers.append(nn.BatchNorm1d(hidden_dim)) # Added batch normalization
|
79
|
-
layers.append(nn.ReLU()) # Added activation function
|
80
|
-
layers.append(nn.Dropout(self.dropout)) # Apply dropout for uncertainty quantification
|
81
|
-
prev_dim = hidden_dim
|
82
|
-
layers.append(KANLayer(prev_dim, output_dim))
|
83
|
-
return nn.Sequential(*layers).to(self.device)
|
84
|
-
|
85
|
-
def _validate_data(self, X, y=None):
|
86
|
-
if not isinstance(X, torch.Tensor):
|
87
|
-
X = torch.FloatTensor(X)
|
88
|
-
if y is not None and not isinstance(y, torch.Tensor):
|
89
|
-
y = torch.FloatTensor(y)
|
90
|
-
return X.to(self.device), (y.to(self.device) if y is not None else None)
|
55
|
+
@abstractmethod
|
56
|
+
def fit(self, X, y):
|
57
|
+
pass
|
91
58
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
for term in edge_formula.split(" + "):
|
96
|
-
if term and term != "0":
|
97
|
-
if "*" in term:
|
98
|
-
coef_str, rest = term.split("*", 1)
|
99
|
-
try:
|
100
|
-
coef = float(coef_str)
|
101
|
-
terms.append(f"{(coef * weight):.4f}*{rest}")
|
102
|
-
except Exception:
|
103
|
-
terms.append(term) # fallback
|
104
|
-
else:
|
105
|
-
try:
|
106
|
-
terms.append(f"{(float(term) * weight):.4f}")
|
107
|
-
except Exception:
|
108
|
-
terms.append(term)
|
109
|
-
return " + ".join(terms) if terms else "0"
|
59
|
+
@abstractmethod
|
60
|
+
def predict(self, X):
|
61
|
+
pass
|
110
62
|
|
111
|
-
def
|
112
|
-
"""
|
113
|
-
if
|
114
|
-
raise
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
for i in range(n_features):
|
123
|
-
for j in range(n_classes):
|
124
|
-
weight = first_layer.combination_weights[i, j].item()
|
125
|
-
if abs(weight) > 1e-4:
|
126
|
-
# Use improved threshold for formula extraction
|
127
|
-
edge_formula = first_layer.edges[i][j].get_symbolic_repr(threshold=1e-6)
|
128
|
-
formulas[i][j] = self._process_edge_formula(edge_formula, weight)
|
129
|
-
else:
|
130
|
-
formulas[i][j] = "0"
|
131
|
-
self.symbolic_formula = formulas
|
132
|
-
return formulas
|
133
|
-
else: # Regressor
|
63
|
+
def get_formula(self):
|
64
|
+
"""Returns the symbolic formula(s) as a string or list of strings."""
|
65
|
+
if self.symbolic_model is None:
|
66
|
+
raise ValueError("Model not fitted yet.")
|
67
|
+
basis_functions = self.symbolic_model['basis_functions']
|
68
|
+
if 'coefficients' in self.symbolic_model:
|
69
|
+
coefficients = self.symbolic_model['coefficients']
|
70
|
+
formula = " + ".join([f"{coefficients[i]:.3f}*{basis_functions[i]}"
|
71
|
+
for i in range(len(coefficients)) if coefficients[i] != 0])
|
72
|
+
return formula if formula else "0"
|
73
|
+
else:
|
134
74
|
formulas = []
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
formulas.append(self._process_edge_formula(edge_formula, 1.0))
|
140
|
-
self.symbolic_formula = formulas
|
75
|
+
for c, coef in enumerate(self.symbolic_model['coefficients_list']):
|
76
|
+
formula = " + ".join([f"{coef[i]:.3f}*{basis_functions[i]}"
|
77
|
+
for i in range(len(coef)) if coef[i] != 0])
|
78
|
+
formulas.append(f"Class {self.classes_[c]}: {formula if formula else '0'}")
|
141
79
|
return formulas
|
142
80
|
|
143
|
-
def
|
144
|
-
"""
|
81
|
+
def feature_importances(self):
|
82
|
+
"""
|
83
|
+
Computes the importance of each original feature based on the symbolic model.
|
145
84
|
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
- A general formula, including softmax for classification
|
150
|
-
- Recommendations and performance results.
|
85
|
+
Returns:
|
86
|
+
--------
|
87
|
+
numpy.ndarray : Normalized feature importances.
|
151
88
|
"""
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
if hasattr(self, 'classes_'):
|
158
|
-
# For classifiers: formulas is a 2D list [feature][class]
|
159
|
-
for i, feature in enumerate(formulas):
|
160
|
-
for j, form in enumerate(feature):
|
161
|
-
formulas_text += f"Feature {i} - Class {j}: {form}\n"
|
162
|
-
general = ("\nGeneral Formula (with softmax):\n"
|
163
|
-
"For each class j: y_j = softmax( sum_i [ symbolic_formula(feature_i, class_j) ] )\n")
|
164
|
-
recs = ("\nRecommendations:\n"
|
165
|
-
"• Use the symbolic formulas for streamlined inference in production.\n"
|
166
|
-
"• Verify predictions with both the neural network and the compiled symbolic predictor.\n")
|
167
|
-
else:
|
168
|
-
# For regressors: formulas is a list
|
169
|
-
for i, form in enumerate(formulas):
|
170
|
-
formulas_text += f"Feature {i}: {form}\n"
|
171
|
-
general = ("\nGeneral Formula:\n"
|
172
|
-
"y = sum_i [ symbolic_formula(feature_i) ]\n")
|
173
|
-
recs = ("\nRecommendations:\n"
|
174
|
-
"• Consider the symbolic formula for lightweight and interpretable inference.\n"
|
175
|
-
"• Validate approximation accuracy against the neural model.\n")
|
176
|
-
|
177
|
-
# Disclaimer regarding experimental usage
|
178
|
-
disclaimer = ("\nDisclaimer:\n"
|
179
|
-
"This experimental model is intended for research purposes only and is not production-ready. "
|
180
|
-
"Feel free to fork and build your own project based on this research: "
|
181
|
-
"https://github.com/silvermete0r/oikan\n")
|
89
|
+
if self.symbolic_model is None:
|
90
|
+
raise ValueError("Model not fitted yet.")
|
91
|
+
basis_functions = self.symbolic_model['basis_functions']
|
92
|
+
n_features = self.symbolic_model['n_features']
|
93
|
+
importances = np.zeros(n_features)
|
182
94
|
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
95
|
+
# Handle regression case
|
96
|
+
if 'coefficients' in self.symbolic_model:
|
97
|
+
coefficients = self.symbolic_model['coefficients']
|
98
|
+
for i, func in enumerate(basis_functions):
|
99
|
+
if coefficients[i] != 0:
|
100
|
+
features_involved = get_features_involved(func)
|
101
|
+
for idx in features_involved:
|
102
|
+
importances[idx] += np.abs(coefficients[i])
|
103
|
+
# Handle classification case with multiple coefficient sets
|
104
|
+
else:
|
105
|
+
for coef in self.symbolic_model['coefficients_list']:
|
106
|
+
for i, func in enumerate(basis_functions):
|
107
|
+
if coef[i] != 0:
|
108
|
+
features_involved = get_features_involved(func)
|
109
|
+
for idx in features_involved:
|
110
|
+
importances[idx] += np.abs(coef[i])
|
192
111
|
|
193
|
-
|
194
|
-
return
|
112
|
+
total = importances.sum()
|
113
|
+
return importances / total if total > 0 else importances
|
195
114
|
|
196
|
-
def
|
197
|
-
"""
|
198
|
-
|
199
|
-
from .utils import ensure_tensor
|
200
|
-
|
201
|
-
if isinstance(x, (list, tuple)):
|
202
|
-
x = np.array(x)
|
115
|
+
def save(self, path):
|
116
|
+
"""
|
117
|
+
Saves the symbolic model to a .json file.
|
203
118
|
|
204
|
-
|
205
|
-
|
206
|
-
|
119
|
+
Parameters:
|
120
|
+
-----------
|
121
|
+
path : str
|
122
|
+
File path to save the model. Should end with .json
|
123
|
+
"""
|
124
|
+
if self.symbolic_model is None:
|
125
|
+
raise ValueError("Model not fitted yet.")
|
126
|
+
|
127
|
+
if not path.endswith('.json'):
|
128
|
+
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
|
+
}
|
207
136
|
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
if isinstance(result, torch.Tensor):
|
215
|
-
total += coef * result
|
216
|
-
else:
|
217
|
-
total += coef * ensure_tensor(result)
|
218
|
-
break
|
219
|
-
except Exception as e:
|
220
|
-
print(f"Warning: Error evaluating term {coef_str}*{func_name}: {str(e)}")
|
221
|
-
continue
|
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()
|
222
143
|
|
223
|
-
|
144
|
+
with open(path, 'w') as f:
|
145
|
+
json.dump(model_data, f, indent=2)
|
224
146
|
|
225
|
-
def
|
226
|
-
"""
|
227
|
-
|
228
|
-
raise NotFittedError("Model must be fitted before prediction")
|
229
|
-
|
230
|
-
X = np.array(X) if not isinstance(X, np.ndarray) else X
|
231
|
-
formulas = self.get_symbolic_formula()
|
232
|
-
predictions = np.zeros((X.shape[0], 1))
|
147
|
+
def load(self, path):
|
148
|
+
"""
|
149
|
+
Loads the symbolic model from a .json file.
|
233
150
|
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
151
|
+
Parameters:
|
152
|
+
-----------
|
153
|
+
path : str
|
154
|
+
File path to load the model from. Should end with .json
|
155
|
+
"""
|
156
|
+
if not path.endswith('.json'):
|
157
|
+
path = path + '.json'
|
158
|
+
|
159
|
+
with open(path, 'r') as f:
|
160
|
+
model_data = json.load(f)
|
243
161
|
|
244
|
-
|
162
|
+
self.symbolic_model = {
|
163
|
+
'n_features': model_data['n_features'],
|
164
|
+
'degree': model_data['degree'],
|
165
|
+
'basis_functions': model_data['basis_functions']
|
166
|
+
}
|
167
|
+
|
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'])
|
245
174
|
|
246
|
-
def
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
for coef_str, func_name in matches:
|
256
|
-
try:
|
257
|
-
coef = float(coef_str)
|
258
|
-
# Search for a matching basis function in ADVANCED_LIB (e.g. 'x', 'x^2', etc.)
|
259
|
-
for key, (notation, func) in ADVANCED_LIB.items():
|
260
|
-
if notation.strip() == func_name.strip():
|
261
|
-
compiled_terms.append((coef, func))
|
262
|
-
break
|
263
|
-
except Exception:
|
264
|
-
continue
|
265
|
-
def prediction_function(x):
|
266
|
-
pred = 0
|
267
|
-
for coef, func in compiled_terms:
|
268
|
-
pred += coef * func(x)
|
269
|
-
return pred
|
270
|
-
return prediction_function
|
175
|
+
def _train_neural_net(self, X, y, output_size, loss_fn):
|
176
|
+
"""Trains the neural network on the input data."""
|
177
|
+
input_size = X.shape[1]
|
178
|
+
self.neural_net = TabularNet(input_size, self.hidden_sizes, output_size, self.activation)
|
179
|
+
optimizer = optim.Adam(self.neural_net.parameters(), lr=self.lr)
|
180
|
+
dataset = torch.utils.data.TensorDataset(torch.tensor(X, dtype=torch.float32),
|
181
|
+
torch.tensor(y, dtype=torch.float32))
|
182
|
+
loader = torch.utils.data.DataLoader(dataset, batch_size=self.batch_size, shuffle=True)
|
183
|
+
self.neural_net.train()
|
271
184
|
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
raise NotFittedError("No model to save. Build and train a model first.")
|
276
|
-
save_dict = {'state_dict': self.model.state_dict()}
|
277
|
-
if hasattr(self, "classes_"):
|
278
|
-
# Save classes_ as a list so that it can be reloaded.
|
279
|
-
save_dict['classes_'] = self.classes_.tolist()
|
280
|
-
torch.save(save_dict, filepath)
|
281
|
-
print(f"Model saved to {filepath}")
|
282
|
-
|
283
|
-
def load_model(self, filepath="models/oikan_model.pth", input_dim=None, output_dim=None):
|
284
|
-
"""Load the model's state dictionary and extra attributes from a file.
|
285
|
-
|
286
|
-
If the model architecture does not exist, it is automatically rebuilt using provided
|
287
|
-
input_dim and output_dim.
|
288
|
-
"""
|
289
|
-
if self.model is None:
|
290
|
-
if input_dim is None or output_dim is None:
|
291
|
-
raise NotFittedError("No model architecture available. Provide input_dim and output_dim to rebuild the model.")
|
292
|
-
self.model = self._build_network(input_dim, output_dim)
|
293
|
-
loaded = torch.load(filepath, map_location=self.device)
|
294
|
-
if isinstance(loaded, dict) and 'state_dict' in loaded:
|
295
|
-
self.model.load_state_dict(loaded['state_dict'])
|
296
|
-
if 'classes_' in loaded:
|
297
|
-
self.classes_ = torch.tensor(loaded['classes_'])
|
185
|
+
if self.verbose:
|
186
|
+
from tqdm import tqdm
|
187
|
+
epoch_iterator = tqdm(range(self.epochs), desc="Training")
|
298
188
|
else:
|
299
|
-
self.
|
300
|
-
self._is_fitted = True # Mark model as fitted after loading
|
301
|
-
print(f"Model loaded from {filepath}")
|
189
|
+
epoch_iterator = range(self.epochs)
|
302
190
|
|
303
|
-
|
304
|
-
|
305
|
-
|
191
|
+
for epoch in epoch_iterator:
|
192
|
+
total_loss = 0
|
193
|
+
for batch_X, batch_y in loader:
|
194
|
+
optimizer.zero_grad()
|
195
|
+
outputs = self.neural_net(batch_X)
|
196
|
+
loss = loss_fn(outputs, batch_y)
|
197
|
+
loss.backward()
|
198
|
+
optimizer.step()
|
199
|
+
total_loss += loss.item()
|
306
200
|
|
307
|
-
|
308
|
-
|
309
|
-
def fit(self, X, y, epochs=100, lr=0.01, verbose=True):
|
310
|
-
X, y = self._validate_data(X, y)
|
311
|
-
if len(y.shape) == 1:
|
312
|
-
y = y.reshape(-1, 1)
|
313
|
-
|
314
|
-
if self.model is None:
|
315
|
-
self.model = self._build_network(X.shape[1], y.shape[1])
|
316
|
-
|
317
|
-
criterion = nn.MSELoss()
|
318
|
-
optimizer = torch.optim.Adam(self.model.parameters(), lr=lr, weight_decay=1e-5)
|
319
|
-
|
320
|
-
self.model.train()
|
321
|
-
self.loss_history = [] # <-- reset loss history at start of training
|
322
|
-
for epoch in range(epochs):
|
323
|
-
optimizer.zero_grad()
|
324
|
-
y_pred = self.model(X)
|
325
|
-
loss = criterion(y_pred, y)
|
326
|
-
|
327
|
-
if torch.isnan(loss):
|
328
|
-
print("Warning: NaN loss detected, reinitializing model...")
|
329
|
-
self.model = None
|
330
|
-
return self.fit(X, y, epochs, lr/10, verbose)
|
331
|
-
|
332
|
-
loss.backward()
|
333
|
-
|
334
|
-
# Clip gradients
|
335
|
-
torch.nn.utils.clip_grad_norm_(self.model.parameters(), max_norm=1.0)
|
336
|
-
|
337
|
-
optimizer.step()
|
338
|
-
|
339
|
-
self.loss_history.append(loss.item()) # <-- save loss value for epoch
|
340
|
-
|
341
|
-
if verbose and (epoch + 1) % 10 == 0:
|
342
|
-
print(f"Epoch [{epoch+1}/{epochs}], Loss: {loss.item():.4f}")
|
343
|
-
|
344
|
-
self._is_fitted = True
|
345
|
-
return self
|
201
|
+
if self.verbose:
|
202
|
+
epoch_iterator.set_postfix({'loss': f'{total_loss/len(loader):.4f}'})
|
346
203
|
|
347
|
-
def
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
204
|
+
def _generate_augmented_data(self, X):
|
205
|
+
"""Generates augmented data by adding Gaussian noise."""
|
206
|
+
n_samples = X.shape[0]
|
207
|
+
X_aug = []
|
208
|
+
for _ in range(self.augmentation_factor):
|
209
|
+
noise = np.random.normal(0, self.sigma, X.shape)
|
210
|
+
X_perturbed = X + noise
|
211
|
+
X_aug.append(X_perturbed)
|
212
|
+
return np.vstack(X_aug)
|
355
213
|
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
self.
|
361
|
-
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
214
|
+
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)
|
220
|
+
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]
|
223
|
+
self.symbolic_model = {
|
224
|
+
'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()
|
228
|
+
}
|
229
|
+
else:
|
230
|
+
coefficients_list = []
|
231
|
+
# Note: Using the same basis functions across classes for simplicity
|
232
|
+
selected_indices = set()
|
233
|
+
for c in range(y.shape[1]):
|
234
|
+
coef = model.coef_[c]
|
235
|
+
indices = np.where(np.abs(coef) > 1e-6)[0]
|
236
|
+
selected_indices.update(indices)
|
237
|
+
selected_indices = list(selected_indices)
|
238
|
+
basis_functions = poly.get_feature_names_out()[selected_indices].tolist()
|
239
|
+
for c in range(y.shape[1]):
|
240
|
+
coef = model.coef_[c]
|
241
|
+
coef_selected = coef[selected_indices].tolist()
|
242
|
+
coefficients_list.append(coef_selected)
|
243
|
+
self.symbolic_model = {
|
244
|
+
'n_features': X.shape[1],
|
245
|
+
'degree': self.polynomial_degree,
|
246
|
+
'basis_functions': basis_functions,
|
247
|
+
'coefficients_list': coefficients_list
|
248
|
+
}
|
391
249
|
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
250
|
+
class OIKANRegressor(OIKAN):
|
251
|
+
"""OIKAN model for regression tasks."""
|
252
|
+
def fit(self, X, y):
|
253
|
+
"""
|
254
|
+
Fits the regressor to the data.
|
255
|
+
|
256
|
+
Parameters:
|
257
|
+
-----------
|
258
|
+
X : array-like of shape (n_samples, n_features)
|
259
|
+
Training data.
|
260
|
+
y : array-like of shape (n_samples,)
|
261
|
+
Target values.
|
262
|
+
"""
|
263
|
+
X = np.asarray(X)
|
264
|
+
y = np.asarray(y).reshape(-1, 1)
|
265
|
+
self._train_neural_net(X, y, output_size=1, loss_fn=nn.MSELoss())
|
266
|
+
X_aug = self._generate_augmented_data(X)
|
267
|
+
self.neural_net.eval()
|
398
268
|
with torch.no_grad():
|
399
|
-
|
400
|
-
|
401
|
-
probs = torch.sigmoid(logits)
|
402
|
-
return np.column_stack([1 - probs.cpu().numpy(), probs.cpu().numpy()])
|
403
|
-
else:
|
404
|
-
return torch.softmax(logits, dim=1).cpu().numpy()
|
269
|
+
y_aug = self.neural_net(torch.tensor(X_aug, dtype=torch.float32)).detach().numpy()
|
270
|
+
self._perform_symbolic_regression(X_aug, y_aug)
|
405
271
|
|
406
272
|
def predict(self, X):
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
def symbolic_predict_proba(self, X):
|
411
|
-
"""Predict class probabilities using only the extracted symbolic formula."""
|
412
|
-
if not self._is_fitted:
|
413
|
-
raise NotFittedError("Model must be fitted before prediction")
|
414
|
-
|
415
|
-
if not isinstance(X, np.ndarray):
|
416
|
-
X = np.array(X)
|
417
|
-
|
418
|
-
# Scale input data similar to training
|
419
|
-
X_scaled = (X - X.mean(axis=0)) / (X.std(axis=0) + 1e-8)
|
420
|
-
|
421
|
-
formulas = self.get_symbolic_formula()
|
422
|
-
n_classes = len(self.classes_)
|
423
|
-
predictions = np.zeros((X.shape[0], n_classes))
|
424
|
-
|
425
|
-
# Evaluate each feature's contribution to each class
|
426
|
-
for i in range(X.shape[1]): # For each feature
|
427
|
-
x = X_scaled[:, i] # Use scaled data
|
428
|
-
for j in range(n_classes): # For each class
|
429
|
-
formula = formulas[i][j]
|
430
|
-
if formula and formula != "0":
|
431
|
-
predictions[:, j] += self._eval_formula(formula, x)
|
432
|
-
|
433
|
-
# Apply softmax with temperature for better separation
|
434
|
-
temperature = 1.0
|
435
|
-
exp_preds = np.exp(predictions / temperature)
|
436
|
-
probas = exp_preds / exp_preds.sum(axis=1, keepdims=True)
|
273
|
+
"""
|
274
|
+
Predicts target values for the input data.
|
437
275
|
|
438
|
-
|
439
|
-
|
440
|
-
|
276
|
+
Parameters:
|
277
|
+
-----------
|
278
|
+
X : array-like of shape (n_samples, n_features)
|
279
|
+
Input data.
|
441
280
|
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
281
|
+
Returns:
|
282
|
+
--------
|
283
|
+
y_pred : ndarray of shape (n_samples,)
|
284
|
+
Predicted values.
|
285
|
+
"""
|
286
|
+
if self.symbolic_model is None:
|
287
|
+
raise ValueError("Model not fitted yet.")
|
288
|
+
X = np.asarray(X)
|
289
|
+
X_transformed = evaluate_basis_functions(X, self.symbolic_model['basis_functions'],
|
290
|
+
self.symbolic_model['n_features'])
|
291
|
+
return np.dot(X_transformed, self.symbolic_model['coefficients'])
|
292
|
+
|
293
|
+
class OIKANClassifier(OIKAN):
|
294
|
+
"""OIKAN model for classification tasks."""
|
295
|
+
def fit(self, X, y):
|
296
|
+
"""
|
297
|
+
Fits the classifier to the data.
|
448
298
|
|
449
|
-
|
299
|
+
Parameters:
|
300
|
+
-----------
|
301
|
+
X : array-like of shape (n_samples, n_features)
|
302
|
+
Training data.
|
303
|
+
y : array-like of shape (n_samples,)
|
304
|
+
Target labels.
|
305
|
+
"""
|
306
|
+
X = np.asarray(X)
|
307
|
+
from sklearn.preprocessing import LabelEncoder
|
308
|
+
le = LabelEncoder()
|
309
|
+
y_encoded = le.fit_transform(y)
|
310
|
+
self.classes_ = le.classes_
|
450
311
|
n_classes = len(self.classes_)
|
451
|
-
|
312
|
+
y_onehot = nn.functional.one_hot(torch.tensor(y_encoded), num_classes=n_classes).float()
|
313
|
+
self._train_neural_net(X, y_onehot, output_size=n_classes, loss_fn=nn.CrossEntropyLoss())
|
314
|
+
X_aug = self._generate_augmented_data(X)
|
315
|
+
self.neural_net.eval()
|
316
|
+
with torch.no_grad():
|
317
|
+
logits_aug = self.neural_net(torch.tensor(X_aug, dtype=torch.float32)).detach().numpy()
|
318
|
+
self._perform_symbolic_regression(X_aug, logits_aug)
|
319
|
+
|
320
|
+
def predict(self, X):
|
321
|
+
"""
|
322
|
+
Predicts class labels for the input data.
|
452
323
|
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
weight = first_layer.combination_weights[i, j].item()
|
458
|
-
|
459
|
-
if abs(weight) > 1e-4:
|
460
|
-
# Improved precision by using a lower threshold
|
461
|
-
edge_formula = edge.get_symbolic_repr(threshold=1e-6)
|
462
|
-
terms = []
|
463
|
-
for term in edge_formula.split(" + "):
|
464
|
-
if term and term != "0":
|
465
|
-
if "*" in term:
|
466
|
-
coef, rest = term.split("*", 1)
|
467
|
-
coef = float(coef) * weight
|
468
|
-
terms.append(f"{coef:.4f}*{rest}")
|
469
|
-
else:
|
470
|
-
terms.append(f"{float(term) * weight:.4f}")
|
471
|
-
|
472
|
-
formulas[i][j] = " + ".join(terms) if terms else "0"
|
473
|
-
else:
|
474
|
-
formulas[i][j] = "0"
|
324
|
+
Parameters:
|
325
|
+
-----------
|
326
|
+
X : array-like of shape (n_samples, n_features)
|
327
|
+
Input data.
|
475
328
|
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
329
|
+
Returns:
|
330
|
+
--------
|
331
|
+
y_pred : ndarray of shape (n_samples,)
|
332
|
+
Predicted class labels.
|
333
|
+
"""
|
334
|
+
if self.symbolic_model is None:
|
335
|
+
raise ValueError("Model not fitted yet.")
|
336
|
+
X = np.asarray(X)
|
337
|
+
X_transformed = evaluate_basis_functions(X, self.symbolic_model['basis_functions'],
|
338
|
+
self.symbolic_model['n_features'])
|
339
|
+
logits = np.dot(X_transformed, np.array(self.symbolic_model['coefficients_list']).T)
|
340
|
+
probabilities = nn.functional.softmax(torch.tensor(logits), dim=1).numpy()
|
341
|
+
return self.classes_[np.argmax(probabilities, axis=1)]
|