oikan 0.0.1.11__py3-none-any.whl → 0.0.2.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/exceptions.py +15 -0
- oikan/model.py +422 -83
- oikan/symbolic.py +24 -125
- oikan/utils.py +39 -36
- oikan-0.0.2.1.dist-info/METADATA +160 -0
- oikan-0.0.2.1.dist-info/RECORD +10 -0
- {oikan-0.0.1.11.dist-info → oikan-0.0.2.1.dist-info}/WHEEL +1 -1
- oikan/metrics.py +0 -48
- oikan/regularization.py +0 -30
- oikan/trainer.py +0 -49
- oikan/visualize.py +0 -69
- oikan-0.0.1.11.dist-info/METADATA +0 -105
- oikan-0.0.1.11.dist-info/RECORD +0 -13
- {oikan-0.0.1.11.dist-info → oikan-0.0.2.1.dist-info/licenses}/LICENSE +0 -0
- {oikan-0.0.1.11.dist-info → oikan-0.0.2.1.dist-info}/top_level.txt +0 -0
oikan/exceptions.py
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
class OikanError(Exception):
|
2
|
+
"""Base exception class for OIKAN"""
|
3
|
+
pass
|
4
|
+
|
5
|
+
class NotFittedError(OikanError):
|
6
|
+
"""Raised when prediction is attempted on unfitted model"""
|
7
|
+
pass
|
8
|
+
|
9
|
+
class DataError(OikanError):
|
10
|
+
"""Raised when there are issues with input data"""
|
11
|
+
pass
|
12
|
+
|
13
|
+
class InitializationError(OikanError):
|
14
|
+
"""Raised when model initialization fails"""
|
15
|
+
pass
|
oikan/model.py
CHANGED
@@ -1,99 +1,438 @@
|
|
1
1
|
import torch
|
2
2
|
import torch.nn as nn
|
3
|
-
|
3
|
+
import numpy as np
|
4
|
+
from sklearn.base import BaseEstimator, RegressorMixin, ClassifierMixin
|
5
|
+
from .utils import ADVANCED_LIB, EdgeActivation
|
6
|
+
from .exceptions import *
|
7
|
+
from datetime import datetime as dt
|
4
8
|
|
5
|
-
class
|
6
|
-
|
7
|
-
def __init__(self
|
9
|
+
class SymbolicEdge(nn.Module):
|
10
|
+
"""Edge-based activation function learner"""
|
11
|
+
def __init__(self):
|
8
12
|
super().__init__()
|
9
|
-
self.
|
10
|
-
self.bias = nn.Parameter(torch.zeros(hidden_dim))
|
13
|
+
self.activation = EdgeActivation()
|
11
14
|
|
12
15
|
def forward(self, x):
|
13
|
-
|
14
|
-
|
16
|
+
return self.activation(x)
|
17
|
+
|
18
|
+
def get_symbolic_repr(self, threshold=1e-4):
|
19
|
+
return self.activation.get_symbolic_repr(threshold)
|
15
20
|
|
16
|
-
class
|
17
|
-
|
18
|
-
def __init__(self, input_dim,
|
21
|
+
class KANLayer(nn.Module):
|
22
|
+
"""Kolmogorov-Arnold Network layer with interpretable edges"""
|
23
|
+
def __init__(self, input_dim, output_dim):
|
19
24
|
super().__init__()
|
20
25
|
self.input_dim = input_dim
|
21
|
-
self.
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
self.basis_output_dim = input_dim * (hidden_units - 4)
|
28
|
-
elif basis_type == 'fourier':
|
29
|
-
# Use Fourier basis transformation for each feature
|
30
|
-
self.basis_functions = nn.ModuleList([FourierBasis(hidden_units // 2) for _ in range(input_dim)])
|
31
|
-
self.basis_output_dim = input_dim * hidden_units
|
32
|
-
elif basis_type == 'combo':
|
33
|
-
# Combine BSpline and Fourier basis on a per-feature basis
|
34
|
-
self.basis_functions_bspline = nn.ModuleList([BSplineBasis(hidden_units) for _ in range(input_dim)])
|
35
|
-
self.basis_functions_fourier = nn.ModuleList([FourierBasis(hidden_units // 2) for _ in range(input_dim)])
|
36
|
-
self.basis_output_dim = input_dim * ((hidden_units - 4) + hidden_units)
|
37
|
-
else:
|
38
|
-
raise ValueError(f"Unsupported basis_type: {basis_type}")
|
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
|
+
])
|
39
32
|
|
40
|
-
|
41
|
-
self.interaction_weights = nn.Parameter(torch.randn(input_dim, input_dim))
|
33
|
+
self.combination_weights = nn.Parameter(torch.randn(input_dim, output_dim) * 0.1)
|
42
34
|
|
43
35
|
def forward(self, x):
|
44
|
-
#
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
basis_output = torch.cat(transformed_features, dim=1)
|
52
|
-
|
53
|
-
# Compute interaction features via fixed matrix multiplication
|
54
|
-
batch_size = x.size(0)
|
55
|
-
x_reshaped = x.view(batch_size, self.input_dim, 1) # Reshape to [batch_size, input_dim, 1]
|
56
|
-
interaction_matrix = torch.sigmoid(self.interaction_weights) # Normalize interaction weights
|
57
|
-
interaction_features = torch.bmm(x_reshaped.transpose(1, 2),
|
58
|
-
x_reshaped * interaction_matrix.unsqueeze(0)) # Result: [batch_size, 1, 1]
|
59
|
-
interaction_features = interaction_features.view(batch_size, -1) # Flatten interaction output
|
60
|
-
|
61
|
-
return torch.cat([basis_output, interaction_features], dim=1)
|
36
|
+
x_split = x.split(1, dim=1) # list of (batch, 1) tensors for each input feature
|
37
|
+
edge_outputs = torch.stack([
|
38
|
+
torch.stack([edge(x_i).squeeze() for edge in edge_list], dim=1)
|
39
|
+
for x_i, edge_list in zip(x_split, self.edges)
|
40
|
+
], dim=1) # shape: (batch, input_dim, output_dim)
|
41
|
+
combined = edge_outputs * self.combination_weights.unsqueeze(0)
|
42
|
+
return combined.sum(dim=1)
|
62
43
|
|
63
|
-
def
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
44
|
+
def get_symbolic_formula(self):
|
45
|
+
"""Extract interpretable formulas for each output"""
|
46
|
+
formulas = []
|
47
|
+
for j in range(self.output_dim):
|
48
|
+
terms = []
|
49
|
+
for i in range(self.input_dim):
|
50
|
+
weight = self.combination_weights[i, j].item()
|
51
|
+
if abs(weight) > 1e-4:
|
52
|
+
edge_formula = self.edges[i][j].get_symbolic_repr()
|
53
|
+
if edge_formula != "0":
|
54
|
+
terms.append(f"({weight:.4f} * ({edge_formula}))")
|
55
|
+
formulas.append(" + ".join(terms) if terms else "0")
|
56
|
+
return formulas
|
57
|
+
|
58
|
+
class BaseOIKAN(BaseEstimator):
|
59
|
+
"""Base OIKAN model implementing common functionality"""
|
60
|
+
def __init__(self, hidden_dims=[64, 32], num_basis=10, degree=3, dropout=0.1):
|
61
|
+
self.hidden_dims = hidden_dims
|
62
|
+
self.num_basis = num_basis
|
63
|
+
self.degree = degree
|
64
|
+
self.dropout = dropout # Dropout probability for uncertainty quantification
|
65
|
+
self.device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') # Auto device chooser
|
66
|
+
self.model = None
|
67
|
+
self._is_fitted = False
|
68
|
+
self.__name = "OIKAN v0.0.2" # Version info (manually configured)
|
69
|
+
self.loss_history = [] # <-- new attribute to store loss values
|
70
|
+
|
71
|
+
def _build_network(self, input_dim, output_dim):
|
72
|
+
layers = []
|
73
|
+
prev_dim = input_dim
|
74
|
+
for hidden_dim in self.hidden_dims:
|
75
|
+
layers.append(KANLayer(prev_dim, hidden_dim))
|
76
|
+
layers.append(nn.Dropout(self.dropout)) # Apply dropout for uncertainty quantification
|
77
|
+
prev_dim = hidden_dim
|
78
|
+
layers.append(KANLayer(prev_dim, output_dim))
|
79
|
+
return nn.Sequential(*layers).to(self.device)
|
80
|
+
|
81
|
+
def _validate_data(self, X, y=None):
|
82
|
+
if not isinstance(X, torch.Tensor):
|
83
|
+
X = torch.FloatTensor(X)
|
84
|
+
if y is not None and not isinstance(y, torch.Tensor):
|
85
|
+
y = torch.FloatTensor(y)
|
86
|
+
return X.to(self.device), (y.to(self.device) if y is not None else None)
|
87
|
+
|
88
|
+
def get_symbolic_formula(self):
|
89
|
+
"""Generate and cache symbolic formulas for production‐ready inference."""
|
90
|
+
if not self._is_fitted:
|
91
|
+
raise NotFittedError("Model must be fitted before extracting formulas")
|
92
|
+
if hasattr(self, "symbolic_formula"):
|
93
|
+
return self.symbolic_formula
|
94
|
+
if hasattr(self, 'classes_'): # Classifier
|
95
|
+
n_features = self.model[0].input_dim
|
96
|
+
n_classes = len(self.classes_)
|
97
|
+
formulas = [[None for _ in range(n_classes)] for _ in range(n_features)]
|
98
|
+
first_layer = self.model[0]
|
99
|
+
for i in range(n_features):
|
100
|
+
for j in range(n_classes):
|
101
|
+
weight = first_layer.combination_weights[i, j].item()
|
102
|
+
if abs(weight) > 1e-4:
|
103
|
+
edge_formula = first_layer.edges[i][j].get_symbolic_repr()
|
104
|
+
terms = []
|
105
|
+
for term in edge_formula.split(" + "):
|
106
|
+
if term and term != "0":
|
107
|
+
if "*" in term:
|
108
|
+
coef, rest = term.split("*", 1)
|
109
|
+
coef = float(coef) * weight
|
110
|
+
terms.append(f"{coef:.4f}*{rest}")
|
111
|
+
else:
|
112
|
+
terms.append(f"{float(term)*weight:.4f}")
|
113
|
+
formulas[i][j] = " + ".join(terms) if terms else "0"
|
114
|
+
else:
|
115
|
+
formulas[i][j] = "0"
|
116
|
+
self.symbolic_formula = formulas
|
117
|
+
return formulas
|
118
|
+
else: # Regressor
|
119
|
+
formulas = []
|
120
|
+
first_layer = self.model[0]
|
121
|
+
for i in range(first_layer.input_dim):
|
122
|
+
formula = first_layer.edges[i][0].get_symbolic_repr()
|
123
|
+
formulas.append(formula)
|
124
|
+
self.symbolic_formula = formulas
|
125
|
+
return formulas
|
126
|
+
|
127
|
+
def save_symbolic_formula(self, filename="outputs/symbolic_formula.txt"):
|
128
|
+
"""Save the cached symbolic formulas to file for production use.
|
129
|
+
|
130
|
+
The file will contain:
|
131
|
+
- A header with the version and timestamp
|
132
|
+
- The symbolic formulas for each feature (and class for classification)
|
133
|
+
- A general formula, including softmax for classification
|
134
|
+
- Recommendations for production use.
|
135
|
+
"""
|
136
|
+
header = f"Generated by {self.__name} | Timestamp: {dt.now()}\n\n"
|
137
|
+
header += "Symbolic Formulas:\n"
|
138
|
+
header += "====================\n"
|
139
|
+
formulas = self.get_symbolic_formula()
|
140
|
+
formulas_text = ""
|
141
|
+
if hasattr(self, 'classes_'):
|
142
|
+
# For classifiers: formulas is a 2D list [feature][class]
|
143
|
+
for i, feature in enumerate(formulas):
|
144
|
+
for j, form in enumerate(feature):
|
145
|
+
formulas_text += f"Feature {i} - Class {j}: {form}\n"
|
146
|
+
general = ("\nGeneral Formula (with softmax):\n"
|
147
|
+
"For each class j: y_j = softmax( sum_i [ symbolic_formula(feature_i, class_j) ] )\n")
|
148
|
+
recs = ("\nRecommendations:\n"
|
149
|
+
"• Use the symbolic formulas for streamlined inference in production.\n"
|
150
|
+
"• Verify predictions with both the neural network and the compiled symbolic predictor.\n")
|
79
151
|
else:
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
152
|
+
# For regressors: formulas is a list
|
153
|
+
for i, form in enumerate(formulas):
|
154
|
+
formulas_text += f"Feature {i}: {form}\n"
|
155
|
+
general = ("\nGeneral Formula:\n"
|
156
|
+
"y = sum_i [ symbolic_formula(feature_i) ]\n")
|
157
|
+
recs = ("\nRecommendations:\n"
|
158
|
+
"• Consider the symbolic formula for lightweight and interpretable inference.\n"
|
159
|
+
"• Validate approximation accuracy against the neural model.\n")
|
160
|
+
|
161
|
+
output = header + formulas_text + general + recs
|
162
|
+
with open(filename, "w") as f:
|
163
|
+
f.write(output)
|
164
|
+
print(f"Symbolic formulas saved to {filename}")
|
89
165
|
|
90
|
-
def
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
166
|
+
def get_feature_scores(self):
|
167
|
+
"""Get feature importance scores based on edge weights."""
|
168
|
+
if not self._is_fitted:
|
169
|
+
raise NotFittedError("Model must be fitted before computing scores")
|
170
|
+
|
171
|
+
weights = self.model[0].combination_weights.detach().cpu().numpy()
|
172
|
+
return np.mean(np.abs(weights), axis=1)
|
173
|
+
|
174
|
+
def _eval_formula(self, formula, x):
|
175
|
+
"""Helper to evaluate a symbolic formula for an input vector x using ADVANCED_LIB basis functions."""
|
176
|
+
import re
|
177
|
+
total = 0
|
178
|
+
pattern = re.compile(r"(-?\d+\.\d+)\*?([\w\(\)\^]+)")
|
179
|
+
matches = pattern.findall(formula)
|
180
|
+
for coef_str, func_name in matches:
|
181
|
+
try:
|
182
|
+
coef = float(coef_str)
|
183
|
+
for key, (notation, func) in ADVANCED_LIB.items():
|
184
|
+
if notation.strip() == func_name.strip():
|
185
|
+
total += coef * func(x)
|
186
|
+
break
|
187
|
+
except Exception:
|
188
|
+
continue
|
189
|
+
return total
|
190
|
+
|
191
|
+
def symbolic_predict(self, X):
|
192
|
+
"""Predict using only the extracted symbolic formula (regressor)."""
|
193
|
+
if not self._is_fitted:
|
194
|
+
raise NotFittedError("Model must be fitted before prediction")
|
195
|
+
X = np.array(X) if not isinstance(X, np.ndarray) else X
|
196
|
+
formulas = self.get_symbolic_formula() # For regressor: list of formula strings.
|
197
|
+
predictions = np.zeros((X.shape[0], 1))
|
198
|
+
for i, formula in enumerate(formulas):
|
199
|
+
x = X[:, i]
|
200
|
+
predictions[:, 0] += self._eval_formula(formula, x)
|
201
|
+
return predictions
|
202
|
+
|
203
|
+
def compile_symbolic_formula(self, filename="output/final_symbolic_formula.txt"):
|
204
|
+
import re
|
205
|
+
from .utils import ADVANCED_LIB # needed to retrieve basis functions
|
206
|
+
with open(filename, "r") as f:
|
207
|
+
content = f.read()
|
208
|
+
# Regex to extract coefficient and function notation.
|
209
|
+
# Matches patterns like: "(-?\d+\.\d+)\*?([\w\(\)\^]+)"
|
210
|
+
matches = re.findall(r"(-?\d+\.\d+)\*?([\w\(\)\^]+)", content)
|
211
|
+
compiled_terms = []
|
212
|
+
for coef_str, func_name in matches:
|
213
|
+
try:
|
214
|
+
coef = float(coef_str)
|
215
|
+
# Search for a matching basis function in ADVANCED_LIB (e.g. 'x', 'x^2', etc.)
|
216
|
+
for key, (notation, func) in ADVANCED_LIB.items():
|
217
|
+
if notation.strip() == func_name.strip():
|
218
|
+
compiled_terms.append((coef, func))
|
219
|
+
break
|
220
|
+
except Exception:
|
221
|
+
continue
|
222
|
+
def prediction_function(x):
|
223
|
+
pred = 0
|
224
|
+
for coef, func in compiled_terms:
|
225
|
+
pred += coef * func(x)
|
226
|
+
return pred
|
227
|
+
return prediction_function
|
228
|
+
|
229
|
+
def save_model(self, filepath="models/oikan_model.pth"):
|
230
|
+
"""Save the current model's state dictionary and extra attributes to a file."""
|
231
|
+
if self.model is None:
|
232
|
+
raise NotFittedError("No model to save. Build and train a model first.")
|
233
|
+
save_dict = {'state_dict': self.model.state_dict()}
|
234
|
+
if hasattr(self, "classes_"):
|
235
|
+
# Save classes_ as a list so that it can be reloaded.
|
236
|
+
save_dict['classes_'] = self.classes_.tolist()
|
237
|
+
torch.save(save_dict, filepath)
|
238
|
+
print(f"Model saved to {filepath}")
|
239
|
+
|
240
|
+
def load_model(self, filepath="models/oikan_model.pth", input_dim=None, output_dim=None):
|
241
|
+
"""Load the model's state dictionary and extra attributes from a file.
|
242
|
+
|
243
|
+
If the model architecture does not exist, it is automatically rebuilt using provided
|
244
|
+
input_dim and output_dim.
|
245
|
+
"""
|
246
|
+
if self.model is None:
|
247
|
+
if input_dim is None or output_dim is None:
|
248
|
+
raise NotFittedError("No model architecture available. Provide input_dim and output_dim to rebuild the model.")
|
249
|
+
self.model = self._build_network(input_dim, output_dim)
|
250
|
+
loaded = torch.load(filepath, map_location=self.device)
|
251
|
+
if isinstance(loaded, dict) and 'state_dict' in loaded:
|
252
|
+
self.model.load_state_dict(loaded['state_dict'])
|
253
|
+
if 'classes_' in loaded:
|
254
|
+
self.classes_ = torch.tensor(loaded['classes_'])
|
95
255
|
else:
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
256
|
+
self.model.load_state_dict(loaded)
|
257
|
+
self._is_fitted = True # Mark model as fitted after loading
|
258
|
+
print(f"Model loaded from {filepath}")
|
259
|
+
|
260
|
+
def get_loss_history(self):
|
261
|
+
"""Retrieve training loss history."""
|
262
|
+
return self.loss_history
|
263
|
+
|
264
|
+
class OIKANRegressor(BaseOIKAN, RegressorMixin):
|
265
|
+
"""OIKAN implementation for regression tasks"""
|
266
|
+
def fit(self, X, y, epochs=100, lr=0.01, batch_size=32, verbose=True):
|
267
|
+
X, y = self._validate_data(X, y)
|
268
|
+
if len(y.shape) == 1:
|
269
|
+
y = y.reshape(-1, 1)
|
270
|
+
|
271
|
+
if self.model is None:
|
272
|
+
self.model = self._build_network(X.shape[1], y.shape[1])
|
273
|
+
|
274
|
+
criterion = nn.MSELoss()
|
275
|
+
optimizer = torch.optim.Adam(self.model.parameters(), lr=lr, weight_decay=1e-5)
|
276
|
+
|
277
|
+
self.model.train()
|
278
|
+
self.loss_history = [] # <-- reset loss history at start of training
|
279
|
+
for epoch in range(epochs):
|
280
|
+
optimizer.zero_grad()
|
281
|
+
y_pred = self.model(X)
|
282
|
+
loss = criterion(y_pred, y)
|
283
|
+
|
284
|
+
if torch.isnan(loss):
|
285
|
+
print("Warning: NaN loss detected, reinitializing model...")
|
286
|
+
self.model = None
|
287
|
+
return self.fit(X, y, epochs, lr/10, batch_size, verbose)
|
288
|
+
|
289
|
+
loss.backward()
|
290
|
+
|
291
|
+
# Clip gradients
|
292
|
+
torch.nn.utils.clip_grad_norm_(self.model.parameters(), max_norm=1.0)
|
293
|
+
|
294
|
+
optimizer.step()
|
295
|
+
|
296
|
+
self.loss_history.append(loss.item()) # <-- save loss value for epoch
|
297
|
+
|
298
|
+
if verbose and (epoch + 1) % 10 == 0:
|
299
|
+
print(f"Epoch [{epoch+1}/{epochs}], Loss: {loss.item():.4f}")
|
300
|
+
|
301
|
+
self._is_fitted = True
|
302
|
+
return self
|
303
|
+
|
304
|
+
def predict(self, X):
|
305
|
+
if not self._is_fitted:
|
306
|
+
raise NotFittedError("Model must be fitted before prediction")
|
307
|
+
|
308
|
+
X = self._validate_data(X)[0]
|
309
|
+
self.model.eval()
|
310
|
+
with torch.no_grad():
|
311
|
+
return self.model(X).cpu().numpy()
|
312
|
+
|
313
|
+
class OIKANClassifier(BaseOIKAN, ClassifierMixin):
|
314
|
+
"""OIKAN implementation for classification tasks"""
|
315
|
+
def fit(self, X, y, epochs=100, lr=0.01, batch_size=32, verbose=True):
|
316
|
+
X, y = self._validate_data(X, y)
|
317
|
+
self.classes_ = torch.unique(y)
|
318
|
+
n_classes = len(self.classes_)
|
319
|
+
|
320
|
+
if self.model is None:
|
321
|
+
self.model = self._build_network(X.shape[1], 1 if n_classes == 2 else n_classes)
|
322
|
+
|
323
|
+
criterion = (nn.BCEWithLogitsLoss() if n_classes == 2
|
324
|
+
else nn.CrossEntropyLoss())
|
325
|
+
optimizer = torch.optim.Adam(self.model.parameters(), lr=lr)
|
326
|
+
|
327
|
+
self.model.train()
|
328
|
+
self.loss_history = [] # <-- reset loss history at start of training
|
329
|
+
for epoch in range(epochs):
|
330
|
+
optimizer.zero_grad()
|
331
|
+
logits = self.model(X)
|
332
|
+
if n_classes == 2:
|
333
|
+
y_tensor = y.float()
|
334
|
+
logits = logits.squeeze()
|
335
|
+
else:
|
336
|
+
y_tensor = y.long()
|
337
|
+
loss = criterion(logits, y_tensor)
|
338
|
+
loss.backward()
|
339
|
+
optimizer.step()
|
340
|
+
|
341
|
+
self.loss_history.append(loss.item()) # <-- save loss value for epoch
|
342
|
+
|
343
|
+
if verbose and (epoch + 1) % 10 == 0:
|
344
|
+
print(f"Epoch [{epoch+1}/{epochs}], Loss: {loss.item():.4f}")
|
345
|
+
|
346
|
+
self._is_fitted = True
|
347
|
+
return self
|
348
|
+
|
349
|
+
def predict_proba(self, X):
|
350
|
+
if not self._is_fitted:
|
351
|
+
raise NotFittedError("Model must be fitted before prediction")
|
352
|
+
|
353
|
+
X = self._validate_data(X)[0]
|
354
|
+
self.model.eval()
|
355
|
+
with torch.no_grad():
|
356
|
+
logits = self.model(X)
|
357
|
+
if len(self.classes_) == 2:
|
358
|
+
probs = torch.sigmoid(logits)
|
359
|
+
return np.column_stack([1 - probs.cpu().numpy(), probs.cpu().numpy()])
|
360
|
+
else:
|
361
|
+
return torch.softmax(logits, dim=1).cpu().numpy()
|
362
|
+
|
363
|
+
def predict(self, X):
|
364
|
+
proba = self.predict_proba(X)
|
365
|
+
return self.classes_[np.argmax(proba, axis=1)]
|
366
|
+
|
367
|
+
def symbolic_predict_proba(self, X):
|
368
|
+
"""Predict class probabilities using only the extracted symbolic formula."""
|
369
|
+
if not self._is_fitted:
|
370
|
+
raise NotFittedError("Model must be fitted before prediction")
|
371
|
+
|
372
|
+
if not isinstance(X, np.ndarray):
|
373
|
+
X = np.array(X)
|
374
|
+
|
375
|
+
# Scale input data similar to training
|
376
|
+
X_scaled = (X - X.mean(axis=0)) / (X.std(axis=0) + 1e-8)
|
377
|
+
|
378
|
+
formulas = self.get_symbolic_formula()
|
379
|
+
n_classes = len(self.classes_)
|
380
|
+
predictions = np.zeros((X.shape[0], n_classes))
|
381
|
+
|
382
|
+
# Evaluate each feature's contribution to each class
|
383
|
+
for i in range(X.shape[1]): # For each feature
|
384
|
+
x = X_scaled[:, i] # Use scaled data
|
385
|
+
for j in range(n_classes): # For each class
|
386
|
+
formula = formulas[i][j]
|
387
|
+
if formula and formula != "0":
|
388
|
+
predictions[:, j] += self._eval_formula(formula, x)
|
389
|
+
|
390
|
+
# Apply softmax with temperature for better separation
|
391
|
+
temperature = 1.0
|
392
|
+
exp_preds = np.exp(predictions / temperature)
|
393
|
+
probas = exp_preds / exp_preds.sum(axis=1, keepdims=True)
|
394
|
+
|
395
|
+
# Clip probabilities to avoid numerical issues
|
396
|
+
probas = np.clip(probas, 1e-7, 1.0)
|
397
|
+
probas = probas / probas.sum(axis=1, keepdims=True)
|
398
|
+
|
399
|
+
return probas
|
400
|
+
|
401
|
+
def get_symbolic_formula(self):
|
402
|
+
"""Extract symbolic formulas for all features and outputs."""
|
403
|
+
if not self._is_fitted:
|
404
|
+
raise NotFittedError("Model must be fitted before extracting formulas")
|
405
|
+
|
406
|
+
n_features = self.model[0].input_dim
|
407
|
+
n_classes = len(self.classes_)
|
408
|
+
formulas = [[[] for _ in range(n_classes)] for _ in range(n_features)]
|
409
|
+
|
410
|
+
first_layer = self.model[0]
|
411
|
+
for i in range(n_features):
|
412
|
+
for j in range(n_classes):
|
413
|
+
edge = first_layer.edges[i][j]
|
414
|
+
weight = first_layer.combination_weights[i, j].item()
|
415
|
+
|
416
|
+
if abs(weight) > 1e-4:
|
417
|
+
# Get the edge formula and scale by the weight
|
418
|
+
edge_formula = edge.get_symbolic_repr()
|
419
|
+
terms = []
|
420
|
+
for term in edge_formula.split(" + "):
|
421
|
+
if term and term != "0":
|
422
|
+
if "*" in term:
|
423
|
+
coef, rest = term.split("*", 1)
|
424
|
+
coef = float(coef) * weight
|
425
|
+
terms.append(f"{coef:.4f}*{rest}")
|
426
|
+
else:
|
427
|
+
terms.append(f"{float(term) * weight:.4f}")
|
428
|
+
|
429
|
+
formulas[i][j] = " + ".join(terms) if terms else "0"
|
430
|
+
else:
|
431
|
+
formulas[i][j] = "0"
|
432
|
+
|
433
|
+
return formulas
|
434
|
+
|
435
|
+
def symbolic_predict(self, X):
|
436
|
+
"""Predict classes using only the extracted symbolic formula."""
|
437
|
+
proba = self.symbolic_predict_proba(X)
|
438
|
+
return self.classes_[np.argmax(proba, axis=1)]
|
oikan/symbolic.py
CHANGED
@@ -1,129 +1,28 @@
|
|
1
|
-
import
|
2
|
-
import numpy as np
|
3
|
-
import networkx as nx
|
4
|
-
import matplotlib.pyplot as plt
|
1
|
+
from .utils import ADVANCED_LIB
|
5
2
|
|
6
|
-
|
7
|
-
'x': lambda x: x,
|
8
|
-
'x^2': lambda x: x**2,
|
9
|
-
'x^3': lambda x: x**3,
|
10
|
-
'x^4': lambda x: x**4,
|
11
|
-
'x^5': lambda x: x**5,
|
12
|
-
'exp': lambda x: np.exp(x),
|
13
|
-
'log': lambda x: np.log(np.abs(x) + 1e-8),
|
14
|
-
'sqrt': lambda x: np.sqrt(np.abs(x)),
|
15
|
-
'tanh': lambda x: np.tanh(x),
|
16
|
-
'sin': lambda x: np.sin(x),
|
17
|
-
'abs': lambda x: np.abs(x)
|
18
|
-
}
|
19
|
-
|
20
|
-
# Helper functions
|
21
|
-
def get_model_predictions(model, X, mode):
|
22
|
-
"""Obtain model predictions; returns flattened predictions for regression, raw outputs for classification."""
|
23
|
-
X_tensor = torch.FloatTensor(X)
|
24
|
-
with torch.no_grad():
|
25
|
-
preds = model(X_tensor)
|
26
|
-
if mode == 'regression':
|
27
|
-
return preds.detach().cpu().numpy().flatten(), None
|
28
|
-
elif mode == 'classification':
|
29
|
-
out = preds.detach().cpu().numpy()
|
30
|
-
# In classification, compute a target difference or fallback to flattening
|
31
|
-
target = (out[:, 0] - out[:, 1]).flatten() if (out.ndim > 1 and out.shape[1] > 1) else out.flatten()
|
32
|
-
return target, out
|
33
|
-
else:
|
34
|
-
raise ValueError("Unknown mode")
|
35
|
-
|
36
|
-
def build_design_matrix(X, return_names=False):
|
37
|
-
"""Construct the design matrix from advanced nonlinear bases with optional feature names."""
|
38
|
-
X_np = np.array(X)
|
39
|
-
n_samples, d = X_np.shape
|
40
|
-
F_parts = [np.ones((n_samples, 1))] # Bias term
|
41
|
-
names = ['1'] if return_names else None
|
42
|
-
for j in range(d):
|
43
|
-
xj = X_np[:, j:j+1]
|
44
|
-
for key, func in ADVANCED_LIB.items():
|
45
|
-
F_parts.append(func(xj))
|
46
|
-
if return_names:
|
47
|
-
names.append(f"{key}(x{j+1})")
|
48
|
-
return (np.hstack(F_parts), names) if return_names else np.hstack(F_parts)
|
49
|
-
|
50
|
-
# Main functions using helpers
|
51
|
-
def extract_symbolic_formula(model, X, mode='regression'):
|
3
|
+
def symbolic_edge_repr(weights, bias=None, threshold=1e-4):
|
52
4
|
"""
|
53
|
-
|
5
|
+
Given a list of weights (floats) and an optional bias,
|
6
|
+
returns a list of structured terms (coefficient, basis function string).
|
54
7
|
"""
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
y_target, out = get_model_predictions(model, X, mode)
|
65
|
-
F = build_design_matrix(X, return_names=False)
|
66
|
-
beta, _, _, _ = np.linalg.lstsq(F, y_target, rcond=None)
|
67
|
-
symbolic_vals = F.dot(beta)
|
68
|
-
if mode == 'regression':
|
69
|
-
mse = np.mean((symbolic_vals - y_target) ** 2)
|
70
|
-
mae = np.mean(np.abs(symbolic_vals - y_target))
|
71
|
-
rmse = np.sqrt(mse)
|
72
|
-
print(f"(Advanced) MSE: {mse:.4f}, MAE: {mae:.4f}, RMSE: {rmse:.4f}")
|
73
|
-
return mse, mae, rmse
|
74
|
-
elif mode == 'classification':
|
75
|
-
sym_preds = np.where(symbolic_vals >= 0, 0, 1)
|
76
|
-
model_classes = np.argmax(out, axis=1) if (out.ndim > 1) else (out >= 0.5).astype(int)
|
77
|
-
if model_classes.shape[0] != sym_preds.shape[0]:
|
78
|
-
raise ValueError("Shape mismatch between symbolic and model predictions.")
|
79
|
-
accuracy = np.mean(sym_preds == model_classes)
|
80
|
-
print(f"(Advanced) Accuracy: {accuracy:.4f}")
|
81
|
-
return accuracy
|
8
|
+
terms = []
|
9
|
+
# weights should be in the same order as ADVANCED_LIB.items()
|
10
|
+
for (_, (notation, _)), w in zip(ADVANCED_LIB.items(), weights):
|
11
|
+
if abs(w) > threshold:
|
12
|
+
terms.append((w, notation))
|
13
|
+
if bias is not None and abs(bias) > threshold:
|
14
|
+
# use "1" to represent the constant term
|
15
|
+
terms.append((bias, "1"))
|
16
|
+
return terms
|
82
17
|
|
83
|
-
def
|
84
|
-
"""
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
G.add_node(node_label)
|
95
|
-
G.add_edge(node_label, "Output", weight=float(coeff_str))
|
96
|
-
# Position nodes for visualization
|
97
|
-
left_nodes = [n for n in G.nodes() if n != "Output"]
|
98
|
-
pos = {}
|
99
|
-
n_left = len(left_nodes)
|
100
|
-
for i, node in enumerate(sorted(left_nodes)):
|
101
|
-
pos[node] = (0, 1 - (i / max(n_left - 1, 1)))
|
102
|
-
pos["Output"] = (1, 0.5)
|
103
|
-
plt.figure(figsize=(12, 8))
|
104
|
-
nx.draw(G, pos, with_labels=True, node_color="skyblue", node_size=2500, font_size=10,
|
105
|
-
arrows=True, arrowstyle='->', arrowsize=20)
|
106
|
-
edge_labels = {(u, v): f"{d['weight']:.2f}" for u, v, d in G.edges(data=True)}
|
107
|
-
nx.draw_networkx_edge_labels(G, pos, edge_labels=edge_labels, font_color='red', font_size=10)
|
108
|
-
plt.title("OIKAN Symbolic Formula Graph")
|
109
|
-
plt.axis("off")
|
110
|
-
plt.show()
|
111
|
-
|
112
|
-
def extract_latex_formula(model, X, mode='regression'):
|
113
|
-
"""Return the extracted symbolic formula formatted as LaTeX code."""
|
114
|
-
formula = extract_symbolic_formula(model, X, mode)
|
115
|
-
terms = formula.split(" + ")
|
116
|
-
latex_terms = []
|
117
|
-
for term in terms:
|
118
|
-
expr = term.strip("()")
|
119
|
-
coeff_str, basis = expr.split("*", 1) if "*" in expr else (expr, "")
|
120
|
-
coeff = float(coeff_str)
|
121
|
-
# Balance parentheses if required
|
122
|
-
missing = basis.count("(") - basis.count(")")
|
123
|
-
if missing > 0:
|
124
|
-
basis = basis + ")" * missing
|
125
|
-
coeff_latex = f"{abs(coeff):.2f}".rstrip("0").rstrip(".")
|
126
|
-
term_latex = coeff_latex if basis.strip() == "1" else f"{coeff_latex} \\cdot {basis.strip()}"
|
127
|
-
latex_terms.append(f"- {term_latex}" if coeff < 0 else f"+ {term_latex}")
|
128
|
-
latex_formula = " ".join(latex_terms).lstrip("+ ").strip()
|
129
|
-
return f"$$ {latex_formula} $$"
|
18
|
+
def format_symbolic_terms(terms):
|
19
|
+
"""
|
20
|
+
Formats a list of structured symbolic terms (coef, basis) to a string.
|
21
|
+
"""
|
22
|
+
formatted_terms = []
|
23
|
+
for coef, basis in terms:
|
24
|
+
if basis == "1":
|
25
|
+
formatted_terms.append(f"{coef:.4f}")
|
26
|
+
else:
|
27
|
+
formatted_terms.append(f"{coef:.4f}*{basis}")
|
28
|
+
return " + ".join(formatted_terms) if formatted_terms else "0"
|
oikan/utils.py
CHANGED
@@ -1,44 +1,47 @@
|
|
1
|
+
from .exceptions import *
|
1
2
|
import torch
|
2
3
|
import torch.nn as nn
|
3
4
|
import numpy as np
|
4
|
-
from scipy.interpolate import BSpline
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
self.register_buffer('knots', torch.FloatTensor(knots))
|
19
|
-
|
20
|
-
def forward(self, x):
|
21
|
-
# Convert tensor to numpy for BSpline evaluation
|
22
|
-
x_np = x.detach().cpu().numpy()
|
23
|
-
basis_values = np.zeros((x_np.shape[0], self.num_knots - self.degree - 1))
|
24
|
-
# Normalize input for stable spline evaluation
|
25
|
-
x_min, x_max = x_np.min(), x_np.max()
|
26
|
-
x_normalized = (x_np - x_min) / (x_max - x_min + 1e-8)
|
27
|
-
for i in range(self.num_knots - self.degree - 1):
|
28
|
-
# Create BSpline basis function for a subset of knots
|
29
|
-
spl = BSpline.basis_element(self.knots[i:i+self.degree+2])
|
30
|
-
basis_values[:, i] = spl(x_normalized.squeeze())
|
31
|
-
basis_values = np.nan_to_num(basis_values, 0)
|
32
|
-
return torch.FloatTensor(basis_values).to(x.device)
|
6
|
+
# Core basis functions with explicit variable notation
|
7
|
+
ADVANCED_LIB = {
|
8
|
+
'x': ('x', lambda x: x),
|
9
|
+
'x^2': ('x^2', lambda x: np.clip(x**2, -100, 100)),
|
10
|
+
'x^3': ('x^3', lambda x: np.clip(x**3, -100, 100)),
|
11
|
+
'exp': ('exp(x)', lambda x: np.exp(np.clip(x, -10, 10))),
|
12
|
+
'log': ('log(x)', lambda x: np.log(np.abs(x) + 1)),
|
13
|
+
'sqrt': ('sqrt(x)', lambda x: np.sqrt(np.abs(x))),
|
14
|
+
'tanh': ('tanh(x)', lambda x: np.tanh(x)),
|
15
|
+
'sin': ('sin(x)', lambda x: np.sin(np.clip(x, -10*np.pi, 10*np.pi))),
|
16
|
+
'abs': ('abs(x)', lambda x: np.abs(x))
|
17
|
+
}
|
33
18
|
|
34
|
-
class
|
35
|
-
|
36
|
-
def __init__(self
|
19
|
+
class EdgeActivation(nn.Module):
|
20
|
+
"""Learnable edge-based activation function."""
|
21
|
+
def __init__(self):
|
37
22
|
super().__init__()
|
38
|
-
self.
|
23
|
+
self.weights = nn.Parameter(torch.randn(len(ADVANCED_LIB)))
|
24
|
+
self.bias = nn.Parameter(torch.zeros(1))
|
39
25
|
|
40
26
|
def forward(self, x):
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
27
|
+
features = []
|
28
|
+
for _, func in ADVANCED_LIB.values():
|
29
|
+
feat = torch.tensor(func(x.detach().cpu().numpy()),
|
30
|
+
dtype=torch.float32).to(x.device)
|
31
|
+
features.append(feat)
|
32
|
+
features = torch.stack(features, dim=-1)
|
33
|
+
return torch.matmul(features, self.weights.unsqueeze(0).T) + self.bias
|
34
|
+
|
35
|
+
def get_symbolic_repr(self, threshold=1e-4):
|
36
|
+
"""Get symbolic representation of the activation function."""
|
37
|
+
significant_terms = []
|
38
|
+
|
39
|
+
for (notation, _), weight in zip(ADVANCED_LIB.values(),
|
40
|
+
self.weights.detach().cpu().numpy()):
|
41
|
+
if abs(weight) > threshold:
|
42
|
+
significant_terms.append(f"{weight:.4f}*{notation}")
|
43
|
+
|
44
|
+
if abs(self.bias.item()) > threshold:
|
45
|
+
significant_terms.append(f"{self.bias.item():.4f}")
|
46
|
+
|
47
|
+
return " + ".join(significant_terms) if significant_terms else "0"
|
@@ -0,0 +1,160 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: oikan
|
3
|
+
Version: 0.0.2.1
|
4
|
+
Summary: OIKAN: Optimized Interpretable Kolmogorov-Arnold Networks
|
5
|
+
Author: Arman Zhalgasbayev
|
6
|
+
License: MIT
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
8
|
+
Classifier: License :: OSI Approved :: MIT License
|
9
|
+
Classifier: Operating System :: OS Independent
|
10
|
+
Requires-Python: >=3.7
|
11
|
+
Description-Content-Type: text/markdown
|
12
|
+
License-File: LICENSE
|
13
|
+
Requires-Dist: torch
|
14
|
+
Requires-Dist: numpy
|
15
|
+
Requires-Dist: scikit-learn
|
16
|
+
Dynamic: license-file
|
17
|
+
|
18
|
+
<!-- logo in the center -->
|
19
|
+
<div align="center">
|
20
|
+
<img src="https://raw.githubusercontent.com/silvermete0r/oikan/main/docs/media/oikan_logo.png" alt="OIKAN Logo" width="200"/>
|
21
|
+
|
22
|
+
<h1>OIKAN: Optimized Interpretable Kolmogorov-Arnold Networks</h1>
|
23
|
+
</div>
|
24
|
+
|
25
|
+
## Overview
|
26
|
+
OIKAN (Optimized Interpretable Kolmogorov-Arnold Networks) is a neuro-symbolic ML framework that combines modern neural networks with classical Kolmogorov-Arnold representation theory. It provides interpretable machine learning solutions through automatic extraction of symbolic mathematical formulas from trained models.
|
27
|
+
|
28
|
+
[](https://badge.fury.io/py/oikan)
|
29
|
+
[](https://pypistats.org/packages/oikan)
|
30
|
+
[](https://pepy.tech/projects/oikan)
|
31
|
+
[](https://opensource.org/licenses/MIT)
|
32
|
+
[](https://github.com/silvermete0r/oikan/issues)
|
33
|
+
[](https://silvermete0r.github.io/oikan/)
|
34
|
+
|
35
|
+
## Key Features
|
36
|
+
- 🧠 **Neuro-Symbolic ML**: Combines neural network learning with symbolic mathematics
|
37
|
+
- 📊 **Automatic Formula Extraction**: Generates human-readable mathematical expressions
|
38
|
+
- 🎯 **Scikit-learn Compatible**: Familiar `.fit()` and `.predict()` interface
|
39
|
+
- 🚀 **Production-Ready**: Export symbolic formulas for lightweight deployment
|
40
|
+
- 📈 **Multi-Task**: Supports both regression and classification problems
|
41
|
+
|
42
|
+
## Scientific Foundation
|
43
|
+
|
44
|
+
OIKAN is based on Kolmogorov's superposition theorem, which states that any multivariate continuous function can be represented as a composition of single-variable functions. We leverage this theory by:
|
45
|
+
|
46
|
+
1. Using neural networks to learn optimal basis functions
|
47
|
+
2. Employing SVD projection for dimensionality reduction
|
48
|
+
3. Applying symbolic regression to extract interpretable formulas
|
49
|
+
|
50
|
+
## Quick Start
|
51
|
+
|
52
|
+
### Installation
|
53
|
+
|
54
|
+
#### Method 1: Via PyPI (Recommended)
|
55
|
+
```bash
|
56
|
+
pip install -qU oikan
|
57
|
+
```
|
58
|
+
|
59
|
+
#### Method 2: Local Development
|
60
|
+
```bash
|
61
|
+
git clone https://github.com/silvermete0r/OIKAN.git
|
62
|
+
cd OIKAN
|
63
|
+
pip install -e . # Install in development mode
|
64
|
+
```
|
65
|
+
|
66
|
+
### Regression Example
|
67
|
+
```python
|
68
|
+
from oikan.model import OIKANRegressor
|
69
|
+
from sklearn.model_selection import train_test_split
|
70
|
+
|
71
|
+
# Initialize model with optimal architecture
|
72
|
+
model = OIKANRegressor(
|
73
|
+
hidden_dims=[16, 8], # Network architecture
|
74
|
+
num_basis=10, # Number of basis functions
|
75
|
+
degree=3, # Polynomial degree
|
76
|
+
dropout=0.1 # Regularization
|
77
|
+
)
|
78
|
+
|
79
|
+
# Fit model (sklearn-style)
|
80
|
+
model.fit(X_train, y_train, epochs=200, lr=0.01)
|
81
|
+
|
82
|
+
# Get predictions
|
83
|
+
y_pred = model.predict(X_test)
|
84
|
+
|
85
|
+
# Save interpretable formula to file with auto-generated guidelines
|
86
|
+
# The output file will contain:
|
87
|
+
# - Detailed symbolic formulas for each feature
|
88
|
+
# - Instructions for practical implementation
|
89
|
+
# - Recommendations for production deployment
|
90
|
+
model.save_symbolic_formula("regression_formula.txt")
|
91
|
+
```
|
92
|
+
|
93
|
+
*Example of the saved symbolic formula instructions: [outputs/regression_symbolic_formula.txt](outputs/regression_symbolic_formula.txt)*
|
94
|
+
|
95
|
+
|
96
|
+
### Classification Example
|
97
|
+
```python
|
98
|
+
from oikan.model import OIKANClassifier
|
99
|
+
|
100
|
+
# Similar sklearn-style interface for classification
|
101
|
+
model = OIKANClassifier(hidden_dims=[16, 8])
|
102
|
+
model.fit(X_train, y_train)
|
103
|
+
probas = model.predict_proba(X_test)
|
104
|
+
|
105
|
+
# Save classification formulas with implementation guidelines
|
106
|
+
# The output file will contain:
|
107
|
+
# - Decision boundary formulas for each class
|
108
|
+
# - Softmax application instructions
|
109
|
+
# - Production deployment recommendations
|
110
|
+
model.save_symbolic_formula("classification_formula.txt")
|
111
|
+
```
|
112
|
+
|
113
|
+
*Example of the saved symbolic formula instructions: [outputs/classification_symbolic_formula.txt](outputs/classification_symbolic_formula.txt)*
|
114
|
+
|
115
|
+
## Architecture Details
|
116
|
+
|
117
|
+
OIKAN's architecture consists of three main components:
|
118
|
+
|
119
|
+
1. **Basis Function Layer**: Learns optimal single-variable transformations
|
120
|
+
- B-spline bases for smooth function approximation
|
121
|
+
- Trigonometric bases for periodic patterns
|
122
|
+
- Polynomial bases for algebraic relationships
|
123
|
+
|
124
|
+
2. **Neural Composition Layer**: Combines transformed features
|
125
|
+
- SVD projection for dimensionality reduction
|
126
|
+
- Dropout for regularization
|
127
|
+
- Skip connections for gradient flow
|
128
|
+
|
129
|
+
3. **Symbolic Extraction Layer**: Generates interpretable formulas
|
130
|
+
- L1 regularization for sparse representations
|
131
|
+
- Symbolic regression for formula extraction
|
132
|
+
- LaTeX export for documentation
|
133
|
+
|
134
|
+
## Contributing
|
135
|
+
|
136
|
+
We welcome contributions! Key areas of interest:
|
137
|
+
|
138
|
+
- Model architecture improvements
|
139
|
+
- Novel basis function implementations
|
140
|
+
- Improved symbolic extraction algorithms
|
141
|
+
- Real-world case studies and applications
|
142
|
+
- Performance optimizations
|
143
|
+
|
144
|
+
Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines.
|
145
|
+
|
146
|
+
## Citation
|
147
|
+
|
148
|
+
If you use OIKAN in your research, please cite:
|
149
|
+
|
150
|
+
```bibtex
|
151
|
+
@software{oikan2025,
|
152
|
+
title = {OIKAN: Optimized Interpretable Kolmogorov-Arnold Networks},
|
153
|
+
author = {Zhalgasbayev, Arman},
|
154
|
+
year = {2025},
|
155
|
+
url = {https://github.com/silvermete0r/OIKAN}
|
156
|
+
}
|
157
|
+
```
|
158
|
+
|
159
|
+
## License
|
160
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
@@ -0,0 +1,10 @@
|
|
1
|
+
oikan/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
+
oikan/exceptions.py,sha256=UqT3uTtfiB8QA_3AMvKdHOme9WL9HZD_d7GHIk00LJw,394
|
3
|
+
oikan/model.py,sha256=iHWKjk_n0Kkw47UO2XFTc0faqGYBrQBJhmmRn1Po4qw,19604
|
4
|
+
oikan/symbolic.py,sha256=TtalmSpBecf33_g7yE3q-RPuCVRWQNaXWE4LsCNZmfg,1040
|
5
|
+
oikan/utils.py,sha256=sivt_8jzATH-eUZ3-P-tsdmyIgKsayibSZeP_MtLTfU,1969
|
6
|
+
oikan-0.0.2.1.dist-info/licenses/LICENSE,sha256=75ASVmU-XIpN-M4LbVmJ_ibgbzbvRLVti8FhnR0BTf8,1096
|
7
|
+
oikan-0.0.2.1.dist-info/METADATA,sha256=vQao-ZUR4BXdu1RgnC7RZQ4na1vGhYU1vmpVttOb2LM,5978
|
8
|
+
oikan-0.0.2.1.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
|
9
|
+
oikan-0.0.2.1.dist-info/top_level.txt,sha256=XwnwKwTJddZwIvtrUsAz-l-58BJRj6HjAGWrfYi_3QY,6
|
10
|
+
oikan-0.0.2.1.dist-info/RECORD,,
|
oikan/metrics.py
DELETED
@@ -1,48 +0,0 @@
|
|
1
|
-
import numpy as np
|
2
|
-
import torch
|
3
|
-
from sklearn.metrics import precision_score, recall_score, f1_score, hamming_loss
|
4
|
-
|
5
|
-
def evaluate_regression(model, X, y):
|
6
|
-
'''Evaluate regression performance by computing MSE, MAE, and RMSE, and print in table format.'''
|
7
|
-
with torch.no_grad():
|
8
|
-
y_pred = model(torch.FloatTensor(X)).numpy().ravel()
|
9
|
-
mse = np.mean((y - y_pred)**2)
|
10
|
-
mae = np.mean(np.abs(y - y_pred))
|
11
|
-
rmse = np.sqrt(mse)
|
12
|
-
|
13
|
-
# Print table
|
14
|
-
header = f"+{'-'*23}+{'-'*12}+"
|
15
|
-
print(header)
|
16
|
-
print(f"| {'Metric':21} | {'Value':9} |")
|
17
|
-
print(header)
|
18
|
-
print(f"| {'Mean Squared Error':21} | {mse:9.4f} |")
|
19
|
-
print(f"| {'Mean Absolute Error':21} | {mae:9.4f} |")
|
20
|
-
print(f"| {'Root Mean Squared Error':21} | {rmse:9.4f} |")
|
21
|
-
print(header)
|
22
|
-
|
23
|
-
return mse, mae, rmse
|
24
|
-
|
25
|
-
def evaluate_classification(model, X, y):
|
26
|
-
'''Evaluate classification performance by computing accuracy, precision, recall, f1-score, and hamming_loss, and printing in table format.'''
|
27
|
-
with torch.no_grad():
|
28
|
-
logits = model(torch.FloatTensor(X))
|
29
|
-
y_pred = torch.argmax(logits, dim=1).numpy()
|
30
|
-
accuracy = np.mean(y_pred == y)
|
31
|
-
precision = precision_score(y, y_pred, average='weighted', zero_division=0)
|
32
|
-
recall = recall_score(y, y_pred, average='weighted', zero_division=0)
|
33
|
-
f1 = f1_score(y, y_pred, average='weighted', zero_division=0)
|
34
|
-
h_loss = hamming_loss(y, y_pred)
|
35
|
-
|
36
|
-
# Print table
|
37
|
-
header = f"+{'-'*15}+{'-'*12}+"
|
38
|
-
print(header)
|
39
|
-
print(f"| {'Metric':13} | {'Value':9} |")
|
40
|
-
print(header)
|
41
|
-
print(f"| {'Accuracy':13} | {accuracy:9.4f} |")
|
42
|
-
print(f"| {'Precision':13} | {precision:9.4f} |")
|
43
|
-
print(f"| {'Recall':13} | {recall:9.4f} |")
|
44
|
-
print(f"| {'F1-score':13} | {f1:9.4f} |")
|
45
|
-
print(f"| {'Hamming Loss':13} | {h_loss:9.4f} |")
|
46
|
-
print(header)
|
47
|
-
|
48
|
-
return accuracy, precision, recall, f1, h_loss
|
oikan/regularization.py
DELETED
@@ -1,30 +0,0 @@
|
|
1
|
-
import torch
|
2
|
-
import torch.nn as nn
|
3
|
-
|
4
|
-
class RegularizedLoss:
|
5
|
-
def __init__(self, base_criterion, model, l1_lambda=0.01, gradient_lambda=0.01):
|
6
|
-
self.base_criterion = base_criterion # Primary loss (e.g. MSE, CrossEntropy)
|
7
|
-
self.model = model
|
8
|
-
self.l1_lambda = l1_lambda
|
9
|
-
self.gradient_lambda = gradient_lambda
|
10
|
-
|
11
|
-
def __call__(self, pred, target, inputs):
|
12
|
-
# Compute the standard loss
|
13
|
-
base_loss = self.base_criterion(pred, target)
|
14
|
-
|
15
|
-
# Calculate L1 regularization to promote sparsity
|
16
|
-
l1_loss = 0
|
17
|
-
for param in self.model.parameters():
|
18
|
-
l1_loss += torch.norm(param, p=1)
|
19
|
-
|
20
|
-
# Compute gradient penalty to enforce smoothness
|
21
|
-
inputs.requires_grad_(True)
|
22
|
-
outputs = self.model(inputs)
|
23
|
-
gradients = torch.autograd.grad(
|
24
|
-
outputs=outputs.sum(),
|
25
|
-
inputs=inputs,
|
26
|
-
create_graph=True
|
27
|
-
)[0]
|
28
|
-
grad_penalty = ((gradients.norm(2, dim=1) - 1) ** 2).mean()
|
29
|
-
|
30
|
-
return base_loss + self.l1_lambda * l1_loss + self.gradient_lambda * grad_penalty
|
oikan/trainer.py
DELETED
@@ -1,49 +0,0 @@
|
|
1
|
-
import torch
|
2
|
-
import torch.nn as nn
|
3
|
-
from .regularization import RegularizedLoss
|
4
|
-
|
5
|
-
def train(model, train_data, epochs=100, lr=0.01, save_path=None, verbose=True):
|
6
|
-
'''Train regression model using MSE loss with regularization.
|
7
|
-
Optionally save the model when training is finished if save_path is provided.
|
8
|
-
'''
|
9
|
-
X_train, y_train = train_data
|
10
|
-
optimizer = torch.optim.Adam(model.parameters(), lr=lr)
|
11
|
-
criterion = nn.MSELoss()
|
12
|
-
reg_loss = RegularizedLoss(criterion, model)
|
13
|
-
|
14
|
-
model.train()
|
15
|
-
for epoch in range(epochs):
|
16
|
-
optimizer.zero_grad() # Reset gradients
|
17
|
-
outputs = model(X_train)
|
18
|
-
loss = reg_loss(outputs, y_train, X_train)
|
19
|
-
loss.backward() # Backpropagate errors
|
20
|
-
optimizer.step() # Update parameters
|
21
|
-
|
22
|
-
if (epoch + 1) % 10 == 0 and verbose:
|
23
|
-
print(f'Epoch [{epoch+1}/{epochs}], Loss: {loss.item():.4f}')
|
24
|
-
if save_path is not None:
|
25
|
-
torch.save(model.state_dict(), save_path)
|
26
|
-
print(f"Model saved to {save_path}")
|
27
|
-
|
28
|
-
def train_classification(model, train_data, epochs=100, lr=0.01, save_path=None, verbose=True):
|
29
|
-
'''Train classification model using CrossEntropy loss with regularization.
|
30
|
-
Optionally save the model when training is finished if save_path is provided.
|
31
|
-
'''
|
32
|
-
X_train, y_train = train_data
|
33
|
-
optimizer = torch.optim.Adam(model.parameters(), lr=lr)
|
34
|
-
criterion = nn.CrossEntropyLoss()
|
35
|
-
reg_loss = RegularizedLoss(criterion, model)
|
36
|
-
|
37
|
-
model.train()
|
38
|
-
for epoch in range(epochs):
|
39
|
-
optimizer.zero_grad() # Reset gradients each epoch
|
40
|
-
outputs = model(X_train)
|
41
|
-
loss = reg_loss(outputs, y_train, X_train)
|
42
|
-
loss.backward() # Backpropagation
|
43
|
-
optimizer.step() # Parameter update
|
44
|
-
|
45
|
-
if (epoch + 1) % 10 == 0 and verbose:
|
46
|
-
print(f'Epoch [{epoch+1}/{epochs}], Loss: {loss.item():.4f}')
|
47
|
-
if save_path is not None:
|
48
|
-
torch.save(model.state_dict(), save_path)
|
49
|
-
print(f"Model saved to {save_path}")
|
oikan/visualize.py
DELETED
@@ -1,69 +0,0 @@
|
|
1
|
-
import torch
|
2
|
-
import numpy as np
|
3
|
-
import matplotlib.pyplot as plt
|
4
|
-
|
5
|
-
def visualize_regression(model, X, y):
|
6
|
-
'''Visualize regression results using true vs predicted scatter plots.'''
|
7
|
-
model.eval()
|
8
|
-
with torch.no_grad():
|
9
|
-
y_pred = model(torch.FloatTensor(X)).numpy()
|
10
|
-
plt.figure(figsize=(10, 6))
|
11
|
-
plt.scatter(X[:, 0], y, color='blue', label='True')
|
12
|
-
plt.scatter(X[:, 0], y_pred, color='red', label='Predicted')
|
13
|
-
plt.legend()
|
14
|
-
plt.show()
|
15
|
-
|
16
|
-
def visualize_classification(model, X, y):
|
17
|
-
'''Visualize classification decision boundaries. For high-dimensional data, uses SVD projection.'''
|
18
|
-
model.eval()
|
19
|
-
if X.shape[1] > 2:
|
20
|
-
X_mean = np.mean(X, axis=0)
|
21
|
-
X_centered = X - X_mean
|
22
|
-
_, _, Vt = np.linalg.svd(X_centered, full_matrices=False)
|
23
|
-
principal = Vt[:2]
|
24
|
-
X_proj = (X - X_mean) @ principal.T
|
25
|
-
x_min, x_max = X_proj[:, 0].min() - 1, X_proj[:, 0].max() + 1
|
26
|
-
y_min, y_max = X_proj[:, 1].min() - 1, X_proj[:, 1].max() + 1
|
27
|
-
xx, yy = np.meshgrid(np.linspace(x_min, x_max, 100),
|
28
|
-
np.linspace(y_min, y_max, 100))
|
29
|
-
grid_2d = np.c_[xx.ravel(), yy.ravel()]
|
30
|
-
X_grid = X_mean + grid_2d @ principal
|
31
|
-
with torch.no_grad():
|
32
|
-
Z = model(torch.FloatTensor(X_grid))
|
33
|
-
Z = torch.argmax(Z, dim=1).numpy().reshape(xx.shape)
|
34
|
-
plt.figure(figsize=(10, 8))
|
35
|
-
plt.contourf(xx, yy, Z, alpha=0.4)
|
36
|
-
plt.scatter(X_proj[:, 0], X_proj[:, 1], c=y, alpha=0.8)
|
37
|
-
plt.title("Classification Visualization (SVD Projection)")
|
38
|
-
plt.show()
|
39
|
-
else:
|
40
|
-
x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
|
41
|
-
y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
|
42
|
-
xx, yy = np.meshgrid(np.linspace(x_min, x_max, 100),
|
43
|
-
np.linspace(y_min, y_max, 100))
|
44
|
-
grid_2d = np.c_[xx.ravel(), yy.ravel()]
|
45
|
-
with torch.no_grad():
|
46
|
-
Z = model(torch.FloatTensor(grid_2d))
|
47
|
-
Z = torch.argmax(Z, dim=1).numpy().reshape(xx.shape)
|
48
|
-
plt.figure(figsize=(10, 8))
|
49
|
-
plt.contourf(xx, yy, Z, alpha=0.4)
|
50
|
-
plt.scatter(X[:, 0], X[:, 1], c=y, alpha=0.8)
|
51
|
-
|
52
|
-
def visualize_time_series_forecasting(model, X, y):
|
53
|
-
'''
|
54
|
-
Visualize time series forecasting results by plotting true vs predicted values.
|
55
|
-
Expected X shape: [samples, seq_len, features] and y: true targets.
|
56
|
-
'''
|
57
|
-
model.eval()
|
58
|
-
with torch.no_grad():
|
59
|
-
y_pred = model(X).detach().cpu().numpy()
|
60
|
-
if isinstance(y, torch.Tensor):
|
61
|
-
y = y.detach().cpu().numpy()
|
62
|
-
plt.figure(figsize=(10, 5))
|
63
|
-
plt.plot(y, label='True', marker='o', linestyle='-')
|
64
|
-
plt.plot(y_pred, label='Predicted', marker='x', linestyle='--')
|
65
|
-
plt.xlabel("Time Step")
|
66
|
-
plt.ylabel("Value")
|
67
|
-
plt.title("Time Series Forecasting Visualization")
|
68
|
-
plt.legend()
|
69
|
-
plt.show()
|
@@ -1,105 +0,0 @@
|
|
1
|
-
Metadata-Version: 2.2
|
2
|
-
Name: oikan
|
3
|
-
Version: 0.0.1.11
|
4
|
-
Summary: OIKAN: Optimized Interpretable Kolmogorov-Arnold Networks
|
5
|
-
Author: Arman Zhalgasbayev
|
6
|
-
License: MIT
|
7
|
-
Classifier: Programming Language :: Python :: 3
|
8
|
-
Classifier: License :: OSI Approved :: MIT License
|
9
|
-
Classifier: Operating System :: OS Independent
|
10
|
-
Requires-Python: >=3.7
|
11
|
-
Description-Content-Type: text/markdown
|
12
|
-
License-File: LICENSE
|
13
|
-
Requires-Dist: torch
|
14
|
-
Requires-Dist: numpy
|
15
|
-
Requires-Dist: sympy
|
16
|
-
Requires-Dist: scipy
|
17
|
-
Requires-Dist: matplotlib
|
18
|
-
|
19
|
-
# OIKAN
|
20
|
-
|
21
|
-
Optimized Interpretable Kolmogorov-Arnold Networks (OIKAN)
|
22
|
-
A deep learning framework for interpretable neural networks using advanced basis functions.
|
23
|
-
|
24
|
-
[](https://badge.fury.io/py/oikan)
|
25
|
-
[](https://pypistats.org/packages/oikan)
|
26
|
-
[](https://pepy.tech/projects/oikan)
|
27
|
-
[](https://opensource.org/licenses/MIT)
|
28
|
-
[](https://github.com/silvermete0r/oikan/issues)
|
29
|
-
[](https://silvermete0r.github.io/oikan/)
|
30
|
-
|
31
|
-
## Key Features
|
32
|
-
- 🚀 Efficient Implementation ~ Optimized KAN architecture with SVD projection
|
33
|
-
- 📊 Advanced Basis Functions ~ B-spline and Fourier basis transformations
|
34
|
-
- 🎯 Multi-Task Support ~ Both regression and classification capabilities
|
35
|
-
- 🔍 Interpretability Tools ~ Extract and visualize symbolic formulas
|
36
|
-
- 📈 Interactive Visualizations ~ Built-in plotting and analysis tools
|
37
|
-
- 🧮 Symbolic Mathematics ~ LaTeX formula extraction and symbolic approximations
|
38
|
-
|
39
|
-
## Installation
|
40
|
-
|
41
|
-
### Method 1: Via PyPI (Recommended)
|
42
|
-
```bash
|
43
|
-
pip install oikan
|
44
|
-
```
|
45
|
-
|
46
|
-
### Method 2: Local Development
|
47
|
-
```bash
|
48
|
-
git clone https://github.com/silvermete0r/OIKAN.git
|
49
|
-
cd OIKAN
|
50
|
-
pip install -e . # Install in development mode
|
51
|
-
```
|
52
|
-
|
53
|
-
## Quick Start
|
54
|
-
|
55
|
-
### Regression Example
|
56
|
-
```python
|
57
|
-
from oikan.model import OIKAN
|
58
|
-
from oikan.trainer import train
|
59
|
-
from oikan.visualize import visualize_regression
|
60
|
-
from oikan.symbolic import extract_symbolic_formula, plot_symbolic_formula, extract_latex_formula
|
61
|
-
|
62
|
-
model = OIKAN(input_dim=2, output_dim=1)
|
63
|
-
train(model, (X_train, y_train))
|
64
|
-
|
65
|
-
visualize_regression(model, X, y)
|
66
|
-
|
67
|
-
formula = extract_symbolic_formula(model, X_test, mode='regression')
|
68
|
-
print("Extracted formula:", formula)
|
69
|
-
|
70
|
-
plot_symbolic_formula(model, X_test, mode='regression')
|
71
|
-
|
72
|
-
latex_formula = extract_latex_formula(model, X_test, mode='regression')
|
73
|
-
print("LaTeX:", latex_formula)
|
74
|
-
```
|
75
|
-
|
76
|
-
### Classification Example
|
77
|
-
```python
|
78
|
-
from oikan.model import OIKAN
|
79
|
-
from oikan.trainer import train_classification
|
80
|
-
from oikan.visualize import visualize_classification
|
81
|
-
from oikan.symbolic import extract_symbolic_formula, plot_symbolic_formula, extract_latex_formula
|
82
|
-
|
83
|
-
model = OIKAN(input_dim=2, output_dim=2)
|
84
|
-
train_classification(model, (X_train, y_train))
|
85
|
-
|
86
|
-
visualize_classification(model, X_test, y_test)
|
87
|
-
|
88
|
-
formula = extract_symbolic_formula(model, X_test, mode='classification')
|
89
|
-
print("Extracted formula:", formula)
|
90
|
-
|
91
|
-
plot_symbolic_formula(model, X_test, mode='classification')
|
92
|
-
|
93
|
-
latex_formula = extract_latex_formula(model, X_test, mode='classification')
|
94
|
-
print("LaTeX:", latex_formula)
|
95
|
-
```
|
96
|
-
|
97
|
-
## Usage
|
98
|
-
- Explore the `oikan/` folder for model architectures, training routines, and symbolic extraction.
|
99
|
-
- Check the `examples/` directory for complete usage examples for both regression and classification.
|
100
|
-
|
101
|
-
## Contributing
|
102
|
-
Contributions are welcome! Submit a Pull Request with your improvements.
|
103
|
-
|
104
|
-
## License
|
105
|
-
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
oikan-0.0.1.11.dist-info/RECORD
DELETED
@@ -1,13 +0,0 @@
|
|
1
|
-
oikan/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
-
oikan/metrics.py,sha256=IF13bW3evsyKfZC2jhI-MPRu2Rl77Elo3of68OF_JW8,1928
|
3
|
-
oikan/model.py,sha256=blpTiAFQ-LxhvWedP5Yf5TgdwlOb4t1BuBMe9d-kJZ0,5342
|
4
|
-
oikan/regularization.py,sha256=xt8JNnPdHRAQgzF_vnyme005hWLunz9Vo2qw6m08NMM,1145
|
5
|
-
oikan/symbolic.py,sha256=RRYHOCOCJr5KXRhdcCPvT_OqyNcCnWCWt7fOtos8rRI,5765
|
6
|
-
oikan/trainer.py,sha256=PwA8PnVUiv5wYlQqj3DTplCAUZljZ4iWJUKUDvmIvX0,2062
|
7
|
-
oikan/utils.py,sha256=xbVgrbhXYj57RdD3uNPchjyfmP6Kur7tngoZPa3qWOw,2094
|
8
|
-
oikan/visualize.py,sha256=ZZiRf0P8cuBiC0reBBGVnSTotBq5oxQIRIEgqSrN6u8,2916
|
9
|
-
oikan-0.0.1.11.dist-info/LICENSE,sha256=75ASVmU-XIpN-M4LbVmJ_ibgbzbvRLVti8FhnR0BTf8,1096
|
10
|
-
oikan-0.0.1.11.dist-info/METADATA,sha256=5EpY9clgm3iQ2nLrtLesX-H8sUhZU_lL7bTEPDFj54U,3848
|
11
|
-
oikan-0.0.1.11.dist-info/WHEEL,sha256=52BFRY2Up02UkjOa29eZOS2VxUrpPORXg1pkohGGUS8,91
|
12
|
-
oikan-0.0.1.11.dist-info/top_level.txt,sha256=XwnwKwTJddZwIvtrUsAz-l-58BJRj6HjAGWrfYi_3QY,6
|
13
|
-
oikan-0.0.1.11.dist-info/RECORD,,
|
File without changes
|
File without changes
|