Download train.py from kabiriitb/rockglacierNET: direct link, hf CLI and curl.
- Browser
- Download file 15.6 kB
-
https://huggingface.co/kabiriitb/rockglacierNET/resolve/main/train.py
- Command line
-
hf download hf://kabiriitb/rockglacierNET/train.py
-
curl -L -o train.py https://huggingface.co/kabiriitb/rockglacierNET/resolve/main/train.py
15.6 kB
| import os | |
| import numpy as np | |
| np.Inf = np.inf # Patch for Keras/Numpy 2.0 compatibility | |
| import tensorflow as tf | |
| # Enable GPU Memory Growth to prevent TF from hoarding all 48GB of VRAM immediately | |
| gpus = tf.config.list_physical_devices('GPU') | |
| if gpus: | |
| try: | |
| for gpu in gpus: | |
| tf.config.experimental.set_memory_growth(gpu, True) | |
| except RuntimeError as e: | |
| print(e) | |
| from data_generator import RockGlacierDataGenerator | |
| from models.swin_unet import build_model as build_swin_unet | |
| from models.attention_unet import build_attention_unet | |
| from models.unet import build_unet | |
| from models.segformer import build_segformer | |
| from tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping, CSVLogger, ReduceLROnPlateau | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Data + band selection (all env-driven; patch size auto-detected) | |
| # DATA_DIR : patches root (default all_data/patches) -> {train,val}/{features,masks} | |
| # BAND_LIST : comma-separated band names, used as-is (highest priority) | |
| # BANDS : a named preset in BAND_SETS below | |
| # RUN_NAME : names the checkpoint dir (all_data/checkpoint_<RUN_NAME>) | |
| # Patch size is read from the data, so the SAME script trains the 64 px (30 m) | |
| # set and the 128 px (10 m) set with no edits. | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| import json as _json, glob as _glob | |
| DATA_DIR = os.environ.get("DATA_DIR", "all_data/patches") | |
| TRAIN_FEAT = f"{DATA_DIR}/train/features" | |
| TRAIN_MASK = f"{DATA_DIR}/train/masks" | |
| VAL_FEAT = os.environ.get("VAL_FEAT", f"{DATA_DIR}/val/features") | |
| VAL_MASK = os.environ.get("VAL_MASK", f"{DATA_DIR}/val/masks") | |
| BAND_SETS = { | |
| "14": ["TPI", "ROUGHNESS", "LST_winter", "NDWI", "LST_summer", "REL_RELIEF", | |
| "ASPECT", "LST_anomaly", "ELEV_COP", "B2", "HILLSHADE", "B8A", "NDMI", "B12"], | |
| "17": ["TPI", "ROUGHNESS", "LST_winter", "NDWI", "LST_summer", "REL_RELIEF", | |
| "ASPECT", "LST_anomaly", "ELEV_COP", "B2", "HILLSHADE", "B8A", "NDMI", "B12", | |
| "B3", "B4", "B8"], | |
| "opt_idx": ["B2", "B3", "B4", "B8", "B8A", "B11", "B12", "NDVI", "NDWI", "NDMI", "NDSI", "BSI", "ALBEDO", "ELEV_COP"], | |
| "terr_therm": ["TPI", "ROUGHNESS", "SLOPE", "ASPECT", "REL_RELIEF", "HILLSHADE", "CURVATURE", "ELEV_COP", "LST_winter", "LST_summer", "LST_annual", "LST_anomaly"], | |
| "mixed_16": ["TPI", "ROUGHNESS", "REL_RELIEF", "ELEV_COP", "SLOPE", "LST_winter", "LST_summer", "NDWI", "NDVI", "B2", "B3", "B4", "B8", "B12", "NDMI", "BSI"], | |
| "top_20": ["TPI", "ROUGHNESS", "LST_winter", "NDWI", "LST_summer", "REL_RELIEF", "ASPECT", "LST_anomaly", "ELEV_COP", "B2", "HILLSHADE", "B8A", "NDMI", "B12", "B3", "B4", "B8", "NDVI", "SLOPE", "CURVATURE"], | |
| } | |
| _band_names = _json.load(open(f"{DATA_DIR}/metadata/norm_stats.json"))["band_names"] | |
| _blist = os.environ.get("BAND_LIST") | |
| _sel = os.environ.get("BANDS", "all") | |
| if _blist: | |
| _bands, RUN_NAME = [b.strip() for b in _blist.split(",") if b.strip()], os.environ.get("RUN_NAME", "custom") | |
| elif _sel in BAND_SETS: | |
| _bands, RUN_NAME = BAND_SETS[_sel], os.environ.get("RUN_NAME", _sel + "band") | |
| else: | |
| _bands, RUN_NAME = None, os.environ.get("RUN_NAME", "all") | |
| if _bands is not None: | |
| _missing = [b for b in _bands if b not in _band_names] | |
| assert not _missing, f"Unknown band(s) {_missing}\nAvailable: {_band_names}" | |
| BAND_INDICES, N_BANDS = [_band_names.index(b) for b in _bands], len(_bands) | |
| else: | |
| BAND_INDICES, N_BANDS, _bands = None, len(_band_names), list(_band_names) | |
| CKPT_DIR = os.environ.get("CKPT_DIR", | |
| "all_data/checkpoint" if RUN_NAME == "all" else f"all_data/checkpoint_{RUN_NAME}") | |
| os.makedirs(CKPT_DIR, exist_ok=True) | |
| # Patch size auto-detected from a sample patch (64 px now, 128 px for 10 m later). | |
| _ph, _pw = np.load(sorted(_glob.glob(f"{TRAIN_FEAT}/*.npy"))[0]).shape[:2] | |
| print(f"[train] run={RUN_NAME} bands={N_BANDS} patch={_ph}x{_pw} data={DATA_DIR}") | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Hyperparameters | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| BATCH_SIZE = int(os.environ.get("BATCH_SIZE", 32)) | |
| EPOCHS = int(os.environ.get("EPOCHS", 150)) | |
| LR = 1e-4 | |
| INPUT_SHAPE = (_ph, _pw, N_BANDS) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Generators | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| print("Initializing Data Generators...") | |
| _COPY_PASTE = os.environ.get("COPY_PASTE", "0") == "1" | |
| _CP_PROB = float(os.environ.get("COPY_PASTE_PROB", 0.5)) | |
| train_gen = RockGlacierDataGenerator(TRAIN_FEAT, TRAIN_MASK, batch_size=BATCH_SIZE, | |
| target_size=INPUT_SHAPE[:2], augment=True, | |
| return_weight=True, radiometric=True, | |
| band_indices=BAND_INDICES, | |
| copy_paste=_COPY_PASTE, copy_paste_prob=_CP_PROB) | |
| # use ALL patches; COPY_PASTE=1 enables copy-paste aug | |
| val_gen = RockGlacierDataGenerator(VAL_FEAT, VAL_MASK, batch_size=BATCH_SIZE, | |
| target_size=INPUT_SHAPE[:2], augment=False, shuffle=False, | |
| return_weight=True, band_indices=BAND_INDICES) | |
| # val keeps ALL patches for honest evaluation | |
| # Record the exact normalization stats used, so evaluate.py matches at test time. | |
| import json | |
| with open(os.path.join(CKPT_DIR, 'norm_stats.json'), 'w') as f: | |
| json.dump({'mean': np.asarray(train_gen.GLOBAL_MEAN).reshape(-1).tolist(), | |
| 'std': np.asarray(train_gen.GLOBAL_STD).reshape(-1).tolist()}, f, indent=2) | |
| # Record the exact run config (bands, patch size) so evaluate.py reproduces it | |
| # with zero chance of a band/order mismatch. | |
| with open(os.path.join(CKPT_DIR, 'run_config.json'), 'w') as f: | |
| json.dump({'run_name': RUN_NAME, 'bands': _bands, 'n_bands': N_BANDS, | |
| 'patch_size': [int(_ph), int(_pw)], 'data_dir': DATA_DIR}, f, indent=2) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Loss / metrics β PADDING-MASKED. | |
| # y_true is (B,H,W,2): channel 0 = label, channel 1 = validity weight (1=real | |
| # pixel, 0=zero-padding). All sums below are restricted to valid pixels so the | |
| # model is never trained or scored on padded regions. | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _split_label_weight(y_true): | |
| yt = tf.cast(y_true[..., 0:1], tf.float32) | |
| w = tf.cast(y_true[..., 1:2], tf.float32) | |
| return yt, w | |
| # Focal-Tversky hyperparameters. | |
| # Tversky index TI = TP / (TP + ALPHA*FP + BETA*FN) | |
| # ALPHA>BETA penalizes false positives harder -> raises precision (the model | |
| # currently over-segments: FP > FN). GAMMA focuses the loss on hard pixels. | |
| ALPHA_FP = 0.4 # slightly higher β new dataset has real background patches in training | |
| BETA_FN = 0.6 # balanced β new dataset has much better class quality | |
| GAMMA_FT = 1.3333 | |
| BCE_WEIGHT = 0.5 # light masked BCE term for stable early gradients | |
| LABEL_SMOOTH = 0.05 | |
| def focal_tversky_loss(y_true, y_pred): | |
| smooth = 1e-6 | |
| yt, w = _split_label_weight(y_true) | |
| yp = tf.cast(y_pred, tf.float32) * w # soft probs, padded pixels zeroed | |
| yt = yt * w | |
| tp = tf.reduce_sum(yt * yp, axis=[1, 2, 3]) | |
| fp = tf.reduce_sum((1.0 - yt) * yp, axis=[1, 2, 3]) | |
| fn = tf.reduce_sum(yt * (1.0 - yp), axis=[1, 2, 3]) | |
| ti = (tp + smooth) / (tp + ALPHA_FP * fp + BETA_FN * fn + smooth) | |
| return tf.reduce_mean(tf.pow(1.0 - ti, GAMMA_FT)) | |
| def combined_loss(y_true, y_pred): | |
| yt, w = _split_label_weight(y_true) | |
| yt_s = yt * (1.0 - LABEL_SMOOTH) + 0.5 * LABEL_SMOOTH # binary label smoothing | |
| bce = tf.keras.backend.binary_crossentropy(yt_s, tf.cast(y_pred, tf.float32)) | |
| bce = tf.reduce_sum(bce * w) / (tf.reduce_sum(w) + 1e-6) # mean over valid pixels | |
| return BCE_WEIGHT * bce + focal_tversky_loss(y_true, y_pred) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Metrics (all at threshold 0.5, valid pixels only; logged by CSVLogger) | |
| # | |
| # IMPORTANT: each metric uses tf.cond to return 0.0 when the batch contains | |
| # no positive pixels, instead of the previous 1e-6/1e-6 = 1.0 artifact. | |
| # With 52% background-only patches, the old smoothed formulas returned 1.0 for | |
| # those batches and inflated the epoch-averaged val_dice by ~0.4β0.5 points, | |
| # causing ModelCheckpoint to save the wrong epoch as "best". | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def dice_coef(y_true, y_pred): | |
| yt, w = _split_label_weight(y_true) | |
| yp = tf.cast(y_pred > 0.5, tf.float32) * w | |
| yt = tf.cast(yt > 0.5, tf.float32) * w | |
| inter = tf.reduce_sum(yt * yp) | |
| denom = tf.reduce_sum(yt) + tf.reduce_sum(yp) | |
| return tf.cond(denom < 0.5, lambda: 0.0, lambda: (2. * inter) / denom) | |
| def precision_m(y_true, y_pred): | |
| yt, w = _split_label_weight(y_true) | |
| yp = tf.cast(y_pred > 0.5, tf.float32) * w | |
| yt = tf.cast(yt > 0.5, tf.float32) * w | |
| tp = tf.reduce_sum(yt * yp) | |
| fp = tf.reduce_sum(yp * (1.0 - yt)) | |
| denom = tp + fp | |
| return tf.cond(denom < 0.5, lambda: 0.0, lambda: tp / denom) | |
| def recall_m(y_true, y_pred): | |
| yt, w = _split_label_weight(y_true) | |
| yp = tf.cast(y_pred > 0.5, tf.float32) * w | |
| yt = tf.cast(yt > 0.5, tf.float32) * w | |
| tp = tf.reduce_sum(yt * yp) | |
| fn = tf.reduce_sum(yt * (1.0 - yp)) | |
| denom = tp + fn | |
| return tf.cond(denom < 0.5, lambda: 0.0, lambda: tp / denom) | |
| def f1_m(y_true, y_pred): | |
| p = precision_m(y_true, y_pred); r = recall_m(y_true, y_pred) | |
| denom = p + r | |
| return tf.cond(denom < 1e-6, lambda: 0.0, lambda: (2.0 * p * r) / denom) | |
| def iou_m(y_true, y_pred): | |
| yt, w = _split_label_weight(y_true) | |
| yp = tf.cast(y_pred > 0.5, tf.float32) * w | |
| yt = tf.cast(yt > 0.5, tf.float32) * w | |
| inter = tf.reduce_sum(yt * yp) | |
| union = tf.reduce_sum(yt) + tf.reduce_sum(yp) - inter | |
| return tf.cond(union < 0.5, lambda: 0.0, lambda: inter / union) | |
| def acc_m(y_true, y_pred): | |
| yt, w = _split_label_weight(y_true) | |
| yp = tf.cast(y_pred > 0.5, tf.float32) | |
| yt_b = tf.cast(yt > 0.5, tf.float32) | |
| correct = tf.cast(tf.equal(yp, yt_b), tf.float32) * w | |
| return tf.reduce_sum(correct) / (tf.reduce_sum(w) + 1e-6) | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Build Model | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| print("Building Model...") | |
| MODEL_TYPE = os.environ.get("MODEL_NAME", "swin_unet") | |
| if MODEL_TYPE == "attention_unet": | |
| model = build_attention_unet(input_shape=INPUT_SHAPE, num_classes=1) | |
| elif MODEL_TYPE == "unet": | |
| model = build_unet(input_shape=INPUT_SHAPE, num_classes=1) | |
| elif MODEL_TYPE == "segformer": | |
| model = build_segformer(input_shape=INPUT_SHAPE, num_classes=1) | |
| else: | |
| model = build_swin_unet(input_shape=INPUT_SHAPE, num_classes=1, num_heads=8) | |
| # Cosine annealing: smoothly decay LR from 1e-4 β 1e-6 over all epochs. | |
| # Replaces ReduceLROnPlateau β no sudden drops, lets the model explore longer. | |
| _total_steps = len(train_gen) * EPOCHS | |
| cosine_lr = tf.keras.optimizers.schedules.CosineDecay( | |
| initial_learning_rate=LR, | |
| decay_steps=_total_steps, | |
| alpha=1e-2, # minimum LR = LR * alpha = 1e-4 * 1e-2 = 1e-6 | |
| ) | |
| _WD = float(os.environ.get("WEIGHT_DECAY", 1e-4)) # raise (e.g. 5e-4) to regularize more | |
| try: | |
| optimizer = tf.keras.optimizers.AdamW(learning_rate=cosine_lr, weight_decay=_WD) | |
| print(f"Optimizer: AdamW + CosineDecay ({_total_steps} steps, wd={_WD})") | |
| except Exception as e: | |
| optimizer = tf.keras.optimizers.Adam(learning_rate=cosine_lr) | |
| print(f"Optimizer: Adam + CosineDecay ({_total_steps} steps)") | |
| model.compile(optimizer=optimizer, | |
| loss=combined_loss, | |
| metrics=[acc_m, dice_coef, precision_m, recall_m, f1_m, iou_m]) | |
| model.summary() | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Callbacks | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| callbacks = [ | |
| ModelCheckpoint(os.path.join(CKPT_DIR, 'best_model.weights.h5'), | |
| monitor='val_loss', mode='min', | |
| save_best_only=True, save_weights_only=True, verbose=1), | |
| # Patience=35: cosine LR decays smoothly so the model needs more room to | |
| # naturally improve without the crutch of sudden LR reductions. | |
| EarlyStopping(monitor='val_loss', patience=35, restore_best_weights=True, verbose=1), | |
| # ReduceLROnPlateau removed β cosine annealing handles LR schedule. | |
| CSVLogger(os.path.join(CKPT_DIR, 'training_history.csv')), | |
| ] | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Train | |
| # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| print("Starting Training...") | |
| history = model.fit( | |
| train_gen, | |
| validation_data=val_gen, | |
| epochs=EPOCHS, | |
| callbacks=callbacks, | |
| ) | |
| print("Training Complete.") | |