Introspection and Persistence
Fuzzy systems provide a major advantage over black-box deep learning architectures: their internal decision-making structures are fully interpretable. highFIS provides dedicated utilities for model introspection (extracting membership parameters, rule tables, and feature importance) and a versioned, secure persistence mechanism.
1. Model Introspection
Fitted estimators can be introspected to analyze and explain their decision rules. The base estimator interface exposes:
summary()— a printed summary of the structure and of the training;inspect()— a high-level summary dictionary of the fitted model;get_mf_params()— the membership-function parameters per input feature;feature_importance()— a normalized importance vector derived from the consequent weights;rule_activation(X)— the normalized rule firing strengths for given inputs.firing_diagnostics(X)— a numerical summary of how the rules fire on given inputs.explain(x)— how the rules produce the prediction of one sample;coverage(X)— how well the rules cover each sample.
A Printed Summary
summary() returns a text that describes the fitted model: its structure, the training
configuration that fit resolved, and how the training ended.
For an HTSKClassifier fitted on the Wine data:
HTSKClassifier
task classification, 3 classes
features 13 seen in fit, 13 used by the model
rules 3 (rule base: coco)
fuzzy sets 3 per feature, GaussianMF
firing strength RuleLayer, T-norm GMeanTNorm
defuzzifier SoftmaxLogDefuzzifier
consequents ClassificationConsequentLayer
parameters 204 trainable (78 antecedent, 126 consequent)
Training
batch size 60
epochs 100 epochs, learning rate 0.01, training loss 1.0056 -> 0.0045, training accuracy 1.0000
The learning rate and the batch size are the values used, also when the estimator was
built with "auto". The families that train in phases (DG-TSK, DG-ALETSK, FSRE-ADATSK)
have one line per phase, and "features used by the model" counts the features they kept.
High-level summary
inspect() returns a dictionary describing the fitted structure. Its keys are
n_rules, n_inputs, feature_names, rule_base, defuzzifier_type, mf_params,
and rule_table.
summary = clf.inspect()
print("Rules:", summary["n_rules"])
print("Features:", summary["feature_names"])
print("Rule base:", summary["rule_base"])
Membership Function Parameters
get_mf_params() returns a serializable dictionary mapping each input feature to a
list of its membership-function configurations. Each entry has a type key naming the
MF class, plus that type's own parameter keys (for example a GaussianMF adds mean
and sigma). The same dictionary is available as summary["mf_params"].
mf_params = clf.get_mf_params()
for feature, mfs in mf_params.items():
print(f"Feature: {feature}")
for i, mf in enumerate(mfs):
params = {k: v for k, v in mf.items() if k != "type"}
print(f" MF {i}: {mf['type']} -> {params}")
The Rule Base Table
The antecedent rule structure is available as summary["rule_table"]: a list of
dictionaries, one per rule. Each dictionary carries a rule_id and maps every input
feature to the index of the membership function it uses in that rule.
summary = clf.inspect()
for rule in summary["rule_table"]:
rule_id = rule["rule_id"]
antecedents = [f"{feat} is MF_{rule[feat]}" for feat in summary["feature_names"]]
print(f"Rule {rule_id}: IF {' AND '.join(antecedents)} THEN [consequent]")
The same table is returned directly by clf.get_rule_table().
Rules as Text
rules_as_text() writes the rule base as IF ... THEN ... sentences. The fuzzy sets of
each feature are named from the order of their centres (low, medium, high for three
sets), and the consequent of each rule is its linear function of the features, per class
for a classifier.
top_featureslimits each rule to the conditions on the most important features and to the coefficients with the largest magnitude;Noneshows everything.labels=["cold", "mild", "hot"]replaces the default names for the features that have that many sets.max_ruleslimits the number of rules written.- Feature names are used when the model was fitted on a pandas
DataFrame; class names come fromclasses_.
The coefficients apply to the features as they were passed to fit, so they are on the
scaled features when the data were scaled first.
Explaining One Prediction
explain(x) describes how the rules produce the prediction of one sample. The output of
a TSK system is the sum, over the rules, of the normalized weight of the rule times the
value of its consequent, so each rule has a contribution that can be read on its own.
For the model above and one test sample:
Prediction: class_2
Output, the sum of the contributions of the 3 rules: class_0: -3.26, class_1: -3.12, class_2: +3.38
Rule 1, weight 0.99: contributes class_0: -3.24, class_1: -3.13, class_2: +3.41
IF x1 is medium AND x2 is high AND x10 is high AND ... (10 more)
Rule 0, weight 0.01: contributes class_0: -0.02, class_1: +0.02, class_2: -0.02
IF x1 is low AND x2 is low AND x10 is low AND ... (10 more)
1 other rule, weight 0.00 in all: contributes class_0: +0.01, class_1: -0.01, class_2: -0.01
- For a classifier the output has one score per class, and the predicted class is the one with the largest score. For a regressor it is the predicted value.
top_rulessets how many rules are described, from the largest weight; the others are summed in one line.Nonedescribes every rule.top_featuresandlabelswork as inrules_as_text().
Feature Importance
feature_importance() returns a normalized vector (summing to 1) that ranks the input
features by their contribution to the consequent, or None when the model has no
first-order consequent to read it from.
importance = clf.feature_importance()
if importance is not None:
for feat, score in zip(clf.inspect()["feature_names"], importance):
print(f"{feat}: {score:.3f}")
Consequent Parameters
A first-order rule computes score_r^c(x) = b_{r,c} + sum_d w_{r,c,d} x_d. Both halves are
available on the estimator as NumPy arrays: get_consequent_weights() returns w and
get_consequent_bias() returns the intercept b.
weights = clf.get_consequent_weights() # (rules, classes, features)
bias = clf.get_consequent_bias() # (rules, classes)
if weights is not None and bias is not None:
print("rule 0, class 0 intercept:", float(bias[0, 0]))
print("rule 0, class 0 slopes:", weights[0, 0].tolist())
Rule-Firing Diagnostics
The normalized rule weights say how each prediction is shared among the rules. When they
stop depending on the sample, the rules no longer partition the input space and the model
is a single linear model, whatever its number of rules. firing_diagnostics(X) measures
this:
| Entry | Meaning |
|---|---|
uniform_fraction |
Fraction of samples on which every rule has the same weight. This is what the underflow of a product of many membership degrees produces. |
dominated_fraction |
Fraction of samples on which one rule has at least 99% of the weight (dominance argument). |
effective_rules, effective_rules_min |
Mean and minimum over the samples of the exponential of the entropy of the weights: 1 when one rule decides, n_rules when all weigh the same. |
mean_firing |
Mean weight of each rule. |
never_firing_rules |
Indices of the rules whose weight stays below never (default 1e-6) on every sample. |
non_finite_fraction |
Fraction of samples with a weight that is not finite. |
n_samples, n_rules |
Size of the weights that were summarized. |
A high dominated_fraction is not a defect in itself: it means the rules split the input
space sharply. A high uniform_fraction is one.
import numpy as np
from highfis import HTSKClassifier, TSKClassifier
rng = np.random.default_rng(0)
X_wide = rng.random((60, 2000)).astype(np.float32)
y_wide = (X_wide[:, 0] > 0.5).astype(int)
product = TSKClassifier(n_mfs=3, epochs=5, random_state=0).fit(X_wide, y_wide) # warns
print(product.firing_diagnostics(X_wide)["uniform_fraction"]) # 1.0
stable = HTSKClassifier(n_mfs=3, epochs=5, random_state=0).fit(X_wide, y_wide)
print(stable.firing_diagnostics(X_wide)["uniform_fraction"]) # 0.0
At the end of fit, the same check runs on the training data (at most 2000 rows) and a
highfis.DegenerateFiringWarning is raised when every rule has the same weight on more
than half of the samples. One rule taking almost all the weight raises no warning: some
families concentrate the weight on high-dimensional data, and dominated_fraction and
mean_firing show it when you ask. Models with a single rule are not checked. To silence
the warning:
import warnings
from highfis import DegenerateFiringWarning
warnings.simplefilter("ignore", DegenerateFiringWarning)
Coverage
coverage(X) says, for each sample, how well the best-matching rule covers it. The
match of a sample with a rule is the mean of its membership degrees in the fuzzy sets of
the rule: 1 at the centre of every set, close to 0 far from all of them. It does not
depend on the number of features nor on the T-norm of the family.
import numpy as np
inside = clf.coverage(X_train)
print(f"training data: min {inside.min():.2f}, median {np.median(inside):.2f}")
print(f"shifted data: median {np.median(clf.coverage(X_test + 2.0)):.2f}")
What to look for: compare new samples with the training data. A sample whose coverage is well below the lowest values seen in training lies outside the region the rules were built on, and the model extrapolates there. The families whose membership function has a positive lower bound (ADPTSK, ADMTSK, DombiTSK, AYATSK) cannot go below that bound.
Selected Features and Gates
DG-TSK, DG-ALETSK and FSRE-ADATSK switch features and rules off with gates and then remove them from the model. Three accessors describe the result:
selected_features_: the column indices, in the data seen infit, of the features the model uses. For the other families it lists every column.get_feature_gates(): one gate value in[0, 1]per selected feature.get_rule_gates(): one gate value in[0, 1]per rule.
The two gate accessors return None for families without gates.
import numpy as np
from highfis import DGTSKClassifier
rng = np.random.default_rng(0)
X_many = rng.random((120, 12))
y_many = (X_many[:, 0] + X_many[:, 3] > 1.0).astype(int)
gated = DGTSKClassifier(n_mfs=3, dg_epochs=10, finetune_epochs=10, random_state=0).fit(X_many, y_many)
print("features kept:", gated.selected_features_.tolist())
print("rules kept:", len(gated.get_rule_gates()))
2. Model Persistence
highFIS features a native, versioned checkpointing mechanism built on top of PyTorch's serialization engine. Rather than relying on Python pickle (which is vulnerable to security exploits and sensitive to package directory shifts), highFIS serialization isolates structural parameters and weights. It uses weights_only=True PyTorch loading, so loading a checkpoint does not execute code.
A checkpoint stores the estimator alone. To keep the preprocessing together with the model, put both in a scikit-learn Pipeline and save the pipeline with joblib; see Save a pipeline and use it on new data.
Warning:
joblibandpicklecan execute arbitrary code when loading. Use them only for files you created or that come from a source you trust.
Saving a Model
Fitted estimators (both classifiers and regressors) expose a .save(path) method:
from highfis import HTSKClassifier
# Fit the classifier
clf = HTSKClassifier(n_mfs=3, random_state=42)
clf.fit(X_train, y_train)
# Save checkpoint to a file
clf.save("models/htsk_iris.pt")
Loading a Model
To restore a saved estimator, call the .load(path) classmethod on the corresponding estimator class:
from highfis import HTSKClassifier
# Load and restore the estimator state
loaded_clf = HTSKClassifier.load("models/htsk_iris.pt")
# Predict using the restored estimator
predictions = loaded_clf.predict(X_test)
Checkpoint Validation and Versioning
Behind the scenes, highFIS validates every checkpoint payload. The loader verifies:
1. Format Identifier: Verifies that the file is a valid highFIS payload.
2. Format Version: Ensures backward compatibility by validating the schema version.
3. Class Matching: Prevents restoring a checkpoint created by a different class (e.g., trying to load a regressor checkpoint into a classifier class).
4. Schema Completeness: Validates that all critical components (estimator_params, model_init, model_state_dict, and fitted_attrs) are present before reconstructing the estimator.