# Install required packages if not present
import subprocess
import sys
def install_package(package):
subprocess.check_call([sys.executable, "-m", "pip", "install", package])
# Check and install tensorflow-datasets if needed
try:
import tensorflow_datasets
except ImportError:
print("Installing tensorflow-datasets...")
install_package("tensorflow-datasets")
import tensorflow_datasetsSession 4: CNN Hands-On Lab - EuroSAT Classification
Building CNNs with TensorFlow/Keras for Earth Observation
Duration: 90 minutes | Difficulty: Intermediate
Dataset: EuroSAT (27,000 Sentinel-2 images, 10 classes)
π― Objectives
By the end of this lab, you will:
- β Build a CNN from scratch using TensorFlow/Keras
- β Train on real satellite imagery (EuroSAT dataset)
- β Achieve >90% accuracy on land use classification
- β Evaluate model performance comprehensively
- β Understand training dynamics and hyperparameters
- β Apply data augmentation for better generalization
π Lab Structure
| Step | Activity | Duration |
|---|---|---|
| 1 | Environment Setup & GPU Check | 5 min |
| 2 | Dataset Download & Exploration | 15 min |
| 3 | Data Preprocessing & Augmentation | 15 min |
| 4 | Build CNN Architecture | 20 min |
| 5 | Training & Monitoring | 20 min |
| 6 | Evaluation & Analysis | 15 min |
π About EuroSAT Dataset
What: Benchmark dataset for satellite image classification
Source: Sentinel-2 RGB and multi-spectral
Images: 27,000 labeled patches (64Γ64 pixels)
Classes: 10 land use/land cover types
Purpose: Standardized comparison of classification methods
Classes: 1. Annual Crop 2. Forest 3. Herbaceous Vegetation 4. Highway 5. Industrial 6. Pasture 7. Permanent Crop 8. Residential 9. River 10. SeaLake
Letβs build your first CNN! π
Step 1: Environment Setup (5 minutes)
First, letβs import libraries and check if GPU is available.
# Core libraries
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from pathlib import Path
import os
import warnings
warnings.filterwarnings('ignore')
# TensorFlow and Keras
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers, models
from tensorflow.keras.preprocessing.image import ImageDataGenerator
from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint, ReduceLROnPlateau
# TensorFlow Datasets
import tensorflow_datasets as tfds
# scikit-learn for metrics
from sklearn.metrics import classification_report, confusion_matrix
# Plotting style
sns.set_style("whitegrid")
plt.rcParams['figure.figsize'] = (12, 8)
print(f"β TensorFlow version: {tf.__version__}")
print(f"β Keras version: {keras.__version__}")
print(f"β NumPy version: {np.__version__}")Check GPU Availability
GPUs dramatically speed up training. Letβs check if one is available.
# Check for GPU
gpus = tf.config.list_physical_devices('GPU')
if gpus:
print(f"\nβ GPU(s) Available: {len(gpus)}")
for gpu in gpus:
print(f" - {gpu.name}")
# Enable memory growth (prevents TensorFlow from allocating all GPU memory)
try:
for gpu in gpus:
tf.config.experimental.set_memory_growth(gpu, True)
print("\nβ GPU memory growth enabled")
except RuntimeError as e:
print(e)
else:
print("\nβ οΈ No GPU found - training will use CPU (slower)")
print(" Consider using Google Colab with GPU runtime:")
print(" Runtime > Change runtime type > Hardware accelerator > GPU")
# Set random seeds for reproducibility
np.random.seed(42)
tf.random.set_seed(42)
print("\nβ Environment ready!")Step 2: Dataset Download & Exploration (15 minutes)
Weβll download the EuroSAT RGB dataset from TensorFlow Datasets.
# Download EuroSAT dataset
print("Downloading EuroSAT RGB dataset...")
print("(This may take a few minutes on first run - ~90MB)")
# Load dataset with fixed splits
(ds_train, ds_val, ds_test), ds_info = tfds.load(
'eurosat/rgb',
split=['train[:70%]', 'train[70%:85%]', 'train[85%:]'],
as_supervised=True,
with_info=True,
shuffle_files=True
)
# Calculate dataset sizes
train_size = tf.data.experimental.cardinality(ds_train).numpy()
val_size = tf.data.experimental.cardinality(ds_val).numpy()
test_size = tf.data.experimental.cardinality(ds_test).numpy()
print(f"\nβ Dataset loaded successfully!")
print(f" Total images: {ds_info.splits['train'].num_examples}")
print(f" Train split: 70% (~{train_size} images)")
print(f" Val split: 15% (~{val_size} images)")
print(f" Test split: 15% (~{test_size} images)")
# Class names
class_names = ds_info.features['label'].names
num_classes = len(class_names)
print(f"\n Classes ({num_classes}):")
for i, name in enumerate(class_names):
print(f" {i}: {name}")Explore Dataset
# Visualize sample images
fig, axes = plt.subplots(4, 5, figsize=(15, 12))
axes = axes.flatten()
# Take 20 samples
sample_images = list(ds_train.take(20))
for idx, (image, label) in enumerate(sample_images):
ax = axes[idx]
# Display image
ax.imshow(image.numpy().astype('uint8'))
label_idx = int(label.numpy())
ax.set_title(f"{class_names[label_idx]}\n(Class {label_idx})",
fontsize=10, fontweight='bold')
ax.axis('off')
plt.suptitle('EuroSAT Sample Images', fontsize=16, fontweight='bold', y=0.995)
plt.tight_layout()
plt.show()
print("\nβ Dataset looks good!")
print(" Notice the variety of land use patterns")Step 3: Data Preprocessing & Augmentation (15 minutes)
We need to: 1. Normalize pixel values (0-255 β 0-1) 2. Batch the data for efficient training 3. Apply data augmentation to prevent overfitting
# Preprocessing function
def preprocess(image, label):
"""
Normalize image to [0, 1] range
"""
image = tf.cast(image, tf.float32) / 255.0
return image, label
# Apply preprocessing
ds_train = ds_train.map(preprocess, num_parallel_calls=tf.data.AUTOTUNE)
ds_val = ds_val.map(preprocess, num_parallel_calls=tf.data.AUTOTUNE)
ds_test = ds_test.map(preprocess, num_parallel_calls=tf.data.AUTOTUNE)
print("β Images normalized to [0, 1]")Data Augmentation
Augmentation creates variations of training images to improve generalization.
Techniques weβll use: - Random horizontal flips - Random vertical flips - Random rotations (90Β°, 180Β°, 270Β°) - Random brightness adjustment
Why safe for satellite imagery: - Land use patterns have no preferred orientation - Small brightness variations are realistic
# Data augmentation layer
data_augmentation = keras.Sequential([
layers.RandomFlip("horizontal_and_vertical"),
layers.RandomRotation(0.25), # Up to 90 degrees
layers.RandomBrightness(0.1), # Β±10% brightness
layers.RandomContrast(0.1),
], name='data_augmentation')
# Augmentation function
def augment(image, label):
image = data_augmentation(image, training=True)
return image, label
# Apply augmentation to training data only
ds_train_augmented = ds_train.map(augment, num_parallel_calls=tf.data.AUTOTUNE)
print("β Data augmentation configured")
print(" Augmentation applied to training data only")
print(" Validation and test data unchanged (for fair evaluation)")Visualize Augmentation
# Show original vs augmented
sample_image, sample_label = next(iter(ds_train))
# Convert label to integer for indexing
label_idx = int(sample_label.numpy())
fig, axes = plt.subplots(2, 4, figsize=(14, 7))
# Original
axes[0, 0].imshow(sample_image.numpy())
axes[0, 0].set_title('Original', fontweight='bold')
axes[0, 0].axis('off')
# Augmented versions
for idx in range(1, 8):
row = idx // 4
col = idx % 4
augmented = data_augmentation(tf.expand_dims(sample_image, 0), training=True)[0]
axes[row, col].imshow(augmented.numpy())
axes[row, col].set_title(f'Augmented {idx}', fontweight='bold')
axes[row, col].axis('off')
plt.suptitle(f'Data Augmentation Examples\nClass: {class_names[label_idx]}',
fontsize=14, fontweight='bold')
plt.tight_layout()
plt.show()
print("\nβ Augmentation creates realistic variations")Create Batched Datasets
# Configuration
BATCH_SIZE = 32
SHUFFLE_BUFFER = 1000
# Batch and prefetch for performance
ds_train_final = ds_train_augmented.shuffle(SHUFFLE_BUFFER).batch(BATCH_SIZE).prefetch(tf.data.AUTOTUNE)
ds_val_final = ds_val.batch(BATCH_SIZE).prefetch(tf.data.AUTOTUNE)
ds_test_final = ds_test.batch(BATCH_SIZE).prefetch(tf.data.AUTOTUNE)
print(f"β Datasets configured")
print(f" Batch size: {BATCH_SIZE}")
print(f" Prefetch enabled for optimal performance")Step 4: Build CNN Architecture (20 minutes)
Now weβll design and build a CNN from scratch!
Architecture Design
Weβll create a 3-block CNN:
Input (64Γ64Γ3)
β
Block 1: Conv(32) β Conv(32) β MaxPool β Dropout
β
Block 2: Conv(64) β Conv(64) β MaxPool β Dropout
β
Block 3: Conv(128) β MaxPool β Dropout
β
Flatten
β
Dense(256) β Dropout
β
Output (10 classes)
Design Principles: - Start with 32 filters, double each block (32β64β128) - Use 3Γ3 convolutions (standard) - MaxPool after each block (reduce dimensions) - Dropout for regularization (prevent overfitting) - ReLU activation (all hidden layers) - Softmax activation (output layer)
4.1: Define the Model
# Build CNN model
def build_cnn_model(input_shape=(64, 64, 3), num_classes=10):
"""
Build a 3-block CNN for EuroSAT classification
Parameters:
-----------
input_shape : tuple
Input image dimensions
num_classes : int
Number of output classes
Returns:
--------
model : keras.Model
Compiled CNN model
"""
model = models.Sequential([
# Input
layers.Input(shape=input_shape),
# Block 1
layers.Conv2D(32, (3, 3), activation='relu', padding='same', name='conv1_1'),
layers.Conv2D(32, (3, 3), activation='relu', padding='same', name='conv1_2'),
layers.MaxPooling2D((2, 2), name='pool1'),
layers.Dropout(0.25, name='dropout1'),
# Block 2
layers.Conv2D(64, (3, 3), activation='relu', padding='same', name='conv2_1'),
layers.Conv2D(64, (3, 3), activation='relu', padding='same', name='conv2_2'),
layers.MaxPooling2D((2, 2), name='pool2'),
layers.Dropout(0.25, name='dropout2'),
# Block 3
layers.Conv2D(128, (3, 3), activation='relu', padding='same', name='conv3_1'),
layers.MaxPooling2D((2, 2), name='pool3'),
layers.Dropout(0.25, name='dropout3'),
# Classifier
layers.Flatten(name='flatten'),
layers.Dense(256, activation='relu', name='fc1'),
layers.Dropout(0.5, name='dropout4'),
layers.Dense(num_classes, activation='softmax', name='output')
], name='EuroSAT_CNN')
return model
# Create model
model = build_cnn_model(input_shape=(64, 64, 3), num_classes=num_classes)
print("β CNN model created")
print(f" Architecture: 3-block CNN")
print(f" Input shape: (64, 64, 3)")
print(f" Output classes: {num_classes}")Model Summary
# Display model architecture
model.summary()
# Calculate total parameters
total_params = model.count_params()
print(f"\nπ Total Parameters: {total_params:,}")
print(f" Trainable: {total_params:,}")
# Breakdown by layer type
conv_params = sum([layer.count_params() for layer in model.layers if 'conv' in layer.name])
dense_params = sum([layer.count_params() for layer in model.layers if 'dense' in layer.name or 'fc' in layer.name])
print(f"\n Convolutional layers: {conv_params:,}")
print(f" Dense layers: {dense_params:,}")Visualize Architecture
# Plot model architecture (optional - may fail without graphviz)
try:
keras.utils.plot_model(
model,
to_file='cnn_architecture.png',
show_shapes=True,
show_layer_names=True,
rankdir='TB', # Top to bottom
dpi=96
)
# Display
from IPython.display import Image
display(Image('cnn_architecture.png'))
except Exception as e:
print("Note: Model visualization requires graphviz. Skipping...")
print("To install: apt-get install graphviz (Linux) or brew install graphviz (Mac)")4.2: Compile the Model
We need to configure: - Loss function: Sparse categorical crossentropy (for integer labels) - Optimizer: Adam (adaptive learning rate) - Metrics: Accuracy (percentage of correct predictions)
# Compile model
model.compile(
optimizer=keras.optimizers.Adam(learning_rate=0.001),
loss='sparse_categorical_crossentropy',
metrics=['accuracy']
)
print("β Model compiled")
print(f" Optimizer: Adam (lr=0.001)")
print(f" Loss: Sparse Categorical Crossentropy")
print(f" Metrics: Accuracy")Step 5: Training & Monitoring (20 minutes)
Time to train! Weβll use callbacks to: - Early Stopping: Stop if validation loss doesnβt improve - Model Checkpoint: Save best model weights - Reduce LR: Lower learning rate when plateauing
5.1: Configure Callbacks
# Create directory for model checkpoints
os.makedirs('model_checkpoints', exist_ok=True)
# Define callbacks
callbacks = [
# Early stopping: stop if val_loss doesn't improve for 10 epochs
EarlyStopping(
monitor='val_loss',
patience=10,
restore_best_weights=True,
verbose=1
),
# Model checkpoint: save best model
ModelCheckpoint(
'model_checkpoints/best_model.h5',
monitor='val_accuracy',
save_best_only=True,
verbose=1
),
# Reduce learning rate: divide by 2 if plateau
ReduceLROnPlateau(
monitor='val_loss',
factor=0.5,
patience=5,
min_lr=1e-7,
verbose=1
)
]
print("β Callbacks configured")
print(" - Early stopping (patience=10)")
print(" - Model checkpoint (save best)")
print(" - Reduce LR on plateau")5.2: Train the Model
β±οΈ Training Time: ~15-20 minutes on GPU, 1-2 hours on CPU
Weβll train for up to 50 epochs, but early stopping will likely halt around 20-30 epochs.
# Train model
print("Starting training...")
print("=" * 70)
EPOCHS = 50
history = model.fit(
ds_train_final,
validation_data=ds_val_final,
epochs=EPOCHS,
callbacks=callbacks,
verbose=1
)
print("=" * 70)
print("\nβ Training complete!")Training Curves
# Plot training history
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))
# Loss curves
ax1.plot(history.history['loss'], label='Training Loss', linewidth=2)
ax1.plot(history.history['val_loss'], label='Validation Loss', linewidth=2)
ax1.set_xlabel('Epoch', fontsize=12, fontweight='bold')
ax1.set_ylabel('Loss', fontsize=12, fontweight='bold')
ax1.set_title('Model Loss Over Time', fontsize=14, fontweight='bold')
ax1.legend(fontsize=11)
ax1.grid(True, alpha=0.3)
# Accuracy curves
ax2.plot(history.history['accuracy'], label='Training Accuracy', linewidth=2)
ax2.plot(history.history['val_accuracy'], label='Validation Accuracy', linewidth=2)
ax2.set_xlabel('Epoch', fontsize=12, fontweight='bold')
ax2.set_ylabel('Accuracy', fontsize=12, fontweight='bold')
ax2.set_title('Model Accuracy Over Time', fontsize=14, fontweight='bold')
ax2.legend(fontsize=11)
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# Print final metrics
final_train_acc = history.history['accuracy'][-1]
final_val_acc = history.history['val_accuracy'][-1]
final_train_loss = history.history['loss'][-1]
final_val_loss = history.history['val_loss'][-1]
print(f"\nπ Final Training Metrics:")
print(f" Train Accuracy: {final_train_acc*100:.2f}%")
print(f" Train Loss: {final_train_loss:.4f}")
print(f" Val Accuracy: {final_val_acc*100:.2f}%")
print(f" Val Loss: {final_val_loss:.4f}")
# Check for overfitting
gap = final_train_acc - final_val_acc
if gap > 0.05:
print(f"\nβ οΈ Possible overfitting: {gap*100:.1f}% gap between train/val accuracy")
else:
print(f"\nβ Good generalization: only {gap*100:.1f}% gap")Step 6: Evaluation & Analysis (15 minutes)
Now letβs evaluate on the test set and analyze results.
6.1: Test Set Evaluation
# Evaluate on test set
print("Evaluating on test set...")
test_loss, test_accuracy = model.evaluate(ds_test_final, verbose=0)
print(f"\nπ― Test Set Results:")
print(f" Accuracy: {test_accuracy*100:.2f}%")
print(f" Loss: {test_loss:.4f}")
if test_accuracy > 0.90:
print("\nπ Excellent! You've achieved >90% accuracy!")
elif test_accuracy > 0.85:
print("\nβ Good! Above 85% is solid for first attempt")
else:
print("\nπ‘ Room for improvement - try tuning hyperparameters")6.2: Confusion Matrix
# Generate predictions
print("Generating predictions for confusion matrix...")
y_true = []
y_pred = []
for images, labels in ds_test_final:
predictions = model.predict(images, verbose=0)
y_true.extend(labels.numpy())
y_pred.extend(np.argmax(predictions, axis=1))
y_true = np.array(y_true)
y_pred = np.array(y_pred)
print(f"β Predictions generated for {len(y_true)} test images")# Compute confusion matrix
cm = confusion_matrix(y_true, y_pred)
# Plot confusion matrix
fig, ax = plt.subplots(figsize=(12, 10))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
xticklabels=class_names, yticklabels=class_names,
cbar_kws={'label': 'Count'}, ax=ax)
ax.set_xlabel('Predicted Label', fontsize=12, fontweight='bold')
ax.set_ylabel('True Label', fontsize=12, fontweight='bold')
ax.set_title('Confusion Matrix - EuroSAT Test Set', fontsize=14, fontweight='bold')
plt.xticks(rotation=45, ha='right')
plt.yticks(rotation=0)
plt.tight_layout()
plt.show()
print("\nβ Confusion matrix generated")
print(" Diagonal = correct predictions")
print(" Off-diagonal = misclassifications")6.3: Per-Class Metrics
# Classification report
report = classification_report(y_true, y_pred, target_names=class_names, output_dict=True)
# Convert to DataFrame for nice display
report_df = pd.DataFrame(report).transpose()
print("\nπ Per-Class Performance:")
print("=" * 80)
print(report_df.round(3))
print("=" * 80)
# Highlight best and worst classes
metrics_df = report_df[:-3] # Exclude accuracy, macro avg, weighted avg
best_class = metrics_df['f1-score'].idxmax()
worst_class = metrics_df['f1-score'].idxmin()
print(f"\n⨠Best performing class: {best_class} (F1={metrics_df.loc[best_class, 'f1-score']:.3f})")
print(f"β οΈ Worst performing class: {worst_class} (F1={metrics_df.loc[worst_class, 'f1-score']:.3f})")Visualize Per-Class Accuracy
# Plot per-class F1 scores
fig, ax = plt.subplots(figsize=(12, 6))
classes = metrics_df.index.tolist()
f1_scores = metrics_df['f1-score'].values
bars = ax.barh(classes, f1_scores, color='steelblue', edgecolor='black', linewidth=1.5)
# Color best and worst
bars[classes.index(best_class)].set_color('green')
bars[classes.index(worst_class)].set_color('orange')
ax.set_xlabel('F1 Score', fontsize=12, fontweight='bold')
ax.set_ylabel('Class', fontsize=12, fontweight='bold')
ax.set_title('Per-Class F1 Scores', fontsize=14, fontweight='bold')
ax.set_xlim(0, 1.0)
ax.grid(axis='x', alpha=0.3)
# Add value labels
for i, (bar, score) in enumerate(zip(bars, f1_scores)):
ax.text(score + 0.01, i, f'{score:.3f}',
va='center', fontsize=10, fontweight='bold')
plt.tight_layout()
plt.show()6.4: Analyze Misclassifications
# Find misclassified examples
misclassified_indices = np.where(y_true != y_pred)[0]
print(f"\nTotal misclassifications: {len(misclassified_indices)} / {len(y_true)}")
print(f"Error rate: {len(misclassified_indices)/len(y_true)*100:.2f}%")
# Show some misclassified examples
if len(misclassified_indices) > 0:
print("\nβ οΈ Note: Displaying misclassified samples (this may take a moment)...")
# Get first few misclassifications
num_samples_to_show = min(12, len(misclassified_indices))
# Create a more efficient way to get misclassified samples
# We'll iterate through test set and collect misclassified images
misclassified_samples = []
batch_idx = 0
for images, labels in ds_test_final:
batch_preds = model.predict(images, verbose=0)
batch_preds = np.argmax(batch_preds, axis=1)
for i in range(len(labels)):
if labels[i] != batch_preds[i]:
misclassified_samples.append({
'image': images[i].numpy(),
'true_label': int(labels[i].numpy()),
'pred_label': int(batch_preds[i])
})
if len(misclassified_samples) >= num_samples_to_show:
break
if len(misclassified_samples) >= num_samples_to_show:
break
# Plot misclassifications
if misclassified_samples:
fig, axes = plt.subplots(3, 4, figsize=(14, 10))
axes = axes.flatten()
for idx, sample in enumerate(misclassified_samples[:12]):
ax = axes[idx]
# Display image
ax.imshow(sample['image'])
true_label = class_names[sample['true_label']]
pred_label = class_names[sample['pred_label']]
ax.set_title(f'True: {true_label}\nPred: {pred_label}',
fontsize=9, fontweight='bold',
color='red')
ax.axis('off')
plt.suptitle('Sample Misclassifications', fontsize=14, fontweight='bold', color='red')
plt.tight_layout()
plt.show()
print("\nπ‘ Misclassification Analysis:")
print(" Look for patterns:")
print(" - Similar-looking classes (e.g., forest types)")
print(" - Ambiguous examples")
print(" - Data augmentation might help")Bonus: Google Earth Engine Integration
To use this model with Google Earth Engine data, you can:
- Export GEE imagery for inference
- Deploy the model for real-time classification
Hereβs how to prepare GEE data for this model:
# Example: Preparing GEE data for model inference
def prepare_gee_data_for_model(image_array):
"""
Prepare Google Earth Engine imagery for CNN inference
Parameters:
-----------
image_array : numpy array
Raw satellite image from GEE (H, W, C)
Returns:
--------
prepared_patches : numpy array
64x64 patches ready for model.predict()
"""
import numpy as np
from skimage.util import view_as_windows
# Normalize to [0, 1]
if image_array.max() > 1:
image_array = image_array / 255.0
# Extract 64x64 patches
patch_size = 64
patches = view_as_windows(image_array,
(patch_size, patch_size, 3),
step=patch_size)
# Reshape for batch prediction
n_patches_y, n_patches_x = patches.shape[:2]
patches = patches.reshape(-1, patch_size, patch_size, 3)
return patches, (n_patches_y, n_patches_x)
# Example usage:
# patches, grid_shape = prepare_gee_data_for_model(gee_image)
# predictions = model.predict(patches)
# classification_map = predictions.reshape(grid_shape + (num_classes,))
print("β GEE integration helper function created")
print(" Use this to prepare Earth Engine exports for classification")π Lab Complete!
Summary
Youβve successfully:
β
Built a CNN from scratch (3 blocks, ~300K parameters)
β
Trained on 27,000 Sentinel-2 images
β
Achieved 90%+ accuracy on EuroSAT dataset
β
Evaluated with confusion matrix and per-class metrics
β
Analyzed misclassifications
Key Takeaways
What Worked Well
- Architecture: 3-block design with progressive filters (32β64β128)
- Regularization: Dropout prevented overfitting
- Data Augmentation: Improved generalization
- Callbacks: Early stopping saved training time
Compared to Random Forest (Session 1-2)
- CNN: 92-95% accuracy (automatic features)
- Random Forest: 85-90% accuracy (manual features)
- Improvement: +5-10% for critical applications
Whatβs Next?
- Transfer Learning: Use pre-trained ResNet (Session 4B)
- U-Net: Pixel-level segmentation (Session 4C)
- Google Earth Engine: Apply to large-scale mapping
- Production: Deploy model for monitoring
Exercises to Try
Easy
- Change batch size (16, 64) and observe effects
- Modify dropout rates (0.1, 0.5)
- Try different learning rates
Medium
- Add another convolutional block
- Use different augmentation techniques
- Experiment with optimizer (SGD vs Adam)
Advanced
- Implement learning rate scheduling
- Add batch normalization layers
- Try different architectures (VGG-style, ResNet-style)
- Integrate with Google Earth Engine exports
Save Your Work
# Save final model
model.save('eurosat_cnn_final.h5')
print("β Model saved: eurosat_cnn_final.h5")
# Save training history
import pickle
with open('training_history.pkl', 'wb') as f:
pickle.dump(history.history, f)
print("β Training history saved: training_history.pkl")
# Export predictions
results = pd.DataFrame({
'true_label': [class_names[i] for i in y_true],
'predicted_label': [class_names[i] for i in y_pred],
'correct': y_true == y_pred
})
results.to_csv('test_predictions.csv', index=False)
print("β Predictions saved: test_predictions.csv")
print("\nπ All done! Ready for deployment or further experimentation.")