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 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
- from .utils import BSplineBasis, FourierBasis
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 AdaptiveBasisLayer(nn.Module):
6
- '''Layer that applies a linear transformation as part of interpretable modeling.'''
7
- def __init__(self, input_dim, hidden_dim):
9
+ class SymbolicEdge(nn.Module):
10
+ """Edge-based activation function learner"""
11
+ def __init__(self):
8
12
  super().__init__()
9
- self.weights = nn.Parameter(torch.randn(input_dim, hidden_dim))
10
- self.bias = nn.Parameter(torch.zeros(hidden_dim))
13
+ self.activation = EdgeActivation()
11
14
 
12
15
  def forward(self, x):
13
- # Linear transformation for adaptive basis processing
14
- return torch.matmul(x, self.weights) + self.bias
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 EfficientKAN(nn.Module):
17
- '''Module computing feature transformations using nonlinear basis functions and interaction terms.'''
18
- def __init__(self, input_dim, hidden_units=10, basis_type='bsplines'):
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.hidden_units = hidden_units
22
- self.basis_type = basis_type
23
-
24
- if basis_type == 'bsplines':
25
- # One BSpline per feature with adjusted output dimensions
26
- self.basis_functions = nn.ModuleList([BSplineBasis(hidden_units) for _ in range(input_dim)])
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
- # Interaction layer: captures pairwise feature interactions
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
- # Process basis functions per type
45
- if self.basis_type == 'combo':
46
- transformed_bspline = [bf(x[:, i].unsqueeze(1)) for i, bf in enumerate(self.basis_functions_bspline)]
47
- transformed_fourier = [bf(x[:, i].unsqueeze(1)) for i, bf in enumerate(self.basis_functions_fourier)]
48
- basis_output = torch.cat(transformed_bspline + transformed_fourier, dim=1)
49
- else:
50
- transformed_features = [bf(x[:, i].unsqueeze(1)) for i, bf in enumerate(self.basis_functions)]
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 get_output_dim(self):
64
- # Output dimension includes both basis and interaction features
65
- return self.basis_output_dim + self.input_dim
66
-
67
- class OIKAN(nn.Module):
68
- '''Main OIKAN model combining nonlinear transformations, SVD-projection, and interpretable layers.
69
- Supports time series forecasting when forecast_mode is True.
70
- '''
71
- def __init__(self, input_dim, output_dim, hidden_units=10, reduced_dim=32, basis_type='bsplines', forecast_mode=False):
72
- super().__init__()
73
- self.forecast_mode = forecast_mode
74
- if self.forecast_mode:
75
- # LSTM encoder for time series forecasting; expects input shape [batch, seq_len, input_dim]
76
- self.lstm = nn.LSTM(input_size=input_dim, hidden_size=input_dim, batch_first=True)
77
- # Process the last hidden state with EfficientKAN
78
- self.efficientkan = EfficientKAN(input_dim, hidden_units, basis_type)
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
- self.efficientkan = EfficientKAN(input_dim, hidden_units, basis_type)
81
- feature_dim = self.efficientkan.get_output_dim()
82
- self.svd_projection = nn.Linear(feature_dim, reduced_dim, bias=False)
83
- feature_dim = reduced_dim
84
- self.interpretable_layers = nn.Sequential(
85
- AdaptiveBasisLayer(feature_dim, 32),
86
- nn.ReLU(),
87
- AdaptiveBasisLayer(32, output_dim)
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 forward(self, x):
91
- if self.forecast_mode:
92
- # x shape: [batch, seq_len, input_dim]
93
- lstm_out, (hidden, _) = self.lstm(x)
94
- x_in = hidden[-1] # Use the last hidden state for forecasting
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
- x_in = x
97
- transformed_x = self.efficientkan(x_in)
98
- transformed_x = self.svd_projection(transformed_x)
99
- return self.interpretable_layers(transformed_x)
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 torch
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
- ADVANCED_LIB = {
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
- Approximate a symbolic formula that represents model behavior using nonlinear bases.
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
- y_target, _ = get_model_predictions(model, X, mode)
56
- F, func_names = build_design_matrix(X, return_names=True)
57
- beta, _, _, _ = np.linalg.lstsq(F, y_target, rcond=None)
58
- # Only include terms with significant coefficients
59
- terms = [f"({c:.2f}*{name})" for c, name in zip(beta, func_names) if abs(c) > 1e-4]
60
- return " + ".join(terms)
61
-
62
- def test_symbolic_formula(model, X, mode='regression'):
63
- """Evaluate the symbolic approximation against the model by computing error metrics."""
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 plot_symbolic_formula(model, X, mode='regression'):
84
- """Plot a graph representation of the extracted symbolic formula."""
85
- formula = extract_symbolic_formula(model, X, mode)
86
- G = nx.DiGraph()
87
- G.add_node("Output")
88
- terms = formula.split(" + ")
89
- # Add nodes for each term with coefficient information
90
- for term in terms:
91
- expr = term.strip("()")
92
- coeff_str, basis = expr.split("*", 1) if "*" in expr else (expr, "unknown")
93
- node_label = f"{basis}\n({float(coeff_str):.2f})"
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
- class BSplineBasis(nn.Module):
7
- '''Module to compute B-Spline basis values for input features.'''
8
- def __init__(self, num_knots=10, degree=3):
9
- super().__init__()
10
- # Ensure ample knots relative to degree
11
- self.num_knots = max(num_knots, degree + 5)
12
- self.degree = degree
13
- # Create inner knots uniformly in [0,1]
14
- inner_knots = np.linspace(0, 1, self.num_knots - 2 * degree)
15
- left_pad = np.zeros(degree)
16
- right_pad = np.ones(degree)
17
- knots = np.concatenate([left_pad, inner_knots, right_pad])
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 FourierBasis(nn.Module):
35
- '''Module to compute Fourier basis representations for input features.'''
36
- def __init__(self, num_frequencies=5):
19
+ class EdgeActivation(nn.Module):
20
+ """Learnable edge-based activation function."""
21
+ def __init__(self):
37
22
  super().__init__()
38
- self.num_frequencies = num_frequencies
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
- # Create a range of frequencies and compute sine and cosine transforms.
42
- frequencies = torch.arange(1, self.num_frequencies + 1, device=x.device, dtype=torch.float)
43
- x_expanded = x * frequencies.view(1, -1) * 2 * np.pi
44
- return torch.cat([torch.sin(x_expanded), torch.cos(x_expanded)], dim=1)
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
+ [![PyPI version](https://badge.fury.io/py/oikan.svg)](https://badge.fury.io/py/oikan)
29
+ [![PyPI Downloads per month](https://img.shields.io/pypi/dm/oikan.svg)](https://pypistats.org/packages/oikan)
30
+ [![PyPI Total Downloads](https://static.pepy.tech/badge/oikan)](https://pepy.tech/projects/oikan)
31
+ [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
32
+ [![GitHub issues](https://img.shields.io/github/issues/silvermete0r/OIKAN.svg)](https://github.com/silvermete0r/oikan/issues)
33
+ [![Docs](https://img.shields.io/badge/docs-passing-brightgreen)](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,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (76.0.0)
2
+ Generator: setuptools (78.1.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
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
- [![PyPI version](https://badge.fury.io/py/oikan.svg)](https://badge.fury.io/py/oikan)
25
- [![PyPI Downloads per month](https://img.shields.io/pypi/dm/oikan.svg)](https://pypistats.org/packages/oikan)
26
- [![PyPI Total Downloads](https://static.pepy.tech/badge/oikan)](https://pepy.tech/projects/oikan)
27
- [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
28
- [![GitHub issues](https://img.shields.io/github/issues/silvermete0r/OIKAN.svg)](https://github.com/silvermete0r/oikan/issues)
29
- [![Docs](https://img.shields.io/badge/docs-passing-brightgreen)](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.
@@ -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,,