6.4. Inspecting Models#

Understanding the internals of machine learning models is essential for interpreting their behavior and gaining insights into their predictions. By inspecting the parameters and hyperparameters of a trained model, we can identify the features that have the most significant impact on the model’s output and explore how the model works. By analyzing the performance of each model across different iterations and hyperparameters, we can assess the variability across models and identify any patterns that might help interpret the model’s outputs. The ability to inspect the internals of machine learning models can help us identify the most critical features that influence the model’s predictions, understand how the model works and make informed decisions about its deployment.

In this context, we will explore how to perform model inspection in julearn. julearn provides an intuitive suite of tools for model inspection and interpretation. We will focus on how to inspect models in julearn’s nested cross-validation workflow. With these techniques, we can gain a better understanding of how the model works and identify any patterns or anomalies that could affect its performance. This knowledge can help us deploy models more effectively and interpret their outputs with greater confidence.

Let’s start by importing some useful utilities:

from pprint import pprint
import seaborn as sns
import numpy as np

from sklearn.model_selection import RepeatedKFold

from julearn import run_cross_validation
from julearn.pipeline import PipelineCreator
from julearn.utils import configure_logging

Now, let’s configure julearn’s logger to get some output as the pipeline is running and get some toy data to play with. In this example, we will use the penguin dataset, and classify the penguin species based on the continuous measures in the dataset.

configure_logging(level="INFO")

# get some data
penguins_df = sns.load_dataset("penguins")
penguins_df = penguins_df.drop(columns=["island", "sex"])
penguins_df = penguins_df.query("species != 'Chinstrap'").dropna()
penguins_df["species"] = penguins_df["species"].replace(
    {"Adelie": 0, "Gentoo": 1}
)
features = [x for x in penguins_df.columns if x != "species"]
2024-05-03 15:22:14,978 - julearn - INFO - ===== Lib Versions =====
2024-05-03 15:22:14,979 - julearn - INFO - numpy: 1.26.4
2024-05-03 15:22:14,979 - julearn - INFO - scipy: 1.13.0
2024-05-03 15:22:14,979 - julearn - INFO - sklearn: 1.4.2
2024-05-03 15:22:14,979 - julearn - INFO - pandas: 2.1.4
2024-05-03 15:22:14,979 - julearn - INFO - julearn: 0.3.2
2024-05-03 15:22:14,979 - julearn - INFO - ========================

We are going to use a fairly simple pipeline, in which we z-score the features and then apply a support vector classifier to classify species.

# create model
pipeline_creator = PipelineCreator(problem_type="classification", apply_to="*")
pipeline_creator.add("zscore")
pipeline_creator.add("svm", kernel="linear", C=np.geomspace(1e-2, 1e2, 5))
print(pipeline_creator)
2024-05-03 15:22:15,440 - julearn - INFO - Adding step zscore that applies to ColumnTypes<types={'*'}; pattern=.*>
2024-05-03 15:22:15,440 - julearn - INFO - Step added
2024-05-03 15:22:15,440 - julearn - INFO - Adding step svm that applies to ColumnTypes<types={'*'}; pattern=.*>
2024-05-03 15:22:15,440 - julearn - INFO - Setting hyperparameter kernel = linear
2024-05-03 15:22:15,440 - julearn - INFO - Tuning hyperparameter C = [1.e-02 1.e-01 1.e+00 1.e+01 1.e+02]
2024-05-03 15:22:15,440 - julearn - INFO - Step added
PipelineCreator:
  Step 0: zscore
    estimator:     StandardScaler()
    apply to:      ColumnTypes<types={'*'}; pattern=.*>
    needed types:  ColumnTypes<types={'*'}; pattern=.*>
    tuning params: {}
  Step 1: svm
    estimator:     SVC(kernel='linear')
    apply to:      ColumnTypes<types={'*'}; pattern=.*>
    needed types:  ColumnTypes<types={'*'}; pattern=.*>
    tuning params: {'svm__C': array([1.e-02, 1.e-01, 1.e+00, 1.e+01, 1.e+02])}

Once this is set up, we can simply call julearn’s run_cross_validation(). Notice, how we set the return_inspector parameter to True. Importantly, we also have to set the return_estimator parameter to "all". This is because julearn’s Inspector extracts all relevant information from estimators after the pipeline has been run. The pipeline will take a few minutes in our example:

