pejmantheory commited on
Commit
426b48a
·
verified ·
1 Parent(s): 7f5feef

Publish the Covertype XGBoost classifier

Browse files

Replaces the breast-cancer demo with a 400-tree model trained on all 581,012 UCI Covertype rows. Test accuracy 0.9109 and macro F1 0.8942, scored once against a majority baseline and a small XGBoost baseline.

Files changed (5) hide show
  1. .gitattributes +1 -0
  2. README.md +50 -46
  3. metrics.json +285 -53
  4. model.json +0 -0
  5. train.py +188 -0
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ model.json filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -7,57 +7,72 @@ tags:
7
  - tabular
8
  - bleu-js
9
  datasets:
10
- - breast_cancer
11
  metrics:
12
  - accuracy
13
  - f1
14
- - roc_auc
15
  model-index:
16
  - name: bleu-xgboost-classifier
17
  results:
18
  - task:
19
- type: binary-classification
20
  dataset:
21
- name: Breast Cancer Wisconsin (Diagnostic)
22
- type: breast_cancer
23
  metrics:
24
  - type: accuracy
25
- value: 0.9473684210526315
26
- - type: f1
27
- value: 0.958904109589041
28
- - type: roc_auc
29
- value: 0.9923941798941799
30
  ---
31
 
32
  # bleu-xgboost-classifier
33
 
34
- XGBoost binary classifier trained by the Bleu.js SDK trainer `bleujs.ml.HybridTrainer`.
35
 
36
- This is a small public-dataset model. It is not the hosted Bleu.js API. That API is `https://api.bleujs.org`. The default `pip install bleu-js` package does not include this model. Load the weights below, or train your own data with `pip install "bleu-js[ml]"`.
37
 
38
- ## Result
39
 
40
- Held-out test set only. Train/test split is 80/20, `random_state=42`, stratified.
41
 
42
- | Metric | Test score |
43
- | --- | --- |
44
- | Accuracy | 0.9474 (108/114) |
45
- | F1 | 0.9589 |
46
- | ROC AUC | 0.9924 |
47
 
48
- 455 training rows, 114 test rows, 30 numeric features. Class `1` is benign and class `0` is malignant, which is the scikit-learn encoding of this dataset. The benign rate is about 0.63 in both splits.
49
 
50
- These scores are for this split of this dataset. They are not a medical-device claim and they do not transfer to other data.
51
 
52
- ## Data and training
 
 
 
 
 
 
 
 
53
 
54
- - **Dataset:** Breast Cancer Wisconsin (Diagnostic), from the UCI Machine Learning Repository, loaded with `sklearn.datasets.load_breast_cancer`.
55
- - **Trainer:** `HybridTrainer(model_type="xgboost")` in `src/bleujs/ml.py`. Quantum features were off.
56
- - **Hyperparameters:** `max_depth=6`, `learning_rate=0.1`, `n_estimators=100`, `objective=binary:logistic`.
57
- - **XGBoost:** 3.2.0. No feature scaler. The trainer fits the classifier on the raw feature values.
58
- - **File:** `model.json` is the XGBoost native save, not a pickle. `metrics.json` records the split, feature names, and scores.
59
 
60
- ## Load the weights
 
 
 
 
 
 
 
 
 
 
 
 
61
 
62
  ```bash
63
  pip install xgboost huggingface_hub
@@ -74,30 +89,19 @@ path = hf_hub_download(
74
  model = xgb.XGBClassifier()
75
  model.load_model(path)
76
 
77
- # X is (n_samples, 30), in the feature order stored in metrics.json
 
78
  pred = model.predict(X)
79
- proba = model.predict_proba(X)
80
- ```
81
-
82
- ## Train the same way on your own table
83
-
84
- ```bash
85
- pip install "bleu-js[ml]"
86
  ```
87
 
88
- ```python
89
- from bleujs.ml import HybridTrainer
90
-
91
- trainer = HybridTrainer(model_type="xgboost")
92
- model = trainer.train(X_train, y_train)
93
- ```
94
 
