{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[],"dockerImageVersionId":28755,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# ===== STAGE 16, FOLD 1/5: RESNET50 ONLY (Custom + Eff + Mob already done, this closes fold 1) =====\n!pip install -q tensorflow==2.19.0\n\nimport os\nos.environ['TF_USE_LEGACY_KERAS'] = '1'\n\nimport random\nimport numpy as np\nimport pandas as pd\nimport tensorflow as tf\nSEED = 42\nos.environ['PYTHONHASHSEED'] = str(SEED)\nos.environ['TF_DETERMINISTIC_OPS'] = '1'\nrandom.seed(SEED); np.random.seed(SEED); tf.random.set_seed(SEED)\nprint(f\"Seed {SEED} set, TF {tf.__version__}, tf.keras module: {tf.keras.__name__}\")\nassert 'tf_keras' in tf.keras.__name__, \"STOP: Keras 3 active, not legacy. Restart before continuing.\"\n\nfrom tensorflow.keras.preprocessing.image import ImageDataGenerator\nfrom tensorflow.keras.applications import ResNet50\nfrom tensorflow.keras.applications.resnet50 import preprocess_input as res_pre\nfrom tensorflow.keras.models import Sequential, load_model\nfrom tensorflow.keras.layers import GlobalAveragePooling2D, Dense, Dropout\nfrom tensorflow.keras.optimizers import Adam\nfrom tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint, CSVLogger\nfrom tensorflow.keras.metrics import AUC\nfrom sklearn.utils.class_weight import compute_class_weight\nfrom sklearn.model_selection import train_test_split\nfrom sklearn.metrics import roc_auc_score, accuracy_score, f1_score, confusion_matrix\n\nFOLD_CSV_PATH = '/kaggle/input/datasets/nihalabhay/chestcvassignments/cv_fold_assignments.csv'\nFINAL_CLASSES = ['no_finding', 'pathology']\nIMG_SIZE, BATCH_SIZE = 224, 32\nPHASE1_EPOCHS, PHASE1_LR, PHASE2_LR, EARLYSTOP_PAT = 10, 1e-3, 1e-5, 7\nAUG = dict(rotation_range=20, width_shift_range=0.1, height_shift_range=0.1,\n           horizontal_flip=True, zoom_range=0.1)\nCURRENT_FOLD = 1\n\nfold_df = pd.read_csv(FOLD_CSV_PATH)\nprint(f\"Loaded cv_fold_assignments.csv: {len(fold_df):,} rows\")\nprint(fold_df['fold'].value_counts().sort_index())\nassert fold_df['fold'].nunique() == 5, \"Fold file does not have 5 folds\"\nassert len(fold_df) == 44976, f\"Row count {len(fold_df)} does not match the locked 44,976\"\n\ntest_df   = fold_df[fold_df['fold'] == CURRENT_FOLD].reset_index(drop=True)\nremainder = fold_df[fold_df['fold'] != CURRENT_FOLD].reset_index(drop=True)\n\ndef safe_split(df, label_col, test_size, rs, tag=\"\"):\n    try:\n        return train_test_split(df, test_size=test_size, stratify=df[label_col], random_state=rs)\n    except ValueError as e:\n        print(f\"WARNING [{tag}]: stratified split failed, unstratified fallback. {e}\")\n        return train_test_split(df, test_size=test_size, random_state=rs)\n\ndef split_group_level_inner(df, label_col='label', val_frac=0.15, rs=SEED, tag=\"\"):\n    grouped = df.groupby('group_id')[label_col].agg(lambda s: s.value_counts().index[0]).reset_index()\n    g_tr, g_va = safe_split(grouped, label_col, val_frac, rs, tag=tag)\n    pick = lambda ids: df[df['group_id'].isin(ids['group_id'])]\n    return pick(g_tr), pick(g_va)\n\ntrain_parts, val_parts = [], []\nfor src in ['chex', 'nih', 'vinbig']:\n    sub = remainder[remainder['source'] == src]\n    tr_s, va_s = split_group_level_inner(sub, tag=f\"fold{CURRENT_FOLD}-{src}-inner\")\n    train_parts.append(tr_s); val_parts.append(va_s)\ntrain_df = pd.concat(train_parts, ignore_index=True)\nval_df   = pd.concat(val_parts, ignore_index=True)\n\nprint(f\"\\nFold {CURRENT_FOLD}: Train {len(train_df):,} ({len(train_df)/len(fold_df)*100:.1f}%) | \"\n      f\"Val {len(val_df):,} ({len(val_df)/len(fold_df)*100:.1f}%) | \"\n      f\"Test {len(test_df):,} ({len(test_df)/len(fold_df)*100:.1f}%)\")\n\nfor src in ['chex', 'nih', 'vinbig']:\n    tr_g = set(train_df[train_df['source']==src]['group_id'])\n    va_g = set(val_df[val_df['source']==src]['group_id'])\n    te_g = set(test_df[test_df['source']==src]['group_id'])\n    ok = tr_g.isdisjoint(te_g) and tr_g.isdisjoint(va_g) and va_g.isdisjoint(te_g)\n    print(f\"  {src}: train/val/test group-disjoint = {ok}\")\n    assert ok, f\"LEAKAGE in fold {CURRENT_FOLD}, source {src}\"\nprint(f\"Fold {CURRENT_FOLD} leakage check: PASS (chex, nih, vinbig)\")\n\ncls = np.array(FINAL_CLASSES)\ncw  = compute_class_weight('balanced', classes=cls, y=train_df['label'])\nCLASS_WEIGHT = {i: w for i, w in enumerate(cw)}\nprint(f\"Fold {CURRENT_FOLD} class_weight (recomputed fresh from THIS fold's train set):\",\n      {c: round(w,3) for c,w in zip(cls, cw)})\n\ndef make_gens(preprocess_fn):\n    train_idg = ImageDataGenerator(preprocessing_function=preprocess_fn, **AUG)\n    eval_idg  = ImageDataGenerator(preprocessing_function=preprocess_fn)\n    common = dict(x_col='image_path', y_col='label', target_size=(IMG_SIZE,IMG_SIZE), batch_size=BATCH_SIZE,\n                  class_mode='categorical', classes=FINAL_CLASSES, color_mode='rgb')\n    return (train_idg.flow_from_dataframe(train_df, shuffle=True, seed=SEED, **common),\n            eval_idg.flow_from_dataframe(val_df, shuffle=False, **common),\n            eval_idg.flow_from_dataframe(test_df, shuffle=False, **common))\n\ndef build_pretrained(base_class, num_classes=2, shape=(224,224,3)):\n    base = base_class(include_top=False, weights='imagenet', input_shape=shape)\n    model = Sequential([base, GlobalAveragePooling2D(), Dense(256,activation='relu'),\n                        Dropout(0.3), Dense(num_classes,activation='softmax')])\n    return model, base\n\ndef finalise(arch_code, model_path, te_gen, live_auc):\n    m = load_model(model_path)\n    proba = m.predict(te_gen, verbose=0)[:,1]\n    y = np.array(te_gen.classes); yhat = (proba >= 0.5).astype(int)\n    auc = roc_auc_score(y, proba); acc = accuracy_score(y, yhat)\n    f1 = f1_score(y, yhat, average='macro')\n    tn, fp, fn, tp = confusion_matrix(y, yhat).ravel()\n    sens = tp/(tp+fn) if (tp+fn) else float('nan')\n    spec = tn/(tn+fp) if (tn+fp) else float('nan')\n    del m; import gc; gc.collect()\n\n    print(f\"Live AUC={live_auc:.4f} | Reloaded AUC={auc:.4f} | Match: {abs(live_auc-auc) < 1e-4}\")\n    assert abs(live_auc - auc) < 1e-4, \"CHECKPOINT MISMATCH, invalid provenance, discard this result\"\n\n    np.savez_compressed(f'/kaggle/working/cv_f{CURRENT_FOLD}_preds_{arch_code}.npz',\n                        proba=proba, y_true=y,\n                        source=test_df['source'].values, group_id=test_df['group_id'].values)\n    return {'fold':CURRENT_FOLD, 'arch':arch_code, 'accuracy':acc, 'macro_f1':f1, 'macro_auc':auc,\n            'macro_sensitivity':sens, 'macro_specificity':spec, 'n_test':len(y),\n            'tn':tn, 'fp':fp, 'fn':fn, 'tp':tp}\n\n# ---- RESNET50 ----\nprint(f\"\\n{'='*20} FOLD {CURRENT_FOLD}: RESNET50 {'='*20}\")\ntr, va, te = make_gens(res_pre)\nmodel, base = build_pretrained(ResNet50)\nbase.trainable = False\nmodel.compile(Adam(PHASE1_LR), 'categorical_crossentropy', ['accuracy', AUC(name='auc')])\nmodel.fit(tr, validation_data=va, epochs=PHASE1_EPOCHS, class_weight=CLASS_WEIGHT, verbose=1,\n          callbacks=[CSVLogger(f'/kaggle/working/cv_f{CURRENT_FOLD}_res_log.csv', append=False)])\nmodel.layers[0].trainable = True\nmodel.compile(Adam(PHASE2_LR), 'categorical_crossentropy', ['accuracy', AUC(name='auc')])\np = f'/kaggle/working/cv_f{CURRENT_FOLD}_res.keras'\nmodel.fit(tr, validation_data=va, epochs=60, class_weight=CLASS_WEIGHT, verbose=1,\n          callbacks=[EarlyStopping(monitor='val_auc', mode='max', patience=EARLYSTOP_PAT, restore_best_weights=True),\n                     ModelCheckpoint(p, monitor='val_auc', mode='max', save_best_only=True),\n                     CSVLogger(f'/kaggle/working/cv_f{CURRENT_FOLD}_res_log.csv', append=True)])\nlive = roc_auc_score(np.array(te.classes), model.predict(te, verbose=0)[:,1])\ndel model, base; import gc; gc.collect()\nres_row = finalise('res', p, te, live)\npd.DataFrame([res_row]).to_csv(f'/kaggle/working/cv_f{CURRENT_FOLD}_res_results.csv', index=False)\n\nprint(f\"\\n\\n{'='*20} FOLD {CURRENT_FOLD}: RESNET50 DONE, FOLD {CURRENT_FOLD} COMPLETE {'='*20}\")\nprint(pd.DataFrame([res_row]).to_string(index=False))\nprint(f\"\\nDownload cv_f{CURRENT_FOLD}_res.keras, cv_f{CURRENT_FOLD}_res_log.csv, \"\n      f\"cv_f{CURRENT_FOLD}_preds_res.npz, cv_f{CURRENT_FOLD}_res_results.csv individually now.\")\nprint(f\"\\nFOLD {CURRENT_FOLD}: all 4 architectures done (custom, eff, mob, res). \"\n      f\"Combine the 4 individual *_results.csv files into cv_f{CURRENT_FOLD}_results.csv \"\n      f\"whenever convenient, per fold-major discipline fold 2 can now begin.\")","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}