Heat Conduction
Intputs
qprime: Linear heat generation rate (\(\frac{W}{m}\))mdot: Mass flow rate (\(\frac{g}{s}\))Tin: Temperature of the fuel boundary (\(K\))R: Fuel radius (\(m\))L: Fuel length (\(m\))Cp: Heat capacity (\(\frac{J}{g \cdot K}\))k: Thermal conductivity (\(\frac{W}{m \cdot K}\))
Output
T: Fuel centerline temperature (\(K\))
This data set consists of 1000 points with seven inputs and one output. The data set was constructed through Latin hypercube sampling of the seven input parameters for heat conduction through a fuel rod. These samples were then used to solve for the fuel centerline temperature analytically. The geometry of the problem is illustrated in the figure below, and it is assumed volumetric heat generation is uniform radially. The problem is defined by
\begin{equation} \frac{1}{r}\frac{d}{dr}(kr\frac{dT}{dr}) + q''' = 0, \end{equation}
with two boundary conditions: \(\frac{dT}{dr}|_{r = 0} = 0\) and \(T(R) = T_{in}\). Therefore, the temperature profile in the fuel is
\begin{equation} T(r) = \frac{q'}{4\pi k}(1 - (r / R)^2) + T_{in}. \end{equation}
The following are a few standard packages and functions that will prove helpful while using pyMAISE along with pyMAISE-specific functionality.
[1]:
# Importing Packages
import time
import cv2
import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import uniform, randint
from sklearn.preprocessing import MinMaxScaler
# pyMAISE specific imports
import pyMAISE as mai
from pyMAISE.datasets import load_heat
from pyMAISE.preprocessing import scale_data, train_test_split, correlation_matrix
# Plot settings
matplotlib_settings = {
"font.size": 12,
"legend.fontsize": 11,
"figure.figsize": (8, 8)
}
plt.rcParams.update(**matplotlib_settings)
pyMAISE Initialization
We start by initializing pyMAISE settings and then importing the data set using pyMAISE.datasets.load_heat() from the pyMAISE dataset library.
[2]:
# Initializing pyMaise settings and the problem we are solving (regression)
global_settings = mai.init(
problem_type=mai.ProblemType.REGRESSION, # Define a regression problem
cuda_visible_devices="-1" # Use CPU only
)
# Get data
data, inputs, outputs = load_heat()
The heat conduction data set has 7 inputs
[3]:
inputs
[3]:
<xarray.DataArray (index: 1000, variable: 7)>
array([[3.59879928e+04, 2.06185816e+02, 5.73151869e+02, ...,
3.44815496e+00, 4.09614034e+00, 9.60945479e-01],
[3.84810558e+04, 1.92378974e+02, 5.73150960e+02, ...,
3.43683275e+00, 4.24918181e+00, 1.01127217e+00],
[3.91432921e+04, 2.05076928e+02, 5.73153975e+02, ...,
3.68145722e+00, 4.23754044e+00, 9.94646131e-01],
...,
[4.01365078e+04, 1.91977771e+02, 5.73151522e+02, ...,
3.63435094e+00, 4.12297685e+00, 1.01905766e+00],
[4.06288682e+04, 1.93001960e+02, 5.73152035e+02, ...,
3.64753610e+00, 4.21262950e+00, 9.68979168e-01],
[3.90566005e+04, 1.87235532e+02, 5.73153905e+02, ...,
3.40448532e+00, 4.06543435e+00, 1.00688469e+00]])
Coordinates:
* index (index) int64 0 1 2 3 4 5 6 7 ... 992 993 994 995 996 997 998 999
* variable (variable) object 'qprime' 'mdot' 'Tin' 'R' 'L' 'Cp' 'k'and 1 output with 1000 data points.
[4]:
outputs
[4]:
<xarray.DataArray (index: 1000, variable: 1)>
array([[1034.13378385],
[1170.31604229],
[1164.89356528],
[1205.25003976],
[1444.71866607],
[1415.16087218],
[1141.02596298],
[1135.41902917],
[1087.37893848],
[1484.62104894],
[1095.53986907],
[1173.29114652],
[1157.14848053],
[1256.81754162],
[1118.14237521],
[1042.34706612],
[1049.5434326 ],
[1181.84317152],
[1425.17119154],
[1201.11399329],
...
[1440.68291936],
[1126.62859092],
[1118.47511015],
[1215.64386477],
[1506.41447337],
[1118.13383746],
[1427.1010686 ],
[1070.54469832],
[1393.14664198],
[1374.22902546],
[1142.38817778],
[1487.82931858],
[1103.84446468],
[1161.70719007],
[1503.51884631],
[1396.11284268],
[1570.4571551 ],
[1438.29381922],
[1374.74521282],
[1187.64399672]])
Coordinates:
* index (index) int64 0 1 2 3 4 5 6 7 ... 992 993 994 995 996 997 998 999
* variable (variable) object 'T'To better understand the data here is a correlation matrix of the data.
[5]:
correlation_matrix(data)
plt.show()
As expected, there is a strong correlation between the linear heat generation rate, prime, and the centerline fuel temperature, T.
Before model training, the data is split into training/test (70% training and 30% testing) along with min-max scaling to make each feature’s effect size comparable. Additionally, this can improve the performance of some models.
[6]:
# Train test split data
xtrain, xtest, ytrain, ytest = train_test_split(data=[inputs, outputs], test_size=0.30)
# Min-Max scaling data
xtrain, xtest, _ = scale_data(xtrain, xtest, MinMaxScaler())
ytrain, ytest, yscaler = scale_data(ytrain, ytest, MinMaxScaler())
Model Initialization
We will examine the performance of 7 models in this data set:
Linear regression:
Linear,Lasso regression:
Lasso,Support vector regression:
SVM,Decision tree regression:
DT,Random forest regression:
RF,K-nearest neighbors regression:
KN,Sequential dense neural networks:
FNN.
For hyperparameter tuning each model, we must initialize the architecture and optimize search spaces. For all the classical models we initialize them using the scikit-learn default configurations. The FNNs are defined as sequential dense neural networks. For the input and hidden dense layers we are hyperparameter tuning the number of nodes, sublayer, and sublayer dropout rate. We are also tuning the number of hidden layers, the Adam learning rate, and batch size.
[7]:
# Initializing all the models wanted in the model)settings variable along with neurel network archetecture/optimization hps
model_settings = {
"models": ["Linear", "Lasso", "SVM", "DT", "RF", "KN", "FNN"],
"FNN": {
"structural_params": {
"Dense_hidden": {
"num_layers": mai.Int(min_value=0, max_value=3),
"units": mai.Int(min_value=25, max_value=400),
"activation": "relu",
"kernel_initializer": "normal",
"sublayer": mai.Choice(["Dropout_hidden", "None"]),
"Dropout_hidden": {
"rate": mai.Float(min_value=0.4, max_value=0.6),
},
},
"Dense_output": {
"units": ytrain.shape[-1],
"activation": "linear",
"kernel_initializer": "normal",
},
},
"optimizer": "Adam",
"Adam": {
"learning_rate": mai.Float(min_value=1e-5, max_value=0.001),
},
"compile_params": {
"loss": "mean_absolute_error",
"metrics": ["mean_absolute_error"],
},
"fitting_params": {
"batch_size": mai.Choice([8, 16, 32]),
"epochs": 50,
"validation_split": 0.15,
},
},
}
# Constructing Tuner object for the search space above
tuner = mai.Tuner(xtrain, ytrain, model_settings=model_settings)
Hyperparameter Tuning
For hyperparameter tuning the classical models, we use pyMAISE.Tuner.random_search with 300 iterations and 5-fold cross-validation. We do this because the classical models are relatively cheap to train, so we can better cover the hyperparameter search space. For the FNNs, we use pyMAISE.Tuner.nn_bayesian_search with 50 iterations and 5-fold cross-validation. We use Bayesian search with fewer configurations as the FNNs are more computationally expensive. The Bayesian search should
converge on an optimal configuration within 30-50 iterations. The cross-validation ensures there is no bias in the configurations saved. The hyperparameter search spaces are outlined below.
[8]:
# Classical Model search space
random_search_spaces = {
"Lasso": {
"alpha": uniform(loc=0.0001, scale=0.0099), # 0.0001 - 0.01
},
"DT": {
"max_depth": randint(low=5, high=50), # 5 - 50
"max_features": [None, "sqrt", "log2", 2, 4, 6],
"min_samples_split": randint(low=2, high=20), # 2 - 20
"min_samples_leaf": randint(low=1, high=20), # 1 - 20
},
"SVM": {
"kernel": ["linear", "poly", "rbf", "sigmoid"],
"degree": randint(low=1, high=5),
"gamma": ["scale", "auto"],
},
"RF": {
"n_estimators": randint(low=50, high=200), # 50 - 200
"criterion": ["squared_error", "absolute_error", "poisson"],
"min_samples_split": randint(low=2, high=20), # 2 - 20
"min_samples_leaf": randint(low=1, high=20), # 1 - 20
"max_features": [None, "sqrt", "log2", 2, 4, 6],
},
"KN": {
"n_neighbors": randint(low=1, high=20), # 1 - 20
"weights": ["uniform", "distance"],
"leaf_size": randint(low=1, high=30), # 1 - 30
"p": randint(low=1, high=10), # 1 - 10
},
}
# Running the tuner for with the search algorithm desired
start = time.time()
random_search_configs = tuner.random_search(
param_spaces=random_search_spaces,
n_iter=300,
n_jobs=6,
cv=5,
)
bayesian_search_configs = tuner.nn_bayesian_search(
objective="r2_score",
max_trials=50,
cv=5,
)
print("Hyperparameter tuning took " + str((time.time() - start) / 60) + " minutes to process.")
Hyperparameter tuning took 29.920362746715547 minutes to process.
With the conclusion of hyper-parameter tuning we can see the training results of each iteration of the bayesian search.
[9]:
ax = tuner.convergence_plot(model_types="FNN")
ax.set_ylim([0, 1])
plt.show()
After about ten iterations, the Bayesian search converges on the optimal solution for this parameter space.
Model Postprocessing
With the models tuned and the top pyMAISE.Settings.num_configs_saved saved, we can now pass these models to the PostProcessor for model comparison and analysis. We will increase the FNN models epochs for better performance.
[10]:
postprocessor = mai.PostProcessor(
data=(xtrain, xtest, ytrain, ytest),
model_configs=[random_search_configs, bayesian_search_configs],
new_model_settings={
"FNN": {"fitting_params": {"epochs": 200}},
},
yscaler=yscaler,
)
The regression performance metrics on fuel centerline temperature are shown below.
[11]:
postprocessor.metrics().drop("Parameter Configurations", axis=1)
[11]:
| Model Types | Train R2 | Train MAE | Train MAPE | Train RMSE | Train RMSPE | Test R2 | Test MAE | Test MAPE | Test RMSE | Test RMSPE | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 20 | RF | 0.997154 | 4.986508 | 0.386106 | 8.052169 | 0.631222 | 0.994233 | 7.923253 | 0.616351 | 11.824369 | 0.918915 |
| 18 | RF | 0.997033 | 5.139566 | 0.397534 | 8.221685 | 0.642415 | 0.994039 | 8.098878 | 0.630486 | 12.021820 | 0.936141 |
| 17 | RF | 0.998024 | 4.427935 | 0.342536 | 6.709407 | 0.533691 | 0.993746 | 8.205877 | 0.637626 | 12.314054 | 0.956969 |
| 19 | RF | 0.997136 | 5.263513 | 0.406251 | 8.077905 | 0.635408 | 0.993728 | 8.350372 | 0.647859 | 12.331529 | 0.954462 |
| 16 | RF | 0.998684 | 3.552243 | 0.275988 | 5.476834 | 0.438886 | 0.993029 | 8.551964 | 0.666155 | 13.000574 | 1.011017 |
| 14 | DT | 0.993846 | 8.446816 | 0.650752 | 11.841301 | 0.915109 | 0.985220 | 12.827502 | 1.009586 | 18.930021 | 1.501253 |
| 13 | DT | 0.993846 | 8.446816 | 0.650752 | 11.841301 | 0.915109 | 0.985220 | 12.827502 | 1.009586 | 18.930021 | 1.501253 |
| 15 | DT | 0.996577 | 6.133811 | 0.476671 | 8.831676 | 0.699559 | 0.984935 | 12.667243 | 0.999214 | 19.111302 | 1.520477 |
| 11 | DT | 0.995777 | 6.739139 | 0.520000 | 9.808999 | 0.759180 | 0.984636 | 12.876282 | 1.016210 | 19.300072 | 1.535464 |
| 12 | DT | 0.994587 | 7.802882 | 0.601207 | 11.106283 | 0.863034 | 0.984309 | 13.410189 | 1.055767 | 19.504322 | 1.550309 |
| 28 | FNN | 0.990703 | 4.107766 | 0.322267 | 14.554742 | 1.100226 | 0.974040 | 7.219007 | 0.559021 | 25.087926 | 1.846443 |
| 30 | FNN | 0.989318 | 8.569111 | 0.674231 | 15.601300 | 1.283347 | 0.973228 | 11.522918 | 0.926079 | 25.476912 | 2.078072 |
| 26 | FNN | 0.984739 | 4.631119 | 0.358272 | 18.647505 | 1.472439 | 0.969190 | 7.493166 | 0.586952 | 27.330885 | 2.143828 |
| 27 | FNN | 0.981239 | 11.610469 | 0.900106 | 20.675833 | 1.634222 | 0.967317 | 14.332504 | 1.134863 | 28.149507 | 2.258070 |
| 29 | FNN | 0.940880 | 26.184468 | 1.958679 | 36.702944 | 2.808690 | 0.932436 | 28.152282 | 2.155911 | 40.473259 | 3.180488 |
| 10 | SVM | 0.895306 | 42.695727 | 3.314075 | 48.841919 | 3.815481 | 0.857033 | 50.145252 | 3.946441 | 58.874553 | 4.650346 |
| 8 | SVM | 0.895306 | 42.695727 | 3.314075 | 48.841919 | 3.815481 | 0.857033 | 50.145252 | 3.946441 | 58.874553 | 4.650346 |
| 7 | SVM | 0.895306 | 42.695727 | 3.314075 | 48.841919 | 3.815481 | 0.857033 | 50.145252 | 3.946441 | 58.874553 | 4.650346 |
| 6 | SVM | 0.895306 | 42.695727 | 3.314075 | 48.841919 | 3.815481 | 0.857033 | 50.145252 | 3.946441 | 58.874553 | 4.650346 |
| 9 | SVM | 0.895306 | 42.695727 | 3.314075 | 48.841919 | 3.815481 | 0.857033 | 50.145252 | 3.946441 | 58.874553 | 4.650346 |
| 5 | Lasso | 0.839945 | 52.227829 | 4.062700 | 60.390247 | 4.745456 | 0.850541 | 52.731179 | 4.168716 | 60.196391 | 4.797726 |
| 4 | Lasso | 0.839980 | 52.218883 | 4.062042 | 60.383614 | 4.744978 | 0.850534 | 52.729709 | 4.168467 | 60.197942 | 4.797652 |
| 3 | Lasso | 0.840101 | 52.185042 | 4.059570 | 60.360787 | 4.743353 | 0.850487 | 52.726402 | 4.167712 | 60.207380 | 4.797624 |
| 2 | Lasso | 0.840165 | 52.163806 | 4.058018 | 60.348782 | 4.742518 | 0.850443 | 52.726202 | 4.167401 | 60.216178 | 4.797823 |
| 1 | Lasso | 0.840206 | 52.148545 | 4.056897 | 60.340929 | 4.741985 | 0.850402 | 52.726049 | 4.167163 | 60.224350 | 4.798088 |
| 0 | Linear | 0.840331 | 52.061837 | 4.050545 | 60.317326 | 4.740691 | 0.849952 | 52.749655 | 4.167604 | 60.315047 | 4.802679 |
| 21 | KN | 1.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.765921 | 66.107932 | 5.263937 | 75.333995 | 6.082310 |
| 24 | KN | 1.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.762813 | 65.333817 | 5.200211 | 75.832477 | 6.125086 |
| 23 | KN | 1.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.762162 | 67.368345 | 5.362470 | 75.936563 | 6.129685 |
| 22 | KN | 1.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.762162 | 67.368345 | 5.362470 | 75.936563 | 6.129685 |
| 25 | KN | 1.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.742511 | 64.803547 | 5.156468 | 79.011268 | 6.367217 |
This data set represents radial heat conduction, so we do not expect linear models to perform well. This is shown by the relatively poor performance of Linear and Lasso. The best-performing models were RF, DT, and FNN with test and train r-squared above 0.99.
Using the pyMAISE.PostProcessor.print_model, we can see the optimal hyperparameter configurations for each model.
[12]:
for model in ["Lasso", "DT", "RF", "KN", "FNN"]:
postprocessor.print_model(model_type=model)
print()
Model Type: Lasso
alpha: 0.00028379044515601027
Model Type: DT
max_depth: 40
max_features: None
min_samples_leaf: 6
min_samples_split: 4
Model Type: RF
criterion: poisson
max_features: 6
min_samples_leaf: 3
min_samples_split: 2
n_estimators: 138
Model Type: KN
leaf_size: 12
n_neighbors: 10
p: 1
weights: distance
Model Type: FNN
Structural Hyperparameters
Layer: Dense_hidden_0
units: 251
sublayer: None
Layer: Dense_hidden_1
units: 184
sublayer: None
Layer: Dense_hidden_2
units: 47
sublayer: None
Layer: Dense_output_0
Compile/Fitting Hyperparameters
Adam_learning_rate: 0.0008821712781015931
batch_size: 8
Model: "FNN"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
Dense_hidden_0 (Dense) (None, 251) 2008
Dense_hidden_1 (Dense) (None, 184) 46368
Dense_hidden_2 (Dense) (None, 47) 8695
Dense_output_0 (Dense) (None, 1) 48
=================================================================
Total params: 57119 (223.12 KB)
Trainable params: 57119 (223.12 KB)
Non-trainable params: 0 (0.00 Byte)
_________________________________________________________________
Below is the network plot for the FNN.
[13]:
postprocessor.nn_network_plot(
to_file="./supporting/heat_conduction_network.png",
show_shapes=True,
show_layer_names=True,
expand_nested=True,
show_layer_activations=True,
)
[13]:
To visualize the performance of these models, we can use the pyMAISE.PostProcessor.diagonal_validation_plot functions to produce diagonal validation plots.
[14]:
models = np.array([["Linear", "Lasso"], ["DT", "KN"], ["RF", "SVM"]])
fig = plt.figure(figsize=(15, 25))
gs = fig.add_gridspec(4, 2)
for i in range(models.shape[0]):
for j in range(models.shape[1]):
ax = fig.add_subplot(gs[i, j])
ax = postprocessor.diagonal_validation_plot(model_type=models[i, j])
ax.set_title(models[i, j])
ax = fig.add_subplot(gs[3, :])
ax = postprocessor.diagonal_validation_plot(model_type="FNN")
ax.set_title("FNN")
plt.show()
The performance differences between RF/DT with the other models are apparent along with the overfitting of KN. The RF and DT predictions are closely spread along \(y=x\) while the KN test predictions are over-predicted at lower temperatures and under-predicted at higher temperatures.
Similarly, the pyMAISE.PostProcessor.validation_plot function produces validation plots showing each prediction’s absolute relative error.
[15]:
fig = plt.figure(figsize=(15, 25))
gs = fig.add_gridspec(4, 2)
for i in range(models.shape[0]):
for j in range(models.shape[1]):
ax = fig.add_subplot(gs[i, j])
ax = postprocessor.validation_plot(model_type=models[i, j])
ax.set_title(models[i, j])
ax = fig.add_subplot(gs[3, :])
ax = postprocessor.validation_plot(model_type="FNN")
ax.set_title("FNN")
plt.show()
The performance of the models is best represented by the magnitudes observed on the y-axis; however, even RF gets as high as \(>10.0\%\) error.
Finally, the most performant FNN learning curve is shown by pyMAISE.PostProcessor.nn_learning_plot.
[16]:
postprocessor.nn_learning_plot()
plt.show()
From the learning curve we can see the top performing neural network based on test \(R^2\) is slightly overfit with validation above training.