{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"none","dataSources":[],"dockerImageVersionId":28755,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# 1. Khởi tạo và Giải nén dữ liệu","metadata":{}},{"cell_type":"code","source":"import os\nimport zipfile\nimport glob\nimport pandas as pd\nimport numpy as np\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\nfrom PIL import Image\nfrom sklearn.model_selection import train_test_split\nfrom tqdm import tqdm\n\n# Thiết lập GPU nếu có\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f\"Đang sử dụng thiết bị: {device}\")\n\n# ==========================================\n# 1. GIẢI NÉN DỮ LIỆU TỪ KAGGLE INPUT\n# ==========================================\nINPUT_DIR = '/kaggle/input/competitions/dogs-vs-cats-redux-kernels-edition'\nWORK_DIR = '/kaggle/working'\n\n# Giải nén tập Train và Test vào thư mục làm việc hiện tại\nfor folder in ['train.zip', 'test.zip']:\n    zip_path = os.path.join(INPUT_DIR, folder)\n    if os.path.exists(zip_path):\n        print(f\"Đang giải nén {folder}...\")\n        with zipfile.ZipFile(zip_path, 'r') as zip_ref:\n            zip_ref.extractall(WORK_DIR)\n\nTRAIN_DIR = os.path.join(WORK_DIR, 'train')\nTEST_DIR = os.path.join(WORK_DIR, 'test')\nprint(\"Hoàn tất giải nén!\")","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-08-05T09:53:07.629670Z","iopub.execute_input":"2026-08-05T09:53:07.629993Z","iopub.status.idle":"2026-08-05T09:53:15.736439Z","shell.execute_reply.started":"2026-08-05T09:53:07.629961Z","shell.execute_reply":"2026-08-05T09:53:15.735649Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 2. Xây Dựng Data Pipeline (Custom Dataset & DataLoader)\nVì nhãn nằm ở tên file (`cat.1.jpg`), chúng ta sẽ code hàm `__getitem__` để tự động bóc tách chữ \"cat\" hoặc \"dog\" ra làm nhãn.\n* Quy ước: **Cat = 0.0**, **Dog = 1.0** (Dùng kiểu float để tính xác suất).","metadata":{}},{"cell_type":"code","source":"# Thu thập toàn bộ đường dẫn ảnh Train\nall_train_paths = glob.glob(f\"{TRAIN_DIR}/*.jpg\")\n\n# Chia tập Train thành 80% Huấn luyện và 20% Đánh giá (Validation)\ntrain_paths, val_paths = train_test_split(all_train_paths, test_size=0.2, random_state=42)\nprint(f\"Số ảnh Train: {len(train_paths)} | Số ảnh Val: {len(val_paths)}\")\n\n# Khởi tạo chuỗi Biến đổi ảnh (Transforms) cơ bản\ntransform_pipeline = transforms.Compose([\n    transforms.Resize((128, 128)),  # Ép kích thước về 128x128\n    transforms.ToTensor(),          # Đưa về dạng Tensor [0, 1]\n    transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]) # Chuẩn hóa\n])\n\n# Định nghĩa Custom Dataset\nclass DogCatDataset(Dataset):\n    def __init__(self, file_paths, is_test=False, transform=None):\n        self.file_paths = file_paths\n        self.is_test = is_test\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.file_paths)\n\n    def __getitem__(self, idx):\n        img_path = self.file_paths[idx]\n        image = Image.open(img_path).convert('RGB')\n        \n        if self.transform:\n            image = self.transform(image)\n            \n        if self.is_test:\n            # Xử lý tập Test: Lấy ID từ tên file (vd: '123.jpg' -> 123)\n            img_id = int(os.path.basename(img_path).split('.')[0])\n            return image, img_id\n        else:\n            # Xử lý tập Train/Val: Bóc tách nhãn từ tên file\n            filename = os.path.basename(img_path)\n            # Label = 1 nếu tên chứa 'dog', ngược lại = 0\n            label = 1.0 if 'dog' in filename else 0.0\n            # Trả về nhãn dưới dạng FloatTensor (1 chiều)\n            return image, torch.tensor([label], dtype=torch.float32)\n\n# Khởi tạo DataLoader\ntrain_dataset = DogCatDataset(train_paths, is_test=False, transform=transform_pipeline)\nval_dataset = DogCatDataset(val_paths, is_test=False, transform=transform_pipeline)\n\n# Đặt num_workers=2 để CPU nạp dữ liệu song song (Chống nghẽn I/O)\ntrain_loader = DataLoader(train_dataset, batch_size=64, shuffle=True, num_workers=2)\nval_loader = DataLoader(val_dataset, batch_size=64, shuffle=False, num_workers=2)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-05T09:53:15.737634Z","iopub.execute_input":"2026-08-05T09:53:15.737948Z","iopub.status.idle":"2026-08-05T09:53:15.802352Z","shell.execute_reply.started":"2026-08-05T09:53:15.737924Z","shell.execute_reply":"2026-08-05T09:53:15.801641Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 3. Xây dựng Kiến trúc CNN\nThiết kế mạng nơ-ron phân cấp gồm 3 khối: Tích chập (Trích xuất đặc trưng) -> ReLU (Kích hoạt) -> MaxPool (Giảm chiều không gian). Cuối cùng là các lớp Tuyến tính (Linear).\nVì Kaggle yêu cầu xuất ra xác suất, đầu ra mạng chỉ có **1 Neuron duy nhất**","metadata":{}},{"cell_type":"code","source":"class BaselineCNN(nn.Module):\n    def __init__(self):\n        super(BaselineCNN, self).__init__()\n        \n        # Khối 1: [3, 128, 128] -> [16, 64, 64]\n        self.conv1 = nn.Conv2d(3, 16, kernel_size=3, padding=1)\n        self.relu1 = nn.ReLU()\n        self.pool1 = nn.MaxPool2d(2, 2)\n        \n        # Khối 2: [16, 64, 64] -> [32, 32, 32]\n        self.conv2 = nn.Conv2d(16, 32, kernel_size=3, padding=1)\n        self.relu2 = nn.ReLU()\n        self.pool2 = nn.MaxPool2d(2, 2)\n        \n        # Khối 3: [32, 32, 32] -> [64, 16, 16]\n        self.conv3 = nn.Conv2d(32, 64, kernel_size=3, padding=1)\n        self.relu3 = nn.ReLU()\n        self.pool3 = nn.MaxPool2d(2, 2)\n        \n        # Kích thước sau 3 lần MaxPool: 128 / (2*2*2) = 16\n        self.flatten_dim = 64 * 16 * 16\n        \n        # Khối phân loại (Đầu ra 1 node)\n        self.fc = nn.Linear(self.flatten_dim, 512)\n        self.relu_fc = nn.ReLU()\n        self.output_layer = nn.Linear(512, 1)\n\n    def forward(self, x):\n        x = self.pool1(self.relu1(self.conv1(x)))\n        x = self.pool2(self.relu2(self.conv2(x)))\n        x = self.pool3(self.relu3(self.conv3(x)))\n        \n        x = x.view(x.size(0), -1) # Flatten\n        x = self.relu_fc(self.fc(x))\n        x = self.output_layer(x)\n        return x\n\nmodel = BaselineCNN().to(device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-08-05T09:53:15.803441Z","execution_failed":"2026-08-05T09:53:14.715Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 4. Vòng Lặp Huấn Luyện (Training Loop)\nĐể tối ưu hóa bài toán phân loại nhị phân (Binary Classification) xuất ra xác suất, chúng ta dùng hàm mất mát **`BCEWithLogitsLoss`**. Hàm này kết hợp sẵn thuật toán xấp xỉ Sigmoid bên trong, giúp mô hình ổn định hơn so với việc tách rời.","metadata":{}},{"cell_type":"code","source":"# Khai báo hàm Mất mát và Tối ưu\ncriterion = nn.BCEWithLogitsLoss()\noptimizer = optim.Adam(model.parameters(), lr=0.001)\n\nEPOCHS = 5\n\nfor epoch in range(EPOCHS):\n    # ---------------- BƯỚC TRAIN ----------------\n    model.train()\n    train_loss = 0\n    \n    progress_bar = tqdm(train_loader, desc=f\"Epoch {epoch+1}/{EPOCHS} [Train]\")\n    for images, labels in progress_bar:\n        images, labels = images.to(device), labels.to(device)\n        \n        optimizer.zero_grad()\n        outputs = model(images)\n        loss = criterion(outputs, labels)\n        loss.backward()\n        optimizer.step()\n        \n        train_loss += loss.item()\n        progress_bar.set_postfix(loss=loss.item())\n        \n    avg_train_loss = train_loss / len(train_loader)\n    \n    # ---------------- BƯỚC VALIDATION ----------------\n    model.eval()\n    val_loss = 0\n    correct = 0\n    total = 0\n    \n    with torch.no_grad():\n        for images, labels in val_loader:\n            images, labels = images.to(device), labels.to(device)\n            outputs = model(images)\n            loss = criterion(outputs, labels)\n            val_loss += loss.item()\n            \n            # Tính accuracy (Ép đầu ra qua hàm Sigmoid để thành Xác suất [0,1])\n            probs = torch.sigmoid(outputs)\n            predicted = (probs >= 0.5).float() # Xác suất >= 50% là Chó\n            \n            total += labels.size(0)\n            correct += (predicted == labels).sum().item()\n            \n    avg_val_loss = val_loss / len(val_loader)\n    val_acc = 100. * correct / total\n    \n    print(f\"Tổng kết Epoch {epoch+1}: Train Loss = {avg_train_loss:.4f} | Val Loss = {avg_val_loss:.4f} | Val Accuracy = {val_acc:.2f}%\\n\")","metadata":{"trusted":true,"execution":{"execution_failed":"2026-08-05T09:53:14.715Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 5. Dự đoán tập Test & Tạo file Submission\nBước cuối cùng là nạp toàn bộ ảnh trong tập Test, chạy qua mô hình đã được huấn luyện. \n*Quan trọng:* Chúng ta phải áp dụng hàm **`torch.sigmoid()`** lên đầu ra cuối cùng của mô hình để ép giá trị về đúng khoảng $[0.0, 1.0]$.","metadata":{}},{"cell_type":"code","source":"# 1. Thu thập đường dẫn ảnh Test\ntest_paths = glob.glob(f\"{TEST_DIR}/*.jpg\")\n\n# Tạo DataLoader cho tập Test (Lưu ý: shuffle=False)\ntest_dataset = DogCatDataset(test_paths, is_test=True, transform=transform_pipeline)\ntest_loader = DataLoader(test_dataset, batch_size=64, shuffle=False, num_workers=2)\n\nprint(\"\\nĐANG DỰ ĐOÁN TẬP TEST...\")\nmodel.eval()\nsubmission_data = []\n\nwith torch.no_grad():\n    for images, img_ids in tqdm(test_loader, desc=\"Testing\"):\n        images = images.to(device)\n        outputs = model(images)\n        \n        # Áp dụng hàm Sigmoid để chuyển logits thành xác suất từ 0 đến 1\n        probabilities = torch.sigmoid(outputs).cpu().numpy()\n        \n        # Gom ID và Xác suất lại với nhau\n        for img_id, prob in zip(img_ids.numpy(), probabilities):\n            submission_data.append({'id': img_id, 'label': prob[0]})\n\n# 2. Tạo DataFrame và lưu file CSV\nsubmission_df = pd.DataFrame(submission_data)\n\n# Sắp xếp lại ID cho gọn gàng (không bắt buộc nhưng nên làm)\nsubmission_df = submission_df.sort_values(by='id')\n\nsubmission_df.to_csv('submission.csv', index=False)\nprint(\"\\nHOÀN TẤT! File 'submission.csv' đã sẵn sàng.\")\ndisplay(submission_df.head(10))\n\n# Tùy chọn: Xóa dữ liệu giải nén để làm nhẹ ổ đĩa Kaggle\nimport shutil\nshutil.rmtree(TRAIN_DIR)\nshutil.rmtree(TEST_DIR)","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}