scores, final_model, inspector = run_cross_validation(
    X=features,
    y="species",
    data=penguins_df,
    model=pipeline_creator,
    seed=200,
    cv=RepeatedKFold(n_repeats=10, n_splits=5, random_state=200),
    return_estimator="all",
    return_inspector=True,
)
2024-05-03 15:22:15,441 - julearn - INFO - Setting random seed to 200
2024-05-03 15:22:15,442 - julearn - INFO - ==== Input Data ====
2024-05-03 15:22:15,442 - julearn - INFO - Using dataframe as input
2024-05-03 15:22:15,442 - julearn - INFO -      Features: ['bill_length_mm', 'bill_depth_mm', 'flipper_length_mm', 'body_mass_g']
2024-05-03 15:22:15,442 - julearn - INFO -      Target: species
2024-05-03 15:22:15,442 - julearn - INFO -      Expanded features: ['bill_length_mm', 'bill_depth_mm', 'flipper_length_mm', 'body_mass_g']
2024-05-03 15:22:15,442 - julearn - INFO -      X_types:{}
2024-05-03 15:22:15,442 - julearn - WARNING - The following columns are not defined in X_types: ['bill_length_mm', 'bill_depth_mm', 'flipper_length_mm', 'body_mass_g']. They will be treated as continuous.
/home/runner/work/julearn/julearn/julearn/prepare.py:505: RuntimeWarning: The following columns are not defined in X_types: ['bill_length_mm', 'bill_depth_mm', 'flipper_length_mm', 'body_mass_g']. They will be treated as continuous.
  warn_with_log(
2024-05-03 15:22:15,443 - julearn - INFO - ====================
2024-05-03 15:22:15,443 - julearn - INFO -
2024-05-03 15:22:15,444 - julearn - INFO - = Model Parameters =
2024-05-03 15:22:15,444 - julearn - INFO - Tuning hyperparameters using grid
2024-05-03 15:22:15,444 - julearn - INFO - Hyperparameters:
2024-05-03 15:22:15,444 - julearn - INFO -      svm__C: [1.e-02 1.e-01 1.e+00 1.e+01 1.e+02]
2024-05-03 15:22:15,444 - julearn - INFO - Using inner CV scheme KFold(n_splits=5, random_state=None, shuffle=False)
2024-05-03 15:22:15,445 - julearn - INFO - Search Parameters:
2024-05-03 15:22:15,445 - julearn - INFO -      cv: KFold(n_splits=5, random_state=None, shuffle=False)
2024-05-03 15:22:15,445 - julearn - INFO - ====================
2024-05-03 15:22:15,445 - julearn - INFO -
2024-05-03 15:22:15,445 - julearn - INFO - = Data Information =
2024-05-03 15:22:15,445 - julearn - INFO -      Problem type: classification
2024-05-03 15:22:15,445 - julearn - INFO -      Number of samples: 274
2024-05-03 15:22:15,445 - julearn - INFO -      Number of features: 4
2024-05-03 15:22:15,445 - julearn - INFO - ====================
2024-05-03 15:22:15,445 - julearn - INFO -
2024-05-03 15:22:15,445 - julearn - INFO -      Number of classes: 2
2024-05-03 15:22:15,445 - julearn - INFO -      Target type: int64
2024-05-03 15:22:15,446 - julearn - INFO -      Class distributions: species
0    151
1    123
Name: count, dtype: int64
2024-05-03 15:22:15,446 - julearn - INFO - Using outer CV scheme RepeatedKFold(n_repeats=10, n_splits=5, random_state=200)
2024-05-03 15:22:15,446 - julearn - INFO - Binary classification problem detected.
2024-05-03 15:22:31,430 - julearn - INFO - Fitting final model

After this is done, we can now use the inspector to look at final model parameters, but also at the parameters of individual models from each fold of the cross-validation. The final model can be inspected using the .model attribute. For example to get a quick overview over the model parameters, run:

# remember to actually import pprint as above, or just print out using print
pprint(inspector.model.get_params())
{'cv': KFold(n_splits=5, random_state=None, shuffle=False),
 'error_score': nan,
 'estimator': Pipeline(steps=[('set_column_types', SetColumnTypes(X_types={})),
                ('zscore',
                 JuColumnTransformer(apply_to=ColumnTypes<types={'*'}; pattern=.*>,
                                     copy=True, name='zscore',
                                     transformer=StandardScaler(),
                                     with_mean=True, with_std=True)),
                ('svm',
                 WrapModel(C=1.0, apply_to=ColumnTypes<types={'*'}; pattern=.*>,
                           break_ties=False, cache_size=200, class_weight=None,
                           coef0=0.0, decision_function_shape='ovr', degree=3,
                           gamma='scale', kernel='linear', max_iter=-1,
                           model=SVC(kernel='linear'), probability=False,
                           random_state=None, shrinking=True, tol=0.001,
                           verbose=False))]),
 'estimator__memory': None,
 'estimator__set_column_types': SetColumnTypes(X_types={}),
 'estimator__set_column_types__X_types': {},
 'estimator__set_column_types__row_select_col_type': None,
 'estimator__set_column_types__row_select_vals': None,
 'estimator__steps': [('set_column_types', SetColumnTypes(X_types={})),
                      ('zscore',
                       JuColumnTransformer(apply_to=ColumnTypes<types={'*'}; pattern=.*>, copy=True,
                    name='zscore', transformer=StandardScaler(), with_mean=True,
                    with_std=True)),
                      ('svm',
                       WrapModel(C=1.0, apply_to=ColumnTypes<types={'*'}; pattern=.*>,
          break_ties=False, cache_size=200, class_weight=None, coef0=0.0,
          decision_function_shape='ovr', degree=3, gamma='scale',
          kernel='linear', max_iter=-1, model=SVC(kernel='linear'),
          probability=False, random_state=None, shrinking=True, tol=0.001,
          verbose=False))],
 'estimator__svm': WrapModel(C=1.0, apply_to=ColumnTypes<types={'*'}; pattern=.*>,
          break_ties=False, cache_size=200, class_weight=None, coef0=0.0,
          decision_function_shape='ovr', degree=3, gamma='scale',
          kernel='linear', max_iter=-1, model=SVC(kernel='linear'),
          probability=False, random_state=None, shrinking=True, tol=0.001,
          verbose=False),
 'estimator__svm__C': 1.0,
 'estimator__svm__apply_to': ColumnTypes<types={'*'}; pattern=.*>,
 'estimator__svm__break_ties': False,
 'estimator__svm__cache_size': 200,
 'estimator__svm__class_weight': None,
 'estimator__svm__coef0': 0.0,
 'estimator__svm__decision_function_shape': 'ovr',
 'estimator__svm__degree': 3,
 'estimator__svm__gamma': 'scale',
 'estimator__svm__kernel': 'linear',
 'estimator__svm__max_iter': -1,
 'estimator__svm__model': SVC(kernel='linear'),
 'estimator__svm__needed_types': None,
 'estimator__svm__probability': False,
 'estimator__svm__random_state': None,
 'estimator__svm__shrinking': True,
 'estimator__svm__tol': 0.001,
 'estimator__svm__verbose': False,
 'estimator__verbose': False,
 'estimator__zscore': JuColumnTransformer(apply_to=ColumnTypes<types={'*'}; pattern=.*>, copy=True,
                    name='zscore', transformer=StandardScaler(), with_mean=True,
                    with_std=True),
 'estimator__zscore__apply_to': ColumnTypes<types={'*'}; pattern=.*>,
 'estimator__zscore__copy': True,
 'estimator__zscore__name': 'zscore',
 'estimator__zscore__needed_types': None,
 'estimator__zscore__row_select_col_type': None,
 'estimator__zscore__row_select_vals': None,
 'estimator__zscore__transformer': StandardScaler(),
 'estimator__zscore__with_mean': True,
 'estimator__zscore__with_std': True,
 'n_jobs': None,
 'param_grid': {'svm__C': array([1.e-02, 1.e-01, 1.e+00, 1.e+01, 1.e+02])},
 'pre_dispatch': '2*n_jobs',
 'refit': True,
 'return_train_score': False,
 'scoring': None,
 'verbose': 0}

This will print out a dictionary containing all the parameters of the final selected estimator. Similarly, we can also get an overview of the fitted parameters:

pprint(inspector.model.get_fitted_params())
{'set_column_types__column_mapper_': {'bill_depth_mm': 'bill_depth_mm__:type:__continuous',
                                      'bill_length_mm': 'bill_length_mm__:type:__continuous',
                                      'body_mass_g': 'body_mass_g__:type:__continuous',
                                      'flipper_length_mm': 'flipper_length_mm__:type:__continuous'},
 'set_column_types__feature_names_in_': Index(['bill_length_mm', 'bill_depth_mm', 'flipper_length_mm', 'body_mass_g'], dtype='object'),
 'svm__model_': SVC(C=0.01, kernel='linear'),
 'zscore__column_transformer_': ColumnTransformer(remainder='passthrough',
                  transformers=[('zscore', StandardScaler(),
                                 <julearn.base.column_types.make_type_selector object at 0x7fd4866bea70>)],
                  verbose_feature_names_out=False),
 'zscore__feature_names_in_': array(['bill_length_mm__:type:__continuous',
       'bill_depth_mm__:type:__continuous',
       'flipper_length_mm__:type:__continuous',
       'body_mass_g__:type:__continuous'], dtype=object),
 'zscore__mean_': array([  42.70291971,   16.83613139,  202.17883212, 4318.06569343]),
 'zscore__n_features_in_': 4,
 'zscore__n_samples_seen_': 274,
 'zscore__scale_': array([  5.18607683,   2.00973207,  15.02045287, 834.40628575]),
 'zscore__var_': array([2.68953929e+01, 4.03902299e+00, 2.25614004e+02, 6.96233850e+05])}

Again, this will print out quite a lot. What if we just want to look at a specific parameter? Well, this somewhat depends on the underlying structure and attributes of the used estimators or transformers, and will likely require some interactive exploring. But the inspector makes it quite easy to interactively explore your final model. For example, to see which sample means were used to z-score features in the final model we can run:

print(inspector.model.get_fitted_params()["zscore__mean_"])
[  42.70291971   16.83613139  202.17883212 4318.06569343]

In addition, sometimes it can be very useful to know what predictions were made in each individual train-test split of the cross-validation. This is where the .folds attribute comes in handy. This attribute has a .predict() method, that makes it very easy to display the predictions made for each sample in each test fold and in each repeat of the cross-validation. It will display a DataFrame with each row corresponding to a sample, and each column corresponding to a repeat of the cross-validation. Simply run:

fold_predictions = inspector.folds.predict()
print(fold_predictions)
     repeat0_p0  repeat1_p0  repeat2_p0  ...  repeat8_p0  repeat9_p0  target
0             0           0           0  ...           0           0       0
1             0           0           0  ...           0           0       0
2             0           0           0  ...           0           0       0
3             0           0           0  ...           0           0       0
4             0           0           0  ...           0           0       0
..          ...         ...         ...  ...         ...         ...     ...
269           1           1           1  ...           1           1       1
270           1           1           1  ...           1           1       1
271           1           1           1  ...           1           1       1
272           1           1           1  ...           1           1       1
273           1           1           1  ...           1           1       1

[274 rows x 11 columns]

This .folds attribute is actually an iterator, that can iterate over every single fold used in the cross-validation, and it yields an instance of a FoldsInspector, which can then be used to explore each model that was fitted during cross-validation. For example, we can collect the C parameters that were selected in each outer fold of our nested cross-validation. That way, we can assess the amount of variance on that particular parameter across folds:

c_values = []
for fold_inspector in inspector.folds:
    fold_model = fold_inspector.model
    c_values.append(
        fold_model.get_fitted_params()["svm__model_"].get_params()["C"]
    )

By printing out the unique values in the c_values list, we realize, that actually there was not much variance across models. In fact, there was only one parameter value ever selected. This may indicate that this is in fact the optimal value, or it may indicate that there is a potential problem with our search grid.

print(set(c_values))
{0.01}

As you can see the inspector provides you with a set of powerful tools to look at what exactly happened in your pipeline and the performance evaluation. It may help you better interpret your models, understand your results and identify problems if there are any. By leveraging these tools, you can gain deeper insights, interpret your models effectively, and address any issues that may arise. Model inspection serves as a valuable asset in the deployment of machine learning models, ensuring transparency, interpretability, and reliable decision-making. With julearn’s model inspection capabilities, you can confidently navigate the complexities of machine learning models and harness their full potential in real-world applications.

Total running time of the script: (0 minutes 16.989 seconds)