mirror of
https://github.com/edoverg/modeconverter.git
synced 2026-10-14 23:06:39 +00:00
305 lines
14 KiB
Python
305 lines
14 KiB
Python
'''Validates the surrogate model (Kriging) by evaluating it on an unseen test set.
|
|
Computes R² score and creates diagnostic plots.
|
|
|
|
Args:
|
|
design_space: SMT DesignSpace object
|
|
xdoe_train: training input samples (N_train, n_dims)
|
|
ydoe_train: training output values (N_train, 1)
|
|
xdoe_test: test input samples (N_test, n_dims)
|
|
ydoe_test: test output values (N_test, 1)
|
|
save_folder: folder to save plots
|
|
Returns:
|
|
Dictionary with metrics (r2_score, rmse, mae, sm)
|
|
'''
|
|
import matplotlib.pyplot as plt
|
|
from sklearn.metrics import r2_score, mean_squared_error, mean_absolute_error
|
|
import numpy as np
|
|
from smt.surrogate_models import KRG
|
|
from smt.design_space import DesignSpace
|
|
|
|
seed = 777
|
|
np.random.seed(seed)
|
|
xlimits = np.array([[100,700], [100,700]])
|
|
design_space = DesignSpace(xlimits, seed=seed)
|
|
results_folder = "results_symm_ego"
|
|
doe = np.loadtxt(f'{results_folder}/ego_doe.txt')
|
|
ndoe = doe.shape[0]
|
|
xdoe = doe[:, :2]
|
|
ydoe = doe[:, 2].reshape(-1, 1)
|
|
train_size = int(ndoe * 0.7)
|
|
xdoe_train = xdoe[:train_size].reshape(-1,2)
|
|
ydoe_train = ydoe[:train_size].reshape(-1,1)
|
|
xdoe_test = xdoe[train_size:].reshape(-1,2)
|
|
ydoe_test = ydoe[train_size:].reshape(-1,1)
|
|
doe_additional = np.loadtxt(f'{results_folder}/ego_additional.txt')
|
|
xdoe_additional = doe_additional[:, :2]
|
|
ydoe_additional = doe_additional[:, 2].reshape(-1, 1)
|
|
|
|
#ego optimal
|
|
optimum = np.loadtxt(f'{results_folder}/ego_opt_results.txt')
|
|
d0_opt = optimum[0]
|
|
d1_opt = optimum[1]
|
|
opt_val = optimum[2]
|
|
# Train the surrogate model
|
|
print("\n" + "="*60)
|
|
print("SURROGATE MODEL VALIDATION")
|
|
print("="*60)
|
|
print(f"Training set size: {xdoe_train.shape[0]} samples")
|
|
print(f"Test set size: {xdoe_test.shape[0]} samples")
|
|
|
|
xdoe_train_ref = np.vstack((xdoe_train, xdoe_additional))
|
|
ydoe_train_ref = np.vstack((ydoe_train, ydoe_additional))
|
|
sm = KRG(design_space=design_space, n_start=25, print_global=False)
|
|
sm.set_training_values(xdoe_train_ref, ydoe_train_ref)
|
|
sm.train()
|
|
|
|
# Predict on both sets
|
|
y_train_pred = sm.predict_values(xdoe_train)
|
|
y_test_pred = sm.predict_values(xdoe_test)
|
|
y_additional_pred = sm.predict_values(xdoe_additional)
|
|
y_train_ref_pred = sm.predict_values(xdoe_train_ref)
|
|
# Compute metrics
|
|
r2_train_ref = r2_score(ydoe_train_ref, y_train_ref_pred)
|
|
r2_test = r2_score(ydoe_test, y_test_pred)
|
|
rmse_train_ref = np.sqrt(mean_squared_error(ydoe_train_ref, y_train_ref_pred))
|
|
rmse_test = np.sqrt(mean_squared_error(ydoe_test, y_test_pred))
|
|
mae_train_ref = mean_absolute_error(ydoe_train_ref, y_train_ref_pred)
|
|
mae_test = mean_absolute_error(ydoe_test, y_test_pred)
|
|
|
|
print(f"\nTraining Set Metrics:")
|
|
print(f" R² Score: {r2_train_ref:.4f}")
|
|
print(f" RMSE: {rmse_train_ref:.6e}")
|
|
print(f" MAE: {mae_train_ref:.6e}")
|
|
|
|
print(f"\nTest Set Metrics:")
|
|
print(f" R² Score: {r2_test:.4f}")
|
|
print(f" RMSE: {rmse_test:.6e}")
|
|
print(f" MAE: {mae_test:.6e}")
|
|
|
|
if r2_test < 0.80:
|
|
print("\n WARNING: Test R² < 0.80. Consider adding more DOE samples.")
|
|
elif r2_test < 0.85:
|
|
print("\n CAUTION: Test R² < 0.85. Model quality is moderate.")
|
|
else:
|
|
print("\n Model quality is good. Ready to proceed with EGO.")
|
|
|
|
print("="*60 + "\n")
|
|
|
|
# Create diagnostic plots
|
|
fig, axes = plt.subplots(2, 2, figsize=(14, 11))
|
|
|
|
# Plot 1: Predicted vs Actual (Training and Test)
|
|
ax = axes[0, 0]
|
|
ax.scatter(ydoe_train, y_train_pred, alpha=0.6, label='Training', s=50, color='C0')
|
|
ax.scatter(ydoe_test, y_test_pred, alpha=0.6, label='Test', s=50, marker='^', color='C1')
|
|
ax.scatter(ydoe_additional, y_additional_pred, alpha=0.6, label='Additional', s=50, marker='s', color='C2')
|
|
y_min, y_max = min(ydoe_train.min(), ydoe_test.min(), ydoe_additional.min()), max(ydoe_train.max(), ydoe_test.max(), ydoe_additional.max())
|
|
ax.plot([y_min, y_max], [y_min, y_max], 'k--', lw=2, label='Perfect prediction')
|
|
ax.set_xlabel('Actual Output', fontsize=20)
|
|
ax.set_ylabel('Predicted Output', fontsize=20)
|
|
ax.set_title('Predicted vs Actual Values', fontsize=20)
|
|
ax.tick_params(axis='both', labelsize=18)
|
|
ax.legend(fontsize=16)
|
|
ax.grid(True, alpha=0.3)
|
|
ax.text(0.02, 0.98, '(a)', transform=ax.transAxes, fontsize=20, fontweight='bold', verticalalignment='top')
|
|
|
|
# Plot 2: Residuals (Test Set)
|
|
ax = axes[0, 1]
|
|
residuals = ydoe_test.flatten() - y_test_pred.flatten()
|
|
ax.scatter(y_test_pred, residuals, alpha=0.6, s=50, marker='^', color='C1')
|
|
ax.axhline(y=0, color='k', linestyle='--', lw=2)
|
|
ax.set_xlabel('Predicted Output', fontsize=20)
|
|
ax.set_ylabel('Residuals', fontsize=20)
|
|
ax.set_title('Residuals vs Predicted Values (Test Set)', fontsize=20)
|
|
ax.tick_params(axis='both', labelsize=18)
|
|
ax.grid(True, alpha=0.3)
|
|
ax.text(0.02, 0.98, '(b)', transform=ax.transAxes, fontsize=20, fontweight='bold', verticalalignment='top')
|
|
|
|
# Plot 3: Design Space Coverage
|
|
ax = axes[1, 0]
|
|
ax.scatter(xdoe_train[:, 0], xdoe_train[:, 1], alpha=0.6, s=50, label='Training', color='C0')
|
|
ax.scatter(xdoe_test[:, 0], xdoe_test[:, 1], alpha=0.6, s=50, marker='^', label='Test', color='C1')
|
|
ax.scatter(xdoe_additional[:, 0], xdoe_additional[:, 1], alpha=0.6, s=50, marker='s', label='Additional', color='C2')
|
|
ax.scatter(d0_opt, d1_opt, color='red', s=100, marker='*', label='EGO Optimum')
|
|
ax.set_xlabel('d0 [nm]', fontsize=20)
|
|
ax.set_ylabel('d1 [nm]', fontsize=20)
|
|
ax.set_title('Design Space Coverage', fontsize=20)
|
|
ax.tick_params(axis='both', labelsize=18)
|
|
ax.legend(fontsize=16, framealpha=0.5, loc='upper left')
|
|
ax.grid(True, alpha=0.3)
|
|
ax.text(0.98, 0.98, '(c)', transform=ax.transAxes, fontsize=20, fontweight='bold', verticalalignment='top', horizontalalignment='right')
|
|
|
|
# Plot 4: R² Score Comparison
|
|
ax = axes[1, 1]
|
|
models = ['Training (Ref)', 'Test']
|
|
r2_scores = [r2_train_ref, r2_test]
|
|
colors = ['green' if r2 > 0.85 else 'orange' if r2 > 0.80 else 'red' for r2 in r2_scores]
|
|
bars = ax.bar(models, r2_scores, color=colors, alpha=0.7, edgecolor='black', linewidth=2)
|
|
ax.axhline(y=0.85, color='green', linestyle='--', linewidth=2,)
|
|
#ax.axhline(y=0.80, color='orange', linestyle='--', linewidth=2, label='Acceptable (0.80)')
|
|
ax.set_ylabel('R² Score', fontsize=20)
|
|
ax.set_title('Model Performance', fontsize=20)
|
|
ax.tick_params(axis='both', labelsize=18)
|
|
ax.set_ylim([0, 1])
|
|
#ax.legend()
|
|
ax.grid(True, alpha=0.3, axis='y')
|
|
ax.text(0.02, 0.98, '(d)', transform=ax.transAxes, fontsize=20, fontweight='bold', verticalalignment='top')
|
|
# Add value labels on bars
|
|
for bar, score in zip(bars, r2_scores):
|
|
height = bar.get_height()
|
|
ax.text(bar.get_x() + bar.get_width()/2., height-0.1,
|
|
f'{score:.3f}', ha='center', va='bottom', fontweight='bold', fontsize=14)
|
|
|
|
plt.subplots_adjust(top=0.95, bottom=0.08, left=0.1, right=0.95, hspace=0.35, wspace=0.3)
|
|
plt.savefig(f'{results_folder}/surrogate_model_validation.pdf', dpi=300, bbox_inches='tight')
|
|
plt.close()
|
|
|
|
fig = plt.figure(figsize=(10, 8))
|
|
ax = fig.add_subplot(111, projection='3d')
|
|
# Create a grid for plotting the surrogate model surface
|
|
# predictions are very cheap thanks to the model)
|
|
x1 = np.linspace(xlimits[0, 0], xlimits[0, 1], 50)
|
|
x2 = np.linspace(xlimits[1, 0], xlimits[1, 1], 50)
|
|
X1, X2 = np.meshgrid(x1, x2)
|
|
X_grid = np.column_stack((X1.flatten(), X2.flatten()))
|
|
y_grid_pred = sm.predict_values(X_grid).reshape(X1.shape)
|
|
surf = ax.plot_surface(X1, X2, y_grid_pred, cmap='viridis', alpha=0.7, edgecolor='none')
|
|
# Plot training points
|
|
ax.scatter(xdoe_additional[:, 0], xdoe_additional[:, 1], ydoe_additional.flatten(), color='blue', marker='o', label='Additional', s=50)
|
|
# Plot only the original training points (without the EGO additional)
|
|
ax.scatter(xdoe_train[:, 0], xdoe_train[:, 1], ydoe_train.flatten(), color='green', label='Train', s=50, marker='d')
|
|
# Plot test points
|
|
ax.scatter(xdoe_test[:, 0], xdoe_test[:, 1], ydoe_test.flatten(), color='red', label='Test', s=50, marker='^')
|
|
# Highlight the optimal point found by EGO
|
|
ax.scatter(d0_opt, d1_opt, opt_val, color='gold', label='EGO Optimum', s=100, edgecolor='black', marker='*')
|
|
ax.set_xlabel('d0 [nm]', fontsize=20, labelpad=15)
|
|
ax.set_ylabel('d1 [nm]', fontsize=20, labelpad=15)
|
|
ax.set_zlabel('Cost Function Value', fontsize=20, labelpad=15)
|
|
ax.tick_params(axis='both', labelsize=18)
|
|
ax.legend(fontsize=16)
|
|
plt.savefig(f'{results_folder}/surrogate_model_surface_final.pdf', dpi=300, bbox_inches='tight')
|
|
plt.close()
|
|
|
|
#add a level set curve to better visualize the surface of the surrogate model
|
|
fig = plt.figure(figsize=(10, 8))
|
|
ax = fig.add_subplot(111)
|
|
# Create a grid for plotting the surrogate model surface
|
|
x1 = np.linspace(xlimits[0, 0], xlimits[0, 1], 100)
|
|
x2 = np.linspace(xlimits[1, 0], xlimits[1, 1], 100)
|
|
X1, X2 = np.meshgrid(x1, x2)
|
|
X_grid = np.column_stack((X1.flatten(), X2.flatten()))
|
|
y_grid_pred = sm.predict_values(X_grid).reshape(X1.shape)
|
|
# Plot the contour of the surrogate model
|
|
contour = ax.contourf(X1, X2, y_grid_pred, levels=50, cmap='viridis', alpha=0.8)
|
|
# Plot training points
|
|
ax.scatter(xdoe_additional[:, 0], xdoe_additional[:, 1], color='blue', marker='o', label='Additional', s=50)
|
|
# Plot only the original training points (without the EGO additional)
|
|
ax.scatter(xdoe_train[:, 0], xdoe_train[:, 1], color='green', label='Train', s=50, marker='d')
|
|
# Plot test points
|
|
ax.scatter(xdoe_test[:, 0], xdoe_test[:, 1], color='red', label='Test', s=50, marker='^')
|
|
# Highlight the optimal point found by EGO
|
|
ax.scatter(d0_opt, d1_opt, color='gold', label='EGO Optimum', s=100, edgecolor='black', marker='*')
|
|
ax.set_xlabel('d0 [nm]', fontsize=20)
|
|
ax.set_ylabel('d1 [nm]', fontsize=20)
|
|
ax.tick_params(axis='both', labelsize=18)
|
|
cbar = plt.colorbar(contour, label='Cost Function Value')
|
|
cbar.ax.tick_params(labelsize=18)
|
|
cbar.set_label('Cost Function Value', fontsize=20)
|
|
ax.legend(fontsize=16)
|
|
plt.savefig(f'{results_folder}/surrogate_model_contour_final.pdf', dpi=300, bbox_inches='tight')
|
|
plt.close()
|
|
|
|
|
|
#let us now consider a different approach to cross validation and work on k-fold cross validation.
|
|
#Consider the whole DOE dataset (40 points). We split the initial dataset into 10 folds.
|
|
#Each fold has 4 points. We will train the surrogate model on 9 folds and test it on the remaining fold. We will repeat this process for all 10 folds and compute the average R² score, RMSE, and MAE across all folds.
|
|
#We make use of the KFold class from sklearn.model_selection to perform the k-fold cross validation.
|
|
#We select the best surrogate model based on the generalization error: the generalization error
|
|
#is computed in the following way: given a surrogate model trained with the 9 folds, we test it with the
|
|
#remaining fold and compute the error as (predicted-true)^2. We then take the average error
|
|
#across the elements of the left-out fold.
|
|
from sklearn.model_selection import KFold
|
|
|
|
full_x_doe = doe[:, :2]
|
|
full_y_doe = doe[:, 2].reshape(-1, 1)
|
|
n_splits = 10
|
|
kf = KFold(n_splits=n_splits, shuffle=True, random_state=seed)
|
|
|
|
surrogates = []
|
|
folds_train_xdoe = []
|
|
folds_train_ydoe = []
|
|
avg_errs = []
|
|
for i, (train_index, test_index) in enumerate(kf.split(full_x_doe)):
|
|
print(f"Fold {i}")
|
|
print(f" Train: index={train_index}")
|
|
print(f" Test: index={test_index}")
|
|
train_values = full_x_doe[train_index]
|
|
test_values = full_x_doe[test_index]
|
|
train_outputs = full_y_doe[train_index]
|
|
test_outputs = full_y_doe[test_index]
|
|
sm_fold = KRG(design_space=design_space, n_start=25, print_global=False)
|
|
sm_fold.set_training_values(train_values, train_outputs)
|
|
sm_fold.train()
|
|
test_pred_values = sm_fold.predict_values(test_values)
|
|
avg_sq_err = np.mean((test_pred_values - test_outputs)**2)
|
|
print(f" Average squared error for fold {i}: {avg_sq_err:.6e}")
|
|
surrogates.append(sm_fold)
|
|
avg_errs.append(avg_sq_err)
|
|
#save the training data for this fold for future use
|
|
folds_train_xdoe.append(train_values)
|
|
folds_train_ydoe.append(train_outputs)
|
|
#make a plot of the predicted vs actual values for the test set of this fold
|
|
plt.figure(figsize=(10, 6))
|
|
plt.scatter(test_outputs, test_pred_values, alpha=0.6, s=50, color='C0')
|
|
y_min, y_max = min(test_outputs.min(), test_pred_values.min()), max(test_outputs.max(), test_pred_values.max())
|
|
plt.plot([y_min, y_max], [y_min, y_max], 'k--', lw=2, label='Perfect prediction')
|
|
plt.xlabel('Actual Output', fontsize=20)
|
|
plt.ylabel('Predicted Output', fontsize=20)
|
|
plt.title(f'Predicted vs Actual Values for Fold {i}', fontsize=20)
|
|
plt.tick_params(axis='both', labelsize=18)
|
|
plt.legend(fontsize=16)
|
|
plt.grid(True, alpha=0.3)
|
|
plt.savefig(f'{results_folder}/kfold_pred_vs_actual_fold_{i}.pdf', dpi=300, bbox_inches='tight')
|
|
plt.close()
|
|
|
|
#select the model with the lowest average squared error
|
|
best_model_index = np.argmin(avg_errs)
|
|
best_model = surrogates[best_model_index]
|
|
#make a plot of the average squared error for each fold
|
|
plt.figure(figsize=(10, 6))
|
|
plt.bar(range(n_splits), avg_errs, color='skyblue', edgecolor='black')
|
|
plt.xlabel('Fold Index', fontsize=20)
|
|
plt.ylabel('Average Squared Error', fontsize=20)
|
|
plt.title('Average Squared Error for Each Fold', fontsize=20)
|
|
plt.xticks(range(n_splits), [f'Fold {i}' for i in range(n_splits)], fontsize=16)
|
|
plt.yticks(fontsize=16)
|
|
plt.grid(axis='y', alpha=0.3)
|
|
plt.savefig(f'{results_folder}/kfold_avg_squared_error.pdf', dpi=300, bbox_inches='tight')
|
|
plt.close()
|
|
|
|
#save the model for future use and the corresponding training and result data for the best fold
|
|
import pickle
|
|
with open(f'{results_folder}/best_surrogate_model.pkl', 'wb') as f:
|
|
pickle.dump(best_model, f)
|
|
with open(f'{results_folder}/best_surrogate_model_training_data.pkl', 'wb') as f:
|
|
pickle.dump((folds_train_xdoe[best_model_index], folds_train_ydoe[best_model_index]), f)
|
|
|
|
#make a surface plot of the best surrogate model
|
|
fig = plt.figure(figsize=(10, 8))
|
|
ax = fig.add_subplot(111, projection='3d')
|
|
# Create a grid for plotting the surrogate model surface
|
|
x1 = np.linspace(xlimits[0, 0], xlimits[0, 1], 50)
|
|
x2 = np.linspace(xlimits[1, 0], xlimits[1, 1], 50)
|
|
X1, X2 = np.meshgrid(x1, x2)
|
|
X_grid = np.column_stack((X1.flatten(), X2.flatten()))
|
|
y_grid_pred = best_model.predict_values(X_grid).reshape(X1.shape)
|
|
surf = ax.plot_surface(X1, X2, y_grid_pred, cmap='viridis', alpha=0.7, edgecolor='none')
|
|
# Plot the full doe training points
|
|
ax.scatter(full_x_doe[:, 0], full_x_doe[:, 1], full_y_doe.flatten(), color='blue', marker='o', label='DOE Points', s=50)
|
|
ax.set_xlabel('d0 [nm]', fontsize=20, labelpad=15)
|
|
ax.set_ylabel('d1 [nm]', fontsize=20, labelpad=15)
|
|
ax.set_zlabel('Cost Function Value', fontsize=20, labelpad=15)
|
|
ax.tick_params(axis='both', labelsize=18)
|
|
ax.legend(fontsize=16)
|
|
plt.savefig(f'{results_folder}/best_surrogate_model_surface.pdf', dpi=300, bbox_inches='tight')
|
|
plt.close() |