{"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":[{"sourceId":112509,"databundleVersionId":14254895,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":14347407,"sourceType":"datasetVersion","datasetId":9160980},{"sourceId":14347655,"sourceType":"datasetVersion","datasetId":9161151},{"sourceId":14493249,"sourceType":"datasetVersion","datasetId":9256831},{"sourceId":4537,"sourceType":"modelInstanceVersion","isSourceIdPinned":false,"modelInstanceId":3329,"modelId":986}],"dockerImageVersionId":31236,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import os\nos.environ['PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION'] = 'python'\n\nimport numpy as np\nimport pandas as pd\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import Dataset, DataLoader\nimport warnings\nimport logging\nfrom PIL import Image\nfrom transformers import AutoImageProcessor, AutoModel\nfrom sklearn.preprocessing import LabelEncoder, StandardScaler\nfrom tqdm.auto import tqdm\nimport gc\n\nwarnings.filterwarnings(\"ignore\")\nlogging.getLogger(\"transformers\").setLevel(logging.ERROR)\n\nprint(\"=\" * 80)\nprint(\"🎓 CSIRO 記憶體優化版 Knowledge Distillation\")\nprint(\"=\" * 80)\n\n# ==========================================\n# 1. 路徑設定\n# ==========================================\n\ndef find_dino_model():\n    print(\"\\n🔍 搜尋DINOv2模型...\")\n    for root, dirs, files in os.walk('/kaggle/input'):\n        if 'config.json' in files and 'dino' in root.lower():\n            print(f\"   ✅ 找到: {root}\")\n            return root\n    raise FileNotFoundError(\"❌ 找不到DINOv2模型\")\n\ndef find_img_dir(base_path='/kaggle/input'):\n    for root, dirs, files in os.walk(base_path):\n        if 'train' in dirs and 'test' in dirs:\n            train_dir = os.path.join(root, 'train')\n            test_dir = os.path.join(root, 'test')\n            if os.path.exists(train_dir) and os.path.exists(test_dir):\n                train_imgs = [f for f in os.listdir(train_dir) if f.endswith('.jpg')]\n                test_imgs = [f for f in os.listdir(test_dir) if f.endswith('.jpg')]\n                if len(train_imgs) > 0 and len(test_imgs) > 0:\n                    print(f\"   ✅ 訓練圖像: {train_dir} ({len(train_imgs)} 張)\")\n                    print(f\"   ✅ 測試圖像: {test_dir} ({len(test_imgs)} 張)\")\n                    return train_dir, test_dir\n    return None, None\n\nCONFIG = {\n    'TRAIN_CSV': '/kaggle/input/train-teacher-date2/train_teacher.csv',\n    'TEST_CSV': None,\n    'DINO_PATH': None,\n    'TRAIN_IMG_DIR': None,\n    'TEST_IMG_DIR': None,\n    'SAVE_DIR': './teacher_student_models',\n    'N_EPOCHS_TEACHER': 12,\n    'N_EPOCHS_STUDENT': 35,\n    'BATCH_SIZE': 4,  # 減小批次\n    'GRADIENT_ACCUMULATION': 2,  # 梯度累積\n    'ALPHA': 0.3,\n    'LR_TEACHER': 1e-4,\n    'LR_STUDENT': 5e-5,\n    'DROPOUT': 0.3,\n    'USE_MIXED_PRECISION': True,\n    'DEVICE': 'cuda' if torch.cuda.is_available() else 'cpu'\n}\n\ntry:\n    CONFIG['DINO_PATH'] = find_dino_model()\n    CONFIG['TRAIN_IMG_DIR'], CONFIG['TEST_IMG_DIR'] = find_img_dir()\n    for root, dirs, files in os.walk('/kaggle/input'):\n        if 'test.csv' in files:\n            CONFIG['TEST_CSV'] = os.path.join(root, 'test.csv')\n            print(f\"   ✅ 測試CSV: {CONFIG['TEST_CSV']}\")\n            break\nexcept Exception as e:\n    print(f\"❌ 路徑搜尋失敗: {e}\")\n    exit(1)\n\nprint(f\"\\n📂 記憶體優化配置:\")\nprint(f\"   批次大小: {CONFIG['BATCH_SIZE']} × 累積{CONFIG['GRADIENT_ACCUMULATION']} = 有效批次8\")\nprint(f\"   混合精度: {CONFIG['USE_MIXED_PRECISION']}\")\nprint(f\"   設備: {CONFIG['DEVICE']}\")\n\ndevice = torch.device(CONFIG['DEVICE'])\n\n# ==========================================\n# 2. Tabular特徵處理器\n# ==========================================\n\nclass TabularFeatureProcessor:\n    def __init__(self):\n        self.state_encoder = LabelEncoder()\n        self.species_encoder = LabelEncoder()\n        self.scaler = StandardScaler()\n        self.fitted = False\n        \n    def _parse_date(self, date_str):\n        try:\n            date = pd.to_datetime(date_str)\n            month = date.month\n            if month in [9, 10, 11]:\n                season = 0\n            elif month in [12, 1, 2]:\n                season = 1\n            elif month in [3, 4, 5]:\n                season = 2\n            else:\n                season = 3\n            return month / 12.0, season / 3.0\n        except:\n            return 0.5, 0.5\n    \n    def fit(self, df):\n        print(\"\\n📊 訓練Tabular特徵編碼器...\")\n        df = df.copy()\n        df['State'] = df['State'].fillna('Unknown')\n        df['Species'] = df['Species'].fillna('Unknown')\n        df['Pre_GSHH_NDVI'] = df['Pre_GSHH_NDVI'].fillna(df['Pre_GSHH_NDVI'].median())\n        df['Height_Ave_cm'] = df['Height_Ave_cm'].fillna(df['Height_Ave_cm'].median())\n        \n        self.state_encoder.fit(df['State'])\n        self.species_encoder.fit(df['Species'])\n        \n        numeric_features = []\n        for _, row in df.iterrows():\n            month, season = self._parse_date(row['Sampling_Date'])\n            numeric_features.append([\n                row['Pre_GSHH_NDVI'],\n                np.log1p(row['Height_Ave_cm']),\n                month,\n                season\n            ])\n        \n        self.scaler.fit(numeric_features)\n        self.fitted = True\n        \n        print(f\"   ✅ State類別: {len(self.state_encoder.classes_)} 個\")\n        print(f\"   ✅ Species類別: {len(self.species_encoder.classes_)} 個\")\n        return self\n    \n    def transform(self, df):\n        if not self.fitted:\n            raise ValueError(\"請先執行 fit()\")\n        \n        df = df.copy()\n        df['State'] = df['State'].fillna('Unknown')\n        df['Species'] = df['Species'].fillna('Unknown')\n        df['Pre_GSHH_NDVI'] = df['Pre_GSHH_NDVI'].fillna(0.5)\n        df['Height_Ave_cm'] = df['Height_Ave_cm'].fillna(10.0)\n        \n        continuous_features = []\n        state_ids = []\n        species_ids = []\n        \n        for _, row in df.iterrows():\n            month, season = self._parse_date(row['Sampling_Date'])\n            continuous_features.append([\n                row['Pre_GSHH_NDVI'],\n                np.log1p(row['Height_Ave_cm']),\n                month,\n                season\n            ])\n            state_ids.append(self.state_encoder.transform([row['State']])[0])\n            species_ids.append(self.species_encoder.transform([row['Species']])[0])\n        \n        continuous_features = self.scaler.transform(continuous_features)\n        \n        return (\n            continuous_features.astype(np.float32),\n            np.array(state_ids, dtype=np.int64),\n            np.array(species_ids, dtype=np.int64)\n        )\n\n# ==========================================\n# 3. Teacher Model\n# ==========================================\n\nclass TeacherModel(nn.Module):\n    def __init__(self, dino_path, n_states, n_species, hidden_dim=512, dropout=0.3):\n        super().__init__()\n        \n        self.dino = AutoModel.from_pretrained(dino_path, local_files_only=True)\n        self.dino_dim = self.dino.config.hidden_size\n        \n        for param in self.dino.parameters():\n            param.requires_grad = False\n        \n        self.continuous_encoder = nn.Sequential(\n            nn.Linear(4, 64),\n            nn.BatchNorm1d(64),\n            nn.ReLU(),\n            nn.Dropout(dropout * 0.5),\n            nn.Linear(64, 128),\n            nn.ReLU()\n        )\n        \n        self.state_embedding = nn.Embedding(n_states, 16)\n        self.species_embedding = nn.Embedding(n_species, 32)\n        \n        self.film_generator = nn.Sequential(\n            nn.Linear(176, 256),\n            nn.ReLU(),\n            nn.Dropout(dropout * 0.5),\n            nn.Linear(256, self.dino_dim * 2)\n        )\n        \n        self.predictor = nn.Sequential(\n            nn.Linear(self.dino_dim + 176, hidden_dim),\n            nn.BatchNorm1d(hidden_dim),\n            nn.ReLU(),\n            nn.Dropout(dropout),\n            nn.Linear(hidden_dim, 256),\n            nn.BatchNorm1d(256),\n            nn.ReLU(),\n            nn.Dropout(dropout * 0.5),\n            nn.Linear(256, 128),\n            nn.ReLU(),\n            nn.Linear(128, 5)\n        )\n    \n    def forward(self, pixel_values, continuous_feat, state_ids, species_ids):\n        with torch.no_grad():\n            img_features = self.dino(pixel_values=pixel_values).last_hidden_state[:, 0, :]\n        \n        cont_feat = self.continuous_encoder(continuous_feat)\n        state_feat = self.state_embedding(state_ids)\n        species_feat = self.species_embedding(species_ids)\n        \n        tabular_feat = torch.cat([cont_feat, state_feat, species_feat], dim=1)\n        \n        film_params = self.film_generator(tabular_feat)\n        gamma, beta = torch.chunk(film_params, 2, dim=1)\n        img_modulated = gamma * img_features + beta\n        \n        combined = torch.cat([img_modulated, tabular_feat], dim=1)\n        output = self.predictor(combined)\n        \n        return F.softplus(output)\n\n# ==========================================\n# 4. Student Model (輕量化)\n# ==========================================\n\nclass StudentModel(nn.Module):\n    def __init__(self, dino_path, hidden_dim=384, dropout=0.3):\n        super().__init__()\n        \n        self.dino = AutoModel.from_pretrained(dino_path, local_files_only=True)\n        self.dino_dim = self.dino.config.hidden_size\n        \n        # 凍結前8層節省記憶體\n        for i, layer in enumerate(self.dino.encoder.layer):\n            if i < 8:\n                for param in layer.parameters():\n                    param.requires_grad = False\n        \n        self.implicit_tabular = nn.Sequential(\n            nn.Linear(self.dino_dim, 192),\n            nn.BatchNorm1d(192),\n            nn.ReLU(),\n            nn.Dropout(dropout * 0.5),\n            nn.Linear(192, 96),\n            nn.ReLU()\n        )\n        \n        self.predictor = nn.Sequential(\n            nn.Linear(self.dino_dim + 96, hidden_dim),\n            nn.BatchNorm1d(hidden_dim),\n            nn.ReLU(),\n            nn.Dropout(dropout),\n            nn.Linear(hidden_dim, 192),\n            nn.BatchNorm1d(192),\n            nn.ReLU(),\n            nn.Dropout(dropout * 0.5),\n            nn.Linear(192, 96),\n            nn.ReLU(),\n            nn.Linear(96, 5)\n        )\n    \n    def forward(self, pixel_values):\n        img_features = self.dino(pixel_values=pixel_values).last_hidden_state[:, 0, :]\n        implicit_tab = self.implicit_tabular(img_features)\n        combined = torch.cat([img_features, implicit_tab], dim=1)\n        output = self.predictor(combined)\n        return F.softplus(output)\n\n# ==========================================\n# 5. Dataset\n# ==========================================\n\nclass BiomassDataset(Dataset):\n    def __init__(self, df, tab_processor, image_processor, img_dir, target_cols):\n        self.df = df.reset_index(drop=True)\n        self.image_processor = image_processor\n        self.img_dir = img_dir\n        self.target_cols = target_cols\n        \n        self.continuous, self.state_ids, self.species_ids = tab_processor.transform(df)\n        self.targets = df[target_cols].values\n    \n    def __len__(self):\n        return len(self.df)\n    \n    def __getitem__(self, idx):\n        img_path = self.df.iloc[idx]['image_path']\n        \n        if not os.path.exists(img_path):\n            img_filename = os.path.basename(img_path)\n            img_path = os.path.join(self.img_dir, img_filename)\n        \n        try:\n            img = Image.open(img_path).convert('RGB')\n            pixel_values = self.image_processor(images=img, return_tensors='pt')['pixel_values'].squeeze(0)\n        except Exception as e:\n            pixel_values = torch.zeros(3, 518, 518)\n        \n        return {\n            'pixel_values': pixel_values,\n            'continuous': torch.FloatTensor(self.continuous[idx]),\n            'state_id': torch.LongTensor([self.state_ids[idx]]).squeeze(),\n            'species_id': torch.LongTensor([self.species_ids[idx]]).squeeze(),\n            'target': torch.FloatTensor(self.targets[idx])\n        }\n\n# ==========================================\n# 6. Loss\n# ==========================================\n\ndef distillation_loss(student_pred, teacher_pred, y_true, alpha=0.3, temperature=1.0):\n    hard_loss = F.mse_loss(student_pred, y_true)\n    soft_student = student_pred / temperature\n    soft_teacher = teacher_pred / temperature\n    soft_loss = F.mse_loss(soft_student, soft_teacher) * (temperature ** 2)\n    total_loss = (1 - alpha) * hard_loss + alpha * soft_loss\n    return total_loss, hard_loss, soft_loss\n\n# ==========================================\n# 7. 訓練\n# ==========================================\n\ndef train_models(config):\n    os.makedirs(config['SAVE_DIR'], exist_ok=True)\n    target_cols = ['Dry_Clover_g', 'Dry_Dead_g', 'Dry_Green_g', 'Dry_Total_g', 'GDM_g']\n    \n    print(\"\\n📂 載入訓練數據...\")\n    train_df = pd.read_csv(config['TRAIN_CSV'])\n    print(f\"✅ {len(train_df)} 個訓練樣本\")\n    \n    tab_processor = TabularFeatureProcessor()\n    tab_processor.fit(train_df)\n    \n    n_states = len(tab_processor.state_encoder.classes_)\n    n_species = len(tab_processor.species_encoder.classes_)\n    \n    image_processor = AutoImageProcessor.from_pretrained(config['DINO_PATH'], local_files_only=True)\n    \n    train_dataset = BiomassDataset(\n        train_df, tab_processor, image_processor,\n        config['TRAIN_IMG_DIR'], target_cols\n    )\n    \n    # 🔥 關鍵修復：加上 drop_last=True\n    train_loader = DataLoader(\n        train_dataset,\n        batch_size=config['BATCH_SIZE'],\n        shuffle=True,\n        num_workers=2,\n        pin_memory=True,\n        drop_last=True  # 🔥 丟棄不完整批次,避免BatchNorm錯誤\n    )\n    \n    # ==========================================\n    # 階段A: 訓練Teacher\n    # ==========================================\n    print(\"\\n\" + \"=\" * 80)\n    print(\"📚 階段A: 訓練Teacher\")\n    print(\"=\" * 80)\n    \n    teacher = TeacherModel(config['DINO_PATH'], n_states, n_species, dropout=config['DROPOUT']).to(device)\n    teacher_optimizer = torch.optim.AdamW(teacher.parameters(), lr=config['LR_TEACHER'], weight_decay=0.01)\n    teacher_scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(teacher_optimizer, config['N_EPOCHS_TEACHER'])\n    \n    scaler = torch.cuda.amp.GradScaler() if config['USE_MIXED_PRECISION'] else None\n    \n    teacher.train()\n    best_teacher_loss = float('inf')\n    \n    for epoch in range(config['N_EPOCHS_TEACHER']):\n        epoch_loss = 0.0\n        pbar = tqdm(train_loader, desc=f\"Epoch {epoch+1}/{config['N_EPOCHS_TEACHER']}\")\n        \n        for batch in pbar:\n            pixel_values = batch['pixel_values'].to(device)\n            continuous = batch['continuous'].to(device)\n            state_id = batch['state_id'].to(device)\n            species_id = batch['species_id'].to(device)\n            target = batch['target'].to(device)\n            \n            if config['USE_MIXED_PRECISION']:\n                with torch.cuda.amp.autocast():\n                    teacher_pred = teacher(pixel_values, continuous, state_id, species_id)\n                    loss = F.mse_loss(teacher_pred, target)\n                scaler.scale(loss).backward()\n                scaler.unscale_(teacher_optimizer)\n                torch.nn.utils.clip_grad_norm_(teacher.parameters(), 1.0)\n                scaler.step(teacher_optimizer)\n                scaler.update()\n                teacher_optimizer.zero_grad()\n            else:\n                teacher_pred = teacher(pixel_values, continuous, state_id, species_id)\n                loss = F.mse_loss(teacher_pred, target)\n                loss.backward()\n                torch.nn.utils.clip_grad_norm_(teacher.parameters(), 1.0)\n                teacher_optimizer.step()\n                teacher_optimizer.zero_grad()\n            \n            epoch_loss += loss.item()\n            pbar.set_postfix({'loss': f'{loss.item():.4f}'})\n        \n        teacher_scheduler.step()\n        avg_loss = epoch_loss / len(train_loader)\n        print(f\"   Loss: {avg_loss:.4f}\")\n        \n        if avg_loss < best_teacher_loss:\n            best_teacher_loss = avg_loss\n            torch.save(teacher.state_dict(), os.path.join(config['SAVE_DIR'], 'teacher.pth'))\n            print(f\"   💾 儲存Teacher\")\n    \n    print(f\"✅ Teacher完成! 最佳Loss = {best_teacher_loss:.4f}\")\n    \n    # 🔥 關鍵:釋放Teacher的GPU記憶體\n    print(\"\\n🧹 釋放Teacher GPU記憶體...\")\n    teacher.eval()\n    for param in teacher.parameters():\n        param.requires_grad = False\n    teacher = teacher.cpu()\n    torch.cuda.empty_cache()\n    gc.collect()\n    print(f\"   ✅ 當前GPU使用: {torch.cuda.memory_allocated(device) / 1024**3:.2f} GB\")\n    \n    # ==========================================\n    # 階段B: Distillation訓練Student\n    # ==========================================\n    print(\"\\n\" + \"=\" * 80)\n    print(\"🎓 階段B: Knowledge Distillation\")\n    print(\"=\" * 80)\n    \n    student = StudentModel(config['DINO_PATH'], dropout=config['DROPOUT']).to(device)\n    student_optimizer = torch.optim.AdamW(student.parameters(), lr=config['LR_STUDENT'], weight_decay=0.01)\n    student_scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(student_optimizer, config['N_EPOCHS_STUDENT'])\n    \n    student.train()\n    best_student_loss = float('inf')\n    \n    for epoch in range(config['N_EPOCHS_STUDENT']):\n        epoch_total = 0.0\n        epoch_hard = 0.0\n        epoch_soft = 0.0\n        \n        pbar = tqdm(train_loader, desc=f\"Epoch {epoch+1}/{config['N_EPOCHS_STUDENT']}\")\n        \n        for step, batch in enumerate(pbar):\n            pixel_values = batch['pixel_values'].to(device)\n            continuous = batch['continuous']\n            state_id = batch['state_id']\n            species_id = batch['species_id']\n            target = batch['target'].to(device)\n            \n            # Teacher在CPU推理\n            with torch.no_grad():\n                teacher_pred = teacher(\n                    batch['pixel_values'],\n                    continuous,\n                    state_id,\n                    species_id\n                ).to(device)\n            \n            if config['USE_MIXED_PRECISION']:\n                with torch.cuda.amp.autocast():\n                    student_pred = student(pixel_values)\n                    total_loss, hard_loss, soft_loss = distillation_loss(\n                        student_pred, teacher_pred, target, alpha=config['ALPHA']\n                    )\n                    total_loss = total_loss / config['GRADIENT_ACCUMULATION']\n                \n                scaler.scale(total_loss).backward()\n                \n                if (step + 1) % config['GRADIENT_ACCUMULATION'] == 0:\n                    scaler.unscale_(student_optimizer)\n                    torch.nn.utils.clip_grad_norm_(student.parameters(), 1.0)\n                    scaler.step(student_optimizer)\n                    scaler.update()\n                    student_optimizer.zero_grad()\n            else:\n                student_pred = student(pixel_values)\n                total_loss, hard_loss, soft_loss = distillation_loss(\n                    student_pred, teacher_pred, target, alpha=config['ALPHA']\n                )\n                total_loss = total_loss / config['GRADIENT_ACCUMULATION']\n                total_loss.backward()\n                \n                if (step + 1) % config['GRADIENT_ACCUMULATION'] == 0:\n                    torch.nn.utils.clip_grad_norm_(student.parameters(), 1.0)\n                    student_optimizer.step()\n                    student_optimizer.zero_grad()\n            \n            epoch_total += total_loss.item() * config['GRADIENT_ACCUMULATION']\n            epoch_hard += hard_loss.item()\n            epoch_soft += soft_loss.item()\n            \n            pbar.set_postfix({\n                'total': f'{total_loss.item() * config[\"GRADIENT_ACCUMULATION\"]:.3f}',\n                'hard': f'{hard_loss.item():.3f}',\n                'soft': f'{soft_loss.item():.3f}'\n            })\n        \n        student_scheduler.step()\n        \n        avg_total = epoch_total / len(train_loader)\n        avg_hard = epoch_hard / len(train_loader)\n        avg_soft = epoch_soft / len(train_loader)\n        \n        print(f\"   Total={avg_total:.4f} | Hard={avg_hard:.4f} | Soft={avg_soft:.4f}\")\n        \n        if avg_total < best_student_loss:\n            best_student_loss = avg_total\n            torch.save({\n                'model': student.state_dict(),\n                'tab_processor': tab_processor\n            }, os.path.join(config['SAVE_DIR'], 'student.pth'))\n            print(f\"   💾 儲存Student\")\n        \n        if (epoch + 1) % 5 == 0:\n            torch.cuda.empty_cache()\n            gc.collect()\n    \n    print(f\"✅ Student完成! 最佳Loss = {best_student_loss:.4f}\")\n    \n    return student, teacher, tab_processor\n\n# ==========================================\n# 8. 預測\n# ==========================================\n\ndef predict_and_submit(student_model, config):\n    print(\"\\n\" + \"=\" * 80)\n    print(\"🔮 測試集預測\")\n    print(\"=\" * 80)\n    \n    student_model.eval()\n    student_model = student_model.to(device)\n    image_processor = AutoImageProcessor.from_pretrained(config['DINO_PATH'], local_files_only=True)\n    \n    test_df = pd.read_csv(config['TEST_CSV'])\n    print(f\"✅ {len(test_df)} 個測試樣本\")\n    \n    test_df['image_id'] = test_df['sample_id'].apply(lambda x: x.split('__')[0])\n    unique_images = test_df['image_id'].unique()\n    \n    predictions_dict = {}\n    \n    with torch.no_grad():\n        for img_id in tqdm(unique_images, desc=\"預測\"):\n            img_filename = f\"{img_id}.jpg\" if not img_id.endswith('.jpg') else img_id\n            img_path = os.path.join(config['TEST_IMG_DIR'], img_filename)\n            \n            try:\n                img = Image.open(img_path).convert('RGB')\n                pixel_values = image_processor(images=img, return_tensors='pt')['pixel_values'].to(device)\n                \n                if config['USE_MIXED_PRECISION']:\n                    with torch.cuda.amp.autocast():\n                        pred = student_model(pixel_values)\n                else:\n                    pred = student_model(pixel_values)\n                \n                predictions_dict[img_id] = pred.cpu().numpy()[0]\n            except Exception as e:\n                predictions_dict[img_id] = np.zeros(5)\n    \n    target_cols = ['Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g', 'Dry_Total_g']\n    \n    submission_data = []\n    for _, row in test_df.iterrows():\n        sample_id = row['sample_id']\n        img_id, target_name = sample_id.split('__')\n        \n        if img_id in predictions_dict:\n            preds = predictions_dict[img_id]\n            target_idx = target_cols.index(target_name)\n            value = max(0, preds[target_idx])\n        else:\n            value = 0.0\n        \n        submission_data.append({\n            'sample_id': sample_id,\n            'target': value\n        })\n    \n    submission_df = pd.DataFrame(submission_data)\n    submission_df.to_csv('submission.csv', index=False)\n    \n    print(\"\\n✅ submission.csv 已生成\")\n    print(f\"   預測範圍: {submission_df['target'].min():.2f} - {submission_df['target'].max():.2f}\")\n\n# ==========================================\n# 9. 主程式\n# ==========================================\n\ndef main():\n    print(\"\\n🚀 開始訓練...\")\n    \n    student, teacher, tab_processor = train_models(CONFIG)\n    predict_and_submit(student, CONFIG)\n    \n    print(\"\\n\" + \"=\" * 80)\n    print(\"🎉 完成!\")\n    print(\"=\" * 80)\n    print(\"\\n記憶體優化策略:\")\n    print(\"  ✅ Teacher移至CPU釋放~7GB GPU記憶體\")\n    print(\"  ✅ 批次大小4 + 梯度累積2 = 有效批次8\")\n    print(\"  ✅ 混合精度訓練(FP16)節省30%記憶體\")\n    print(\"  ✅ Student凍結DINOv2前8層\")\n    print(\"  ✅ 輕量化Student架構(hidden_dim=384)\")\n    print(\"  ✅ drop_last=True 避免BatchNorm錯誤\")\n    print(\"=\" * 80)\n\nif __name__ == \"__main__\":\n    main()","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-12-28T10:16:26.294538Z","iopub.execute_input":"2025-12-28T10:16:26.295229Z","iopub.status.idle":"2025-12-28T10:16:53.88759Z","shell.execute_reply.started":"2025-12-28T10:16:26.295201Z","shell.execute_reply":"2025-12-28T10:16:53.886555Z"}},"outputs":[],"execution_count":null}]}