CNN Refinement - Implementation
For the theory, see Methodology > CNN Refinement. This page is the algorithm view.
Architecture in code
The model is built in the nested _build_model() helper inside train_bias_correction_model() in src/deep_learning.py:
m = Sequential([
Input(shape=input_shape),
Conv2D(num_filters_1, filter_size_1, activation='relu', padding='same'),
MaxPooling2D((2, 2)),
Dropout(dropout_rate_1),
Conv2D(num_filters_2, filter_size_2, activation='relu', padding='same'),
MaxPooling2D((2, 2)),
Dropout(dropout_rate_2),
Flatten(),
Dense(dense_layer_size, activation='relu'),
Dropout(dropout_rate_dense),
Dense(int(np.prod(input_shape[:-1])), activation='linear'),
Reshape(input_shape[:-1])
])
m.compile(optimizer=optimizer, loss='mean_squared_error', metrics=['mae'])padding='same' keeps the spatial dimensions through the Conv2D layers; the two MaxPooling2D stages then halve them, and the final Dense -> Reshape head restores the original grid shape. The Flatten -> Dense bottleneck is where most of the parameters live - this is the “future U-Net” point in the Changelog backlog.
Per-dekad model
A separate model is trained for each of the 36 dekads. The function is:
from src.deep_learning import train_bias_correction_model
# dekad_str is '01', '11', or '21'
model_name = f"bias_correction_model_month{month:02d}_dekad{dekad_str}"
model = train_bias_correction_model(
lseqm_field, cpc_field, model_name,
epochs=50, batch_size=64,
validation_split=0.2,
interactive=False,
)The function returns the trained keras.Model (reloaded from the best checkpoint). Early-stopping patience is not a call argument - it comes from deep_learning.early_stopping_patience in config.yml.
Inputs are (n_samples, n_lat, n_lon) arrays - one sample per (year, day-in-dekad) combination. For Indonesia 2001-2025 that is ~25 years x 10 days = ~250 samples per dekad. For Bali the same.
Training stops on val_loss early-stopping (patience 5). On Bali / Colab CPU, a single dekad completes in ~40-90 seconds.
Persistence
Trained models are saved to data/output/trained_models/:
trained_models/
bias_correction_model_month{MM}_dekad{DD}.keras
The .keras file is the whole artefact - there is no normalisation sidecar. Normalisation is per-sample: at inference each daily field is divided by its own maximum and the prediction is multiplied back by that same maximum, so no stored constants are needed.
The notebooks (02_lseqmdl_bias_correction.ipynb Step 8) have a non-interactive switch via existing_model_action in config: use_existing reuses a previously trained model, overwrite retrains.
Application
apply_deeplearning_model(model, lseqm_field, blend_alpha=..., confidence_mask=...) runs inference. It does the blend internally and returns the refined field directly.
Memory notes for batch runs
TensorFlow / Keras can pin process memory across calls. If you batch all 36 dekads in a single Python session (which the notebooks do), peak memory can grow even though each model is small. Two mitigations:
tf.keras.backend.clear_session()between dekads. Currently in the Changelog backlog.- Restart the kernel between nb02 (training) and nb06 (visualisation). The CNN model is not needed in nb06.
Cost
Per dekad: training is dominated by the ~50 epochs over ~250 samples on the AOI grid. For Bali (9 x 14), the Flatten -> Dense weight matrix is small and training takes ~40-90 seconds on CPU. For Indonesia (171 x 461), the same Dense layer is far larger and training takes ~5-10 minutes per dekad on CPU.