{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":96416,"databundleVersionId":11456366,"sourceType":"competition"}],"dockerImageVersionId":31090,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"# =============================================================================\n# BƯỚC 0: CÀI ĐẶT THƯ VIỆN\n# =============================================================================\n!pip install timm -q\nprint(\"Thư viện 'timm' đã được cài đặt.\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# BƯỚC 1: IMPORT CÁC THƯ VIỆN\n# =============================================================================\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\nimport pandas as pd\nimport numpy as np\nfrom sklearn.model_selection import StratifiedKFold\nimport os\nimport gc\nimport timm\n\nprint(\"Tất cả các thư viện đã được import thành công!\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# BƯỚC 2: CẤU HÌNH DỰ ÁN\n# =============================================================================\nclass Config:\n    TRAIN_FILE = '/kaggle/input/digit-recognizer-challenge/train.csv'\n    TEST_FILE = '/kaggle/input/digit-recognizer-challenge/test.csv'\n    SUBMISSION_FILE = '/kaggle/working/submission.csv'\n    \n    DEVICE = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n    EPOCHS = 20\n    BATCH_SIZE = 128\n    LEARNING_RATE = 1e-3\n    RANDOM_STATE = 42\n    N_SPLITS = 5\n    N_TTA = 10\n    LABEL_SMOOTHING = 0.1\n\nprint(f\"Thiết bị đang sử dụng: {Config.DEVICE}\")\nprint(f\"Chiến lược FINAL BOSS: Ensemble lai (ResNet18 + Vision Transformer).\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# BƯỚC 3: XÂY DỰNG LỚP DATASET (Không thay đổi)\n# =============================================================================\nclass MNISTDataset(Dataset):\n    def __init__(self, images, labels=None, transform=None):\n        self.images = images; self.labels = labels; self.transform = transform\n    def __len__(self): return len(self.images)\n    def __getitem__(self, idx):\n        image = self.images[idx].reshape(28, 28)\n        label = self.labels[idx] if self.labels is not None else -1\n        if self.transform: image = self.transform(image)\n        return image, label\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# BƯỚC 4: ĐỊNH NGHĨA CÁC PHÉP BIẾN ĐỔI (TRANSFORMS)\n# =============================================================================\n# --- Transform cho ResNet ---\nresnet_train_transform = transforms.Compose([\n    transforms.ToPILImage(),\n    transforms.RandomAffine(degrees=10, translate=(0.1, 0.1), scale=(0.9, 1.1)),\n    transforms.ToTensor(),\n    transforms.Normalize((0.1307,), (0.3081,)),\n    transforms.RandomErasing(p=0.5, scale=(0.02, 0.2), ratio=(0.3, 3.3), value=0),\n])\nresnet_test_transform = transforms.Compose([transforms.ToTensor(), transforms.Normalize((0.1307,), (0.3081,))])\n\n# Thêm bước Resize để phóng to ảnh lên 224x224\nvit_train_transform = transforms.Compose([\n    transforms.ToPILImage(),\n    transforms.Resize((224, 224)), # <-- ĐÂY LÀ BƯỚC QUAN TRỌNG\n    transforms.RandomAffine(degrees=10, translate=(0.1, 0.1), scale=(0.9, 1.1)),\n    transforms.ToTensor(),\n    transforms.Normalize((0.1307,), (0.3081,)),\n    transforms.RandomErasing(p=0.5, scale=(0.02, 0.2), ratio=(0.3, 3.3), value=0),\n])\nvit_test_transform = transforms.Compose([\n    transforms.ToPILImage(),\n    transforms.Resize((224, 224)), # <-- ĐÂY LÀ BƯỚC QUAN TRỌNG\n    transforms.ToTensor(),\n    transforms.Normalize((0.1307,), (0.3081,))\n])\nprint(\"Đã định nghĩa các bộ transform riêng cho ResNet và ViT.\")\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# BƯỚC 5: ĐỊNH NGHĨA CÁC KIẾN TRÚC MẠNG (Không thay đổi)\n# =============================================================================\nclass BasicBlock(nn.Module):\n    expansion = 1\n    def __init__(self, in_planes, planes, stride=1):\n        super(BasicBlock, self).__init__()\n        self.conv1 = nn.Conv2d(in_planes, planes, kernel_size=3, stride=stride, padding=1, bias=False)\n        self.bn1 = nn.BatchNorm2d(planes); self.conv2 = nn.Conv2d(planes, planes, kernel_size=3, stride=1, padding=1, bias=False)\n        self.bn2 = nn.BatchNorm2d(planes); self.shortcut = nn.Sequential()\n        if stride != 1 or in_planes != self.expansion*planes: self.shortcut = nn.Sequential(nn.Conv2d(in_planes, self.expansion*planes, kernel_size=1, stride=stride, bias=False), nn.BatchNorm2d(self.expansion*planes))\n    def forward(self, x):\n        out = nn.ReLU(inplace=True)(self.bn1(self.conv1(x))); out = self.bn2(self.conv2(out)); out += self.shortcut(x); out = nn.ReLU(inplace=True)(out)\n        return out\nclass ResNet(nn.Module):\n    def __init__(self, block, num_blocks, num_classes=10):\n        super(ResNet, self).__init__(); self.in_planes = 64; self.conv1 = nn.Conv2d(1, 64, kernel_size=3, stride=1, padding=1, bias=False)\n        self.bn1 = nn.BatchNorm2d(64); self.layer1 = self._make_layer(block, 64, num_blocks[0], stride=1); self.layer2 = self._make_layer(block, 128, num_blocks[1], stride=2)\n        self.layer3 = self._make_layer(block, 256, num_blocks[2], stride=2); self.layer4 = self._make_layer(block, 512, num_blocks[3], stride=2)\n        self.linear = nn.Linear(512*block.expansion, num_classes)\n    def _make_layer(self, block, planes, num_blocks, stride):\n        strides = [stride] + [1]*(num_blocks-1); layers = []\n        for s in strides: layers.append(block(self.in_planes, planes, s)); self.in_planes = planes * block.expansion\n        return nn.Sequential(*layers)\n    def forward(self, x):\n        out = nn.ReLU(inplace=True)(self.bn1(self.conv1(x))); out = self.layer1(out); out = self.layer2(out); out = self.layer3(out); out = self.layer4(out)\n        out = nn.AdaptiveAvgPool2d((1, 1))(out); out = out.view(out.size(0), -1); out = self.linear(out)\n        return out\ndef ResNet18(): return ResNet(BasicBlock, [2, 2, 2, 2])\ndef create_vit(): return timm.create_model(\"vit_tiny_patch16_224\", pretrained=True, in_chans=1, num_classes=10)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# BƯỚC 6: ĐỊNH NGHĨA HÀM MẤT MÁT (Không thay đổi)\n# =============================================================================\nclass LabelSmoothingCrossEntropy(nn.Module):\n    def __init__(self, classes, smoothing=0.1):\n        super(LabelSmoothingCrossEntropy, self).__init__(); self.confidence = 1.0 - smoothing; self.smoothing = smoothing; self.cls = classes; self.logsoftmax = nn.LogSoftmax(dim=-1)\n    def forward(self, pred, target):\n        pred = self.logsoftmax(pred)\n        with torch.no_grad():\n            true_dist = torch.zeros_like(pred); true_dist.fill_(self.smoothing / (self.cls - 1)); true_dist.scatter_(1, target.data.unsqueeze(1), self.confidence)\n        return torch.mean(torch.sum(-true_dist * pred, dim=-1))\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# BƯỚC 7: HÀM HUẤN LUYỆN VÀ DỰ ĐOÁN CHUNG\n# =============================================================================\ndef train_and_predict(model_name, X, y, X_test):\n    test_predictions = []\n    skf = StratifiedKFold(n_splits=Config.N_SPLITS, shuffle=True, random_state=Config.RANDOM_STATE)\n\n    for fold, (train_idx, val_idx) in enumerate(skf.split(X, y)):\n        print(f\"\\n===== FOLD {fold+1}/{Config.N_SPLITS} for {model_name} =====\")\n        X_train, X_val, y_train, y_val = X[train_idx], X[val_idx], y[train_idx], y[val_idx]\n        \n        # ### SỬA LỖI: Chọn bộ transform phù hợp với model ###\n        if model_name == 'ResNet18':\n            current_train_transform = resnet_train_transform\n            current_test_transform = resnet_test_transform\n        else: # ViT\n            current_train_transform = vit_train_transform\n            current_test_transform = vit_test_transform\n\n        train_dataset = MNISTDataset(X_train, y_train, transform=current_train_transform)\n        val_dataset = MNISTDataset(X_val, y_val, transform=current_test_transform)\n        train_loader = DataLoader(train_dataset, batch_size=Config.BATCH_SIZE, shuffle=True, num_workers=2, pin_memory=True)\n        val_loader = DataLoader(val_dataset, batch_size=Config.BATCH_SIZE, shuffle=False, num_workers=2, pin_memory=True)\n        \n        if model_name == 'ResNet18': model = ResNet18().to(Config.DEVICE)\n        else: model = create_vit().to(Config.DEVICE)\n            \n        criterion = LabelSmoothingCrossEntropy(classes=10, smoothing=Config.LABEL_SMOOTHING)\n        optimizer = optim.AdamW(model.parameters(), lr=Config.LEARNING_RATE)\n        scheduler = optim.lr_scheduler.OneCycleLR(optimizer, max_lr=Config.LEARNING_RATE, total_steps=len(train_loader) * Config.EPOCHS)\n        \n        best_accuracy = 0.0\n        model_path_fold = f\"/kaggle/working/{model_name}_fold_{fold}.pth\"\n\n        for epoch in range(Config.EPOCHS):\n            model.train()\n            for images, labels in train_loader:\n                images, labels = images.to(Config.DEVICE, non_blocking=True), labels.to(Config.DEVICE, non_blocking=True)\n                optimizer.zero_grad(); outputs = model(images); loss = criterion(outputs, labels); loss.backward(); optimizer.step(); scheduler.step()\n\n            model.eval()\n            correct = 0\n            with torch.no_grad():\n                for images, labels in val_loader:\n                    images, labels = images.to(Config.DEVICE, non_blocking=True), labels.to(Config.DEVICE, non_blocking=True)\n                    outputs = model(images)\n                    _, predicted = torch.max(outputs.data, 1)\n                    correct += (predicted == labels).sum().item()\n            val_accuracy = 100 * correct / len(val_dataset)\n            if val_accuracy > best_accuracy: best_accuracy = val_accuracy; torch.save(model.state_dict(), model_path_fold)\n        \n        print(f\"Fold {fold+1} Best Val Acc: {best_accuracy:.2f}%\")\n\n        model.load_state_dict(torch.load(model_path_fold))\n        model.eval()\n        \n        # ### SỬA LỖI: Dùng đúng transform cho TTA ###\n        tta_dataset = MNISTDataset(X_test, transform=current_train_transform)\n        tta_loader = DataLoader(tta_dataset, batch_size=Config.BATCH_SIZE, shuffle=False, num_workers=2, pin_memory=True)\n        fold_preds = []\n        with torch.no_grad():\n            for _ in range(Config.N_TTA):\n                pass_preds = []\n                for images, _ in tta_loader: images = images.to(Config.DEVICE, non_blocking=True); outputs = model(images); pass_preds.append(torch.softmax(outputs, dim=1))\n                fold_preds.append(torch.cat(pass_preds))\n        test_predictions.append(torch.mean(torch.stack(fold_preds), dim=0))\n        \n        del model, train_loader, val_loader; gc.collect(); torch.cuda.empty_cache()\n\n    return torch.mean(torch.stack(test_predictions), dim=0)\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# =============================================================================\n# BƯỚC 8: THỰC THI ENSEMBLE LAI VÀ TẠO SUBMISSION\n# =============================================================================\ntrain_df = pd.read_csv(Config.TRAIN_FILE)\ntest_df = pd.read_csv(Config.TEST_FILE)\nX = train_df.drop(columns='label').values.astype(np.uint8)\ny = train_df['label'].values\nX_test = test_df.values.astype(np.uint8)\n\nprint(\"\\n--- BƯỚC 8: Bắt đầu quá trình Huấn luyện và Dự đoán ---\")\nprint(f\"\\n{'='*25} BẮT ĐẦU HUẤN LUYỆN ENSEMBLE RESNET-18 {'='*25}\")\nresnet_preds = train_and_predict('ResNet18', X, y, X_test)\n\nprint(f\"\\n{'='*25} BẮT ĐẦU HUẤN LUYỆN ENSEMBLE VISION TRANSFORMER {'='*25}\")\nvit_preds = train_and_predict('ViT', X, y, X_test)\n\nprint(f\"\\n{'='*25} KẾT HỢP KẾT QUẢ CUỐI CÙNG {'='*25}\")\n# Gán trọng số cao hơn cho ResNet vì nó đã được chứng minh là tốt hơn\nfinal_preds_tensor = (resnet_preds * 0.7) + (vit_preds * 0.3) # Tăng trọng số cho ResNet\n_, final_labels = torch.max(final_preds_tensor, 1)\nfinal_labels = final_labels.cpu().numpy()\n\nsubmission_df = pd.DataFrame({'ImageId': np.arange(1, len(final_labels) + 1), 'Label': final_labels})\nsubmission_df.to_csv(Config.SUBMISSION_FILE, index=False)\nprint(f\"File '{Config.SUBMISSION_FILE}' đã được tạo thành công!\")\nprint(\"Xem trước 5 dòng đầu tiên:\")\nprint(submission_df.head())","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}