{"metadata":{"kernelspec":{"display_name":"Python 3","language":"python","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"colab":{"provenance":[]},"kaggle":{"accelerator":"none","dataSources":[{"sourceId":112509,"databundleVersionId":14254895,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":676314,"sourceType":"modelInstanceVersion","isSourceIdPinned":true,"modelInstanceId":512758,"modelId":527399}],"dockerImageVersionId":31192,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":false}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"import numpy as np\nimport pandas as pd\nimport matplotlib.pyplot as plt\nimport seaborn as sns\nfrom PIL import Image\nimport warnings\nwarnings.filterwarnings('ignore')\n\nplt.style.use('seaborn-v0_8')\nsns.set_palette(\"husl\")\n\nimport os\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))","metadata":{"id":"MTD7jCC0nZh3","trusted":true,"execution":{"iopub.status.busy":"2025-12-08T09:20:06.688670Z","iopub.execute_input":"2025-12-08T09:20:06.694953Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train = pd.read_csv(\"/kaggle/input/csiro-biomass/train.csv\")\ntest = pd.read_csv(\"/kaggle/input/csiro-biomass/test.csv\")\nsample_submission = pd.read_csv(\"/kaggle/input/csiro-biomass/sample_submission.csv\")\ntrain.info()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"Forma de train:\", train.shape)\ntrain.head()","metadata":{"id":"NzeFQMc1n8LJ","trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Exploración y Análisis de Datos ","metadata":{}},{"cell_type":"code","source":"print(f\"Número de imágenes únicas en train: {train['image_path'].nunique()}\")\nprint(f\"Número de muestras en test: {len(test)}\")\nprint(f\"Número de imágenes únicas en test: {test['image_path'].nunique()}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(train.isnull().sum())","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"No hay datos faltantes","metadata":{}},{"cell_type":"code","source":"print(\"Análisis de columnas numericas\")\ntrain.describe()","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"Análisis de columnas categoricas\")\ntrain[[\"State\", \"Species\", \"target_name\"]].describe()\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"\\nEstados únicos:\")\nprint(train['State'].value_counts())","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"\\nEspecies únicas:\")\nprint(train['Species'].value_counts())","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"\\nTargets únicos:\")\nprint(train['target_name'].value_counts())","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"\\nFechas de muestreo:\")\nprint(f\"Rango de fechas: {train['Sampling_Date'].min()} a {train['Sampling_Date'].max()}\")\nprint(f\"Número de fechas únicas: {train['Sampling_Date'].nunique()}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"print(\"Análisis de la variable target\")\n\ntarget_stats = train.groupby('target_name')['target'].agg(['mean', 'median', 'std', 'min', 'max', 'count'])\nprint(\"\\nEstadísticas por tipo de target:\")\nprint(target_stats)\n\nfig, axes = plt.subplots(3, 2, figsize=(7, 7))\naxes = axes.flatten()\n\ntargets = train['target_name'].unique()\nfor i, target in enumerate(targets):\n    data = train[train['target_name'] == target]['target']\n    axes[i].hist(data, bins=50, edgecolor='black', alpha=0.7)\n    axes[i].set_title(target, fontsize=12, fontweight='bold')\n    axes[i].set_xlabel('Valor')\n    axes[i].set_ylabel('Frecuencia')\n    axes[i].grid(True, alpha=0.3)\n\nfig.delaxes(axes[5])\n\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Análisis de correlaciones entre variables numéricas\nnumeric_cols = ['Pre_GSHH_NDVI', 'Height_Ave_cm', 'target']\ncorrelation_matrix = train[numeric_cols].corr()\n\nplt.figure(figsize=(5, 5))\nsns.heatmap(correlation_matrix, annot=True, cmap='coolwarm', center=0, \n            square=True, linewidths=1, cbar_kws={\"shrink\": 0.8})\nplt.title('Matriz de Correlación entre Variables Numéricas', fontsize=14, fontweight='bold')\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Visualización de relaciones entre variables\nfig, axes = plt.subplots(2, 2, figsize=(15, 15))\n\n# Target por estado\ntrain.boxplot(column='target', by='State', ax=axes[0, 0], color='royalblue')\naxes[0, 0].set_title('Distribución de Target por Estado')\naxes[0, 0].set_xlabel('Estado')\naxes[0, 0].set_ylabel('Target')\naxes[0, 0].tick_params(axis='x', rotation=45)\n\n# Target por especie (top 5)\ntop_species = train['Species'].value_counts().head(5).index\ntrain_filtered = train[train['Species'].isin(top_species)]\ntrain_filtered.boxplot(column='target', by='Species', ax=axes[0, 1], color='royalblue')\naxes[0, 1].set_title('Distribución de Target por Especie (Top 5)')\naxes[0, 1].set_xlabel('Especie')\naxes[0, 1].set_ylabel('Target')\naxes[0, 1].tick_params(axis='x', rotation=45)\n\n# NDVI vs target\naxes[1, 0].scatter(train['Pre_GSHH_NDVI'], train['target'], alpha=0.5)\naxes[1, 0].set_xlabel('Pre_GSHH_NDVI')\naxes[1, 0].set_ylabel('Target')\naxes[1, 0].set_title('Relación entre NDVI y Target')\naxes[1, 0].grid(True, alpha=0.3)\n\n# altura vs target\naxes[1, 1].scatter(train['Height_Ave_cm'], train['target'], alpha=0.5)\naxes[1, 1].set_xlabel('Height_Ave_cm')\naxes[1, 1].set_ylabel('Target')\naxes[1, 1].set_title('Relación entre Altura y Target')\naxes[1, 1].grid(True, alpha=0.3)\n\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"sample_images = train['image_path'].unique()[:5]\nimage_info = []\n\nfor img_path in sample_images:\n    img = Image.open('/kaggle/input/csiro-biomass/'+img_path)\n    image_info.append({\n        'path': img_path,\n        'size': img.size,\n        'mode': img.mode,\n        'format': img.format\n    })\n\nif image_info:\n    img_df = pd.DataFrame(image_info)\n    print(\"\\nInformación de imágenes de muestra:\")\n    print(img_df)\n    \n    example_img = Image.open('/kaggle/input/csiro-biomass/' + sample_images[0])\n    plt.figure(figsize=(10, 8))\n    plt.imshow(example_img)\n    plt.title(f'Imagen de ejemplo: {os.path.basename(sample_images[0])}', fontsize=12, fontweight='bold')\n    plt.axis('off')\n    plt.tight_layout()\n    plt.show()\nelse:\n    print(\"No se encontraron imágenes en las rutas especificadas.\")\n","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"## Estrategia de Modelado\n\nPara este problema de predicción de biomasa, utilizaremos un enfoque de Deep Learning con CNN que combina:\n\n1. Extracción de características visuales: Usando un modelo preentrenado (EfficientNet) para extraer características de las imágenes de pastizales\n2. Información contextual: Incorporando el tipo de target (Dry_Green_g, Dry_Dead_g, etc.) como embedding adicional\n3. Regresión multi-target: Predicción de valores continuos para cada componente de biomasa\n\n¿Por qué este enfoque?\n- Las imágenes contienen información visual crucial sobre la cantidad de biomasa\n- Los modelos preentrenados capturan patrones visuales complejos sin necesidad de entrenar desde cero\n- El embedding del target_name permite al modelo aprender diferentes patrones para cada tipo de biomasa\n","metadata":{}},{"cell_type":"markdown","source":"# Preprocesamiento","metadata":{}},{"cell_type":"markdown","source":"### Dataset Personalizado\n\nCreamos una clase BiomassDataset que:\n- Carga imágenes desde rutas especificadas en el DataFrame\n- Aplica transformaciones de data augmentation (rotación, flip, color jitter) para entrenamiento\n- Codifica el target_name como índice numérico\n- Caché de imágenes: carga todas las imágenes en memoria evita I/O repetido y acelera significativamente el entrenamiento\n\nOptimización aplicada: El caché es especialmente útil aquí porque tenemos solo 357 imágenes únicas, pero 1785 muestras (5 targets por imagen). \n","metadata":{}},{"cell_type":"code","source":"import torch\nimport torch.nn as nn\nimport torch.optim as optim\nfrom torch.utils.data import Dataset, DataLoader\nfrom torchvision import transforms\nfrom PIL import Image\nimport timm\nfrom sklearn.model_selection import train_test_split\n\ndevice = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\nprint(f'Usando dispositivo: {device}')","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Dataset personalizado para cargar imágenes y targets\nclass BiomassDataset(Dataset):\n    def __init__(self, df, image_dir='', transform=None, is_train=True, cache_images=False):\n        \"\"\"\n        df: DataFrame con image_path, target_name, y target \n        image_dir: Directorio base donde están las imágenes\n        transform: Transformaciones a aplicar a las imágenes\n        is_train: Si es True, incluye el target\n        cache_images: Si True, cachea las imágenes en memoria \n        \"\"\"\n        self.df = df.reset_index(drop=True)\n        self.image_dir = image_dir\n        self.transform = transform\n        self.is_train = is_train\n        self.cache_images = cache_images\n        \n        # Codificar target_name\n        self.target_names = ['Dry_Green_g', 'Dry_Dead_g', 'Dry_Clover_g', 'GDM_g', 'Dry_Total_g']\n        self.target_name_to_idx = {name: idx for idx, name in enumerate(self.target_names)}\n        \n        # Cache de imágenes \n        self.image_cache = {}\n        if self.cache_images:\n            print(\"Cargando imágenes en caché...\")\n            for idx in range(len(self.df)):\n                row = self.df.iloc[idx]\n                image_path = '/kaggle/input/csiro-biomass/' + os.path.join(self.image_dir, row['image_path'])\n                try:\n                    image = Image.open(image_path).convert('RGB')\n                    self.image_cache[idx] = image\n                except Exception as e:\n                    print(f\"Error cargando {image_path}: {e}\")\n                    self.image_cache[idx] = Image.new('RGB', (224, 224), color='black')\n            print(f\"Caché completado: {len(self.image_cache)} imágenes cargadas\")\n    \n    def __len__(self):\n        return len(self.df)\n    \n    def __getitem__(self, idx):\n        row = self.df.iloc[idx]\n        \n        if self.cache_images and idx in self.image_cache:\n            # Hacer copia segura de la imagen en caché para evitar modificar el original\n            cached_img = self.image_cache[idx]\n            image = cached_img.copy() if hasattr(cached_img, 'copy') else Image.fromarray(np.array(cached_img))\n        else:\n            image_path = os.path.join(self.image_dir, row['image_path'])\n            try:\n                image = Image.open(image_path).convert('RGB')\n            except Exception as e:\n                print(f\"Error cargando {image_path}: {e}\")\n    \n                image = Image.new('RGB', (224, 224), color='black')\n        \n        # Aplicar transformaciones\n        if self.transform:\n            image = self.transform(image)\n        \n        # Preparar salida\n        if self.is_train:\n            target = torch.tensor(row['target'], dtype=torch.float32)\n            target_name_idx = torch.tensor(\n                self.target_name_to_idx[row['target_name']], \n                dtype=torch.long\n            )\n            return image, target, target_name_idx\n        else:\n            target_name_idx = torch.tensor(\n                self.target_name_to_idx[row['target_name']], \n                dtype=torch.long\n            )\n            return image, target_name_idx","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### Arquitectura del Modelo\n\nEl modelo BiomassCNN utiliza:\n\n1. Backbone preentrenado (EfficientNet-B0): \n   - Extrae características visuales de las imágenes\n   - Preentrenado en ImageNet (millones de imágenes)\n   - EfficientNet-B0 es ~3x más rápido que B3 con pérdida mínima de precisión\n\n2. Embedding de target_name: \n   - Convierte el tipo de target (Dry_Green_g, etc.) en un vector de 32 dimensiones\n   - Permite al modelo aprender patrones específicos para cada tipo de biomasa\n\n3. Regresor combinado:\n   - Combina características de imagen + embedding de target","metadata":{}},{"cell_type":"code","source":"class BiomassCNN(nn.Module):\n    def __init__(self, model_name='efficientnet_b0', pretrained=True, num_targets=5, \n                 pretrained_path=None):\n        \"\"\"\n        model_name: Nombre del modelo preentrenado \n                    - 'efficientnet_b0' (más rápido, recomendado para CPU)\n                    - 'efficientnet_b3' (más lento pero más preciso)\n                    - 'resnet50' (alternativa rápida)\n        pretrained: Si usar pesos preentrenados en ImageNet\n        num_targets: Número de targets a predecir (5 en este caso)\n        pretrained_path: Ruta al archivo de pesos preentrenados (para uso sin internet)\n        \"\"\"\n        super(BiomassCNN, self).__init__()\n        \n        # Configurar directorio de modelos si no está configurado\n        if pretrained_path is None:\n            model_dir = './pretrained_models'\n            if os.path.exists(model_dir):\n                os.environ['TORCH_HOME'] = model_dir\n        \n        # Cargar modelo preentrenado usando timm\n        # Si pretrained=False, crea el modelo sin pesos\n        self.backbone = timm.create_model(\n            model_name, \n            pretrained=pretrained and pretrained_path is None,  # Solo descargar si no hay path local\n            num_classes=0,  # Sin capa de clasificación final\n            global_pool='avg'  # Global average pooling\n        )\n        \n        # Si se proporciona una ruta local, cargar desde ahí (para uso sin internet)\n        if pretrained_path is not None and os.path.exists(pretrained_path):\n            print(f\"Cargando pesos preentrenados desde: {pretrained_path}\")\n            state_dict = torch.load(pretrained_path, map_location='cpu')\n            self.backbone.load_state_dict(state_dict)\n            print(\"✓ Pesos preentrenados cargados desde archivo local\")\n        elif not pretrained and pretrained_path is None:\n            print(\"⚠ Usando modelo sin pesos preentrenados\")\n        \n        # Obtener dimensión de características del backbone\n        with torch.no_grad():\n            dummy_input = torch.randn(1, 3, 224, 224)\n            features = self.backbone(dummy_input)\n            feature_dim = features.shape[1]\n        \n        # Capas para regresión multi-target\n        # modelo que predice todos los targets\n        self.regressor = nn.Sequential(\n            nn.Dropout(0.3),\n            nn.Linear(feature_dim, 512),\n            nn.ReLU(),\n            nn.Dropout(0.2),\n            nn.Linear(512, 256),\n            nn.ReLU(),\n            nn.Dropout(0.1),\n            nn.Linear(256, num_targets)  # 5 salidas, una por cada target\n        )\n        \n        # Embedding para target_name \n        self.target_name_embedding = nn.Embedding(num_targets, 32)\n        \n        # Regresor que combina características de imagen + embedding de target_name\n        self.combined_regressor = nn.Sequential(\n            nn.Dropout(0.3),\n            nn.Linear(feature_dim + 32, 512),  # features + target_name embedding\n            nn.ReLU(),\n            nn.Dropout(0.2),\n            nn.Linear(512, 256),\n            nn.ReLU(),\n            nn.Dropout(0.1),\n            nn.Linear(256, 1)  # Una salida por target_name\n        )\n    \n    def forward(self, x, target_name_idx=None, use_target_name=True):\n        \"\"\"\n        x: Imagen (batch_size, 3, H, W)\n        target_name_idx: Índice del target_name (batch_size,)\n        use_target_name: Si usar el target_name como input adicional\n        \"\"\"\n        # Extraer características de la imagen\n        image_features = self.backbone(x)  # (batch_size, feature_dim)\n        \n        if use_target_name and target_name_idx is not None:\n            # Usar target_name como input adicional\n            target_embedding = self.target_name_embedding(target_name_idx)  # (batch_size, 32)\n            combined_features = torch.cat([image_features, target_embedding], dim=1)\n            output = self.combined_regressor(combined_features).squeeze(-1)\n        else:\n            # Predecir todos los targets\n            output = self.regressor(image_features)  # (batch_size, 5)\n            # Si se proporciona target_name_idx, seleccionar el target correspondiente\n            if target_name_idx is not None:\n                output = output[torch.arange(len(output)), target_name_idx]\n        \n        return output","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Transformaciones de datos \ntrain_transform = transforms.Compose([\n    transforms.Resize((224, 448)),  \n    transforms.RandomCrop(224),  \n    transforms.RandomHorizontalFlip(p=0.5),\n    transforms.RandomRotation(degrees=10),  \n    transforms.ColorJitter(brightness=0.15, contrast=0.15, saturation=0.15, hue=0.05),  \n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])  #\n])\n\nval_transform = transforms.Compose([\n    transforms.Resize((224, 448)),  \n    transforms.CenterCrop(224),\n    transforms.ToTensor(),\n    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])\n])\n\ntest_transform = val_transform  ","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Entrenamiento\n\nEl entrenamiento sigue este flujo:\n\n1. Forward pass: Pasa imágenes y target_name a través del modelo\n2. Cálculo de pérdida: MSE (Mean Squared Error) entre predicciones y valores reales\n3. Backward pass: Calcula gradientes y actualiza pesos del modelo\n4. Validación: Evalúa el modelo en conjunto de validación \n5. Métricas: Calcula R² para medir calidad de las predicciones\n6. Early stopping: Detiene si no hay mejora después de 5 épocas \n\n","metadata":{}},{"cell_type":"code","source":"# Preparar datos para entrenamiento\n# Dividir por imagen única para evitar data leakage\nunique_images = train['image_path'].unique()\ntrain_images, val_images = train_test_split(unique_images, test_size=0.2, random_state=42)\n\ntrain_df = train[train['image_path'].isin(train_images)].copy()\nval_df = train[train['image_path'].isin(val_images)].copy()\n\nprint(f\"Imágenes de entrenamiento: {len(train_images)}\")\nprint(f\"Imágenes de validación: {len(val_images)}\")\nprint(f\"Muestras de entrenamiento: {len(train_df)}\")\nprint(f\"Muestras de validación: {len(val_df)}\")\n\n# Crear datasets\nuse_cache = True  \ntrain_dataset = BiomassDataset(train_df, transform=train_transform, is_train=True, cache_images=use_cache)\nval_dataset = BiomassDataset(val_df, transform=val_transform, is_train=True, cache_images=use_cache)\n\n# Crear dataloaders\nimport platform\nnum_workers = 0 if platform.system() == 'Windows' else 2\n\nbatch_size = 8 if device.type == 'cpu' else 16  \ntrain_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, num_workers=num_workers, pin_memory=True if device.type == 'cuda' else False)\nval_loader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False, num_workers=num_workers, pin_memory=True if device.type == 'cuda' else False)\n\nprint(f\"Batch size: {batch_size}, Num workers: {num_workers}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"Modelo preentrenado de https://huggingface.co/timm/efficientnet_b0.ra_in1k","metadata":{}},{"cell_type":"code","source":"# Crear modelo\nmodel_name = 'efficientnet_b0' if device.type == 'cpu' else 'efficientnet_b3'\nprint(f\"Usando modelo: {model_name} (dispositivo: {device.type})\")\n\n# Configurar ruta al modelo preentrenado local (para uso sin internet)\nmodel_dir = '/kaggle/input/efficientnet-b0/pytorch/default/1/efficientnet_b0_pretrained.pth'\n\nmodel = BiomassCNN(model_name=model_name, pretrained=False, num_targets=5, pretrained_path=model_dir)\n    \nmodel = model.to(device)\n\n# Función de pérdida ponderada \ntarget_weights = {\n    'Dry_Green_g': 0.1,\n    'Dry_Dead_g': 0.1,\n    'Dry_Clover_g': 0.1,\n    'GDM_g': 0.2,\n    'Dry_Total_g': 0.5\n}\n\ndef weighted_mse_loss(pred, target, target_name_idx, target_names_list):\n    \"\"\"\n    Pérdida MSE ponderada según el tipo de target\n    \"\"\"\n    weights = torch.tensor([target_weights[name] for name in target_names_list], \n                          device=pred.device)\n    sample_weights = weights[target_name_idx]\n    mse = (pred - target) ** 2\n    weighted_mse = mse * sample_weights\n    return weighted_mse.mean()\n\n# Optimizador y scheduler\noptimizer = optim.AdamW(model.parameters(), lr=1e-4, weight_decay=1e-4)\nscheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=10, eta_min=1e-6)\n\ncriterion = nn.MSELoss()  ","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Función de entrenamiento\ndef train_epoch(model, train_loader, optimizer, criterion, device, verbose=True):\n    model.train()\n    total_loss = 0\n    num_batches = 0\n    \n    # Mostrar progreso cada N batches\n    total_batches = len(train_loader)\n    print_every = max(1, total_batches // 10)  \n    \n    for batch_idx, (images, targets, target_name_idx) in enumerate(train_loader):\n        images = images.to(device, non_blocking=True)\n        targets = targets.to(device, non_blocking=True)\n        target_name_idx = target_name_idx.to(device, non_blocking=True)\n        \n        optimizer.zero_grad()\n        \n        # Forward pass\n        predictions = model(images, target_name_idx, use_target_name=True)\n        loss = criterion(predictions, targets)\n        \n        # Backward pass\n        loss.backward()\n        optimizer.step()\n        \n        total_loss += loss.item()\n        num_batches += 1\n        \n        # Mostrar progreso\n        if verbose and (batch_idx + 1) % print_every == 0:\n            print(f\"  Batch {batch_idx + 1}/{total_batches} - Loss: {loss.item():.4f}\")\n    \n    return total_loss / num_batches\n\n# Función de validación\ndef validate(model, val_loader, criterion, device):\n    model.eval()\n    total_loss = 0\n    all_predictions = []\n    all_targets = []\n    num_batches = 0\n    \n    with torch.no_grad():\n        for images, targets, target_name_idx in val_loader:\n            images = images.to(device, non_blocking=True)\n            targets = targets.to(device, non_blocking=True)\n            target_name_idx = target_name_idx.to(device, non_blocking=True)\n            \n            predictions = model(images, target_name_idx, use_target_name=True)\n            loss = criterion(predictions, targets)\n            \n            total_loss += loss.item()\n            all_predictions.extend(predictions.cpu().numpy())\n            all_targets.extend(targets.cpu().numpy())\n            num_batches += 1\n    \n    # Calcular R²\n    from sklearn.metrics import r2_score\n    r2 = r2_score(all_targets, all_predictions)\n    \n    return total_loss / num_batches, r2, np.array(all_predictions), np.array(all_targets)","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# Entrenamiento \nimport time\n\nnum_epochs = 3 \nbest_val_r2 = -np.inf\npatience = 5\npatience_counter = 0\n\ntrain_losses = []\nval_losses = []\nval_r2_scores = []\n\nprint(\"Iniciando entrenamiento...\")\nprint(f\"Dispositivo: {device}\")\nprint(f\"Batch size: {batch_size}\")\nprint(f\"Número de batches por época: {len(train_loader)}\")\nprint(\"-\" * 50)\n\nfor epoch in range(num_epochs):\n    epoch_start_time = time.time()\n    \n    print(f\"\\nÉpoca {epoch+1}/{num_epochs}\")\n    train_loss = train_epoch(model, train_loader, optimizer, criterion, device, verbose=True)\n    \n    val_start_time = time.time()\n    val_loss, val_r2, val_preds, val_targets = validate(model, val_loader, criterion, device)\n    val_time = time.time() - val_start_time\n    \n    scheduler.step()\n    \n    train_losses.append(train_loss)\n    val_losses.append(val_loss)\n    val_r2_scores.append(val_r2)\n    \n    epoch_time = time.time() - epoch_start_time\n    \n    print(f\"\\nResumen Época {epoch+1}:\")\n    print(f\"  Train Loss: {train_loss:.4f}\")\n    print(f\"  Val Loss: {val_loss:.4f}\")\n    print(f\"  Val R²: {val_r2:.4f}\")\n    print(f\"  LR: {scheduler.get_last_lr()[0]:.6f}\")\n    print(f\"  Tiempo total: {epoch_time:.2f}s (validación: {val_time:.2f}s)\")\n    \n    # Guardar mejor modelo\n    if val_r2 > best_val_r2:\n        best_val_r2 = val_r2\n        patience_counter = 0\n        torch.save(model.state_dict(), 'best_model.pth')\n        print(f\"  ✓ Mejor modelo guardado (R²: {best_val_r2:.4f})\")\n    else:\n        patience_counter += 1\n    \n    if patience_counter >= patience:\n        print(f\"Early stopping después de {epoch+1} épocas\")\n        break\n\nprint(f\"\\n{'='*50}\")\nprint(f\"Mejor R² de validación: {best_val_r2:.4f}\")","metadata":{"trusted":true},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"# Predicciones","metadata":{}},{"cell_type":"code","source":"# Cargar mejor modelo para predicciones\nmodel.load_state_dict(torch.load('best_model.pth'))\nmodel.eval()\n\n# Preparar datos de test\ntest_dataset = BiomassDataset(test, transform=test_transform, is_train=False, cache_images=use_cache)\ntest_loader = DataLoader(\n    test_dataset, \n    batch_size=batch_size, \n    shuffle=False, \n    num_workers=num_workers,\n    pin_memory=True if device.type == 'cuda' else False\n)\n\n# Hacer predicciones\nall_test_predictions = []\n\nprint(\"Generando predicciones en test...\")\nwith torch.no_grad():\n    for images, target_name_idx in test_loader:\n        images = images.to(device, non_blocking=True)\n        target_name_idx = target_name_idx.to(device, non_blocking=True)\n        \n        predictions = model(images, target_name_idx, use_target_name=True)\n        all_test_predictions.extend(predictions.cpu().numpy())\n\n# Crear submission\nsubmission = pd.DataFrame({\n    'sample_id': test['sample_id'].values,\n    'target': all_test_predictions\n})\n\nsubmission.to_csv('submission.csv', index=False)\nprint(\"Submission guardado en 'submission.csv'\")\nprint(f\"\\nEstadísticas de predicciones:\")\nprint(submission['target'].describe())\nprint(f\"\\nPrimeras predicciones:\")\nprint(submission.head(10))","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}