{"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":113558,"databundleVersionId":14174843,"sourceType":"competition"}],"dockerImageVersionId":31153,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"<div style=\"\n  text-align:center;\n  background: linear-gradient(120deg, #0f2027, #203a43, #2c5364);\n  color:#f0f4ff;\n  padding:60px 40px;\n  border-radius:18px;\n  box-shadow:0 8px 18px rgba(0,0,0,0.35);\n  line-height:1.8em;\n  font-size:18px;\n\">\n\n<h1 style=\"font-size:40px; margin-bottom:10px; color:#ffffff;\">\n🧬 Scientific Image Forgery Detection\n</h1>\n\n<p style=\"\n  font-size:20px; \n  color:#b8c9ff; \n  margin-top:10px; \n  margin-bottom:25px;\n  font-style:italic; \n  text-align:center;\n  display:block;\n  width:100%;\n\">\n“Protecting the truth in science — one pixel at a time.”\n</p>\n\n<hr style=\"width:120px; border:1px solid #b8c9ff; margin:25px auto;\">\n\n<p style=\"max-width:850px; margin:auto; color:#e5eaff;\">\nIn modern research, <b>image integrity defines scientific credibility</b>.  \nThis notebook explores <b>copy–move forgeries</b> in biomedical figures and demonstrates how  \n<b>AI-based visual forensics</b> can expose hidden manipulations that threaten data integrity.  \n<br><br>\nA fusion of <b>exploratory analysis</b> and <b>deep learning classification</b> lays the foundation  \nfor future pixel-level segmentation models designed to safeguard research authenticity.\n</p>\n\n<p style=\"margin-top:40px; font-style:italic; color:#cfd8ff; font-size:15px;\">\nDeveloped by <b>Djamila</b> | Kaggle Notebook 2025\n</p>\n</div>\n","metadata":{}},{"cell_type":"code","source":"import os\nimport pandas as pd\nimport numpy as np\nimport cv2\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom PIL import Image\nfrom pathlib import Path\nfrom scipy.ndimage import uniform_filter\n\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-07T13:55:43.105236Z","iopub.execute_input":"2025-11-07T13:55:43.105623Z","iopub.status.idle":"2025-11-07T13:55:43.999033Z","shell.execute_reply.started":"2025-11-07T13:55:43.105601Z","shell.execute_reply":"2025-11-07T13:55:43.998465Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 🧠 1. Dataset Overview\nBefore diving into forgery characteristics, let's explore the overall dataset structure —  \nincluding class balance and image dimensions.","metadata":{}},{"cell_type":"code","source":"\nbase_dir = \"/kaggle/input/recodai-luc-scientific-image-forgery-detection\"\ntrain_auth = os.listdir(f\"{base_dir}/train_images/authentic\")\ntrain_forg = os.listdir(f\"{base_dir}/train_images/forged\")\n\nplt.bar([\"Authentic\", \"Forged\"], [len(train_auth), len(train_forg)], color=[\"green\", \"red\"])\nplt.title(\"Distribution des images : authentiques vs falsifiées\")\nplt.ylabel(\"Nombre d'images\")\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-07T13:55:46.537241Z","iopub.execute_input":"2025-11-07T13:55:46.537932Z","iopub.status.idle":"2025-11-07T13:55:46.650173Z","shell.execute_reply.started":"2025-11-07T13:55:46.537910Z","shell.execute_reply":"2025-11-07T13:55:46.649526Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Display 3 examples of authentic, forged images and their corresponding masks\nplt.figure(figsize=(15,10))\n\nfor i in range(3):\n    # Load one authentic image\n    img_auth = Image.open(os.path.join(base_dir, \"train_images/authentic\", train_auth[i]))\n    \n    # Load one forged image and its mask\n    img_forg = Image.open(os.path.join(base_dir, \"train_images/forged\", train_forg[i]))\n    mask_forg = np.load(os.path.join(base_dir, \"train_masks\", train_forg[i].split('.')[0] + \".npy\"))\n    \n    # Handle multiple masks (stacked along depth)\n    if mask_forg.ndim == 3:\n        mask_forg = np.max(mask_forg, axis=0)\n    \n    # --- Display ---\n    plt.subplot(3,3,3*i + 1)\n    plt.imshow(img_auth)\n    plt.title(f\"Authentic #{i+1}\")\n    plt.axis(\"off\")\n    \n    plt.subplot(3,3,3*i + 2)\n    plt.imshow(img_forg)\n    plt.title(f\"Forged #{i+1}\")\n    plt.axis(\"off\")\n    \n    plt.subplot(3,3,3*i + 3)\n    plt.imshow(mask_forg, cmap=\"Reds\")\n    plt.title(f\"Mask #{i+1} (Tampered)\")\n    plt.axis(\"off\")\n\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-27T03:14:15.693753Z","iopub.execute_input":"2025-10-27T03:14:15.694301Z","iopub.status.idle":"2025-10-27T03:14:16.798627Z","shell.execute_reply.started":"2025-10-27T03:14:15.694276Z","shell.execute_reply":"2025-10-27T03:14:16.797831Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Image dimensions (width vs height)\ndims_auth, dims_forg = [], []\nfor folder, dims_list in [(\"authentic\", dims_auth), (\"forged\", dims_forg)]:\n    for f in os.listdir(f\"{base_dir}/train_images/{folder}\")[:300]:\n        img = Image.open(os.path.join(base_dir, \"train_images\", folder, f))\n        dims_list.append(img.size)\n\nauth_df = np.array(dims_auth)\nforg_df = np.array(dims_forg)\n\nplt.figure(figsize=(10,5))\nsns.scatterplot(x=auth_df[:,0], y=auth_df[:,1], label=\"Authentic\", color=\"green\")\nsns.scatterplot(x=forg_df[:,0], y=forg_df[:,1], label=\"Forged\", color=\"red\")\nplt.title(\"Image Dimensions — Width vs Height\")\nplt.xlabel(\"Width (px)\")\nplt.ylabel(\"Height (px)\")\nplt.legend()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-27T02:53:53.397851Z","iopub.execute_input":"2025-10-27T02:53:53.398146Z","iopub.status.idle":"2025-10-27T02:53:54.187872Z","shell.execute_reply.started":"2025-10-27T02:53:53.398125Z","shell.execute_reply":"2025-10-27T02:53:54.187051Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 🔬 2. Mask-Level Statistics\nWe now analyze the **tampered regions** themselves — their size, proportion,  \nand frequency per image — to assess how subtle or extensive manipulations are.\n","metadata":{}},{"cell_type":"code","source":"# Define the mask file list\nmask_files = os.listdir(f\"{base_dir}/train_masks\")\n\n# Forged region size (log scale)\nmask_areas = []\nfor m in mask_files:\n    mask = np.load(os.path.join(base_dir, \"train_masks\", m))\n    mask = np.max(mask, axis=0) if mask.ndim == 3 else mask\n    mask_areas.append(mask.sum())\n\nplt.figure(figsize=(8,4))\nsns.histplot(mask_areas, bins=50, log_scale=True, color=\"crimson\")\nplt.title(\"Forged Region Size Distribution (Log Scale)\")\nplt.xlabel(\"Number of Forged Pixels\")\nplt.ylabel(\"Number of Images\")\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-27T02:55:34.356324Z","iopub.execute_input":"2025-10-27T02:55:34.356797Z","iopub.status.idle":"2025-10-27T02:57:03.999274Z","shell.execute_reply.started":"2025-10-27T02:55:34.356772Z","shell.execute_reply":"2025-10-27T02:57:03.998569Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Percentage of tampered pixels per image\nmask_percent = []\nfor m in mask_files:\n    mask = np.load(os.path.join(base_dir, \"train_masks\", m))\n    mask = np.max(mask, axis=0) if mask.ndim == 3 else mask\n    area = mask.sum()\n    h, w = mask.shape\n    mask_percent.append(area / (h*w))\n\nplt.figure(figsize=(7,4))\nsns.histplot(mask_percent, bins=30, color='orange')\nplt.title(\"Percentage of Tampered Area per Image\")\nplt.xlabel(\"Proportion of Forged Pixels (%)\")\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-27T02:57:44.671634Z","iopub.execute_input":"2025-10-27T02:57:44.672321Z","iopub.status.idle":"2025-10-27T02:57:51.325505Z","shell.execute_reply.started":"2025-10-27T02:57:44.672296Z","shell.execute_reply":"2025-10-27T02:57:51.324627Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 🌍 3. Spatial Patterns\nWhere do falsifications occur most often?  \nThese visualizations highlight **global and local spatial trends**.\n","metadata":{}},{"cell_type":"code","source":"# Global average heatmap of tampered areas\nheatmap = np.zeros((512,512))\nfor m in mask_files:\n    mask = np.load(os.path.join(base_dir, \"train_masks\", m))\n    mask = np.max(mask, axis=0) if mask.ndim == 3 else mask\n    mask = np.array(Image.fromarray(mask).resize((512,512)))\n    heatmap += mask\n\nplt.imshow(heatmap/len(mask_files), cmap=\"hot\")\nplt.title(\"Global Heatmap of Forged Regions\")\nplt.colorbar(label=\"Tampering Frequency\")\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-27T02:57:55.816642Z","iopub.execute_input":"2025-10-27T02:57:55.817208Z","iopub.status.idle":"2025-10-27T02:58:16.273388Z","shell.execute_reply.started":"2025-10-27T02:57:55.817185Z","shell.execute_reply":"2025-10-27T02:58:16.272671Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Center positions of forged regions\ncenters_x, centers_y = [], []\nfor m in mask_files:\n    mask = np.load(os.path.join(base_dir, \"train_masks\", m))\n    mask = np.max(mask, axis=0) if mask.ndim == 3 else mask\n    y, x = np.where(mask > 0)\n    if len(x) > 0:\n        centers_x.append(np.mean(x))\n        centers_y.append(np.mean(y))\n\nplt.figure(figsize=(6,6))\nplt.hexbin(centers_x, centers_y, gridsize=50, cmap='inferno')\nplt.title(\"Spatial Distribution of Tampered Region Centers\")\nplt.xlabel(\"x (width)\")\nplt.ylabel(\"y (height)\")\nplt.colorbar(label=\"Frequency\")\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-27T02:58:20.606380Z","iopub.execute_input":"2025-10-27T02:58:20.607053Z","iopub.status.idle":"2025-10-27T02:58:37.347408Z","shell.execute_reply.started":"2025-10-27T02:58:20.607031Z","shell.execute_reply":"2025-10-27T02:58:37.346642Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 🎨 4. Geometric and Visual Characteristics\nWe now analyze the **shape** and **brightness** of the forged areas  \nto understand if visual properties differ between authentic and manipulated figures.\n","metadata":{}},{"cell_type":"code","source":"# Width/height ratio of forged areas\nratios = []\nfor m in mask_files:\n    mask = np.load(os.path.join(base_dir, \"train_masks\", m))\n    mask = np.max(mask, axis=0) if mask.ndim == 3 else mask\n    y, x = np.where(mask > 0)\n    if len(x) > 0:\n        h = y.max() - y.min() + 1\n        w = x.max() - x.min() + 1\n        ratios.append(w/h)\n\nplt.figure(figsize=(7,4))\nsns.histplot(ratios, bins=40, color=\"teal\")\nplt.title(\"Width-to-Height Ratio of Tampered Regions\")\nplt.xlabel(\"Width / Height Ratio\")\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-27T02:58:54.571486Z","iopub.execute_input":"2025-10-27T02:58:54.572199Z","iopub.status.idle":"2025-10-27T02:59:12.303526Z","shell.execute_reply.started":"2025-10-27T02:58:54.572174Z","shell.execute_reply":"2025-10-27T02:59:12.302867Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Average brightness (luminance) comparison\ndef get_luminance(img_path):\n    img = Image.open(img_path).convert(\"L\")\n    return np.mean(img), np.std(img)\n\nauth_means, auth_stds = zip(*[get_luminance(os.path.join(base_dir, \"train_images/authentic\", f))\n                              for f in os.listdir(f\"{base_dir}/train_images/authentic\")[:300]])\n\nforg_means, forg_stds = zip(*[get_luminance(os.path.join(base_dir, \"train_images/forged\", f))\n                              for f in os.listdir(f\"{base_dir}/train_images/forged\")[:300]])\n\nplt.figure(figsize=(6,5))\nsns.kdeplot(auth_means, label=\"Authentic\", color=\"green\")\nsns.kdeplot(forg_means, label=\"Forged\", color=\"red\")\nplt.title(\"Brightness Distribution — Authentic vs Forged\")\nplt.xlabel(\"Average Brightness\")\nplt.legend()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-27T03:03:56.205155Z","iopub.execute_input":"2025-10-27T03:03:56.205471Z","iopub.status.idle":"2025-10-27T03:04:16.724162Z","shell.execute_reply.started":"2025-10-27T03:03:56.205451Z","shell.execute_reply":"2025-10-27T03:04:16.723407Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 🔗 5. Cross-Feature Relationship\nFinally, let's check whether **brightness** correlates with **forgery size** —  \nto see if exposure or tone impacts manipulation scale.\n","metadata":{}},{"cell_type":"code","source":"from PIL import Image\nimport numpy as np\nimport os\n\ndef get_luminance(img_path):\n    img = Image.open(img_path).convert(\"L\")\n    return np.mean(img), np.std(img)\n\n# Average brightness for authentic and forged images\nauth_means, _ = zip(*[get_luminance(os.path.join(base_dir, \"train_images/authentic\", f))\n                      for f in os.listdir(f\"{base_dir}/train_images/authentic\")[:300]])\n\nforg_means, _ = zip(*[get_luminance(os.path.join(base_dir, \"train_images/forged\", f))\n                      for f in os.listdir(f\"{base_dir}/train_images/forged\")[:300]])\n\ndf_patterns = pd.DataFrame({\n    \"mask_area\": mask_areas[:len(forg_means)],\n    \"luminosity\": forg_means[:len(mask_areas)]\n})\n\nsns.scatterplot(x=\"luminosity\", y=\"mask_area\", data=df_patterns)\nplt.title(\"Brightness vs Forged Area Size\")\nplt.xlabel(\"Average Brightness\")\nplt.ylabel(\"Forged Area (pixels)\")\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-27T03:01:00.003793Z","iopub.execute_input":"2025-10-27T03:01:00.004299Z","iopub.status.idle":"2025-10-27T03:01:20.475620Z","shell.execute_reply.started":"2025-10-27T03:01:00.004276Z","shell.execute_reply":"2025-10-27T03:01:20.474898Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"mask_sizes = []\nmask_files = os.listdir(f\"{base_dir}/train_masks\")\n\nfor m in mask_files[:1000]:\n    mask = np.load(os.path.join(base_dir, \"train_masks\", m))\n    mask_sizes.append(mask.sum())\n\nplt.figure(figsize=(8,4))\nsns.histplot(mask_sizes, bins=40, log_scale=True, color='crimson')\nplt.title(\"Distribution de la taille des zones falsifiées (en pixels)\")\nplt.xlabel(\"Surface falsifiée (échelle log)\")\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-27T02:00:03.979754Z","iopub.execute_input":"2025-10-27T02:00:03.980087Z","iopub.status.idle":"2025-10-27T02:00:23.455124Z","shell.execute_reply.started":"2025-10-27T02:00:03.980063Z","shell.execute_reply":"2025-10-27T02:00:23.454165Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## 🔍 Pattern 6 — Directional Inconsistency Analysis (DIC Map)\n\nThis pattern focuses on **detecting subtle structural inconsistencies** in forged images by analyzing the **local gradient orientation coherence**.  \nIn authentic images, neighboring pixels usually follow smooth and consistent gradient directions, while in manipulated areas, the edges often display **abrupt directional changes** or unnatural texture transitions.  \nBy computing a *Directional Inconsistency Coefficient (DIC)* using Sobel or Scharr filters, we can visualize and quantify the variance of local edge orientations — highlighting zones that deviate from natural image statistics.\n","metadata":{}},{"cell_type":"code","source":"\nfrom pathlib import Path\nimport cv2\nfrom scipy.ndimage import uniform_filter\n\n# --- Fonction principale ---\ndef directional_incoherence(img_path, window_size=9):\n    img = cv2.imread(img_path)\n    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)\n    gray = cv2.GaussianBlur(gray, (3,3), 0)\n\n    gx = cv2.Sobel(gray, cv2.CV_32F, 1, 0, ksize=3)\n    gy = cv2.Sobel(gray, cv2.CV_32F, 0, 1, ksize=3)\n\n    theta = np.arctan2(gy, gx)\n    mean_theta = uniform_filter(theta, size=window_size)\n    mean_theta2 = uniform_filter(theta**2, size=window_size)\n    var_theta = mean_theta2 - mean_theta**2\n\n    dic = cv2.normalize(var_theta, None, 0, 1, cv2.NORM_MINMAX)\n    return dic\n\n\n# --- Exemple visuel ---\nauth_dir = f\"{base_dir}/train_images/authentic\"\nforg_dir = f\"{base_dir}/train_images/forged\"\n\nauth_example = Path(auth_dir) / train_auth[0]\nforg_example = Path(forg_dir) / train_forg[0]\n\ndic_auth = directional_incoherence(str(auth_example))\ndic_forg = directional_incoherence(str(forg_example))\n\nplt.figure(figsize=(12,6))\nplt.subplot(2,2,1); plt.imshow(cv2.imread(str(auth_example))[:,:,::-1]); plt.title(\"Authentic - Image\"); plt.axis(\"off\")\nplt.subplot(2,2,2); plt.imshow(dic_auth, cmap=\"inferno\"); plt.title(\"Authentic - DIC map\"); plt.axis(\"off\")\n\nplt.subplot(2,2,3); plt.imshow(cv2.imread(str(forg_example))[:,:,::-1]); plt.title(\"Forged - Image\"); plt.axis(\"off\")\nplt.subplot(2,2,4); plt.imshow(dic_forg, cmap=\"inferno\"); plt.title(\"Forged - DIC map\"); plt.axis(\"off\")\nplt.show()\n\n\n# --- Statistiques globales ---\ndef compute_dic_stats(img_folder, file_list, n=20):\n    vals = []\n    for f in file_list[:n]:\n        try:\n            dic = directional_incoherence(os.path.join(img_folder, f))\n            vals.append(dic.mean())\n        except Exception as e:\n            print(f\"Erreur sur {f}: {e}\")\n            continue\n    return np.mean(vals), np.std(vals)\n\nmean_auth, std_auth = compute_dic_stats(auth_dir, train_auth)\nmean_forg, std_forg = compute_dic_stats(forg_dir, train_forg)\n\nprint(f\"Authentic → DIC moyen: {mean_auth:.4f} ± {std_auth:.4f}\")\nprint(f\"Forged    → DIC moyen: {mean_forg:.4f} ± {std_forg:.4f}\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-11-07T13:55:58.380967Z","iopub.execute_input":"2025-11-07T13:55:58.381467Z","iopub.status.idle":"2025-11-07T13:56:02.838090Z","shell.execute_reply.started":"2025-11-07T13:55:58.381446Z","shell.execute_reply":"2025-11-07T13:56:02.837406Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 🧠 Insight\n\nThe DIC maps reveal the **spatial coherence of edges** across the image.  \nAuthentic regions typically exhibit uniform orientation patterns (low variance), while forged areas show **disrupted gradient flows** or **high-frequency anomalies**.  \nAlthough global averages may appear similar, analyzing local DIC variance or combining the DIC maps with deep feature embeddings (e.g., DINOv2 or CNN outputs) can significantly enhance the detection of **localized manipulations** such as copy-move or splicing.\n","metadata":{}},{"cell_type":"markdown","source":"## 🤖 Baseline Deep Learning Model — Authentic vs Forged Classification\nWe train a ResNet-18 model using transfer learning to distinguish between authentic and manipulated scientific images.  \nThis first baseline provides a strong foundation before moving to pixel-level segmentation.\n","metadata":{}},{"cell_type":"code","source":"import os\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import models, transforms\nfrom PIL import Image\nfrom sklearn.model_selection import train_test_split\n\nbase_dir = \"/kaggle/input/recodai-luc-scientific-image-forgery-detection\"\n\ndata = []\nfor folder, label in [(\"authentic\", 0), (\"forged\", 1)]:\n    folder_path = os.path.join(base_dir, \"train_images\", folder)\n    for f in os.listdir(folder_path):\n        if f.lower().endswith((\".png\", \".jpg\", \".jpeg\", \".tif\")):\n            data.append((os.path.join(folder_path, f), label))\n\ntrain_data, val_data = train_test_split(data, test_size=0.2, stratify=[d[1] for d in data], random_state=42)\n\nclass ForgeryDataset(Dataset):\n    def __init__(self, data, transform=None):\n        self.data = data\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.data)\n\n    def __getitem__(self, idx):\n        path, label = self.data[idx]\n        img = Image.open(path).convert(\"RGB\")\n        if self.transform:\n            img = self.transform(img)\n        return img, torch.tensor(label, dtype=torch.long)\n\ntransform_train = transforms.Compose([\n    transforms.Resize((224, 224)),\n    transforms.RandomHorizontalFlip(),\n    transforms.RandomRotation(10),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n])\n\ntransform_val = transforms.Compose([\n    transforms.Resize((224, 224)),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n])\n\ntrain_ds = ForgeryDataset(train_data, transform=transform_train)\nval_ds = ForgeryDataset(val_data, transform=transform_val)\n\ntrain_loader = DataLoader(train_ds, batch_size=16, shuffle=True)\nval_loader = DataLoader(val_ds, batch_size=16, shuffle=False)\n\nmodel = models.resnet18(pretrained=True)\nmodel.fc = nn.Linear(model.fc.in_features, 2)  # 2 classes\n\ndevice = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nmodel = model.to(device)\n\ncriterion = nn.CrossEntropyLoss()\noptimizer = optim.Adam(model.parameters(), lr=1e-4)\n\nEPOCHS = 10\n\nfor epoch in range(EPOCHS):\n    model.train()\n    total_loss = 0\n    for imgs, labels in train_loader:\n        imgs, labels = imgs.to(device), labels.to(device)\n        optimizer.zero_grad()\n        outputs = model(imgs)\n        loss = criterion(outputs, labels)\n        loss.backward()\n        optimizer.step()\n        total_loss += loss.item()\n    print(f\"Epoch {epoch+1}/{EPOCHS} - Loss: {total_loss/len(train_loader):.4f}\")\n\nmodel.eval()\ncorrect, total = 0, 0\nwith torch.no_grad():\n    for imgs, labels in val_loader:\n        imgs, labels = imgs.to(device), labels.to(device)\n        outputs = model(imgs)\n        preds = outputs.argmax(dim=1)\n        correct += (preds == labels).sum().item()\n        total += labels.size(0)\n\nprint(f\"Validation accuracy: {100 * correct / total:.2f}%\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-27T02:31:59.907032Z","iopub.execute_input":"2025-10-27T02:31:59.907302Z","iopub.status.idle":"2025-10-27T02:51:00.574445Z","shell.execute_reply.started":"2025-10-27T02:31:59.907281Z","shell.execute_reply":"2025-10-27T02:51:00.573575Z"}},"outputs":[],"execution_count":null}]}