{"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":"code","source":"# This Python 3 environment comes with many helpful analytics libraries installed\n# It is defined by the kaggle/python Docker image: https://github.com/kaggle/docker-python\n# For example, here's several helpful packages to load\n\nimport numpy as np # linear algebra\nimport pandas as pd # data processing, CSV file I/O (e.g. pd.read_csv)\n\n# Input data files are available in the read-only \"../input/\" directory\n# For example, running this (by clicking run or pressing Shift+Enter) will list all files under the input directory\n\nimport os\n\ncount=0\n\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))\n        count+=1\n\n        if count==20:\n            break\n\n    if count==20:\n        break\n        \n\n# You can write up to 20GB to the current directory (/kaggle/working/) that gets preserved as output when you create a version using \"Save & Run All\" \n# You can also write temporary files to /kaggle/temp/, but they won't be saved outside of the current session\n\n# Use the kagglehub client library to attach Kaggle resources like competitions, datasets, and models to your session\n# Learn more about kagglehub: https://github.com/Kaggle/kagglehub/blob/main/README.md\n\nimport kagglehub\n# kagglehub.dataset_download('<owner>/<dataset-slug>')","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2026-06-27T03:03:52.674213Z","iopub.execute_input":"2026-06-27T03:03:52.675211Z","iopub.status.idle":"2026-06-27T03:03:53.633570Z","shell.execute_reply.started":"2026-06-27T03:03:52.675160Z","shell.execute_reply":"2026-06-27T03:03:53.632899Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import matplotlib.pyplot as plt\nfrom glob import glob  #glob 라이브러리 안에 glob 함수 사용\nfrom PIL import Image\nimport cv2\nimport numpy as np","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-27T03:03:53.634789Z","iopub.execute_input":"2026-06-27T03:03:53.635016Z","iopub.status.idle":"2026-06-27T03:03:53.638962Z","shell.execute_reply.started":"2026-06-27T03:03:53.634995Z","shell.execute_reply":"2026-06-27T03:03:53.638240Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from dataclasses import dataclass\n\n@dataclass        #__init__을 생략해줌. 기존 방식(클래스에 속성 부여)은 속성 하나 바꾸려면 클래스 자체가 바뀌어야 했는데, 이 방식은 a=BaseConfig(BATCH_SIZE=64)으로 변경 가능 \nclass BaseConfig:\n    EPOCHS: int = 10\n    BATCH_SIZE: int = 32\n    LR: float = 3e-5\n    \n\n    IMG_SIZE: int = 384\n    SEED: int = 42","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-27T03:03:53.639861Z","iopub.execute_input":"2026-06-27T03:03:53.640148Z","iopub.status.idle":"2026-06-27T03:03:53.654057Z","shell.execute_reply.started":"2026-06-27T03:03:53.640125Z","shell.execute_reply":"2026-06-27T03:03:53.653432Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 시드 고정","metadata":{}},{"cell_type":"code","source":"import random\nimport torch\n\ndef set_seed(seed=42):\n    random.seed(seed)    #파이선 기본 내장 random 모듈 시드\n    np.random.seed(seed)    #numpy 랜덤 시드, albumentation가 numpy랜덤 모듈을 사용\n    torch.manual_seed(seed)    #CPU에서 동작하는 pytorch 랜덤 시드(모델 가중치 초기화 등)\n    torch.cuda.manual_seed(seed)       # 단일 GPU용\n    torch.cuda.manual_seed_all(seed)    #멀티 GPU에서 동작하는 pytorch 랜덤 시드(dropout 등)\n\n\n#worker init fn(서브 프로세스 시드 고정)\ndef seed_worker(worker_id):\n    worker_seed = torch.initial_seed() % 2**32\n    np.random.seed(worker_seed)\n    random.seed(worker_seed)\n\ng = torch.Generator()  # 제너레이터 생성\ng.manual_seed(0)  # 제너레이터 시드값 고정\n#전역 시드 고정해도 프로세스들의 순서가 달라지면 학습되는 데이터 순서가 달라질 수 있으므로 shuffle을 통제","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-27T03:03:53.655663Z","iopub.execute_input":"2026-06-27T03:03:53.655943Z","iopub.status.idle":"2026-06-27T03:03:53.669263Z","shell.execute_reply.started":"2026-06-27T03:03:53.655922Z","shell.execute_reply":"2026-06-27T03:03:53.668739Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\n\nset_seed()\nSEED=42\n\n\n#학습/테스트 데이터 목록 확인\ntrain_df=pd.read_csv('../input/competitions/aptos2019-blindness-detection/train.csv')\ntest_df=pd.read_csv('../input/competitions/aptos2019-blindness-detection/test.csv')\ntrain_df = train_df.sample(frac=1, random_state=SEED).reset_index(drop=True)  #df의 행을 랜덤 샘플링(frac은 샘플링 비율을 설정)-아렇게 해야 같은 병변끼리 모여있는 상황 최소화\n#random state 안쓰면 넘파이 난수 엔진 참고, 따로 고정할 필요 없음\n\ntrain_df.head(3)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-27T03:03:53.670060Z","iopub.execute_input":"2026-06-27T03:03:53.670400Z","iopub.status.idle":"2026-06-27T03:03:53.697210Z","shell.execute_reply.started":"2026-06-27T03:03:53.670378Z","shell.execute_reply":"2026-06-27T03:03:53.696686Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 데이터 분포 확인","metadata":{}},{"cell_type":"code","source":"from sklearn.model_selection import train_test_split\n\nx=train_df['id_code']\ny=train_df['diagnosis']\n\ntrain_x, valid_x, train_y, valid_y=train_test_split(x, y, test_size=0.15, stratify=y)\nprint(train_x.shape, valid_x.shape)\n\nfig, axes=plt.subplots(1,2, figsize=(10,4))\ntrain_y.hist(ax=axes[0])\naxes[0].set_title('Train Class Distribution')\n\nvalid_y.hist(ax=axes[1])\naxes[1].set_title('Valid Class Distribution')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-27T03:03:53.697991Z","iopub.execute_input":"2026-06-27T03:03:53.698269Z","iopub.status.idle":"2026-06-27T03:03:53.953718Z","shell.execute_reply.started":"2026-06-27T03:03:53.698249Z","shell.execute_reply":"2026-06-27T03:03:53.952931Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Image Plot","metadata":{}},{"cell_type":"code","source":"#train_df에 이미지 path 만들기\nall_image_path={os.path.basename(x).replace('.png',''):x for x in\n               glob(os.path.join('..','input','competitions','aptos2019-blindness-detection','train_images','*.png'))}\nprint('Scans Found:', len(all_image_path), 'Total image Index:', len(train_df))\n\ntrain_df['path']=train_df['id_code'].map(all_image_path.get)\ntrain_df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-27T03:03:53.954795Z","iopub.execute_input":"2026-06-27T03:03:53.955131Z","iopub.status.idle":"2026-06-27T03:03:53.975134Z","shell.execute_reply.started":"2026-06-27T03:03:53.955095Z","shell.execute_reply":"2026-06-27T03:03:53.974581Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#test_df에 이미지 path 만들기\nall_image_path={os.path.basename(x).replace('.png',''):x for x in\n               glob(os.path.join('..','input','competitions','aptos2019-blindness-detection','test_images','*.png'))}\nprint('Scans Found:', len(all_image_path), 'Total image Index:', len(test_df))\n\ntest_df['path']=test_df['id_code'].map(all_image_path.get)\ntest_df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-27T03:03:53.975918Z","iopub.execute_input":"2026-06-27T03:03:53.976163Z","iopub.status.idle":"2026-06-27T03:03:53.991541Z","shell.execute_reply.started":"2026-06-27T03:03:53.976140Z","shell.execute_reply":"2026-06-27T03:03:53.990983Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#image plot 함수\ndef plot_images(df, rows, columns, figsize):\n    fig, axes=plt.subplots(rows, columns, figsize=figsize)\n    idx=0\n    for i in range(rows):\n        for j in range(columns):\n            image=np.array(Image.open(df['path'].values[idx]).convert('RGB'))\n            axes[i,j].imshow(image)\n            axes[i,j].set_title(f'Label: {df['diagnosis'].values[idx]}, id: {df['id_code'].values[idx]}')\n\n            idx+=1\n\n    plt.show()\n\n\nplot_images(train_df, 6,6, (25,25))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-27T03:03:53.992491Z","iopub.execute_input":"2026-06-27T03:03:53.992761Z","iopub.status.idle":"2026-06-27T03:04:07.257230Z","shell.execute_reply.started":"2026-06-27T03:03:53.992742Z","shell.execute_reply":"2026-06-27T03:04:07.256369Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### image preprocessing","metadata":{}},{"cell_type":"code","source":"#이미지에 여백 문제/조도, 색상 문제가 있는 것으로 확인\n#Autocrop과 Ben Graham 전처리로 해결\n\n#AutoCrop 코드\ndef AutoCrop(img, tol=7):   #tol=tolerance(허용 오차)-밝기 7 이하인 픽셀은 크롭\n    gray_image=cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)\n\n    mask=gray_image>tol\n    row_mask=mask.any(1)  #열을 흝으며 해당 행에 true를 찾음-그래서 1\n    col_mask=mask.any(0)\n\n    check_shape=img[:,:,0][np.ix_(row_mask, col_mask)].shape   \n    #np.ix_(mask.any(1), mask.any(0)) 브로드케스팅 계산이 가능한 형태로 튜플 반환((3,1),(1,3) 형태의 array)\n\n    if check_shape[0]==0:\n        return img\n\n    else:\n        img1=img[:,:,0][np.ix_(row_mask, col_mask)]\n        img2=img[:,:,1][np.ix_(row_mask, col_mask)]\n        img3=img[:,:,2][np.ix_(row_mask, col_mask)]\n\n        img=np.stack([img1, img2, img3], axis=-1)\n\n        return img","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-27T03:04:07.261778Z","iopub.execute_input":"2026-06-27T03:04:07.262053Z","iopub.status.idle":"2026-06-27T03:04:07.268425Z","shell.execute_reply.started":"2026-06-27T03:04:07.262028Z","shell.execute_reply":"2026-06-27T03:04:07.267598Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#Ben Graham 전처리 코드\ndef ben_color(img):\n    blur=cv2.GaussianBlur(img, (0,0), sigmaX=20)  #가우시안 커널 (0,0)으로 두면 시그마 값에 맞춰서 자동 설정\n\n    #조도 보정 공식 구현 (4*original-4*blur+128)\n    result=cv2.addWeighted(img, 4, blur, -4, 128)\n    return result\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-27T03:04:07.269818Z","iopub.execute_input":"2026-06-27T03:04:07.270298Z","iopub.status.idle":"2026-06-27T03:04:07.284289Z","shell.execute_reply.started":"2026-06-27T03:04:07.270261Z","shell.execute_reply":"2026-06-27T03:04:07.283574Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#시각화 해보기\nimage=np.array(Image.open(train_df['path'].values[0]).convert('RGB'))\nimg1=AutoCrop(image)\nimg2=ben_color(img1)\n\nfig, axes=plt.subplots(1,3, figsize=(10,4))\naxes[0].imshow(image)\naxes[0].set_title('Original')\n\naxes[1].imshow(img1)\naxes[1].set_title('Autocrop')\n\naxes[2].imshow(img2)\naxes[2].set_title('Autocrop+Ben Color')\n\nfor ax in axes:\n    ax.axis('off')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-27T03:04:07.285177Z","iopub.execute_input":"2026-06-27T03:04:07.285356Z","iopub.status.idle":"2026-06-27T03:04:08.297558Z","shell.execute_reply.started":"2026-06-27T03:04:07.285339Z","shell.execute_reply":"2026-06-27T03:04:08.297001Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Train Valid split","metadata":{}},{"cell_type":"code","source":"train_df, valid_df=train_test_split(train_df, test_size=0.15, random_state=SEED, stratify=train_df['diagnosis'])\n\nprint(train_df.shape, valid_df.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-27T03:04:08.298530Z","iopub.execute_input":"2026-06-27T03:04:08.298832Z","iopub.status.idle":"2026-06-27T03:04:08.308732Z","shell.execute_reply.started":"2026-06-27T03:04:08.298799Z","shell.execute_reply":"2026-06-27T03:04:08.308047Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Dataset","metadata":{}},{"cell_type":"code","source":"#훈련 데이터용 transform\nimport albumentations as A  #cv2기반으로 만들어진 라이브러리\nfrom albumentations.pytorch import ToTensorV2\n\n#3000개의 데이터수는 ViT에게 너무 적음\n\ntrain_transform=A.Compose([\n    A.Resize(384,384),\n    A.HorizontalFlip(p=0.5),  #좌우 뒤집에도 질환 찾는데 문제 없음(좌안 우안 변경)\n    A.VerticalFlip(p=0.5),    #상하 뒤집에도 질환 찾는데 문제 없음\n    A.RandomRotate90(p=0.5),\n    A.Normalize(),  #ImageNet std/mean 으로 정규화\n    ToTensorV2()]\n)\n\n\n#검증/테스트 데이터용 transform   #이미지 transform 안하는 이유-불필요한 왜곡 추가가 목적이라 원본 이미지 예측에는 사용 안함\ntest_transform=A.Compose([\n    A.Resize(384,384),\n    A.Normalize(),\n    ToTensorV2()]\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-27T03:04:08.309861Z","iopub.execute_input":"2026-06-27T03:04:08.310122Z","iopub.status.idle":"2026-06-27T03:04:08.322095Z","shell.execute_reply.started":"2026-06-27T03:04:08.310100Z","shell.execute_reply":"2026-06-27T03:04:08.321392Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from torch.utils.data import Dataset\nfrom torch.utils.data import DataLoader\n\nclass APTOSData(Dataset):\n    def __init__(self, df, transform=None, is_test=False):\n        self.df=df\n        self.transform=transform\n        self.is_test=is_test\n\n    def __getitem__(self,idx):\n        image=np.array(Image.open(self.df['path'].iloc[idx]).convert('RGB'))\n      \n\n        img=AutoCrop(image)\n        img=ben_color(img)\n\n        if self.transform:\n            img=self.transform(image=img)['image']   #딕셔너리 반환(mask같은거 넘기면 같이 반환)\n\n        if self.is_test:\n            return img\n\n        \n        labels=self.df['diagnosis'].iloc[idx]\n        labels=torch.tensor(labels, dtype=torch.float)  #Smooth L1 Loss를 손실함수로 사용\n\n        return img, labels\n\n    def __len__(self):\n        return len(self.df)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-27T03:04:08.322933Z","iopub.execute_input":"2026-06-27T03:04:08.323239Z","iopub.status.idle":"2026-06-27T03:04:08.338927Z","shell.execute_reply.started":"2026-06-27T03:04:08.323217Z","shell.execute_reply":"2026-06-27T03:04:08.338223Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#dataset 생성\ntrain_dataset=APTOSData(train_df, transform=train_transform)\nvalid_dataset=APTOSData(valid_df, transform=train_transform)\ntest_dataset=APTOSData(test_df, transform=test_transform, is_test=True)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-27T03:04:08.339831Z","iopub.execute_input":"2026-06-27T03:04:08.340100Z","iopub.status.idle":"2026-06-27T03:04:08.351181Z","shell.execute_reply.started":"2026-06-27T03:04:08.340080Z","shell.execute_reply":"2026-06-27T03:04:08.350426Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#dataloader 생성\nconfig=BaseConfig()\n\ntrain_dataloader=DataLoader(train_dataset, batch_size=config.BATCH_SIZE, worker_init_fn=seed_worker, shuffle=True, generator=g, num_workers=2)\nvalid_dataloader=DataLoader(valid_dataset, batch_size=config.BATCH_SIZE, worker_init_fn=seed_worker, shuffle=True, generator=g, num_workers=2,\n                           drop_last=False)  #default도 false이다. valid랑 test는 모든 데이터 대상으로 적용되야 하기 때문에 False.\ntest_dataloader=DataLoader(test_dataset, batch_size=config.BATCH_SIZE, worker_init_fn=seed_worker, shuffle=True, generator=g, num_workers=2,\n                          drop_last=False)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-27T03:04:08.352153Z","iopub.execute_input":"2026-06-27T03:04:08.352502Z","iopub.status.idle":"2026-06-27T03:04:08.366123Z","shell.execute_reply.started":"2026-06-27T03:04:08.352480Z","shell.execute_reply":"2026-06-27T03:04:08.365492Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### ViT","metadata":{}},{"cell_type":"code","source":"!pip install timm  #torch image model 라이브러리(허깅페이스는 태생이 nlp라 비전에는 timm을 더 많이 사용)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-27T03:04:08.366843Z","iopub.execute_input":"2026-06-27T03:04:08.367096Z","iopub.status.idle":"2026-06-27T03:04:11.860767Z","shell.execute_reply.started":"2026-06-27T03:04:08.367075Z","shell.execute_reply":"2026-06-27T03:04:11.860048Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import timm\n\nmodel=timm.create_model('vit_base_patch16_384', pretrained=True, num_classes=1)  #224모델을 384로 파인튜닝 한 모델, 헤드는 초기화\n\ndummy_img=torch.randn(2,3,384,384)\noutput=model(dummy_img)\nprint(output.shape)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-27T03:04:11.862813Z","iopub.execute_input":"2026-06-27T03:04:11.863061Z","iopub.status.idle":"2026-06-27T03:04:14.298081Z","shell.execute_reply.started":"2026-06-27T03:04:11.863035Z","shell.execute_reply":"2026-06-27T03:04:14.297330Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"for name, params in model.named_parameters():\n    print(name)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-27T03:04:14.299149Z","iopub.execute_input":"2026-06-27T03:04:14.299471Z","iopub.status.idle":"2026-06-27T03:04:14.309631Z","shell.execute_reply.started":"2026-06-27T03:04:14.299445Z","shell.execute_reply":"2026-06-27T03:04:14.308783Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"!pip install torchinfo","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-27T03:04:14.310759Z","iopub.execute_input":"2026-06-27T03:04:14.311100Z","iopub.status.idle":"2026-06-27T03:04:17.549605Z","shell.execute_reply.started":"2026-06-27T03:04:14.311077Z","shell.execute_reply":"2026-06-27T03:04:17.548870Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"from torchinfo import summary\n\nsummary(model, input_size=(2,3,384,384))","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-27T03:04:17.550800Z","iopub.execute_input":"2026-06-27T03:04:17.551469Z","iopub.status.idle":"2026-06-27T03:04:18.166930Z","shell.execute_reply.started":"2026-06-27T03:04:17.551437Z","shell.execute_reply":"2026-06-27T03:04:18.166105Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Training","metadata":{}},{"cell_type":"code","source":"from torch import optim\nfrom torch import nn\n\ndevice='cuda' if torch.cuda.is_available() else 'cpu'\n\nmodel=model.to(device)\n\ngrad_params=[p for p in model.named_parameters() if p[1].requires_grad]\n\nno_decay = [\"bias\", \"norm\"]   # Bias나 LayerNorm은 weight decay 사용 안하고 보호(bias는 과적합에 영향x, layernorm은 모델 표현력을 올려주는 계층)\n\noptimizer_grouped_parameters = [\n    {\n        \"params\": [p for n, p in grad_params if not any(nd in n for nd in no_decay)],\n        \"weight_decay\": 1e-2,  #weight decay가 그래디언트 항하고 분리, lr과 곱해져서 빼지기 때문에 큰 값 아님.그리고 vit는 과적합에 취약\n    },\n    {\n        \"params\": [p for n, p in grad_params if any(nd in n for nd in no_decay)],\n        \"weight_decay\": 0.0, \n    },\n]\n\noptimizer = optim.AdamW(optimizer_grouped_parameters, lr=BaseConfig.LR)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-27T03:04:18.167995Z","iopub.execute_input":"2026-06-27T03:04:18.168307Z","iopub.status.idle":"2026-06-27T03:04:18.178775Z","shell.execute_reply.started":"2026-06-27T03:04:18.168285Z","shell.execute_reply":"2026-06-27T03:04:18.178043Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"#### balance loss 만들기","metadata":{}},{"cell_type":"code","source":"#클래스별 불균형이 심해서 Smooth L1 Loss에 클래스 개수 역수 곱한 가중치 추가(balanced loss)\nfrom collections import Counter\n\nlabels=train_df['diagnosis']\ncounter=Counter(labels)\ntotal=len(labels)\n\nclass_weights={c: total/(len(counter)*count) for c, count in counter.items()}\n#len(counter)을 곱해주는건 가중치 평균을 1로 맞추기 위함(가중치 너무 커지거나 작아지면 loss 영향)\nprint(class_weights)\nsorted_weights=[class_weights[i] for i in range(len(counter))]\nprint(sorted_weights)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-27T03:04:18.179731Z","iopub.execute_input":"2026-06-27T03:04:18.180010Z","iopub.status.idle":"2026-06-27T03:04:18.193697Z","shell.execute_reply.started":"2026-06-27T03:04:18.179977Z","shell.execute_reply":"2026-06-27T03:04:18.192860Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class BalancedLoss(nn.Module):\n    def __init__(self, class_weights):\n        super().__init__()\n        #weight 버퍼로 등록(device 변경할떄 필요), self.class_weights 속성으로 등록\n        self.register_buffer('class_weights', torch.FloatTensor(class_weights))    \n        self.loss=nn.SmoothL1Loss(reduction='none')\n\n    def forward(self, pred, target):\n        # pred가 (32, 1)로 들어오면 (32,)로 짜부러뜨리기\n        if pred.dim() == 2 and pred.size(1) == 1:\n            pred = pred.squeeze(1)\n        loss=self.loss(pred, target)\n        weight=self.class_weights[target.long()]\n\n        return (loss*weight).mean()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-27T03:04:18.194643Z","iopub.execute_input":"2026-06-27T03:04:18.194993Z","iopub.status.idle":"2026-06-27T03:04:18.207254Z","shell.execute_reply.started":"2026-06-27T03:04:18.194962Z","shell.execute_reply":"2026-06-27T03:04:18.206533Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"criterion=BalancedLoss(sorted_weights).to(device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-27T03:04:18.208072Z","iopub.execute_input":"2026-06-27T03:04:18.208338Z","iopub.status.idle":"2026-06-27T03:04:18.241978Z","shell.execute_reply.started":"2026-06-27T03:04:18.208308Z","shell.execute_reply":"2026-06-27T03:04:18.241464Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"#스케쥴러 설정-0.5배씩 줄이기\nscheduler=optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.5, patience=2)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-27T03:04:18.242711Z","iopub.execute_input":"2026-06-27T03:04:18.242987Z","iopub.status.idle":"2026-06-27T03:04:18.248251Z","shell.execute_reply.started":"2026-06-27T03:04:18.242950Z","shell.execute_reply":"2026-06-27T03:04:18.247642Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"#### 학습 시작","metadata":{}},{"cell_type":"code","source":"from tqdm.notebook import tqdm\n\nbest_val_loss=float('inf')\n\nfor epoch in range(BaseConfig.EPOCHS):\n\n    \n    save_path='best_aptos_model.pt'\n\n    #모델 훈련\n    model.train()\n    train_loss=0.0\n\n    for images, labels in tqdm(train_dataloader):\n        images=images.to(device)\n        labels=labels.to(device)\n\n        output=model(images)\n        loss=criterion(output, labels)\n\n        optimizer.zero_grad()\n        loss.backward()\n        optimizer.step()\n\n        train_loss+=loss.item()*images.shape[0]\n\n\n    train_loss=train_loss/len(train_dataloader.dataset)\n    print(f'Epoch: {epoch+1:4d}, Train Loss: {train_loss:.4f}')\n\n    model.eval()\n    val_loss=0\n\n    with torch.no_grad():\n        for images, labels in tqdm(valid_dataloader):\n            images=images.to(device)\n            labels=labels.to(device)\n\n            output=model(images)\n            loss=criterion(output, labels)\n\n            val_loss+=loss.item()*images.shape[0]\n\n    val_loss=val_loss/len(valid_dataloader.dataset)\n\n    scheduler.step(val_loss)\n    current_lr=optimizer.param_groups[0]['lr']\n    print(f'Validation Loss: {val_loss:.4f}, Current LR: {current_lr:.6f}')\n\n    if val_loss<best_val_loss:\n        print('가장 낮은 val loss 기록, 모델 저장')\n\n        best_val_loss=val_loss\n        torch.save(model.state_dict(), save_path)\n    ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-27T04:17:34.386585Z","iopub.execute_input":"2026-06-27T04:17:34.387346Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 추론","metadata":{}},{"cell_type":"code","source":"submission=pd.read_csv('../input/competitions/aptos2019-blindness-detection/sample_submission.csv')\nprint(submission.head())\ntest_df.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-27T03:22:42.323667Z","iopub.status.idle":"2026-06-27T03:22:42.324113Z","shell.execute_reply.started":"2026-06-27T03:22:42.323903Z","shell.execute_reply":"2026-06-27T03:22:42.323926Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"model.load_state_dict(torch.load('best_aptos_model.pt', map_location=device))\n\npred_list=[]\n\nmodel.eval()\n\nwith torch.no_grad():\n\n    for images in tqdm(test_dataloader):\n        images=images.to(device)\n        output=model(images)\n\n        pred_list.append(output.cpu())  #VRAM용량이 부족해질 수 있으므로 cpu로 옮기기\n\n    pred=torch.cat(pred_list, dim=0)\n    pred=pred.squeeze()\n    pred=torch.clamp(pred, 0, 4)\n    pred=torch.round(pred).long()\n\nsubmission['diagnosis']=pred.numpy()\nsubmission.head()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-27T03:22:42.325903Z","iopub.status.idle":"2026-06-27T03:22:42.326213Z","shell.execute_reply.started":"2026-06-27T03:22:42.326086Z","shell.execute_reply":"2026-06-27T03:22:42.326108Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"submission.to_csv('submission.csv', index=False)\nprint('성공적으로 제출 완료')","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-06-27T03:22:42.327954Z","iopub.status.idle":"2026-06-27T03:22:42.328382Z","shell.execute_reply.started":"2026-06-27T03:22:42.328162Z","shell.execute_reply":"2026-06-27T03:22:42.328187Z"}},"outputs":[],"execution_count":null}]}