warpgbm 0.1.22__tar.gz → 0.1.23__tar.gz
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.
- {warpgbm-0.1.22/warpgbm.egg-info → warpgbm-0.1.23}/PKG-INFO +1 -1
- {warpgbm-0.1.22 → warpgbm-0.1.23}/pyproject.toml +1 -1
- {warpgbm-0.1.22 → warpgbm-0.1.23}/tests/test_fit_predict_corr.py +9 -1
- warpgbm-0.1.23/version.txt +1 -0
- {warpgbm-0.1.22 → warpgbm-0.1.23}/warpgbm/core.py +147 -11
- {warpgbm-0.1.22 → warpgbm-0.1.23}/warpgbm/cuda/best_split_kernel.cu +1 -1
- {warpgbm-0.1.22 → warpgbm-0.1.23/warpgbm.egg-info}/PKG-INFO +1 -1
- warpgbm-0.1.22/version.txt +0 -1
- {warpgbm-0.1.22 → warpgbm-0.1.23}/LICENSE +0 -0
- {warpgbm-0.1.22 → warpgbm-0.1.23}/MANIFEST.in +0 -0
- {warpgbm-0.1.22 → warpgbm-0.1.23}/README.md +0 -0
- {warpgbm-0.1.22 → warpgbm-0.1.23}/setup.cfg +0 -0
- {warpgbm-0.1.22 → warpgbm-0.1.23}/setup.py +0 -0
- {warpgbm-0.1.22 → warpgbm-0.1.23}/tests/__init__.py +0 -0
- {warpgbm-0.1.22 → warpgbm-0.1.23}/warpgbm/__init__.py +0 -0
- {warpgbm-0.1.22 → warpgbm-0.1.23}/warpgbm/cuda/__init__.py +0 -0
- {warpgbm-0.1.22 → warpgbm-0.1.23}/warpgbm/cuda/binner.cu +0 -0
- {warpgbm-0.1.22 → warpgbm-0.1.23}/warpgbm/cuda/histogram_kernel.cu +0 -0
- {warpgbm-0.1.22 → warpgbm-0.1.23}/warpgbm/cuda/node_kernel.cpp +0 -0
- {warpgbm-0.1.22 → warpgbm-0.1.23}/warpgbm/cuda/predict.cu +0 -0
- {warpgbm-0.1.22 → warpgbm-0.1.23}/warpgbm.egg-info/SOURCES.txt +0 -0
- {warpgbm-0.1.22 → warpgbm-0.1.23}/warpgbm.egg-info/dependency_links.txt +0 -0
- {warpgbm-0.1.22 → warpgbm-0.1.23}/warpgbm.egg-info/requires.txt +0 -0
- {warpgbm-0.1.22 → warpgbm-0.1.23}/warpgbm.egg-info/top_level.txt +0 -0
@@ -29,7 +29,15 @@ def test_fit_predictpytee_correlation():
|
|
29
29
|
)
|
30
30
|
|
31
31
|
start_fit = time.time()
|
32
|
-
model.fit(
|
32
|
+
model.fit(
|
33
|
+
X,
|
34
|
+
y,
|
35
|
+
era_id=era,
|
36
|
+
X_eval=X,
|
37
|
+
y_eval=y,
|
38
|
+
eval_every_n_trees=10,
|
39
|
+
early_stopping_rounds=1,
|
40
|
+
)
|
33
41
|
fit_time = time.time() - start_fit
|
34
42
|
print(f" Fit time: {fit_time:.3f} seconds")
|
35
43
|
|
@@ -0,0 +1 @@
|
|
1
|
+
0.1.23
|
@@ -70,6 +70,7 @@ class WarpGBM(BaseEstimator, RegressorMixin):
|
|
70
70
|
self.rows_per_thread = rows_per_thread
|
71
71
|
self.L2_reg = L2_reg
|
72
72
|
self.L1_reg = L1_reg
|
73
|
+
self.forest = [{} for _ in range(self.n_estimators)]
|
73
74
|
|
74
75
|
def _validate_hyperparams(self, **kwargs):
|
75
76
|
# Type checks
|
@@ -122,9 +123,95 @@ class WarpGBM(BaseEstimator, RegressorMixin):
|
|
122
123
|
f"Invalid histogram_computer: {kwargs['histogram_computer']}. Choose from {list(histogram_kernels.keys())}."
|
123
124
|
)
|
124
125
|
|
125
|
-
def
|
126
|
+
def validate_fit_params(
|
127
|
+
self, X, y, era_id, X_eval, y_eval, eval_every_n_trees, early_stopping_rounds
|
128
|
+
):
|
129
|
+
# ─── Required: X and y ───
|
130
|
+
if not isinstance(X, np.ndarray) or not isinstance(y, np.ndarray):
|
131
|
+
raise TypeError("X and y must be numpy arrays.")
|
132
|
+
if X.ndim != 2:
|
133
|
+
raise ValueError(f"X must be 2-dimensional, got shape {X.shape}")
|
134
|
+
if y.ndim != 1:
|
135
|
+
raise ValueError(f"y must be 1-dimensional, got shape {y.shape}")
|
136
|
+
if X.shape[0] != y.shape[0]:
|
137
|
+
raise ValueError(
|
138
|
+
f"X and y must have the same number of rows. Got {X.shape[0]} and {y.shape[0]}."
|
139
|
+
)
|
140
|
+
|
141
|
+
# ─── Optional: era_id ───
|
142
|
+
if era_id is not None:
|
143
|
+
if not isinstance(era_id, np.ndarray):
|
144
|
+
raise TypeError("era_id must be a numpy array.")
|
145
|
+
if era_id.ndim != 1:
|
146
|
+
raise ValueError(
|
147
|
+
f"era_id must be 1-dimensional, got shape {era_id.shape}"
|
148
|
+
)
|
149
|
+
if len(era_id) != len(y):
|
150
|
+
raise ValueError(
|
151
|
+
f"era_id must have same length as y. Got {len(era_id)} and {len(y)}."
|
152
|
+
)
|
153
|
+
|
154
|
+
# ─── Optional: Eval Set ───
|
155
|
+
eval_args = [X_eval, y_eval, eval_every_n_trees]
|
156
|
+
if any(arg is not None for arg in eval_args):
|
157
|
+
# Require all of them
|
158
|
+
if X_eval is None or y_eval is None or eval_every_n_trees is None:
|
159
|
+
raise ValueError(
|
160
|
+
"If using eval set, X_eval, y_eval, and eval_every_n_trees must all be defined."
|
161
|
+
)
|
162
|
+
|
163
|
+
if not isinstance(X_eval, np.ndarray) or not isinstance(y_eval, np.ndarray):
|
164
|
+
raise TypeError("X_eval and y_eval must be numpy arrays.")
|
165
|
+
if X_eval.ndim != 2:
|
166
|
+
raise ValueError(
|
167
|
+
f"X_eval must be 2-dimensional, got shape {X_eval.shape}"
|
168
|
+
)
|
169
|
+
if y_eval.ndim != 1:
|
170
|
+
raise ValueError(
|
171
|
+
f"y_eval must be 1-dimensional, got shape {y_eval.shape}"
|
172
|
+
)
|
173
|
+
if X_eval.shape[0] != y_eval.shape[0]:
|
174
|
+
raise ValueError(
|
175
|
+
f"X_eval and y_eval must have same number of rows. Got {X_eval.shape[0]} and {y_eval.shape[0]}."
|
176
|
+
)
|
177
|
+
|
178
|
+
if not isinstance(eval_every_n_trees, int) or eval_every_n_trees <= 0:
|
179
|
+
raise ValueError(
|
180
|
+
f"eval_every_n_trees must be a positive integer, got {eval_every_n_trees}."
|
181
|
+
)
|
182
|
+
|
183
|
+
if early_stopping_rounds is not None:
|
184
|
+
if (
|
185
|
+
not isinstance(early_stopping_rounds, int)
|
186
|
+
or early_stopping_rounds <= 0
|
187
|
+
):
|
188
|
+
raise ValueError(
|
189
|
+
f"early_stopping_rounds must be a positive integer, got {early_stopping_rounds}."
|
190
|
+
)
|
191
|
+
else:
|
192
|
+
# No early stopping = set to "never trigger"
|
193
|
+
early_stopping_rounds = self.n_estimators + 1
|
194
|
+
|
195
|
+
return early_stopping_rounds # May have been defaulted here
|
196
|
+
|
197
|
+
def fit(
|
198
|
+
self,
|
199
|
+
X,
|
200
|
+
y,
|
201
|
+
era_id=None,
|
202
|
+
X_eval=None,
|
203
|
+
y_eval=None,
|
204
|
+
eval_every_n_trees=None,
|
205
|
+
early_stopping_rounds=None,
|
206
|
+
):
|
207
|
+
early_stopping_rounds = self.validate_fit_params(
|
208
|
+
X, y, era_id, X_eval, y_eval, eval_every_n_trees, early_stopping_rounds
|
209
|
+
)
|
210
|
+
|
126
211
|
if era_id is None:
|
127
212
|
era_id = np.ones(X.shape[0], dtype="int32")
|
213
|
+
|
214
|
+
# Train data preprocessing
|
128
215
|
self.bin_indices, era_indices, self.bin_edges, self.unique_eras, self.Y_gpu = (
|
129
216
|
self.preprocess_gpu_data(X, y, era_id)
|
130
217
|
)
|
@@ -137,8 +224,23 @@ class WarpGBM(BaseEstimator, RegressorMixin):
|
|
137
224
|
self.best_bins = torch.zeros(
|
138
225
|
self.num_features, device=self.device, dtype=torch.int32
|
139
226
|
)
|
227
|
+
|
228
|
+
# ─── Optional Eval Set ───
|
229
|
+
if X_eval is not None and y_eval is not None:
|
230
|
+
self.bin_indices_eval = self.bin_data_with_existing_edges(X_eval)
|
231
|
+
self.Y_gpu_eval = torch.from_numpy(y_eval).to(torch.float32).to(self.device)
|
232
|
+
self.eval_every_n_trees = eval_every_n_trees
|
233
|
+
self.early_stopping_rounds = early_stopping_rounds
|
234
|
+
else:
|
235
|
+
self.bin_indices_eval = None
|
236
|
+
self.Y_gpu_eval = None
|
237
|
+
self.eval_every_n_trees = None
|
238
|
+
self.early_stopping_rounds = None
|
239
|
+
|
240
|
+
# ─── Grow the forest ───
|
140
241
|
with torch.no_grad():
|
141
|
-
self.
|
242
|
+
self.grow_forest()
|
243
|
+
|
142
244
|
return self
|
143
245
|
|
144
246
|
def preprocess_gpu_data(self, X_np, Y_np, era_id_np):
|
@@ -292,11 +394,34 @@ class WarpGBM(BaseEstimator, RegressorMixin):
|
|
292
394
|
"right": right_child,
|
293
395
|
}
|
294
396
|
|
397
|
+
def compute_eval(self, i):
|
398
|
+
if self.eval_every_n_trees == None:
|
399
|
+
return
|
400
|
+
|
401
|
+
if i % self.eval_every_n_trees == 0:
|
402
|
+
eval_preds = self.predict_binned(self.bin_indices_eval)
|
403
|
+
eval_loss = ((self.Y_gpu_eval - eval_preds) ** 2).mean().item()
|
404
|
+
self.eval_loss.append(eval_loss)
|
405
|
+
|
406
|
+
train_loss = ((self.Y_gpu - self.gradients) ** 2).mean().item()
|
407
|
+
self.training_loss.append(train_loss)
|
408
|
+
|
409
|
+
if len(self.eval_loss) > self.early_stopping_rounds:
|
410
|
+
if self.eval_loss[-self.early_stopping_rounds] < self.eval_loss[-1]:
|
411
|
+
self.stop = True
|
412
|
+
|
413
|
+
print(
|
414
|
+
f"🌲 Tree {i+1}/{self.n_estimators} | Train MSE: {train_loss:.6f} | Eval MSE: {eval_loss:.6f}"
|
415
|
+
)
|
416
|
+
|
417
|
+
del eval_preds, eval_loss, train_loss
|
418
|
+
|
295
419
|
def grow_forest(self):
|
296
|
-
forest = [{} for _ in range(self.n_estimators)]
|
297
420
|
self.training_loss = []
|
421
|
+
self.eval_loss = [] # <-- if eval set is given
|
422
|
+
self.stop = False
|
298
423
|
|
299
|
-
for i in
|
424
|
+
for i in range(self.n_estimators):
|
300
425
|
self.residual = self.Y_gpu - self.gradients
|
301
426
|
|
302
427
|
self.root_gradient_histogram, self.root_hessian_histogram = (
|
@@ -309,21 +434,21 @@ class WarpGBM(BaseEstimator, RegressorMixin):
|
|
309
434
|
self.root_node_indices,
|
310
435
|
depth=0,
|
311
436
|
)
|
312
|
-
forest[i] = tree
|
313
|
-
|
314
|
-
|
315
|
-
|
437
|
+
self.forest[i] = tree
|
438
|
+
|
439
|
+
self.compute_eval(i)
|
440
|
+
|
441
|
+
if self.stop:
|
442
|
+
break
|
316
443
|
|
317
444
|
print("Finished training forest.")
|
318
|
-
return forest
|
319
445
|
|
320
|
-
def
|
446
|
+
def bin_data_with_existing_edges(self, X_np):
|
321
447
|
X_tensor = torch.from_numpy(X_np).to(torch.float32).pin_memory()
|
322
448
|
num_samples = X_tensor.size(0)
|
323
449
|
bin_indices = torch.zeros(
|
324
450
|
(num_samples, self.num_features), dtype=torch.int8, device=self.device
|
325
451
|
)
|
326
|
-
|
327
452
|
with torch.no_grad():
|
328
453
|
for f in range(self.num_features):
|
329
454
|
X_f = X_tensor[:, f].to(self.device, non_blocking=True)
|
@@ -332,10 +457,16 @@ class WarpGBM(BaseEstimator, RegressorMixin):
|
|
332
457
|
node_kernel.custom_cuda_binner(X_f, bin_edges_f, bin_indices_f)
|
333
458
|
bin_indices[:, f] = bin_indices_f
|
334
459
|
|
460
|
+
return bin_indices
|
461
|
+
|
462
|
+
def predict_binned(self, bin_indices):
|
463
|
+
num_samples = bin_indices.size(0)
|
464
|
+
|
335
465
|
tree_tensor = torch.stack(
|
336
466
|
[
|
337
467
|
self.flatten_tree(tree, max_nodes=2 ** (self.max_depth + 1))
|
338
468
|
for tree in self.forest
|
469
|
+
if tree
|
339
470
|
]
|
340
471
|
).to(self.device)
|
341
472
|
|
@@ -344,6 +475,11 @@ class WarpGBM(BaseEstimator, RegressorMixin):
|
|
344
475
|
bin_indices.contiguous(), tree_tensor.contiguous(), self.learning_rate, out
|
345
476
|
)
|
346
477
|
|
478
|
+
return out
|
479
|
+
|
480
|
+
def predict(self, X_np):
|
481
|
+
bin_indices = self.bin_data_with_existing_edges(X_np)
|
482
|
+
out = self.predict_binned(bin_indices)
|
347
483
|
return out.cpu().numpy()
|
348
484
|
|
349
485
|
def flatten_tree(self, tree, max_nodes):
|
@@ -38,7 +38,7 @@ __global__ void best_split_kernel_global_only(
|
|
38
38
|
|
39
39
|
if (H_L >= min_child_samples && H_R >= min_child_samples)
|
40
40
|
{
|
41
|
-
float gain = (G_L * G_L) / (H_L + eps) + (G_R * G_R) / (H_R + eps);
|
41
|
+
float gain = (G_L * G_L) / (H_L + eps) + (G_R * G_R) / (H_R + eps) - (G_total * G_total) / (H_total + eps);
|
42
42
|
if (gain > best_gain)
|
43
43
|
{
|
44
44
|
best_gain = gain;
|
warpgbm-0.1.22/version.txt
DELETED
@@ -1 +0,0 @@
|
|
1
|
-
0.1.22
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|