Detailed tutorial for beginners β every parameter explained
EuroSAT_RandomForest.ipynbimport numpy as np
import matplotlib.pyplot as plt
import tensorflow as tf
from tensorflow.keras.preprocessing import image_dataset_from_directory
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report, confusion_matrix, ConfusionMatrixDisplay, accuracy_score
from sklearn.model_selection import train_test_split, RandomizedSearchCV
import requests
import zipfile
import os
import joblib
from tqdm import tqdm
import warnings
warnings.filterwarnings("ignore")
print("β
All imports successful!")
numpy β numerical arrays.matplotlib β plotting.tensorflow.keras β we only use image_dataset_from_directory to load images efficiently.sklearn.ensemble.RandomForestClassifier β the Random Forest algorithm.train_test_split β splits data into train/validation.RandomizedSearchCV β fast hyperparameter tuning (random sampling).requests, zipfile, os β download and extract the dataset.joblib β save/load trained models (more efficient than pickle for large sklearn models).tqdm β shows a progress bar during download.
def download_file(url, dest_path):
response = requests.get(url, stream=True)
total_size = int(response.headers.get("content-length", 0))
with open(dest_path, "wb") as f:
with tqdm(total=total_size, unit="B", unit_scale=True, desc=dest_path) as pbar:
for chunk in response.iter_content(chunk_size=1024):
f.write(chunk)
pbar.update(len(chunk))
ZENODO_URL = "https://zenodo.org/record/7711810/files/EuroSAT_RGB.zip"
ZIP_PATH = "EuroSAT_RGB.zip"
EXTRACT_DIR = "EuroSAT_RGB"
if not os.path.exists(ZIP_PATH):
print("Downloading...")
download_file(ZENODO_URL, ZIP_PATH)
if not os.path.exists(EXTRACT_DIR):
print("Extracting...")
with zipfile.ZipFile(ZIP_PATH, "r") as zip_ref:
zip_ref.extractall(EXTRACT_DIR)
data_dir = os.path.join(EXTRACT_DIR, "EuroSAT_RGB")
image_dataset_from_directory automatically reads class subfolders, resizes images, and creates a tf.data.Dataset. We reserve 20% as a final test set (never touched during training).IMG_SIZE = (64, 64)
BATCH_SIZE = 32
full_train_ds = image_dataset_from_directory(
data_dir,
validation_split=0.2,
subset="training",
seed=123,
image_size=IMG_SIZE,
batch_size=BATCH_SIZE,
label_mode="int"
)
test_ds = image_dataset_from_directory(
data_dir,
validation_split=0.2,
subset="validation",
seed=123,
image_size=IMG_SIZE,
batch_size=BATCH_SIZE,
label_mode="int"
)
class_names = full_train_ds.class_names
print(f"Classes: {class_names}")
validation_split=0.2 β hold out 20% of all images as a test set.subset="training"/"validation" β get the two disjoint parts.seed=123 β ensures the same split every time (reproducibility).image_size=(64,64) β resize all patches to 64Γ64.batch_size=32 β load 32 images at a time (memory efficient).label_mode="int" β labels become integers 0..9 instead of one-hot.
def dataset_to_xy(dataset):
images, labels = [], []
for batch_imgs, batch_labels in dataset:
for i in range(batch_imgs.shape[0]):
img = batch_imgs[i].numpy().astype(np.float32) / 255.0
images.append(img.flatten())
labels.append(batch_labels[i].numpy())
return np.array(images), np.array(labels)
X_full, y_full = dataset_to_xy(full_train_ds)
X_train, X_val, y_train, y_val = train_test_split(
X_full, y_full, test_size=0.125, random_state=42, stratify=y_full
)
X_test, y_test = dataset_to_xy(test_ds)
print(f"Train: {X_train.shape[0]}, Val: {X_val.shape[0]}, Test: {X_test.shape[0]}")
X_full is 80% of total data, taking 12.5% of it gives a validation set that is 10% of the original total.
rf_baseline = RandomForestClassifier(
n_estimators=100,
random_state=42,
n_jobs=-1,
verbose=1
)
rf_baseline.fit(X_train, y_train)
y_val_pred = rf_baseline.predict(X_val)
print(f"Baseline Validation Accuracy: {accuracy_score(y_val, y_val_pred):.4f}")
n_estimators=100 β number of decision trees in the forest. More trees β better performance but slower training. 100 is a good start.random_state=42 β ensures every run produces the same random forest (reproducible).n_jobs=-1 β use all available CPU cores for parallel training (much faster).verbose=1 β prints progress during training (useful to know it's working)..fit(X_train, y_train) β trains the model on the training set..predict(X_val) β predicts labels for validation images.
RandomizedSearchCV to find better hyperparameters. It randomly samples combinations (much faster than trying all).param_dist = {
"n_estimators": [100, 150, 200, 250],
"max_depth": [10, 20, 30, None],
"min_samples_split": [2, 5, 10],
"min_samples_leaf": [1, 2, 4]
}
rf_tuned = RandomForestClassifier(random_state=42, n_jobs=-1)
random_search = RandomizedSearchCV(
rf_tuned,
param_distributions=param_dist,
n_iter=15,
cv=3,
scoring="accuracy",
verbose=2,
n_jobs=-1,
random_state=42
)
random_search.fit(X_train, y_train)
best_rf = random_search.best_estimator_
print(f"Best params: {random_search.best_params_}")
print(f"Best CV accuracy: {random_search.best_score_:.4f}")
n_estimators β number of trees. More trees reduce variance but increase training time. We try 100β250.max_depth β maximum depth of each tree. Deeper trees can overfit. None means expand until leaves are pure.min_samples_split β minimum number of samples required to split an internal node. Higher values prevent overfitting.min_samples_leaf β minimum samples at a leaf node. Smoother models with larger values.n_iter=15 β try 15 random combinations out of the 4Γ4Γ3Γ3 = 144 possible. Much faster than GridSearchCV.cv=3 β 3βfold crossβvalidation (splits training data into 3 parts, trains on 2, validates on 1, repeats).scoring="accuracy" β optimise for classification accuracy.verbose=2 β prints detailed progress.random_state=42 β makes the random sampling reproducible.
y_test_pred = best_rf.predict(X_test)
test_acc = accuracy_score(y_test, y_test_pred)
print(f"π― Test Accuracy: {test_acc:.4f}")
print("\nClassification Report:")
print(classification_report(y_test, y_test_pred, target_names=class_names))
cm = confusion_matrix(y_test, y_test_pred)
fig, ax = plt.subplots(figsize=(10,8))
ConfusionMatrixDisplay(cm, display_labels=class_names).plot(ax=ax, cmap="Greens", xticks_rotation=45)
plt.show()
accuracy_score β proportion of correct predictions.classification_report β shows precision, recall, f1-score per class.confusion_matrix β a table where rows are true labels, columns predicted labels. Diagonal = correct, offβdiagonal = errors.
importances = best_rf.feature_importances_
imp_reshaped = importances.reshape(64, 64, 3)
imp_per_pixel = np.max(imp_reshaped, axis=2)
plt.figure(figsize=(10,8))
plt.imshow(imp_per_pixel, cmap="hot")
plt.colorbar(label="Importance")
plt.title("Pixel Importance Heatmap")
plt.axis("off")
plt.show()
test_images = X_test.reshape(-1,64,64,3)
preds = best_rf.predict(X_test)
fig, axes = plt.subplots(4,6, figsize=(18,12))
for i, ax in enumerate(axes.flat):
ax.imshow(test_images[i])
ax.axis("off")
color = "#22c55e" if y_test[i]==preds[i] else "#ef4444"
ax.set_title(class_names[preds[i]], color=color, fontsize=8)
plt.show()
joblib.dump(best_rf, "eurosat_rf.pkl")
print("Model saved.")
try:
from google.colab import files
files.download("eurosat_rf.pkl")
except: pass
joblib is more efficient for large sklearn models (like Random Forest with many trees). .dump saves the model to disk; .load reloads it later.
RandomizedSearchCV over GridSearchCV?