95
- Python 3.11, 3.12, or 3.13. Source: [HelloblueAI/Bleu.js](https://github.com/HelloblueAI/Bleu.js).
96
 
97
- ## License
98
 
99
- MIT. The Wisconsin breast cancer data is distributed with scikit-learn from the UCI repository.
100
 
101
  ## Contact
102
 
103
- support@helloblue.ai
 
7
  - tabular
8
  - bleu-js
9
  datasets:
10
+ - covertype
11
  metrics:
12
  - accuracy
13
  - f1
 
14
  model-index:
15
  - name: bleu-xgboost-classifier
16
  results:
17
  - task:
18
+ type: multiclass-classification
19
  dataset:
20
+ name: UCI Covertype
21
+ type: covertype
22
  metrics:
23
  - type: accuracy
24
+ value: 0.9108887033897576
25
+ - type: macro_f1
26
+ value: 0.8941719351718758
27
+ - type: weighted_f1
28
+ value: 0.9115377043223944
29
  ---
30
 
31
  # bleu-xgboost-classifier
32
 
33
+ Multiclass XGBoost model that predicts forest cover type from cartographic measurements. Trained for the Bleu.js open-source SDK on the full public UCI Covertype table: **581,012 rows**, **54 features**, **7 cover types**.
34
 
35
+ This is not the hosted Bleu.js API. That API is `https://api.bleujs.org`. `pip install bleu-js` does not download these weights.
36
 
37
+ ## Held-out test result
38
 
39
+ The test split is 20% of the table (116,203 rows), stratified, `random_state=42`. It was scored once. Tree count was chosen on an inner 10% slice of the training rows only. Early stopping did not fire, so the published model uses the full cap of **400 trees**.
40
 
41
+ | Model | Accuracy | Macro F1 | Weighted F1 |
42
+ | --- | ---: | ---: | ---: |
43
+ | This model | 0.9109 | 0.8942 | 0.9115 |
44
+ | Small XGBoost baseline (depth 3, 50 trees, no class weights) | 0.7319 | 0.5328 | 0.7167 |
45
+ | Majority class | 0.4876 | 0.0937 | 0.3197 |
46
 
47
+ Log loss on the test rows is 0.2391 for this model and 0.6358 for the small baseline.
48
 
49
+ Per-class F1 on the same test rows:
50
 
51
+ | Cover type | F1 | Support |
52
+ | --- | ---: | ---: |
53
+ | Spruce/Fir | 0.9057 | 42,368 |
54
+ | Lodgepole Pine | 0.9164 | 56,661 |
55
+ | Ponderosa Pine | 0.9371 | 7,151 |
56
+ | Cottonwood/Willow | 0.8942 | 549 |
57
+ | Aspen | 0.7673 | 1,899 |
58
+ | Douglas-fir | 0.8961 | 3,473 |
59
+ | Krummholz | 0.9425 | 4,102 |
60
 
61
+ Aspen is the weak class. Lodgepole Pine is almost half the table, which is why accuracy alone is a poor summary and macro F1 is the number to quote.
 
 
 
 
62
 
63
+ The largest gain features are Cache la Poudre wilderness, several soil-type indicators, and elevation. The full list is in `metrics.json`.
64
+
65
+ ## What the weights are
66
+
67
+ - `model.json` — XGBoost native format, not a pickle. About 29 MB.
68
+ - `metrics.json` — split, hyperparameters, baselines, per-class scores, feature gain.
69
+ - `train.py` — the program that produced this file. Run it again to reproduce the scores.
70
+
71
+ Settings: `max_depth=8`, `learning_rate=0.1`, `subsample=0.8`, `colsample_bytree=0.8`, `min_child_weight=5`, `reg_lambda=1.0`, `tree_method=hist`, `objective=multi:softprob`, balanced sample weights, XGBoost 3.2.0.
72
+
73
+ `bleujs.ml.HybridTrainer` fits a binary classifier (`binary:logistic`). This model has seven classes, so it is trained with XGBoost directly. The SDK trainer remains the path for your own binary tables: `pip install "bleu-js[ml]"`.
74
+
75
+ ## Load
76
 
77
  ```bash
78
  pip install xgboost huggingface_hub
 
89
  model = xgb.XGBClassifier()
90
  model.load_model(path)
91
 
92
+ # X shape (n_samples, 54), column order in metrics.json
93
+ # class 0 is Spruce/Fir ... class 6 is Krummholz (UCI codes 1..7)
94
  pred = model.predict(X)
 
 
 
 
 
 
 
95
  ```
96
 
97
+ ## Data
 
 
 
 
 
98
 
99
+ Covertype, UCI Machine Learning Repository, loaded with `sklearn.datasets.fetch_covtype`. Four wilderness areas in Roosevelt National Forest, Colorado. Copyright Jock A. Blackard and Colorado State University. The model card, training script, and published weights are MIT. The dataset keeps its own terms.
100
 
101
+ ## Out of scope
102
 
103
+ These scores are for this split of this Colorado table. They are not a map product, not a claim about other forests, and not a leaderboard entry. No search was run against the test split.
104
 
105
  ## Contact
106
 
107
+ support@helloblue.ai · [HelloblueAI/Bleu.js](https://github.com/HelloblueAI/Bleu.js)
metrics.json CHANGED
@@ -1,65 +1,297 @@
1
  {
2
- "dataset": "breast_cancer",
3
- "dataset_name": "Breast Cancer Wisconsin (Diagnostic)",
4
- "source": "sklearn.datasets.load_breast_cancer",
5
- "origin": "UCI Machine Learning Repository, via scikit-learn",
6
- "task": "binary classification",
7
- "positive_class": "benign",
8
- "negative_class": "malignant",
9
- "feature_names": [
10
- "mean radius",
11
- "mean texture",
12
- "mean perimeter",
13
- "mean area",
14
- "mean smoothness",
15
- "mean compactness",
16
- "mean concavity",
17
- "mean concave points",
18
- "mean symmetry",
19
- "mean fractal dimension",
20
- "radius error",
21
- "texture error",
22
- "perimeter error",
23
- "area error",
24
- "smoothness error",
25
- "compactness error",
26
- "concavity error",
27
- "concave points error",
28
- "symmetry error",
29
- "fractal dimension error",
30
- "worst radius",
31
- "worst texture",
32
- "worst perimeter",
33
- "worst area",
34
- "worst smoothness",
35
- "worst compactness",
36
- "worst concavity",
37
- "worst concave points",
38
- "worst symmetry",
39
- "worst fractal dimension"
40
  ],
 
41
  "split": {
42
  "test_size": 0.2,
 
43
  "random_state": 42,
44
  "stratify": true
45
  },
46
- "trainer": "bleujs.ml.HybridTrainer",
47
- "quantum_components": false,
 
48
  "hyperparameters": {
49
- "max_depth": 6,
50
  "learning_rate": 0.1,
51
- "n_estimators": 100,
52
- "objective": "binary:logistic"
 
 
 
 
 
 
 
 
 
53
  },
54
  "xgboost_version": "3.2.0",
55
- "metrics_on_held_out_test": {
56
- "accuracy": 0.9473684210526315,
57
- "f1": 0.958904109589041,
58
- "roc_auc": 0.9923941798941799,
59
- "n_train": 455,
60
- "n_test": 114,
61
- "n_features": 30,
62
- "positive_rate_train": 0.6263736263736264,
63
- "positive_rate_test": 0.631578947368421
64
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
  }
 
1
  {
2
+ "dataset": "UCI Covertype",
3
+ "source": "sklearn.datasets.fetch_covtype",
4
+ "citation": "Blackard, Jock A. Covertype. UCI Machine Learning Repository.",
5
+ "n_rows": 581012,
6
+ "n_features": 54,
7
+ "class_names": [
8
+ "Spruce/Fir",
9
+ "Lodgepole Pine",
10
+ "Ponderosa Pine",
11
+ "Cottonwood/Willow",
12
+ "Aspen",
13
+ "Douglas-fir",
14
+ "Krummholz"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  ],
16
+ "label_encoding": "model class i is UCI cover type i+1",
17
  "split": {
18
  "test_size": 0.2,
19
+ "inner_val_size": 0.1,
20
  "random_state": 42,
21
  "stratify": true
22
  },
23
+ "n_train": 464809,
24
+ "n_test": 116203,
25
+ "n_estimators_selected_on_inner_val": 400,
26
  "hyperparameters": {
27
+ "max_depth": 8,
28
  "learning_rate": 0.1,
29
+ "subsample": 0.8,
30
+ "colsample_bytree": 0.8,
31
+ "min_child_weight": 5,
32
+ "reg_lambda": 1.0,
33
+ "tree_method": "hist",
34
+ "objective": "multi:softprob",
35
+ "num_class": 7,
36
+ "random_state": 42,
37
+ "n_jobs": -1,
38
+ "n_estimators": 400,
39
+ "sample_weight": "balanced"
40
  },
41
  "xgboost_version": "3.2.0",
42
+ "trainer_note": "XGBClassifier. bleujs.ml.HybridTrainer targets binary classification with binary:logistic, so this multiclass model is trained with the same library and a documented multiclass objective.",
43
+ "test_metrics": {
44
+ "accuracy": 0.9108887033897576,
45
+ "macro_f1": 0.8941719351718758,
46
+ "weighted_f1": 0.9115377043223944,
47
+ "log_loss": 0.23914723098278046
48
+ },
49
+ "simple_xgboost_baseline": {
50
+ "accuracy": 0.7318916034870012,
51
+ "macro_f1": 0.5327755702159982,
52
+ "weighted_f1": 0.7166892879546547,
53
+ "log_loss": 0.6357948184013367
54
+ },
55
+ "majority_baseline": {
56
+ "accuracy": 0.48760359026875383,
57
+ "macro_f1": 0.09365083038028243,
58
+ "weighted_f1": 0.3196513678755306,
59
+ "log_loss": 8.168821334838867
60
+ },
61
+ "per_class_f1": {
62
+ "Spruce/Fir": 0.9057362118202865,
63
+ "Lodgepole Pine": 0.9163635050597145,
64
+ "Ponderosa Pine": 0.9370629370629371,
65
+ "Cottonwood/Willow": 0.8941798941798942,
66
+ "Aspen": 0.767287784679089,
67
+ "Douglas-fir": 0.8960845413900556,
68
+ "Krummholz": 0.9424886720111537
69
+ },
70
+ "per_class_support": {
71
+ "Spruce/Fir": 42368,
72
+ "Lodgepole Pine": 56661,
73
+ "Ponderosa Pine": 7151,
74
+ "Cottonwood/Willow": 549,
75
+ "Aspen": 1899,
76
+ "Douglas-fir": 3473,
77
+ "Krummholz": 4102
78
+ },
79
+ "feature_importance_gain": [
80
+ {
81
+ "feature": "Wilderness_Area_3",
82
+ "gain": 249.5696563720703
83
+ },
84
+ {
85
+ "feature": "Soil_Type_38",
86
+ "gain": 129.66807556152344
87
+ },
88
+ {
89
+ "feature": "Soil_Type_11",
90
+ "gain": 120.6527328491211
91
+ },
92
+ {
93
+ "feature": "Soil_Type_2",
94
+ "gain": 113.96896362304688
95
+ },
96
+ {
97
+ "feature": "Elevation",
98
+ "gain": 106.00982666015625
99
+ },
100
+ {
101
+ "feature": "Soil_Type_9",
102
+ "gain": 95.48499298095703
103
+ },
104
+ {
105
+ "feature": "Soil_Type_29",
106
+ "gain": 79.35009765625
107
+ },
108
+ {
109
+ "feature": "Soil_Type_3",
110
+ "gain": 69.28760528564453
111
+ },
112
+ {
113
+ "feature": "Wilderness_Area_0",
114
+ "gain": 67.1938247680664
115
+ },
116
+ {
117
+ "feature": "Soil_Type_39",
118
+ "gain": 65.25533294677734
119
+ },
120
+ {
121
+ "feature": "Soil_Type_37",
122
+ "gain": 62.631927490234375
123
+ },
124
+ {
125
+ "feature": "Soil_Type_17",
126
+ "gain": 60.56175231933594
127
+ },
128
+ {
129
+ "feature": "Soil_Type_1",
130
+ "gain": 53.69615936279297
131
+ },
132
+ {
133
+ "feature": "Soil_Type_34",
134
+ "gain": 49.5882453918457
135
+ },
136
+ {
137
+ "feature": "Soil_Type_21",
138
+ "gain": 46.41263198852539
139
+ },
140
+ {
141
+ "feature": "Wilderness_Area_2",
142
+ "gain": 43.265750885009766
143
+ },
144
+ {
145
+ "feature": "Soil_Type_12",
146
+ "gain": 40.007808685302734
147
+ },
148
+ {
149
+ "feature": "Soil_Type_16",
150
+ "gain": 38.801177978515625
151
+ },
152
+ {
153
+ "feature": "Soil_Type_31",
154
+ "gain": 34.57275390625
155
+ },
156
+ {
157
+ "feature": "Wilderness_Area_1",
158
+ "gain": 31.31096839904785
159
+ },
160
+ {
161
+ "feature": "Soil_Type_5",
162
+ "gain": 30.942785263061523
163
+ },
164
+ {
165
+ "feature": "Soil_Type_4",
166
+ "gain": 28.343896865844727
167
+ },
168
+ {
169
+ "feature": "Soil_Type_30",
170
+ "gain": 24.622051239013672
171
+ },
172
+ {
173
+ "feature": "Soil_Type_10",
174
+ "gain": 24.064062118530273
175
+ },
176
+ {
177
+ "feature": "Soil_Type_0",
178
+ "gain": 23.00113296508789
179
+ },
180
+ {
181
+ "feature": "Soil_Type_32",
182
+ "gain": 22.943172454833984
183
+ },
184
+ {
185
+ "feature": "Soil_Type_22",
186
+ "gain": 22.0693302154541
187
+ },
188
+ {
189
+ "feature": "Soil_Type_28",
190
+ "gain": 21.283544540405273
191
+ },
192
+ {
193
+ "feature": "Soil_Type_23",
194
+ "gain": 19.36112403869629
195
+ },
196
+ {
197
+ "feature": "Hillshade_9am",
198
+ "gain": 18.579858779907227
199
+ },
200
+ {
201
+ "feature": "Soil_Type_19",
202
+ "gain": 17.969446182250977
203
+ },
204
+ {
205
+ "feature": "Horizontal_Distance_To_Hydrology",
206
+ "gain": 17.332067489624023
207
+ },
208
+ {
209
+ "feature": "Soil_Type_36",
210
+ "gain": 16.103273391723633
211
+ },
212
+ {
213
+ "feature": "Horizontal_Distance_To_Roadways",
214
+ "gain": 15.786291122436523
215
+ },
216
+ {
217
+ "feature": "Soil_Type_25",
218
+ "gain": 15.337019920349121
219
+ },
220
+ {
221
+ "feature": "Horizontal_Distance_To_Fire_Points",
222
+ "gain": 14.771939277648926
223
+ },
224
+ {
225
+ "feature": "Soil_Type_13",
226
+ "gain": 14.268491744995117
227
+ },
228
+ {
229
+ "feature": "Soil_Type_18",
230
+ "gain": 12.966172218322754
231
+ },
232
+ {
233
+ "feature": "Soil_Type_33",
234
+ "gain": 11.3596830368042
235
+ },
236
+ {
237
+ "feature": "Vertical_Distance_To_Hydrology",
238
+ "gain": 11.144617080688477
239
+ },
240
+ {
241
+ "feature": "Aspect",
242
+ "gain": 10.377111434936523
243
+ },
244
+ {
245
+ "feature": "Soil_Type_8",
246
+ "gain": 9.88647174835205
247
+ },
248
+ {
249
+ "feature": "Hillshade_Noon",
250
+ "gain": 9.631880760192871
251
+ },
252
+ {
253
+ "feature": "Soil_Type_27",
254
+ "gain": 9.097264289855957
255
+ },
256
+ {
257
+ "feature": "Slope",
258
+ "gain": 8.322596549987793
259
+ },
260
+ {
261
+ "feature": "Hillshade_3pm",
262
+ "gain": 8.168411254882812
263
+ },
264
+ {
265
+ "feature": "Soil_Type_14",
266
+ "gain": 7.7810516357421875
267
+ },
268
+ {
269
+ "feature": "Soil_Type_20",
270
+ "gain": 7.463456630706787
271
+ },
272
+ {
273
+ "feature": "Soil_Type_15",
274
+ "gain": 7.146697998046875
275
+ },
276
+ {
277
+ "feature": "Soil_Type_26",
278
+ "gain": 7.062173366546631
279
+ },
280
+ {
281
+ "feature": "Soil_Type_24",
282
+ "gain": 4.399511814117432
283
+ },
284
+ {
285
+ "feature": "Soil_Type_35",
286
+ "gain": 3.7608084678649902
287
+ },
288
+ {
289
+ "feature": "Soil_Type_7",
290
+ "gain": 1.7688950300216675
291
+ },
292
+ {
293
+ "feature": "Soil_Type_6",
294
+ "gain": 1.6422888040542603
295
+ }
296
+ ]
297
  }
model.json CHANGED
The diff for this file is too large to render. See raw diff
 
train.py ADDED
@@ -0,0 +1,188 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Train and score the Bleu.js forest-cover XGBoost classifier.
2
+
3
+ Protocol, fixed before the test set is scored:
4
+ - Data: sklearn.datasets.fetch_covtype (UCI Covertype, full table)
5
+ - Labels shifted from 1..7 to 0..6
6
+ - Outer split: stratified 80/20, random_state=42. The 20% is scored once.
7
+ - Inner split: stratified 90/10 of the training rows, random_state=42,
8
+ used only to pick n_estimators with early stopping.
9
+ - Final fit: all training rows, n_estimators = best_iteration + 1,
10
+ sample_weight = balanced class weights from the training rows.
11
+ - Baselines, same test rows, no retuning: majority class; a depth-3
12
+ XGBoost with 50 trees and no class weights.
13
+ """
14
+
15
+ import json
16
+ from pathlib import Path
17
+
18
+ import numpy as np
19
+ import xgboost as xgb
20
+ from sklearn.datasets import fetch_covtype
21
+ from sklearn.metrics import (
22
+ accuracy_score,
23
+ classification_report,
24
+ f1_score,
25
+ log_loss,
26
+ )
27
+ from sklearn.model_selection import train_test_split
28
+ from sklearn.utils.class_weight import compute_sample_weight
29
+
30
+ OUT = Path(__file__).resolve().parent
31
+ RANDOM_STATE = 42
32
+ CLASS_NAMES = [
33
+ "Spruce/Fir",
34
+ "Lodgepole Pine",
35
+ "Ponderosa Pine",
36
+ "Cottonwood/Willow",
37
+ "Aspen",
38
+ "Douglas-fir",
39
+ "Krummholz",
40
+ ]
41
+ FEATURE_NAMES = [
42
+ "Elevation",
43
+ "Aspect",
44
+ "Slope",
45
+ "Horizontal_Distance_To_Hydrology",
46
+ "Vertical_Distance_To_Hydrology",
47
+ "Horizontal_Distance_To_Roadways",
48
+ "Hillshade_9am",
49
+ "Hillshade_Noon",
50
+ "Hillshade_3pm",
51
+ "Horizontal_Distance_To_Fire_Points",
52
+ *[f"Wilderness_Area_{i}" for i in range(4)],
53
+ *[f"Soil_Type_{i}" for i in range(40)],
54
+ ]
55
+
56
+
57
+ def scores(y_true, pred, proba):
58
+ return {
59
+ "accuracy": float(accuracy_score(y_true, pred)),
60
+ "macro_f1": float(f1_score(y_true, pred, average="macro")),
61
+ "weighted_f1": float(f1_score(y_true, pred, average="weighted")),
62
+ "log_loss": float(log_loss(y_true, proba, labels=list(range(7)))),
63
+ }
64
+
65
+
66
+ def main():
67
+ bunch = fetch_covtype(as_frame=False)
68
+ X = np.asarray(bunch.data, dtype=np.float32)
69
+ y = np.asarray(bunch.target, dtype=np.int32) - 1
70
+ if X.shape[1] != len(FEATURE_NAMES) or y.min() != 0 or y.max() != 6:
71
+ raise SystemExit(f"Unexpected covertype shape {X.shape} labels {y.min()}..{y.max()}")
72
+
73
+ X_train, X_test, y_train, y_test = train_test_split(
74
+ X, y, test_size=0.2, random_state=RANDOM_STATE, stratify=y
75
+ )
76
+ X_fit, X_val, y_fit, y_val = train_test_split(
77
+ X_train, y_train, test_size=0.1, random_state=RANDOM_STATE, stratify=y_train
78
+ )
79
+
80
+ common = dict(
81
+ max_depth=8,
82
+ learning_rate=0.1,
83
+ subsample=0.8,
84
+ colsample_bytree=0.8,
85
+ min_child_weight=5,
86
+ reg_lambda=1.0,
87
+ tree_method="hist",
88
+ objective="multi:softprob",
89
+ num_class=7,
90
+ random_state=RANDOM_STATE,
91
+ n_jobs=-1,
92
+ )
93
+ search = xgb.XGBClassifier(n_estimators=400, early_stopping_rounds=20, **common)
94
+ search.fit(
95
+ X_fit,
96
+ y_fit,
97
+ sample_weight=compute_sample_weight("balanced", y_fit),
98
+ eval_set=[(X_val, y_val)],
99
+ verbose=False,
100
+ )
101
+ n_trees = int(search.best_iteration) + 1
102
+
103
+ model = xgb.XGBClassifier(n_estimators=n_trees, **common)
104
+ model.fit(
105
+ X_train,
106
+ y_train,
107
+ sample_weight=compute_sample_weight("balanced", y_train),
108
+ verbose=False,
109
+ )
110
+
111
+ simple = xgb.XGBClassifier(
112
+ n_estimators=50,
113
+ max_depth=3,
114
+ learning_rate=0.1,
115
+ tree_method="hist",
116
+ objective="multi:softprob",
117
+ num_class=7,
118
+ random_state=RANDOM_STATE,
119
+ n_jobs=-1,
120
+ )
121
+ simple.fit(X_train, y_train, verbose=False)
122
+
123
+ proba = model.predict_proba(X_test)
124
+ pred = proba.argmax(axis=1)
125
+ simple_proba = simple.predict_proba(X_test)
126
+ simple_pred = simple_proba.argmax(axis=1)
127
+ majority = int(np.bincount(y_train).argmax())
128
+ majority_pred = np.full_like(y_test, majority)
129
+ majority_proba = np.zeros_like(proba)
130
+ majority_proba[:, majority] = 1.0
131
+
132
+ report = classification_report(
133
+ y_test, pred, target_names=CLASS_NAMES, output_dict=True, digits=4
134
+ )
135
+ gain = model.get_booster().get_score(importance_type="gain")
136
+ importance = sorted(
137
+ (
138
+ {"feature": FEATURE_NAMES[int(k[1:])], "gain": float(v)}
139
+ for k, v in gain.items()
140
+ ),
141
+ key=lambda row: row["gain"],
142
+ reverse=True,
143
+ )
144
+
145
+ payload = {
146
+ "dataset": "UCI Covertype",
147
+ "source": "sklearn.datasets.fetch_covtype",
148
+ "citation": "Blackard, Jock A. Covertype. UCI Machine Learning Repository.",
149
+ "n_rows": int(X.shape[0]),
150
+ "n_features": int(X.shape[1]),
151
+ "class_names": CLASS_NAMES,
152
+ "label_encoding": "model class i is UCI cover type i+1",
153
+ "split": {
154
+ "test_size": 0.2,
155
+ "inner_val_size": 0.1,
156
+ "random_state": RANDOM_STATE,
157
+ "stratify": True,
158
+ },
159
+ "n_train": int(len(y_train)),
160
+ "n_test": int(len(y_test)),
161
+ "n_estimators_selected_on_inner_val": n_trees,
162
+ "hyperparameters": {**common, "n_estimators": n_trees, "sample_weight": "balanced"},
163
+ "xgboost_version": xgb.__version__,
164
+ "trainer_note": (
165
+ "XGBClassifier. bleujs.ml.HybridTrainer targets binary classification "
166
+ "with binary:logistic, so this multiclass model is trained with the "
167
+ "same library and a documented multiclass objective."
168
+ ),
169
+ "test_metrics": scores(y_test, pred, proba),
170
+ "simple_xgboost_baseline": scores(y_test, simple_pred, simple_proba),
171
+ "majority_baseline": scores(y_test, majority_pred, majority_proba),
172
+ "per_class_f1": {name: float(report[name]["f1-score"]) for name in CLASS_NAMES},
173
+ "per_class_support": {name: int(report[name]["support"]) for name in CLASS_NAMES},
174
+ "feature_importance_gain": importance,
175
+ }
176
+ model.save_model(OUT / "model.json")
177
+ (OUT / "metrics.json").write_text(json.dumps(payload, indent=2))
178
+ print(json.dumps({
179
+ "n_trees": n_trees,
180
+ "test": payload["test_metrics"],
181
+ "simple": payload["simple_xgboost_baseline"],
182
+ "majority": payload["majority_baseline"],
183
+ "per_class_f1": payload["per_class_f1"],
184
+ }, indent=2))
185
+
186
+
187
+ if __name__ == "__main__":
188
+ main()