{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"pygments_lexer":"ipython3","nbconvert_exporter":"python","version":"3.6.4","file_extension":".py","codemirror_mode":{"name":"ipython","version":3},"name":"python","mimetype":"text/x-python"},"kaggle":{"accelerator":"gpu","dataSources":[{"sourceId":52254,"databundleVersionId":9674523,"sourceType":"competition"},{"sourceId":14593051,"sourceType":"datasetVersion","datasetId":9319337},{"sourceId":738439,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":562501,"modelId":575094}],"dockerImageVersionId":31259,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"!pip install monai","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import pandas as pd\nimport os\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport numpy as np\nimport matplotlib.pyplot as plt\nfrom torch.amp import GradScaler, autocast # 속도 및 메모리 최적화\nfrom monai.transforms import (\n    Compose, MapTransform, SelectItemsd, RandFlipd, RandAffined,\n    RandGridDistortiond, RandGaussianNoised, RandAdjustContrastd, \n    RandGaussianSmoothd, Transposed, ToTensord\n)\nfrom monai.data import Dataset, DataLoader\nfrom sklearn.model_selection import train_test_split\nfrom monai.networks.nets import resnet18\nimport torchmetrics # AUC 계산을 쉽게 해주는 라이브러리\nfrom torch.optim.lr_scheduler import CosineAnnealingLR, ReduceLROnPlateau\nimport pickle  #파일저장에\nfrom tqdm import tqdm  # 학습 진행 상황 시각화를 위해 추가\nfrom torch.optim import AdamW\nfrom joblib import Parallel, delayed    # cpu 사용하기 해해\nimport timm\nfrom collections import OrderedDict\n\n\n# 0. 설정 및 경로\nBASE_DIR = '/kaggle/input/rsna-2023-abdominal-trauma-detection/'\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nNUM_EPOCHS = 30\nCLASS_NAME_LIST = ['bowel', 'extravasation', 'kidney', 'liver', 'spleen', 'any_injury']\nLEARNING_RATE = 1e-4\n\nMONAI_MODEL_SAVE_PATH = '/kaggle/working/monai_ct_convnext_v6_3.pth'\n\nMONAI_MODEL_SAVE_PATH_CONTINUE = '/kaggle/input/ct-v6/pytorch/default/3/monai_ct_convnext_v6_2_ep17.pth'\n\nIMAGE_TARGET = (64,224,224)\nNUM_SLICES = 64\n\n# 전처리된 데이터를 저장할 폴더\nSAVE_DIR = '/kaggle/input/rsna-2023-atd-preprocessed-s224/result/'\nos.makedirs(SAVE_DIR, exist_ok=True)\n\n# 파일 읽기\ntrain_df = pd.read_csv(f'{BASE_DIR}train_2024.csv') # 파일명 확인 필요 (보통 train.csv)\ntags_df = pd.read_parquet(f'{BASE_DIR}train_dicom_tags.parquet')\n\n# 고유 폴더 경로 추출 및 환자 ID 연결\ntags_df['series_path'] = tags_df['path'].str.split('/').str[:-1].str.join('/')\nunique_series = tags_df[['PatientID', 'series_path']].drop_duplicates()\n\ndata_dicts = []\nfor idx, row in unique_series.iterrows():\n    p_id = int(row['PatientID'])\n    s_path = row['series_path']\n    \n    # 해당 환자의 라벨 정보 가져오기\n    patient_labels = train_df[train_df['patient_id'] == p_id]\n    if len(patient_labels) == 0: continue # 라벨 없는 경우 제외\n    labels = patient_labels.iloc[0]\n    \n    data_dicts.append({\n        \"image\": f\"{BASE_DIR}{s_path}\",\n        \"patient_id\": p_id,\n\n        # 2진 분류 (Healthy, Injury) -> [1, 0] 또는 [0, 1] 형태가 됨\n        \"bowel\": labels[['bowel_healthy', 'bowel_injury']].values.astype(\"float32\"),\n        \"extravasation\": labels[['extravasation_healthy', 'extravasation_injury']].values.astype(\"float32\"),\n        \n        # 3중 분류 (Healthy, Low, High) -> [1, 0, 0], [0, 1, 0], [0, 0, 1] 형태가 됨\n        \"liver\": labels[['liver_healthy', 'liver_low', 'liver_high']].values.astype(\"float32\"),\n        \"kidney\": labels[['kidney_healthy', 'kidney_low', 'kidney_high']].values.astype(\"float32\"),\n        \"spleen\": labels[['spleen_healthy', 'spleen_low', 'spleen_high']].values.astype(\"float32\"),\n\n        # any_injury가 1이면 \"어딘가 이상함\", 0이면 \"완전 건강\"\n        \"any_injury\": np.array([1 - labels['any_injury'], labels['any_injury']]).astype(\"float32\")\n        \n    })\n\npatient_ids = train_df['patient_id'].unique()\ntrain_ids, val_ids = train_test_split(patient_ids, test_size=0.2, random_state=42)\ntrain_files = [d for d in data_dicts if d['patient_id'] in train_ids] # data_dicts에 patient_id 키 추가 필요\nval_files = [d for d in data_dicts if d['patient_id'] in val_ids]\n\nprint(f\"준비된 데이터 수: {len(data_dicts)}\")\nprint(f\"디바이스: {DEVICE}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class Timm_Model(torch.nn.Module):\n    def __init__(self, model_name='convnext_tiny', num_slices=64):\n        super().__init__()\n        # 특징 추출기 (ConvNeXt)\n        # num_classes = 1000 (기본값): 모델의 최종 출력이 1,000개의 숫자(카테고리 점수)로 나옵니다.\n        # num_classes = 0: 1,000개를 맞히는 마지막 층을 아예 없애버립니다. 대신, 그 바로 직전 단계인 **'이미지의 핵심 특징 정보(Feature Vector)'**를 그대로 출력합니다.\n        self.backbone = timm.create_model(model_name, pretrained=True, num_classes=0)\n        \n\n        for param in self.backbone.parameters():\n            param.requires_grad = False\n            \n        self.dim = self.backbone.num_features # tiny 기준 768\n        self.num_slices = num_slices\n        self.gated_norm = nn.LayerNorm(self.dim)\n\n        # Position Encoding (슬라이스 번호 매기기)\n        self.position_embedding = nn.Parameter(torch.zeros(1, num_slices, self.dim))\n        self.position_dropout = nn.Dropout(0.1)\n\n        encoder_layer = nn.TransformerEncoderLayer(\n            d_model=self.dim, \n            nhead=8, \n            dim_feedforward=self.dim * 2, \n            dropout=0.1, \n            batch_first=True\n        )\n        self.transformer_encoder = nn.TransformerEncoder(encoder_layer, num_layers=2)\n\n        # 어텐션 풀링: 64장 중 수상한 놈을 골라내는 '심사위원'\n        self.attention_net = nn.Sequential(\n            nn.Linear(self.dim, 256),\n            nn.Tanh(),\n            nn.Dropout(0.1), # 추가\n            nn.Linear(256, 1)\n        )\n        \n        # \"종합 이상 징후 탐지\" 전용 헤드 (의심 모델 역할)\n        self.suspicion_head = nn.Sequential(\n            nn.Linear(self.dim, 256),  # 768개를 256개의 핵심 의심 후보로 압축\n            nn.LayerNorm(256),  # 학습을 안정적으로 만들어줌\n            nn.ReLU(),            # 중요한 의심 신호만 통과시킴\n            nn.Dropout(0.2),      # 과적합 방지 (너무 예민해지는 것 방지)\n            nn.Linear(256, 2)     # 최종 경보 [정상, 이상]\n        )\n\n        # \"정밀 병명 분류\" 전용 헤드 (분류 모델 역할)\n        # 장기별 결과 2 or 3개 도출\n        self.organ_heads = nn.ModuleDict({\n            'bowel': nn.Linear(self.dim, 2),\n            'extravasation': nn.Linear(self.dim, 2),\n            'liver': nn.Linear(self.dim, 3),\n            'kidney': nn.Linear(self.dim, 3),\n            'spleen': nn.Linear(self.dim, 3)\n        })\n\n    # [입력 데이터] (Batch, 64 Slices, 3, 128, 128)\n    #     ↓\n    # ==========================================================\n    # 1. [Backbone: ConvNeXt-Tiny] -> \"시각 신경 (이미지 스캐너)\"\n    # - 64장의 슬라이스를 각각 스캔하여 768차원의 특징 추출\n    # - (64, 3, 128, 128) -> (64, 768)\n    # ==========================================================\n    #     ↓\n    # 2. [Position Embedding] -> \"인덱스 부여 (공간 좌표)\"\n    # - 각 특징에 \"이건 1번(머리), 이건 64번(골반)\"이라는 위치 정보 주입\n    # ==========================================================\n    #     ↓\n    # 3. [Transformer Encoder] -> \"종합 분석 (슬라이스 간 대화)\"\n    # - 64장의 특징들이 서로 정보를 교환하며 전후 맥락 파악\n    # - \"5번 슬라이스의 상처가 10번까지 이어지네? 큰 부상이다!\"\n    # ==========================================================\n    #     ↓\n    # 4. [Attention Pooling] -> \"심사위원 (결정적 증거 포착)\"\n    # - 64장 중 가장 수상한(부상이 의심되는) 슬라이스에 높은 점수 부여\n    # - 64개의 특징을 단 1개의 '필살기 특징 벡터'로 압축 (1, 768)\n    # ==========================================================\n    #     ↓\n    # ==========================================================\n    # 5. [suspicion_head] -> \"응급의학과 의사\" (전체 부상 유무 판단)\n    # - [부상 확률 (injury_prob)] (0.0 ~ 1.0)\n    # ==========================================================\n    #     ↓\n    # ==========================================================\n    # [organ_heads] -> \"전문의 진단 (최종 판단)\"\n    # - Bowel, Liver, Kidney, Spleen 등 정밀 진단\n    # - 부상 확률을 곱하기 때문에 부상에 결과에 영향을 받음\n    # ==========================================================\n    def forward(self, x):\n        # 2.5D 방식으로 전체 슬라이스 훑기\n        # x shape: (Batch, 64, 3, 128, 128)\n        b, s, c, h, w = x.shape\n        \n        chunk_size = 8 # 한 번에 처리할 슬라이스 개수 (메모리에 따라 조절)\n        all_features = []\n        \n        for i in range(0, s, chunk_size):\n            # x_chunk: (Batch, 16, 3, 128, 128)\n            x_chunk = x[:, i : i + chunk_size] \n            \n            # 2D 연산을 위해 일시적으로 배치 차원으로 합침\n            x_chunk = x_chunk.reshape(-1, c, h, w) # (Batch*16, 3, 128, 128)\n            \n            # 백본 통과 (이 순간 메모리 사용량이 chunk_size만큼으로 제한됨)\n            feat_chunk = self.backbone(x_chunk) # (Batch*16, 768)\n            \n            # 다시 슬라이스 차원 분리 후 리스트에 저장\n            feat_chunk = feat_chunk.view(b, -1, self.dim) \n\n            all_features.append(feat_chunk)\n\n        # 모든 특징 합치기\n        features = torch.cat(all_features, dim=1) # (Batch, 64, 768)\n    \n        # --- [Step 2] Position Encoding: 위치 정보 주입 ---\n        # 데이터가 정렬되어 들어와도, 모델이 이를 '좌표'로 인식하게 함\n        features = features + self.position_embedding\n        features = self.position_dropout(features)\n\n        # --- [Step 3] Transformer Encoder: 슬라이스 간 상호작용 ---\n        # 64장의 슬라이스가 서로의 정보를 참조하여 입체적인 특징으로 진화\n        features = self.transformer_encoder(features) # (Batch, 64, 768)\n\n        # Attention Pooling으로 '이상 지점' 증폭\n        # 각 슬라이스의 수상함 점수 계산\n        att_scores = self.attention_net(features) # (B, 64, 1)\n        \n         # 점수를 0~1 사이 비중(가중치)으로 변환\n        att_weights = F.softmax(att_scores, dim=1) # (B, S, 1)\n        \n        # 가중치를 곱해서 하나로 합침 (가장 수상한 슬라이스 정보가 증폭됨)\n        combined = torch.sum(features * att_weights, dim=1) # (B, 768)\n\n        # 결과 도출\n        # 1. 먼저 \"부상 유무\"를 판단합니다.\n        injury_logits = self.suspicion_head(combined) # (B, 2)\n        # 부상일 확률(Probability)을 구합니다.\n        injury_prob = torch.softmax(injury_logits, dim=1)[:, 1:2] # (B, 1)\n\n        # 2. [핵심] 부상 확률을 장기별 특징에 곱해줍니다 (Gating)\n        # 부상이 아닐 것 같으면(0에 가까우면) 장기별 점수들도 0에 가까워지도록 강제합니다.\n        gated_features = self.gated_norm(combined * injury_prob)\n\n        # 3. 정밀 진단은 이 게이트를 통과한 특징으로 수행합니다.\n        out = {k: head(gated_features) for k, head in self.organ_heads.items()}\n        out['any_injury'] = injury_logits\n\n        return out\n\n\ndef monai_train_pipeline():\n    return Compose([\n        LoadNpyTransformd(keys=[\"image\"]),\n\n        # 공간적 변형 (Spatial)\n        # spatial_axis: 0=S(Slices), 1=H, 2=W\n        RandFlipd(keys=[\"image\"], prob=0.5, spatial_axis=1), # 좌우 반전\n        RandFlipd(keys=[\"image\"], prob=0.5, spatial_axis=2), # 상하 반전\n        \n        RandAffined(\n            keys=[\"image\"],\n            prob=0.3,\n            # (S, H, W) 각 축에 대한 회전/스케일\n            rotate_range=(0.1, 0.1, 0.1), \n            scale_range=(0.1, 0.1, 0.1),\n            translate_range=(15, 15, 15),\n            padding_mode=\"zeros\",\n            mode=\"bilinear\"\n        ),\n        \n        # 형태적 변형 (Grid Distortion)\n        # 매우 강력하지만 연산량이 많음. T4 x2에서는 CPU 병목을 확인하며 사용할 것.\n        RandGridDistortiond(\n            keys=[\"image\"],\n            prob=0.2,\n            num_cells=(4, 4, 4),\n            distort_limit=(-0.05, 0.05), # 변형 강도 추가 (옵션)\n            mode=\"bilinear\"\n        ),\n        \n        # 강도 및 노이즈 (Intensity)\n        RandGaussianNoised(keys=[\"image\"], prob=0.2, mean=0.0, std=0.05),\n        RandAdjustContrastd(keys=[\"image\"], prob=0.2, gamma=(0.7, 1.3)),\n        RandGaussianSmoothd(keys=[\"image\"], prob=0.1, sigma_x=(0.5, 1.0)),\n        \n        # 모델 입력을 위해 다시 원래 차원으로 복구 (S, C, H, W)\n        # Timm_Model이 (Batch, Slices, C, H, W)를 기대하므로\n        Transposed(keys=[\"image\"], indices=(1, 0, 2, 3)),\n\n        ToTensord(keys=[\"image\"] + CLASS_NAME_LIST),\n        \n        SelectItemsd(keys=[\"image\"] + CLASS_NAME_LIST)\n    ])\n\n\ndef monai_val_pipeline():\n    return Compose([\n        LoadNpyTransformd(keys=[\"image\"]),\n        Transposed(keys=[\"image\"], indices=(1, 0, 2, 3)),\n        ToTensord(keys=[\"image\"] + CLASS_NAME_LIST),\n        SelectItemsd(keys=[\"image\"] + CLASS_NAME_LIST)\n    ])\n    \n\nclass LoadNpyTransformd(MapTransform):\n    def __call__(self, data):\n        d = dict(data)\n        file_path = d[\"image\"]\n        try:\n            img = np.load(file_path)\n            if img.shape[-1] == 3: # (64, 128, 128, 3)인 경우\n                img = np.transpose(img, (3, 0, 1, 2)) # (3, 64, 128, 128)\n            d[\"image\"] = torch.from_numpy(img).float()\n        except Exception as e:\n            print(f\"\\n❌ npy 파일 로드 실패: {file_path} | 에러: {e}\")\n            raise e\n        return d\n\n\ndef evaluate(model, loader, epoch, criterion_dict):\n    model.eval()\n    val_epoch_loss = 0\n    auc_metrics = torch.nn.ModuleDict({\n        'bowel': torchmetrics.AUROC(task=\"multiclass\", num_classes=2),\n        'extravasation': torchmetrics.AUROC(task=\"multiclass\", num_classes=2),\n        'liver': torchmetrics.AUROC(task=\"multiclass\", num_classes=3),\n        'kidney': torchmetrics.AUROC(task=\"multiclass\", num_classes=3),\n        'spleen': torchmetrics.AUROC(task=\"multiclass\", num_classes=3),\n        'any_injury': torchmetrics.AUROC(task=\"multiclass\", num_classes=2)\n    }).to(DEVICE)\n\n    with torch.no_grad():\n        val_loop = tqdm(loader, desc=f\"Epoch {epoch+1}/{NUM_EPOCHS} [Validation]\", leave=False)\n        for batch in val_loop:\n            inputs = batch[\"image\"].to(DEVICE)\n            outputs = model(inputs)\n\n            # 분류\n            loss = 0\n            for k in CLASS_NAME_LIST:\n                preds = torch.softmax(outputs[k], dim=1)\n                if hasattr(preds, \"as_tensor\"): # MetaTensor인 경우에만 호출\n                    preds = preds.as_tensor()\n\n                # 2. 정답값 처리\n                target = batch[k].to(DEVICE)\n                if hasattr(target, \"as_tensor\"): # 여기서 메타데이터 제거\n                    target = target.as_tensor()\n\n                # 이미 target이 순수 텐서이므로, 이후 연산 결과(target_idx)도 순수 텐서입니다.\n                if target.dim() > 1:\n                    target_idx = torch.argmax(target, dim=1)\n                else:\n                    target_idx = target.long()\n\n                # [수정] target_idx.as_tensor() 줄을 삭제하거나 아래와 같이 안전하게 변경\n                # target_idx = target_idx.as_tensor() <- 이 줄이 에러의 원인이었습니다. 삭제하세요.\n\n                # 3. 메트릭 업데이트\n                auc_metrics[k].update(preds, target_idx)\n\n                # 4. Loss 계산 (outputs[k]도 안전하게 변환)\n                out_k = outputs[k].as_tensor() if hasattr(outputs[k], \"as_tensor\") else outputs[k]\n                loss_func = criterion_dict[k]\n                loss += loss_func(out_k, target_idx)\n                    \n            val_epoch_loss += loss.item()\n            val_loop.set_postfix(val_loss=loss.item())\n\n    avg_val_loss = val_epoch_loss / len(loader)\n    \n    # AUC 계산\n    auc_results = {k: auc_metrics[k].compute().item() for k in CLASS_NAME_LIST}\n    for k in CLASS_NAME_LIST: auc_metrics[k].reset()\n    \n    return auc_results, avg_val_loss\n\n\ndef process_one_item(item):\n    new_item = item.copy()\n    \n    # 원본 경로에서 시리즈 ID 추출 (예: train_images/123/456 -> 456)\n    \n    s_id = new_item['image'].split(\"/\")[-1]\n    \n    # 통합 전처리 파일 경로 예: /kaggle/working/final_output/456.npy\n    target_path = os.path.join(SAVE_DIR, f\"{s_id}.npy\")\n\n    if os.path.isfile(target_path):\n        \n        new_item['image'] = target_path\n        return new_item\n        \n    return None\n    \n\n# Stage 1 (Epoch 0-5): \"신입 의사 교육\"\n# 동결: Backbone (이미지 스캐너)\n# 학습: Transformer + Heads (판단 로직)\n# 이유: 이미 똑똑한 스캐너는 그대로 두고, 의료 데이터의 특징을 조합해 판단하는 뇌(Heads)부터 먼저 가르칩니다.\n# Stage 2 (Epoch 6-10): \"전문가와 손발 맞추기\"\n# 동결 해제: Backbone의 마지막 층(Stage 3)\n# 이유: 판단 능력이 생긴 뇌에 맞춰, 가장 정교한 정보를 뽑는 마지막 시각 층을 미세 조정합니다.\n# Stage 3 (Epoch 11-20): \"팀워크 전체 최적화\"\n# 동결 해제: 전체 파라미터\n# 이유: 모든 층이 한 팀이 되어 의료 영상의 아주 미세한 특징까지 잡아내도록 아주 낮은 학습률로 마무리 훈련을 합니다.\ndef train(train_files_preprocessed, val_files_preprocessed, \n          train_pipeline, val_pipeline,\n          model_save_path):\n    \n    \n    # 비교하기\n    # 구분\t    \tBCEWithLogitsLoss\t\t      \t\tCrossEntropyLoss\n    # 풀네임\t\t    Binary Cross Entropy with Logits\t\t(Multiclass) Cross Entropy\n    # 주요 목적\t\t이진 분류 (Yes or No)\t\t\t    \t다중 분류 (A, B, C 중 하나)\n    # 출력 노드 수\t1개 (0~1 사이의 확률)\t\t\t    \tN개 (각 클래스별 점수)\n    # 활성 함수\t\tSigmoid (내장됨)\t\t\t\t    \tSoftmax (내장됨)\n    # 타겟 라벨\t\t0.0 또는 1.0 (Float)\t\t\t     \t0, 1, 2... 인덱스 (Long)\n    # 특징\t\t    각 타겟이 독립적임 (Multi-label 가능)\t타겟 간 경쟁 관계 (합이 1이 됨)\n    \n    train_ds = Dataset(data=train_files_preprocessed, transform=train_pipeline)\n    val_ds = Dataset(data=val_files_preprocessed, transform=val_pipeline)\n    \n    train_loader = DataLoader(train_ds, batch_size=2, shuffle=True, num_workers=4, pin_memory=(DEVICE.type == 'cuda') )\n    val_loader = DataLoader(val_ds, batch_size=2, shuffle=True, num_workers=4, pin_memory=(DEVICE.type == 'cuda') )\n    \n    # 2. 모델, 손실함수, 옵티마이저\n    model = Timm_Model(model_name='convnext_tiny').to(DEVICE)\n    if torch.cuda.device_count() > 1:\n        print(\"2개의 GPU를 사용합니다.\")\n        model = nn.DataParallel(model) # 모델을 복사하여 양쪽 GPU에 분산\n\n    criterion_dict = {\n        'bowel': nn.CrossEntropyLoss(weight=torch.tensor([1.0, 10.0]).to(DEVICE), label_smoothing=0.05),\n        'extravasation': nn.CrossEntropyLoss(weight=torch.tensor([1.0, 10.0]).to(DEVICE), label_smoothing=0.05),\n        'any_injury': nn.CrossEntropyLoss(weight=torch.tensor([1.0, 10.0]).to(DEVICE), label_smoothing=0.05),\n        'liver': nn.CrossEntropyLoss(weight=torch.tensor([1.0, 3.0, 5.0]).to(DEVICE), label_smoothing=0.05),\n        'kidney': nn.CrossEntropyLoss(weight=torch.tensor([1.0, 3.0, 5.0]).to(DEVICE), label_smoothing=0.05),\n        'spleen': nn.CrossEntropyLoss(weight=torch.tensor([1.0, 3.0, 5.0]).to(DEVICE), label_smoothing=0.05),\n    }\n\n    scaler = GradScaler('cuda', enabled=(DEVICE.type == 'cuda')) # DEVICE가 object이므로 .type 추가 권장\n    \n    history = {\n        \"train_loss\": [],\n        \"val_loss\": [],\n        \"auc_avg_loss\": [],  # 이전에 만든 AUC도 기록\n        \"auc_details\": []\n    }\n\n    # 1. 초기 설정: Epoch 0~5 (Head만 학습)\n    def set_stage_1_head_only(model):\n        # 일단 모든 파라미터를 얼림\n        for param in model.parameters():\n            param.requires_grad = True\n        \n        target_model = model.module if hasattr(model, 'module') else model\n        for param in target_model.backbone.parameters():\n            param.requires_grad = False\n        \n        trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)\n        print(f\">>> Stage 1 설정 완료: Backbone 동결 / Transformer & Heads 해제\")\n        print(f\">>> 현재 학습 가능한 파라미터 수: {trainable_params:,}개\")\n        \n        #requires_grad가 True인 것들만 옵티마이저에 전달\n        return AdamW(filter(lambda p: p.requires_grad, model.parameters()), lr=LEARNING_RATE)\n\n    # optimizer = set_stage_1_head_only(model)\n    optimizer = AdamW(model.parameters(), lr=LEARNING_RATE) \n    scheduler = ReduceLROnPlateau(optimizer, mode='min', factor=0.1, patience=3)\n    start_epoch = 0\n    if os.path.exists(MONAI_MODEL_SAVE_PATH_CONTINUE):\n        print(f\"==> 로드 중: {MONAI_MODEL_SAVE_PATH_CONTINUE}\")\n        checkpoint = torch.load(MONAI_MODEL_SAVE_PATH_CONTINUE, map_location=DEVICE)\n        \n        # [수정] module. 접두어 제거 후 로드\n        state_dict = checkpoint['model']\n        new_state_dict = OrderedDict()\n        for k, v in state_dict.items():\n            name = k[7:] if k.startswith('module.') else k\n            new_state_dict[name] = v\n        \n        model.load_state_dict(new_state_dict) # 수정된 new_state_dict를 넣어야 합니다.\n        optimizer.load_state_dict(checkpoint['optimizer'])\n        print(\"✅ optimizer 상태 로드 완료\")\n\n        scheduler.load_state_dict(checkpoint['scheduler'])\n        print(\"✅ scheduler 로드 완료\")\n\n        scaler.load_state_dict(checkpoint['scaler'])\n        print(\"✅ scaler 로드 완료\")\n\n        history = checkpoint['history']\n        print(\"✅ history 로드 완료\")\n\n        start_epoch = checkpoint['epoch']\n        print(f\"✅ {start_epoch} 에포크부터 재개합니다.\")\n\n    best_mean_auc = 0.0\n\n    for epoch in range(start_epoch, NUM_EPOCHS):\n        \n        target_model = model.module if hasattr(model, 'module') else model\n        if start_epoch >= 11:\n            for param in target_model.parameters(): param.requires_grad = True\n            print(\"🔓 모든 층 활성화 완료\")\n        elif start_epoch >= 6:\n            for param in target_model.backbone.stages[3].parameters(): param.requires_grad = True\n            print(\"🔓 백본 스테이지 3 활성화 완료\")\n        \n        model.train()\n        train_epoch_loss = 0\n    \n    \n        train_loop = tqdm(train_loader, desc=f\"Epoch {epoch+1}/{NUM_EPOCHS} [Train]\", leave=False)\n        for batch in train_loop:\n            inputs = batch[\"image\"].to(DEVICE)\n            optimizer.zero_grad()\n            \n            with autocast(device_type=DEVICE.type, enabled=(DEVICE.type == 'cuda')):\n                outputs = model(inputs)\n                \n                # Loss 계산 (초기화 중요)\n                loss = 0\n                for k in CLASS_NAME_LIST:\n                    target = batch[k].to(DEVICE)\n                    if hasattr(target, \"as_tensor\"):\n                        target = target.as_tensor() # 메타데이터 미리 제거\n\n                    if target.dim() > 1:\n                        target = torch.argmax(target, dim=1)\n                    target = target.long() # 이제 target은 확실한 순수 텐서\n                    \n                    # 예측값도 안전하게 처리\n                    pred_k = outputs[k].as_tensor() if hasattr(outputs[k], \"as_tensor\") else outputs[k]\n                    loss_func = criterion_dict[k]\n\n                    if k == 'any_injury':\n                        # 문지기인 any_injury는 손실값 자체에도 배율을 주어 학습을 리드하게 함\n                        loss += loss_func(pred_k, target)\n                    else:\n                        loss += loss_func(pred_k, target)\n                \n            scaler.scale(loss).backward()\n            scaler.step(optimizer)\n            scaler.update()\n    \n            train_epoch_loss += loss.item()\n            train_loop.set_postfix(loss=loss.item())\n    \n        avg_train_loss = train_epoch_loss / len(train_loader)\n    \n    \n        # 에포크 종료 후 성능 출력\n        auc_results, avg_val_loss = evaluate(model, val_loader, epoch, criterion_dict) # 실무에선 val_loader 사용 권장\n        mean_auc = sum(auc_results.values()) / len(auc_results)\n        \n        history[\"train_loss\"].append(avg_train_loss)\n        history[\"val_loss\"].append(avg_val_loss)\n        history[\"auc_avg_loss\"].append(mean_auc)\n        history[\"auc_details\"].append(auc_results)\n    \n        current_lr = optimizer.param_groups[0]['lr']\n        \n        # CosineAnnealingLR 사용 시 (에포크 끝날 때마다 호출)\n        # scheduler.step()\n        \n        # ReduceLROnPlateau 사용시\n        scheduler.step(avg_val_loss) \n        \n        print(f\"\\n>>> Epoch {epoch+1} Summary\")\n        print(f\"LR: {current_lr:.6f} | Train Loss: {avg_train_loss:.4f} | Val Loss: {avg_val_loss:.4f}\")\n        print(f\"Mean AUC: {mean_auc:.4f}\")\n        for organ, val in auc_results.items():\n            print(f\" - {organ:15s}: {val:.4f}\")\n        print(\"-\" * 50)\n\n        # 5. 모델 가중치 저장\n        raw_model = model.module if hasattr(model, 'module') else model\n        save_dict = {\n            'epoch': epoch + 1,\n            'model': raw_model.state_dict(),\n            'optimizer': optimizer.state_dict(),\n            'scheduler': scheduler.state_dict(),\n            'scaler': scaler.state_dict(),\n            'history': history\n        }\n        current_save_path = model_save_path.replace(\".pth\", f\"_ep{epoch}.pth\")\n        torch.save(save_dict, f\"{current_save_path}\")\n        print(f\"✅ 모델 가중치 저장 완료: {current_save_path}\")\n        \n        if mean_auc > best_mean_auc:\n            best_mean_auc = mean_auc\n            best_model_path = model_save_path.replace(\".pth\", \"_best.pth\")\n            torch.save(save_dict, best_model_path)\n            print(f\"🔥 Best AUC 갱신! ({best_mean_auc:.4f}) 별도 저장 완료\")\n            \n        # 6. 학습 히스토리 저장 (Pickle)\n        history_save_path = model_save_path.replace(\".pth\", \".pkl\")\n        with open(history_save_path, 'wb') as file:\n            pickle.dump(history, file)\n        print(f\"✅ 학습 히스토리 저장 완료: {history_save_path}\")\n\n    return history\n\n    \ndef show_history(history):\n    plt.figure(figsize=(15, 6))\n\n    # 1. Loss 그래프 (Training vs Validation)\n    plt.subplot(1, 3, 1)\n    plt.plot(history[\"train_loss\"], label=\"Train Loss\", marker='o')\n    plt.plot(history[\"val_loss\"], label=\"Val Loss\", marker='o')\n    plt.title(\"Training & Validation Loss\")\n    plt.xlabel(\"Epoch\")\n    plt.ylabel(\"Loss\")\n    plt.grid(True)\n    plt.legend()\n    \n    # 2. Mean AUC 그래프\n    # 키 이름을 val_auc_mean으로 수정했습니다.\n    plt.subplot(1, 3, 2)\n    plt.plot(history[\"auc_avg_loss\"], label=\"Mean Val AUC\", color='orange', marker='s')\n    plt.title(\"Mean Validation AUC\")\n    plt.xlabel(\"Epoch\")\n    plt.ylabel(\"AUC\")\n    plt.grid(True)\n    plt.legend()\n    \n    # 장기별로 리스트를 추출하여 그래프 그리기\n    plt.subplot(1, 3, 3) # 1행 3열 중 3번째 (에러 해결 지점)\n    for organ in CLASS_NAME_LIST:\n        # 각 장기별 데이터를 추출하여 루프 안에서 그립니다.\n        organ_auc_history = [epoch_data[organ] for epoch_data in history[\"auc_details\"]]\n        plt.plot(organ_auc_history, label=f\"{organ}\")\n    \n    plt.title(\"Validation AUC by Organ\")\n    plt.xlabel(\"Epoch\")\n    plt.ylabel(\"AUC\")\n    plt.ylim(0.4, 1.05) # AUC가 1일 수도 있으므로 1.05 정도로 설정\n    plt.grid(True, linestyle='--')\n    # 범례가 많을 수 있으므로 그래프 옆으로 뺍니다.\n    plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left', fontsize='small') \n    \n    plt.tight_layout()\n    plt.show()\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_files_subset = train_files[:]\n\ntrain_results = []\nfor item in train_files_subset:\n    train_results.append(process_one_item(item))\n\n# 에포크 에러 방지를 위해 None 제거 (주소록 업데이트)\ntrain_files_preprocessed = [r for r in train_results if r is not None]\n\nval_files_subset = val_files[:]\n\nval_results = []\nfor item in val_files_subset:\n    val_results.append(process_one_item(item))\n\n# 에포크 에러 방지를 위해 None 제거 (주소록 업데이트)\nval_files_preprocessed = [r for r in val_results if r is not None]\n\n# 학습\nmonai_train_loader_pipeline = monai_train_pipeline()\nmonai_val_loader_pipeline = monai_val_pipeline()\n\nprint(\"=\" * 25,\"Train\",\"=\" * 25)\nhistory = train(train_files_preprocessed, val_files_preprocessed, \n                monai_train_loader_pipeline, monai_val_loader_pipeline,\n                MONAI_MODEL_SAVE_PATH)\n\nshow_history(history)","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}