Downloads · 30 days
4
8% of all-time downloads
shanexf/24679-places-water-cnn
24679-places-water-cnn is a image classification model from shanexf. Use it when you need a label for an image. It is set up for pytorch. The card lists the license as mit.
Classifies a photograph of a place as water (a river, lake, ocean, waterfall, pond, canal or fountain is visibly identifiable) or nowater. Built for the 24-679 neural-network AutoML homework to practise budgeted archi…
Downloads · 30 days
4
8% of all-time downloads
All-time downloads
49
Public
Repo size
7.1 MB
Likes
0
Public
Click a slice to open those files.
.pt6.2 MB · 85%
From the Hugging Face model README
Classifies a photograph of a place as water (a river, lake, ocean, waterfall, pond, canal or fountain is visibly
identifiable) or no_water. Built for the 24-679 neural-network AutoML homework to practise budgeted architecture /
hyperparameter search with early stopping, leakage-safe validation, and model documentation. It is trained on 23 original
photographs and is a classroom demonstration, not a production water detector.
ssg1/places-water-binary (commit 38d945896ea893a52d2473388461ff25a0126583), MIT license,
prepared by a classmate for Homework 1: 34 original iPhone photos from their camera roll, largest-square-cropped and
LANCZOS-resized to 224×224 RGB, labelled by the dataset author (17 water / 17 no-water originals).parent_id) appears in more than one split (asserted in the notebook).is_augmented.P(water) ≥ 0.5 → water.none
(none = normalise only; light = random horizontal flip + random resized crop scale 0.8–1.0;
strong = flip + random resized crop scale 0.5–1.0 + rotation ±10° + colour jitter 0.3/0.3/0.3/0.05), applied on top of
the dataset's offline augmentation. No augmentation is applied at evaluation time.The published validation split has only 5 photos, so model selection used stratified grouped 3-fold cross-validation over the 23 training originals: each fold trains on whole photo families (original + its augmented copies) and validates on the held-out original photos only, so no augmented copy of a validation photo is ever in that fold's training set. The search objective was the mean over folds of the best validation log-loss. The published validation and test photos were scored once, after the search, with the final model.
n_blocks 2–4, base_width
{16, 24, 32, 48} (doubling per block, capped at 256), kernel_size {3, 5}, convs_per_block 1–2, batch-norm on/off,
dense_units {0, 64, 128}; MobileNet fine-tuning {head_only, full}; dropout 0–0.5; optimizer {Adam, AdamW, SGD+Nesterov};
lr 1e-4–1e-2 (log); weight_decay 1e-6–1e-2 (log); LR schedule {constant, cosine}; augmentation {none, light, strong};
label smoothing {0, 0.1}; batch size {16, 32}; input resolution {64, 96, 128}.Best trial #17: MobileNetV3-Small (ImageNet-pretrained), new 2-class head, fine-tuning mode head_only, dropout 0.19; 1,519,906 parameters.
Mean CV validation log-loss 0.556, mean CV validation accuracy 0.690
(per fold: loss [0.42, 0.622, 0.626], accuracy [0.75, 0.571, 0.75]).
{
"family": "mobilenet_v3_small",
"resolution": 128,
"batch_size": 16,
"optimizer": "adam",
"lr": 0.000272713757722017,
"weight_decay": 3.899248336041237e-06,
"lr_schedule": "cosine",
"augmentation": "none",
"label_smoothing": 0.1,
"dropout": 0.1857493936554521,
"finetune_mode": "head_only"
}
MobileNetV3(
(features): Sequential(
(0): Conv2dNormActivation(
(0): Conv2d(3, 16, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(1): BatchNorm2d(16, eps=0.001, momentum=0.01, affine=True, track_running_stats=True)
(2): Hardswish()
)
(1): InvertedResidual(
(block): Sequential(
(0): Conv2dNormActivation(
(0): Conv2d(16, 16, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), groups=16, bias=False)
(1): BatchNorm2d(16, eps=0.001, momentum=0.01, affine=True, track_running_stats=True)
(2): ReLU(inplace=True)
)
(1): SqueezeExcitation(
(avgpool): AdaptiveAvgPool2d(output_size=1)
(fc1): Conv2d(16, 8, kernel_size=(1, 1), stride=(1, 1))
(fc2): Conv2d(8, 16, kernel_size=(1, 1), stride=(1, 1))
(activation): ReLU()
(scale_activation): Hardsigmoid()
)
(2): Conv2dNormActivation(
(0): Conv2d(16, 16, kernel_size=(1, 1), stride=(1, 1), bias=False)
(1): BatchNorm2d(16, eps=0.001, momentum=0.01, affine=True, track_running_stats=True)
)
)
)
(2): InvertedResidual(
(block): Sequential(
(0): Conv2dNormActivation(
(0): Conv2d(16, 72, kernel_size=(1, 1), stride=(1, 1), bias=False)
(1): BatchNorm2d(72, eps=0.001, momentum=0.01, affine=True, track_running_stats=True)
(2): ReLU(inplace=True)
)
(1): Conv2dNormActivation(
(0): Conv2d(72, 72, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), groups=72, bias=False)
(1): BatchNorm2d(72, eps=0.001, momentum=0.01, affine=True, track_running_stats=True)
(2): ReLU(inplace=True)
)
(2): Conv2dNormActivation(
(0): Conv2d(72, 24, kernel_size=(1, 1), stride=(1, 1), bias=False)
(1): BatchNorm2d(24, eps=0.001, momentum=0.01, affine=True, track_running_stats=True)
)
)
)
(3): InvertedResidual(
(block): Sequential(
(0): Conv2dNormActivation(
(0): Conv2d(24, 88, kernel_size=(1, 1), stride=(1, 1), bias=False)
(1): BatchNorm2d(88, eps=0.001, momentum=0.01, affine=True, track_running_stats=True)
(2): ReLU(inplace=True)
)
(1): Conv2dNormActivation(
(0): Conv2d(88, 88, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), groups=88, bias=False)
(1): BatchNorm2d(88, eps=0.001, momentum=0.01, affine=True, track_running_stats=True)
(2): ReLU(inplace=True)
)
(2): Conv2dNormActivation(
(0): Conv2d(88, 24, kernel_size=(1, 1), stride=(1, 1), bias=False)
(1): BatchNorm2d(24, eps=0.001, momentum=0.01, affine=True, track_running_stats=True)
)
)
)
(4): InvertedResidual(
(block): Sequential(
(0): Conv2dNormActivation(
(0): Conv2d(24, 96, kernel_size=(1, 1), stride=(1, 1), bias=False)
(1): BatchNorm2d(96, eps=0.001, momentum=0.01, affine=True, track_running_stats=True)
(2): Hardswish()
)
(1): Conv2dNormActivation(
(0): Conv2d(96, 96, kernel_size=(5, 5), stride=(2, 2), padding=(2, 2), groups=96, bias=False)
(1): BatchNorm2d(96, eps=0.001, momentum=0.01, affine=True, track_running_stats=True)
(2): Hardswish()
)
(2): SqueezeExcitation(
(avgpool): AdaptiveAvgPool2d(output_size=1)
(fc1): Conv2d(96, 24, kernel_size=(1, 1), stride=(1, 1))
(fc2): Conv2d(24, 96, kernel_size=(1, 1), stride=(1, 1))
(activation): ReLU()
(scale_activation): Hardsigmoid()
)
(3): Conv2dNormActivation(
(0): Conv2d(96, 40, kernel_size=(1, 1), stride=(1, 1), bias=False)
(1): BatchNorm2d(40, eps=0.001, momentum=0.01, affine=True, track_running_stats=True)
)
)
)
(5): InvertedResidual(
(block): Sequential(
(0): Conv2dNormActivation(
(0): Conv2d(40, 240, kernel_size=(1, 1), stride=(1, 1), bias=False)
(1): BatchNorm2d(240, eps=0.001, momentum=0.01, affine=True, track_running_stats=True)
(2): Hardswish()
)
(1): Conv2dNormActivation(
(0): Conv2d(240, 240, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2), groups=240, bias=False)
(1): BatchNorm2d(240, eps=0.001, momentum=0.01, affine=True, track_running_stats=True)
(2): Hardswish()
)
(2): SqueezeExcitation(
(avgpool): AdaptiveAvgPool2d(output_size=1)
(fc1): Conv2d(240, 64, kernel_size=(1, 1), stride=(1, 1))
(fc2): Conv2d(64, 240, kernel_size=(1, 1), stride=(1, 1))
(activation): ReLU()
(scale_activation): Hardsigmoid()
)
(3): Conv2dNormActivation(
(0): Conv2d(240, 40, kernel_size=(1, 1), stride=(1, 1), bias=False)
(1): BatchNorm2d(40, eps=0.001, momentum=0.01, affine=True, track_running_stats=True)
)
)
)
(6): InvertedResidual(
(block): Sequential(
(0): Conv2dNormActivation(
(0): Conv2d(40, 240, kernel_size=(1, 1), stride=(1, 1), bias=False)
(1): BatchNorm2d(240, eps=0.001, momentum=0.01, affine=True, track_running_stats=True)
(2): Hardswish()
)
(1): Conv2dNormActivation(
(0): Conv2d(240, 240, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2), groups=240, bias=False)
(1): BatchNorm2d(240, eps=0.001, momentum=0.01, affine=True, track_running_stats=True)
(2): Hardswish()
)
(2): SqueezeExcitation(
(avgpool): AdaptiveAvgPool2d(output_size=1)
(fc1): Conv2d(240, 64, kernel_size=(1, 1), stride=(1, 1))
(fc2): Conv2d(64, 240, kernel_size=(1, 1), stride=(1, 1))
(activation): ReLU()
(scale_activation): Hardsigmoid()
)
(3): Conv2dNormActivation(
(0): Conv2d(240, 40, kernel_size=(1, 1), stride=(1, 1), bias=False)
(1): BatchNorm2d(40, eps=0.001, momentum=0.01, affine=True, track_running_stats=True)
)
)
)
(7): InvertedResidual(
(block): Sequential(
(0): Conv2dNormActivation(
(0): Conv2d(40, 120, kernel_size=(1, 1), stride=(1, 1), bias=False)
(1): BatchNorm2d(120, eps=0.001, momentum=0.01, affine=True, track_running_stats=True)
(2): Hardswish()
)
(1): Conv2dNormActivation(
(0): Conv2d(120, 120, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2), groups=120, bias=False)
(1): BatchNorm2d(120, eps=0.001, momentum=0.01, affine=True, track_running_stats=True)
(2): Hardswish()
)
(2): SqueezeExcitation(
(avgpool): AdaptiveAvgPool2d(output_size=1)
(fc1): Conv2d(120, 32, kernel_size=(1, 1), stride=(1, 1))
(fc2): Conv2d(32, 120, kernel_size=(1, 1), stride=(1, 1))
(activation): ReLU()
(scale_activation): Hardsigmoid()
)
(3): Conv2dNormActivation(
(0): Conv2d(120, 48, kernel_size=(1, 1), stride=(1, 1), bias=False)
(1): BatchNorm2d(48, eps=0.001, momentum=0.01, affine=True, track_running_stats=True)
)
)
)
(8): InvertedResidual(
(block): Sequential(
(0): Conv2dNormActivation(
(0): Conv2d(48, 144, kernel_size=(1, 1), stride=(1, 1), bias=False)
(1): BatchNorm2d(144, eps=0.001, momentum=0.01, affine=True, track_running_stats=True)
(2): Hardswish()
)
(1): Conv2dNormActivation(
(0): Conv2d(144, 144, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2), groups=144, bias=False)
(1): BatchNorm2d(144, eps=0.001, momentum=0.01, affine=True, track_running_stats=True)
(2): Hardswish()
)
(2): SqueezeExcitation(
(avgpool): AdaptiveAvgPool2d(output_size=1)
(fc1): Conv2d(144, 40, kernel_size=(1, 1), stride=(1, 1))
(fc2): Conv2d(40, 144, kernel_size=(1, 1), stride=(1, 1))
(activation): ReLU()
(scale_activation): Hardsigmoid()
)
(3): Conv2dNormActivation(
(0): Conv2d(144, 48, kernel_size=(1, 1), stride=(1, 1), bias=False)
(1): BatchNorm2d(48, eps=0.001, momentum=0.01, affine=True, track_running_stats=True)
)
)
)
(9): InvertedResidual(
(block): Sequential(
(0): Conv2dNormActivation(
(0): Conv2d(48, 288, kernel_size=(1, 1), stride=(1, 1), bias=False)
(1): BatchNorm2d(288, eps=0.001, momentum=0.01, affine=True, track_running_stats=True)
(2): Hardswish()
)
(1): Conv2dNormActivation(
(0): Conv2d(288, 288, kernel_size=(5, 5), stride=(2, 2), padding=(2, 2), groups=288, bias=False)
(1): BatchNorm2d(288, eps=0.001, momentum=0.01, affine=True, track_running_stats=True)
(2): Hardswish()
)
(2): SqueezeExcitation(
(avgpool): AdaptiveAvgPool2d(output_size=1)
(fc1): Conv2d(288, 72, kernel_size=(1, 1), stride=(1, 1))
(fc2): Conv2d(72, 288, kernel_size=(1, 1), stride=(1, 1))
(activation): ReLU()
(scale_activation): Hardsigmoid()
)
(3): Conv2dNormActivation(
(0): Conv2d(288, 96, kernel_size=(1, 1), stride=(1, 1), bias=False)
(1): BatchNorm2d(96, eps=0.001, momentum=0.01, affine=True, track_running_stats=True)
)
)
)
(10): InvertedResidual(
(block): Sequential(
(0): Conv2dNormActivation(
(0): Conv2d(96, 576, kernel_size=(1, 1), stride=(1, 1), bias=False)
(1): BatchNorm2d(576, eps=0.001, momentum=0.01, affine=True, track_running_stats=True)
(2): Hardswish()
)
(1): Conv2dNormActivation(
(0): Conv2d(576, 576, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2), groups=576, bias=False)
(1): BatchNorm2d(576, eps=0.001, momentum=0.01, affine=True, track_running_stats=True)
(2): Hardswish()
)
(2): SqueezeExcitation(
(avgpool): AdaptiveAvgPool2d(output_size=1)
(fc1): Conv2d(576, 144, kernel_size=(1, 1), stride=(1, 1))
(fc2): Conv2d(144, 576, kernel_size=(1, 1), stride=(1, 1))
(activation): ReLU()
(scale_activation): Hardsigmoid()
)
(3): Conv2dNormActivation(
(0): Conv2d(576, 96, kernel_size=(1, 1), stride=(1, 1), bias=False)
(1): BatchNorm2d(96, eps=0.001, momentum=0.01, affine=True, track_running_stats=True)
)
)
)
(11): InvertedResidual(
(block): Sequential(
(0): Conv2dNormActivation(
(0): Conv2d(96, 576, kernel_size=(1, 1), stride=(1, 1), bias=False)
(1): BatchNorm2d(576, eps=0.001, momentum=0.01, affine=True, track_running_stats=True)
(2): Hardswish()
)
(1): Conv2dNormActivation(
(0): Conv2d(576, 576, kernel_size=(5, 5), stride=(1, 1), padding=(2, 2), groups=576, bias=False)
(1): BatchNorm2d(576, eps=0.001, momentum=0.01, affine=True, track_running_stats=True)
(2): Hardswish()
)
(2): SqueezeExcitation(
(avgpool): AdaptiveAvgPool2d(output_size=1)
(fc1): Conv2d(576, 144, kernel_size=(1, 1), stride=(1, 1))
(fc2): Conv2d(144, 576, kernel_size=(1, 1), stride=(1, 1))
(activation): ReLU()
(scale_activation): Hardsigmoid()
)
(3): Conv2dNormActivation(
(0): Conv2d(576, 96, kernel_size=(1, 1), stride=(1, 1), bias=False)
(1): BatchNorm2d(96, eps=0.001, momentum=0.01, affine=True, track_running_stats=True)
)
)
)
(12): Conv2dNormActivation(
(0): Conv2d(96, 576, kernel_size=(1, 1), stride=(1, 1), bias=False)
(1): BatchNorm2d(576, eps=0.001, momentum=0.01, affine=True, track_running_stats=True)
(2): Hardswish()
)
)
(avgpool): AdaptiveAvgPool2d(output_size=1)
(classifier): Sequential(
(0): Linear(in_features=576, out_features=1024, bias=True)
(1): Hardswish()
(2): Dropout(p=0.1857493936554521, inplace=False)
(3): Linear(in_features=1024, out_features=2, bias=True)
)
)

