{"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":"none","dataSources":[{"sourceId":112509,"databundleVersionId":14254895,"sourceType":"competition"}],"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import pandas as pd\nimport numpy as np\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nimport warnings\nwarnings.filterwarnings(action = 'ignore')\n\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.data import DataLoader, Dataset\nfrom torchvision import transforms, datasets\n\nimport os\nimport random\nfrom PIL import Image\n\n# ====================================================\n# Seed para reproducibilidad\n# ====================================================\ndef seed_everything(seed=42):\n    \"\"\"Establece seed para reproducibilidad en todas las librerías\"\"\"\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.cuda.manual_seed_all(seed)\n    # Para reproducibilidad en operaciones determinísticas de CUDA\n    torch.backends.cudnn.deterministic = True\n    torch.backends.cudnn.benchmark = False\n\n# Establecer seed (cambiar este valor para diferentes experimentos)\nSEED = 42\nseed_everything(SEED)\nprint(f\"Seed establecido: {SEED}\")\n\ntrain = pd.read_csv(\"/kaggle/input/csiro-biomass/train.csv\")\ntest = pd.read_csv(\"/kaggle/input/csiro-biomass/test.csv\")\n\ntrain[\"sample_id\"] = train[\"sample_id\"].str.split(\"__\", expand = True)[0]\ntrain = train.pivot(index = train.columns[:-2].tolist(), columns = \"target_name\", values = \"target\")\ntrain.columns.name = None\ntrain.reset_index(inplace = True)\n\ntest[\"sample_id\"] = test[\"sample_id\"].str.split(\"__\", expand = True)[0]\ntest = test.drop_duplicates('sample_id').reset_index(drop = True)\n# test = test.pivot(index = test.columns[:-1].tolist(), columns = \"target_name\", values = \"target\")\n\nBASE_PATH = \"/kaggle/input/csiro-biomass/\"\nBATCH_SIZE = 8\nDEVICE = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\n# ====================================================\n# Modo de Submission\n# ====================================================\n# Si SUBMISSION_MODE = True, solo hace predicciones (no entrena)\n# Si SUBMISSION_MODE = False, entrena el modelo normalmente\nSUBMISSION_MODE = False  # Cambiar a True para solo hacer predicciones\n\ntransform = transforms.Compose([\n    transforms.Resize((384, 384)),\n    transforms.ToTensor()\n])\n\nclass Dataset_v1(Dataset):\n    def __init__(self, df, transform = None, mode = \"train\"):\n        self.df = df\n        self.transform = transform\n        self.mode = mode\n\n    def __len__(self):\n        return len(self.df)\n\n    def __getitem__(self, idx):\n        \n        RELATIVE_PATH = self.df.image_path.iloc[idx]\n        img_path = os.path.join(BASE_PATH, RELATIVE_PATH)\n\n        with Image.open(img_path) as img:\n                image = img.convert(\"RGB\")\n\n        image = self.transform(image)\n\n        if self.mode == 'test':\n            # 테스트 모드일 때는 (이미지, 이미지_ID) 반환\n            img_id = self.df.sample_id.iloc[idx]\n            return image, img_id\n\n        else:\n            labels = self.df.iloc[idx][['Dry_Clover_g', 'Dry_Dead_g', 'Dry_Green_g', 'Dry_Total_g', 'GDM_g']]\n            labels = torch.tensor(labels, dtype = torch.float32)\n\n            return image, labels\n\npin = (DEVICE.type == \"cuda\")\n\n# Split para validación (10 imágenes)\nval_size = 10\ntrain_df = train.iloc[:-val_size].reset_index(drop=True)\nval_df = train.iloc[-val_size:].reset_index(drop=True)\n\nprint(f\"Train size: {len(train_df)}, Val size: {len(val_df)}\")\n\n# Generator con seed para reproducibilidad en DataLoader shuffle\ngenerator = torch.Generator()\ngenerator.manual_seed(SEED)\n\ntrain_data = Dataset_v1(df = train_df, transform = transform, mode = \"train\")\ntrain_loader = DataLoader(dataset = train_data, batch_size = BATCH_SIZE, shuffle = True, drop_last = False, num_workers = 0, pin_memory=pin, generator=generator)\n\nval_data = Dataset_v1(df = val_df, transform = transform, mode = \"train\")\nval_loader = DataLoader(dataset = val_data, batch_size = BATCH_SIZE, shuffle = False, drop_last = False, num_workers = 0, pin_memory=pin)\n\ntest_data = Dataset_v1(df = test, transform = transform, mode = 'test')\ntest_loader = DataLoader(dataset = test_data, batch_size = BATCH_SIZE, shuffle = False, drop_last = False, num_workers = 0, pin_memory=pin)\nclass CNN(nn.Module):\n    def __init__(self):\n        super(CNN, self).__init__()\n        self.conv1 = nn.Sequential(\n            nn.Conv2d(in_channels = 3, out_channels = 64, kernel_size = 3, padding = 1),\n            nn.BatchNorm2d(64),\n            nn.ReLU())\n        self.conv2 = nn.Sequential(\n            nn.Conv2d(in_channels = 64, out_channels = 64, kernel_size = 3, padding = 1),\n            nn.BatchNorm2d(64),\n            nn.ReLU(),\n            nn.AvgPool2d(2,2))\n\n        self.conv3 = nn.Sequential(\n            nn.Conv2d(in_channels = 64, out_channels = 128, kernel_size = 3, padding = 1),\n            nn.BatchNorm2d(128),\n            nn.ReLU())\n        self.conv4 = nn.Sequential(\n            nn.Conv2d(in_channels = 128, out_channels = 128, kernel_size = 3, padding = 1),\n            nn.BatchNorm2d(128),\n            nn.ReLU(),\n            nn.AvgPool2d(2,2))\n\n        self.conv5 = nn.Sequential(\n            nn.Conv2d(in_channels = 128, out_channels = 256, kernel_size = 3, padding = 1),\n            nn.BatchNorm2d(256),\n            nn.ReLU())\n        self.conv6 = nn.Sequential(\n            nn.Conv2d(in_channels = 256, out_channels = 256, kernel_size = 3, padding = 1),\n            nn.BatchNorm2d(256),\n            nn.ReLU(),\n            nn.AvgPool2d(2,2))\n\n        self.conv7 = nn.Sequential(\n            nn.Conv2d(in_channels = 256, out_channels = 512, kernel_size = 3, padding = 1),\n            nn.BatchNorm2d(512),\n            nn.ReLU())\n        self.conv8 = nn.Sequential(\n            nn.Conv2d(in_channels = 512, out_channels = 512, kernel_size = 3, padding = 1),\n            nn.BatchNorm2d(512),\n            nn.ReLU(),\n            nn.AvgPool2d(2,2))\n\n        self.gap = nn.AdaptiveAvgPool2d(1)\n\n        self.fc1 = nn.Sequential(\n            nn.BatchNorm1d(512),\n            nn.Dropout(0.2),\n            nn.Linear(512, 5))\n\n    def forward(self, x):\n        x = self.conv1(x)\n        x = self.conv2(x)\n        x = self.conv3(x)\n        x = self.conv4(x)\n        x = self.conv5(x)\n        x = self.conv6(x)\n        x = self.conv7(x)\n        x = self.conv8(x)\n        x = self.gap(x)\n        x = x.view(x.size(0), -1)\n        x = self.fc1(x)\n\n        return x\nmodel = CNN().to(DEVICE)\noptimizer = torch.optim.Adam(model.parameters(), lr = 1e-3, weight_decay = 1e-4)  # Reducido de 1e-3 para mayor estabilidad\nscheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(\n    optimizer, mode='max', factor=0.5, patience=5, min_lr=1e-7  # mode='max' porque optimizamos R² (mayor es mejor)\n)\n# Usar MSELoss (igual que original.py que usa reg:squarederror)\nloss_function = nn.MSELoss()\n\ndef calculate_r2(y_true, y_pred, verbose=False):\n    \"\"\"Calcula R² ponderado con transformación log - 5 targets\"\"\"\n    # Pesos por target: [Dry_Green_g, Dry_Dead_g, Dry_Clover_g, GDM_g, Dry_Total_g]\n    target_weights = np.array([0.1, 0.1, 0.1, 0.2, 0.5])\n    target_cols = ['Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g', 'Dry_Total_g']\n    \n    if y_true.ndim == 1: y_true = y_true.reshape(-1, 1)\n    if y_pred.ndim == 1: y_pred = y_pred.reshape(-1, 1)\n    \n    # Aplicar transformación log(1 + x) para evitar problemas con ceros\n    y_true_log = np.log1p(np.maximum(0, y_true))\n    y_pred_log = np.log1p(np.maximum(0, y_pred))\n    \n    # R² por target individual\n    if verbose:\n        print(\"\\n=== R² por Target (log transform) ===\")\n        for i, col in enumerate(target_cols):\n            ss_res_i = np.sum((y_true_log[:, i] - y_pred_log[:, i])**2)\n            ss_tot_i = np.sum((y_true_log[:, i] - np.mean(y_true_log[:, i]))**2)\n            r2_i = 1 - (ss_res_i / (ss_tot_i + 1e-8))\n            print(f\"  {col}: R²={r2_i:.6f} (weight={target_weights[i]})\")\n    \n    # R² ponderado con transformación log\n    ss_res = np.sum(target_weights * np.sum((y_true_log - y_pred_log)**2, axis=0))\n    global_mean = np.average(np.mean(y_true_log, axis=0), weights=target_weights)\n    ss_tot = np.sum(target_weights * np.sum((y_true_log - global_mean)**2, axis=0))\n    return 1 - (ss_res / (ss_tot + 1e-8))\n\ndef Train(model, train_loader, optimizer, log_interval = 10):\n    model.train()\n    total_loss = 0.0\n    for batch_idx, (image, label) in enumerate(train_loader):\n        optimizer.zero_grad()\n        image = image.to(DEVICE)\n        label = label.to(DEVICE)\n        output = model(image)\n        loss = loss_function(output, label)\n        loss.backward()\n        optimizer.step()\n        total_loss += loss.item()\n\n        if batch_idx % log_interval == 0:\n            print(f\"EPOCH: {EPOCH} [{batch_idx * len(image)} / {len(train_loader.dataset)}] {100. * batch_idx / len(train_loader):.4f}%\\tTrain Loss:{loss.item():.6f}\")\n    \n    avg_loss = total_loss / len(train_loader)\n    return avg_loss\n\ndef Validate(model, val_loader, val_df):\n    \"\"\"Valida el modelo y calcula loss y R² con 5 targets (igual que original.py)\"\"\"\n    model.eval()\n    total_loss = 0.0\n    all_preds_5 = []\n    all_labels_5 = []\n    \n    with torch.no_grad():\n        for image, label in val_loader:\n            image = image.to(DEVICE)\n            label = label.to(DEVICE)\n            output = model(image)\n            loss = loss_function(output, label)\n            total_loss += loss.item()\n            \n            all_preds_5.append(output.cpu().numpy())\n            all_labels_5.append(label.cpu().numpy())\n    \n    avg_loss = total_loss / len(val_loader)\n    \n    # Convertir a arrays numpy\n    # Modelo output: [Dry_Clover_g, Dry_Dead_g, Dry_Green_g, Dry_Total_g, GDM_g]\n    all_preds_5 = np.concatenate(all_preds_5, axis=0)  # [N, 5]\n    all_labels_5 = np.concatenate(all_labels_5, axis=0)  # [N, 5]\n    \n    # Clamp a valores no negativos\n    all_preds_5 = np.maximum(0, all_preds_5)\n    \n    # Reordenar para calculate_r2: [Dry_Green_g, Dry_Dead_g, Dry_Clover_g, GDM_g, Dry_Total_g]\n    # Modelo: [Clover, Dead, Green, Total, GDM] -> R²: [Green, Dead, Clover, GDM, Total]\n    all_preds_5_reordered = np.column_stack([\n        all_preds_5[:, 2],  # Green\n        all_preds_5[:, 1],  # Dead\n        all_preds_5[:, 0],  # Clover\n        all_preds_5[:, 4],  # GDM\n        all_preds_5[:, 3]   # Total\n    ])\n    \n    # Obtener los 5 targets reales del DataFrame (mismo orden que calculate_r2 espera)\n    all_labels_5_reordered = val_df[['Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g', 'Dry_Total_g']].values\n    \n    # Calcular R² con los 5 targets\n    r2 = calculate_r2(all_labels_5_reordered, all_preds_5_reordered)\n    \n    return avg_loss, r2\n\n# def Test(model = model, test_loader = test_loader):\n#     model.eval()\n#     predictions = []\n#     sample_ids = []\n#     with torch.no_grad():\n#         for image, sample_id in test_loader:\n#             image = image.to(DEVICE)\n#             # sample_id = sample_id.to(DEVICE)\n#             output = model(image)\n#             pred_3 = torch.clamp(output, min = 0).cpu().numpy()\n#             clover, dead, green = pred_3[:, 0], pred_3[:, 1], pred_3[:, 2]\n#             total = clover + dead + green\n#             gdm = clover + green\n#             pred_5 = np.column_stack([clover, dead, green, total, gdm])\n#             predictions.extend(pred_5.flatten())\n            \n#     return predictions\ndef Test(model, test_loader):\n    \"\"\"Genera predicciones y retorna DataFrame con predicciones (igual que original.py)\"\"\"\n    model.eval()\n    all_preds_5 = []\n    all_img_ids = []\n    \n    with torch.no_grad():\n        for image, img_ids in test_loader:\n            image = image.to(DEVICE, non_blocking = True)\n            output = model(image)\n            preds = torch.clamp(output, min=0)\n            preds = torch.nan_to_num(preds, nan=0.0, posinf=0.0, neginf=0.0).cpu().numpy()\n            \n            all_preds_5.append(preds)\n            all_img_ids.extend(img_ids)\n    \n    # Convertir a arrays numpy\n    # Modelo output: [Dry_Clover_g, Dry_Dead_g, Dry_Green_g, Dry_Total_g, GDM_g]\n    all_preds_5 = np.concatenate(all_preds_5, axis=0)  # [N, 5]\n    \n    # Crear DataFrame con predicciones directamente desde las 5 salidas del modelo\n    pred_df = pd.DataFrame({\n        'sample_id': all_img_ids,\n        'Dry_Clover_g': all_preds_5[:, 0],\n        'Dry_Dead_g': all_preds_5[:, 1],\n        'Dry_Green_g': all_preds_5[:, 2],\n        'Dry_Total_g': all_preds_5[:, 3],\n        'GDM_g': all_preds_5[:, 4]\n    })\n    \n    return pred_df\nEPOCHS = 50  # Aumentar épocas para dar oportunidad al early stopping\npatience = 10  # Aumentar patience para no parar tan temprano\nbest_val_r2 = -float('inf')\npatience_counter = 0\n\n# Paths separados: carga (puede ser de input, solo lectura) y guardado (debe ser escribible)\nPRETRAINED_MODEL_PATH = \"/kaggle/input/cnn-small/best_cnn.pth\"  # Path para cargar backbone pre-entrenado\nMODEL_PATH = \"/kaggle/working/best_cnn.pth\"  # Path para guardar modelo (debe ser escribible)\n\n# ====================================================\n# Modo Submission: Solo predicciones\n# ====================================================\nif SUBMISSION_MODE:\n    print(\"\\n\" + \"=\"*60)\n    print(\"MODO SUBMISSION: Solo predicciones (sin entrenamiento)\")\n    print(\"=\"*60)\n    \n    # Cargar modelo para predicciones\n    model_loaded = False\n    if os.path.exists(PRETRAINED_MODEL_PATH):\n        print(f\"\\nCargando modelo desde {PRETRAINED_MODEL_PATH}...\")\n        checkpoint = torch.load(PRETRAINED_MODEL_PATH, map_location=DEVICE)\n        model.load_state_dict(checkpoint, strict=True)\n        print(\"Modelo cargado exitosamente para submission.\")\n        model_loaded = True\n    elif os.path.exists(MODEL_PATH):\n        print(f\"\\nCargando modelo desde {MODEL_PATH}...\")\n        checkpoint = torch.load(MODEL_PATH, map_location=DEVICE)\n        model.load_state_dict(checkpoint, strict=True)\n        print(\"Modelo cargado exitosamente para submission.\")\n        model_loaded = True\n    else:\n        raise FileNotFoundError(\n            f\"No se encontró modelo en {PRETRAINED_MODEL_PATH} ni en {MODEL_PATH}. \"\n            \"Necesitas un modelo entrenado para modo submission.\"\n        )\n    \n    # Saltar entrenamiento y ir directo a predicciones\n    print(\"\\nSaltando entrenamiento (modo submission)...\")\n    \nelse:\n    # ====================================================\n    # Modo Normal: Entrenamiento\n    # ====================================================\n    # Cargar backbone pre-entrenado si existe (solo lectura, desde input)\n    if os.path.exists(PRETRAINED_MODEL_PATH):\n        print(f\"\\nCargando backbone pre-entrenado desde {PRETRAINED_MODEL_PATH}...\")\n        checkpoint = torch.load(PRETRAINED_MODEL_PATH, map_location=DEVICE)\n        \n        # Cargar solo las capas compatibles (backbone)\n        # strict=False permite que fc1 no esté en el checkpoint (se inicializa desde cero)\n        model.load_state_dict(checkpoint, strict=True)\n        print(\"Backbone pre-entrenado cargado exitosamente.\")\n        print(\"Nota: La capa fc1 se inicializa desde cero (no estaba en el checkpoint).\")\n        \n        # Opcional: Validar para ver cómo está\n        val_loss, val_r2 = Validate(model, val_loader, val_df)\n        print(f\"Estado inicial Validacion - Loss: {val_loss:.6f}, R²: {val_r2:.6f}\")\n        best_val_r2 = val_r2  # Actualizar mejor score\n    elif os.path.exists(MODEL_PATH):\n        # Si no hay backbone pre-entrenado, intentar cargar modelo completo guardado anteriormente\n        print(f\"\\nCargando modelo existente desde {MODEL_PATH}...\")\n        checkpoint = torch.load(MODEL_PATH, map_location=DEVICE)\n        model.load_state_dict(checkpoint, strict=True)\n        print(\"Modelo cargado exitosamente.\")\n        \n        # Opcional: Validar para ver cómo está\n        val_loss, val_r2 = Validate(model, val_loader, val_df)\n        print(f\"Estado inicial Validacion - Loss: {val_loss:.6f}, R²: {val_r2:.6f}\")\n        best_val_r2 = val_r2  # Actualizar mejor score\n    else:\n        print(\"\\nNo se encontró modelo guardado, iniciando entrenamiento desde cero.\")\n\n    # ====================================================\n    # Entrenamiento\n    # ====================================================\n    for EPOCH in range(1, EPOCHS + 1):\n        # Entrenamiento\n        train_loss = Train(model, train_loader, optimizer, log_interval = 5)\n        \n        # Validación\n        val_loss, val_r2 = Validate(model, val_loader, val_df)\n        print(f\"EPOCH: {EPOCH} - Train Loss: {train_loss:.6f}, Val Loss: {val_loss:.6f}, Val R²: {val_r2:.6f}, LR: {optimizer.param_groups[0]['lr']:.2e}\")\n        \n        # Scheduler: reduce LR cuando val_r2 no mejora (consistente con early stopping)\n        scheduler.step(val_r2)  # Usar val_r2 en lugar de val_loss para consistencia\n        \n        # Early stopping basado en R² (mayor es mejor)\n        if val_r2 > best_val_r2:\n            best_val_r2 = val_r2\n            torch.save(model.state_dict(), MODEL_PATH)\n            print(f\"  Guardado nuevo mejor modelo en {MODEL_PATH}\")\n            patience_counter = 0\n        else:\n            patience_counter += 1\n            print(f\"  Patience: {patience_counter}/{patience}\")\n            \n            if patience_counter >= patience:\n                print(f\"\\nEarly stopping en epoch {EPOCH}\")\n                print(f\"Mejor Val R²: {best_val_r2:.6f}\")\n                break\n    \n    # Cargar el mejor modelo guardado para predicciones (solo en modo normal)\n    if os.path.exists(MODEL_PATH):\n        print(f\"\\n{'='*60}\")\n        print(f\"Cargando mejor modelo desde {MODEL_PATH} para predicciones...\")\n        print(f\"Mejor Val R²: {best_val_r2:.6f}\")\n        print(f\"{'='*60}\")\n        checkpoint = torch.load(MODEL_PATH, map_location=DEVICE)\n        model.load_state_dict(checkpoint, strict=True)\n        print(\"Mejor modelo cargado exitosamente\")\n        \n        # Validar que el modelo cargado es el mejor\n        val_loss, val_r2 = Validate(model, val_loader, val_df)\n        print(f\"Validación del modelo cargado - Loss: {val_loss:.6f}, R²: {val_r2:.6f}\")\n    else:\n        print(f\"\\nAdvertencia: No se encontró modelo guardado en {MODEL_PATH}\")\n        print(\"Usando modelo actual para predicciones (puede no ser el mejor)\")\n\n# ====================================================\n# Generar predicciones\n# ====================================================\nprint(f\"\\n{'='*60}\")\nprint(f\"Generando predicciones...\")\nprint(f\"{'='*60}\")\npred_df = Test(model, test_loader)\n\n# 2. Merge predictions with test DataFrame (igual que original.py)\ndf_sub = test.merge(pred_df, on='sample_id', how='left')\n\ntarget_cols = ['Dry_Clover_g', 'Dry_Dead_g', 'Dry_Green_g', 'Dry_Total_g', 'GDM_g']\nsub_rows = []\nfor i, row in df_sub.iterrows():\n    img_id = row['sample_id']\n    for col in target_cols:\n        sub_rows.append({\n            'sample_id': f\"{img_id}__{col}\",\n            'target': row[col]\n        })\n\n# 5. Create DataFrame and save\nsubmission = pd.DataFrame(sub_rows)\nsubmission[\"target\"] = submission[\"target\"].replace([np.inf, -np.inf], 0.0).fillna(0.0).astype(float)\nsubmission.to_csv(\"submission.csv\", index=False)\nprint(f\"Saved submission.csv with {len(submission)} rows\")\nassert os.path.exists(\"submission.csv\")\nassert submission.shape[1] == 2\nassert submission['target'].isnull().sum() == 0","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}