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.*