File size: 14,763 Bytes
820fab0 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 | # 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:
1. **Pooled-std formula wrong** β used `(nβ1)Β·pop_var` instead of `nΒ·pop_var`.
2. **Class imbalance read from band-1's filtered count**, not the raw mask β wrong if band-1 had NoData.
3. **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.*
|