Download development_log.md from kabiriitb/rockglacierNET: direct link, hf CLI and curl.
- Browser
- Download file 14.8 kB
-
https://huggingface.co/kabiriitb/rockglacierNET/resolve/main/development_log.md
- Command line
-
hf download hf://kabiriitb/rockglacierNET/development_log.md
-
curl -L -o development_log.md https://huggingface.co/kabiriitb/rockglacierNET/resolve/main/development_log.md
Rock-Glacier Segmentation β Complete Development Log
A chronological record of the entire project: every experiment, every bug, every dead end, and
every fix β in the order it happened. For the clean topic-organised summary see ablation.md; this
document is the full story.
Final result: 3-seed Swin-UNet ensemble + 8-way TTA β test Dice 0.668, IoU 0.501 (5-fold CV 0.583 Β± 0.014). Data: 671 clean 10 m patches from the Kumar et al. (2026) InSAR inventory.
PHASE 0 β Starting Point & First Diagnostics
Context: A 30 m model existed (val Dice 0.7618, but this used a metric that was later found to be
inflated). The team migrated to a new 10 m dataset (patches_10m/, 28 bands) and performance
collapsed. The investigation started here.
0.1 Cohen's d separability script review (check_separability.py)
Found and fixed three bugs:
- Pooled-std formula wrong β used
(nβ1)Β·pop_varinstead ofnΒ·pop_var. - Class imbalance read from band-1's filtered count, not the raw mask β wrong if band-1 had NoData.
- Hard-coded conclusion ("607 pairs", "4.5:1") not computed from data. Made dynamic. (Also: Windows absolute paths made portable.)
0.2 Data health analysis of the old 10 m data (temp script, then deleted)
Key findings that explained the regression:
- NaN contamination: ~20 % of pixels NaN across almost all 28 bands (only bands 18 & 27 clean); 267 M NaN values total; 73.6 % of patches affected.
- Severe class imbalance: 19.8 : 1 background : glacier (glacier β 4.8 % of pixels).
- 52.4 % of patches were pure background (all-BG masks).
0.3 Fixed NaN normalisation bug in data_generator.py
Old code filled NaN with 0 before normalisation β those pixels became βmean/std (a biased,
non-zero signal). Fixed to fill NaN β 0 after normalisation (= band mean, a neutral value).
PHASE 1 β Diagnosing the Broken Training Pipeline (old data)
Analysed the 9 existing band-selection runs (run_12_native β¦ run_28_all). All poor (best val Dice
β 0.49). Found four training-pipeline bugs:
| # | Bug | Effect | Fix |
|---|---|---|---|
| 1 | Metric inflation β empty (all-bg) batches returned 1e-6/1e-6 = 1.0 |
val Dice inflated +0.35β0.45; wrong "best" epoch saved | tf.cond returns 0 for empty batches |
| 2 | Loss weights inverted β Ξ±_FP=0.6 > Ξ²_FN=0.4 at 19.8:1 imbalance | model learned "predict background when unsure" | flipped to Ξ±=0.3/Ξ²=0.7 (later 0.4/0.6) |
| 3 | NaN bias (see 0.3) | phantom signal on ~20 % of pixels | fill after normalisation |
| 4 | LR killed early β ReduceLROnPlateau(patience=5) | LR collapsed before convergence | later replaced by cosine annealing |
Other changes: BATCH_SIZE 8 β 32 (A6000s were idle), ModelCheckpoint monitor β val_loss.
Result β run_20_FIXED: val Dice 0.5304 (up from ~0.43). Confirmed the model was sound; the
remaining gap was a data-quality problem.
PHASE 2 β Attempts to Squeeze the Old Data (mostly dead ends)
These were tried on the noisy patches_10m data. Most failed and were reverted.
| Attempt | Result | Verdict |
|---|---|---|
require_glacier=True (drop 1,611 bg patches) |
imbalance 19.8:1 β 8.9:1 | kept |
min_glacier_pct=5% filter |
dropped noisy sliver patches | mixed |
NaN-pixel exclusion from loss (valid=0 on NaN) |
β0.036 Dice | β reverted β model over-predicted glacier on cloud pixels; the band-mean fill was useful regularisation |
Hard-negative mining (bg_keep_n=300) |
β0.11 Dice (0.55β0.42) | β reverted β Focal-Tversky gives loss=1.0 on any FP in an all-bg patch, suppressing glacier globally |
| 256Γ256 patches (re-extract) | underperformed | β confounded by other data issues |
Conclusion of Phase 2: the old data (20 % NaN, mixed seasons, snow) had a hard ceiling. No training trick could fix it. Decision: rebuild the data.
PHASE 3 β New Data Collection (the pivot that worked)
3.1 Diagnosis of the old GEE collection
The old script used a 3-year all-season median with only QA60 cloud masking β retained cloud shadow, haze, and seasonal snow, leaving ~20 % NaN at high altitude.
3.2 New pipeline design
- Temporal window: OctoberβNovember only (post-monsoon, pre-winter) β glacier exposed, no fresh snow, minimal cloud.
- Cloud masking: Sentinel-2 cloud-probability product < 35 % (catches shadow/haze, unlike QA60).
- Terrain: Copernicus GLO-30 DEM derivatives computed at native res then resampled.
- SAR: Sentinel-1 VV OctβNov composite.
- Project:
rock-glacier-net.
3.3 Collection bugs fixed (many)
| Bug | Fix |
|---|---|
gcloud not found on headless server |
notebook auth + copy ~/.config/earthengine/credentials from laptop |
TopologyException invalid geometry |
buffer(0) + drop invalid before union |
focalStd doesn't exist in EE Python |
reduceNeighborhood(stdDev) |
reduceToImage band named "first" not "glacier" |
.rename('glacier') |
| All-NaN patches (100 % missing) | NIR shadow mask nir>0.15 too aggressive at altitude β removed; widened window to SepβNov; cloud threshold 20β35 % |
| Slope/Aspect/Hillshade/TWI all NaN | ee.Terrain.* needs a projected CRS β reproject DEM to UTM 32645 first |
3.4 Key design change β per-glacier collection (user's idea)
Instead of one global mosaic, the pipeline (collect_patches_gee.py) is driven by the polygon
inventory: each glacier β 128Γ128 window at its centroid, with all polygons in the window
rasterised jointly (so adjacent glaciers are labelled correctly), then the 28-band feature patch is
clipped for that window.
3.5 Result β clean dataset patches_v2
- 671 patches (613 glacier, 58 background) β 469 train / 101 val / 101 test.
- Optical NaN: 0 % (verified across all patches β vs 20 % before).
- All 28 bands valid (fixed terrain). Elevation 4,500β5,900 m. Verified with multi-band plots.
The clean data collection was the single largest accuracy driver in the whole project.
3.6 Inventory provenance (confirmed later)
The 626-polygon inventory is from Kumar et al. (2026), iScience β SBAS-InSAR time-series detection (2022β2024) + STL filtering + Google Earth Pro optical refinement. Cited in the paper.
PHASE 4 β First Training on Clean Data
| Run | Setup | Val Dice | Note |
|---|---|---|---|
| sanity (20 ep) | val=train | 0.5588 | quick check β data learns cleanly |
run_v2_full |
require_glacier + min_glacier 5 % | 0.4994 | filters hurt on small clean data |
run_v2_full2 |
all 469 patches | 0.5514 | filtering removed β better |
Lesson: on the small clean dataset, aggressive patch filtering hurts; use all patches.
PHASE 5 β Band Selection (2 rounds, 13 experiments)
5.1 Cohen's d ranking (clean data)
Terrain dominates β top 4 are all terrain: Surface Roughness (0.496), Slope (0.489), TPI (0.424), TRI (0.361). Then NDVI, B12, TWI, SAR, Elevationβ¦ Bottom 4 (NDSI, IronOxide, Plan/Profile Curvature) are noise (d < 0.05).
5.2 All 13 experiments (Attention U-Net, val Dice)
| Config | Bands | Dice | Config | Bands | Dice | |
|---|---|---|---|---|---|---|
| Top-20 | 20 | 0.5700 | Top-8 | 8 | 0.5288 | |
| Top-22 | 22 | 0.5540 | Curated-18 | 18 | 0.5143 | |
| All-28 | 28 | 0.5514 | Terrain+SAR | 8 | 0.5086 | |
| Top-24 | 24 | 0.5497 | Terrain-only | 10 | 0.4738 | |
| Top-16 | 16 | 0.5448 | Optical-only | 12 | 0.4465 | |
| Top-12 | 12 | 0.5438 | Top-18 | 18 | 0.5355 | |
| Top-20 β SAR | 19 | 0.5341 |
Winner: Top-20 (0.5700). Adding the bottom-4 noise bands (24, 28) hurts.
5.3 Modality ablation
Terrain +0.124, Optical +0.062, SAR +0.036 β terrain β 2Γ optical's discriminative power.
PHASE 6 β Model & Training Improvements (cumulative)
| Step | Change | Val Dice* | Ξ |
|---|---|---|---|
| A | Attention U-Net, 469 patches, top-20 | 0.5700 | β |
| B | + train on train+val (570 patches) | 0.6330 | +0.063 |
| C | + Swin-UNet | 0.7171 | +0.084 |
| D | + cosine LR, 150 ep | 0.7648 | +0.048 |
| E | + cosine LR, 250 ep | 0.8174 | +0.053 |
* Steps BβE used val = train (memorisation scores) β used only for relative comparison; the honest numbers are the test-set results in Phase 7.
Findings: Swin-UNet gave the biggest single jump (+0.084) and near-zero overfit. Cosine annealing beat ReduceLROnPlateau by +0.048. 250 ep converged at epoch 206 (150 ep was still rising).
PHASE 7 β Honest Test Evaluation
Built evaluate_ensemble.py (multi-model, 8-way TTA, threshold sweep). Held-out test = 101 patches
never seen in training.
| Configuration | Test Dice | IoU | Prec | Rec |
|---|---|---|---|---|
| Single Swin-UNet | 0.6529 | 0.4847 | 0.6256 | 0.6827 |
| 3-seed ensemble (chosen) | 0.6679 | 0.5014 | 0.6087 | 0.7398 |
| 5-fold CV ensemble | 0.6629 | 0.4957 | 0.6278 | 0.7021 |
| 8-model mega ensemble | 0.6679 | 0.5014 | 0.6482 | 0.6889 |
| 5-model copy-paste ensemble | 0.6528 | 0.4845 | 0.6328 | 0.6741 |
5-fold CV: 0.5770 / 0.5639 / 0.5972 / 0.5824 / 0.5948 β 0.583 Β± 0.014 (very stable).
"Why is test (0.67) > CV (0.58)?" β investigated, not leakage
Decomposed on fold-0 (same single model, same method):
- training-time CV metric (per-batch, thr 0.5) β 0.577
- +0.013 pooled + optimal-threshold + TTA
- +0.022 test sample marginally easier (normal variation)
- +0.051 ensembling = 0.663. All legitimate β the numbers are measured differently.
Ensembling saturates: 3, 5, 8 models all land at 0.663β0.668. Chose 3-seed (best Dice/IoU, fewest models, highest recall 0.740 β best for inventory completeness).
PHASE 8 β Attempts to Break 0.70 (all failed / marginal)
Once clean data + Swin + cosine + ensemble was in place, tried to push higher. Every attempt A/B-tested:
| Technique | Result | Verdict |
|---|---|---|
| CutOut augmentation | 0.5214 (β0.05 vs 0.57) | β too aggressive for 570 patches |
| Copy-paste augmentation | fold-0 +0.022, but full 5-fold CV only +0.005 (variance β); test ensemble 0.6528 | β fold-0 gain was luck; hurt on test |
| LovΓ‘sz-Softmax loss | 0.5777 vs 0.5770 (+0.0008) | β neutral, not worth complexity |
| Higher weight decay (5e-4) | negligible | β |
| Mega 10-model ensemble | crashed (OOM from other user's job), rerun ~0.66 | no gain |
Root cause of the ceiling: the model is data-limited, not overfit (CV std 0.014, train-test gap mostly a measurement artefact). No training trick moved it. Only more labelled data would help β which the team chose not to collect.
PHASE 9 β Baselines, Figures & Paper
9.1 Random-Forest baseline (the "do you need DL?" check)
Per-pixel RF (300 trees) on the same 20 bands β test Dice 0.6005. Competitive with the CNN baselines (matches U-Net 0.610, Attn 0.604; beats SegFormer 0.555), but well below Swin (0.653). Message: deep learning helps here only with the right inductive bias.
9.2 Four-architecture benchmark (held-out test)
| Model | Dice | IoU | Prec | Rec |
|---|---|---|---|---|
| Random Forest | 0.6005 | 0.4291 | 0.5190 | 0.7125 |
| Plain U-Net | 0.6104 | 0.4392 | 0.6072 | 0.6135 |
| Attention U-Net | 0.6035 | 0.4322 | 0.5910 | 0.6166 |
| SegFormer (MiT-B0) | 0.5547 | 0.3838 | 0.5056 | 0.6145 |
| Swin-UNet | 0.6529 | 0.4847 | 0.6256 | 0.6827 |
SegFormer had the best validation but generalised worst on test β global attention over-adapts on a small dataset; Swin's windowed attention is the better inductive bias.
9.3 Figures & paper
make_paper_figures.py: georeferenced (lat/lon) 5-panel per-patch figures, top-5 detailed, best/worst montages (GT-yellow vs pred-cyan outlines), training curves.paper.tex: 3 TikZ diagrams (data pipeline, Swin-UNet architecture, ensemble inference) + 7 tables- 6 figures. Title finalised: "A Multi-Modal Deep-Learning Benchmark for Rock Glacier Mapping in the Sikkim Himalaya at 10 m Resolution." Inventory cited (Kumar et al., 2026). No named "novel model" β framed honestly as an application/benchmark study.
Full Journey β One Table
| # | Milestone | Score | Type |
|---|---|---|---|
| 1 | Old 10 m attempt (noisy data) | 0.5304 | val |
| 2 | Clean data + band selection (Attn) | 0.5700 | val |
| 3 | + train on train+val | 0.6330 | val* |
| 4 | + Swin-UNet | 0.7171 | val* |
| 5 | + cosine LR, 250 ep | 0.8174 | val* |
| 6 | Single Swin-UNet | 0.6529 | test |
| 7 | 3-seed ensemble + TTA (final) | 0.6679 | test |
| 8 | 5-fold cross-validation | 0.583 Β± 0.014 | CV |
* val = train (memorisation). Bold rows are the honest held-out test numbers reported in the paper.
Everything That FAILED (so it isn't retried)
- β NaN-pixel exclusion from loss (β0.04)
- β Hard-negative background mining (β0.11)
- β CutOut augmentation (β0.05)
- β Copy-paste augmentation (fold-luck; washed out; hurt on test)
- β LovΓ‘sz-Softmax loss (neutral)
- β 256Γ256 patches (underperformed)
- β Larger ensembles beyond 3 models (saturated at ~0.667)
- β Higher weight decay (negligible)
Everything That WORKED
- β Fixing the 4 pipeline bugs (metric inflation, loss weights, NaN, LR) β restored the model
- β Clean OctβNov cloud-masked data collection (0 % NaN) β biggest single gain
- β Per-glacier collection with joint polygon rasterisation (correct adjacent-glacier labels)
- β 20-band selection (terrain-dominated)
- β Swin-UNet architecture (+0.084)
- β Cosine annealing LR (+0.048)
- β Training on train+val for the final model
- β 3-seed ensemble + 8-way TTA (+0.015)
- β Random-Forest baseline (proves DL is justified)
The Single Most Important Lesson
It was a data problem, not a model problem. The model was sound from early on; the 10 m regression
was caused by 20 % NaN pixels, metric bugs, and inverted loss weights. Rebuilding the data collection
(clean post-monsoon cloud-masked composites) delivered the largest gain, and the final ceiling
(0.67 test) is set by dataset size (671 patches, CV std 0.014 = near-zero overfitting), not by the
architecture. More labelled data β not more training tricks β is the path beyond 0.67.
This log records the full experimental history from the chat session. Numbers are from actual runs
stored under bandselection/. See ablation.md for the topic-organised summary and paper.tex for
the manuscript.