{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.7.6","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":336454,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":279271,"modelId":300187}],"dockerImageVersionId":29928,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"!pip install -q efficientnet_pytorch > /dev/null\n!pip install pyuploadcare","metadata":{"_uuid":"d629ff2d2480ee46fbb7e2d37f6b5fab8052498a","_cell_guid":"79c7e3d0-c299-4dcb-8224-4455121ee9b0","trusted":true,"_kg_hide-output":true,"execution":{"iopub.status.busy":"2025-04-14T02:30:34.449496Z","iopub.execute_input":"2025-04-14T02:30:34.449845Z","iopub.status.idle":"2025-04-14T02:30:44.664416Z","shell.execute_reply.started":"2025-04-14T02:30:34.449810Z","shell.execute_reply":"2025-04-14T02:30:44.663635Z"},"jupyter":{"source_hidden":true},"_kg_hide-input":false},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from pyuploadcare import Uploadcare\nimport os\nfrom glob import glob\nimport time\n\ndef upload_to_uploadcare(files, kaggle_output_dir=\"/kaggle/working\", public_key=\"1b392feaa5532feaf3b7\", secret_key=\"670a01f0a11dd7bbfb6f\"):\n    \"\"\"\n    Upload files từ Kaggle lên Uploadcare thông qua API\n    \n    Args:\n        files: List các tên file cần upload hoặc pattern (ví dụ: [\"model.pth\", \"*.json\"])\n        kaggle_output_dir: Thư mục chứa file output trên Kaggle\n        public_key: Public key của Uploadcare\n        secret_key: Secret key của Uploadcare\n        \n    Returns:\n        list: Danh sách thông tin các file đã upload (URL, file ID)\n    \"\"\"\n    try:\n        uploadcare = Uploadcare(public_key=public_key, secret_key=secret_key)\n        print(f\"[INFO] Đã khởi tạo client Uploadcare với public key: {public_key}\")\n    except Exception as e:\n        print(f\"[ERROR] Không thể khởi tạo client Uploadcare: {str(e)}\")\n        return []\n    # Tìm tất cả files cần upload\n    all_files = []\n    \n    if isinstance(files, str):\n        files = [files]\n    \n    for file_pattern in files:\n        if '*' in file_pattern:\n            # Nếu là pattern, tìm tất cả file phù hợp\n            matched_files = glob(os.path.join(kaggle_output_dir, file_pattern))\n            all_files.extend(matched_files)\n        else:\n            # Nếu là tên file cụ thể\n            file_path = os.path.join(kaggle_output_dir, file_pattern)\n            if os.path.exists(file_path):\n                all_files.append(file_path)\n            else:\n                print(f\"[WARNING] File không tồn tại: {file_path}\")\n    \n    if not all_files:\n        print(\"[ERROR] Không tìm thấy file nào để upload!\")\n        return []\n    \n    # Upload từng file\n    uploaded_files = []\n    \n    for file_path in all_files:\n        file_name = os.path.basename(file_path)\n        file_size = os.path.getsize(file_path) / 1024 # MB\n        \n        print(f\"[UPLOAD] Đang upload {file_name} ({file_size:.2f} KB)...\")\n        start_time = time.time()\n        \n        try:\n            with open(file_path, \"rb\") as file_object:\n                ucare_file = uploadcare.upload(file_object)\n            \n            upload_time = time.time() - start_time\n            upload_speed = file_size / upload_time if upload_time > 0 else 0\n            \n            file_info = {\n                'name': file_name,\n                'size_mb': file_size,\n                'uuid': ucare_file.uuid,\n                'cdn_url': f\"https://ucarecdn.com/{ucare_file.uuid}/\",\n                'original_path': file_path,\n                'upload_time_sec': upload_time\n            }\n            \n            uploaded_files.append(file_info)\n            \n            print(f\"[SUCCESS] Đã upload {file_name} ({file_size:.2f} KB) trong {upload_time:.2f}s ({upload_speed:.2f} KB/s)\")\n            print(f\"[SUCCESS] URL: {file_info['cdn_url']}\")\n            \n        except Exception as e:\n            print(f\"[ERROR] Không thể upload {file_name}: {str(e)}\")\n    \n    #return uploaded_files","metadata":{"trusted":true,"jupyter":{"source_hidden":true},"execution":{"iopub.status.busy":"2025-04-14T02:31:12.940567Z","iopub.execute_input":"2025-04-14T02:31:12.940878Z","iopub.status.idle":"2025-04-14T02:31:12.952730Z","shell.execute_reply.started":"2025-04-14T02:31:12.940846Z","shell.execute_reply":"2025-04-14T02:31:12.951734Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<span style=\"font-size:38px;\"> 1. TIỀN XỬ LÍ DỮ LIỆU </span>","metadata":{}},{"cell_type":"code","source":"from glob import glob\nfrom sklearn.model_selection import GroupKFold\nimport cv2\nfrom skimage import io\nimport torch\nfrom torch import nn\nimport os\nfrom datetime import datetime\nimport time\nimport random\nimport pandas as pd\nimport numpy as np\nimport albumentations as A\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom tqdm.notebook import tqdm\nfrom albumentations.pytorch.transforms import ToTensorV2\nfrom torch.utils.data import Dataset, DataLoader\nfrom PIL import Image\n# import warnings\n# warnings.filterwarnings(\"ignore\", category=ResourceWarning)\nimport shutil\nimport os\nfrom catalyst.data.sampler import BalanceClassSampler\n\n\n\nSEED = 42\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 = True\n\nseed_everything(SEED)\n\nDATA_ROOT_PATH = '../input/alaska2-image-steganalysis'","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-14T02:31:24.692610Z","iopub.execute_input":"2025-04-14T02:31:24.692933Z","iopub.status.idle":"2025-04-14T02:31:24.705638Z","shell.execute_reply.started":"2025-04-14T02:31:24.692901Z","shell.execute_reply":"2025-04-14T02:31:24.704686Z"},"jupyter":{"source_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def prepare_dataset_with_groupkfold(n_splits=5):\n    \"\"\"Chuẩn bị dataset với GroupKFold, phân chia theo image_name\n    \n    Lưu ý: Cách phân chia này đảm bảo rằng các biến thể khác nhau của cùng một ảnh\n    (ví dụ: Cover, JMiPOD, JUNIWARD, UERD của cùng một ảnh gốc) sẽ được đặt trong\n    cùng một fold để tránh data leakage.\n    \"\"\"\n    dataset = []\n\n    # Tạo dataframe chứa thông tin về tất cả ảnh\n    for label, kind in enumerate(['Cover', 'JMiPOD', 'JUNIWARD', 'UERD']):\n        for path in glob(f'{DATA_ROOT_PATH}/{kind}/*.jpg'):\n            # Lấy tên ảnh từ đường dẫn\n            image_name = os.path.basename(path)\n            dataset.append({\n                'kind': kind,\n                'image_name': image_name,\n                'label': label\n            })\n\n    # Xáo trộn dữ liệu\n    random.shuffle(dataset)\n    dataset = pd.DataFrame(dataset)\n    \n    # Khởi tạo GroupKFold\n    gkf = GroupKFold(n_splits=n_splits)\n\n    # Áp dụng GroupKFold, nhóm theo image_name\n    dataset.loc[:, 'fold'] = 0\n    for fold_number, (train_index, val_index) in enumerate(gkf.split(X=dataset.index, y=dataset['label'], groups=dataset['image_name'])):\n        dataset.loc[dataset.iloc[val_index].index, 'fold'] = fold_number\n    \n    # Hiển thị phân phối lớp theo fold\n    plt.figure(figsize=(12, 6))\n    for fold in range(n_splits):\n        plt.subplot(1, n_splits, fold+1)\n        fold_data = dataset[dataset['fold'] == fold]\n        sns.countplot(x='label', data=fold_data, palette='viridis')\n        plt.title(f'Fold {fold}')\n        plt.xlabel('Lớp')\n        if fold == 0:\n            plt.ylabel('Số lượng ảnh')\n        else:\n            plt.ylabel('')\n        plt.xticks(range(4), ['Cover', 'JMiPOD', 'JUNIWARD', 'UERD'], rotation=45)\n    \n    plt.tight_layout()\n    plt.suptitle('Phân phối lớp trong từng fold', fontsize=16, y=1.05)\n    plt.show()\n    \n    return dataset\n\n# Tạo dataset với GroupKFold\ndataset = prepare_dataset_with_groupkfold()\n\n# Hiển thị thông tin dataframe\nprint(\"Thông tin dataframe:\")\nprint(dataset.head())\nprint(\"\\nPhân phối fold:\")\nprint(dataset['fold'].value_counts())\nprint(\"\\nPhân phối lớp:\")\nprint(dataset['label'].value_counts())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-14T02:32:32.058404Z","iopub.execute_input":"2025-04-14T02:32:32.058700Z","iopub.status.idle":"2025-04-14T02:32:37.226221Z","shell.execute_reply.started":"2025-04-14T02:32:32.058675Z","shell.execute_reply":"2025-04-14T02:32:37.225432Z"},"jupyter":{"source_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def get_train_transforms():\n    \"\"\"Augmentations đơn giản cho tập huấn luyện từ Notebook-3\"\"\"\n    return A.Compose([\n            A.HorizontalFlip(p=0.5),\n            A.VerticalFlip(p=0.5),\n            A.Resize(height=512, width=512, p=1.0),\n            ToTensorV2(p=1.0),\n        ], p=1.0)\n\ndef get_valid_transforms():\n    \"\"\"Transforms cho validation từ Notebook-3\"\"\"\n    return A.Compose([\n            A.Resize(height=512, width=512, p=1.0),\n            ToTensorV2(p=1.0),\n        ], p=1.0)\n\ndef preprocess_image(image_path, transform=None, color_space='rgb'):\n    \"\"\"\n    Hàm tiền xử lý ảnh cho cả không gian màu RGB và YCbCr\n    \n    Args:\n        image_path: Đường dẫn tới file ảnh\n        transform: Các biến đổi cần áp dụng (albumentations)\n        color_space: Không gian màu ('rgb' hoặc 'ycbcr')\n        \n    Returns:\n        Ảnh đã được tiền xử lý\n    \"\"\"\n    if color_space.lower() == 'rgb':\n        # Đọc ảnh với OpenCV (RGB)\n        image = cv2.imread(image_path)\n        image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB).astype(np.float32)\n        image /= 255.0\n    else:\n        # Đọc ảnh với PIL và chuyển sang YCbCr\n        image = Image.open(image_path).convert('YCbCr')\n        image = np.array(image).astype(np.float32) / 255.0\n    \n    # Áp dụng các biến đổi nếu có\n    if transform is not None:\n        image = transform(image=image)['image']\n    \n    return image","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-14T02:32:45.373095Z","iopub.execute_input":"2025-04-14T02:32:45.373436Z","iopub.status.idle":"2025-04-14T02:32:45.382023Z","shell.execute_reply.started":"2025-04-14T02:32:45.373402Z","shell.execute_reply":"2025-04-14T02:32:45.381105Z"},"jupyter":{"source_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<span style=\"font-size:38px;\"> 2. TẠO ĐẶC TRƯNG </span>","metadata":{}},{"cell_type":"code","source":"def onehot(size, target):\n    \"\"\"\n    Chuyển đổi nhãn thành vector one-hot\n    \n    Args:\n        size: Kích thước của vector one-hot (số lượng lớp)\n        target: Nhãn cần chuyển đổi\n        \n    Returns:\n        Vector one-hot\n    \"\"\"\n    vec = torch.zeros(size, dtype=torch.float32)\n    vec[target] = 1.\n    return vec\n\nclass DatasetRetriever(Dataset):\n    \"\"\"\n    Lớp quản lý dataset, xử lý việc tải ảnh và áp dụng transforms\n    \"\"\"\n    def __init__(self, kinds, image_names, labels, transforms=None, color_space='rgb'):\n        super().__init__()\n        self.kinds = kinds\n        self.image_names = image_names\n        self.labels = labels\n        self.transforms = transforms\n        self.color_space = color_space\n\n    def __getitem__(self, index: int):\n        kind, image_name, label = self.kinds[index], self.image_names[index], self.labels[index]\n        \n        # Sử dụng hàm preprocess_image đã định nghĩa trước đó\n        image = preprocess_image(\n            f'{DATA_ROOT_PATH}/{kind}/{image_name}', \n            transform=self.transforms, \n            color_space=self.color_space\n        )\n        \n        # Chuyển đổi nhãn thành vector one-hot\n        target = onehot(4, label)\n        return image, target\n\n    def __len__(self) -> int:\n        return self.image_names.shape[0]\n\n    def get_labels(self):\n        \"\"\"\n        Trả về danh sách các nhãn, hữu ích cho việc cân bằng lớp\n        \"\"\"\n        return list(self.labels)\n\ndef create_dataloaders(dataset, fold_number, batch_size=16, num_workers=4, color_space='rgb'):\n    \"\"\"\n    Tạo DataLoader cho tập huấn luyện và tập kiểm thử\n    \n    Args:\n        dataset: DataFrame chứa thông tin về ảnh\n        fold_number: Fold được sử dụng làm tập kiểm thử\n        batch_size: Kích thước batch\n        num_workers: Số worker cho việc tải dữ liệu\n        color_space: Không gian màu ('rgb' hoặc 'ycbcr')\n        \n    Returns:\n        train_loader, val_loader, train_dataset, val_dataset\n    \"\"\"\n    # Tạo dataset cho tập huấn luyện\n    train_dataset = DatasetRetriever(\n        kinds=dataset[dataset['fold'] != fold_number].kind.values,\n        image_names=dataset[dataset['fold'] != fold_number].image_name.values,\n        labels=dataset[dataset['fold'] != fold_number].label.values,\n        transforms=get_train_transforms(),\n        color_space=color_space\n    )\n    \n    # Tạo dataset cho tập kiểm thử\n    val_dataset = DatasetRetriever(\n        kinds=dataset[dataset['fold'] == fold_number].kind.values,\n        image_names=dataset[dataset['fold'] == fold_number].image_name.values,\n        labels=dataset[dataset['fold'] == fold_number].label.values,\n        transforms=get_valid_transforms(),\n        color_space=color_space\n    )\n    \n    # Sử dụng BalanceClassSampler để cân bằng lớp trong quá trình huấn luyện\n    train_loader = DataLoader(\n        train_dataset,\n        sampler=BalanceClassSampler(labels=train_dataset.get_labels(), mode=\"downsampling\"),\n        batch_size=batch_size,\n        pin_memory=True,  # Bật pin_memory\n        drop_last=True,\n        num_workers=num_workers,    # Tăng số worker (nên bằng số lõi CPU)\n    )\n    \n    # DataLoader cho tập kiểm thử\n    val_loader = DataLoader(\n        val_dataset, \n        batch_size=batch_size,\n        num_workers=num_workers,\n        shuffle=False,\n        sampler=SequentialSampler(val_dataset),\n        pin_memory=True,\n    )\n    \n    return train_loader, val_loader, train_dataset, val_dataset\n\n# Ví dụ sử dụng\ndef show_dataset_example():\n    \"\"\"Hiển thị một số ví dụ từ dataset\"\"\"\n    fold_number = 0\n    #dataset = prepare_dataset_with_groupkfold()\n    # Tạo train và validation dataset\n    train_dataset = DatasetRetriever(\n        kinds=dataset[dataset['fold'] != fold_number].kind.values,\n        image_names=dataset[dataset['fold'] != fold_number].image_name.values,\n        labels=dataset[dataset['fold'] != fold_number].label.values,\n        transforms=get_valid_transforms()  # Sử dụng valid transforms để không làm biến đổi ảnh\n    )\n    \n    # Hiển thị một số ví dụ\n    fig, axes = plt.subplots(2, 4, figsize=(16, 8))\n    \n    for i in range(8):\n        image, target = train_dataset[i]\n        label = torch.argmax(target).item()\n        label_names = ['Cover', 'JMiPOD', 'JUNIWARD', 'UERD']\n        \n        # Chuyển tensor thành numpy để hiển thị\n        numpy_image = image.permute(1, 2, 0).cpu().numpy()\n        \n        row, col = i // 4, i % 4\n        axes[row, col].imshow(numpy_image)\n        axes[row, col].set_title(f'Class: {label_names[label]}')\n        axes[row, col].axis('off')\n    \n    plt.tight_layout()\n    plt.suptitle('Ví dụ từ Dataset', fontsize=16, y=1.02)\n    plt.show()\n    \n    return train_dataset\n\n# Hiển thị ví dụ từ dataset\ntrain_dataset = show_dataset_example()\n\n# Kiểm tra vector one-hot\nprint(\"Ví dụ về vector one-hot cho từng lớp:\")\nfor i in range(4):\n    print(f\"Class {i}: {onehot(4, i)}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-14T02:32:54.382761Z","iopub.execute_input":"2025-04-14T02:32:54.383058Z","iopub.status.idle":"2025-04-14T02:32:55.322206Z","shell.execute_reply.started":"2025-04-14T02:32:54.383033Z","shell.execute_reply":"2025-04-14T02:32:55.321488Z"},"jupyter":{"source_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<span style=\"font-size:38px;\"> 3. XÂY DỰNG MÔ HÌNH </span>","metadata":{}},{"cell_type":"code","source":"class CheckpointCallback:\n    \"\"\"\n    Callback để lưu checkpoint trong quá trình huấn luyện\n    \"\"\"\n    def __init__(self, checkpoint_interval=1, save_path='checkpoints', model_prefix='model'):\n        self.checkpoint_interval = checkpoint_interval\n        self.save_path = save_path\n        self.model_prefix = model_prefix\n        os.makedirs(save_path, exist_ok=True)\n        \n    def on_epoch_end(self, epoch, logs=None):\n        \"\"\"Lưu checkpoint sau mỗi số epoch nhất định\"\"\"\n        logs = logs or {}\n        \n        if (epoch + 1) % self.checkpoint_interval == 0:\n            # Lấy các giá trị cần thiết\n            model = logs.get('model')\n            optimizer = logs.get('optimizer')\n            scheduler = logs.get('scheduler')\n            scaler = logs.get('scaler')\n            val_auc = logs.get('val_auc', 0)\n            \n            # Tạo dữ liệu để lưu vào file data\n            data = {\n                'epoch': epoch,\n                'train_loss': logs.get('train_loss', 0),\n                'val_loss': logs.get('val_loss', 0),\n                'val_acc': logs.get('val_acc', 0),\n                'val_auc': val_auc,\n                'lr': logs.get('lr', 0),\n                'class_acc': logs.get('class_acc', {})\n            }\n            \n            # Tạo tên file data và model\n            data_path = os.path.join(\n                self.save_path, \n                f\"data_epoch{epoch+1}.json\"\n            )\n            \n            model_path = os.path.join(\n                self.save_path, \n                f\"{self.model_prefix}_epoch{epoch+1}_{val_auc:.4f}.pth\"\n            )\n            \n            # Lưu file data\n            with open(data_path, 'w') as f:\n                import json\n                json.dump(data, f, indent=4)\n            \n            # Lưu file model\n            torch.save({\n                'epoch': epoch,\n                'model_state_dict': model.state_dict(),\n                'optimizer_state_dict': optimizer.state_dict(),\n                'scheduler_state_dict': scheduler.state_dict() if scheduler else None,\n                'scaler_state_dict': scaler.state_dict() if scaler else None,\n                'val_auc': val_auc\n            }, model_path)\n            \n            print(f\"\\n[CHECKPOINT] Đã lưu data tại: {data_path}\")\n            print(f\"[CHECKPOINT] Đã lưu model tại: {model_path}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-14T02:33:03.810165Z","iopub.execute_input":"2025-04-14T02:33:03.810525Z","iopub.status.idle":"2025-04-14T02:33:03.821607Z","shell.execute_reply.started":"2025-04-14T02:33:03.810468Z","shell.execute_reply":"2025-04-14T02:33:03.820719Z"},"jupyter":{"source_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from efficientnet_pytorch import EfficientNet\n## # Import thư viện\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nimport torch.nn.functional as F\nfrom efficientnet_pytorch import EfficientNet\nfrom torch.optim.lr_scheduler import ReduceLROnPlateau\nfrom torch.utils.data.sampler import SequentialSampler\nfrom sklearn import metrics\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport time\nimport os\nfrom tqdm.notebook import tqdm\nfrom torch.cuda.amp import  GradScaler\n\n# Compatibility layer for PyTorch versions before 1.6\ntry:\n    from torch.cuda.amp import autocast\nexcept ImportError:\n    # Create a dummy autocast context manager for older PyTorch versions\n    class autocast:\n        def __init__(self, enabled=True, dtype=None):\n            self.enabled = enabled\n            self.dtype = dtype\n        def __enter__(self):\n            pass\n        def __exit__(self, exc_type, exc_val, exc_tb):\n            pass\n\n\n# Xác định thiết bị\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f\"Using device: {device}\")\n\n# Định nghĩa lớp EfficientNetwork dựa trên Notebook-1\nclass EfficientNetwork(nn.Module):\n    \"\"\"\n    Mô hình EfficientNet-B2 cho bài toán phân loại ảnh Alaska2\n    \n    Args:\n        output_size: Số lượng lớp đầu ra (4 cho Alaska2)\n    \"\"\"\n    def __init__(self, output_size=4):\n        super().__init__()\n        \n        # Tải pretrained EfficientNet-B2\n        self.features = EfficientNet.from_pretrained('efficientnet-b2')\n        \n        # Lớp fully connected cuối cùng để phân loại\n        # EfficientNet-B2 có đầu ra là 1408 features\n        self.classifier = nn.Linear(1408, output_size)\n        \n    def forward(self, x, feature_extract=False):\n        \"\"\"\n        Forward pass\n        \n        Args:\n            x: Tensor đầu vào\n            feature_extract: Nếu True, trả về features trước lớp phân loại\n            \n        Returns:\n            Logits hoặc features tùy thuộc vào feature_extract\n        \"\"\"\n        # Trích xuất đặc trưng\n        features = self.features.extract_features(x)\n        \n        # Global average pooling\n        pooled_features = F.adaptive_avg_pool2d(features, 1)\n        pooled_features = pooled_features.flatten(start_dim=1)\n        \n        # Nếu chỉ trích xuất đặc trưng\n        if feature_extract:\n            return pooled_features\n        \n        # Phân loại\n        logits = self.classifier(pooled_features)\n        \n        return logits\n\n# Định nghĩa lớp EfficientNetwork b3\nclass EfficientNetB3Model(nn.Module):\n    \"\"\"\n    Mô hình EfficientNet-B3 cho bài toán phân loại ảnh Alaska2\n    \n    Args:\n        output_size: Số lượng lớp đầu ra (4 cho Alaska2)\n        pretrained: Có sử dụng pretrained weights không\n    \"\"\"\n    def __init__(self, output_size=4, pretrained=True):\n        super().__init__()\n        \n        # Tải pretrained EfficientNet-B3\n        if pretrained:\n            self.features = EfficientNet.from_pretrained('efficientnet-b3')\n        else:\n            self.features = EfficientNet.from_name('efficientnet-b3')\n        \n        # Lớp fully connected cuối cùng\n        # EfficientNet-B3 có đầu ra là 1536 features\n        self.classifier = nn.Linear(1536, output_size)\n        \n        # Thêm dropout để giảm overfitting\n        self.dropout = nn.Dropout(0.3)\n        \n    def forward(self, x, feature_extract=False):\n        # Trích xuất đặc trưng\n        features = self.features.extract_features(x)\n        \n        # Global average pooling\n        pooled_features = F.adaptive_avg_pool2d(features, 1)\n        pooled_features = pooled_features.flatten(start_dim=1)\n        \n        # Nếu chỉ trích xuất đặc trưng\n        if feature_extract:\n            return pooled_features\n        \n        # Áp dụng dropout\n        x = self.dropout(pooled_features)\n        \n        # Phân loại\n        logits = self.classifier(x)\n        \n        return logits\n# Hàm tính weighted AUC từ Notebook-1\n\ndef alaska_weighted_auc(y_true, y_valid):\n    \"\"\"\n    Hàm tính weighted AUC theo định nghĩa của cuộc thi Alaska2\n    \n    Args:\n        y_true: Nhãn thực (binary: 0 = Cover, 1 = Steganography)\n        y_valid: Dự đoán xác suất\n        \n    Returns:\n        weighted_auc: Weighted AUC score\n    \"\"\"\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    \n    # Size of subsets\n    areas = np.array(tpr_thresholds[1:]) - np.array(tpr_thresholds[:-1])\n    \n    # The total area is normalized by the sum of weights\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 mask.any():\n            x_padding = np.linspace(fpr[mask][-1], 1, 100)\n            \n            x = np.concatenate([fpr[mask], x_padding])\n            y = np.concatenate([tpr[mask], [y_max] * len(x_padding)])\n            # Normalize such that curve starts at y = 0\n            y = y - y_min \n            score = metrics.auc(x, y)\n            submetric = score * weight\n            competition_metric += submetric\n        \n    return competition_metric / normalization","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-14T02:33:12.697245Z","iopub.execute_input":"2025-04-14T02:33:12.697589Z","iopub.status.idle":"2025-04-14T02:33:12.718096Z","shell.execute_reply.started":"2025-04-14T02:33:12.697558Z","shell.execute_reply":"2025-04-14T02:33:12.717366Z"},"_kg_hide-input":true,"jupyter":{"source_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def train_model(model, train_loader, val_loader, num_epochs=10, \n               learning_rate=1e-3, weight_decay=1e-5,\n               factor=0.5, patience=2, device=device, save_path='models',\n               use_amp=True, accumulation_steps=4, cp_path=None, callbacks=None, best_model = None):\n    \"\"\"\n    Huấn luyện mô hình với hỗ trợ callbacks và checkpoint\n    \n    Args:\n        model: Mô hình cần huấn luyện\n        train_loader, val_loader: DataLoader cho tập train và validation\n        num_epochs: Số epochs huấn luyện\n        learning_rate: Tốc độ học\n        weight_decay: Hệ số weight decay cho regularization\n        label_smoothing: Hệ số label smoothing\n        factor, patience: Thông số cho ReduceLROnPlateau\n        device: Thiết bị huấn luyện (CPU/GPU)\n        save_path: Thư mục lưu model\n        use_amp: Bật/tắt Automatic Mixed Precision\n        accumulation_steps: Số bước tích lũy gradient\n        resume_from: Đường dẫn đến checkpoint để tiếp tục huấn luyện\n        callbacks: Danh sách callbacks sử dụng trong quá trình huấn luyện\n    \"\"\"\n    os.makedirs(save_path, exist_ok=True)\n    \n    # Khởi tạo callbacks nếu không có\n    callbacks = callbacks or []\n    \n    # Khởi tạo biến theo dõi và lịch sử\n    start_epoch = 0\n    best_auc = 0\n    best_model_epoch = 0\n    best_model_state = None\n    history = {\n        'train_loss': [],\n        'val_loss': [],\n        'val_acc': [],\n        'val_auc': [],\n        'lr': []\n    }\n    \n    # Định nghĩa loss function \n    criterion = nn.CrossEntropyLoss()\n    \n    # Khởi tạo optimizer AdamW\n    optimizer = optim.AdamW(model.parameters(), lr=learning_rate, weight_decay=weight_decay)\n    \n    # Khởi tạo scheduler ReduceLROnPlateau\n    scheduler = ReduceLROnPlateau(optimizer, mode='max', factor=factor, \n                                 patience=patience, verbose=True)\n    \n    # Khởi tạo GradScaler cho Mixed Precision Training\n    scaler = GradScaler(enabled=use_amp and device.type == 'cuda')\n    \n    # Nếu có checkpoint, tiếp tục từ checkpoint\n    if cp_path and os.path.exists(cp_path) and best_model:\n        \n        model_path, best_auc = load_best_model(best_model)\n        model.load_state_dict(torch.load(model_path))\n        print(f\"[RESUME] Loading checkpoint from {cp_path}\")\n        checkpoint = torch.load(cp_path)\n        optimizer.load_state_dict(checkpoint['optimizer_state_dict'])\n        \n        if 'scheduler_state_dict' in checkpoint and checkpoint['scheduler_state_dict'] is not None:\n            scheduler.load_state_dict(checkpoint['scheduler_state_dict'])\n        \n        # Khôi phục GradScaler nếu có\n        if 'scaler_state_dict' in checkpoint and checkpoint['scaler_state_dict'] is not None and use_amp and device.type == 'cuda':\n            scaler.load_state_dict(checkpoint['scaler_state_dict'])\n\n        start_epoch = checkpoint['epoch'] + 1\n        \n        print(f\"[RESUME] Tiếp tục huấn luyện từ epoch {start_epoch}, Best AUC: {best_auc:.4f}\")\n    \n    end_epoch = start_epoch + num_epochs\n    print(f\"[INFO] Bắt đầu quá trình huấn luyện từ epoch {start_epoch+1} đến {end_epoch}\")\n    print(f\"[CONFIG] Learning rate: {optimizer.param_groups[0]['lr']}, Weight decay: {weight_decay}\")\n    print(f\"[CONFIG] Scheduler: factor={scheduler.factor}, patience={scheduler.patience}\")\n    print(f\"[CONFIG] Mixed Precision: {use_amp}, Gradient Accumulation: {accumulation_steps} steps\")\n    print(f\"[CONFIG] Device: {device}\")\n    \n    # In số lượng batch\n    print(f\"[INFO] Số lượng batch trong tập huấn luyện: {len(train_loader)}\")\n    print(f\"[INFO] Số lượng batch trong tập validation: {len(val_loader)}\")\n    \n    # Thống kê số lượng tham số mô hình\n    total_params = sum(p.numel() for p in model.parameters())\n    trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)\n    print(f\"[MODEL] Tổng số tham số: {total_params:,}\")\n    print(f\"[MODEL] Số tham số có thể huấn luyện: {trainable_params:,}\")\n    \n    # Thông báo bắt đầu huấn luyện cho callbacks\n\n    \n    # Vòng lặp huấn luyện\n    for epoch in range(start_epoch, end_epoch):\n        print(f\"\\n{'='*30} EPOCH {epoch+1}/{end_epoch} {'='*30}\")\n        start_time = time.time()\n        \n        # Thông báo bắt đầu epoch cho callbacks\n        epoch_logs = {'epoch': epoch}\n        # === Training ===\n        model.train()\n        train_loss = 0\n        batch_times = []\n        optimizer.zero_grad()  # Zero gradients trước khi bắt đầu epoch\n        \n        print(f\"[TRAIN] Bắt đầu huấn luyện epoch {epoch+1}\")\n        batch_start = time.time()\n        \n        for batch_idx, (images, targets) in enumerate(train_loader):\n            # Thông báo bắt đầu batch cho callbacks\n                \n            # Đưa dữ liệu lên device\n            images = images.to(device)\n            targets = targets.to(device)\n            \n            # Convert targets to class indices if they are one-hot encoded\n            if targets.dim() > 1 and targets.size(1) > 1:\n                targets = torch.argmax(targets, dim=1)\n            \n            # Ensure targets are Long type\n            targets = targets.long()\n            \n            # Mixed Precision Training\n            with autocast(enabled=use_amp and device.type == 'cuda'):\n                # Forward pass\n                outputs = model(images)\n                \n                # Tính loss và chia cho số bước tích lũy\n                loss = criterion(outputs, targets) / accumulation_steps\n            \n            # Backward với Mixed Precision\n            scaler.scale(loss).backward()\n            \n            # Cập nhật weights sau khi tích lũy đủ gradient\n            if (batch_idx + 1) % accumulation_steps == 0 or (batch_idx + 1) == len(train_loader):\n                # Cập nhật weights và zero gradients\n                scaler.step(optimizer)\n                scaler.update()\n                optimizer.zero_grad()\n            \n            train_loss += loss.item() * accumulation_steps\n            \n            # Tính thời gian cho batch này\n            if (batch_idx + 1) % 50 == 0:\n                batch_end = time.time()\n                batch_time = batch_end - batch_start\n                batch_times.append(batch_time)\n                \n                log_msg = f\"[TRAIN] Batch {batch_idx+1}/{len(train_loader)}: Loss = {loss.item()*accumulation_steps:.4f}, Thời gian: {batch_time:.2f}s\"\n                print(log_msg, end='\\r')  \n            # Bắt đầu tính thời gian cho batch tiếp theo\n                batch_start = time.time()\n        \n        # Tính loss trung bình trên tập train\n        train_loss = train_loss / len(train_loader)\n        avg_batch_time = sum(batch_times) / len(batch_times) if batch_times else 0\n        \n        print(f\"[TRAIN] Kết thúc epoch {epoch+1} - Loss: {train_loss:.4f}, Thời gian trung bình/batch: {avg_batch_time:.4f}s\")\n        \n        # === Validation ===\n        print(f\"[VALID] Bắt đầu đánh giá epoch {epoch+1}\")\n        model.eval()\n        val_loss = 0\n        all_targets = []\n        all_predictions = []\n        val_start_time = time.time()\n        \n        with torch.no_grad():\n            for images, targets in val_loader:\n                # Đưa dữ liệu lên device\n                images = images.to(device)\n                targets = targets.to(device)\n                \n                # Save original targets for metrics calculation\n                original_targets = targets.clone()\n                \n                # Convert targets to class indices if they are one-hot encoded\n                if targets.dim() > 1 and targets.size(1) > 1:\n                    targets = torch.argmax(targets, dim=1)\n                \n                # Ensure targets are Long type for loss calculation\n                targets = targets.long()\n                \n                # Forward pass - không cần Mixed Precision trong validation\n                outputs = model(images)\n                \n                # Tính loss\n                loss = criterion(outputs, targets)\n                val_loss += loss.item()\n                \n                # Lưu lại targets và predictions\n                all_targets.append(original_targets.cpu().numpy())\n                all_predictions.append(F.softmax(outputs, dim=1).cpu().numpy())\n        \n        val_time = time.time() - val_start_time\n        print(f\"[VALID] Thời gian đánh giá: {val_time:.2f}s\")\n        \n        # Tính loss trung bình trên tập validation\n        val_loss = val_loss / len(val_loader)\n        \n        # Ghép all_targets và all_predictions thành một mảng duy nhất\n        all_targets = np.vstack(all_targets)\n        all_predictions = np.vstack(all_predictions)\n        \n        # Tính độ chính xác\n        predicted_classes = np.argmax(all_predictions, axis=1)\n        true_classes = np.argmax(all_targets, axis=1)\n        accuracy = np.mean(predicted_classes == true_classes)\n        \n        # Phân tích sai số theo lớp\n        class_acc = {}\n        for class_idx in range(4):\n            class_mask = (true_classes == class_idx)\n            if np.sum(class_mask) > 0:\n                class_correct = np.sum((predicted_classes == true_classes) & class_mask)\n                class_total = np.sum(class_mask)\n                class_acc[class_idx] = class_correct / class_total\n        \n        # Chuyển về dạng binary classification cho AUC\n        # Nhãn: 0 = Cover (class 0), 1 = Steganography (class 1, 2, 3)\n        binary_targets = (true_classes != 0).astype(int)\n        \n        # Sửa lại phần tính binary_predictions để rõ ràng hơn\n        # Dự đoán: Xác suất steganography = 1 - P(Cover)\n        binary_predictions = 1 - all_predictions[:, 0]\n        \n        # Tính AUC\n        auc_score = alaska_weighted_auc(binary_targets, binary_predictions)\n        \n        # Lưu lại lịch sử\n        history['train_loss'].append(train_loss)\n        history['val_loss'].append(val_loss)\n        history['val_acc'].append(accuracy)\n        history['val_auc'].append(auc_score)\n        history['lr'].append(optimizer.param_groups[0]['lr'])\n        \n        epoch_time = time.time() - start_time\n        \n        # In kết quả epoch với thêm thông tin\n        print(f\"\\n[RESULT] Epoch {epoch+1}/{end_epoch} - Tổng thời gian: {epoch_time:.2f}s\")\n        print(f\"[RESULT] Train Loss: {train_loss:.4f} - Val Loss: {val_loss:.4f}\")\n        print(f\"[RESULT] Accuracy: {accuracy:.4f} - AUC: {auc_score:.4f}\")\n        print(f\"[RESULT] Learning Rate: {optimizer.param_groups[0]['lr']:.6f}\")\n        \n        # Lưu model tốt nhất dựa trên AUC\n        if auc_score > best_auc:\n            improvement = auc_score - best_auc\n            best_auc = auc_score\n            best_model_epoch = epoch + 1\n            best_model_state = model.state_dict().copy()\n            print(f\"\\n[SAVE] 🌟 Cập nhật model tốt nhất với cải thiện AUC: +{improvement:.4f}\")\n        else:\n            print(f\"\\n[INFO] Không có cải thiện AUC. Tốt nhất: {best_auc:.4f}, Hiện tại: {auc_score:.4f}\")\n        \n        # Cập nhật learning rate với scheduler\n        old_lr = optimizer.param_groups[0]['lr']\n        scheduler.step(auc_score)\n        new_lr = optimizer.param_groups[0]['lr']\n        \n        if old_lr != new_lr:\n            print(f\"\\n[LR] 📉 Giảm learning rate: {old_lr:.6f} -> {new_lr:.6f}\")\n        \n        # Thông báo kết thúc epoch cho callbacks\n        epoch_logs.update({\n            'train_loss': train_loss,\n            'val_loss': val_loss,\n            'val_acc': accuracy,\n            'val_auc': auc_score,\n            'class_acc': class_acc,\n            'lr': optimizer.param_groups[0]['lr'],\n            'model': model,\n            'optimizer': optimizer,\n            'scheduler': scheduler,\n            'scaler': scaler,\n        })\n        for callback in callbacks:\n            callback.on_epoch_end(epoch, epoch_logs)\n    \n    # Lưu model tốt nhất sau khi hoàn thành tất cả các epochs\n    if best_model_state is not None:\n        model_path = os.path.join(save_path, f\"efficientnet_best_auc{best_auc:.4f}.pth\")\n        torch.save(best_model_state, model_path)\n        print(\"\\n\" + \"=\"*70)\n        print(f\"[SAVE] Đã lưu model tốt nhất (epoch {best_model_epoch}) với AUC: {best_auc:.4f}\")\n        print(f\"[SAVE] Path: {model_path}\")\n    else:\n        model_path = None\n        print(\"[WARNING] Không tìm thấy model tốt nhất để lưu\")\n    \n    # Thông báo kết thúc huấn luyện cho callbacks\n\n    \n    print(\"\\n\" + \"=\"*70)\n    print(f\"[DONE] Hoàn thành quá trình huấn luyện sau {num_epochs} epochs\")\n    print(f\"[BEST] AUC tốt nhất đạt được: {best_auc:.4f} tại epoch {best_model_epoch}\")\n    if model_path:\n        print(f\"[BEST] Model tốt nhất được lưu tại: {model_path}\")\n    print(\"=\"*70)\n    \n    return best_model_epoch, model_path","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-14T02:33:24.370459Z","iopub.execute_input":"2025-04-14T02:33:24.370910Z","iopub.status.idle":"2025-04-14T02:33:24.432260Z","shell.execute_reply.started":"2025-04-14T02:33:24.370870Z","shell.execute_reply":"2025-04-14T02:33:24.431408Z"},"jupyter":{"source_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def plot_training_from_json(json_file_path, figsize=(18, 15), save_path=None):\n    \"\"\"\n    Vẽ biểu đồ quá trình huấn luyện từ file JSON chứa dữ liệu training metrics\n    \n    Args:\n        json_file_path: Đường dẫn đến file JSON chứa dữ liệu\n        figsize: Kích thước của figure (width, height)\n        save_path: Đường dẫn để lưu hình ảnh (nếu None, chỉ hiển thị)\n        \n    Returns:\n        fig: Matplotlib figure object\n    \"\"\"\n    import json\n    import matplotlib.pyplot as plt\n    import numpy as np\n    import matplotlib.ticker as ticker\n    \n    # Đọc dữ liệu từ file JSON\n    try:\n        with open(json_file_path, 'r') as f:\n            data = json.load(f)\n    except Exception as e:\n        print(f\"[ERROR] Không thể đọc file JSON: {str(e)}\")\n        return None\n    \n    # Kiểm tra xem dữ liệu có đúng định dạng không\n    required_fields = ['epochs', 'train_loss', 'val_loss', 'val_acc', 'val_auc', 'lr']\n    for field in required_fields:\n        if field not in data:\n            print(f\"[ERROR] Thiếu trường dữ liệu '{field}' trong file JSON\")\n            return None\n    \n    # Tạo figure và axes\n    fig = plt.figure(figsize=figsize)\n    \n    # Đảm bảo epochs là các số nguyên\n    epochs = data['epochs']\n    \n    # 1. Biểu đồ Loss\n    ax1 = plt.subplot2grid((4, 4), (0, 0), colspan=2, rowspan=1)\n    ax1.plot(epochs, data['train_loss'], 'b-', label='Train Loss')\n    ax1.plot(epochs, data['val_loss'], 'r-', label='Validation Loss')\n    ax1.set_title('Loss qua các Epoch', fontsize=14)\n    ax1.set_xlabel('Epoch')\n    ax1.set_ylabel('Loss')\n    ax1.legend()\n    ax1.grid(True, linestyle='--', alpha=0.6)\n    \n    # Đánh dấu loss thấp nhất\n    min_val_loss_idx = np.argmin(data['val_loss'])\n    min_val_loss = data['val_loss'][min_val_loss_idx]\n    min_epoch = epochs[min_val_loss_idx]\n    ax1.plot(min_epoch, min_val_loss, 'ro', markersize=8)\n    ax1.annotate(f'Min: {min_val_loss:.4f}', \n                xy=(min_epoch, min_val_loss),\n                xytext=(min_epoch + 0.5, min_val_loss),\n                fontsize=10,\n                arrowprops=dict(facecolor='black', shrink=0.05, width=1.5))\n    \n    # Chỉ đặt ticks là các số nguyên\n    \n    # 2. Biểu đồ Accuracy và AUC\n    ax2 = plt.subplot2grid((4, 4), (1, 0), colspan=2, rowspan=1)\n    ax2.plot(epochs, data['val_acc'], 'g-', label='Accuracy')\n    ax2.plot(epochs, data['val_auc'], 'm-', label='AUC')\n    ax2.set_title('Accuracy & AUC', fontsize=14)\n    ax2.set_xlabel('Epoch')\n    ax2.set_ylabel('Score')\n    ax2.legend()\n    ax2.grid(True, linestyle='--', alpha=0.6)\n    \n    # Đánh dấu AUC cao nhất\n    max_auc_idx = np.argmax(data['val_auc'])\n    max_auc = data['val_auc'][max_auc_idx]\n    max_auc_epoch = epochs[max_auc_idx]\n    ax2.plot(max_auc_epoch, max_auc, 'mo', markersize=8)\n    ax2.annotate(f'Max AUC: {max_auc:.4f}', \n                xy=(max_auc_epoch, max_auc),\n                xytext=(max_auc_epoch, max_auc + 0.05),\n                fontsize=10,\n                arrowprops=dict(facecolor='black', shrink=0.05, width=1.5))\n    \n    # Chỉ đặt ticks là các số nguyên\n    \n    # 3. Biểu đồ Learning Rate\n    ax3 = plt.subplot2grid((4, 4), (2, 0), colspan=2, rowspan=1)\n    ax3.plot(epochs, data['lr'], 'c-', marker='o')\n    ax3.set_title('Learning Rate', fontsize=14)\n    ax3.set_xlabel('Epoch')\n    ax3.set_ylabel('Learning Rate')\n    ax3.set_yscale('log')  # Log scale cho learning rate\n    ax3.grid(True, linestyle='--', alpha=0.6)\n    \n    # Chỉ đặt ticks là các số nguyên\n    \n    # 4. Biểu đồ độ chính xác theo lớp (Class Accuracy)\n    if 'class_acc' in data and data['class_acc']:\n        ax4 = plt.subplot2grid((4, 4), (2, 2), colspan=2, rowspan=1)\n        \n        # Lấy số lượng lớp từ dữ liệu\n        class_keys = sorted([key for key in data['class_acc'][0].keys()])\n        class_names = ['Cover', 'JMiPOD', 'JUNIWARD', 'UERD']  # Tên lớp tương ứng\n        \n        # Tạo mảng class accuracy\n        class_acc_data = {}\n        for class_key in class_keys:\n            class_acc_data[class_key] = [epoch_data.get(class_key, 0) for epoch_data in data['class_acc']]\n        \n        # Vẽ biểu đồ cho từng lớp\n        for class_key in class_keys:\n            class_idx = int(class_key)\n            if class_idx < len(class_names):\n                label = f\"{class_names[class_idx]} (Class {class_key})\"\n            else:\n                label = f\"Class {class_key}\"\n            ax4.plot(epochs, class_acc_data[class_key], marker='o', label=label)\n        \n        ax4.set_title('Độ Chính Xác Theo Lớp', fontsize=14)\n        ax4.set_xlabel('Epoch')\n        ax4.set_ylabel('Accuracy')\n        ax4.legend()\n        ax4.grid(True, linestyle='--', alpha=0.6)\n        \n        # Chỉ đặt ticks là các số nguyên\n    \n\n    \n    # 6. Biểu đồ tóm tắt\n    ax6 = plt.subplot2grid((4, 4), (3, 0), colspan=2, rowspan=1)\n    \n    # Tạo barchart cho epoch cuối cùng\n    if 'class_acc' in data and data['class_acc']:\n        last_epoch_class_acc = data['class_acc'][-1]\n        class_idxs = [int(key) for key in class_keys]\n        class_accs = [last_epoch_class_acc.get(key, 0) for key in class_keys]\n        \n        classes = [class_names[idx] if idx < len(class_names) else f\"Class {idx}\" for idx in class_idxs]\n        bars = ax6.bar(classes, class_accs, color='skyblue')\n        \n        # Thêm giá trị trên mỗi bar\n        for bar in bars:\n            height = bar.get_height()\n            ax6.annotate(f'{height:.2f}',\n                        xy=(bar.get_x() + bar.get_width() / 2, height),\n                        xytext=(0, 3),  # 3 points vertical offset\n                        textcoords=\"offset points\",\n                        ha='center', va='bottom', fontsize=9, rotation=0)\n        \n        ax6.set_title('Độ Chính Xác Theo Lớp (Epoch Cuối)', fontsize=14)\n        ax6.set_ylim(0, 1.0)\n        ax6.set_ylabel('Accuracy')\n        ax6.tick_params(axis='x', rotation=45)\n    \n    # Thiết lập layout\n    for ax in [ax1, ax2, ax3, ax4]:\n        if hasattr(ax, 'xaxis'):\n            ax.set_xticks(epochs)\n    plt.tight_layout()\n    plt.suptitle(f'Quá Trình Huấn Luyện (Epoch {min(epochs)}-{max(epochs)})', \n                fontsize=18, y=0.98)\n    plt.subplots_adjust(top=0.93)\n    \n    # Lưu hình nếu được yêu cầu\n    if save_path:\n        plt.savefig(save_path, dpi=300, bbox_inches='tight')\n        print(f\"[INFO] Đã lưu biểu đồ tại: {save_path}\")\n    \n    plt.show()\n    #return fig","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-14T02:33:29.543258Z","iopub.execute_input":"2025-04-14T02:33:29.543602Z","iopub.status.idle":"2025-04-14T02:33:29.571740Z","shell.execute_reply.started":"2025-04-14T02:33:29.543571Z","shell.execute_reply":"2025-04-14T02:33:29.570749Z"},"jupyter":{"source_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def load_best_model(folder_path):\n    \"\"\"\n    Tìm file model có AUC cao nhất trong một thư mục dựa vào tên file\n    \n    Args:\n        folder_path: Đường dẫn đến thư mục chứa các file model\n        \n    Returns:\n        tuple: (model_path, auc) - Đường dẫn đến file model có AUC cao nhất và giá trị AUC\n    \"\"\"\n    import os\n    import re\n    from glob import glob\n    \n    # Kiểm tra thư mục có tồn tại không\n    if not os.path.exists(folder_path):\n        print(f\"[ERROR] Thư mục '{folder_path}' không tồn tại\")\n        return None, 0.0\n    \n    # Tìm tất cả file .pth trong thư mục\n    model_files = glob(os.path.join(folder_path, \"*.pth\"))\n    \n    if not model_files:\n        print(f\"[WARNING] Không tìm thấy file .pth nào trong thư mục '{folder_path}'\")\n        return None, 0.0\n    \n    # Tìm file có AUC cao nhất\n    best_model = None\n    best_auc = -1.0\n    \n    for model_file in model_files:\n        # Tìm giá trị AUC từ tên file\n        # Pattern để tìm auc + số thập phân, ví dụ: auc0.8241\n        match = re.search(r'auc(\\d+\\.\\d+)', os.path.basename(model_file))\n        \n        if match:\n            try:\n                auc = float(match.group(1))\n                if auc > best_auc:\n                    best_auc = auc\n                    best_model = model_file\n            except ValueError:\n                # Bỏ qua nếu không thể chuyển đổi thành float\n                continue\n    \n    if best_model is None:\n        print(f\"[WARNING] Không tìm thấy file .pth nào có chứa 'auc' trong tên file\")\n        return None, 0.0\n    \n    print(f\"[INFO] Tìm thấy model có AUC cao nhất: {os.path.basename(best_model)} (AUC: {best_auc:.4f})\")\n    return best_model, best_auc\n\n# Hàm để load model từ file\ndef load_model(model_path, device=device):\n    \"\"\"\n    Load model từ file\n    \n    Args:\n        model_path: Đường dẫn đến file model\n        device: Thiết bị để chạy mô hình\n        \n    Returns:\n        model: Mô hình đã load\n    \"\"\"\n    model = EfficientNetwork().to(device)\n    pretrained = torch.load(model_path)\n    if 'model_state_dict' in pretrained:\n        print('* msd detect \\n')\n        model.load_state_dict(pretrained['model_state_dict'])\n    else:\n        model.load_state_dict(pretrained)\n    # model.load_state_dict(torch.load(model_path)['model_state_dict'])\n    return model\n\ndef combine_epoch_data(directory, output_file=None):\n    \"\"\"\n    Kết hợp các file data_epoch{number}.json trong một thư mục\n    \n    Args:\n        directory: Thư mục chứa các file data\n        output_file: Tên file output. Nếu None, tự động tạo tên dựa trên epochs\n        \n    Returns:\n        combined_data: Dữ liệu đã kết hợp\n        output_path: Đường dẫn file output\n    \"\"\"\n    import glob\n    import json\n    import re\n    \n    # Tìm tất cả file data_epoch*.json\n    data_files = glob.glob(os.path.join(directory, \"data_epoch*.json\"))\n    if not data_files:\n        print(f\"[WARNING] Không tìm thấy file data_epoch*.json trong {directory}\")\n        return None, None\n    \n    # Sắp xếp file theo số epoch\n    data_files.sort(key=lambda x: int(re.search(r'data_epoch(\\d+)\\.json', x).group(1)))\n    \n    # Khởi tạo combined_data\n    combined_data = {\n        'epochs': [],\n        'train_loss': [],\n        'val_loss': [],\n        'val_acc': [],\n        'val_auc': [],\n        'lr': [],\n        'class_acc': []\n    }\n    \n    # Đọc và kết hợp dữ liệu từ các file\n    for file_path in data_files:\n        try:\n            with open(file_path, 'r') as f:\n                data = json.load(f)\n            \n            # Thêm dữ liệu vào combined_data\n            combined_data['epochs'].append(data['epoch'] + 1)  # +1 vì epoch đếm từ 0\n            combined_data['train_loss'].append(data['train_loss'])\n            combined_data['val_loss'].append(data['val_loss'])\n            combined_data['val_acc'].append(data['val_acc'])\n            combined_data['val_auc'].append(data['val_auc'])\n            combined_data['lr'].append(data['lr'])\n            combined_data['class_acc'].append(data['class_acc'])\n            \n            print(f\"[INFO] Đã đọc file {os.path.basename(file_path)}\")\n        except Exception as e:\n            print(f\"[ERROR] Không thể đọc file {file_path}: {str(e)}\")\n    \n    # Tìm epoch đầu và cuối\n    if combined_data['epochs']:\n        start_epoch = min(combined_data['epochs'])\n        end_epoch = max(combined_data['epochs'])\n        \n        # Tạo tên file output nếu không được cung cấp\n        if output_file is None:\n            output_file = f\"data_epoch_{start_epoch}_{end_epoch}.json\"\n        \n        output_path = os.path.join('/kaggle/working/', output_file)\n        \n        # Lưu combined_data ra file\n        with open(output_path, 'w') as f:\n            json.dump(combined_data, f, indent=4)\n        \n        print(f\"[INFO] Đã lưu dữ liệu kết hợp tại: {output_path}\")\n        return combined_data, output_path\n    else:\n        print(\"[WARNING] Không có dữ liệu epoch để kết hợp\")\n        return None, None\ndef combine_result_files(directory, output_file=None):\n    \"\"\"\n    Kết hợp các file result (data_epoch_x_y.json) thành một file duy nhất\n    \n    Args:\n        directory: Thư mục chứa các file result\n        output_file: Tên file output. Nếu None, tự động tạo tên dựa trên epochs\n        \n    Returns:\n        combined_data: Dữ liệu đã kết hợp\n        output_path: Đường dẫn file output\n    \"\"\"\n    import glob\n    import json\n    import re\n    \n    # Tìm tất cả file data_epoch_x_y.json\n    result_files = glob.glob(os.path.join(directory, \"data_epoch_*_*.json\"))\n    if not result_files:\n        print(f\"[WARNING] Không tìm thấy file data_epoch_*_*.json trong {directory}\")\n        return None, None\n    \n    # Phân tích start_epoch và end_epoch từ tên file\n    file_info = []\n    for file_path in result_files:\n        match = re.search(r'data_epoch_(\\d+)_(\\d+)\\.json', file_path)\n        if match:\n            start_epoch = int(match.group(1))\n            end_epoch = int(match.group(2))\n            file_info.append((file_path, start_epoch, end_epoch))\n    \n    # Sắp xếp file theo start_epoch\n    file_info.sort(key=lambda x: x[1])\n    \n    # Khởi tạo combined_data\n    combined_data = {\n        'epochs': [],\n        'train_loss': [],\n        'val_loss': [],\n        'val_acc': [],\n        'val_auc': [],\n        'lr': [],\n        'class_acc': []\n    }\n    \n    # Đọc và kết hợp dữ liệu từ các file\n    for file_path, start_epoch, end_epoch in file_info:\n        try:\n            with open(file_path, 'r') as f:\n                data = json.load(f)\n            \n            # Thêm dữ liệu vào combined_data\n            combined_data['epochs'].extend(data['epochs'])\n            combined_data['train_loss'].extend(data['train_loss'])\n            combined_data['val_loss'].extend(data['val_loss'])\n            combined_data['val_acc'].extend(data['val_acc'])\n            combined_data['val_auc'].extend(data['val_auc'])\n            combined_data['lr'].extend(data['lr'])\n            combined_data['class_acc'].extend(data['class_acc'])\n            \n            print(f\"[INFO] Đã đọc file {os.path.basename(file_path)}\")\n        except Exception as e:\n            print(f\"[ERROR] Không thể đọc file {file_path}: {str(e)}\")\n    \n    # Loại bỏ các bản ghi trùng lặp dựa trên epoch\n    unique_epochs = {}\n    for i, epoch in enumerate(combined_data['epochs']):\n        if epoch not in unique_epochs:\n            unique_epochs[epoch] = i\n    \n    # Lọc dữ liệu để loại bỏ trùng lặp\n    for key in combined_data.keys():\n        if key != 'class_acc':  # class_acc là một list của dict, xử lý riêng\n            combined_data[key] = [combined_data[key][i] for i in unique_epochs.values()]\n    \n    # Xử lý riêng cho class_acc\n    combined_data['class_acc'] = [combined_data['class_acc'][i] for i in unique_epochs.values()]\n    \n    # Tìm epoch đầu và cuối sau khi đã lọc\n    if combined_data['epochs']:\n        start_epoch = min(combined_data['epochs'])\n        end_epoch = max(combined_data['epochs'])\n        \n        # Tạo tên file output nếu không được cung cấp\n        if output_file is None:\n            output_file = f\"data_epoch_{start_epoch}_{end_epoch}.json\"\n        \n        output_path = os.path.join('/kaggle/working/', output_file)\n        \n        # Lưu combined_data ra file\n        with open(output_path, 'w') as f:\n            json.dump(combined_data, f, indent=4)\n        \n        print(f\"[INFO] Đã lưu dữ liệu kết hợp tại: {output_path}\")\n        return combined_data, output_path\n    else:\n        print(\"[WARNING] Không có dữ liệu epoch để kết hợp\")\n        return None, None\n\n\n\n\ndef load_checkpoint(directory, epoch=None):\n    \"\"\"\n    Tải checkpoint từ một thư mục với epoch cụ thể\n    hoặc tìm checkpoint của epoch cao nhất nếu epoch=None\n    \n    Args:\n        directory: Thư mục chứa các file checkpoint\n        epoch: Epoch cụ thể cần tải. Nếu None, tải epoch cao nhất\n        \n    Returns:\n        checkpoint: Dữ liệu checkpoint đã tải\n        model_path: Đường dẫn đến file model\n    \"\"\"\n    import glob\n    import re\n    \n    # Tìm tất cả file model\n    model_files = glob.glob(os.path.join(directory, \"*_epoch*_*.pth\"))\n    \n    if not model_files:\n        print(f\"[WARNING] Không tìm thấy file model trong {directory}\")\n        return None, None\n    \n    # Nếu epoch được chỉ định, tìm file model tương ứng\n    if epoch is not None:\n        model_path = None\n        for file_path in model_files:\n            match = re.search(r'_epoch(\\d+)_', file_path)\n            if match and int(match.group(1)) == epoch:\n                model_path = file_path\n                break\n        \n        if model_path is None:\n            print(f\"[WARNING] Không tìm thấy model cho epoch {epoch} trong {directory}\")\n            return None, None\n    else:\n        # Nếu epoch không được chỉ định, tìm file model của epoch cao nhất\n        models_with_epoch = []\n        for file_path in model_files:\n            match = re.search(r'_epoch(\\d+)_', file_path)\n            if match:\n                epoch_num = int(match.group(1))\n                models_with_epoch.append((file_path, epoch_num))\n        \n        if not models_with_epoch:\n            print(f\"[WARNING] Không thể trích xuất thông tin epoch từ tên file model trong {directory}\")\n            return None, None\n        \n        # Sắp xếp theo epoch và lấy epoch cao nhất\n        model_path, highest_epoch = max(models_with_epoch, key=lambda x: x[1])\n        print(f\"[INFO] Tìm thấy model cho epoch cao nhất ({highest_epoch}): {model_path}\")\n    \n    # Tải checkpoint\n    try:\n        checkpoint = torch.load(model_path, map_location='cpu')\n        print(f\"[INFO] Đã tải checkpoint từ {model_path}\")\n        return checkpoint, model_path\n    except Exception as e:\n        print(f\"[ERROR] Không thể tải checkpoint từ {model_path}: {str(e)}\")\n        return None, None","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-14T02:35:46.780344Z","iopub.execute_input":"2025-04-14T02:35:46.780705Z","iopub.status.idle":"2025-04-14T02:35:46.815616Z","shell.execute_reply.started":"2025-04-14T02:35:46.780665Z","shell.execute_reply":"2025-04-14T02:35:46.814936Z"},"jupyter":{"source_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def train_and_evaluate_auto_session(fold_number=0, batch_size=16, \n                              resume_from=None, \n                                   start_epoch=0,\n                                 epochs_per_session=3,\n                                   save_dir='training'):\n    \"\"\"\n    Huấn luyện mô hình với tính toán tự động số epoch cho mỗi session dựa trên thời gian chạy thực tế\n    \n    Args:\n        fold_number: Fold được sử dụng làm tập kiểm thử\n        batch_size: Kích thước batch\n        color_space: Không gian màu ('rgb' hoặc 'ycbcr')\n        resume_from: File checkpoint để tiếp tục từ phiên trước\n        session_number: Số thứ tự phiên\n        total_desired_epochs: Tổng số epoch mong muốn chạy cho toàn bộ quá trình huấn luyện\n        runtime_limit_hours: Giới hạn thời gian chạy (giờ)\n        epochs_per_session: Số epoch cho mỗi session (nếu None, sẽ tự động tính)\n        save_dir: Thư mục lưu dữ liệu\n        \n    Returns:\n        session_info: Thông tin về phiên huấn luyện\n    \"\"\"\n\n    _, cp_path = load_checkpoint(resume_from)\n    # Tạo thư mục lưu trữ\n    os.makedirs(save_dir, exist_ok=True)\n    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n\n    \n    # Bật CUDA benchmarking để tăng tốc\n    torch.backends.cudnn.benchmark = True\n    \n    # Tạo tên phiên dựa trên thời gian và số thứ tự\n    # session_path = os.path.join(save_dir)\n    os.makedirs(save_dir, exist_ok=True)\n    \n    # Tạo dataloader\n    train_loader, val_loader, train_dataset, val_dataset = create_dataloaders(\n        dataset, fold_number, batch_size, num_workers=4\n    )\n    \n    # Khởi tạo model\n    model = EfficientNetwork().to(device)\n    #pretrained = torch.load('/kaggle/input/b2/pytorch/default/1/efficientnet_b2_best_auc0.7873.pth')\n    # if 'model_state_dict' in pretrained:\n    #     print('* msd detect \\n')\n    #     #model.load_state_dict(pretrained['model_state_dict'])\n    # else:\n    #     model.load_state_dict(pretrained)\n    # Thiết lập callbacks với checkpoint\n    checkpoint_callback = CheckpointCallback(\n        checkpoint_interval=1,\n        save_path=save_dir,\n    )\n    \n    history, best_model_path = train_model(\n        model, \n        train_loader, \n        val_loader, \n        num_epochs=epochs_per_session,\n        accumulation_steps=4,\n        callbacks=[checkpoint_callback],\n        device=device,\n        save_path=save_dir,\n        cp_path = cp_path,\n        best_model =  resume_from\n    )\n    \n    # Lưu lịch sử huấn luyện\n \n    # Combine epoch data từ session này\n    combined_data, combined_path = combine_result_files(resume_from)\n    _, total_path = combine_result_files('/kaggle/working/')\n    \n    # Tính toán số epoch và session còn lại\n    \n    # Tìm best model của session này\n    combined_data, combined_path = combine_result_files(path)\n    _, combined_path = combine_epoch_data('/kaggle/working/training')\n    _, total_path = combine_result_files('/kaggle/working/')\n    _,best_model_info  = load_checkpoint('/kaggle/working/training')\n    best_model_path,_ = load_best_model('/kaggle/working/training')\n\n    \n    # Tạo file session_info.json tóm tắt kết quả session\n    uploaded_files = upload_to_uploadcare(files=[best_model_path,total_path,best_model_info])\n    # In thông báo các file cần tải xuống cho lần chạy tiếp theo\n    print(\"\\n[IMPORTANT] HÃY TẢI XUỐNG CÁC FILE SAU ĐỂ TIẾP TỤC HUẤN LUYỆN:\")\n    print(f\"1. Model / Session : {os.path.basename(best_model_path)}\")\n    print(f\"2. Data / Session :{os.path.basename(total_path)}\")\n    print(f\"3. Checkpoint / Epoch: {os.path.basename(best_model_info)}\")\n\n\n\n    \n   ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-14T02:35:59.477119Z","iopub.execute_input":"2025-04-14T02:35:59.477418Z","iopub.status.idle":"2025-04-14T02:35:59.487992Z","shell.execute_reply.started":"2025-04-14T02:35:59.477391Z","shell.execute_reply":"2025-04-14T02:35:59.487218Z"},"jupyter":{"source_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<span style=\"font-size:28px;\"> * RUN TRAINNING * </span>","metadata":{}},{"cell_type":"code","source":"def get_max_number_folder(base_path):\n    \"\"\"\n    Trả về đường dẫn đến thư mục có tên là số lớn nhất trong path gốc\n    \n    Args:\n        base_path: Đường dẫn gốc cần tìm kiếm\n        \n    Returns:\n        String: Đường dẫn đầy đủ đến thư mục có số lớn nhất\n    \"\"\"\n    import os\n    \n    # Kiểm tra xem path gốc có tồn tại không\n    if not os.path.exists(base_path):\n        print(f\"[ERROR] Path gốc không tồn tại: {base_path}\")\n        return None\n    \n    # Lấy danh sách các thư mục trong path gốc\n    try:\n        folders = [f for f in os.listdir(base_path) if os.path.isdir(os.path.join(base_path, f))]\n    except Exception as e:\n        print(f\"[ERROR] Không thể liệt kê thư mục trong {base_path}: {str(e)}\")\n        return None\n    \n    # Lọc ra các thư mục có tên là số\n    numeric_folders = []\n    for folder in folders:\n        try:\n            numeric_folders.append(int(folder))\n        except ValueError:\n            # Bỏ qua các thư mục không phải số\n            continue\n    \n    # Nếu không có thư mục nào là số\n    if not numeric_folders:\n        print(f\"[WARNING] Không tìm thấy thư mục nào có tên là số trong {base_path}\")\n        return None\n    \n    # Tìm số lớn nhất\n    max_number = max(numeric_folders)\n    \n    # Trả về đường dẫn đầy đủ\n    return os.path.join(base_path, str(max_number))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-14T02:36:07.093820Z","iopub.execute_input":"2025-04-14T02:36:07.094098Z","iopub.status.idle":"2025-04-14T02:36:07.101344Z","shell.execute_reply.started":"2025-04-14T02:36:07.094075Z","shell.execute_reply":"2025-04-14T02:36:07.100516Z"},"jupyter":{"source_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"path = get_max_number_folder('/kaggle/input/model-b2/pytorch/model/')\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-14T02:36:14.460081Z","iopub.execute_input":"2025-04-14T02:36:14.460381Z","iopub.status.idle":"2025-04-14T02:36:14.464383Z","shell.execute_reply.started":"2025-04-14T02:36:14.460354Z","shell.execute_reply":"2025-04-14T02:36:14.463432Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Phiên huấn luyện \n\ntrain_and_evaluate_auto_session(\n    fold_number=0,\n    batch_size=16,\n    start_epoch=0,  # Bắt đầu từ epoch 0\n    epochs_per_session=3,   # 3 epochs đầu tiên\n    resume_from= path,  # Không có checkpoint trước đó\n)\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-14T02:36:14.678614Z","iopub.execute_input":"2025-04-14T02:36:14.678882Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Kết hợp lịch sử các phiên\n\ncombined_data, combined_path = combine_result_files(cp_path)\n#history = copy_file_from_input_to_output(combined_path)\n# total_data, total_path = combine_result_files('/kaggle/working/')\n# Vẽ biểu đồ lịch sử kết hợp\nplot_training_from_json(json_file_path=combined_path)\n\n# Tìm model tốt nhất\n#best_model_info = show_latetest_checkpoint_model(cp_path)\nbest_model, _ = load_best_model(cp_path)\n# Load và đánh giá model tốt nhất\n# if best_model_info and best_model_info['model_path']:\n#     model = load_model(best_model_info['model_path'])\n# if best_model :\n#     model = load_model(best_model)  \n#     # Đánh giá model\n#     _, val_loader, _, _ = create_dataloaders(dataset, fold_number=0, batch_size=16)\n#     accuracy, auc_score, confusion_matrix = evaluate_model(model, val_loader)\n    \n#     print(f\"\\n[EVALUATE] Model tốt nhất - Accuracy: {accuracy:.4f}, AUC: {auc_score:.4f}\")\n#     plot_confusion_matrix(confusion_matrix)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-13T08:29:43.635414Z","iopub.execute_input":"2025-04-13T08:29:43.635734Z","iopub.status.idle":"2025-04-13T08:29:44.634139Z","shell.execute_reply.started":"2025-04-13T08:29:43.635701Z","shell.execute_reply":"2025-04-13T08:29:44.633391Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"<span style=\"font-size:28px;\">* Tổng hợp Trainning *</span>","metadata":{}},{"cell_type":"markdown","source":"<span style=\"font-size:38px;\"> 4. TESTING VÀ PREDICT </span>","metadata":{}},{"cell_type":"code","source":"class DatasetSubmissionRetriever(Dataset):\n    \"\"\"\n    Dataset dùng cho phần dự đoán tập Test\n    \"\"\"\n    def __init__(self, image_names, transforms=None, color_space='rgb'):\n        super().__init__()\n        self.image_names = image_names\n        self.transforms = transforms\n        self.color_space = color_space\n\n    def __getitem__(self, index: int):\n        image_name = self.image_names[index]\n        # Sử dụng hàm tiền xử lý đã tạo trước đó\n        image = preprocess_image(\n            f'{DATA_ROOT_PATH}/Test/{image_name}',\n            transform=self.transforms,\n            color_space=self.color_space\n        )\n        return image_name, image\n\n    def __len__(self) -> int:\n        return len(self.image_names)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-13T08:33:04.415896Z","iopub.execute_input":"2025-04-13T08:33:04.416191Z","iopub.status.idle":"2025-04-13T08:33:04.422381Z","shell.execute_reply.started":"2025-04-13T08:33:04.416166Z","shell.execute_reply":"2025-04-13T08:33:04.421565Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def predict_with_tta(model, images, device=device):\n    \"\"\"\n    Dự đoán với Test Time Augmentation (TTA)\n    \n    Args:\n        model: Mô hình đã huấn luyện\n        images: Tensor ảnh đầu vào\n        device: Thiết bị để chạy mô hình\n        \n    Returns:\n        outputs: Dự đoán với TTA\n    \"\"\"\n    # Dự đoán gốc\n    out0 = model(images)\n    \n    # Lật dọc\n    images_vertical = images.flip(2)\n    out1 = model(images_vertical)\n    \n    # Lật ngang\n    images_horizontal = images.flip(3)\n    out2 = model(images_horizontal)\n    \n    # Lật cả dọc và ngang\n    images_both = images_horizontal.flip(2)\n    out3 = model(images_both)\n    \n    # Kết hợp kết quả (trọng số bằng nhau)\n    outputs = (out0 + out1 + out2 + out3) / 4.0\n    \n    return outputs","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-13T08:33:04.426793Z","iopub.execute_input":"2025-04-13T08:33:04.427139Z","iopub.status.idle":"2025-04-13T08:33:04.440249Z","shell.execute_reply.started":"2025-04-13T08:33:04.427102Z","shell.execute_reply":"2025-04-13T08:33:04.439484Z"},"jupyter":{"source_hidden":true}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def generate_predictions(model_path, batch_size=16, num_workers=4, use_tta=True, color_space='rgb'):\n    \"\"\"\n    Tạo dự đoán cho tập Test và tạo file submission\n    \n    Args:\n        model_path: Đường dẫn đến mô hình tốt nhất\n        batch_size: Kích thước batch\n        num_workers: Số lượng worker\n        use_tta: Có sử dụng Test Time Augmentation hay không\n        color_space: Không gian màu sử dụng ('rgb' hoặc 'ycbcr')\n    \n    Returns:\n        submission_df: DataFrame chứa kết quả dự đoán\n    \"\"\"\n    # Tải mô hình tốt nhất\n    print(f\"[INFO] Đang tải mô hình từ {model_path}\")\n    model = load_model(model_path)\n    model.eval()\n    \n    # Lấy danh sách ảnh Test\n    test_images = np.array([os.path.basename(path) for path in glob(f'{DATA_ROOT_PATH}/Test/*.jpg')])\n    print(f\"[INFO] Tìm thấy {len(test_images)} ảnh trong tập Test\")\n    \n    # Tạo dataset và dataloader\n    test_dataset = DatasetSubmissionRetriever(\n        image_names=test_images,\n        transforms=get_valid_transforms(),\n        color_space=color_space\n    )\n    \n    test_loader = DataLoader(\n        test_dataset,\n        batch_size=batch_size,\n        shuffle=False,\n        num_workers=num_workers,\n        pin_memory=False,\n        drop_last=False,\n    )\n    \n    # Biến lưu kết quả\n    result = {'Id': [], 'Label': []}\n    \n    # Thực hiện dự đoán\n    print(f\"[INFO] Bắt đầu dự đoán với {'TTA' if use_tta else 'không TTA'}\")\n    with torch.no_grad():\n        for image_names, images in tqdm(test_loader, desc=\"Predicting\"):\n            images = images.to(device)\n            \n            # Nếu dùng TTA\n            if use_tta:\n                outputs = predict_with_tta(model, images)\n            else:\n                outputs = model(images)\n            \n            # Tính xác suất\n            probs = F.softmax(outputs, dim=1).cpu().numpy()\n            \n            # Tính điểm steganographic (Cách tính từ cuộc thi: 1 - P(Cover))\n            stego_scores = 1 - probs[:, 0]\n            \n            # Lưu kết quả\n            result['Id'].extend(image_names)\n            result['Label'].extend(stego_scores)\n    \n    # Tạo DataFrame kết quả\n    submission_df = pd.DataFrame(result)\n    print(f\"[INFO] Hoàn thành dự đoán. Số lượng dự đoán: {len(submission_df)}\")\n    \n    return submission_df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-13T08:33:45.984740Z","iopub.execute_input":"2025-04-13T08:33:45.985114Z","iopub.status.idle":"2025-04-13T08:33:45.995647Z","shell.execute_reply.started":"2025-04-13T08:33:45.985083Z","shell.execute_reply":"2025-04-13T08:33:45.994778Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def save_submission(submission_df, filename='submission.csv'):\n    \"\"\"\n    Lưu kết quả dự đoán vào file submission\n    \n    Args:\n        submission_df: DataFrame chứa kết quả dự đoán\n        filename: Tên file lưu kết quả\n    \"\"\"\n    submission_df.to_csv(filename, index=False)\n    print(f\"[INFO] Đã lưu file submission tại: {filename}\")\n    \n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-13T08:33:50.248297Z","iopub.execute_input":"2025-04-13T08:33:50.248615Z","iopub.status.idle":"2025-04-13T08:33:50.252972Z","shell.execute_reply.started":"2025-04-13T08:33:50.248583Z","shell.execute_reply":"2025-04-13T08:33:50.252253Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def run_inference(model_path=None, batch_size=32, use_tta=True, color_space='rgb'):\n    \"\"\"\n    Chạy toàn bộ quá trình inference từ tải mô hình đến tạo file submission\n    \n    Args:\n        model_path: Đường dẫn đến mô hình tốt nhất. Nếu None, sẽ tìm mô hình có AUC cao nhất\n        batch_size: Kích thước batch\n        use_tta: Có sử dụng Test Time Augmentation hay không\n        color_space: Không gian màu sử dụng ('rgb' hoặc 'ycbcr')\n        output_file: Tên file submission. Nếu None, sẽ tạo tên theo thời gian\n    \n    Returns:\n        submission_df: DataFrame chứa kết quả dự đoán\n    \"\"\"\n\n    \n    # Nếu không cung cấp tên file output, tạo tên theo thời gian\n    \n    # Tạo dự đoán\n    submission_df = generate_predictions(\n        model_path=model_path,\n        batch_size=batch_size,\n        use_tta=use_tta,\n        color_space=color_space\n    )\n    \n    # Lưu kết quả\n    save_submission(submission_df)\n    \n    return submission_df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-13T08:34:56.187777Z","iopub.execute_input":"2025-04-13T08:34:56.188159Z","iopub.status.idle":"2025-04-13T08:34:56.192771Z","shell.execute_reply.started":"2025-04-13T08:34:56.188128Z","shell.execute_reply":"2025-04-13T08:34:56.192090Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"submission_df = run_inference(\n    model_path= best_model ,  # Tự động tìm model tốt nhất\n    batch_size=32,    # Có thể tăng batch size khi dự đoán vì không cần lưu gradient\n    use_tta=True,     # Sử dụng Test Time Augmentation từ Notebook-1\n    color_space='rgb' # Cùng không gian màu với lúc huấn luyện\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-13T08:34:59.817310Z","iopub.execute_input":"2025-04-13T08:34:59.817627Z","iopub.status.idle":"2025-04-13T08:37:34.009747Z","shell.execute_reply.started":"2025-04-13T08:34:59.817591Z","shell.execute_reply":"2025-04-13T08:37:34.008788Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def visualize_submission(submission_file='submission.csv', num_examples=5, test_dir=None):\n    \"\"\"\n    Trực quan hóa dữ liệu từ file submission.csv\n    \n    Args:\n        submission_file: Đường dẫn đến file submission.csv\n        num_examples: Số lượng ví dụ ảnh hiển thị\n        test_dir: Đường dẫn đến thư mục chứa ảnh test (mặc định là DATA_ROOT_PATH/Test)\n    \"\"\"\n    import pandas as pd\n    import matplotlib.pyplot as plt\n    import seaborn as sns\n    import numpy as np\n    import cv2\n    from glob import glob\n    import os\n    \n    # Đọc file submission\n    df = pd.read_csv(submission_file)\n    print(f\"Đã đọc file {submission_file} với {len(df)} dự đoán\")\n    \n    # Tạo giao diện subplot\n    fig = plt.figure(figsize=(20, 15))\n    \n    # 1. Biểu đồ phân phối xác suất\n    ax1 = plt.subplot2grid((3, 3), (0, 0), colspan=2, rowspan=1)\n    try:\n        sns.histplot(df['Label'], bins=50, kde=True, ax=ax1)\n    except:\n        # Fallback nếu không có histplot\n        ax1.hist(df['Label'], bins=50)\n        ax1.set_ylabel('Số lượng')\n    \n    ax1.set_title('Phân phối xác suất steganography', fontsize=15)\n    ax1.set_xlabel('Xác suất chứa steganography')\n    \n    # 2. Biểu đồ tần suất tích lũy\n    ax2 = plt.subplot2grid((3, 3), (0, 2), colspan=1, rowspan=1)\n    sorted_probs = np.sort(df['Label'].values)\n    cumulative = np.linspace(0, 1, len(sorted_probs))\n    ax2.plot(sorted_probs, cumulative)\n    ax2.set_title('Tần suất tích lũy', fontsize=15)\n    ax2.set_xlabel('Xác suất')\n    ax2.set_ylabel('Tần suất tích lũy')\n    ax2.grid(True)\n    \n    # 3. Bảng thống kê\n    ax3 = plt.subplot2grid((3, 3), (1, 0), colspan=1, rowspan=1)\n    ax3.axis('off')\n    \n    stats = {\n        'Min': df['Label'].min(),\n        'Max': df['Label'].max(),\n        'Mean': df['Label'].mean(),\n        'Median': df['Label'].median(),\n        'Std': df['Label'].std(),\n        'Count': len(df),\n        '% High Confidence\\n(>0.8)': (df['Label'] > 0.8).mean() * 100,\n        '% Low Confidence\\n(<0.2)': (df['Label'] < 0.2).mean() * 100\n    }\n    \n    stats_text = \"THỐNG KÊ SUBMISSION\\n\\n\"\n    for k, v in stats.items():\n        if isinstance(v, float):\n            stats_text += f\"{k}: {v:.4f}\\n\"\n        else:\n            stats_text += f\"{k}: {v}\\n\"\n    \n    ax3.text(0.1, 0.9, stats_text, fontsize=14, va='top')\n    \n    # 4. Dự đoán theo phân vị\n    ax4 = plt.subplot2grid((3, 3), (1, 1), colspan=2, rowspan=1)\n    \n    # Chia thành 5 nhóm phân vị\n    quantiles = [0, 0.2, 0.4, 0.6, 0.8, 1.0]\n    labels = ['0-20%', '20-40%', '40-60%', '60-80%', '80-100%']\n    df['quantile'] = pd.qcut(df['Label'], q=quantiles, labels=labels)\n    \n    quantile_counts = df['quantile'].value_counts().sort_index()\n    ax4.bar(quantile_counts.index, quantile_counts.values, color='skyblue')\n    ax4.set_title('Phân phối theo nhóm xác suất', fontsize=15)\n    ax4.set_xlabel('Nhóm xác suất')\n    ax4.set_ylabel('Số lượng')\n    \n    for i, v in enumerate(quantile_counts.values):\n        ax4.text(i, v + 5, str(v), ha='center')\n    \n    # 5. Hiển thị một số ảnh ví dụ\n    if test_dir is None:\n        test_dir = f'{DATA_ROOT_PATH}/Test/'\n    \n    # Chỉ hiển thị ảnh nếu thư mục test tồn tại\n    if os.path.exists(test_dir):\n        # Chọn một số ảnh đại diện từ các mức xác suất khác nhau\n        sample_indices = []\n        quantiles = [0.05, 0.25, 0.5, 0.75, 0.95]\n        \n        for q in quantiles:\n            idx = (np.abs(df['Label'].values - np.quantile(df['Label'].values, q))).argmin()\n            sample_indices.append(idx)\n            \n        # Hiển thị các ảnh ví dụ\n        for i, idx in enumerate(sample_indices[:num_examples]):\n            img_name = df.iloc[idx]['Id']\n            prob = df.iloc[idx]['Label']\n            \n            ax = plt.subplot2grid((3, 3), (2, i % 3), colspan=1, rowspan=1)\n            \n            try:\n                img = cv2.imread(os.path.join(test_dir, img_name))\n                img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)\n                ax.imshow(img)\n                ax.set_title(f\"P(Stego): {prob:.4f}\", fontsize=14)\n                ax.axis('off')\n            except:\n                ax.text(0.5, 0.5, f\"Không thể hiển thị ảnh\\n{img_name}\\nP(Stego): {prob:.4f}\", \n                        ha='center', va='center', fontsize=12)\n                ax.axis('off')\n    \n    plt.tight_layout()\n    plt.suptitle('Trực quan hóa kết quả Submission', fontsize=20, y=0.98)\n    plt.subplots_adjust(top=0.9)\n    plt.show()\n    \n    # Một số phân tích thêm\n    print(\"\\nPhân tích dự đoán:\")\n    print(f\"Số lượng ảnh có khả năng cao chứa steganography (>0.8): {(df['Label'] > 0.8).sum()} ({(df['Label'] > 0.8).mean()*100:.2f}%)\")\n    print(f\"Số lượng ảnh có khả năng thấp chứa steganography (<0.2): {(df['Label'] < 0.2).sum()} ({(df['Label'] < 0.2).mean()*100:.2f}%)\")\n    print(f\"Số lượng ảnh có dự đoán không chắc chắn (0.4-0.6): {((df['Label'] >= 0.4) & (df['Label'] <= 0.6)).sum()} ({((df['Label'] >= 0.4) & (df['Label'] <= 0.6)).mean()*100:.2f}%)\")\n    \n    return df","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-13T08:38:47.575658Z","iopub.execute_input":"2025-04-13T08:38:47.576008Z","iopub.status.idle":"2025-04-13T08:38:47.602126Z","shell.execute_reply.started":"2025-04-13T08:38:47.575967Z","shell.execute_reply":"2025-04-13T08:38:47.601260Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Ví dụ sử dụng\ndef analyze_submission_results(submission_file='/kaggle/working/submission.csv'):\n    \"\"\"\n    Phân tích kết quả từ file submission với trực quan hóa\n    \n    Args:\n        submission_file: Đường dẫn đến file submission.csv\n    \"\"\"\n    print(f\"[INFO] Phân tích kết quả từ file {submission_file}\")\n    \n    # Trực quan hóa kết quả\n    submission_df = visualize_submission(\n        submission_file=submission_file,\n        num_examples=3,\n        test_dir=f'{DATA_ROOT_PATH}/Test/'\n    )\n    \n    return submission_df\n\nanalyze_submission_results()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-04-13T08:38:56.222986Z","iopub.execute_input":"2025-04-13T08:38:56.223277Z","iopub.status.idle":"2025-04-13T08:38:57.130717Z","shell.execute_reply.started":"2025-04-13T08:38:56.223252Z","shell.execute_reply":"2025-04-13T08:38:57.129930Z"}},"outputs":[],"execution_count":null}]}