Add serving-oriented tiled inference and jittered training support
Browse files
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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
ys, xs = np.where(mask > 0.5)
|
| 49 |
if ys.size == 0:
|
| 50 |
return []
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 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 |
-
|
|
|
|
| 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"]),
|