Holdout scores (final CNN vs. baselines; water = positive class)
| accuracy | balanced accuracy | precision (water) | recall (water) | F1 (water) | log-loss | |
|---|---|---|---|---|---|---|
| ('validation', 'final CNN') | 0.8 | 0.833 | 1 | 0.667 | 0.8 | 0.471 |
| ('validation', 'colour-logistic baseline') | 0.4 | 0.5 | 0 | 0 | 0 | 0.717 |
| ('validation', 'majority-class baseline') | 0.4 | 0.5 | 0 | 0 | 0 | 9.671 |
| ('test', 'final CNN') | 0.833 | 0.833 | 1 | 0.667 | 0.8 | 0.474 |
| ('test', 'colour-logistic baseline') | 1 | 1 | 1 | 1 | 1 | 0.625 |
| ('test', 'majority-class baseline') | 0.5 | 0.5 | 0 | 0 | 0 | 8.059 |
| seed | validation accuracy | validation log-loss | test accuracy | test log-loss |
|---|---|---|---|---|
| 0 | 0.8 | 0.514 | 0.833 | 0.318 |
| 1 | 0.6 | 0.551 | 1 | 0.33 |
| 2 | 0.8 | 0.456 | 0.833 | 0.371 |

Per-photo test predictions
| source_id | actual | predicted | P(water) | correct |
|---|---|---|---|---|
| place_water_013 | water | water | 0.748 | True |
| place_water_014 | water | water | 0.503 | True |
| place_water_008 | water | no_water | 0.295 | False |
| place_no_water_015 | no_water | no_water | 0.041 | True |
| place_no_water_014 | no_water | no_water | 0.382 | True |
| place_no_water_000 | no_water | no_water | 0.117 | True |
Per-photo validation predictions
| source_id | actual | predicted | P(water) | correct |
|---|---|---|---|---|
| place_no_water_001 | no_water | no_water | 0.129 | True |
| place_no_water_002 | no_water | no_water | 0.191 | True |
| place_water_015 | water | water | 0.575 | True |
| place_water_009 | water | no_water | 0.392 | False |
| place_water_000 | water | water | 0.596 | True |
Misclassified holdout photos in this run:
validation photo place_water_009: actual water, predicted no_water (P(water) = 0.39)test photo place_water_008: actual water, predicted no_water (P(water) = 0.29)Expected weaknesses given the training data (hypotheses, not measured on held-out data of each kind):
Linux-6.6.122+-x86_64-with-glibc2.39, Python 3.13.15, 2 CPU cores, 13.6 GB RAM,
device cuda (Tesla T4). Search budget 1500 s / 40 trials (used 310.1 s);
final fit 1.3 s; three seed-check refits. PyTorch 2.11.0+cu128, torchvision 0.26.0+cu128,
Optuna 5.0.0. Run started 2026-09-21T02:24:17+00:00; seeds: 24679 for the search and final fit.
import json, torch, huggingface_hub, importlib.util
from PIL import Image
from torchvision.transforms import v2
repo = "shanexf/24679-places-water-cnn"
code_path = huggingface_hub.hf_hub_download(repo, "water_cnn.py")
spec = importlib.util.spec_from_file_location("water_cnn", code_path); water_cnn = importlib.util.module_from_spec(spec); spec.loader.exec_module(water_cnn)
config = json.load(open(huggingface_hub.hf_hub_download(repo, "config.json")))
model = water_cnn.build_model(config["model_config"], pretrained=False)
model.load_state_dict(torch.load(huggingface_hub.hf_hub_download(repo, "model.pt"), map_location="cpu")); model.eval()
res = config["input"]["resolution"]
prep = v2.Compose([v2.PILToTensor(), v2.Resize((res, res), antialias=True), v2.ToDtype(torch.float32, scale=True),
v2.Normalize(config["input"]["mean"], config["input"]["std"])])
img = Image.open("photo.jpg").convert("RGB")
side = min(img.size); img = img.crop(((img.width - side) // 2, (img.height - side) // 2, (img.width + side) // 2, (img.height + side) // 2))
p_water = torch.softmax(model(prep(img)[None]), dim=1)[0, 1].item()
print("water" if p_water >= 0.5 else "no_water", round(p_water, 3))
MIT, matching the source dataset. Intended for coursework.
Claude (Anthropic) was used to draft this notebook (search space, training loop, evaluation and model-card template) following the 24-679 lecture material. The author ran the notebook end to end, checked the outputs, and edited the text. The photographs were taken and labelled by a classmate; none were generated by AI.
shanexf — 24-679, Carnegie Mellon University, Fall 2026.