yx21e commited on
Commit
9ac0772
·
verified ·
1 Parent(s): 4e0c49c

Add serving-oriented tiled inference and jittered training support

Browse files
Files changed (1) hide show
  1. training/train_cold_tiled_mainline.py +160 -9
training/train_cold_tiled_mainline.py CHANGED
@@ -28,6 +28,11 @@ def read_rows(path: Path) -> List[Dict[str, str]]:
28
  return list(csv.DictReader(fh))
29
 
30
 
 
 
 
 
 
31
  def integral_image(mask: np.ndarray) -> np.ndarray:
32
  return np.pad(mask.astype(np.int32), ((1, 0), (1, 0)), mode="constant").cumsum(0).cumsum(1)
33
 
@@ -44,16 +49,47 @@ def centered_tile_origin(y: int, x: int, tile_h: int, tile_w: int, h: int, w: in
44
  return int(top), int(left)
45
 
46
 
47
- def positive_tile_origins(mask: np.ndarray, tile_h: int, tile_w: int, max_tiles: int, rng: random.Random) -> List[Tuple[int, int]]:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  ys, xs = np.where(mask > 0.5)
49
  if ys.size == 0:
50
  return []
51
- origins = list(
52
- {
53
- centered_tile_origin(int(y), int(x), tile_h, tile_w, mask.shape[0], mask.shape[1])
54
- for y, x in zip(ys, xs)
55
- }
56
- )
 
 
 
 
 
 
 
 
 
 
 
 
57
  rng.shuffle(origins)
58
  if max_tiles > 0:
59
  origins = origins[:max_tiles]
@@ -191,9 +227,103 @@ class UNetSmallFlex(nn.Module):
191
  return logits
192
 
193
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
  class ColdFeatureStore:
195
- def __init__(self, rows: List[Dict[str, str]]):
196
  self.cache: Dict[str, Dict[str, np.ndarray]] = {}
 
197
  static_path = Path(rows[0]["static_path"])
198
  static_npz = np.load(static_path, allow_pickle=True)
199
  static = self._sanitize(static_npz["static"].astype(np.float32))
@@ -211,6 +341,7 @@ class ColdFeatureStore:
211
  if "firewx_valid" in sample:
212
  extra.append(sample["firewx_valid"].astype(np.float32))
213
  x = np.concatenate([weather, firewx, *extra, static_x], axis=0).astype(np.float32)
 
214
  y = np.nan_to_num(sample["y_occ"].astype(np.float32), nan=0.0, posinf=0.0, neginf=0.0)
215
  self.cache[str(row["sample_id"])] = {"x": x, "y": y}
216
 
@@ -220,6 +351,18 @@ class ColdFeatureStore:
220
  x = np.nan_to_num(x, nan=0.0, posinf=0.0, neginf=0.0)
221
  return x.astype(np.float32, copy=False)
222
 
 
 
 
 
 
 
 
 
 
 
 
 
223
  def get(self, sample_id: str) -> Dict[str, np.ndarray]:
224
  return self.cache[sample_id]
225
 
@@ -511,6 +654,7 @@ def build_train_tiles(
511
  max_positive_tiles_per_sample: int,
512
  min_negative_tiles_per_sample: int,
513
  neg_pos_ratio: float,
 
514
  rng: random.Random,
515
  ) -> List[Dict[str, object]]:
516
  tile_rows: List[Dict[str, object]] = []
@@ -523,6 +667,7 @@ def build_train_tiles(
523
  tile_w=tile_size,
524
  max_tiles=max_positive_tiles_per_sample,
525
  rng=rng,
 
526
  )
527
  neg_count = max(min_negative_tiles_per_sample, int(math.ceil(len(pos_origins) * neg_pos_ratio)))
528
  neg_origins = negative_tile_origins(
@@ -678,7 +823,8 @@ def main() -> None:
678
  train_rows = read_rows(index_root / "splits" / "train.csv")
679
  val_rows = read_rows(index_root / "splits" / "val.csv")
680
  test_rows = read_rows(index_root / "splits" / "test.csv")
681
- store = ColdFeatureStore(train_rows + val_rows + test_rows)
 
682
 
683
  tile_rows = build_train_tiles(
684
  train_rows=train_rows,
@@ -687,6 +833,7 @@ def main() -> None:
687
  max_positive_tiles_per_sample=int(config.get("max_positive_tiles_per_sample", 64)),
688
  min_negative_tiles_per_sample=int(config.get("min_negative_tiles_per_sample", 4)),
689
  neg_pos_ratio=float(config.get("negative_to_positive_ratio", 2.0)),
 
690
  rng=rng,
691
  )
692
  train_ds = TrainTileDataset(tile_rows=tile_rows, store=store, augment_flip=bool(config.get("augment_flip", True)), seed=int(config.get("seed", 7)))
@@ -747,6 +894,7 @@ def main() -> None:
747
  "positive_train_tiles": int(sum(1 for row in tile_rows if row["tile_type"] == "positive")),
748
  "negative_train_tiles": int(sum(1 for row in tile_rows if row["tile_type"] == "negative")),
749
  "input_channels": in_ch,
 
750
  "train_positive_rate": train_positive_rate,
751
  "metric_reference_positive_rate": metric_reference_positive_rate,
752
  "pos_weight_source": pos_weight_source,
@@ -755,6 +903,7 @@ def main() -> None:
755
  "loss_type": str(config.get("loss_type", "bce")),
756
  "train_target_mode": str(config.get("train_target_mode", "hard")),
757
  "train_target_radius": int(config.get("train_target_radius", 0)),
 
758
  }
759
  (metric_dir / "tile_summary.json").write_text(json.dumps(summary_seed, indent=2), encoding="utf-8")
760
 
@@ -874,6 +1023,7 @@ def main() -> None:
874
  "num_train_tiles": len(tile_rows),
875
  "positive_train_tiles": int(sum(1 for row in tile_rows if row["tile_type"] == "positive")),
876
  "negative_train_tiles": int(sum(1 for row in tile_rows if row["tile_type"] == "negative")),
 
877
  "train_positive_rate": train_positive_rate,
878
  "metric_reference_positive_rate": metric_reference_positive_rate,
879
  "pos_weight": pos_weight,
@@ -882,6 +1032,7 @@ def main() -> None:
882
  "aux_spatial_loss_weight": aux_spatial_loss_weight,
883
  "aux_positive_rate": aux_positive_rate,
884
  "init_checkpoint": str(init_checkpoint) if init_checkpoint else "",
 
885
  "best_val_pr_auc": float(val_best["pr_auc"]),
886
  "best_val_auroc": float(val_best["auroc"]),
887
  "best_test_pr_auc": float(test_best["pr_auc"]),
 
28
  return list(csv.DictReader(fh))
29
 
30
 
31
+ def write_json(path: Path, data: Dict[str, object]) -> None:
32
+ path.parent.mkdir(parents=True, exist_ok=True)
33
+ path.write_text(json.dumps(data, indent=2), encoding="utf-8")
34
+
35
+
36
  def integral_image(mask: np.ndarray) -> np.ndarray:
37
  return np.pad(mask.astype(np.int32), ((1, 0), (1, 0)), mode="constant").cumsum(0).cumsum(1)
38
 
 
49
  return int(top), int(left)
50
 
51
 
52
+ def containing_tile_origin(y: int, x: int, tile_h: int, tile_w: int, h: int, w: int, rng: random.Random) -> Tuple[int, int]:
53
+ max_top = max(h - tile_h, 0)
54
+ max_left = max(w - tile_w, 0)
55
+ top_min = max(0, y - tile_h + 1)
56
+ top_max = min(y, max_top)
57
+ left_min = max(0, x - tile_w + 1)
58
+ left_max = min(x, max_left)
59
+ if top_min > top_max or left_min > left_max:
60
+ return centered_tile_origin(y, x, tile_h, tile_w, h, w)
61
+ return int(rng.randint(top_min, top_max)), int(rng.randint(left_min, left_max))
62
+
63
+
64
+ def positive_tile_origins(
65
+ mask: np.ndarray,
66
+ tile_h: int,
67
+ tile_w: int,
68
+ max_tiles: int,
69
+ rng: random.Random,
70
+ placement: str = "center",
71
+ ) -> List[Tuple[int, int]]:
72
  ys, xs = np.where(mask > 0.5)
73
  if ys.size == 0:
74
  return []
75
+ origin_fn = containing_tile_origin if placement == "random_containing" else centered_tile_origin
76
+ origins = []
77
+ seen = set()
78
+ indices = list(range(int(ys.size)))
79
+ rng.shuffle(indices)
80
+ for i in indices:
81
+ y = int(ys[i])
82
+ x = int(xs[i])
83
+ if placement == "random_containing":
84
+ origin = origin_fn(y, x, tile_h, tile_w, mask.shape[0], mask.shape[1], rng)
85
+ else:
86
+ origin = origin_fn(y, x, tile_h, tile_w, mask.shape[0], mask.shape[1])
87
+ if origin in seen:
88
+ continue
89
+ seen.add(origin)
90
+ origins.append(origin)
91
+ if max_tiles > 0 and len(origins) >= max_tiles:
92
+ break
93
  rng.shuffle(origins)
94
  if max_tiles > 0:
95
  origins = origins[:max_tiles]
 
227
  return logits
228
 
229
 
230
+ def normalization_config(config: Dict[str, object]) -> Dict[str, object]:
231
+ raw = config.get("input_normalization", {})
232
+ if raw is True:
233
+ return {"enabled": True}
234
+ if isinstance(raw, dict):
235
+ return raw
236
+ return {"enabled": False}
237
+
238
+
239
+ def compute_input_normalization_stats(
240
+ rows: List[Dict[str, str]],
241
+ continuous_channel_indices: List[int],
242
+ eps: float,
243
+ ) -> Dict[str, object]:
244
+ count: Dict[int, int] = {int(idx): 0 for idx in continuous_channel_indices}
245
+ sum_x: Dict[int, float] = {int(idx): 0.0 for idx in continuous_channel_indices}
246
+ sum_x2: Dict[int, float] = {int(idx): 0.0 for idx in continuous_channel_indices}
247
+ channel_names: List[str] | None = None
248
+ static_names: List[str] | None = None
249
+ for row in rows:
250
+ sample = np.load(row["sample_path"], allow_pickle=True)
251
+ weather = ColdFeatureStore._sanitize(sample["weather"].astype(np.float32))
252
+ firewx = ColdFeatureStore._sanitize(sample["firewx"].astype(np.float32))
253
+ extra = []
254
+ if "firewx_valid" in sample:
255
+ extra.append(sample["firewx_valid"].astype(np.float32))
256
+ static_npz = np.load(row["static_path"], allow_pickle=True)
257
+ static = ColdFeatureStore._sanitize(static_npz["static"].astype(np.float32))
258
+ static_parts = []
259
+ if "static_valid" in static_npz:
260
+ static_parts.append(static_npz["static_valid"].astype(np.float32))
261
+ static_parts.append(static)
262
+ if channel_names is None:
263
+ weather_names = [str(v) for v in sample.get("weather_names", np.array([], dtype=object)).tolist()]
264
+ firewx_names = [str(v) for v in sample.get("firewx_names", np.array([], dtype=object)).tolist()]
265
+ extra_names = ["firewx_valid"] if "firewx_valid" in sample else []
266
+ static_names = [str(v) for v in static_npz.get("static_names", np.array([], dtype=object)).tolist()]
267
+ static_valid_names = ["static_valid"] if "static_valid" in static_npz else []
268
+ channel_names = weather_names + firewx_names + extra_names + static_valid_names + static_names
269
+ x = np.concatenate([weather, firewx, *extra, *static_parts], axis=0).astype(np.float32)
270
+ for idx in continuous_channel_indices:
271
+ arr = x[int(idx)].astype(np.float64, copy=False).ravel()
272
+ count[int(idx)] += int(arr.size)
273
+ sum_x[int(idx)] += float(arr.sum())
274
+ sum_x2[int(idx)] += float(np.square(arr).sum())
275
+ channels = []
276
+ for idx in continuous_channel_indices:
277
+ n = max(int(count[int(idx)]), 1)
278
+ mean = float(sum_x[int(idx)] / n)
279
+ variance = max(float(sum_x2[int(idx)] / n - mean * mean), 0.0)
280
+ std = float(max(math.sqrt(variance), eps))
281
+ channels.append(
282
+ {
283
+ "index": int(idx),
284
+ "name": channel_names[int(idx)] if channel_names and int(idx) < len(channel_names) else str(idx),
285
+ "mean": mean,
286
+ "std": std,
287
+ "count": int(count[int(idx)]),
288
+ }
289
+ )
290
+ return {
291
+ "enabled": True,
292
+ "method": "per-channel z-score",
293
+ "stats_source": "train split full maps",
294
+ "continuous_channel_indices": [int(idx) for idx in continuous_channel_indices],
295
+ "eps": float(eps),
296
+ "channels": channels,
297
+ "channel_names": channel_names or [],
298
+ "static_names": static_names or [],
299
+ }
300
+
301
+
302
+ def load_or_compute_input_normalization_stats(
303
+ config: Dict[str, object],
304
+ train_rows: List[Dict[str, str]],
305
+ metric_dir: Path,
306
+ ) -> Dict[str, object] | None:
307
+ norm_cfg = normalization_config(config)
308
+ if not bool(norm_cfg.get("enabled", False)):
309
+ return None
310
+ stats_path_value = str(norm_cfg.get("stats_path", "")).strip()
311
+ if stats_path_value:
312
+ stats = load_json(Path(stats_path_value))
313
+ else:
314
+ stats = compute_input_normalization_stats(
315
+ rows=train_rows,
316
+ continuous_channel_indices=[int(v) for v in norm_cfg.get("continuous_channel_indices", list(range(10)))],
317
+ eps=float(norm_cfg.get("eps", 1e-6)),
318
+ )
319
+ write_json(metric_dir / "input_normalization_stats.json", stats)
320
+ return stats
321
+
322
+
323
  class ColdFeatureStore:
324
+ def __init__(self, rows: List[Dict[str, str]], normalization_stats: Dict[str, object] | None = None):
325
  self.cache: Dict[str, Dict[str, np.ndarray]] = {}
326
+ self.normalization_stats = normalization_stats
327
  static_path = Path(rows[0]["static_path"])
328
  static_npz = np.load(static_path, allow_pickle=True)
329
  static = self._sanitize(static_npz["static"].astype(np.float32))
 
341
  if "firewx_valid" in sample:
342
  extra.append(sample["firewx_valid"].astype(np.float32))
343
  x = np.concatenate([weather, firewx, *extra, static_x], axis=0).astype(np.float32)
344
+ x = self._apply_normalization(x)
345
  y = np.nan_to_num(sample["y_occ"].astype(np.float32), nan=0.0, posinf=0.0, neginf=0.0)
346
  self.cache[str(row["sample_id"])] = {"x": x, "y": y}
347
 
 
351
  x = np.nan_to_num(x, nan=0.0, posinf=0.0, neginf=0.0)
352
  return x.astype(np.float32, copy=False)
353
 
354
+ def _apply_normalization(self, x: np.ndarray) -> np.ndarray:
355
+ if not self.normalization_stats:
356
+ return x
357
+ for item in self.normalization_stats.get("channels", []):
358
+ idx = int(item["index"])
359
+ if idx < 0 or idx >= x.shape[0]:
360
+ continue
361
+ mean = float(item["mean"])
362
+ std = max(float(item["std"]), float(self.normalization_stats.get("eps", 1e-6)))
363
+ x[idx] = (x[idx] - mean) / std
364
+ return x.astype(np.float32, copy=False)
365
+
366
  def get(self, sample_id: str) -> Dict[str, np.ndarray]:
367
  return self.cache[sample_id]
368
 
 
654
  max_positive_tiles_per_sample: int,
655
  min_negative_tiles_per_sample: int,
656
  neg_pos_ratio: float,
657
+ positive_tile_placement: str,
658
  rng: random.Random,
659
  ) -> List[Dict[str, object]]:
