{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.10.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":19991,"databundleVersionId":1117522,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":11436705,"sourceType":"datasetVersion","datasetId":7163714}],"dockerImageVersionId":30919,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true},"colab":{"provenance":[]}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# Tên Nhóm: 6\n\n**Bảng Phân Công**\n\n| Thành viên | MSSV     | Nhiệm vụ                                                        |\n|------------|----------|------------------------------------------------------------------|\n| Nguyễn Nhật Tiến       | 21127181 | Tìm hiểu bài toán và huấn luyện mô hình, viết báo cáo             |\n| Nguyễn Anh Thư        | 21127177 | Tìm hiểu bài toán và huấn luyện mô hình, quay video               |\n","metadata":{}},{"cell_type":"markdown","source":"# Đồ Án Phát Hiện Ảnh Giấu Tin (Image Steganalysis)\n\n## 1. Giới thiệu chung\n\nĐồ án này tập trung vào bài toán phát hiện ảnh giấu tin (Image Steganalysis) sử dụng các kỹ thuật học sâu (Deep Learning). Steganalysis là quá trình phát hiện sự hiện diện của thông tin được giấu trong các phương tiện số như hình ảnh.\n\n**Bộ dữ liệu**: ALASKA2 - bộ dữ liệu chuẩn gồm:\n- Ảnh Cover (ảnh gốc không giấu tin)\n- 3 phương pháp giấu tin: \n  - JMiPOD\n  - JUNIWARD \n  - UERD\n\n**Mục tiêu**:\n1. Phân loại ảnh Cover/Stego\n2. Xác định phương pháp giấu tin nếu là ảnh Stego\n\n","metadata":{}},{"cell_type":"markdown","source":"## 2. Bài toán chi tiết\n\n### Đặc điểm bài toán\n- Bài toán phân loại 4 lớp\n- Input: Ảnh độ phân giải cao\n- Output: \n  - 0: Cover\n  - 1: JMiPOD\n  - 2: JUNIWARD\n  - 3: UERD\n\n### Thách thức\n- Cân bằng giữa độ chính xác và thời gian xử lý\n- Xử lý ảnh có kích thước lớn\n- Phân biệt các phương pháp giấu tin tinh vi","metadata":{}},{"cell_type":"markdown","source":"## 3. Giải pháp kỹ thuật\n\n### a. Kiến trúc mô hình:\n\n```python\ndef get_model():\n    model = EfficientNet.from_pretrained(\"efficientnet-b2\")\n    model._fc = nn.Linear(in_features=1408, out_features=4, bias=True)\n    return model\n```\n\n- Sử dụng EfficientNet-B2 đã được pretrain trên ImageNet\n\n- Thay thế lớp fully connected cuối cùng để phù hợp với bài toán 4 lớp\n\n### b. Xử lý mất cân bằng lớp:\n\n```python\nclass BalanceClassSampler(Sampler):\n    def __init__(self, labels, mode=\"downsampling\"):\n```\n- Sử dụng custom sampler để cân bằng số lượng mẫu giữa các lớp\n\n- Hỗ trợ cả downsampling và upsampling\n### c. Hàm loss:\n\n```python\nclass LabelSmoothing(nn.Module):\n    def __init__(self, smoothing=0.05):\n```\n- Sử dụng label smoothing để tránh overfitting\n\n- Kết hợp giữa cross-entropy loss và smoothing regularization\n### d. Đánh giá hiệu năng:\n```python\ndef alaska_weighted_auc(y_true, y_valid):\n    # Tính toán weighted AUC theo tiêu chí của cuộc thi\n```\n- Sử dụng weighted AUC làm metric chính\n\n- Các vùng khác nhau của curve ROC được đánh trọng số khác nhau\n\n### e. Quá trình huấn luyện:\n```python\nclass AlaskaLearner:\n    def fit(self, train_loader, valid_loader):\n```\n- Hỗ trợ mixed precision training để tăng tốc độ\n\n- Sử dụng OneCycleLR scheduler để tối ưu hóa quá trình học\n\n- Theo dõi và log chi tiết quá trình huấn luyện\n\n### f. Kỹ thuật tăng cường:\n- Cross-validation với 5 folds\n\n- Tự động tạo folds từ thư mục dữ liệu gốc\n\n- Hỗ trợ resume training từ checkpoint","metadata":{}},{"cell_type":"markdown","source":"## 4. Phân tích dataset\n\n### Số lượng ảnh trong mỗi folder","metadata":{}},{"cell_type":"code","source":"import os\n\ndataset_path = \"/kaggle/input/alaska2-image-steganalysis\"\n\ncover_path = os.path.join(dataset_path, \"Cover\")\njmp_path = os.path.join(dataset_path, \"JMiPOD\")\njnw_path = os.path.join(dataset_path, \"JUNIWARD\")\nuerd_path = os.path.join(dataset_path, \"UERD\")\ntest_path = os.path.join(dataset_path, \"Test\")\n\nnum_cover = len(os.listdir(cover_path))\nnum_jmp = len(os.listdir(jmp_path))\nnum_jnw = len(os.listdir(jnw_path))\nnum_uerd = len(os.listdir(uerd_path))\nnum_test = len(os.listdir(test_path))\n\nprint(f\"Cover images: {num_cover}\")\nprint(f\"JMiPOD images: {num_jmp}\")\nprint(f\"JUNIWARD images: {num_jnw}\")\nprint(f\"UERD images: {num_uerd}\")\nprint(f\"Test images: {num_test}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-16T05:15:07.158580Z","iopub.execute_input":"2025-04-16T05:15:07.158961Z","iopub.status.idle":"2025-04-16T05:15:08.760507Z","shell.execute_reply.started":"2025-04-16T05:15:07.158931Z","shell.execute_reply":"2025-04-16T05:15:08.759582Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Các sample trong mỗi folder","metadata":{}},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nimport cv2\nimport random\n\n# Function to display images\ndef show_images(image_folder, num_samples=5):\n    image_files = sorted(os.listdir(image_folder))[:num_samples]  # Get first 5 images\n\n    plt.figure(figsize=(15, 5))\n    for i, img_name in enumerate(image_files):\n        img_path = os.path.join(image_folder, img_name)\n        img = cv2.imread(img_path)  # Read image\n        img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)  # Convert to RGB\n\n        plt.subplot(1, num_samples, i + 1)\n        plt.imshow(img)\n        plt.title(img_name)\n        plt.axis(\"off\")\n\n    plt.show()\n\n# Display cover images\nprint(\"Cover Images:\")\nshow_images(cover_path)\n\n# Display stego images\nprint(\"Stego Images:\")\nshow_images(jmp_path)\nshow_images(jnw_path)\nshow_images(uerd_path)\n\n# Display test images\nprint(\"Test Images:\")\nshow_images(test_path)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-16T06:21:20.675318Z","iopub.execute_input":"2025-04-16T06:21:20.675650Z","iopub.status.idle":"2025-04-16T06:21:25.907767Z","shell.execute_reply.started":"2025-04-16T06:21:20.675621Z","shell.execute_reply":"2025-04-16T06:21:25.906770Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Kiểm tra Image Properties","metadata":{}},{"cell_type":"code","source":"import numpy as np\nfrom PIL import Image\n\n# Load a sample image\nsample_image_path = os.path.join(cover_path, os.listdir(cover_path)[0])\nimage = Image.open(sample_image_path)\n\n# Image properties\nprint(f\"Image format: {image.format}\")\nprint(f\"Image size (width x height): {image.size}\")\nprint(f\"Color mode: {image.mode}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-16T06:22:19.625388Z","iopub.execute_input":"2025-04-16T06:22:19.625676Z","iopub.status.idle":"2025-04-16T06:22:19.859509Z","shell.execute_reply.started":"2025-04-16T06:22:19.625654Z","shell.execute_reply":"2025-04-16T06:22:19.858320Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Histogram của sample 0 trong các folder (trừ test):","metadata":{}},{"cell_type":"code","source":"def plot_histogram(image_path, title):\n    img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)  # Convert to grayscale\n    plt.hist(img.ravel(), bins=256, color='blue', alpha=0.7, label='Pixel Values')\n    plt.title(title)\n    plt.xlabel('Pixel Intensity')\n    plt.ylabel('Frequency')\n    plt.show()\n\n# Choose a sample image from each category\ncover_sample = os.path.join(cover_path, os.listdir(cover_path)[0])\njmp_sample = os.path.join(jmp_path, os.listdir(jmp_path)[0])\njnw_sample = os.path.join(jnw_path, os.listdir(jnw_path)[0])\nuerd_sample = os.path.join(uerd_path, os.listdir(uerd_path)[0])\n\nplot_histogram(cover_sample, \"Histogram of Cover Image\")\nplot_histogram(jmp_sample, \"Histogram of JMiPOD Image\")\nplot_histogram(jnw_sample, \"Histogram of JUNIWARD Image\")\nplot_histogram(uerd_sample, \"Histogram of UERD Image\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-16T06:23:11.214688Z","iopub.execute_input":"2025-04-16T06:23:11.215013Z","iopub.status.idle":"2025-04-16T06:23:15.476865Z","shell.execute_reply.started":"2025-04-16T06:23:11.214990Z","shell.execute_reply":"2025-04-16T06:23:15.475921Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Nhìn vào histogram ta thấy sự khác biệt giữa các ảnh (trước và sau khi dùng các phương pháp ẩn dữ liệu) là không rõ ràng, khiến cho các phương pháp giấu tin này khó phát hiện qua mắt thường","metadata":{}},{"cell_type":"code","source":"from skimage.feature import graycomatrix, graycoprops\nfrom skimage.io import imread\nfrom skimage.color import rgb2gray\nfrom scipy.stats import entropy\nimport numpy as np\n\n# Feature extraction function\ndef extract_features(image_path):\n    img = imread(image_path)\n    gray_img = rgb2gray(img)\n\n    # Mean and Standard Deviation\n    mean_intensity = np.mean(gray_img)\n    std_intensity = np.std(gray_img)\n\n    # Entropy\n    hist, _ = np.histogram(gray_img.ravel(), bins=256, density=True)\n    img_entropy = entropy(hist)\n\n    return mean_intensity, std_intensity, img_entropy\n\n# Extract features for cover and stego images\ncover_features = extract_features(cover_sample)\njmp_features = extract_features(jmp_sample)\njnw_features = extract_features(jnw_sample)\nuerd_features = extract_features(uerd_sample)\n\nprint(\"Cover Image Features (Mean, Std, Entropy):\", cover_features)\nprint(\"JMiPOD Image Features (Mean, Std, Entropy):\", jmp_features)\nprint(\"JUNIWARD Image Features (Mean, Std, Entropy):\", jnw_features)\nprint(\"UERD Image Features (Mean, Std, Entropy):\", uerd_features)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-16T06:38:17.725406Z","iopub.execute_input":"2025-04-16T06:38:17.725789Z","iopub.status.idle":"2025-04-16T06:38:18.538795Z","shell.execute_reply.started":"2025-04-16T06:38:17.725759Z","shell.execute_reply":"2025-04-16T06:38:18.537969Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Phân tích Image Features (Mean, Std, Entropy)\n\n#### 1. Mean Intensity (Brightness)\n- **Cover Image:** `0.311339`\n- **JMiPOD:** `0.311329`\n- **JUNIWARD:** `0.311326`\n- **UERD:** `0.311348`\n\n**Nhận xét:**  \nGiá trị trung bình đại diện cho độ sáng trung bình của ảnh (chuẩn hóa trong khoảng [0,1]). Các giá trị gần như giống hệt nhau cho thấy việc nhúng tin không làm thay đổi đáng kể độ sáng tổng thể của ảnh.\n\n---\n\n#### 2. Standard Deviation (Contrast)\n- **Cover Image:** `0.178886`\n- **JMiPOD:** `0.178917`\n- **JUNIWARD:** `0.178895`\n- **UERD:** `0.178906`\n\n**Nhận xét:**  \nĐộ lệch chuẩn đo lường mức độ phân tán của cường độ điểm ảnh (độ tương phản). Sự khác biệt nhỏ cho thấy các thuật toán giấu tin được thiết kế để **không gây ra thay đổi có thể nhận biết bằng mắt thường** trong khi vẫn duy trì độ tương phản gốc.\n\n---\n\n#### 3. Entropy (Information Content)\n- **Cover Image:** `5.1324`\n- **JMiPOD:** `5.1247`\n- **JUNIWARD:** `5.1291`\n- **UERD:** `5.1189`\n\n**Nhận xét:**  \nEntropy đo lường tính **ngẫu nhiên hoặc độ phức tạp** của giá trị điểm ảnh. Ảnh stego có **entropy thấp hơn** một chút so với ảnh cover, cho thấy quá trình nhúng tin làm **giảm tính ngẫu nhiên**, có thể do các thay đổi có cấu trúc được tạo ra bởi thuật toán giấu tin.","metadata":{}},{"cell_type":"markdown","source":"## 5. Quá trình xử lí dataset và huấn luyện mô hình","metadata":{}},{"cell_type":"markdown","source":"### a. Cài đặt các thư viện cần thiết","metadata":{}},{"cell_type":"code","source":"!pip install torch torchvision efficientnet-pytorch numpy pandas scikit-learn opencv-python tqdm","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-07T14:15:56.942555Z","iopub.execute_input":"2025-04-07T14:15:56.942864Z","iopub.status.idle":"2025-04-07T14:16:00.420759Z","shell.execute_reply.started":"2025-04-07T14:15:56.942841Z","shell.execute_reply":"2025-04-07T14:16:00.419672Z"},"id":"vr5FlLHk1nod","outputId":"68b8171d-d463-4b27-94f2-14a4efec8124"},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader, Sampler, SequentialSampler\nfrom torch.optim import AdamW\nfrom torch.optim.lr_scheduler import OneCycleLR\nfrom efficientnet_pytorch import EfficientNet\nimport numpy as np\nimport cv2\nimport pandas as pd\nfrom sklearn.model_selection import GroupKFold\nfrom sklearn import metrics\nimport os\nimport glob\nimport random\nimport time\nfrom datetime import datetime\nimport datetime\nimport warnings\n\nwarnings.filterwarnings(\"ignore\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-16T14:18:53.541446Z","iopub.execute_input":"2025-04-16T14:18:53.541768Z","iopub.status.idle":"2025-04-16T14:18:57.012778Z","shell.execute_reply.started":"2025-04-16T14:18:53.541743Z","shell.execute_reply":"2025-04-16T14:18:57.011626Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### b. Configuration","metadata":{}},{"cell_type":"code","source":"class TrainGlobalConfig:\n    # Removed csv_file reference since we'll generate folds\n    fold_number = 0\n    num_workers = 4\n    batch_size = 16\n    n_epochs = 10\n    lr = 2e-4\n    seed = 42\n\n    verbose = True\n    verbose_step = 1\n\n    step_scheduler = True\n    valid_scheduler = False\n\n    SchedulerClass = OneCycleLR\n    scheduler_params = dict(\n        max_lr=lr,\n        epochs=n_epochs,\n        steps_per_epoch=None,\n        pct_start=0.1,\n        anneal_strategy=\"cos\",\n        cycle_momentum=True,\n        div_factor=10.0,\n    )\n    def __init__(self):\n        # Calculate steps_per_epoch after dataloader is created\n        self.steps_per_epoch = None\n\n    @property\n    def scheduler_params(self):\n        params = dict(\n            max_lr=self.lr,\n            epochs=self.n_epochs,\n            steps_per_epoch=self.steps_per_epoch,  # Will be set later\n            pct_start=0.1,\n            anneal_strategy=\"cos\",\n            cycle_momentum=True,\n            div_factor=10.0,\n        )\n        return params","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### c. Blance Class Sampler","metadata":{}},{"cell_type":"markdown","source":"Lớp `BalanceClassSampler` thực hiện cân bằng dữ liệu giữa các lớp bằng 2 phương pháp:\n1. **Downsampling**: Giảm số lượng mẫu của lớp nhiều về bằng lớp ít nhất\n2. **Upsampling**: Tăng số lượng mẫu của lớp ít lên bằng lớp nhiều nhất","metadata":{}},{"cell_type":"code","source":"class BalanceClassSampler(Sampler):\n    def __init__(self, labels, mode=\"downsampling\"):\n        \"\"\"\n        Args:\n            labels: array of class labels\n            mode: \"downsampling\" or \"upsampling\"\n        \"\"\"\n        self.labels = np.array(labels)\n        self.mode = mode\n\n        # Get class counts\n        unique_labels, counts = np.unique(self.labels, return_counts=True)\n        self.class_counts = dict(zip(unique_labels, counts))\n\n        # Determine sampling counts\n        if mode == \"downsampling\":\n            self.sample_count = min(counts)\n        elif mode == \"upsampling\":\n            self.sample_count = max(counts)\n        else:\n            raise ValueError(f\"Unsupported mode: {mode}\")\n\n        # Generate indices\n        self.indices = self._generate_indices()\n\n    def _generate_indices(self):\n        indices = []\n        for label, count in self.class_counts.items():\n            label_indices = np.where(self.labels == label)[0]\n            if self.mode == \"downsampling\":\n                selected = np.random.choice(label_indices, self.sample_count, replace=False)\n            else:  # upsampling\n                selected = np.random.choice(label_indices, self.sample_count, replace=True)\n            indices.extend(selected)\n\n        np.random.shuffle(indices)\n        return indices\n\n    def __iter__(self):\n        return iter(self.indices)\n\n    def __len__(self):\n        return len(self.indices)\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### d. Xử lí dataset cho training và validation","metadata":{}},{"cell_type":"markdown","source":"- Đọc ảnh từ đường dẫn\n- Chuyển BGR → RGB và chuẩn hóa [0,1]\n- Chuyển label sang one-hot encoding\n- Trả về tensor ảnh (3,C,H,W) và label","metadata":{}},{"cell_type":"code","source":"class AlaskaDataset(Dataset):\n    def __init__(self, df, root_path, num_classes=4, transforms=None):\n        self.df = df\n        self.root_path = root_path\n        self.num_classes = num_classes\n        self.transforms = transforms\n\n    def __getitem__(self, index):\n        filename = self.df.iloc[index][\"filename\"]\n        image = cv2.imread(f\"{self.root_path}/{filename}\", cv2.IMREAD_COLOR)\n        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB).astype(np.float32)\n        image /= 255.0\n\n        if self.transforms:\n            sample = {\"image\": image}\n            sample = self.transforms(**sample)\n            image = sample[\"image\"]\n\n        label_idx = self.df.iloc[index][\"label\"]\n        target = self._onehot(self.num_classes, label_idx)\n        return torch.tensor(image).permute(2, 0, 1), target\n\n    def __len__(self):\n        return len(self.df)\n\n    def get_labels(self):\n        return list(self.df[\"label\"].values)\n\n    def _onehot(self, num_classes, target):\n        vec = torch.zeros(num_classes, dtype=torch.float32)\n        vec[target] = 1.0\n        return vec\n\n\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### e. Xử lí dataset cho test","metadata":{}},{"cell_type":"markdown","source":"- Chỉ đọc ảnh không cần label\n- Trả về tên file và tensor ảnh\n- Không có one-hot encoding","metadata":{}},{"cell_type":"code","source":"class AlaskaTestDataset(Dataset):\n    def __init__(self, image_names, root_path, transforms=None):\n        self.image_names = image_names\n        self.root_path = root_path\n        self.transforms = transforms\n\n    def __getitem__(self, index):\n        image_name = self.image_names[index]\n        image = cv2.imread(f\"{self.root_path}/Test/{image_name}\", cv2.IMREAD_COLOR)\n        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB).astype(np.float32)\n        image /= 255.0\n\n        if self.transforms:\n            sample = {\"image\": image}\n            sample = self.transforms(**sample)\n            image = sample[\"image\"]\n\n        return image_name, torch.tensor(image).permute(2, 0, 1)\n\n    def __len__(self):\n        return len(self.image_names)\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### f. Mô hình và đánh giá (evaluate)","metadata":{}},{"cell_type":"markdown","source":"- Sử dụng EfficientNet-B2 đã được pre-train trên ImageNet\n\n- Thay thế lớp fully connected cuối cùng:\n\n    - Đầu vào: 1408 features\n\n    - Đầu ra: 4 classes (Cover, JMiPOD, JUNIWARD, UERD)\n\n- Tận dụng transfer learning để tiết kiệm thời gian huấn luyện\n\n- Khi training: Áp dụng label smoothing để tránh overfitting\n\n\n**Các Metrics:**\n- AverageMeter\n- Weighted AUC\n- RocAucMeter - Theo dõi AUC trong quá trình train","metadata":{}},{"cell_type":"code","source":"def get_model():\n    model = EfficientNet.from_pretrained(\"efficientnet-b2\")\n    model._fc = nn.Linear(in_features=1408, out_features=4, bias=True)\n    return model\n\nclass LabelSmoothing(nn.Module):\n    def __init__(self, smoothing=0.05):\n        super().__init__()\n        self.confidence = 1.0 - smoothing\n        self.smoothing = smoothing\n\n    def forward(self, logits, targets):\n        if self.training:\n            logits = logits.float()\n            targets = targets.float()\n\n            log_probs = F.log_softmax(logits, dim=-1)\n            nll_loss = (-log_probs * targets).sum(-1)\n            smooth_loss = -log_probs.mean(dim=-1)\n            loss = self.confidence * nll_loss + self.smoothing * smooth_loss\n            return loss.mean()\n        else:\n            return F.cross_entropy(logits, targets.argmax(dim=1))\n\nclass AverageMeter:\n    def __init__(self):\n        self.reset()\n\n    def reset(self):\n        self.val = 0\n        self.avg = 0\n        self.sum = 0\n        self.count = 0\n\n    def update(self, val, n=1):\n        self.val = val\n        self.sum += val * n\n        self.count += n\n        self.avg = self.sum / self.count\n\ndef alaska_weighted_auc(y_true, y_valid):\n    tpr_thresholds = [0.0, 0.4, 1.0]\n    weights = [2, 1]\n\n    fpr, tpr, thresholds = metrics.roc_curve(y_true, y_valid, pos_label=1)\n    areas = np.array(tpr_thresholds[1:]) - np.array(tpr_thresholds[:-1])\n    normalization = np.dot(areas, weights)\n\n    competition_metric = 0\n    for idx, weight in enumerate(weights):\n        y_min = tpr_thresholds[idx]\n        y_max = tpr_thresholds[idx + 1]\n        mask = (y_min < tpr) & (tpr < y_max)\n\n        if sum(mask) != 0:\n            x_padding = np.linspace(fpr[mask][-1], 1, 100)\n            x = np.concatenate([fpr[mask], x_padding])\n            y = np.concatenate([tpr[mask], [y_max] * len(x_padding)])\n            y = y - y_min\n            score = metrics.auc(x, y)\n        else:\n            score = 1.0\n\n        submetric = score * weight\n        competition_metric += submetric\n\n    return competition_metric / normalization\n\nclass RocAucMeter:\n    def __init__(self):\n        self.reset()\n\n    def reset(self):\n        self.y_true = np.array([0, 1])\n        self.y_pred = np.array([0.5, 0.5])\n        self.score = 0\n\n    def update(self, y_pred, y_true):\n        y_true = y_true.cpu().numpy().argmax(axis=1).clip(min=0, max=1).astype(int)\n        y_pred = 1 - F.softmax(y_pred, dim=1).data.cpu().numpy()[:, 0]\n        self.y_true = np.hstack((self.y_true, y_true))\n        self.y_pred = np.hstack((self.y_pred, y_pred))\n        self.score = alaska_weighted_auc(self.y_true, self.y_pred)\n\n    @property\n    def avg(self):\n        return self.score\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### g. Training pipeline","metadata":{}},{"cell_type":"markdown","source":"- Hỗ trợ Mixed Precision Training:\n\n    - Tăng tốc độ huấn luyện\n    \n    - Tiết kiệm bộ nhớ GPU\n\n- OneCycleLR Scheduler:\n\n    - Tự động điều chỉnh learning rate\n    \n    - Giúp hội tụ nhanh hơn\n\n- Theo dõi thời gian Thực:\n\n    - Ước lượng thời gian còn lại\n    \n    - Log chi tiết từng bước\n\n- Quản lý checkpoint:\n\n    - Tự động lưu model tốt nhất\n    \n    - Giới hạn số lượng checkpoint\n    \n    - Hỗ trợ resume training\n","metadata":{}},{"cell_type":"code","source":"class AlaskaLearner:\n    def __init__(self, model, config, base_dir=\"./\"):\n        self.model = model.cuda()\n        self.config = config\n        self.base_dir = base_dir\n        self.log_path = f\"{self.base_dir}/log.txt\"\n        self.best_loss = 1e5\n        self.start_time = time.time()  # Track overall training start time\n        self.epoch_times = []  # To store epoch durations for estimation\n\n        self.optimizer = AdamW(self.model.parameters(), lr=config.lr)\n        self.scheduler = config.SchedulerClass(\n        self.optimizer,\n        **config.scheduler_params  # Now gets updated params\n    )\n        self.criterion = LabelSmoothing().cuda()\n\n        self.scaler = torch.cuda.amp.GradScaler()\n\n        self.log(\"Learner initialized with mixed precision support.\")\n\n    def fit(self, train_loader, valid_loader):\n        total_steps = len(train_loader) * self.config.n_epochs\n        self.log(f\"Total training steps: {total_steps}\")\n        total_start_time = time.time()\n\n        for epoch in range(self.config.n_epochs):\n            epoch_start_time = time.time()  # This is the correct variable name\n            self.log(f\"\\nEpoch {epoch + 1}/{self.config.n_epochs} - {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\")\n\n            # Training\n            train_loss, auc_scores = self._train_epoch(train_loader)\n            train_time = time.time() - epoch_start_time\n            self.epoch_times.append(train_time)\n\n            # Calculate remaining time\n            avg_epoch_time = np.mean(self.epoch_times)\n            remaining_epochs = self.config.n_epochs - (epoch + 1)\n            remaining_time = remaining_epochs * avg_epoch_time\n\n            self.log(\n                f\"[TRAIN] Loss: {train_loss.avg:.5f}, AUC: {auc_scores.avg:.5f}, \"\n                f\"Time: {self._format_time(train_time)}, \"\n                f\"Remaining: ~{self._format_time(remaining_time)}\"\n            )\n\n            # Save checkpoint every epoch\n            checkpoint_path = f\"{self.base_dir}/fold{self.config.fold_number}-epoch{epoch + 1}-checkpoint.bin\"\n            self.save(checkpoint_path)\n            self.log(f\"Saved checkpoint to {checkpoint_path}\")\n\n            # Validation\n            t = time.time()\n            valid_loss, auc_scores = self._valid_epoch(valid_loader)\n            valid_time = time.time() - t\n            self.log(\n                f\"[VALID] Loss: {valid_loss.avg:.5f}, AUC: {auc_scores.avg:.5f}, Time: {valid_time:.1f}s\"\n            )\n\n            if valid_loss.avg < self.best_loss:\n                self.best_loss = valid_loss.avg\n                best_path = f\"{self.base_dir}/fold{self.config.fold_number}-best-checkpoint-{str(epoch + 1).zfill(3)}epoch.bin\"\n                self.save(best_path)\n                self.log(f\"New best model saved to {best_path}\")\n\n                # Keep only 3 best checkpoints\n                for path in sorted(\n                    glob.glob(f\"{self.base_dir}/fold{self.config.fold_number}-best-checkpoint-*epoch.bin\")\n                )[:-3]:\n                    os.remove(path)\n                    self.log(f\"Removed old checkpoint: {path}\")\n\n            if self.config.valid_scheduler:\n                self.scheduler.step(metrics=valid_loss.avg)\n\n            # Log epoch completion time - FIXED THIS LINE\n            epoch_time = time.time() - epoch_start_time  # Changed from epoch_start to epoch_start_time\n            self.log(f\"Epoch {epoch + 1} completed in {epoch_time:.2f} seconds\")\n\n        # Log total training time\n        total_time = time.time() - total_start_time\n        self.log(f\"\\nTraining completed in {self._format_time(total_time)}\")\n\n    def _train_epoch(self, train_loader):\n        self.model.train()\n        train_loss = AverageMeter()\n        auc_scores = RocAucMeter()\n        batch_times = AverageMeter()\n        epoch_start_time = time.time()\n        last_log_time = epoch_start_time\n\n        for step, (images, targets) in enumerate(train_loader):\n            batch_start_time = time.time()\n            images = images.cuda().float()\n            targets = targets.cuda().float()\n\n            self.optimizer.zero_grad()\n\n            with torch.cuda.amp.autocast():\n                outputs = self.model(images)\n                loss = self.criterion(outputs, targets)\n\n            self.scaler.scale(loss).backward()\n            self.scaler.step(self.optimizer)\n            self.scaler.update()\n\n            if self.config.step_scheduler:\n                self.scheduler.step()\n\n            auc_scores.update(outputs, targets)\n            train_loss.update(loss.item(), images.size(0))\n\n            batch_time = time.time() - batch_start_time\n            batch_times.update(batch_time)\n\n            # Live time estimation - update every 30 seconds or at end of epoch\n            current_time = time.time()\n            if current_time - last_log_time > 30 or step == len(train_loader) - 1:\n                elapsed_time = current_time - epoch_start_time\n                batches_remaining = len(train_loader) - step - 1\n                estimated_remaining = batches_remaining * batch_times.avg\n\n                self.log(\n                    f\"Step {step + 1}/{len(train_loader)} | \"\n                    f\"Batch: {batch_time:.2f}s ({batch_times.avg:.2f}s avg) | \"\n                    f\"Elapsed: {self._format_time(elapsed_time)} | \"\n                    f\"ETA: {self._format_time(estimated_remaining)} | \"\n                    f\"Loss: {train_loss.avg:.4f} | AUC: {auc_scores.avg:.4f}\"\n                )\n                last_log_time = current_time\n\n        return train_loss, auc_scores\n\n    def _format_time(self, seconds):\n        \"\"\"Helper method to format time in human-readable way\"\"\"\n        if seconds < 60:\n            return f\"{seconds:.0f}s\"\n        elif seconds < 3600:\n            return f\"{seconds // 60:.0f}m {seconds % 60:.0f}s\"\n        else:\n            return f\"{seconds // 3600:.0f}h {(seconds % 3600) // 60:.0f}m\"\n\n    def _valid_epoch(self, valid_loader):\n        self.model.eval()\n        valid_loss = AverageMeter()\n        auc_scores = RocAucMeter()\n\n        with torch.no_grad():\n            for step, (images, targets) in enumerate(valid_loader):\n                images = images.cuda().float()\n                targets = targets.cuda().float()\n\n                with torch.cuda.amp.autocast():  # <-- NEW\n                    outputs = self.model(images)\n                    loss = self.criterion(outputs, targets)\n\n                auc_scores.update(outputs, targets)\n                valid_loss.update(loss.item(), images.size(0))\n\n                if self.config.verbose and (step % self.config.verbose_step == 0):\n                    print(\n                        f\"Validation Step {step}/{len(valid_loader)}, \"\n                        f\"Loss: {valid_loss.avg:.4f}, AUC: {auc_scores.avg:.4f}\",\n                        end=\"\\r\"\n                    )\n\n        return valid_loss, auc_scores\n\n    def save(self, path):\n        torch.save({\n            \"model_state_dict\": self.model.state_dict(),\n            \"optimizer_state_dict\": self.optimizer.state_dict(),\n            \"scheduler_state_dict\": self.scheduler.state_dict(),\n            \"best_loss\": self.best_loss,\n            \"config\": self.config,\n        }, path)\n\n    def load(self, path):\n        checkpoint = torch.load(path)\n        self.model.load_state_dict(checkpoint[\"model_state_dict\"])\n        self.optimizer.load_state_dict(checkpoint[\"optimizer_state_dict\"])\n\n        # Special handling for scheduler\n        if \"scheduler_state_dict\" in checkpoint:\n            # Completely reinitialize the scheduler with current config\n            self.scheduler = self.config.SchedulerClass(\n                self.optimizer,\n                **self.config.scheduler_params\n            )\n\n        self.best_loss = checkpoint[\"best_loss\"]\n\n    def log(self, message):\n        if self.config.verbose:\n            print(message)\n        with open(self.log_path, \"a+\") as logger:\n            logger.write(f\"{message}\\n\")\n\n    def load_checkpoint(model, checkpoint_path, optimizer=None, scheduler=None):\n        try:\n            checkpoint = torch.load(checkpoint_path)\n            model.load_state_dict(checkpoint[\"model_state_dict\"])\n\n            if optimizer is not None and \"optimizer_state_dict\" in checkpoint:\n                optimizer.load_state_dict(checkpoint[\"optimizer_state_dict\"])\n\n            if scheduler is not None and \"scheduler_state_dict\" in checkpoint:\n                scheduler.load_state_dict(checkpoint[\"scheduler_state_dict\"])\n\n            best_loss = checkpoint.get(\"best_loss\", float(\"inf\"))\n            config = checkpoint.get(\"config\", None)\n\n            print(f\"Successfully loaded checkpoint from {checkpoint_path}\")\n            return best_loss, config\n\n        except Exception as e:\n            print(f\"Error loading checkpoint from {checkpoint_path}: {str(e)}\")\n            return float(\"inf\"), None\n\n# Utility Functions\n\ndef format_time(seconds):\n    \"\"\"Convert seconds to human-readable format\"\"\"\n    if seconds < 60:\n        return f\"{seconds:.0f}s\"\n    elif seconds < 3600:\n        return f\"{seconds // 60:.0f}m {seconds % 60:.0f}s\"\n    else:\n        return f\"{seconds // 3600:.0f}h {(seconds % 3600) // 60:.0f}m\"\n\ndef seed_everything(seed):\n    random.seed(seed)\n    os.environ[\"PYTHONHASHSEED\"] = str(seed)\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    torch.cuda.manual_seed(seed)\n    torch.backends.cudnn.deterministic = True\n    torch.backends.cudnn.benchmark = False\n\ndef load_checkpoint(model, checkpoint_path, optimizer=None, scheduler=None):\n    try:\n        checkpoint = torch.load(checkpoint_path)\n        model.load_state_dict(checkpoint[\"model_state_dict\"])\n\n        if optimizer is not None and \"optimizer_state_dict\" in checkpoint:\n            optimizer.load_state_dict(checkpoint[\"optimizer_state_dict\"])\n\n        if scheduler is not None and \"scheduler_state_dict\" in checkpoint:\n            # Only load scheduler state if requested\n            scheduler.load_state_dict(checkpoint[\"scheduler_state_dict\"])\n\n        best_loss = checkpoint.get(\"best_loss\", float(\"inf\"))\n        config = checkpoint.get(\"config\", None)\n\n        print(f\"Successfully loaded checkpoint from {checkpoint_path}\")\n        return best_loss, config\n\n    except Exception as e:\n        print(f\"Error loading checkpoint from {checkpoint_path}: {str(e)}\")\n        return float(\"inf\"), None\n\ndef create_folds(data_root, n_splits=5):\n    \"\"\"Create cross-validation folds from the original dataset structure\"\"\"\n    dataset = []\n    classes = [\"Cover\", \"JMiPOD\", \"JUNIWARD\", \"UERD\"]\n\n    for label, class_name in enumerate(classes):\n        image_paths = glob.glob(f\"{data_root}/{class_name}/*.jpg\")\n        for path in image_paths:\n            dataset.append({\n                \"filename\": f\"{class_name}/{os.path.basename(path)}\",\n                \"label\": label,\n                \"image_name\": os.path.basename(path)\n            })\n\n    random.shuffle(dataset)\n    df = pd.DataFrame(dataset)\n\n    # Create stratified folds\n    gkf = GroupKFold(n_splits=n_splits)\n    df[\"fold\"] = 0\n    for fold_number, (train_index, val_index) in enumerate(\n        gkf.split(X=df.index, y=df[\"label\"], groups=df[\"image_name\"])\n    ):\n        df.loc[df.iloc[val_index].index, \"fold\"] = fold_number\n\n    return df\n\ndef train_pipeline(config, data_root=\"/kaggle/input/alaska2-image-steganalysis\", resume_from=None):\n    seed_everything(config.seed)\n\n    # Generate folds from original dataset structure\n    df = create_folds(data_root)\n\n    # Create datasets\n    train_dataset = AlaskaDataset(\n        df=df[df[\"fold\"] != config.fold_number],\n        root_path=data_root\n    )\n\n    valid_dataset = AlaskaDataset(\n        df=df[df[\"fold\"] == config.fold_number],\n        root_path=data_root\n    )\n\n    # Create data loaders with custom sampler\n    train_loader = DataLoader(\n        train_dataset,\n        sampler=BalanceClassSampler(labels=train_dataset.get_labels(), mode=\"downsampling\"),\n        batch_size=config.batch_size,\n        num_workers=config.num_workers,\n        pin_memory=True,\n        drop_last=True,\n        persistent_workers=True,\n    )\n\n    valid_loader = DataLoader(\n        valid_dataset,\n        batch_size=config.batch_size,\n        num_workers=config.num_workers,\n        shuffle=False,\n        sampler=SequentialSampler(valid_dataset),\n        pin_memory=True,\n    )\n\n    # Update scheduler steps\n    config.steps_per_epoch = len(train_loader)\n\n    # Initialize model and learner\n    model = get_model()\n    learner = AlaskaLearner(model=model, config=config)\n\n    # Resume training if checkpoint provided\n    if resume_from is not None:\n        # Don't load scheduler state - we'll create a fresh one\n        best_loss, loaded_config = load_checkpoint(\n            model=model,\n            checkpoint_path=resume_from,\n            optimizer=learner.optimizer,\n            scheduler=None  # Skip loading scheduler state\n        )\n        if loaded_config is not None:\n            learner.best_loss = best_loss\n            print(f\"Resuming training from checkpoint: {resume_from}\")\n\n    # Train the model\n    learner.fit(train_loader, valid_loader)\n\ndef create_submission(model_path, data_root=\"/kaggle/input/alaska2-image-steganalysis\", output_file=\"submission.csv\"):\n    # Load model\n    checkpoint = torch.load(model_path)\n    config = checkpoint[\"config\"]\n    model = get_model().cuda()\n    model.load_state_dict(checkpoint[\"model_state_dict\"])\n    model.eval()\n\n    # Create test dataset\n    test_image_names = [os.path.basename(x) for x in glob.glob(f\"{data_root}/Test/*.jpg\")]\n    test_dataset = AlaskaTestDataset(\n        image_names=test_image_names,\n        root_path=data_root\n    )\n\n    test_loader = DataLoader(\n        test_dataset,\n        batch_size=config.batch_size,\n        shuffle=False,\n        num_workers=config.num_workers,\n        drop_last=False,\n    )\n\n    # Run inference\n    results = {\"Id\": [], \"Label\": []}\n    with torch.no_grad():\n        for image_names, images in test_loader:\n            images = images.cuda().float()\n            outputs = model(images)\n            probs = 1 - F.softmax(outputs, dim=1).data.cpu().numpy()[:, 0]\n\n            results[\"Id\"].extend(image_names)\n            results[\"Label\"].extend(probs)\n\n    # Save submission\n    submission = pd.DataFrame(results)\n    submission.to_csv(output_file, index=False)\n    print(f\"Submission saved to {output_file}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"if __name__ == \"__main__\":\n    config = TrainGlobalConfig()\n\n    # Check for existing checkpoints to resume from\n    checkpoint_files = glob.glob(\"fold0-best-checkpoint-005epoch.bin\") + glob.glob(\"fold*-epoch*-checkpoint.bin\")\n\n    if checkpoint_files:\n        latest_checkpoint = max(checkpoint_files, key=os.path.getctime)\n        latest_checkpoint = \"fold0-best-checkpoint-005epoch.bin\"\n        print(f\"Found existing checkpoint: {latest_checkpoint}\")\n        resume = input(\"Do you want to resume training from this checkpoint? (y/n): \").lower()\n        if resume == 'y':\n            config = TrainGlobalConfig()  # Reinitialize config\n            train_pipeline(config, resume_from=latest_checkpoint)\n        else:\n            train_pipeline(config)\n    else:\n        train_pipeline(config)\n\n\n    # Create submission using the best model\n    best_models = glob.glob(f\"fold{config.fold_number}-best-checkpoint-*epoch.bin\")\n    if best_models:\n        best_model = max(best_models, key=os.path.getctime)\n        create_submission(best_model)\n    else:\n        print(\"No best model found for submission\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-05T19:12:37.137805Z","iopub.execute_input":"2025-04-05T19:12:37.138212Z","execution_failed":"2025-04-06T04:46:10.894Z"},"id":"5NOQCuyx1noh","outputId":"b862b8c7-2c10-4a67-a668-eb2eaac08c83"},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### h. Sử dụng model tốt nhất để tạo file submission ","metadata":{}},{"cell_type":"code","source":"if __name__ == \"__main__\":\n    config = TrainGlobalConfig()\n    # Create submission using the best model\n    best_models = glob.glob(f\"fold{config.fold_number}-best-checkpoint-005epoch.bin\")\n    if best_models:\n        create_submission(best_model)\n    else:\n        print(\"No best model found for submission\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-06T05:05:24.450045Z","iopub.execute_input":"2025-04-06T05:05:24.450388Z","iopub.status.idle":"2025-04-06T05:06:10.967325Z","shell.execute_reply.started":"2025-04-06T05:05:24.450360Z","shell.execute_reply":"2025-04-06T05:06:10.966309Z"},"id":"SEMhGmxp1noh","outputId":"56848d45-e916-4b7f-8698-f3044e1bf7d1"},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nimport matplotlib.image as mpimg\n\nimage_path = \"/kaggle/input/finalsubmission/result.png\"\n\nimg = mpimg.imread(image_path)\n\nplt.figure(figsize=(10, 10), dpi=300)\n\nplt.imshow(img)\nplt.axis('off')\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-16T14:34:34.503821Z","iopub.execute_input":"2025-04-16T14:34:34.504171Z","iopub.status.idle":"2025-04-16T14:34:35.165620Z","shell.execute_reply.started":"2025-04-16T14:34:34.504146Z","shell.execute_reply":"2025-04-16T14:34:35.164678Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"**Với private score: 0.812 và public score: 0.825, mô hình đang hoạt động tốt, nhưng còn cách xa so với con số 0.936 (cao nhất trên leaderboard). Tuy nhiên kết quả này có thể cải thiện nếu có tài nguyên GPU lớn hơn và không bị giới hạn thời gian training (12 tiếng trên Kaggle)**","metadata":{}},{"cell_type":"markdown","source":"## 6. Khó khăn\n- Việc chưa thích nghi với xử lí dataset lớn khiến nhóm em mất khá nhiều thời gian ban đầu cho việc này\n- Giới hạn về GPU trên Kaggle và thời gian mỗi session là 12 tiếng. Khiến cho việc training bị gián đoạn nếu muốn train với nhiều epoch hơn (hiện tại là 5, AUC cải thiện từ 0.59 -> 0.825). Vẫn có thể cải thiện AUC score nếu train với 10, 15 hay 20 epochs.","metadata":{}},{"cell_type":"markdown","source":"## 7. Hướng phát triển\nTối ưu hóa Feature Engineering:\n- Tiếp tục tìm kiếm và tạo ra các feature mới có khả năng biểu diễn tốt thông tin của dữ liệu có thể giúp mô hình học tốt hơn.\n\nEnsemble Learning:\n- Xây dựng ensemble từ nhiều mô hình khác nhau có thể giúp giảm sai số dự đoán và tăng cường khả năng tổng quát hóa.\n\nHyperparameter Tuning:\n- Thử nghiệm với các chiến lược tối ưu hyperparameters như grid search, random search hay Bayesian optimization để đạt được hiệu năng tối ưu.\n\nCross-validationi:\n- Sử dụng các kỹ thuật cross-validation phù hợp sẽ giúp đánh giá mô hình một cách chính xác hơn và giảm nguy cơ overfitting.\n\nPhân tích lỗi:\n- Kiểm tra kỹ các trường hợp dự đoán sai để tìm ra những điểm yếu của mô hình hiện tại, từ đó cải tiến cả về cấu trúc mô hình lẫn dữ liệu huấn luyện.","metadata":{}},{"cell_type":"markdown","source":"## Kiến thức học được qua đồ án\n- Xây dựng một training pipeline hoàn chỉnh\n- Xử lí dataset lớn\n- Phân chia thời gian cho một đồ án, teamwork\n- Hiểu hơn về image steganalysis và đọc các thông số đánh giá, qua đó cải thiện mô hình qua các chỉ số được theo dõi.","metadata":{}}]}