{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":106372,"databundleVersionId":12864984}],"dockerImageVersionId":31328,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"# 데이터 준비 부분\n하이퍼 파라미터 설정값을 미리 저장하고 seed를 고정한다.\n이후 데이터셋을 구성한다.","metadata":{}},{"cell_type":"code","source":"import os\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nfrom sklearn.model_selection import train_test_split\nimport torchvision.transforms as transforms\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision.datasets.folder import default_loader\nfrom sklearn.preprocessing import LabelEncoder\nfrom tqdm import tqdm\nimport random\nimport joblib","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T08:33:30.647423Z","iopub.execute_input":"2026-04-29T08:33:30.647866Z","iopub.status.idle":"2026-04-29T08:33:30.654840Z","shell.execute_reply.started":"2026-04-29T08:33:30.647823Z","shell.execute_reply":"2026-04-29T08:33:30.653821Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(os.listdir(\"/kaggle/input/competitions/2025-bamboo-summer-competiton-dl-pr\"))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T08:33:30.658148Z","iopub.execute_input":"2026-04-29T08:33:30.658725Z","iopub.status.idle":"2026-04-29T08:33:30.669945Z","shell.execute_reply.started":"2026-04-29T08:33:30.658686Z","shell.execute_reply":"2026-04-29T08:33:30.669151Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#하이퍼 파라미터\nCFG = {\n    'IMG_SIZE':128, \n    'EPOCHS':20,\n    'LEARNING_RATE':1e-4,\n    'BATCH_SIZE':32,\n    'NUM_WORKERS': 2,\n    'SEED':42\n}\n\n# 디바이스 설정\ndevice = torch.device('cuda') if torch.cuda.is_available() else torch.device('cpu')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T08:33:30.671360Z","iopub.execute_input":"2026-04-29T08:33:30.672016Z","iopub.status.idle":"2026-04-29T08:33:30.682468Z","shell.execute_reply.started":"2026-04-29T08:33:30.671913Z","shell.execute_reply":"2026-04-29T08:33:30.681826Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Seed 고정 => 너무 많은 랜덤성 줄이기\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(CFG['SEED'])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T08:33:30.683494Z","iopub.execute_input":"2026-04-29T08:33:30.683860Z","iopub.status.idle":"2026-04-29T08:33:30.700028Z","shell.execute_reply.started":"2026-04-29T08:33:30.683836Z","shell.execute_reply":"2026-04-29T08:33:30.699063Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 라벨 인코딩 및 구성 확인\n\ntrain = pd.read_csv(\"/kaggle/input/competitions/2025-bamboo-summer-competiton-dl-pr/train.csv\")\ntest = pd.read_csv(\"/kaggle/input/competitions/2025-bamboo-summer-competiton-dl-pr/test.csv\")\n\n# 라벨 인코딩\nle = LabelEncoder()\ntrain[\"label_encoded\"] = le.fit_transform(train[\"label\"])\n\n# 첫 5개 예시\ntrain.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T08:33:30.702241Z","iopub.execute_input":"2026-04-29T08:33:30.702611Z","iopub.status.idle":"2026-04-29T08:33:30.731469Z","shell.execute_reply.started":"2026-04-29T08:33:30.702585Z","shell.execute_reply":"2026-04-29T08:33:30.730574Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 데이터셋\nclass BamDataset(Dataset):\n    def __init__(self, dataframe, img_dir, transform = None, train = True):\n        self.df = dataframe.reset_index(drop = True) # 인덱스 추가 없이 재정렬\n        self.img_dir = img_dir # 이미지들 있는 폴더\n        self.transform = transform\n        self.train = train # 학습/테스트 구분\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        row = self.df.iloc[idx] # idx번째 행\n        img_path = os.path.join(self.img_dir, row[\"filename\"]) #사진 별 경로\n        image = default_loader(img_path) # PIL 이미지 로드\n        if self.transform:\n            image = self.transform(image)\n\n        if self.train:\n            label = row[\"label_encoded\"]\n            return image, label # 학습 데이터는 라벨 필요\n        else:\n            return image, row[\"filename\"] # 제출할 때 파일 이름이 필요해서 테스트 데이터에 같이 넣어둠","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T08:33:30.732640Z","iopub.execute_input":"2026-04-29T08:33:30.733057Z","iopub.status.idle":"2026-04-29T08:33:30.741665Z","shell.execute_reply.started":"2026-04-29T08:33:30.732971Z","shell.execute_reply":"2026-04-29T08:33:30.740574Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 전처리\n\ntrain_transform = transforms.Compose([\n    transforms.Resize((CFG['IMG_SIZE'], CFG['IMG_SIZE'])),\n    transforms.RandomHorizontalFlip(),\n    transforms.ColorJitter(),\n    transforms.ToTensor(),\n])\ntest_transform = transforms.Compose([\n    transforms.Resize((CFG['IMG_SIZE'], CFG['IMG_SIZE'])),\n    transforms.ToTensor(),\n])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T08:33:30.743091Z","iopub.execute_input":"2026-04-29T08:33:30.743594Z","iopub.status.idle":"2026-04-29T08:33:30.754554Z","shell.execute_reply.started":"2026-04-29T08:33:30.743547Z","shell.execute_reply":"2026-04-29T08:33:30.753499Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# train/test 데이터로 분리\n# 해당 대회에서는 학습, 테스트셋이 따로 있어서 학습 데이터를 학습/검증으로 2개로 나눔\n\ntrain_df, val_df = train_test_split(\n    train,\n    test_size=0.2,\n    stratify=train[\"label_encoded\"] # 클래스의 비율을 유지하면서 분리\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T08:33:30.755939Z","iopub.execute_input":"2026-04-29T08:33:30.756333Z","iopub.status.idle":"2026-04-29T08:33:30.779586Z","shell.execute_reply.started":"2026-04-29T08:33:30.756305Z","shell.execute_reply":"2026-04-29T08:33:30.778518Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 데이터 로더\n\ntrain_dataset = BamDataset(train_df, img_dir=os.path.join(\"/kaggle/input/competitions/2025-bamboo-summer-competiton-dl-pr/train\"), transform=train_transform)\nval_dataset = BamDataset(val_df, img_dir=os.path.join(\"/kaggle/input/competitions/2025-bamboo-summer-competiton-dl-pr/train\"), transform=train_transform)\n\ntrain_loader = DataLoader(train_dataset, batch_size=CFG['BATCH_SIZE'], shuffle=True, num_workers=CFG['NUM_WORKERS'])\nval_loader = DataLoader(val_dataset, batch_size=CFG['BATCH_SIZE'], shuffle=False, num_workers=CFG['NUM_WORKERS'])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T08:33:30.780721Z","iopub.execute_input":"2026-04-29T08:33:30.781322Z","iopub.status.idle":"2026-04-29T08:33:30.789058Z","shell.execute_reply.started":"2026-04-29T08:33:30.781275Z","shell.execute_reply":"2026-04-29T08:33:30.788042Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 모델 구성","metadata":{}},{"cell_type":"code","source":"# 베이직 블록\n\nclass BasicBlock(nn.Module):\n    expansion = 1 # 베이직 블록은 입출력이 같음\n\n    def __init__(self, in_channels, out_channels, stride=1, downsample=None):\n        super(BasicBlock, self).__init__()\n        \n        self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False)\n        self.bn1 = nn.BatchNorm2d(out_channels)\n        self.relu = nn.ReLU(unplace = True)\n        self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=False)\n        self.bn2 = nn.BatchNorm2d(out_channels)\n        self.downsample = downsample\n\n    def forward(self, x):\n        identity = x\n\n        # pre-activation 방식을 적용하여 relu 이후에 conv 실시\n        out = self.bn1(x)\n        out = self.relu(out)\n        out = self.conv1(out)\n\n        out = self.bn2(out)\n        out = self.relu(out) \n        out = self.conv2(out)\n        \n        if self.downsample is not None:\n            identity = self.downsample(x)\n\n        out += identity\n\n        return out\n        ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T08:33:30.819648Z","iopub.execute_input":"2026-04-29T08:33:30.820469Z","iopub.status.idle":"2026-04-29T08:33:30.827888Z","shell.execute_reply.started":"2026-04-29T08:33:30.820438Z","shell.execute_reply":"2026-04-29T08:33:30.826918Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# 보틀넥 블록\n\nclass BottleneckBlock(nn.Module):\n    expansion = 4\n\n    def __init__(self, in_channels, mid_channels, stride=1, downsample=None):\n        super(BottleneckBlock, self).__init__()\n        \n        out_channels = mid_channels * self.expansion\n\n        self.conv1 = nn.Conv2d(in_channels, mid_channels, kernel_size=1, bias=False)\n        self.bn1 = nn.BatchNorm2d(in_channels) # pre-activation을 적용해서 이 부분도 채널 수를 맞춰줘야 한다.\n\n        self.conv2 = nn.Conv2d(mid_channels, mid_channels, kernel_size=3,stride=stride, padding=1, bias=False)\n        self.bn2 = nn.BatchNorm2d(mid_channels)\n\n        self.conv3 = nn.Conv2d(mid_channels, out_channels, kernel_size=1, bias=False)\n        self.bn3 = nn.BatchNorm2d(mid_channels)\n\n        self.relu = nn.ReLU(inplace=True)\n        self.downsample = downsample\n\n    def forward(self, x):\n        identity = x\n\n        #마찬가지로 relu 이후에 conv를 실시한다.\n        out = self.bn1(x)\n        out = self.relu(out)\n        out = self.conv1(out)\n\n        out = self.bn2(out)\n        out = self.relu(out)\n        out = self.conv2(out)\n        \n        out = self.bn3(out)\n        out = self.relu(out) \n        out = self.conv3(out)\n\n        if self.downsample is not None:\n            identity = self.downsample(x)\n\n        out += identity\n        \n\n        return out","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T08:33:30.831225Z","iopub.execute_input":"2026-04-29T08:33:30.831635Z","iopub.status.idle":"2026-04-29T08:33:30.850025Z","shell.execute_reply.started":"2026-04-29T08:33:30.831609Z","shell.execute_reply":"2026-04-29T08:33:30.849063Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# ResNet 구성\n\nclass ResNet(nn.Module):\n    def __init__(self, block, layers, num_classes=1000):\n        super(ResNet, self).__init__()\n        self.in_channels = 64\n\n        # stem layer로 3채널 이미지를 64채널로 변환\n        # self.conv1 = nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3, bias=False) <- 224 크기일 때 기준\n        # 이미지 사이즈가 128이므로 stem layer에서 너무 빠르게 감소하지 않게 설정\n        self.conv1 = nn.Conv2d(3, 64, kernel_size=3, stride=1, padding=1, bias=False)\n        self.bn1 = nn.BatchNorm2d(64)\n        self.relu = nn.ReLU(inplace=True)\n        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)\n\n        # 4개의 Residual Stage\n        self.layer1 = self._make_layer(block, 64, layers[0], stride=1)\n        self.layer2 = self._make_layer(block, 128, layers[1], stride=2)\n        self.layer3 = self._make_layer(block, 256, layers[2], stride=2)\n        self.layer4 = self._make_layer(block, 512, layers[3], stride=2)\n\n        # 분류층\n        self.avgpool = nn.AdaptiveAvgPool2d((1, 1))\n        self.fc = nn.Linear(512 * block.expansion, num_classes)\n\n        # He 초기화\n        self._initialize_weights()\n\n    def _make_layer(self, block, mid_channels, num_blocks, stride):\n        downsample = None\n        out_channels = mid_channels * block.expansion\n\n        if stride != 1 or self.in_channels != out_channels:\n            downsample = nn.Sequential(\n                nn.Conv2d(\n                    self.in_channels, out_channels,\n                    kernel_size=1, stride=stride, bias=False\n                ),\n                nn.BatchNorm2d(out_channels),\n            )\n\n        layers = []\n        # 첫번째 블록 추가, 이후 블록의 입력 채널은 이 블록의 out channel 개수임\n        layers.append(block(self.in_channels, mid_channels, stride, downsample))\n        self.in_channels = out_channels\n        \n        # 이후 블록부터는 stride와 다운샘플링=None으로 설정한다\n        for _ in range(1, num_blocks):\n            layers.append(block(self.in_channels, mid_channels))\n\n        return nn.Sequential(*layers)\n\n    def _initialize_weights(self):\n        for m in self.modules():\n            if isinstance(m, nn.Conv2d):\n                nn.init.kaiming_normal_(\n                    m.weight, mode='fan_out', nonlinearity='relu'\n                )\n            elif isinstance(m, nn.BatchNorm2d):\n                nn.init.constant_(m.weight, 1)\n                nn.init.constant_(m.bias, 0)\n\n    def forward(self, x):\n        # Stem\n        x = self.conv1(x)      \n        x = self.bn1(x)\n        x = self.relu(x)\n        x = self.maxpool(x)     \n\n        # residual 블록\n        x = self.layer1(x)      \n        x = self.layer2(x)     \n        x = self.layer3(x)      \n        x = self.layer4(x)     \n\n        # 분류\n        x = self.avgpool(x)    \n        x = torch.flatten(x, 1)\n        x = self.fc(x)\n\n        return x","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T08:33:30.851741Z","iopub.execute_input":"2026-04-29T08:33:30.852216Z","iopub.status.idle":"2026-04-29T08:33:30.873762Z","shell.execute_reply.started":"2026-04-29T08:33:30.852179Z","shell.execute_reply":"2026-04-29T08:33:30.872799Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"num_classes = len(le.classes_)\n\ndef resnet50(num_classes):\n    return ResNet(BottleneckBlock, [3, 4, 6, 3], num_classes)\n\nmodel = resnet50(num_classes)\nmodel = model.to(device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T08:33:30.875091Z","iopub.execute_input":"2026-04-29T08:33:30.875606Z","iopub.status.idle":"2026-04-29T08:33:31.279911Z","shell.execute_reply.started":"2026-04-29T08:33:30.875573Z","shell.execute_reply":"2026-04-29T08:33:31.279047Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 학습","metadata":{}},{"cell_type":"code","source":"criterion = nn.CrossEntropyLoss()\noptimizer = torch.optim.AdamW(model.parameters(), lr=CFG['LEARNING_RATE'])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T08:33:31.280946Z","iopub.execute_input":"2026-04-29T08:33:31.281327Z","iopub.status.idle":"2026-04-29T08:33:31.288305Z","shell.execute_reply.started":"2026-04-29T08:33:31.281291Z","shell.execute_reply":"2026-04-29T08:33:31.287517Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"for epoch in range(CFG['EPOCHS']):\n    model.train()\n    running_loss = 0.0\n    # tdqm으로 진행률을 바 형태로 시각적으로 확인 가능\n    for images, labels in tqdm(train_loader, desc=f\"[Train] Epoch {epoch+1}/{CFG['EPOCHS']}\"):\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        running_loss += loss.item()\n\n    avg_train_loss = running_loss / len(train_loader) # 에폭 평균 로스\n\n    model.eval()\n    val_loss = 0.0\n    correct = 0\n    total = 0\n    with torch.no_grad(): # 평가 때는 gradient 계산 멈춰서 메모리 절약\n        for images, labels in tqdm(val_loader, desc=f\"[Val] Epoch {epoch+1}/{CFG['EPOCHS']}\"):\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            preds = torch.argmax(outputs, dim=1)\n            correct += (preds == labels).sum().item() # 잘 예측한 개수\n            total += labels.size(0)\n\n    avg_val_loss = val_loss / len(val_loader)\n    val_acc = correct / total\n\n    print(f\"Epoch {epoch+1} | Train Loss: {avg_train_loss:.4f} | Val Loss: {avg_val_loss:.4f} | Val Acc: {val_acc:.4f}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T08:33:31.290180Z","iopub.execute_input":"2026-04-29T08:33:31.290478Z","iopub.status.idle":"2026-04-29T08:52:28.357753Z","shell.execute_reply.started":"2026-04-29T08:33:31.290448Z","shell.execute_reply":"2026-04-29T08:52:28.356848Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# 제출\n최종적으로 테스트셋을 통해 제출 파일을 만든다.","metadata":{}},{"cell_type":"code","source":"test_dataset = BamDataset(test, img_dir=os.path.join(\"/kaggle/input/competitions/2025-bamboo-summer-competiton-dl-pr/test\"), transform=test_transform, train=False)\ntest_loader = DataLoader(test_dataset, batch_size=CFG['BATCH_SIZE'], shuffle=False, num_workers=CFG['NUM_WORKERS'])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T08:52:28.359543Z","iopub.execute_input":"2026-04-29T08:52:28.360293Z","iopub.status.idle":"2026-04-29T08:52:28.365298Z","shell.execute_reply.started":"2026-04-29T08:52:28.360243Z","shell.execute_reply":"2026-04-29T08:52:28.364688Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model.eval()\npredictions = []\nfilenames = []\n\nwith torch.no_grad():\n    for images, fnames in tqdm(test_loader, desc=\"Predicting\"):\n        images = images.to(device)\n        outputs = model(images)\n        probs = torch.softmax(outputs, dim=1)\n        preds = torch.argmax(probs, dim=1).cpu().numpy()\n        labels = le.inverse_transform(preds)\n        predictions.extend(labels)\n        filenames.extend(fnames)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T08:52:28.366246Z","iopub.execute_input":"2026-04-29T08:52:28.366599Z","iopub.status.idle":"2026-04-29T08:52:38.691521Z","shell.execute_reply.started":"2026-04-29T08:52:28.366575Z","shell.execute_reply":"2026-04-29T08:52:38.690547Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"submission = pd.read_csv('/kaggle/input/competitions/2025-bamboo-summer-competiton-dl-pr/sample_submission.csv')\nsubmission['label'] = predictions\nsubmission.to_csv(os.path.join(\"baseline_submission.csv\"), index=False)\nprint(\"✅ submission.csv 생성 완료!\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-04-29T08:52:38.693036Z","iopub.execute_input":"2026-04-29T08:52:38.693641Z","iopub.status.idle":"2026-04-29T08:52:38.716696Z","shell.execute_reply.started":"2026-04-29T08:52:38.693607Z","shell.execute_reply":"2026-04-29T08:52:38.715882Z"}},"outputs":[],"execution_count":null}]}