660
  tile_rows: List[Dict[str, object]] = []
 
667
  tile_w=tile_size,
668
  max_tiles=max_positive_tiles_per_sample,
669
  rng=rng,
670
+ placement=positive_tile_placement,
671
  )
672
  neg_count = max(min_negative_tiles_per_sample, int(math.ceil(len(pos_origins) * neg_pos_ratio)))
673
  neg_origins = negative_tile_origins(
 
823
  train_rows = read_rows(index_root / "splits" / "train.csv")
824
  val_rows = read_rows(index_root / "splits" / "val.csv")
825
  test_rows = read_rows(index_root / "splits" / "test.csv")
826
+ normalization_stats = load_or_compute_input_normalization_stats(config, train_rows, metric_dir)
827
+ store = ColdFeatureStore(train_rows + val_rows + test_rows, normalization_stats=normalization_stats)
828
 
829
  tile_rows = build_train_tiles(
830
  train_rows=train_rows,
 
833
  max_positive_tiles_per_sample=int(config.get("max_positive_tiles_per_sample", 64)),
834
  min_negative_tiles_per_sample=int(config.get("min_negative_tiles_per_sample", 4)),
835
  neg_pos_ratio=float(config.get("negative_to_positive_ratio", 2.0)),
836
+ positive_tile_placement=str(config.get("positive_tile_placement", "center")),
837
  rng=rng,
838
  )
839
  train_ds = TrainTileDataset(tile_rows=tile_rows, store=store, augment_flip=bool(config.get("augment_flip", True)), seed=int(config.get("seed", 7)))
 
894
  "positive_train_tiles": int(sum(1 for row in tile_rows if row["tile_type"] == "positive")),
895
  "negative_train_tiles": int(sum(1 for row in tile_rows if row["tile_type"] == "negative")),
896
  "input_channels": in_ch,
897
+ "positive_tile_placement": str(config.get("positive_tile_placement", "center")),
898
  "train_positive_rate": train_positive_rate,
899
  "metric_reference_positive_rate": metric_reference_positive_rate,
900
  "pos_weight_source": pos_weight_source,
 
903
  "loss_type": str(config.get("loss_type", "bce")),
904
  "train_target_mode": str(config.get("train_target_mode", "hard")),
905
  "train_target_radius": int(config.get("train_target_radius", 0)),
906
+ "input_normalization": normalization_stats if normalization_stats else {"enabled": False},
907
  }
908
  (metric_dir / "tile_summary.json").write_text(json.dumps(summary_seed, indent=2), encoding="utf-8")
909
 
 
1023
  "num_train_tiles": len(tile_rows),
1024
  "positive_train_tiles": int(sum(1 for row in tile_rows if row["tile_type"] == "positive")),
1025
  "negative_train_tiles": int(sum(1 for row in tile_rows if row["tile_type"] == "negative")),
1026
+ "positive_tile_placement": str(config.get("positive_tile_placement", "center")),
1027
  "train_positive_rate": train_positive_rate,
1028
  "metric_reference_positive_rate": metric_reference_positive_rate,
1029
  "pos_weight": pos_weight,
 
1032
  "aux_spatial_loss_weight": aux_spatial_loss_weight,
1033
  "aux_positive_rate": aux_positive_rate,
1034
  "init_checkpoint": str(init_checkpoint) if init_checkpoint else "",
1035
+ "input_normalization": normalization_stats if normalization_stats else {"enabled": False},
1036
  "best_val_pr_auc": float(val_best["pr_auc"]),
1037
  "best_val_auroc": float(val_best["auroc"]),
1038
  "best_test_pr_auc": float(test_best["pr_auc"]),