CoolFace
Apppublic

LordZeee/ndvi-convlstm

sourceHugging Faceupdated 1y agoView on Hugging Face
0likes
train_model.py673 linesDownload Raw Back to src
1# model_convlstm_train_focused.py2import os3import numpy as np4import torch5from torch.utils.data import Dataset, DataLoader6import torch.nn as nn7import torch.nn.functional as F # For attention8import torch.optim as optim9from sklearn.model_selection import train_test_split10from sklearn.preprocessing import StandardScaler11import glob12import yaml13import argparse14import pickle15import pandas as pd16 17# Global variable to hold the loaded configuration18CONFIG = None19 20class NpzSequenceDataset(Dataset):21    def __init__(self, file_paths, config, scalers=None, is_train=False, fit_scalers_on_subset_size=0):22        self.file_paths = file_paths23        self.config = config24        self.train_opts = config.get('training_options', {})25        self.input_len = config['time_parameters']['input_len']26        self.pred_len = config['time_parameters']['pred_len']27        self.model_input_band_order = self.train_opts.get('input_band_names', [])28        if not self.model_input_band_order:29            raise ValueError("'input_band_names' must be defined in config's training_options.")30        self.num_model_input_channels = len(self.model_input_band_order)31        self.target_band_name = self.train_opts.get('target_band_name', 'NDVI')32        self.scalers = scalers33        self.is_train = is_train34        self.nodata_value = config.get('nodata_value_in_export', -9999.0) 35 36        if self.is_train and self.scalers is None and fit_scalers_on_subset_size > 0:37            print(f"Fitting scalers on a subset of up to {fit_scalers_on_subset_size} training files...")38            self._fit_scalers_on_subset(min(fit_scalers_on_subset_size, len(self.file_paths)))39        elif self.scalers is None:40            print("Warning: No scalers provided for NpzSequenceDataset. Data will be unscaled.")41 42    def _fit_scalers_on_subset(self, num_files_to_sample):43        self.scalers = {44            'input_scalers': [StandardScaler() for _ in range(self.num_model_input_channels)],45            'target_scaler': StandardScaler()46        }47        if num_files_to_sample == 0:48            print("Scaler fitting skipped (0 files to sample). Scalers remain un-fitted.")49            return50 51        print(f"Performing scaler fitting on {num_files_to_sample} random training files.")52        sample_file_paths = np.random.choice(self.file_paths, size=num_files_to_sample, replace=False)53        54        for f_path in sample_file_paths:55            try:56                data = np.load(f_path)57                input_d_raw = data['input_data']58                target_d_raw = data['target_data']59                bands_in_file = list(data['input_bands'])60 61                ordered_input_d_for_fitting = np.full(62                    (input_d_raw.shape[0], input_d_raw.shape[1], input_d_raw.shape[2], self.num_model_input_channels),63                    self.nodata_value, 64                    dtype=np.float3265                )66                for config_band_idx, config_band_name in enumerate(self.model_input_band_order):67                    try:68                        file_band_idx = bands_in_file.index(config_band_name)69                        ordered_input_d_for_fitting[..., config_band_idx] = input_d_raw[..., file_band_idx]70                    except ValueError:71                        pass 72 73                for band_idx in range(self.num_model_input_channels):74                    band_data = ordered_input_d_for_fitting[..., band_idx]75                    valid_pixels = band_data[band_data != self.nodata_value].reshape(-1, 1)76                    if len(valid_pixels) > 1: # partial_fit needs at least 2 samples for variance calculation77                        self.scalers['input_scalers'][band_idx].partial_fit(valid_pixels)78                79                valid_target_pixels = target_d_raw[target_d_raw != self.nodata_value].reshape(-1, 1)80                if len(valid_target_pixels) > 1:81                    self.scalers['target_scaler'].partial_fit(valid_target_pixels)82            except Exception as e:83                print(f"Error fitting scaler with file {f_path}: {e}")84        print("Scaler fitting on subset complete.")85        for i, scaler in enumerate(self.scalers['input_scalers']):86            if not hasattr(scaler, 'mean_'): # Check if scaler was actually fitted87                print(f"Warning: Input scaler for channel {i} ({self.model_input_band_order[i] if i < len(self.model_input_band_order) else 'Unknown'}) was not fitted (e.g., all NoData in sample).")88        if not hasattr(self.scalers['target_scaler'], 'mean_'):89            print(f"Warning: Target scaler was not fitted.")90 91 92    def __len__(self):93        return len(self.file_paths)94 95    def __getitem__(self, idx):96        file_path = self.file_paths[idx]97        try:98            npz_content = np.load(file_path)99            input_data_raw = npz_content['input_data'].astype(np.float32)100            target_data_raw = npz_content['target_data'].astype(np.float32)101            bands_in_file = list(npz_content['input_bands'])102            avg_input_clear_pixel_count = npz_content.get('avg_input_clear_pixel_count', 1.0).astype(np.float32) 103        except Exception as e:104            print(f"Error loading .npz file {file_path}: {e}. Returning dummy data.")105            dummy_input = np.zeros((self.input_len, self.config['minicube_parameters']['minicube_size_pixels'], self.config['minicube_parameters']['minicube_size_pixels'], self.num_model_input_channels), dtype=np.float32)106            dummy_target = np.zeros((self.pred_len, self.config['minicube_parameters']['minicube_size_pixels'], self.config['minicube_parameters']['minicube_size_pixels'], 1), dtype=np.float32)107            dummy_weight = np.array(1.0, dtype=np.float32)108            return torch.from_numpy(np.transpose(dummy_input, (0, 3, 1, 2))).float(), \109                   torch.from_numpy(np.transpose(dummy_target, (0, 3, 1, 2))).float(), \110                   torch.from_numpy(dummy_weight).float()111 112        ordered_input_data = np.full((self.input_len, input_data_raw.shape[1], input_data_raw.shape[2], self.num_model_input_channels), self.nodata_value, dtype=np.float32)113        for config_band_idx, config_band_name in enumerate(self.model_input_band_order):114            try:115                file_band_idx = bands_in_file.index(config_band_name)116                ordered_input_data[..., config_band_idx] = input_data_raw[..., file_band_idx]117            except ValueError: pass 118        119        input_data_scaled = ordered_input_data.copy()120        target_data_scaled = target_data_raw.copy()121 122        if self.scalers:123            for band_idx in range(self.num_model_input_channels): 124                if band_idx < len(self.scalers['input_scalers']) and hasattr(self.scalers['input_scalers'][band_idx], 'mean_'):125                    band_data = ordered_input_data[..., band_idx]126                    valid_mask_ch = (band_data != self.nodata_value)127                    if np.any(valid_mask_ch):128                        data_to_scale_ch = band_data[valid_mask_ch].reshape(-1,1)129                        scaled_data_ch = self.scalers['input_scalers'][band_idx].transform(data_to_scale_ch)130                        temp_scaled_band = np.full_like(band_data, self.nodata_value) 131                        temp_scaled_band[valid_mask_ch] = scaled_data_ch.flatten()132                        input_data_scaled[..., band_idx] = temp_scaled_band133            134            if hasattr(self.scalers.get('target_scaler'), 'mean_'):135                valid_mask_target = (target_data_raw != self.nodata_value)136                if np.any(valid_mask_target):137                    data_to_scale_target = target_data_raw[valid_mask_target].reshape(-1,1)138                    scaled_data_target = self.scalers['target_scaler'].transform(data_to_scale_target)139                    temp_scaled_target = np.full_like(target_data_raw, self.nodata_value)140                    temp_scaled_target[valid_mask_target] = scaled_data_target.flatten()141                    target_data_scaled = temp_scaled_target.reshape(target_data_raw.shape)142        143        input_data_final = np.transpose(input_data_scaled, (0, 3, 1, 2))144        target_data_final = np.transpose(target_data_scaled, (0, 3, 1, 2))145        146        max_expected_clear_count = self.train_opts.get('max_avg_clear_pixel_for_weighting', 3.0) # e.g. S2 might have up to 3-4 clear views in a week147        sample_weight = np.clip(avg_input_clear_pixel_count / max_expected_clear_count, 0.1, 1.0) # Normalize and clip148 149        return torch.from_numpy(input_data_final).float(), \150               torch.from_numpy(target_data_final).float(), \151               torch.tensor(sample_weight, dtype=torch.float32)152 153 154# --- ConvLSTM Model Definitions with Temporal Attention ---155class ConvLSTMCell(nn.Module):156    def __init__(self, input_dim, hidden_dim, kernel_size, bias):157        super(ConvLSTMCell, self).__init__()158        self.input_dim = input_dim159        self.hidden_dim = hidden_dim160        self.kernel_size = kernel_size 161        self.padding = kernel_size[0] // 2, kernel_size[1] // 2162        self.bias = bias163        self.conv = nn.Conv2d(in_channels=self.input_dim + self.hidden_dim,164                              out_channels=4 * self.hidden_dim,165                              kernel_size=self.kernel_size,166                              padding=self.padding,167                              bias=self.bias)168    def forward(self, input_tensor, cur_state):169        h_cur, c_cur = cur_state170        combined_conv = self.conv(torch.cat([input_tensor, h_cur], dim=1))171        cc_i, cc_f, cc_o, cc_g = torch.split(combined_conv, self.hidden_dim, dim=1)172        i, f, o, g = torch.sigmoid(cc_i), torch.sigmoid(cc_f), torch.sigmoid(cc_o), torch.tanh(cc_g)173        c_next = f * c_cur + i * g174        h_next = o * torch.tanh(c_next)175        return h_next, c_next176    def init_hidden(self, batch_size, image_size):177        height, width = image_size178        return (torch.zeros(batch_size, self.hidden_dim, height, width, device=self.conv.weight.device),179                torch.zeros(batch_size, self.hidden_dim, height, width, device=self.conv.weight.device))180 181class ConvLSTMEncoder(nn.Module):182    def __init__(self, input_dim, hidden_dims_list, kernel_sizes_list, num_layers, bias=True):183        super(ConvLSTMEncoder, self).__init__()184        self.input_dim, self.hidden_dims_list, self.kernel_sizes_list, self.num_layers = input_dim, hidden_dims_list, kernel_sizes_list, num_layers185        cell_list = []186        for i in range(self.num_layers):187            cur_input_dim = self.input_dim if i == 0 else self.hidden_dims_list[i-1]188            cell_list.append(ConvLSTMCell(cur_input_dim, self.hidden_dims_list[i], self.kernel_sizes_list[i], bias))189        self.cell_list = nn.ModuleList(cell_list)190 191    def forward(self, input_tensor): 192        b, seq_len, _, h, w = input_tensor.size()193        current_layer_input_sequence = input_tensor 194        all_layer_final_states = []195        last_layer_output_sequence = None196 197        for layer_idx in range(self.num_layers):198            h_state, c_state = self.cell_list[layer_idx].init_hidden(b, (h, w))199            output_hidden_states_this_layer_for_next_input = [] 200            for t in range(seq_len):201                h_state, c_state = self.cell_list[layer_idx](current_layer_input_sequence[:, t, :, :, :], [h_state, c_state])202                output_hidden_states_this_layer_for_next_input.append(h_state)203            204            all_layer_final_states.append([h_state, c_state]) 205            current_layer_input_sequence = torch.stack(output_hidden_states_this_layer_for_next_input, dim=1)206            if layer_idx == self.num_layers - 1:207                last_layer_output_sequence = current_layer_input_sequence 208        209        return all_layer_final_states, last_layer_output_sequence210 211 212class TemporalAttention(nn.Module):213    def __init__(self, hidden_dim_encoder_last_layer):214        super(TemporalAttention, self).__init__()215        # This attention mechanism will operate on the sequence of outputs from the last encoder layer.216        # These outputs are spatial feature maps (Batch, SeqLen, Channels_last_hidden, H, W).217        # We need to reduce the spatial dimensions to get a vector per time step for attention scoring.218        self.hidden_dim = hidden_dim_encoder_last_layer219        220        # A common way is to flatten or average pool the spatial dimensions.221        # Then apply a linear layer to get an energy score for each time step.222        # For ConvLSTM outputs, a simple approach is to use a 1x1 convolution to reduce channels to 1 (energy),223        # then softmax over time. Or, adaptive average pool then linear.224 225        # Let's use adaptive average pooling to (1,1) then a linear layer for scoring.226        self.attention_scorer = nn.Linear(self.hidden_dim, 1)227 228    def forward(self, encoder_output_sequence):229        # encoder_output_sequence shape: (Batch, SeqLen, Channels_last_hidden, H, W)230        b, seq_len, c, h, w = encoder_output_sequence.shape231        232        # Reshape for pooling: (Batch * SeqLen, Channels, H, W)233        reshaped_for_pooling = encoder_output_sequence.view(b * seq_len, c, h, w)234        # Adaptive average pool to reduce H, W to 1, 1235        pooled_spatial = F.adaptive_avg_pool2d(reshaped_for_pooling, (1,1)) # (B*S, C, 1, 1)236        # Reshape back: (Batch, SeqLen, Channels)237        pooled_temporal_sequence = pooled_spatial.view(b, seq_len, c)238 239        # Calculate attention scores (energies)240        # energies shape: (Batch, SeqLen, 1)241        energies = self.attention_scorer(pooled_temporal_sequence) 242        243        # Apply softmax over the sequence length dimension to get weights244        # attn_weights shape: (Batch, SeqLen, 1)245        attn_weights = F.softmax(energies, dim=1)246        247        # Calculate context vector: weighted sum of encoder outputs (pooled_temporal_sequence)248        # attn_weights needs to be (B, S, 1), pooled_temporal_sequence is (B, S, C)249        # We want context_vector to be (B, C)250        context_vector = torch.sum(attn_weights * pooled_temporal_sequence, dim=1) # (B, C)251        252        return context_vector # This is the context vector for the decoder253 254 255class ConvLSTMDecoderWithAttention(nn.Module):256    def __init__(self, output_dim, hidden_dims_list, kernel_sizes_list, num_layers, attention_context_dim, bias=True):257        super(ConvLSTMDecoderWithAttention, self).__init__()258        self.output_dim = output_dim 259        self.hidden_dims_list = hidden_dims_list 260        self.kernel_sizes_list = kernel_sizes_list 261        self.num_layers = num_layers262        self.attention_context_dim = attention_context_dim # Dim of context vector from TemporalAttention263 264        cell_list = []265        for i in range(self.num_layers):266            # The input to the first ConvLSTM cell of the decoder at each step needs to incorporate267            # the previous prediction AND potentially the attention context.268            # A common way is to concatenate the context with the input at each step,269            # or use the context to initialize/modify the decoder's initial hidden state.270 271            # Let's try modifying the initial hidden state of the first decoder layer using the context.272            # The input_dim for the first cell (i=0) will be self.output_dim (from the previous prediction).273            cur_input_dim_cell = self.output_dim if i == 0 else self.hidden_dims_list[i-1]274            cell_list.append(ConvLSTMCell(cur_input_dim_cell, self.hidden_dims_list[i], self.kernel_sizes_list[i], bias))275        self.cell_list = nn.ModuleList(cell_list)276        277        # Linear layers to transform the attention context vector to initialize/modify278        # the first decoder layer's hidden (h) and cell (c) states.279        self.W_h_context = nn.Linear(self.attention_context_dim, self.hidden_dims_list[0])280        self.W_c_context = nn.Linear(self.attention_context_dim, self.hidden_dims_list[0])281 282        self.output_conv = nn.Conv2d(self.hidden_dims_list[-1], self.output_dim, 1, padding=0)283 284    def forward(self, encoder_final_states_list, attention_context_vector, pred_len):285        # encoder_final_states_list: list of (h,c) from each encoder layer286        # attention_context_vector: (batch, attention_context_dim)287        b, _, h_enc, w_enc = encoder_final_states_list[0][0].size() 288 289        current_decoder_hidden_states = []290        for i in range(self.num_layers):291            h_enc_layer, c_enc_layer = encoder_final_states_list[i]292            if i == 0: # Modify initial state of the first decoder layer with attention context293                # Project context vector and expand to spatial dimensions294                # W_h_context(attention_context_vector) -> (B, hidden_dims_list[0])295                # .unsqueeze(-1).unsqueeze(-1) -> (B, hidden_dims_list[0], 1, 1)296                # .expand_as(h_enc_layer) -> (B, hidden_dims_list[0], H, W)297                h_modifier = self.W_h_context(attention_context_vector).unsqueeze(-1).unsqueeze(-1).expand_as(h_enc_layer)298                c_modifier = self.W_c_context(attention_context_vector).unsqueeze(-1).unsqueeze(-1).expand_as(c_enc_layer)299                300                # Combine, e.g., by adding and applying tanh301                h_init_dec0 = torch.tanh(h_enc_layer + h_modifier)302                c_init_dec0 = torch.tanh(c_enc_layer + c_modifier)303                current_decoder_hidden_states.append([h_init_dec0, c_init_dec0])304            else: # For other layers, initialize with encoder's final state for that layer305                current_decoder_hidden_states.append([h_enc_layer, c_enc_layer])306 307        predictions = []308        # Initial input to the decoder (e.g., a zero tensor with shape of one output frame)309        decoder_input_frame = torch.zeros(b, self.output_dim, h_enc, w_enc, device=attention_context_vector.device)310 311        for t in range(pred_len):312            current_layer_input_for_cell_propagation = decoder_input_frame 313            next_time_step_hidden_states = [] # Store states for this time step from all layers314            for layer_idx in range(self.num_layers):315                h_prev_layer_t, c_prev_layer_t = current_decoder_hidden_states[layer_idx]316                317                # Input to current cell:318                # For layer 0: it's the decoder_input_frame (previous prediction or zeros)319                # For layer > 0: it's the h_state from the previous layer *at this same prediction time step*320                input_to_cell = current_layer_input_for_cell_propagation if layer_idx == 0 else next_time_step_hidden_states[-1][0]321                322                h_next_layer_t, c_next_layer_t = self.cell_list[layer_idx](input_to_cell, [h_prev_layer_t, c_prev_layer_t])323                next_time_step_hidden_states.append([h_next_layer_t, c_next_layer_t])324                current_layer_input_for_cell_propagation = h_next_layer_t # Output h becomes input for next cell in stack325            326            current_decoder_hidden_states = next_time_step_hidden_states # Update states for the next time step327            output_frame = self.output_conv(current_decoder_hidden_states[-1][0]) # Predict from last layer's h_state328            predictions.append(output_frame)329            decoder_input_frame = output_frame # Use current prediction as input for the next time step (teacher forcing can be added here)330        331        return torch.stack(predictions, dim=1)332 333 334class Seq2SeqConvLSTMAttention(nn.Module):335    def __init__(self, num_input_channels, num_output_channels, hidden_dims_list, kernel_sizes_list, num_layers, bias=True):336        super(Seq2SeqConvLSTMAttention, self).__init__()337        self.encoder = ConvLSTMEncoder(num_input_channels, hidden_dims_list, kernel_sizes_list, num_layers, bias)338        self.attention = TemporalAttention(hidden_dims_list[-1]) # Attention on last encoder layer's hidden dim339        self.decoder = ConvLSTMDecoderWithAttention(num_output_channels, hidden_dims_list, kernel_sizes_list, num_layers, 340                                                    attention_context_dim=hidden_dims_list[-1], bias=bias)341 342    def forward(self, input_tensor, pred_len):343        encoder_final_states_list, encoder_output_sequence = self.encoder(input_tensor)344        context_vector = self.attention(encoder_output_sequence)345        predictions = self.decoder(encoder_final_states_list, context_vector, pred_len)346        return predictions347 348 349class ConvLSTMDecoder(nn.Module):350    def __init__(self, output_dim, hidden_dims_list, kernel_sizes_list, num_layers, bias=True):351        super(ConvLSTMDecoder, self).__init__()352        self.output_dim, self.hidden_dims_list, self.kernel_sizes_list, self.num_layers = output_dim, hidden_dims_list, kernel_sizes_list, num_layers353        cell_list = []354        for i in range(self.num_layers):355            cur_input_dim = self.output_dim if i == 0 else self.hidden_dims_list[i-1]356            cell_list.append(ConvLSTMCell(cur_input_dim, self.hidden_dims_list[i], self.kernel_sizes_list[i], bias))357        self.cell_list = nn.ModuleList(cell_list)358        self.output_conv = nn.Conv2d(self.hidden_dims_list[-1], self.output_dim, 1, padding=0)359 360    def forward(self, encoder_final_states, pred_len):361        b, _, h, w = encoder_final_states[0][0].size()362        current_hidden_states = encoder_final_states 363        predictions = []364        decoder_input_frame = torch.zeros(b, self.output_dim, h, w, device=encoder_final_states[0][0].device)365        for t in range(pred_len):366            current_layer_input_for_cell = decoder_input_frame 367            next_time_step_hidden_states = []368            for layer_idx in range(self.num_layers):369                h_state, c_state = current_hidden_states[layer_idx]370                input_to_cell = current_layer_input_for_cell if layer_idx == 0 else next_time_step_hidden_states[-1][0]371                h_state, c_state = self.cell_list[layer_idx](input_to_cell, [h_state, c_state])372                next_time_step_hidden_states.append([h_state, c_state])373                current_layer_input_for_cell = h_state 374            current_hidden_states = next_time_step_hidden_states 375            output_frame = self.output_conv(current_hidden_states[-1][0]) 376            predictions.append(output_frame)377            decoder_input_frame = output_frame 378        return torch.stack(predictions, dim=1)379 380 381# Standard Seq2SeqConvLSTM (without attention, for comparison or if attention is disabled)382class Seq2SeqConvLSTM(nn.Module):383    def __init__(self, num_input_channels, num_output_channels, hidden_dims_list, kernel_sizes_list, num_layers, bias=True):384        super(Seq2SeqConvLSTM, self).__init__()385        self.encoder = ConvLSTMEncoder(num_input_channels, hidden_dims_list, kernel_sizes_list, num_layers, bias)386        # Standard decoder does not use attention_context_dim387        # Its input_dim for the first cell in the feedback loop is num_output_channels388        self.decoder = ConvLSTMDecoder(num_output_channels, hidden_dims_list, kernel_sizes_list, num_layers, bias=bias) # Reusing the simpler decoder389 390    def forward(self, input_tensor, pred_len):391        encoder_final_states_list, _ = self.encoder(input_tensor) # Ignore encoder_output_sequence392        predictions = self.decoder.forward(encoder_final_states_list, pred_len) # Call original decoder's forward393        return predictions394 395 396# --- Training and Evaluation Functions ---397def calculate_rmse(predictions, targets, nodata_value=-9999.0):398    targets = targets.to(predictions.device)399    valid_mask = (targets != nodata_value) & (~torch.isnan(targets)) & (~torch.isnan(predictions))400    if not torch.any(valid_mask): return torch.tensor(float('nan'), device=predictions.device)401    squared_diff = torch.where(valid_mask, (predictions - targets)**2, torch.zeros_like(predictions))402    mean_squared_error = torch.sum(squared_diff) / torch.sum(valid_mask.float()).clamp(min=1e-6) # clamp to avoid div by zero403    return torch.sqrt(mean_squared_error)404 405def train_epoch(model, dataloader, optimizer, criterion, device, grad_clip_value=None, use_sample_weights=False):406    model.train()407    epoch_loss, epoch_rmse, num_batches = 0.0, 0.0, 0408    for batch_idx, batch_data in enumerate(dataloader):409        inputs, targets, sample_weights = batch_data[0].to(device), batch_data[1].to(device), batch_data[2].to(device)410        411        optimizer.zero_grad()412        outputs = model(inputs, targets.size(1))413        414        nodata_val = CONFIG.get('training_options', {}).get('nodata_value_for_plotting', -9999.0) 415        valid_target_mask = (targets != nodata_val) & (~torch.isnan(targets)) 416        valid_mask_for_loss = valid_target_mask & (~torch.isnan(outputs))417 418        if not torch.any(valid_mask_for_loss):419            # print(f"Warning: All targets/outputs are NoData/NaN in train batch {batch_idx}. Skipping.")420            continue421 422        per_pixel_loss = criterion(outputs, targets) 423        masked_loss_values = per_pixel_loss[valid_mask_for_loss] # Get only valid loss values424 425        if masked_loss_values.numel() == 0: # If no valid pixels after masking426            # print(f"Warning: Loss tensor is empty after masking in train batch {batch_idx}. Skipping.")427            continue428        429        # For sample weighting, we need to calculate loss per sample first, then weight, then mean.430        # This requires a bit more care if pixels within a sample can be masked.431        # Simpler approach for now: if using sample weights, apply it to the mean loss of valid pixels per sample.432        # Better: weight each valid pixel's loss, then average.433        434        # Calculate mean loss over valid pixels for each sample in the batch435        # masked_loss (B, T, C, H, W), valid_mask_for_loss (B, T, C, H, W)436        temp_masked_loss = torch.where(valid_mask_for_loss, per_pixel_loss, torch.zeros_like(per_pixel_loss))437        sum_loss_per_sample = torch.sum(temp_masked_loss, dim=[1,2,3,4])438        num_valid_pixels_per_sample = torch.sum(valid_mask_for_loss.float(), dim=[1,2,3,4]).clamp(min=1e-6)439        mean_loss_per_sample = sum_loss_per_sample / num_valid_pixels_per_sample440 441 442        if use_sample_weights:443            # sample_weights shape: (B)444            weighted_mean_loss_per_sample = mean_loss_per_sample * sample_weights445            batch_loss = weighted_mean_loss_per_sample.mean() # Mean of weighted sample losses446        else:447            batch_loss = mean_loss_per_sample.mean() # Mean of unweighted sample losses448            449        rmse = calculate_rmse(outputs, targets, nodata_val)450 451        if torch.isnan(batch_loss).item(): 452            print(f"Warning: NaN loss in train batch {batch_idx}. Skipping."); 453            continue454        batch_loss.backward()455        if grad_clip_value: torch.nn.utils.clip_grad_norm_(model.parameters(), grad_clip_value)456        optimizer.step()457        epoch_loss += batch_loss.item()458        if not torch.isnan(rmse).item(): 459            epoch_rmse += rmse.item() 460        num_batches += 1461        if batch_idx % CONFIG.get('training_options', {}).get('log_interval', 10) == 0:462            print(f"  Train Batch {batch_idx+1}/{len(dataloader)}: Loss={batch_loss.item():.4f}, RMSE={rmse.item() if not torch.isnan(rmse).item() else float('nan'):.4f}")463    if num_batches == 0: return float('nan'), float('nan')464    return epoch_loss / num_batches, epoch_rmse / num_batches465 466def evaluate_epoch(model, dataloader, criterion, device): 467    model.eval()468    epoch_loss, epoch_rmse, num_batches = 0.0, 0.0, 0469    with torch.no_grad():470        for batch_idx, batch_data in enumerate(dataloader):471            inputs, targets, _ = batch_data[0].to(device), batch_data[1].to(device), batch_data[2].to(device) # Ignore sample_weights for eval loss472            outputs = model(inputs, targets.size(1))473            474            nodata_val = CONFIG.get('training_options', {}).get('nodata_value_for_plotting', -9999.0)475            valid_target_mask = (targets != nodata_val) & (~torch.isnan(targets))476            valid_mask_for_loss = valid_target_mask & (~torch.isnan(outputs))477 478            if not torch.any(valid_mask_for_loss):479                continue480            481            per_pixel_loss = criterion(outputs, targets)482            masked_loss_values = per_pixel_loss[valid_mask_for_loss]483            if masked_loss_values.numel() == 0: continue484            485            batch_loss = masked_loss_values.mean() # Mean of valid pixel losses across the batch486            rmse = calculate_rmse(outputs, targets, nodata_val)487 488            if torch.isnan(batch_loss).item(): 489                print(f"Warning: NaN loss in val batch {batch_idx}. Skipping."); 490                continue491            epoch_loss += batch_loss.item()492            if not torch.isnan(rmse).item(): 493                epoch_rmse += rmse.item() 494            num_batches += 1495    if num_batches == 0: return float('nan'), float('nan')496    return epoch_loss / num_batches, epoch_rmse / num_batches497 498# --- Main Script ---499if __name__ == '__main__':500    parser = argparse.ArgumentParser(description="Train ConvLSTM model on .npz minicube sequences.")501    parser.add_argument("config_file", type=str, help="Path to the YAML configuration file.")502    args = parser.parse_args()503 504    print(f"Loading configuration from: {args.config_file}")505    try:506        with open(args.config_file, 'r') as f:507            CONFIG = yaml.safe_load(f)508    except FileNotFoundError:509        print(f"Error: Configuration file '{args.config_file}' not found.")510        exit()511    except yaml.YAMLError as e:512        print(f"Error parsing YAML configuration file: {e}")513        exit()514 515    516    # # --- START DEBUG BLOCK 1 ---517    # print("\n--- DEBUG: Immediately after loading CONFIG ---")518    # if CONFIG is None:519    #     print("CONFIG is None after loading!")520    # else:521    #     print(f"Type of CONFIG: {type(CONFIG)}")522    #     # print(f"Full CONFIG content: {CONFIG}") # Can be very verbose523        524    #     training_options_from_config = CONFIG.get('training_options')525    #     if training_options_from_config is None:526    #         print("'training_options' key NOT FOUND in CONFIG.")527    #     else:528    #         print(f"Type of training_options_from_config: {type(training_options_from_config)}")529    #         model_parameters_from_config = training_options_from_config.get('model_parameters')530    #         if model_parameters_from_config is None:531    #             print("'model_parameters' key NOT FOUND under 'training_options'.")532    #         else:533    #             print(f"Type of model_parameters_from_config: {type(model_parameters_from_config)}")534    #             hidden_dims_val_from_config = model_parameters_from_config.get('hidden_dims')535    #             if hidden_dims_val_from_config is None:536    #                 print("'hidden_dims' key NOT FOUND under 'model_parameters'. Default will be used.")537    #             else:538    #                 print(f"SUCCESS: 'hidden_dims' found in config: {hidden_dims_val_from_config}")539    # print("--- END DEBUG BLOCK 1 ---\n")540 541    train_opts = CONFIG.get('training_options', {})542    data_dir = CONFIG['directories'].get('processed_training_data_dir')543    model_output_dir = CONFIG['directories'].get('model_output_dir', 'model_outputs') 544    os.makedirs(model_output_dir, exist_ok=True)545 546    if not data_dir: print("Error: 'processed_training_data_dir' not found in config."); exit()547    device = torch.device(train_opts.get('device', 'cuda' if torch.cuda.is_available() else 'cpu'))548    print(f"Using device: {device}")549 550    all_npz_files = []551    for ecoregion_info in CONFIG['ecoregions']:552        ecoregion_name = ecoregion_info['name']553        ecoregion_processed_dir = os.path.join(data_dir, ecoregion_name)554        if os.path.exists(ecoregion_processed_dir):555            all_npz_files.extend(glob.glob(os.path.join(ecoregion_processed_dir, "*.npz")))556        else: print(f"Warning: Processed data directory not found: {ecoregion_processed_dir}")557    558    if not all_npz_files: print(f"Error: No .npz files found under {data_dir}. Run prepare_training_data.py first."); exit()559    print(f"Found {len(all_npz_files)} total .npz sequence files.")560 561    train_files, val_files = train_test_split(all_npz_files, test_size=train_opts.get('validation_split', 0.25), random_state=train_opts.get('random_seed', 42))562    val_files, test_files = train_test_split(val_files, test_size=0.2, random_state=train_opts.get('random_seed', 42))563    print(f"Training files: {len(train_files)}, Validation files: {len(val_files)}, Test files: {len(test_files)}")564    test_files_save_path = os.path.join(model_output_dir, f"test_files_seed{train_opts.get('random_seed', 42)}.txt")565    val_files_save_path = os.path.join(model_output_dir, f"validation_files_seed{train_opts.get('random_seed', 42)}.txt")566 567    for file_list, name_suffix in [(train_files, "train"), (val_files, "validation"), (test_files, "test")]:568        if file_list: # Only save if the list is not empty569            list_save_path = os.path.join(model_output_dir, f"{name_suffix}_files_seed{train_opts.get('random_seed', 42)}.txt")570            with open(list_save_path, 'w') as f:571                for item in file_list:572                    f.write(f"{item}\n")573            print(f"{name_suffix.capitalize()} file list saved to {list_save_path}")574 575    print("Initializing training dataset and fitting scalers (if configured)...")576    fit_subset_size = train_opts.get('fit_scalers_on_subset_size', 100 if len(train_files) > 100 else max(1, len(train_files)))577    if not train_files: 578        print("Error: No files available for training dataset after train/val split. Cannot fit scalers.")579        fit_subset_size = 0 580        581    train_dataset = NpzSequenceDataset(file_paths=train_files, config=CONFIG, scalers=None, is_train=True, fit_scalers_on_subset_size=fit_subset_size)582    583    fitted_scalers = None584    if hasattr(train_dataset, 'scalers') and train_dataset.scalers is not None:585        if train_dataset.scalers.get('input_scalers') and \586           len(train_dataset.scalers['input_scalers']) > 0 and \587           hasattr(train_dataset.scalers['input_scalers'][0], 'mean_') and \588           hasattr(train_dataset.scalers.get('target_scaler'), 'mean_'):589            fitted_scalers = train_dataset.scalers590            scaler_save_path = os.path.join(model_output_dir, 'fitted_scalers.pkl')591            try:592                with open(scaler_save_path, 'wb') as f: pickle.dump(fitted_scalers, f)593                print(f"Fitted scalers saved to {scaler_save_path}")594            except Exception as e: print(f"Error saving scalers: {e}")595        else:596            print("Warning: Scalers were initialized but not fitted (likely due to no training files or small subset). Data will be unscaled.")597    else: print("Warning: Training dataset did not produce scalers. Data will be unscaled.")598 599    val_dataset = NpzSequenceDataset(file_paths=val_files, config=CONFIG, scalers=fitted_scalers, is_train=False)600    train_loader = DataLoader(train_dataset, batch_size=train_opts.get('batch_size', 4), shuffle=True, num_workers=train_opts.get('num_workers', 0), pin_memory=True if device.type == 'cuda' else False)601    val_loader = DataLoader(val_dataset, batch_size=train_opts.get('batch_size', 4), shuffle=False, num_workers=train_opts.get('num_workers', 0), pin_memory=True if device.type == 'cuda' else False)602 603    input_band_names_from_config = train_opts.get('input_band_names', [])604    if not input_band_names_from_config: print("Error: 'input_band_names' must be defined in config."); exit()605    num_input_channels = len(input_band_names_from_config)606    num_output_channels = 1 # Predicting NDVI607 608    model_params = train_opts.get('model_parameters', {})609    hidden_dims = model_params.get('hidden_dims') 610    kernel_sizes_config = model_params.get('kernel_sizes', [[3,3], [3,3], [3,3]]) 611    kernel_sizes_tuples = [tuple(ks) for ks in kernel_sizes_config]612    num_layers = model_params.get('num_layers', 3)613    use_attention = model_params.get('use_attention', True) 614    #     # --- START DEBUG BLOCK 2 ---615    # print("\n--- DEBUG: After fetching model_params ---")616    # print(f"train_opts dictionary: {train_opts}")617    # print(f"model_params dictionary: {model_params}")618    # # --- END DEBUG BLOCK 2 ---\n")619 620    print(f"\n hidden_dims value being used for model: {hidden_dims}\n") #621    if len(hidden_dims) != num_layers or len(kernel_sizes_tuples) != num_layers: print("Error: Length of hidden_dims/kernel_sizes must match num_layers."); exit()622 623    if use_attention:624        print("Using Seq2SeqConvLSTM with Temporal Attention.")625        model = Seq2SeqConvLSTMAttention(num_input_channels, num_output_channels, hidden_dims, kernel_sizes_tuples, num_layers).to(device)626    else:627        print("Using standard Seq2SeqConvLSTM (no attention).")628        model = Seq2SeqConvLSTM(num_input_channels, num_output_channels, hidden_dims, kernel_sizes_tuples, num_layers).to(device)629    630    print(f"Model initialized: {num_input_channels} input channels, {num_output_channels} output channels, {num_layers} layers. Attention: {use_attention}")631 632    criterion = nn.MSELoss(reduction='none') 633    optimizer = optim.Adam(model.parameters(), lr=train_opts.get('learning_rate', 0.001))634    scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, mode='min', factor=0.1, patience=train_opts.get('lr_patience', 5))635 636    num_epochs = train_opts.get('num_epochs', 20)637    best_val_rmse = float('inf')638    model_save_full_path = os.path.join(model_output_dir, train_opts.get('model_save_path', 'convlstm_model_best.pth'))639    grad_clip = train_opts.get('gradient_clipping', None)640    use_sample_weighting_from_config = train_opts.get('use_sample_weighting', True) 641    training_log = []642 643    print(f"Sample weighting during training: {use_sample_weighting_from_config}")644 645    for epoch in range(1, num_epochs + 1):646        print(f"\nEpoch {epoch}/{num_epochs}")647        current_lr = optimizer.param_groups[0]['lr']648        print(f"Current learning rate: {current_lr}")649        train_loss, train_rmse = train_epoch(model, train_loader, optimizer, criterion, device, 650                                             grad_clip_value=grad_clip, 651                                             use_sample_weights=use_sample_weighting_from_config)652        val_loss, val_rmse = evaluate_epoch(model, val_loader, criterion, device) 653        654        print(f"Epoch Summary: Train Loss={train_loss:.4f}, Train RMSE={train_rmse:.4f} | Val Loss={val_loss:.4f}, Val RMSE={val_rmse:.4f}")655        training_log.append({'epoch': epoch, 'train_loss': train_loss, 'train_rmse': train_rmse, 'val_loss': val_loss, 'val_rmse': val_rmse})656 657        scheduler.step(val_rmse if not np.isnan(val_rmse) else float('inf')) 658        659        if not np.isnan(val_rmse) and val_rmse < best_val_rmse:660            best_val_rmse = val_rmse661            torch.save(model.state_dict(), model_save_full_path)662            print(f"Model improved and saved to {model_save_full_path} (Val RMSE: {best_val_rmse:.4f})")663 664        log_df = pd.DataFrame(training_log)665        log_save_path = os.path.join(model_output_dir, 'training_log.csv')666        log_df.to_csv(log_save_path, index=False)667        print(f"Training log saved to {log_save_path}")668 669    print("\nTraining finished.")670    print(f"Best validation RMSE: {best_val_rmse:.4f}") 671 672    print("\nTo visualize predictions, run 'plot_model_predictions.py' with the appropriate config and saved model path.")673