Justin-Williams-CV-Benchmarking 1.0.0__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.
- justin_williams_cv_benchmarking/__init__.py +21 -0
- justin_williams_cv_benchmarking/benchmark.py +416 -0
- justin_williams_cv_benchmarking/classical_models.py +58 -0
- justin_williams_cv_benchmarking/config.py +56 -0
- justin_williams_cv_benchmarking/data_loader.py +439 -0
- justin_williams_cv_benchmarking/evaluation.py +160 -0
- justin_williams_cv_benchmarking/neural_models.py +280 -0
- justin_williams_cv_benchmarking/preprocessing.py +273 -0
- justin_williams_cv_benchmarking/visualization.py +239 -0
- justin_williams_cv_benchmarking-1.0.0.dist-info/METADATA +702 -0
- justin_williams_cv_benchmarking-1.0.0.dist-info/RECORD +14 -0
- justin_williams_cv_benchmarking-1.0.0.dist-info/WHEEL +5 -0
- justin_williams_cv_benchmarking-1.0.0.dist-info/licenses/LICENSE +21 -0
- justin_williams_cv_benchmarking-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Justin_Williams_CV_Benchmarking.
|
|
2
|
+
|
|
3
|
+
Benchmark six classical and neural image classifiers on any labelled image
|
|
4
|
+
dataset with a single function call::
|
|
5
|
+
|
|
6
|
+
from justin_williams_cv_benchmarking import benchmark_image_classification
|
|
7
|
+
|
|
8
|
+
results = benchmark_image_classification(
|
|
9
|
+
dataset="./animals",
|
|
10
|
+
dataset_type="folder",
|
|
11
|
+
target_labels=["cat", "dog", "horse"],
|
|
12
|
+
color_mode="rgb",
|
|
13
|
+
)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
__version__ = "1.0.0"
|
|
17
|
+
|
|
18
|
+
from .benchmark import benchmark_image_classification # noqa: E402
|
|
19
|
+
from .data_loader import DatasetError # noqa: E402
|
|
20
|
+
|
|
21
|
+
__all__ = ["benchmark_image_classification", "DatasetError", "__version__"]
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
"""The public entry point: :func:`benchmark_image_classification`.
|
|
2
|
+
|
|
3
|
+
One call loads any of the four supported dataset organisations, standardises
|
|
4
|
+
it, creates a single stratified split, trains all six required models on that
|
|
5
|
+
one split, evaluates them with identical metrics, writes every required
|
|
6
|
+
artefact to ``benchmark_results/`` and returns the whole comparison.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
import platform
|
|
14
|
+
import random
|
|
15
|
+
import time
|
|
16
|
+
from typing import Any, Dict
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
|
|
20
|
+
from . import __version__
|
|
21
|
+
from .classical_models import build_classical_models, describe
|
|
22
|
+
from .config import (
|
|
23
|
+
CLASSIFICATION_REPORT_DIR,
|
|
24
|
+
CNN_BATCH_SIZE,
|
|
25
|
+
CNN_EARLY_STOPPING_PATIENCE,
|
|
26
|
+
CNN_MAX_EPOCHS,
|
|
27
|
+
CONFUSION_MATRIX_DIR,
|
|
28
|
+
IMAGE_SIZE,
|
|
29
|
+
MODEL_FILE_STEMS,
|
|
30
|
+
MODEL_ORDER,
|
|
31
|
+
OUTPUT_DIR,
|
|
32
|
+
PACKAGE_NAME,
|
|
33
|
+
RANDOM_SEED,
|
|
34
|
+
TEST_SIZE,
|
|
35
|
+
)
|
|
36
|
+
from .data_loader import DatasetError, load_dataset
|
|
37
|
+
from .evaluation import (
|
|
38
|
+
build_summary,
|
|
39
|
+
best_model_name,
|
|
40
|
+
classification_report_frame,
|
|
41
|
+
compute_metrics,
|
|
42
|
+
confusion_matrix_array,
|
|
43
|
+
prediction_examples,
|
|
44
|
+
)
|
|
45
|
+
from .neural_models import build_cnn_model, build_mlp_model
|
|
46
|
+
from .preprocessing import distribution, prepare_dataset, stratified_split
|
|
47
|
+
from .visualization import (
|
|
48
|
+
ensure_directory,
|
|
49
|
+
plot_class_distribution,
|
|
50
|
+
plot_confusion_matrix,
|
|
51
|
+
plot_model_comparison,
|
|
52
|
+
plot_prediction_examples,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
__all__ = ["benchmark_image_classification", "DatasetError"]
|
|
56
|
+
|
|
57
|
+
# Models fed the flattened feature vectors rather than the image tensor.
|
|
58
|
+
_FLAT_INPUT_MODELS = ("Logistic Regression", "Decision Tree", "Random Forest", "SVM", "Neural Network")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _seed_everything(seed: int = RANDOM_SEED) -> None:
|
|
62
|
+
random.seed(seed)
|
|
63
|
+
np.random.seed(seed)
|
|
64
|
+
try:
|
|
65
|
+
import torch
|
|
66
|
+
|
|
67
|
+
torch.manual_seed(seed)
|
|
68
|
+
except Exception: # pragma: no cover - torch is a hard dependency in practice
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _json_safe(value: Any) -> Any:
|
|
73
|
+
"""Recursively convert NumPy scalars/arrays into JSON-serialisable values."""
|
|
74
|
+
if isinstance(value, dict):
|
|
75
|
+
return {str(key): _json_safe(item) for key, item in value.items()}
|
|
76
|
+
if isinstance(value, (list, tuple)):
|
|
77
|
+
return [_json_safe(item) for item in value]
|
|
78
|
+
if isinstance(value, np.ndarray):
|
|
79
|
+
return value.tolist()
|
|
80
|
+
if isinstance(value, (np.integer,)):
|
|
81
|
+
return int(value)
|
|
82
|
+
if isinstance(value, (np.floating,)):
|
|
83
|
+
number = float(value)
|
|
84
|
+
return None if np.isnan(number) else number
|
|
85
|
+
if isinstance(value, (np.bool_,)):
|
|
86
|
+
return bool(value)
|
|
87
|
+
if isinstance(value, float) and np.isnan(value):
|
|
88
|
+
return None
|
|
89
|
+
return value
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _build_models() -> Dict[str, object]:
|
|
93
|
+
"""All six models, in reporting order."""
|
|
94
|
+
models = build_classical_models()
|
|
95
|
+
models["Neural Network"] = build_mlp_model()
|
|
96
|
+
models["Simple CNN"] = build_cnn_model()
|
|
97
|
+
return {name: models[name] for name in MODEL_ORDER}
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _write_json(path: str, payload: Any) -> None:
|
|
101
|
+
with open(path, "w", encoding="utf-8") as handle:
|
|
102
|
+
json.dump(_json_safe(payload), handle, indent=2, sort_keys=False)
|
|
103
|
+
handle.write("\n")
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def benchmark_image_classification(
|
|
107
|
+
dataset,
|
|
108
|
+
dataset_type: str,
|
|
109
|
+
target_labels,
|
|
110
|
+
color_mode: str,
|
|
111
|
+
) -> dict:
|
|
112
|
+
"""Load an image dataset, train all required classifiers,
|
|
113
|
+
and return a complete benchmark comparison.
|
|
114
|
+
|
|
115
|
+
Parameters
|
|
116
|
+
----------
|
|
117
|
+
dataset:
|
|
118
|
+
Dataset root directory, CSV/JSON/JSONL manifest path, Pandas DataFrame,
|
|
119
|
+
or NumPy image array.
|
|
120
|
+
dataset_type:
|
|
121
|
+
One of ``"folder"``, ``"csv"``, ``"json"`` or ``"array"``.
|
|
122
|
+
target_labels:
|
|
123
|
+
Class-folder names, manifest label-field name, DataFrame label column,
|
|
124
|
+
or a label vector.
|
|
125
|
+
color_mode:
|
|
126
|
+
``"grayscale"`` for one channel or ``"rgb"`` for three.
|
|
127
|
+
|
|
128
|
+
Returns
|
|
129
|
+
-------
|
|
130
|
+
dict
|
|
131
|
+
Benchmark table, best model, dataset summary, split information,
|
|
132
|
+
per-model results, confusion matrices, class-level reports and
|
|
133
|
+
warnings. The same artefacts are written to ``benchmark_results/``.
|
|
134
|
+
"""
|
|
135
|
+
_seed_everything()
|
|
136
|
+
|
|
137
|
+
# -- 1. load and validate ------------------------------------------------
|
|
138
|
+
loaded = load_dataset(dataset, dataset_type, target_labels)
|
|
139
|
+
|
|
140
|
+
# -- 2. standardise ------------------------------------------------------
|
|
141
|
+
prepared = prepare_dataset(loaded, color_mode)
|
|
142
|
+
warnings = list(prepared.warnings)
|
|
143
|
+
class_names = prepared.class_names
|
|
144
|
+
n_classes = len(class_names)
|
|
145
|
+
|
|
146
|
+
# -- 3. one stratified split, reused by every model ----------------------
|
|
147
|
+
train_index, test_index = stratified_split(prepared.y)
|
|
148
|
+
y_train = prepared.y[train_index]
|
|
149
|
+
y_test = prepared.y[test_index]
|
|
150
|
+
|
|
151
|
+
features_train = prepared.features[train_index]
|
|
152
|
+
features_test = prepared.features[test_index]
|
|
153
|
+
images_train = prepared.images[train_index]
|
|
154
|
+
images_test = prepared.images[test_index]
|
|
155
|
+
|
|
156
|
+
# -- 4. train and evaluate every model on that one split -----------------
|
|
157
|
+
model_results: Dict[str, dict] = {}
|
|
158
|
+
confusion_matrices: Dict[str, np.ndarray] = {}
|
|
159
|
+
classification_reports: Dict[str, Any] = {}
|
|
160
|
+
|
|
161
|
+
for name, model in _build_models().items():
|
|
162
|
+
uses_flat = name in _FLAT_INPUT_MODELS
|
|
163
|
+
x_train = features_train if uses_flat else images_train
|
|
164
|
+
x_test = features_test if uses_flat else images_test
|
|
165
|
+
|
|
166
|
+
record: Dict[str, Any] = {
|
|
167
|
+
"status": "success",
|
|
168
|
+
"error": None,
|
|
169
|
+
"input_representation": "flattened features" if uses_flat else "image tensor",
|
|
170
|
+
"training_samples": int(len(train_index)),
|
|
171
|
+
"testing_samples": int(len(test_index)),
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
started = time.perf_counter()
|
|
176
|
+
model.fit(x_train, y_train)
|
|
177
|
+
record["training_time_seconds"] = float(time.perf_counter() - started)
|
|
178
|
+
|
|
179
|
+
started = time.perf_counter()
|
|
180
|
+
predictions = np.asarray(model.predict(x_test))
|
|
181
|
+
inference_seconds = float(time.perf_counter() - started)
|
|
182
|
+
|
|
183
|
+
if predictions.shape[0] != y_test.shape[0]:
|
|
184
|
+
raise RuntimeError(
|
|
185
|
+
"model returned %d predictions for %d test images"
|
|
186
|
+
% (predictions.shape[0], y_test.shape[0])
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
record["inference_time_seconds_total"] = inference_seconds
|
|
190
|
+
record["inference_time_ms_per_image"] = (
|
|
191
|
+
inference_seconds / max(len(y_test), 1) * 1000.0
|
|
192
|
+
)
|
|
193
|
+
record["metrics"] = compute_metrics(y_test, predictions, n_classes)
|
|
194
|
+
record["predictions"] = predictions
|
|
195
|
+
record["configuration"] = describe(model)
|
|
196
|
+
|
|
197
|
+
confusion_matrices[name] = confusion_matrix_array(y_test, predictions, n_classes)
|
|
198
|
+
classification_reports[name] = classification_report_frame(
|
|
199
|
+
y_test, predictions, class_names
|
|
200
|
+
)
|
|
201
|
+
record["prediction_examples"] = prediction_examples(
|
|
202
|
+
y_test, predictions, test_index, class_names
|
|
203
|
+
)
|
|
204
|
+
if name == "Simple CNN":
|
|
205
|
+
record["epochs_run"] = int(getattr(model, "epochs_run_", 0))
|
|
206
|
+
record["best_epoch"] = int(getattr(model, "best_epoch_", 0))
|
|
207
|
+
record["stopped_early"] = bool(getattr(model, "stopped_early_", False))
|
|
208
|
+
|
|
209
|
+
except Exception as exc: # noqa: BLE001 - a failed model is reported, not hidden
|
|
210
|
+
record["status"] = "failed"
|
|
211
|
+
record["error"] = "%s: %s" % (type(exc).__name__, exc)
|
|
212
|
+
record.setdefault("training_time_seconds", float("nan"))
|
|
213
|
+
record["inference_time_ms_per_image"] = float("nan")
|
|
214
|
+
record["metrics"] = {}
|
|
215
|
+
record["predictions"] = np.array([], dtype=np.int64)
|
|
216
|
+
record["configuration"] = describe(model)
|
|
217
|
+
warnings.append("Model %r failed and is reported without metrics: %s" % (name, record["error"]))
|
|
218
|
+
|
|
219
|
+
model_results[name] = record
|
|
220
|
+
|
|
221
|
+
# -- 5. rank -------------------------------------------------------------
|
|
222
|
+
summary = build_summary(model_results)
|
|
223
|
+
best_model = best_model_name(summary)
|
|
224
|
+
if not best_model:
|
|
225
|
+
warnings.append("Every model failed; there is no best model to report.")
|
|
226
|
+
|
|
227
|
+
# -- 6. distributions ----------------------------------------------------
|
|
228
|
+
split_information = {
|
|
229
|
+
"training_samples": int(len(train_index)),
|
|
230
|
+
"testing_samples": int(len(test_index)),
|
|
231
|
+
"test_size": TEST_SIZE,
|
|
232
|
+
"random_seed": RANDOM_SEED,
|
|
233
|
+
"stratified": True,
|
|
234
|
+
"shared_by_all_models": True,
|
|
235
|
+
"full_class_distribution": distribution(prepared.y, class_names),
|
|
236
|
+
"training_class_distribution": distribution(y_train, class_names),
|
|
237
|
+
"testing_class_distribution": distribution(y_test, class_names),
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
dataset_information = {
|
|
241
|
+
"dataset_type": loaded.dataset_type,
|
|
242
|
+
"number_of_images": int(prepared.images.shape[0]),
|
|
243
|
+
"number_of_classes": n_classes,
|
|
244
|
+
"class_names": list(class_names),
|
|
245
|
+
"color_mode": prepared.color_mode,
|
|
246
|
+
"image_shape": list(prepared.image_shape),
|
|
247
|
+
"flattened_features": int(prepared.features.shape[1]),
|
|
248
|
+
"skipped_samples": len(prepared.skipped),
|
|
249
|
+
"skipped_details": prepared.skipped,
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
# -- 7. write every required artefact ------------------------------------
|
|
253
|
+
output_directory = _write_outputs(
|
|
254
|
+
summary=summary,
|
|
255
|
+
model_results=model_results,
|
|
256
|
+
confusion_matrices=confusion_matrices,
|
|
257
|
+
classification_reports=classification_reports,
|
|
258
|
+
class_names=class_names,
|
|
259
|
+
dataset_information=dataset_information,
|
|
260
|
+
split_information=split_information,
|
|
261
|
+
best_model=best_model,
|
|
262
|
+
warnings=warnings,
|
|
263
|
+
images_test=images_test,
|
|
264
|
+
y_test=y_test,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
"package_information": {"name": PACKAGE_NAME, "version": __version__},
|
|
269
|
+
"summary": summary,
|
|
270
|
+
"best_model": best_model,
|
|
271
|
+
"dataset_information": dataset_information,
|
|
272
|
+
"split_information": split_information,
|
|
273
|
+
"model_results": model_results,
|
|
274
|
+
"confusion_matrices": confusion_matrices,
|
|
275
|
+
"classification_reports": classification_reports,
|
|
276
|
+
"warnings": warnings,
|
|
277
|
+
"output_directory": output_directory,
|
|
278
|
+
"class_names": list(class_names),
|
|
279
|
+
"test_indices": test_index,
|
|
280
|
+
"train_indices": train_index,
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def _write_outputs(
|
|
285
|
+
summary,
|
|
286
|
+
model_results,
|
|
287
|
+
confusion_matrices,
|
|
288
|
+
classification_reports,
|
|
289
|
+
class_names,
|
|
290
|
+
dataset_information,
|
|
291
|
+
split_information,
|
|
292
|
+
best_model,
|
|
293
|
+
warnings,
|
|
294
|
+
images_test,
|
|
295
|
+
y_test,
|
|
296
|
+
) -> str:
|
|
297
|
+
"""Create ``benchmark_results/`` and populate it with every required file."""
|
|
298
|
+
import sklearn
|
|
299
|
+
|
|
300
|
+
root = ensure_directory(os.path.abspath(OUTPUT_DIR))
|
|
301
|
+
matrices_dir = ensure_directory(os.path.join(root, CONFUSION_MATRIX_DIR))
|
|
302
|
+
reports_dir = ensure_directory(os.path.join(root, CLASSIFICATION_REPORT_DIR))
|
|
303
|
+
|
|
304
|
+
summary.to_csv(os.path.join(root, "benchmark_summary.csv"), index=False)
|
|
305
|
+
|
|
306
|
+
metrics_payload = {
|
|
307
|
+
"package_information": {"name": PACKAGE_NAME, "version": __version__},
|
|
308
|
+
"best_model": best_model,
|
|
309
|
+
"dataset_information": dataset_information,
|
|
310
|
+
"split_information": split_information,
|
|
311
|
+
"summary": summary.to_dict(orient="records"),
|
|
312
|
+
"model_results": {
|
|
313
|
+
name: {
|
|
314
|
+
key: value
|
|
315
|
+
for key, value in record.items()
|
|
316
|
+
if key not in ("predictions", "configuration")
|
|
317
|
+
}
|
|
318
|
+
for name, record in model_results.items()
|
|
319
|
+
},
|
|
320
|
+
"confusion_matrices": {
|
|
321
|
+
name: matrix.tolist() for name, matrix in confusion_matrices.items()
|
|
322
|
+
},
|
|
323
|
+
"class_names": list(class_names),
|
|
324
|
+
"warnings": warnings,
|
|
325
|
+
}
|
|
326
|
+
_write_json(os.path.join(root, "benchmark_metrics.json"), metrics_payload)
|
|
327
|
+
|
|
328
|
+
try:
|
|
329
|
+
torch_version = __import__("torch").__version__
|
|
330
|
+
except Exception: # pragma: no cover
|
|
331
|
+
torch_version = "unavailable"
|
|
332
|
+
|
|
333
|
+
run_configuration = {
|
|
334
|
+
"package_information": {"name": PACKAGE_NAME, "version": __version__},
|
|
335
|
+
"internal_constants": {
|
|
336
|
+
"image_size": [IMAGE_SIZE, IMAGE_SIZE],
|
|
337
|
+
"aspect_ratio_preserved": True,
|
|
338
|
+
"padding": "centred zero padding",
|
|
339
|
+
"pixel_range": [0, 1],
|
|
340
|
+
"random_seed": RANDOM_SEED,
|
|
341
|
+
"test_size": TEST_SIZE,
|
|
342
|
+
"stratified": True,
|
|
343
|
+
"output_directory": OUTPUT_DIR,
|
|
344
|
+
"cnn_max_epochs": CNN_MAX_EPOCHS,
|
|
345
|
+
"cnn_batch_size": CNN_BATCH_SIZE,
|
|
346
|
+
"cnn_early_stopping_patience": CNN_EARLY_STOPPING_PATIENCE,
|
|
347
|
+
"data_augmentation": False,
|
|
348
|
+
"pretrained_weights": False,
|
|
349
|
+
},
|
|
350
|
+
"dataset_information": dataset_information,
|
|
351
|
+
"split_information": split_information,
|
|
352
|
+
"model_configuration": {
|
|
353
|
+
name: record.get("configuration", {}) for name, record in model_results.items()
|
|
354
|
+
},
|
|
355
|
+
"model_status": {
|
|
356
|
+
name: {"status": record.get("status"), "error": record.get("error")}
|
|
357
|
+
for name, record in model_results.items()
|
|
358
|
+
},
|
|
359
|
+
"environment": {
|
|
360
|
+
"python": platform.python_version(),
|
|
361
|
+
"platform": platform.platform(),
|
|
362
|
+
"numpy": np.__version__,
|
|
363
|
+
"scikit_learn": sklearn.__version__,
|
|
364
|
+
"torch": torch_version,
|
|
365
|
+
},
|
|
366
|
+
}
|
|
367
|
+
_write_json(os.path.join(root, "run_configuration.json"), run_configuration)
|
|
368
|
+
|
|
369
|
+
plot_class_distribution(
|
|
370
|
+
{
|
|
371
|
+
"Full dataset": split_information["full_class_distribution"],
|
|
372
|
+
"Training": split_information["training_class_distribution"],
|
|
373
|
+
"Testing": split_information["testing_class_distribution"],
|
|
374
|
+
},
|
|
375
|
+
os.path.join(root, "class_distribution.png"),
|
|
376
|
+
)
|
|
377
|
+
plot_model_comparison(summary, os.path.join(root, "model_comparison.png"))
|
|
378
|
+
|
|
379
|
+
for name in MODEL_ORDER:
|
|
380
|
+
stem = MODEL_FILE_STEMS[name]
|
|
381
|
+
matrix = confusion_matrices.get(name)
|
|
382
|
+
if matrix is None:
|
|
383
|
+
matrix = np.zeros((len(class_names), len(class_names)), dtype=int)
|
|
384
|
+
plot_confusion_matrix(
|
|
385
|
+
matrix, class_names, name, os.path.join(matrices_dir, "%s.png" % stem)
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
report = classification_reports.get(name)
|
|
389
|
+
report_path = os.path.join(reports_dir, "%s.csv" % stem)
|
|
390
|
+
if report is None:
|
|
391
|
+
import pandas as pd
|
|
392
|
+
|
|
393
|
+
pd.DataFrame(
|
|
394
|
+
{
|
|
395
|
+
"class": list(class_names),
|
|
396
|
+
"precision": [float("nan")] * len(class_names),
|
|
397
|
+
"recall": [float("nan")] * len(class_names),
|
|
398
|
+
"f1-score": [float("nan")] * len(class_names),
|
|
399
|
+
"support": [float("nan")] * len(class_names),
|
|
400
|
+
"status": ["model failed"] * len(class_names),
|
|
401
|
+
}
|
|
402
|
+
).to_csv(report_path, index=False)
|
|
403
|
+
else:
|
|
404
|
+
report.to_csv(report_path, index=False)
|
|
405
|
+
|
|
406
|
+
if best_model and model_results.get(best_model, {}).get("status") == "success":
|
|
407
|
+
plot_prediction_examples(
|
|
408
|
+
images_test,
|
|
409
|
+
y_test,
|
|
410
|
+
model_results[best_model]["predictions"],
|
|
411
|
+
class_names,
|
|
412
|
+
best_model,
|
|
413
|
+
os.path.join(root, "prediction_examples.png"),
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
return root
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""The four classical Scikit-learn classifiers.
|
|
2
|
+
|
|
3
|
+
Logistic Regression and the SVM are wrapped in a ``Pipeline`` whose
|
|
4
|
+
``StandardScaler`` is fitted inside ``fit`` and therefore only ever sees
|
|
5
|
+
training data. Tree-based models are scale invariant and are used bare.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Dict
|
|
11
|
+
|
|
12
|
+
from .config import RANDOM_SEED
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def build_classical_models() -> Dict[str, object]:
|
|
16
|
+
"""Return {display name: unfitted estimator} for the classical models."""
|
|
17
|
+
from sklearn.ensemble import RandomForestClassifier
|
|
18
|
+
from sklearn.linear_model import LogisticRegression
|
|
19
|
+
from sklearn.pipeline import Pipeline
|
|
20
|
+
from sklearn.preprocessing import StandardScaler
|
|
21
|
+
from sklearn.svm import SVC
|
|
22
|
+
from sklearn.tree import DecisionTreeClassifier
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
"Logistic Regression": Pipeline(
|
|
26
|
+
[
|
|
27
|
+
("scaler", StandardScaler()),
|
|
28
|
+
("classifier", LogisticRegression(max_iter=1000, random_state=RANDOM_SEED)),
|
|
29
|
+
]
|
|
30
|
+
),
|
|
31
|
+
"Decision Tree": DecisionTreeClassifier(random_state=RANDOM_SEED),
|
|
32
|
+
"Random Forest": RandomForestClassifier(
|
|
33
|
+
n_estimators=200, random_state=RANDOM_SEED, n_jobs=-1
|
|
34
|
+
),
|
|
35
|
+
"SVM": Pipeline(
|
|
36
|
+
[
|
|
37
|
+
("scaler", StandardScaler()),
|
|
38
|
+
("classifier", SVC(kernel="rbf", random_state=RANDOM_SEED)),
|
|
39
|
+
]
|
|
40
|
+
),
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def describe(estimator) -> dict:
|
|
45
|
+
"""Flat, JSON-safe view of an estimator's effective parameters."""
|
|
46
|
+
try:
|
|
47
|
+
params = estimator.get_params(deep=True)
|
|
48
|
+
except AttributeError:
|
|
49
|
+
return {"repr": repr(estimator)}
|
|
50
|
+
|
|
51
|
+
described = {}
|
|
52
|
+
for key, value in sorted(params.items()):
|
|
53
|
+
if isinstance(value, (str, int, float, bool)) or value is None:
|
|
54
|
+
described[key] = value
|
|
55
|
+
else:
|
|
56
|
+
described[key] = repr(value)
|
|
57
|
+
described["estimator"] = type(estimator).__name__
|
|
58
|
+
return described
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Internal, non-configurable constants for the benchmark.
|
|
2
|
+
|
|
3
|
+
The assignment requires a four-input public API, so image size, random seed,
|
|
4
|
+
training epochs and output location are fixed here rather than exposed as
|
|
5
|
+
function parameters. Every value in this module is copied verbatim into
|
|
6
|
+
``benchmark_results/run_configuration.json`` on each run.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
PACKAGE_NAME = "Justin_Williams_CV_Benchmarking"
|
|
10
|
+
IMPORT_NAME = "justin_williams_cv_benchmarking"
|
|
11
|
+
|
|
12
|
+
# --- image standardisation -------------------------------------------------
|
|
13
|
+
IMAGE_SIZE = 64 # every image becomes IMAGE_SIZE x IMAGE_SIZE
|
|
14
|
+
PAD_COLOR = 0 # aspect ratio is preserved by zero padding
|
|
15
|
+
PIXEL_SCALE = 255.0 # normalise raw 8-bit pixels into [0, 1]
|
|
16
|
+
|
|
17
|
+
# --- reproducibility -------------------------------------------------------
|
|
18
|
+
RANDOM_SEED = 42
|
|
19
|
+
|
|
20
|
+
# --- stratified split ------------------------------------------------------
|
|
21
|
+
TEST_SIZE = 0.20 # 80% train / 20% test, stratified by label
|
|
22
|
+
CNN_VALIDATION_SIZE = 0.10 # carved out of the TRAINING portion only
|
|
23
|
+
|
|
24
|
+
# --- neural network training ----------------------------------------------
|
|
25
|
+
CNN_MAX_EPOCHS = 20
|
|
26
|
+
CNN_BATCH_SIZE = 32
|
|
27
|
+
CNN_EARLY_STOPPING_PATIENCE = 3
|
|
28
|
+
CNN_LEARNING_RATE = 1e-3
|
|
29
|
+
CNN_DROPOUT = 0.30
|
|
30
|
+
|
|
31
|
+
MLP_HIDDEN_LAYERS = (128, 64)
|
|
32
|
+
MLP_MAX_ITER = 100
|
|
33
|
+
|
|
34
|
+
# --- dataset validation ----------------------------------------------------
|
|
35
|
+
MIN_IMAGES_PER_CLASS = 5 # recommended minimum; below this we warn
|
|
36
|
+
MIN_CLASSES = 2 # below this we raise
|
|
37
|
+
SUPPORTED_EXTENSIONS = (".png", ".jpg", ".jpeg", ".bmp", ".tif", ".tiff")
|
|
38
|
+
IMAGE_PATH_COLUMN = "image_path"
|
|
39
|
+
|
|
40
|
+
# --- output ----------------------------------------------------------------
|
|
41
|
+
OUTPUT_DIR = "benchmark_results"
|
|
42
|
+
CONFUSION_MATRIX_DIR = "confusion_matrices"
|
|
43
|
+
CLASSIFICATION_REPORT_DIR = "classification_reports"
|
|
44
|
+
|
|
45
|
+
# --- model registry --------------------------------------------------------
|
|
46
|
+
# Display name -> output file stem. Order is the reporting order used before
|
|
47
|
+
# the summary table is sorted by Macro F1-score.
|
|
48
|
+
MODEL_FILE_STEMS = {
|
|
49
|
+
"Logistic Regression": "logistic_regression",
|
|
50
|
+
"Decision Tree": "decision_tree",
|
|
51
|
+
"Random Forest": "random_forest",
|
|
52
|
+
"SVM": "svm",
|
|
53
|
+
"Neural Network": "neural_network",
|
|
54
|
+
"Simple CNN": "simple_cnn",
|
|
55
|
+
}
|
|
56
|
+
MODEL_ORDER = tuple(MODEL_FILE_STEMS)
|