Duck Typing for MFML#

This example demonstrates the “duck typing” flexibility of the ModelMFML orchestrator. DUck typing is summarized well with ‘If it walks like a duck, quacks like a duck, and swims like a duck, it must be a duck.’ Here, we use it to mean that we can use any ML architecture that has certain attributes. While MFML-QC provides an ultra-fast, built-in Kernel Ridge Regressor (KRR), you are not forced into using it.

Because the ModelMFML class is flexible, you can pass any custom machine learning model (such as a Neural Network or Random Forest from scikit-learn) using the base_estimator argument, provided it has standard .fit(X, y) or .train(X,y), and .predict(X) methods!

Imports and Helper Functions#

We reuse the exact same helper functions from the previous tutorial to extract and strictly nest our multi-fidelity training data.

[1]:
import os
import numpy as np
import matplotlib.pyplot as plt
from mfml_qc.datasets import load_benzene_data
from mfml_qc.mfml import ModelMFML
from mfml_qc.utils import build_hierarchy_arrays, top_down_subsetting

# We will use tha random forest regressor from scikit learn
from sklearn.ensemble import RandomForestRegressor

Loading and Splitting Data#

[2]:
dataset = load_benzene_data()

X_CM = dataset["X_CM"]
data = dataset["energies"]

hierarchy_cols = [2, 3, 6, 7]
num_fids = len(hierarchy_cols)

train_mask = data[:, 0] < 12288
test_mask = data[:, 0] >= 12288

X_train_parent = X_CM[train_mask]
X_test = X_CM[test_mask]
data_train = data[train_mask]
data_test = data[test_mask]

y_trains, indexes, means = build_hierarchy_arrays(data_train, hierarchy_cols)

# We use the same target sample sizes as the previous tutorial
n_trains_target = [1024, 512, 256, 128]
subset_y_trains, subset_indexes = top_down_subsetting(
    y_trains, indexes, n_trains_target, seed=42
)

Passing a Custom Base Estimator#

Instead of initializing the default Kernel Ridge Regressor, we will initialize a Random Forest Regressor from scikit-learn. Before we get into the code, let’s briefly review the mechanics of a Random Forest Regressor. Unlike Kernel Ridge Regression, which relies on measuring continuous structural distances (kernels), a Random Forest is an ensemble method constructed from multiple decision trees.

For a given input geometry representation \(x\), each individual decision tree \(k\) in the forest makes an independent prediction \(h_k(x)\). The final ensemble prediction \(\hat{y}\) is simply the average of all \(K\) trees:

\begin{align}\hat{y}(x) = \frac{1}{K} \sum_{k=1}^{K} h_k(x)\end{align}

By training each tree on a random subset of the data (bootstrapping) and considering a random subset of features at each split, the Random Forest drastically reduces the variance and overfitting that is common in single decision trees.

Notice how we pass the initialized sk_model directly to the base_estimator argument of the ModelMFML object. The script will automatically duplicate this tree-based model for all 2N-1 required sub-models!

[3]:
sk_model = RandomForestRegressor(
    n_estimators=50, max_depth=10, random_state=42, n_jobs=-1
)

# the mfml model class takes the random forest regressor as the base-estimator
mfml_model = ModelMFML(base_estimator=sk_model, p_bar=False)

# The class internally handles calling `.fit()` on the Random Forest models natively
mfml_model.train(
    X_train_parent=X_train_parent, y_trains=subset_y_trains, indexes=subset_indexes
)

Predicting and Evaluating#

[4]:
y_test_true = data_test[:, hierarchy_cols[-1]]

# the `default` optimiser carries out basic MFML
preds = mfml_model.predict(X_test=X_test, optimiser="default")

# Un-center the predictions
preds += means[-1]

mae = np.mean(np.abs(preds - y_test_true))
mae_kcal = mae * 23  # eV to kcal/mol

print(f"MFML (Random Forest) Test Set MAE: {mae:.6f} eV ({mae_kcal:.4f} kcal/mol)")

MFML (Random Forest) Test Set MAE: 0.029552 eV (0.6797 kcal/mol)

Parity Plot#

We can visualize the performance of our Random Forest-based MFML model. While kernels generally perform better for molecular representations like the Coulomb Matrix, this example shows that the framework is completely model-agnostic and can be used in a modular fashion.

[5]:
plt.figure(figsize=(5, 5))

# Plot the scatter points
plt.scatter(y_test_true, preds, alpha=0.6, color="lightcoral", edgecolor="k", s=25)

# Calculate the limits to draw a perfect 45-degree diagonal line
min_val = min(np.min(y_test_true), np.min(preds))
max_val = max(np.max(y_test_true), np.max(preds))
plt.plot([min_val, max_val], [min_val, max_val], "k--", lw=2)

plt.xlabel("True def2-TZVP Energy (eV)")
plt.ylabel("MFML Predicted Energy (eV)")
plt.title("Parity Plot (MFML-Random Forest)")
plt.grid(True, linestyle="--", alpha=0.6)
plt.tight_layout()
plt.show()
../_images/auto_examples_04_duck_typing_mfml_10_0.png