kanboost 0.0.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.
kanboost/__init__.py ADDED
@@ -0,0 +1,23 @@
1
+ """
2
+ KANBoost: Gradient Boosting with Kolmogorov-Arnold Network learners.
3
+
4
+ An interpretable alternative to tree-based gradient boosting (XGBoost,
5
+ LightGBM, CatBoost), using shallow KAN networks as weak learners instead
6
+ of decision trees.
7
+ """
8
+
9
+ from .classifier import KANBoostClassifier
10
+ from .regressor import KANBoostRegressor
11
+ from .metrics import classification_report_dict, print_classification_report
12
+
13
+ __version__ = "0.0.1"
14
+ __all__ = [
15
+ "KANBoostClassifier",
16
+ "KANBoostRegressor",
17
+ "classification_report_dict",
18
+ "print_classification_report",
19
+ ]
20
+
21
+ # Hyperparameter tuning lives in the separate `kantun` package, so that
22
+ # kanboost's core dependency footprint stays minimal and kantun can be
23
+ # used to tune other model types too. Install with: pip install kantun
kanboost/_base.py ADDED
@@ -0,0 +1,198 @@
1
+ """
2
+ kanboost._base -- shared machinery for KANBoostClassifier / KANBoostRegressor.
3
+
4
+ Contains:
5
+ - input validation
6
+ - the shared boosting loop skeleton
7
+ - sklearn-compatible get_params / set_params (so KANBoost estimators work
8
+ inside sklearn Pipelines, GridSearchCV, and kantun without adapters)
9
+ - feature importances (shared by both estimators)
10
+ - suppression of pykan's noisy checkpoint logging
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import contextlib
16
+ import io
17
+
18
+ import numpy as np
19
+ import pandas as pd
20
+ import torch
21
+ import torch.nn as nn
22
+ from kan import KAN
23
+ from sklearn.base import BaseEstimator
24
+
25
+ from .encoders import TabularPreprocessor
26
+
27
+
28
+ @contextlib.contextmanager
29
+ def _suppress_pykan_noise():
30
+ """pykan prints checkpoint messages to stdout and tqdm progress bars
31
+ to stderr on every instantiation/fit; silence both."""
32
+ out, err = io.StringIO(), io.StringIO()
33
+ with contextlib.redirect_stdout(out), contextlib.redirect_stderr(err):
34
+ yield
35
+
36
+
37
+ def _validate_Xy(X, y):
38
+ """Validate and normalize training inputs. Returns (X_df, y_arr)."""
39
+ if not isinstance(X, pd.DataFrame):
40
+ X = pd.DataFrame(np.asarray(X))
41
+ X.columns = [f"f{i}" for i in range(X.shape[1])]
42
+
43
+ y = np.asarray(y, dtype=float).ravel()
44
+
45
+ if len(X) != len(y):
46
+ raise ValueError(
47
+ f"X and y have inconsistent lengths: {len(X)} vs {len(y)}"
48
+ )
49
+ if len(X) == 0:
50
+ raise ValueError("X is empty.")
51
+ if np.isnan(y).any():
52
+ raise ValueError(
53
+ "y contains NaN values. Remove or impute them before fitting."
54
+ )
55
+ return X, y
56
+
57
+
58
+ class _BaseKANBoost(BaseEstimator):
59
+ """Shared base for the boosting estimators. Not part of the public API.
60
+
61
+ Inherits sklearn's BaseEstimator, which provides get_params/set_params,
62
+ __sklearn_tags__, and full compatibility with sklearn Pipelines,
63
+ GridSearchCV, cross_val_score, and clone().
64
+ """
65
+
66
+ def __init__(
67
+ self,
68
+ n_estimators: int = 100,
69
+ learning_rate: float = 0.1,
70
+ kan_hidden: int = 3,
71
+ kan_grid: int = 2,
72
+ kan_k: int = 3,
73
+ kan_steps: int = 20,
74
+ kan_lr: float = 0.02,
75
+ early_stopping_rounds: int | None = 10,
76
+ categorical_cols=None,
77
+ random_state: int = 42,
78
+ verbose: bool = False,
79
+ ):
80
+ self.n_estimators = n_estimators
81
+ self.learning_rate = learning_rate
82
+ self.kan_hidden = kan_hidden
83
+ self.kan_grid = kan_grid
84
+ self.kan_k = kan_k
85
+ self.kan_steps = kan_steps
86
+ self.kan_lr = kan_lr
87
+ self.early_stopping_rounds = early_stopping_rounds
88
+ self.categorical_cols = categorical_cols
89
+ self.random_state = random_state
90
+ self.verbose = verbose
91
+
92
+ # fitted state
93
+ self.preprocessor_ = None
94
+ self.learners_ = []
95
+ self.init_pred_ = None
96
+ self.best_iteration_ = None
97
+ self.feature_names_in_ = None
98
+
99
+ # ------------------------------------------------------------------
100
+ # NOTE: get_params/set_params/__sklearn_tags__ come from BaseEstimator.
101
+ # ------------------------------------------------------------------
102
+
103
+ # ------------------------------------------------------------------
104
+ # shared internals
105
+ # ------------------------------------------------------------------
106
+ def _validate_hyperparams(self):
107
+ if self.n_estimators < 1:
108
+ raise ValueError("n_estimators must be >= 1")
109
+ if not (0 < self.learning_rate <= 1):
110
+ raise ValueError("learning_rate must be in (0, 1]")
111
+ if self.kan_hidden < 1 or self.kan_grid < 1 or self.kan_steps < 1:
112
+ raise ValueError("kan_hidden, kan_grid, kan_steps must be >= 1")
113
+
114
+ def _prepare_fit(self, X, y):
115
+ """Common preamble: seeds, validation, preprocessing."""
116
+ self._validate_hyperparams()
117
+ torch.manual_seed(self.random_state)
118
+ np.random.seed(self.random_state)
119
+
120
+ X, y = _validate_Xy(X, y)
121
+ self.feature_names_in_ = list(X.columns)
122
+
123
+ self.preprocessor_ = TabularPreprocessor(
124
+ categorical_cols=self.categorical_cols or []
125
+ )
126
+ X_arr = self.preprocessor_.fit_transform(X, y)
127
+
128
+ if np.isnan(X_arr).any():
129
+ raise ValueError(
130
+ "Preprocessed X contains NaN values. KANBoost does not yet "
131
+ "handle missing values natively -- impute before fitting."
132
+ )
133
+ return X, y, X_arr
134
+
135
+ def _new_learner(self, n_features: int, seed_offset: int) -> KAN:
136
+ with _suppress_pykan_noise():
137
+ return KAN(
138
+ width=[n_features, self.kan_hidden, 1],
139
+ grid=self.kan_grid,
140
+ k=self.kan_k,
141
+ seed=self.random_state + seed_offset,
142
+ )
143
+
144
+ def _fit_learner(self, learner: KAN, X_t: torch.Tensor, residual: np.ndarray):
145
+ r_t = torch.tensor(residual, dtype=torch.float32).unsqueeze(1)
146
+ dataset = {
147
+ "train_input": X_t, "train_label": r_t,
148
+ "test_input": X_t, "test_label": r_t,
149
+ }
150
+ with _suppress_pykan_noise():
151
+ learner.fit(
152
+ dataset, opt="Adam", steps=self.kan_steps, lr=self.kan_lr,
153
+ loss_fn=nn.MSELoss(),
154
+ )
155
+ with torch.no_grad():
156
+ return learner(X_t).numpy().flatten()
157
+
158
+ def _check_fitted(self):
159
+ if not self.learners_:
160
+ raise RuntimeError(
161
+ f"This {type(self).__name__} instance is not fitted yet. "
162
+ "Call fit() before predicting."
163
+ )
164
+
165
+ def _transform_X(self, X) -> torch.Tensor:
166
+ self._check_fitted()
167
+ if not isinstance(X, pd.DataFrame):
168
+ X = pd.DataFrame(np.asarray(X), columns=self.feature_names_in_)
169
+ X_arr = self.preprocessor_.transform(X)
170
+ return torch.tensor(X_arr, dtype=torch.float32)
171
+
172
+ # ------------------------------------------------------------------
173
+ def feature_importances(self) -> np.ndarray:
174
+ """
175
+ Approximate per-feature importance: L2 norm of each learner's
176
+ first-layer spline coefficients per input dimension, summed over
177
+ the ensemble and normalized to 1. A rough analogue of GBDT
178
+ 'gain' importance.
179
+ """
180
+ self._check_fitted()
181
+ n_features = self.learners_[0].width[0][0]
182
+ importances = np.zeros(n_features)
183
+ for learner in self.learners_[: self.best_iteration_]:
184
+ coef = learner.act_fun[0].coef.detach().numpy()
185
+ importances += np.linalg.norm(coef, axis=(1, 2))
186
+ total = importances.sum()
187
+ return importances / total if total > 0 else importances
188
+
189
+ def feature_importances_dict(self) -> dict:
190
+ """Feature importances keyed by input column name, sorted desc."""
191
+ imps = self.feature_importances()
192
+ # preprocessor may reorder: numeric cols first, then categorical
193
+ ordered_names = (
194
+ list(self.preprocessor_.numeric_cols_)
195
+ + list(self.preprocessor_.categorical_cols)
196
+ )
197
+ pairs = sorted(zip(ordered_names, imps), key=lambda kv: -kv[1])
198
+ return {name: float(v) for name, v in pairs}
kanboost/classifier.py ADDED
@@ -0,0 +1,169 @@
1
+ """
2
+ KANBoostClassifier: binary classification via gradient boosting with
3
+ shallow KAN networks as weak learners.
4
+
5
+ Follows the classic Friedman (2001) gradient boosting recipe:
6
+
7
+ F_0(x) = log-odds of the base rate
8
+ for t = 1..T:
9
+ r_t = pseudo-residuals = y - sigmoid(F_{t-1}(x)) (logloss)
10
+ f_t = a small KAN fit to (X, r_t)
11
+ F_t(x) = F_{t-1}(x) + learning_rate * f_t(x)
12
+ prediction = sigmoid(F_T(x))
13
+
14
+ Each weak learner is a small, shallow KAN so it plays the same structural
15
+ role a shallow decision tree plays in XGBoost/CatBoost: a cheap,
16
+ high-bias/low-variance component that is only useful in aggregate.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import numpy as np
22
+ import torch
23
+
24
+ from sklearn.base import ClassifierMixin
25
+
26
+ from ._base import _BaseKANBoost, _validate_Xy
27
+
28
+
29
+ def _sigmoid(z: np.ndarray) -> np.ndarray:
30
+ return 1.0 / (1.0 + np.exp(-np.clip(z, -30, 30)))
31
+
32
+
33
+ class KANBoostClassifier(ClassifierMixin, _BaseKANBoost):
34
+ """Gradient-boosted KAN ensemble for binary classification.
35
+
36
+ Parameters
37
+ ----------
38
+ n_estimators : int, default=100
39
+ Maximum number of boosting iterations (weak learners).
40
+ learning_rate : float, default=0.1
41
+ Shrinkage applied to each learner's contribution.
42
+ kan_hidden : int, default=3
43
+ Hidden-layer width of each weak KAN learner.
44
+ kan_grid : int, default=2
45
+ Number of B-spline grid intervals per edge function.
46
+ kan_k : int, default=3
47
+ B-spline polynomial degree.
48
+ kan_steps : int, default=20
49
+ Optimizer steps used to fit each weak learner.
50
+ kan_lr : float, default=0.02
51
+ Learning rate of each weak learner's inner optimizer.
52
+ early_stopping_rounds : int or None, default=10
53
+ Stop if validation logloss hasn't improved for this many rounds.
54
+ Requires eval_set in fit(). None disables early stopping.
55
+ categorical_cols : list of str, optional
56
+ Column names to target-mean encode automatically.
57
+ random_state : int, default=42
58
+ verbose : bool, default=False
59
+ """
60
+
61
+
62
+ def fit(self, X, y, eval_set: tuple | None = None):
63
+ """Fit the boosted ensemble.
64
+
65
+ Parameters
66
+ ----------
67
+ X : DataFrame or array of shape (n_samples, n_features)
68
+ y : array of shape (n_samples,) with values in {0, 1}
69
+ eval_set : (X_val, y_val) tuple, optional
70
+ Validation data for early stopping.
71
+ """
72
+ X, y, X_arr = self._prepare_fit(X, y)
73
+
74
+ classes = np.unique(y)
75
+ if not np.array_equal(classes, [0, 1]) and not np.array_equal(classes, [0]) \
76
+ and not np.array_equal(classes, [1]):
77
+ raise ValueError(
78
+ f"KANBoostClassifier supports binary targets in {{0, 1}}; "
79
+ f"got classes {classes}. Multiclass is on the roadmap."
80
+ )
81
+ self.classes_ = np.array([0, 1])
82
+
83
+ X_t = torch.tensor(X_arr, dtype=torch.float32)
84
+ n_features = X_arr.shape[1]
85
+
86
+ p = float(np.clip(y.mean(), 1e-6, 1 - 1e-6))
87
+ self.init_pred_ = float(np.log(p / (1 - p)))
88
+ F = np.full(len(y), self.init_pred_)
89
+
90
+ X_val_t = y_val = F_val = None
91
+ if eval_set is not None:
92
+ X_val_df, y_val = eval_set
93
+ X_val_df, y_val = _validate_Xy(X_val_df, y_val)
94
+ X_val_arr = self.preprocessor_.transform(X_val_df)
95
+ X_val_t = torch.tensor(X_val_arr, dtype=torch.float32)
96
+ F_val = np.full(len(y_val), self.init_pred_)
97
+
98
+ best_val_loss = np.inf
99
+ rounds_since_best = 0
100
+ self.learners_ = []
101
+ self.best_iteration_ = None
102
+
103
+ for t in range(self.n_estimators):
104
+ residual = y - _sigmoid(F)
105
+
106
+ learner = self._new_learner(n_features, seed_offset=t)
107
+ update = self._fit_learner(learner, X_t, residual)
108
+ F += self.learning_rate * update
109
+ self.learners_.append(learner)
110
+
111
+ if X_val_t is not None:
112
+ with torch.no_grad():
113
+ F_val += self.learning_rate * learner(X_val_t).numpy().flatten()
114
+ val_prob = np.clip(_sigmoid(F_val), 1e-7, 1 - 1e-7)
115
+ val_loss = -float(np.mean(
116
+ y_val * np.log(val_prob) + (1 - y_val) * np.log(1 - val_prob)
117
+ ))
118
+
119
+ if self.verbose:
120
+ print(f"[{t + 1}/{self.n_estimators}] val_logloss={val_loss:.5f}")
121
+
122
+ if val_loss < best_val_loss - 1e-5:
123
+ best_val_loss = val_loss
124
+ rounds_since_best = 0
125
+ self.best_iteration_ = t + 1
126
+ else:
127
+ rounds_since_best += 1
128
+ if (self.early_stopping_rounds is not None
129
+ and rounds_since_best >= self.early_stopping_rounds):
130
+ if self.verbose:
131
+ print(f"Early stopping at iteration {t + 1}")
132
+ break
133
+ elif self.verbose and (t + 1) % max(1, self.n_estimators // 10) == 0:
134
+ print(f"[{t + 1}/{self.n_estimators}] "
135
+ f"train residual std={residual.std():.4f}")
136
+
137
+ if self.best_iteration_ is None:
138
+ self.best_iteration_ = len(self.learners_)
139
+ return self
140
+
141
+ # ------------------------------------------------------------------
142
+ def _raw_score(self, X) -> np.ndarray:
143
+ X_t = self._transform_X(X)
144
+ F = np.full(X_t.shape[0], self.init_pred_)
145
+ for learner in self.learners_[: self.best_iteration_]:
146
+ with torch.no_grad():
147
+ F += self.learning_rate * learner(X_t).numpy().flatten()
148
+ return F
149
+
150
+ def predict_proba(self, X) -> np.ndarray:
151
+ """Return array of shape (n_samples, 2): P(class 0), P(class 1)."""
152
+ prob_pos = _sigmoid(self._raw_score(X))
153
+ return np.vstack([1 - prob_pos, prob_pos]).T
154
+
155
+ def predict(self, X, threshold: float = 0.5) -> np.ndarray:
156
+ """Return hard 0/1 predictions at the given probability threshold."""
157
+ return (self.predict_proba(X)[:, 1] >= threshold).astype(int)
158
+
159
+ def evaluate(self, X, y, threshold: float = 0.5, verbose: bool = True) -> dict:
160
+ """Predict on X and report confusion matrix, accuracy, precision,
161
+ recall, F1, and ROC-AUC against y. Returns the metrics dict."""
162
+ from .metrics import classification_report_dict, print_classification_report
163
+
164
+ y_prob = self.predict_proba(X)[:, 1]
165
+ y_pred = (y_prob >= threshold).astype(int)
166
+ report = classification_report_dict(y, y_pred, y_prob)
167
+ if verbose:
168
+ print_classification_report(report)
169
+ return report
kanboost/encoders.py ADDED
@@ -0,0 +1,81 @@
1
+ """
2
+ Preprocessing utilities for KANBoost.
3
+
4
+ KAN is sensitive to input scale (splines are defined on a bounded grid),
5
+ so numeric features must be scaled/clipped, and categorical features
6
+ must be encoded to numbers before reaching the model. This module
7
+ automates both steps so the end user can pass a raw-ish DataFrame,
8
+ similar to how CatBoost handles categorical columns internally.
9
+ """
10
+
11
+ import numpy as np
12
+ import pandas as pd
13
+ from sklearn.preprocessing import RobustScaler
14
+
15
+
16
+ class TabularPreprocessor:
17
+ """
18
+ Automatic preprocessing for mixed numeric/categorical tabular data.
19
+
20
+ - Numeric columns: outlier clipping (1st/99th percentile) + RobustScaler
21
+ + final clip to [-1, 1] (KAN's default spline grid range).
22
+ - Categorical columns: target-mean encoding (smoothed), computed on
23
+ the training fold only, to avoid leakage -- similar in spirit to
24
+ CatBoost's ordered target statistics (simplified, non-ordered version).
25
+ """
26
+
27
+ def __init__(self, categorical_cols=None, smoothing: float = 10.0):
28
+ self.categorical_cols = categorical_cols or []
29
+ self.smoothing = smoothing
30
+ self.numeric_cols_ = None
31
+ self.scaler_ = None
32
+ self.clip_low_ = None
33
+ self.clip_high_ = None
34
+ self.cat_maps_ = {}
35
+ self.global_mean_ = None
36
+
37
+ def fit(self, X: pd.DataFrame, y: np.ndarray):
38
+ self.numeric_cols_ = [c for c in X.columns if c not in self.categorical_cols]
39
+ self.global_mean_ = float(np.mean(y))
40
+
41
+ # --- numeric ---
42
+ if self.numeric_cols_:
43
+ X_num = X[self.numeric_cols_].to_numpy(dtype=float)
44
+ self.clip_low_ = np.nanpercentile(X_num, 1, axis=0)
45
+ self.clip_high_ = np.nanpercentile(X_num, 99, axis=0)
46
+ X_num = np.clip(X_num, self.clip_low_, self.clip_high_)
47
+ self.scaler_ = RobustScaler()
48
+ self.scaler_.fit(X_num)
49
+
50
+ # --- categorical: smoothed target-mean encoding ---
51
+ for col in self.categorical_cols:
52
+ stats = (
53
+ pd.DataFrame({col: X[col].astype(str), "y": y})
54
+ .groupby(col)["y"]
55
+ .agg(["mean", "count"])
56
+ )
57
+ smoothed = (
58
+ stats["mean"] * stats["count"] + self.global_mean_ * self.smoothing
59
+ ) / (stats["count"] + self.smoothing)
60
+ self.cat_maps_[col] = smoothed.to_dict()
61
+
62
+ return self
63
+
64
+ def transform(self, X: pd.DataFrame) -> np.ndarray:
65
+ parts = []
66
+ if self.numeric_cols_:
67
+ X_num = X[self.numeric_cols_].to_numpy(dtype=float)
68
+ X_num = np.clip(X_num, self.clip_low_, self.clip_high_)
69
+ X_num = self.scaler_.transform(X_num)
70
+ X_num = np.clip(X_num, -1, 1)
71
+ parts.append(X_num)
72
+
73
+ for col in self.categorical_cols:
74
+ mapping = self.cat_maps_[col]
75
+ encoded = X[col].astype(str).map(mapping).fillna(self.global_mean_)
76
+ parts.append(encoded.to_numpy(dtype=float).reshape(-1, 1))
77
+
78
+ return np.hstack(parts) if parts else np.empty((len(X), 0))
79
+
80
+ def fit_transform(self, X: pd.DataFrame, y: np.ndarray) -> np.ndarray:
81
+ return self.fit(X, y).transform(X)
kanboost/metrics.py ADDED
@@ -0,0 +1,59 @@
1
+ """
2
+ Evaluation utilities for KANBoost models.
3
+
4
+ Kept as plain functions (not baked only into the classifier) so they can
5
+ also be reused by the tuning module and by users who just want metrics
6
+ on any set of predictions.
7
+ """
8
+
9
+ import numpy as np
10
+ from sklearn.metrics import (
11
+ confusion_matrix,
12
+ f1_score,
13
+ precision_score,
14
+ recall_score,
15
+ accuracy_score,
16
+ roc_auc_score,
17
+ )
18
+
19
+
20
+ def classification_report_dict(y_true, y_pred, y_prob=None) -> dict:
21
+ """
22
+ Returns a dict with confusion matrix + standard classification
23
+ metrics. If y_prob (probability of the positive class) is given,
24
+ also includes ROC-AUC.
25
+ """
26
+ y_true = np.asarray(y_true)
27
+ y_pred = np.asarray(y_pred)
28
+
29
+ cm = confusion_matrix(y_true, y_pred)
30
+ report = {
31
+ "confusion_matrix": cm.tolist(),
32
+ "confusion_matrix_labels": ["true_negative_row0", "true_positive_row1"],
33
+ "accuracy": float(accuracy_score(y_true, y_pred)),
34
+ "precision": float(precision_score(y_true, y_pred, zero_division=0)),
35
+ "recall": float(recall_score(y_true, y_pred, zero_division=0)),
36
+ "f1": float(f1_score(y_true, y_pred, zero_division=0)),
37
+ }
38
+ if y_prob is not None:
39
+ try:
40
+ report["auc"] = float(roc_auc_score(y_true, y_prob))
41
+ except ValueError:
42
+ report["auc"] = None
43
+ return report
44
+
45
+
46
+ def print_classification_report(report: dict, class_names=("0", "1")) -> None:
47
+ """Pretty-print a report dict returned by classification_report_dict."""
48
+ cm = np.array(report["confusion_matrix"])
49
+ print("Confusion Matrix:")
50
+ print(f" predicted {class_names[0]} predicted {class_names[1]}")
51
+ print(f" actual {class_names[0]:>3} {cm[0, 0]:>10d} {cm[0, 1]:>10d}")
52
+ print(f" actual {class_names[1]:>3} {cm[1, 0]:>10d} {cm[1, 1]:>10d}")
53
+ print()
54
+ print(f"Accuracy : {report['accuracy']:.4f}")
55
+ print(f"Precision: {report['precision']:.4f}")
56
+ print(f"Recall : {report['recall']:.4f}")
57
+ print(f"F1 score : {report['f1']:.4f}")
58
+ if report.get("auc") is not None:
59
+ print(f"ROC-AUC : {report['auc']:.4f}")
kanboost/regressor.py ADDED
@@ -0,0 +1,113 @@
1
+ """
2
+ KANBoostRegressor: regression via gradient boosting with shallow KAN
3
+ learners, minimizing squared error (residual = y - F(x) directly).
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import numpy as np
9
+ import torch
10
+
11
+ from sklearn.base import RegressorMixin
12
+
13
+ from ._base import _BaseKANBoost, _validate_Xy
14
+
15
+
16
+ class KANBoostRegressor(RegressorMixin, _BaseKANBoost):
17
+ """Gradient-boosted KAN ensemble for regression (squared-error loss).
18
+
19
+ Parameters are identical to KANBoostClassifier; see its docstring.
20
+ """
21
+
22
+
23
+ def fit(self, X, y, eval_set: tuple | None = None):
24
+ """Fit the boosted ensemble.
25
+
26
+ Parameters
27
+ ----------
28
+ X : DataFrame or array of shape (n_samples, n_features)
29
+ y : array of shape (n_samples,), continuous target
30
+ eval_set : (X_val, y_val) tuple, optional
31
+ Validation data for early stopping on MSE.
32
+ """
33
+ X, y, X_arr = self._prepare_fit(X, y)
34
+
35
+ X_t = torch.tensor(X_arr, dtype=torch.float32)
36
+ n_features = X_arr.shape[1]
37
+
38
+ self.init_pred_ = float(y.mean())
39
+ F = np.full(len(y), self.init_pred_)
40
+
41
+ X_val_t = y_val = F_val = None
42
+ if eval_set is not None:
43
+ X_val_df, y_val = eval_set
44
+ X_val_df, y_val = _validate_Xy(X_val_df, y_val)
45
+ X_val_arr = self.preprocessor_.transform(X_val_df)
46
+ X_val_t = torch.tensor(X_val_arr, dtype=torch.float32)
47
+ F_val = np.full(len(y_val), self.init_pred_)
48
+
49
+ best_val_loss = np.inf
50
+ rounds_since_best = 0
51
+ self.learners_ = []
52
+ self.best_iteration_ = None
53
+
54
+ for t in range(self.n_estimators):
55
+ residual = y - F
56
+
57
+ learner = self._new_learner(n_features, seed_offset=t)
58
+ update = self._fit_learner(learner, X_t, residual)
59
+ F += self.learning_rate * update
60
+ self.learners_.append(learner)
61
+
62
+ if X_val_t is not None:
63
+ with torch.no_grad():
64
+ F_val += self.learning_rate * learner(X_val_t).numpy().flatten()
65
+ val_loss = float(np.mean((y_val - F_val) ** 2))
66
+
67
+ if self.verbose:
68
+ print(f"[{t + 1}/{self.n_estimators}] val_mse={val_loss:.5f}")
69
+
70
+ if val_loss < best_val_loss - 1e-6:
71
+ best_val_loss = val_loss
72
+ rounds_since_best = 0
73
+ self.best_iteration_ = t + 1
74
+ else:
75
+ rounds_since_best += 1
76
+ if (self.early_stopping_rounds is not None
77
+ and rounds_since_best >= self.early_stopping_rounds):
78
+ if self.verbose:
79
+ print(f"Early stopping at iteration {t + 1}")
80
+ break
81
+
82
+ if self.best_iteration_ is None:
83
+ self.best_iteration_ = len(self.learners_)
84
+ return self
85
+
86
+ def predict(self, X) -> np.ndarray:
87
+ """Return continuous predictions of shape (n_samples,)."""
88
+ X_t = self._transform_X(X)
89
+ F = np.full(X_t.shape[0], self.init_pred_)
90
+ for learner in self.learners_[: self.best_iteration_]:
91
+ with torch.no_grad():
92
+ F += self.learning_rate * learner(X_t).numpy().flatten()
93
+ return F
94
+
95
+ def evaluate(self, X, y, verbose: bool = True) -> dict:
96
+ """Predict on X and report MSE, RMSE, MAE, and R^2 against y."""
97
+ from sklearn.metrics import (
98
+ mean_squared_error, mean_absolute_error, r2_score,
99
+ )
100
+
101
+ y = np.asarray(y, dtype=float).ravel()
102
+ y_pred = self.predict(X)
103
+ mse = float(mean_squared_error(y, y_pred))
104
+ report = {
105
+ "mse": mse,
106
+ "rmse": float(np.sqrt(mse)),
107
+ "mae": float(mean_absolute_error(y, y_pred)),
108
+ "r2": float(r2_score(y, y_pred)),
109
+ }
110
+ if verbose:
111
+ for k, v in report.items():
112
+ print(f"{k.upper():5s}: {v:.5f}")
113
+ return report
@@ -0,0 +1,171 @@
1
+ Metadata-Version: 2.4
2
+ Name: kanboost
3
+ Version: 0.0.1
4
+ Summary: Gradient boosting with Kolmogorov-Arnold Network (KAN) learners -- an interpretable alternative to tree-based boosting (XGBoost/LightGBM/CatBoost).
5
+ Author: Tuama M Hamzah
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Tuama M Hamzah
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Project-URL: Homepage, https://github.com/tuamah/kanboost
29
+ Classifier: Development Status :: 3 - Alpha
30
+ Classifier: License :: OSI Approved :: MIT License
31
+ Classifier: Programming Language :: Python :: 3
32
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
33
+ Requires-Python: >=3.10
34
+ Description-Content-Type: text/markdown
35
+ License-File: LICENSE
36
+ Requires-Dist: pykan>=0.2.5
37
+ Requires-Dist: torch>=2.0
38
+ Requires-Dist: numpy>=1.24
39
+ Requires-Dist: pandas>=1.5
40
+ Requires-Dist: scikit-learn>=1.3
41
+ Dynamic: license-file
42
+
43
+ # KANBoost
44
+
45
+ **Gradient boosting with Kolmogorov-Arnold Network (KAN) learners** — an
46
+ interpretable, from-scratch alternative to tree-based boosting frameworks
47
+ (XGBoost, LightGBM, CatBoost).
48
+
49
+ Instead of decision trees as weak learners, KANBoost fits a sequence of
50
+ small, shallow [KAN](https://arxiv.org/abs/2404.19756) networks to the
51
+ pseudo-residuals of the previous stage, following the classic Friedman
52
+ (2001) gradient boosting recipe. Because each KAN edge is a learnable
53
+ univariate spline rather than an opaque weight, the resulting ensemble
54
+ exposes per-feature shape functions that are directly inspectable —
55
+ closer to a Generalized Additive Model than a black box.
56
+
57
+ > **Status: early-stage research project.** This is *not* a drop-in
58
+ > replacement for CatBoost/XGBoost in production. See
59
+ > [Benchmarks](#benchmarks) and [Honest limitations](#honest-limitations)
60
+ > below before using this for anything important.
61
+
62
+ ## Why this exists
63
+
64
+ As of mid-2026, there is no widely-used, pip-installable library that
65
+ combines KAN with gradient boosting. A closely related idea was
66
+ published as **GB-KAN** (ICAART 2026), but no public code accompanies
67
+ that paper. KANBoost is an independent, from-scratch open-source
68
+ implementation of the same general idea, plus:
69
+
70
+ - automatic handling of categorical features (smoothed target-mean
71
+ encoding, done fold-safe), instead of requiring manual one-hot encoding
72
+ - built-in early stopping on a validation set
73
+ - approximate feature importances derived from learned spline coefficients
74
+
75
+ ## Install
76
+
77
+ ```bash
78
+ git clone https://github.com/tuamah/kanboost.git
79
+ cd kanboost
80
+ pip install -r requirements.txt
81
+ pip install -e .
82
+ ```
83
+
84
+ ## Quickstart
85
+
86
+ ```python
87
+ import pandas as pd
88
+ from sklearn.model_selection import train_test_split
89
+ from kanboost import KANBoostClassifier
90
+
91
+ df = pd.read_csv("your_data.csv")
92
+ X = df.drop(columns=["target"])
93
+ y = df["target"].values
94
+
95
+ X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2)
96
+
97
+ model = KANBoostClassifier(
98
+ n_estimators=100,
99
+ learning_rate=0.2,
100
+ kan_hidden=4,
101
+ kan_grid=3,
102
+ categorical_cols=["region", "plan_type"], # optional
103
+ early_stopping_rounds=10,
104
+ )
105
+ model.fit(X_train, y_train, eval_set=(X_val, y_val))
106
+
107
+ probs = model.predict_proba(X_val)[:, 1]
108
+ importances = model.feature_importances()
109
+ ```
110
+
111
+ ## Benchmarks
112
+
113
+ Preliminary results on a real-world telecom churn dataset (100K rows,
114
+ 10 numeric features used, 8K-row sample for the KANBoost run due to
115
+ current training-speed limits):
116
+
117
+ | Model | Test AUC | Notes |
118
+ |---|---|---|
119
+ | CatBoost (tuned, full data, ~100 columns) | **0.6992** | production baseline |
120
+ | KANBoostClassifier (this repo, 10 features, 8K sample) | 0.64 | early prototype, untuned |
121
+ | Plain KAN (no boosting) | 0.65 | single model, same features |
122
+ | Plain MLP | 0.59–0.62 | same features |
123
+
124
+ **Read this table honestly**: KANBoost does not yet beat CatBoost on
125
+ this dataset. The goal of this repo, at this stage, is to establish a
126
+ working, extensible implementation and an honest baseline — not to claim
127
+ state-of-the-art results.
128
+
129
+ ## Honest limitations
130
+
131
+ - **Speed**: each weak learner is a full KAN forward/backward pass in
132
+ pure PyTorch. This is currently far slower per-iteration than a
133
+ histogram-based tree split in XGBoost/CatBoost/LightGBM.
134
+ - **Tuning**: hyperparameters (`kan_grid`, `kan_hidden`, `kan_steps`,
135
+ `learning_rate`) interact in ways that are not yet well understood;
136
+ expect to need real tuning for your dataset.
137
+ - **Categorical encoding** is a simple smoothed target-mean encoder, not
138
+ CatBoost's ordered boosting scheme — it can leak on small folds if not
139
+ used carefully.
140
+ - **Missing values** are not yet handled natively; impute before fitting.
141
+
142
+ ## Roadmap
143
+
144
+ See [`ROADMAP.md`](./ROADMAP.md) for the full project plan, including
145
+ planned speed optimizations (FastKAN-style RBF basis, `torch.compile`),
146
+ symbolic-formula extraction for the full ensemble, and benchmark
147
+ expansion to standard UCI datasets.
148
+
149
+ ## Contributing
150
+
151
+ Issues and PRs welcome, especially:
152
+ - speed optimizations for the per-iteration KAN fit
153
+ - better categorical encoding
154
+ - benchmark results on additional public datasets
155
+
156
+ ## License
157
+
158
+ MIT — see [`LICENSE`](./LICENSE).
159
+
160
+ ## Citation / related work
161
+
162
+ If you use this, please also cite the KAN paper and, where relevant,
163
+ the GB-KAN paper this project is conceptually closest to:
164
+
165
+ ```
166
+ Liu, Z., Wang, Y., Vaidya, S., et al. (2024). KAN: Kolmogorov-Arnold
167
+ Networks. arXiv:2404.19756.
168
+
169
+ [GB-KAN authors] (2026). Gradient Boosting with Interpretable
170
+ Kolmogorov-Arnold Networks. ICAART 2026.
171
+ ```
@@ -0,0 +1,11 @@
1
+ kanboost/__init__.py,sha256=bLGvCFaiCYC3ik9N1ScWH38UI5WJZfbvGnbL3wThhaY,775
2
+ kanboost/_base.py,sha256=Vmxzg1p2R3EdlIB8vUXIcNXNtLDbniHi1_Z7l7zku6o,7142
3
+ kanboost/classifier.py,sha256=6Vqv-xQIgusAnyee-Uk6kqIzg2PFpeEU04jzSDcL3Lw,6662
4
+ kanboost/encoders.py,sha256=FU_CFkGNQwoY7DJU_7b4bgvcYNcAien13Wm_kFie9Sc,3182
5
+ kanboost/metrics.py,sha256=YpgMHR1VpM1KMfMmNJOB6-W3cqVdg8pDa7EM_oXQH4c,2132
6
+ kanboost/regressor.py,sha256=uQjNrMzd4lHf6MqIrtP-i9e5fAkXGxAhJ__4buXlMDw,3981
7
+ kanboost-0.0.1.dist-info/licenses/LICENSE,sha256=TibUcjm19hWQsu8odGoBCn5xJmN3FV7n8cKriciimJ8,1071
8
+ kanboost-0.0.1.dist-info/METADATA,sha256=au9AODbHCp2W0EfLxGUVNpMsl9YpzWhHz1bYrZT7mg4,6729
9
+ kanboost-0.0.1.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
10
+ kanboost-0.0.1.dist-info/top_level.txt,sha256=Smj79RKLj6B69IA60ZJUtsYUQb5eYdZwOMIrwYVEHcg,9
11
+ kanboost-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tuama M Hamzah
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ kanboost