{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":19991,"databundleVersionId":1117522,"sourceType":"competition"},{"sourceId":13732237,"sourceType":"datasetVersion","datasetId":8737086},{"sourceId":13879622,"sourceType":"datasetVersion","datasetId":8842916}],"dockerImageVersionId":31192,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import numpy as np\nsrm_weights = np.load('/kaggle/input/kernels-srm/SRM_Kernels.npy').astype(np.float32)\nprint(srm_weights.shape)","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================\n# CELL 1 – IMPORT & GLOBAL CONFIG\n# ============================\nimport os, glob\nimport numpy as np\nimport tensorflow as tf\nimport tensorflow.keras.layers as L\nimport tensorflow.keras.backend as K\nfrom tensorflow.keras.applications import ResNet50\nfrom tensorflow.keras.applications.resnet50 import preprocess_input\nfrom tensorflow.keras.regularizers import l2\n\nprint(\"[INFO] TensorFlow:\", tf.__version__)\n\nIMG_SIZE   = 256\nBATCH_SIZE = 32\nAUTO       = tf.data.AUTOTUNE\n\n# ======== Define 3*tanh activation (đúng như lúc train) ========\ndef tanh3(x):\n    return 3.0 * K.tanh(x)\n\nDEFAULT_LABEL_SMOOTH = 0.05","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cell 2\n# ============================\n# LOAD SRM KERNELS\n# ============================\ndef find_srm_path():\n    # Tự tìm file SRM_Kernels.npy trong /kaggle/working hoặc /kaggle/input/*\n    cand = glob.glob('/kaggle/working/**/SRM_Kernels.npy', recursive=True)\n    if cand:\n        return cand[0]\n    for root in glob.glob('/kaggle/input/*'):\n        cand = glob.glob(os.path.join(root, '**', 'SRM_Kernels.npy'), recursive=True)\n        if cand:\n            return cand[0]\n    return None\n\nsrm_path = find_srm_path()\nassert srm_path is not None, \"Chưa thấy SRM_Kernels.npy trong Input/Working!\"\nprint(\"[SRM] path:\", srm_path)\n\nsrm_weights = np.load(srm_path).astype(np.float32)\nprint(\"[SRM] shape:\", srm_weights.shape)\nassert srm_weights.shape == (5, 5, 1, 30)\nbias_srm = np.ones((30,), dtype=np.float32)\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Cell 3\n# ============================\n# BUILD SRM-ResNet50 MODEL\n# ============================\ndef focal_loss(alpha0=0.65, alpha1=0.35, gamma=2.0):\n    def _loss(y_true, y_pred):\n        eps = 1e-7\n        y_pred = tf.clip_by_value(y_pred, eps, 1 - eps)\n        pt = y_true * y_pred + (1 - y_true) * (1 - y_pred)\n        alpha = y_true * alpha1 + (1 - y_true) * alpha0\n        return -tf.reduce_mean(alpha * tf.pow(1 - pt, gamma) * tf.math.log(pt))\n    return _loss\n\nUSE_FOCAL = False\ndef build_srm_resnet50(input_shape=(IMG_SIZE, IMG_SIZE, 3),\n                       stem_out=3,\n                       train_backbone=False,\n                       label_smoothing=0.05,\n                       lr=1e-4):\n  \n    inp = L.Input(shape=input_shape, name=\"input_rgb\")\n\n    # 1) SRM (grayscale -> conv fixed)\n    gray = L.Lambda(lambda t: tf.image.rgb_to_grayscale(t), name='to_gray')(inp)\n    srm  = L.Conv2D(30, 5, padding='same', use_bias=True,\n                    trainable=False, name='srm_conv')(gray)\n    srm  = L.Activation(tanh3, name='tanh3')(srm)\n    srm  = L.BatchNormalization(name='srm_bn')(srm)\n\n    # 2) Stem 1x1: 30 -> 3 channels (match ResNet pretrained)\n    stem = L.Conv2D(stem_out, 1, padding='same', use_bias=False, name='stem_1x1')(srm)\n    stem = L.BatchNormalization(name='stem_bn')(stem)\n\n    # 3) ResNet50 backbone\n    pre  = L.Lambda(lambda t: preprocess_input(t * 255.0), name='resnet_pre')(stem)\n    base = ResNet50(include_top=False, weights='imagenet',\n                    pooling='avg', name='resnet50')\n    base.trainable = train_backbone\n    feat = base(pre)\n\n    # 4) Classification head – thêm L2 + Dropout để giảm overfitting\n    x = L.Dense(256, activation='relu', kernel_regularizer=l2(1e-4),name=\"dense_256\")(feat)\n    x = L.Dropout(0.3, name=\"dropout_0_3\")(x)\n    out = L.Dense(1, activation='sigmoid', name='head')(x)\n\n    model = tf.keras.Model(inputs=inp, outputs=out, name='SRM_ResNet50')\n\n    \n\n    # Nạp trọng số SRM cố định\n    model.get_layer('srm_conv').set_weights([srm_weights, bias_srm])\n\n    # -------- CHỌN LOSS --------\n    if USE_FOCAL:\n        loss_fn = focal_loss(alpha0=0.65, alpha1=0.35)   # ưu tiên cover\n    else:\n        loss_fn = tf.keras.losses.BinaryCrossentropy(label_smoothing=label_smoothing)\n        \n    # Compile\n    model.compile(\n        optimizer=tf.keras.optimizers.Adam(lr),\n        loss=tf.keras.losses.BinaryCrossentropy(label_smoothing=label_smoothing),\n        metrics=[\n            'accuracy',\n            tf.keras.metrics.AUC(name='auc'),\n            tf.keras.metrics.Precision(name='precision'),\n            tf.keras.metrics.Recall(name='recall')\n        ]\n    )\n    return model","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================\n# CELL 4 – DECODE + AUGMENT + DATASET\n# ============================\n\ndef decode_img(path, label=None):\n    \"\"\"Đọc file JPEG, resize, scale về [0,1].\"\"\"\n    bits  = tf.io.read_file(path)\n    image = tf.image.decode_jpeg(bits, channels=3)\n    image = tf.image.resize(image, (IMG_SIZE, IMG_SIZE))\n    image = tf.clip_by_value(image / 255.0, 0.0, 1.0)\n    return (image, label) if label is not None else image\n\ndef jpeg_reencode(img):\n    \"\"\"Random chất lượng JPEG để tăng noise nén.\"\"\"\n    img8 = tf.image.convert_image_dtype(img, tf.uint8)\n    seed = tf.random.uniform([2], maxval=2**31-1, dtype=tf.int32)\n    img8 = tf.image.stateless_random_jpeg_quality(img8, 70, 100, seed=seed)\n    return tf.image.convert_image_dtype(img8, tf.float32)\n\ndef augment(image, label):\n    \"\"\"Augmentation moderate để giảm overfitting.\"\"\"\n    # Flip\n    image = tf.image.random_flip_left_right(image)\n    image = tf.image.random_flip_up_down(image)\n\n    # Rotate 0/90/180/270\n    if tf.random.uniform([]) < 0.3:\n        k = tf.random.uniform([], minval=1, maxval=4, dtype=tf.int32)\n        image = tf.image.rot90(image, k)\n\n    # Gaussian noise\n    if tf.random.uniform([]) < 0.3:\n        noise = tf.random.normal(tf.shape(image), mean=0.0, stddev=0.05)\n        image = tf.clip_by_value(image + noise, 0.0, 1.0)\n\n    # JPEG re-encode\n    if tf.random.uniform([]) < 0.4:\n        image = jpeg_reencode(image)\n\n    return image, label\n\ndef make_ds(paths, labels=None, training=False, batch=BATCH_SIZE):\n    \"\"\"Tạo tf.data Dataset cho train/valid/test.\"\"\"\n    if labels is None:\n        ds = tf.data.Dataset.from_tensor_slices(paths) \\\n                            .map(decode_img, num_parallel_calls=AUTO)\n    else:\n        ds = tf.data.Dataset.from_tensor_slices((paths, labels)) \\\n                            .map(decode_img, num_parallel_calls=AUTO)\n\n    if training:\n        ds = ds.shuffle(4096) \\\n               .map(augment, num_parallel_calls=AUTO) \\\n               .repeat()\n\n    ds = ds.batch(batch).prefetch(AUTO)\n    return ds\n\nprint(\"[INFO] Dataset helpers ready.\")\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# ============================\n# CELL 5 – ALASKA2 + REAL IMAGE SPLITS\n# ============================\nfrom sklearn.model_selection import train_test_split\ndef build_alaska2_splits(root_dir,\n                         pos_dirs=(\"JMiPOD\", \"JUNIWARD\", \"UERD\"),\n                         neg_dir=\"Cover\",\n                         test_size=0.2,\n                         random_state=42,\n                         limit_per_class=None,\n                        ):\n\n    # 1) Negatives (Cover)\n    neg_paths = sorted(glob.glob(os.path.join(root_dir, neg_dir, \"*.jpg\"))) \\\n              + sorted(glob.glob(os.path.join(root_dir, neg_dir, \"*.jpeg\"))) \\\n              + sorted(glob.glob(os.path.join(root_dir, neg_dir, \"*.JPG\")))\n\n    # 2) Positives (Stego)\n    pos_paths = []\n    for d in pos_dirs:\n        p = sorted(glob.glob(os.path.join(root_dir, d, \"*.jpg\"))) \\\n          + sorted(glob.glob(os.path.join(root_dir, d, \"*.jpeg\"))) \\\n          + sorted(glob.glob(os.path.join(root_dir, d, \"*.JPG\")))\n        if limit_per_class is not None:\n            p = p[:limit_per_class]\n        pos_paths.extend(p)\n\n    # 3) Balance stego vs cover (theo min)\n    if limit_per_class is not None:\n        n_pos = len(pos_paths)\n        rng = np.random.default_rng(42)\n        if len(neg_paths) > n_pos:\n            neg_paths = list(rng.choice(neg_paths, size=n_pos, replace=False))\n    else:\n        n_pos, n_neg = len(pos_paths), len(neg_paths)\n        if n_neg > n_pos:\n            neg_paths = neg_paths[:n_pos]\n        elif n_pos > n_neg:\n            pos_paths = pos_paths[:n_neg]\n\n    print(f\"[BALANCE] pos={len(pos_paths)}  neg={len(neg_paths)}\")\n\n    # 4) Thêm real images vào negative (vẫn label=0 cho đơn giản)\n    '''if real_dir is not None and os.path.isdir(real_dir):\n        real_paths = sorted(glob.glob(os.path.join(real_dir, \"*.jpg\"))) \\\n                   + sorted(glob.glob(os.path.join(real_dir, \"*.jpeg\"))) \\\n                   + sorted(glob.glob(os.path.join(real_dir, \"*.png\")))\n        print(f\"[REAL] Found {len(real_paths)} real images.\")\n        neg_paths = list(neg_paths) + list(real_paths)\n    else:\n        print(\"[REAL] No real image folder found OR real_dir=None\")\n'''\n    # 5) Build dataset\n    paths  = np.array(pos_paths + neg_paths)\n    labels = np.array([1] * len(pos_paths) + [0] * len(neg_paths), dtype=np.int32)\n\n    X_train, X_valid, y_train, y_valid = train_test_split(\n        paths, labels,\n        test_size=test_size,\n        random_state=random_state,\n        stratify=labels\n    )\n \n    print(\"[SPLIT] train labels:\", np.bincount(y_train),\n      \" valid labels:\", np.bincount(y_valid))\n\n    return X_train, X_valid, y_train, y_valid, pos_paths, neg_paths\n\n# ============================\n# AUTODETECT ALASKA2 ROOT\n# ============================\nDEFAULT_ROOTS = [\n    \"/kaggle/input/alaska2-image-steganalysis\",\n    \"/content/alaska2\",\n    \"/content/ALASKA2\",\n    \"/data/ALASKA2\"\n]\nroot_dir = next((p for p in DEFAULT_ROOTS if os.path.isdir(p)), None)\nassert root_dir is not None, \"Không thấy thư mục ALASKA2!\"\nprint(\"[INFO] Using ALASKA2 root:\", root_dir)\n\n# ============================\n# RUN DATASET SPLIT\n# ============================\n# Thư mục ảnh thật (nếu có)\n\ntrain_paths, valid_paths, train_labels, valid_labels, pos_paths, neg_paths = build_alaska2_splits(\n    root_dir=root_dir,\n    pos_dirs=(\"JMiPOD\", \"JUNIWARD\", \"UERD\"),\n    neg_dir=\"Cover\",\n    test_size=0.2,\n    random_state=42,1000,      \n)\n\nprint(\"[INFO] Final Train size:\", len(train_paths))\nprint(\"[INFO] Final Valid size:\", len(valid_paths))\n\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n!rm -f /kaggle/working/srm_resnet50_best.keras\n!rm -f /kaggle/working/srm_resnet50_finetuned.keras\n!rm -f /kaggle/working/srm_stage1.keras\n!rm -f /kaggle/working/best_threshold.txt\n\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================\n# CELL 6 – DATASET OBJECTS + CLASS WEIGHTS\n# ============================\nfrom sklearn.utils.class_weight import compute_class_weight\n\nassert 'train_paths' in locals() and 'train_labels' in locals(), \\\n       \"Chưa chạy Cell 3 – thiếu train_paths / train_labels\"\n\nBATCH_FOR_TRAIN = 20\n\ntrain_ds = make_ds(train_paths, train_labels,\n                   training=True,\n                   batch=BATCH_FOR_TRAIN)\nvalid_ds = make_ds(valid_paths, valid_labels,\n                   training=False,\n                   batch=BATCH_FOR_TRAIN)\n\nprint(\"train_ds:\", train_ds)\nprint(\"valid_ds:\", valid_ds)\n\n# Class weight để cân bằng loss\n\ny_train_np = np.array(train_labels)\nclass_weights_arr = compute_class_weight(\n    class_weight='balanced',\n    classes=np.unique(y_train_np),\n    y=y_train_np\n)\nclass_weights = {0: 1.8, 1: 1.0}\nprint(\"Class weights fixed =>\", class_weights)\n\n# ============================\n#  STAGE 1: FREEZE BACKBONE (WARMUP)\n# ============================\nprint(\"\\n === STAGE 1: FREEZE BACKBONE (WARMUP) ===\")\n\nmodel = build_srm_resnet50(\n    train_backbone=False,     \n    label_smoothing=0.2,\n    lr=1e-4\n)\n\n\nMAX_STEPS = 244\nsteps = min(max(1, len(train_paths) // BATCH_FOR_TRAIN), MAX_STEPS)\nprint(\"steps_per_epoch =\", steps)\n\nhist1 = model.fit(\n    train_ds,\n    validation_data=valid_ds,\n    epochs=4,                   \n    steps_per_epoch=steps,\n    verbose=1\n)\n\nmodel.save('/kaggle/working/srm_stage1.h5')\nprint(\" Saved Stage 1 => /kaggle/working/srm_stage1.h5\")\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================\n# CELL 7 – STAGE 2: FINE-TUNE conv5 ONLY\n# ============================\nfrom tensorflow.keras.layers import BatchNormalization\nfrom tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau, ModelCheckpoint\n\nprint(\"\\n === STAGE 2: FINE-TUNE conv5 ONLY ===\")\n\nbase = model.get_layer('resnet50')\n\nfor l in base.layers:\n    if 'conv5_' in l.name:\n        l.trainable = True\n    else:\n        l.trainable = False\n\nfor l in base.layers:\n    if isinstance(l, BatchNormalization):\n        l.trainable = False\n\nmodel.compile(\n    optimizer=tf.keras.optimizers.Adam(learning_rate=1e-5),\n    loss=tf.keras.losses.BinaryCrossentropy(label_smoothing=0.2),\n    metrics=[\n        'accuracy',\n        tf.keras.metrics.AUC(name='auc'),\n        tf.keras.metrics.Precision(name='precision'),\n        tf.keras.metrics.Recall(name='recall')\n    ]\n)\n\ncbs = [\n    EarlyStopping(monitor='val_loss', mode='min',\n                  patience=4, restore_best_weights=True),\n    ReduceLROnPlateau(monitor='val_loss', mode='min',\n                      factor=0.5, patience=2, min_lr=1e-7),\n    ModelCheckpoint('/kaggle/working/srm_resnet50_best.h5',\n                    monitor='val_loss', mode='min',\n                    save_best_only=True)\n]\n\nhist2 = model.fit(\n    train_ds,\n    validation_data=valid_ds,\n    epochs=20,\n    steps_per_epoch=steps,\n    callbacks=cbs,\n    class_weight=class_weights,\n    verbose=1\n)\n\n# 6) SAVE MODELS\nmodel.save(\"/kaggle/working/srm_resnet50_finetuned.h5\")\nprint(\"Saved FINAL => /kaggle/working/srm_resnet50_finetuned.h5\")\nprint(\"Best checkpoint => /kaggle/working/srm_resnet50_best.h5\")\n# cell 8\n# === Evaluate mô hình fine-tuned ===\nval_ds = make_ds(valid_paths, valid_labels,\n                 training=False,\n                 batch=BATCH_FOR_TRAIN)\n\neval_res = model.evaluate(val_ds, verbose=1)\nprint(\"[EVAL]\", dict(zip(model.metrics_names, eval_res)))\n\nprint(\n    f\"\\nAccuracy:  {eval_res[1]:.4f}, \"\n    f\"AUC:       {eval_res[2]:.4f}, \"\n    f\"Precision: {eval_res[3]:.4f}, \"\n    f\"Recall:    {eval_res[4]:.4f}\"\n)\n\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ============================\n# CELL 8 – TÌM THRESHOLD ƯU TIÊN PRECISION\n# ============================\nfrom sklearn.metrics import precision_score, recall_score, f1_score\nimport pandas as pd\nimport matplotlib.pyplot as plt\n\nval_ds = make_ds(valid_paths, valid_labels, training=False, batch=BATCH_FOR_TRAIN)\ny_probs = model.predict(val_ds).ravel()\ny_true  = np.array(valid_labels)\n\nTARGET_PRECISION = 0.85    # precision cao => giảm nhầm Cover\n\nbest_th = 0.5\nbest_prec = 0\n\nrecords = []\n\nfor th in np.arange(0.10, 0.91, 0.01):\n    y_pred = (y_probs >= th).astype(int)\n    prec = precision_score(y_true, y_pred, zero_division=0)\n    rec  = recall_score(y_true, y_pred)\n    f1   = f1_score(y_true, y_pred)\n    records.append((th, prec, rec, f1))\n\n    if prec > best_prec:\n        best_prec = prec\n        best_th = th\n\n# chọn threshold ưu tiên precision cao\nchosen_th = best_th\nprint(f\"🔥 Best threshold by PRECISION = {chosen_th:.2f}\")\nprint(f\"   Precision = {best_prec:.4f}\")\n\nwith open(\"/kaggle/working/best_threshold_precision.txt\",\"w\") as f:\n    f.write(str(chosen_th))\n\n# Vẽ đường Threshold vs Precision\nth_df = pd.DataFrame(records, columns=['th','precision','recall','f1'])\nplt.plot(th_df['th'], th_df['precision'])\nplt.xlabel(\"Threshold\")\nplt.ylabel(\"Precision\")\nplt.grid(True)\nplt.title(\"Precision theo Threshold (ưu tiên giảm nhầm Cover → Stego)\")\nplt.show()\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"\n# ==== SAU KHI TRAIN GIAI ĐOẠN 2 ====\nfinal_train_acc = hist2.history['accuracy'][-1]\nfinal_val_acc = hist2.history['val_accuracy'][-1]\nlast_epoch = len(hist2.history['accuracy'])\n\nprint(f\"\\nTrain Accuracy: {final_train_acc:.4f}, epoch:{last_epoch}\")\nprint(f\"Validation Accuracy: {final_val_acc:.4f}, epoch:{last_epoch}\")\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"preds = model.predict(valid_ds)\nprint(preds[:50])\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#Biểu đồ TRAIN vs VALIDATION ACCURACY\nimport matplotlib.pyplot as plt\n\nplt.figure(figsize=(8,5))\nplt.plot(hist2.history['accuracy'], label='Train Accuracy')\nplt.plot(hist2.history['val_accuracy'], label='Validation Accuracy')\nplt.title(\"Train vs Validation Accuracy (Stage 2)\")\nplt.xlabel(\"Epoch\")\nplt.ylabel(\"Accuracy\")\nplt.legend()\nplt.grid(True)\nplt.show()\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#Biểu đồ TRAIN vs VALIDATION LOSS\nplt.figure(figsize=(8,5))\nplt.plot(hist2.history['loss'], label='Train Loss')\nplt.plot(hist2.history['val_loss'], label='Validation Loss')\nplt.title(\"Train vs Validation Loss (Stage 2)\")\nplt.xlabel(\"Epoch\")\nplt.ylabel(\"Loss\")\nplt.legend()\nplt.grid(True)\nplt.show()\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#TÍNH CONFUSION MATRIX\nval_ds = make_ds(valid_paths, valid_labels,\n                 training=False,\n                 batch=BATCH_FOR_TRAIN)\n\ny_probs = model.predict(val_ds).ravel()\n\n# dùng threshold tối ưu nếu có:\ny_pred = (y_probs >= 0.6).astype(int)   # hoặc best_th nếu bạn đã tính threshold tối ưu\n\nfrom sklearn.metrics import confusion_matrix, ConfusionMatrixDisplay\n \ny_true = np.array(valid_labels)\ncm = confusion_matrix(y_true, y_pred)\ndisp = ConfusionMatrixDisplay(confusion_matrix=cm, display_labels=['Cover','Stego'])\ndisp.plot(cmap='Blues')\nplt.title(\"Confusion Matrix\")\nplt.show()\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Lấy xác suất dự đoán\nval_ds = make_ds(valid_paths, valid_labels,\n                 training=False,\n                 batch=BATCH_FOR_TRAIN)\n\ny_probs = model.predict(val_ds, verbose=1).ravel()\ny_pred = (y_probs >= 0.6).astype(int)   # threshold 0.5\ny_true = np.array(valid_labels)\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import seaborn as sns\nimport matplotlib.pyplot as plt\nfrom sklearn.metrics import confusion_matrix\n\ncm = confusion_matrix(y_true, y_pred)\n\ncm_percent = cm / cm.sum(axis=1, keepdims=True) * 100  # đổi sang %\n\nplt.figure(figsize=(6,5))\nsns.heatmap(cm_percent, annot=True, fmt=\".0f\", cmap=\"YlOrRd\",\n            xticklabels=[\"Cover\", \"Stego\"],\n            yticklabels=[\"Cover\", \"Stego\"])\nplt.title(\"Confusion Matrix for SRM-ResNet50 (%)\")\nplt.xlabel(\"Predicted Class\")\nplt.ylabel(\"True Class\")\nplt.show()\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import seaborn as sns\nimport matplotlib.pyplot as plt\nfrom sklearn.metrics import confusion_matrix\n\ncm = confusion_matrix(y_true, y_pred)\n\ncm_percent = cm / cm.sum(axis=1, keepdims=True) * 100  # đổi sang %\n\nplt.figure(figsize=(6,5))\nsns.heatmap(cm_percent, annot=True, fmt=\".0f\", cmap=\"YlOrRd\",\n            xticklabels=[\"Cover\", \"Stego\"],\n            yticklabels=[\"Cover\", \"Stego\"])\nplt.title(\"Confusion Matrix for SRM-ResNet50 (%)\")\nplt.xlabel(\"Predicted Class\")\nplt.ylabel(\"True Class\")\nplt.show()\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"tn, fp, fn, tp = confusion_matrix(y_true, y_pred).ravel()\n\nprint(\"Cover class (0):\")\nprint(\"TP:\", tn, \"FP:\", fp, \"FN:\", fn, \"TN:\", tp)\n\nprint(\"\\nStego class (1):\")\nprint(\"TP:\", tp, \"FP:\", fn, \"FN:\", fp, \"TN:\", tn)\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from sklearn.metrics import classification_report\nimport pandas as pd\n\nreport = classification_report(y_true, y_pred, target_names=[\"Cover\", \"Stego\"], output_dict=True)\ndf = pd.DataFrame(report).transpose()\n\nplt.figure(figsize=(7,5))\nsns.heatmap(df.iloc[:2, :3], annot=True, cmap=\"Reds\", fmt=\".3f\")\nplt.title(\"Classification Report for SRM-ResNet50\")\nplt.show()\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# CELL 9 – QUICK SANITY CHECK TỪNG CLASS\n# ============================\ntest_dirs = [\"Cover\", \"JMiPOD\", \"JUNIWARD\", \"UERD\"]\n\nfor d in test_dirs:\n    test_paths = sorted(glob.glob(os.path.join(root_dir, d, \"*.jpg\")))[:16]\n    if not test_paths:\n        print(f\"[WARN] Không tìm thấy ảnh trong {d}, bỏ qua.\")\n        continue\n\n    test_ds = make_ds(test_paths, None,\n                      training=False,\n                      batch=min(16, len(test_paths)))\n    probs = model.predict(test_ds, verbose=0).ravel()\n    print(f\"\\n[{d}] -> prob mean={probs.mean():.4f}, min={probs.min():.4f}, max={probs.max():.4f}\")\n    print(\" sample:\", np.round(probs[:8], 4))\n\n","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}