Optimization and Training Strategies
In highFIS, the model definition is decoupled from the optimization loops. This separation of concerns allows the model layers to focus purely on PyTorch forward execution, while training protocols handle batching, learning rate schedules, parameter locking/freezing, and specialized multi-phase algorithms.
All training strategies inherit from the abstract class BaseTrainer and decouple the optimization loop from the scikit-learn estimator interface.
The Trainer Architecture
Each estimator class in highFIS delegates its .fit() call to a dedicated trainer. The trainer handles the standard training loop (epoch-wise validation, early stopping, logging) as well as advanced phase transitions.
graph TD
Estimator[Estimator.fit] --> Trainer[BaseTrainer]
Trainer --> GT[GradientTrainer]
Trainer --> DGT[DGTrainer]
Trainer --> FSRET[FSRETrainer]
GT -->|Single-phase| GD[Standard Mini-batch SGD/Adam]
DGT -->|Three-phase| GateOpt[Data-Guided Antecedent & Gate Optimization]
FSRET -->|Three-phase| FSREOpt[Feature Selection & Rule Extraction Optimization]
1. GradientTrainer
The GradientTrainer implements standard single-phase mini-batch gradient descent. It is used by baseline models like TSK, HTSK, LogTSK, DombiTSK, ADMTSK, AYATSK, ADATSK, and ADPTSK.
Note: All model parameters (antecedent means/widths, rule weights, and consequent weights) are optimized simultaneously using standard PyTorch optimizers (e.g., Adam) and learning rate schedulers.
Configuration Parameters
You can configure the trainer directly through the estimator's constructor arguments:
* epochs: Total training epochs (default: 100).
* learning_rate: Step size for weight updates (default: 0.01).
* batch_size: Size of mini-batches — see Batch size below. Accepts an
integer, None for full-batch training, or "auto" (the default) to follow the
source paper's protocol for that model family.
* weight_decay: L2 regularization strength, applied to the consequent parameters
(antecedent and rule parameters are left decay-free).
* eval_metrics_every: Evaluate training metrics every n epochs (default: 1); 0
skips the training-metric pass entirely. This only affects the "train_<metric>"
entries in history_ — validation metrics, which drive early stopping, are evaluated
every epoch regardless. Raising it (or setting 0) is a cheap speed-up when you do
not need per-epoch training curves.
* scheduler_class / scheduler_params: Learning-rate schedule; see below.
Numerical precision
Models train in whatever floating-point precision is PyTorch's default when fit is
called. The default is single precision (float32); to train and predict in double
precision, set it before fitting:
import torch
torch.set_default_dtype(torch.float64)
clf.fit(X, y) # parameters, inputs and predictions are all float64
Batch size
batch_size takes three kinds of value:
| value | meaning |
|---|---|
an int |
exactly that batch size. Always wins. |
"auto" (default) |
the batch protocol of the family's source paper, resolved from the training-set size when fit runs. |
None |
full-batch training. |
The papers these models come from prescribe different protocols, so there is no single
sensible number; "auto" follows each one:
| family | policy | source |
|---|---|---|
| TSK, HTSK, LogTSK | 512, clamped to min(N, 60) when it exceeds the training set |
Cui et al. 2021 |
| HDFIS-prod / HDFIS-min | 64 |
Xue et al. 2023 |
| DombiTSK, ADMTSK | 0.1 · N |
Xue et al. 2025 |
| DG-ALETSK | 0.1 · N |
Xue et al. 2023 |
| ADPTSK | full batch below 500 samples, else 0.2 · N |
Xue et al. 2025 |
| AYATSK | full batch below 500 samples, else 0.1 · N |
Xue et al. 2025 |
| AdaTSK, FSRE-AdaTSK, DG-TSK | full batch | Xue et al. 2022 / 2023 |
| MHTSK | 64 (the paper specifies no batch size) |
— |
After fit, the size actually used is available as batch_size_, while the batch_size
parameter keeps the value you passed:
from highfis import HDFISProdClassifier
clf = HDFISProdClassifier(n_mfs=3, epochs=5, random_state=42)
clf.fit(X_train, y_train)
print("configured:", clf.batch_size, "| used:", clf.batch_size_)
Example: Training with GradientTrainer and Inspecting History
This example shows how to configure a model using standard gradient optimization, and then inspect the resulting training logs.
from sklearn.datasets import make_classification
from sklearn.preprocessing import MinMaxScaler
from highfis import HTSKClassifier
# Generate mock classification data
X, y = make_classification(n_samples=500, n_features=10, random_state=42)
X_scaled = MinMaxScaler().fit_transform(X)
# Instantiate the HTSK Classifier with custom optimization settings
clf = HTSKClassifier(
n_mfs=3,
mf_init="kmeans",
epochs=120,
learning_rate=0.005,
batch_size=64,
weight_decay=1e-5,
verbose=True,
random_state=42
)
# Fit the model (delegates training to GradientTrainer)
clf.fit(X_scaled, y)
# The training history is captured in clf.history_
print("Final training loss:", clf.history_["train_total_loss"][-1])
print("Epochs executed:", clf.history_["stopped_epoch"])
2. DGTrainer (Double-Gated Training)
Specialized for DGTSK and DGALETSK model families, the DGTrainer implements the three-phase Data-Guided (DG) training protocol. This protocol is critical for establishing sparse high-dimensional structures by systematically pruning feature and rule gates.
┌─────────────────────────────────────────┐
│ Phase 1: Antecedent & Consequent Warmup │
│ (Gate parameters are frozen) │
└────────────────────┬────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Phase 2: Gate Optimization │
│ (Antecedent parameters are frozen) │
└────────────────────┬────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Phase 3: Joint Fine-Tuning │
│ (All parameters unfrozen and optimized) │
└─────────────────────────────────────────┘
The Three Phases
- Warm-up Phase (
fit_warmup_phase): Antecedent membership functions and consequents are optimized to match the target signal. The structural gates are frozen to prevent premature pruning. - Gate Selection Phase (
fit_dg_phase): The gate parameters (\(\lambda, \theta\)) are unfrozen and optimized under an L1 or entropy penalty to induce sparsity, while antecedents remain frozen. - Joint Fine-Tuning (
fit_joint_phase): All parameters are unfrozen and optimized jointly to polish the final sparse model.
Key DGTrainer Settings
dg_epochs/finetune_epochs: Duration of the gate optimization and fine-tuning phases.dg_learning_rate/finetune_learning_rate: Separate step sizes for phase-specific updates.zeta_lambda/zeta_theta: Pruning threshold grid search options (defaults:[0.0, 0.25, 0.5, 0.75, 1.0]).use_lse: Re-estimates consequent weights using Least Squares Estimation (LSE) during search.structural_pruning: Hard-prunes the PyTorch neural network parameters of dropped rules/features to speed up forward/backward passes.
Example: Double-Gated Sparse Feature Selection
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from highfis import DGTSKClassifier
from highfis.optim import DGTrainer
# Create high-dimensional data (e.g. 50 features, only 10 informative)
X, y = make_classification(n_samples=1000, n_features=50, n_informative=10, random_state=42)
X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=42)
# Define custom three-phase trainer settings
custom_trainer = DGTrainer(
dg_epochs=30,
dg_learning_rate=0.01,
zeta_lambda=[0.1, 0.3, 0.5, 0.7], # Custom feature gate threshold grid
zeta_theta=[0.2, 0.4, 0.6], # Custom rule gate threshold grid
use_lse=True,
finetune_epochs=150,
finetune_learning_rate=0.005,
verbose=True
)
# Instantiate the estimator and assign our trainer
clf = DGTSKClassifier(
n_mfs=3,
trainer=custom_trainer,
random_state=42
)
# Train the model (automatically cycles through DG optimization, threshold search, and fine-tuning)
clf.fit(X_train, y_train, x_val=X_val, y_val=y_val)
# Introspect the pruned structural statistics (recorded during threshold search)
threshold = clf.history_["threshold"]
print("Retained Feature Indices:", threshold["surviving_feature_indices"])
print("Retained Rules:", len(threshold["surviving_rule_indices"]))
3. FSRETrainer (Feature Selection & Rule Extraction)
The FSRETrainer implements a custom three-phase optimization protocol tailored for FSRE-ADATSK estimators. It targets the simultaneous optimization of softmin aggregation parameters and the structural feature/rule gates.
- Phase 1 (Feature Selection): Focuses on determining the initial membership function layouts and optimizing feature gates \(\lambda_d\).
- Phase 2 (Rule Extraction): Locks selected features, expands the system to the full Rule Base (\(En-FRB\)), and optimizes rule gates \(\theta_r\) under pruning penalties.
- Phase 3 (Refinement/Fine-tuning): Locks the pruned architecture and updates the remaining membership parameters and consequent weights to restore accuracy.
Key FSRETrainer Settings
fs_epochs/re_epochs/finetune_epochs: Durations for the three phases.zeta_lambda: Feature pruning threshold coefficient (default:0.5). Larger values retain more features. Recommend0.4for high-dimensional datasets.zeta_theta: Rule pruning threshold coefficient (default:0.3). Recommend0.5for high-dimensional datasets.structural_pruning: Hard-prunes the structural tensors in the model to optimize execution speed.
Example: Feature Selection and Rule Extraction in Softmin TSK
from sklearn.datasets import make_classification
from highfis import FSREADATSKClassifier
from highfis.optim import FSRETrainer
# Load classification data
X, y = make_classification(n_samples=800, n_features=20, random_state=42)
# Configure the three-phase FSRE trainer
fsre_trainer = FSRETrainer(
fs_epochs=20,
fs_learning_rate=0.01,
re_epochs=20,
re_learning_rate=0.01,
finetune_epochs=100,
finetune_learning_rate=0.005,
zeta_lambda=0.4, # Pruning threshold coefficient for features
zeta_theta=0.5, # Pruning threshold coefficient for rules
verbose=True
)
# Instantiate the model with our customized FSRE training loop
clf = FSREADATSKClassifier(
n_mfs=3,
trainer=fsre_trainer,
random_state=42
)
# Fit the classifier
clf.fit(X, y)
Optimizer selection
The optimizer is chosen automatically from the model family, so there is no
optimizer_class constructor argument. ADATSK models train with SGD; ADPTSK,
ADMTSK, DombiTSK and AYATSK use Adam; the remaining single-phase models use AdamW
with weight decay applied only to the consequent parameters. The DG-TSK and
DG-ALETSK estimators additionally expose an optimizer_type argument
("sgd", "adam", or "adamw") to follow their respective papers.
To supply a fully custom optimizer instance, build a GradientTrainer yourself and
pass it via trainer= (only the DG and FSRE estimators accept a trainer= argument),
or call GradientTrainer.fit(model, x, y, optimizer=my_optimizer) directly.
Learning-rate schedules
Every estimator accepts a scheduler_class plus scheduler_params. You pass the
scheduler class, not an instance — the scheduler must bind to the optimizer, and
the optimizer is only built inside fit, so a pre-constructed scheduler would decay an
optimizer that is never stepped.
from torch.optim.lr_scheduler import StepLR
from highfis import HTSKClassifier
clf = HTSKClassifier(
epochs=150,
learning_rate=1e-2,
scheduler_class=StepLR,
scheduler_params={"step_size": 50, "gamma": 0.5},
random_state=42,
)
clf.fit(X_train, y_train)
# The realised rate for each epoch is recorded in history_.
print("Learning rate per epoch:", clf.history_["lr"])
For the multi-phase DG-TSK, DG-ALETSK and FSRE-ADATSK estimators each phase builds
its own optimizer, so the class is instantiated once per phase.
Note:
scheduler_classis a class object, whichtorch.load(weights_only=True)cannot unpickle, so it is not written into checkpoints. A model reloaded from disk is not resuming training and does not need it.
4. Training History and Metric Logging
Each trainer populates the estimator's history_ attribute after fitting the model. Internally, the optimization loop computes all metrics on the training and validation sets using pure PyTorch on the active device (e.g., CPU or CUDA GPU) to avoid expensive, synchronous data transfers to the host memory during training. Only the final scalar metrics are converted to Python floats when written to the history dictionary at the end of each epoch.
History Structure and Keys
The history_ dictionary contains the following keys depending on the trainer and parameters:
"train": The loss value computed on the training set for each epoch."ur": The Uniform Regularization loss value computed on the training set for each epoch."val": The validation loss computed on the validation set for each epoch (only present ifvalidation_dataorx_val/y_valis provided)."train_<metric>"/"val_<metric>": The epoch-wise performance of custom or default metrics."stopped_epoch": The final epoch index executed (especially useful when early stopping is triggered).
Default and Custom Metrics
If no metrics are explicitly specified, the trainers automatically configure task-appropriate defaults:
* Classification: Defaults to "accuracy" (creating "train_accuracy" and "val_accuracy" keys).
* Regression: Defaults to "mse" (creating "train_mse" and "val_mse" keys).
The "train_<metric>" keys are only present when eval_metrics_every is greater than 0
(the default is 1). The "val_<metric>" keys are present whenever a validation set is
supplied. Passing metrics=[] to .fit() disables both.
Reading history_ correctly
Each series carries one canonical name — the loss decomposition is
train_total_loss = train_loss + ur_weight * train_ur_loss:
| key | meaning |
|---|---|
train_loss |
main task loss per epoch |
train_ur_loss |
uniform-regularisation term per epoch (logged even when ur_weight=0) |
train_total_loss |
the optimised objective per epoch |
val_loss, val_<metric> |
validation series (only with a validation set) |
lr |
learning rate used in each epoch |
stopped_epoch |
number of epochs actually executed |
best_epoch |
index of the best-validation epoch, or None without validation |
!!! warning "The last row is not always the fitted model"
With a validation set and `restore_best=True` (the default), training continues past
the best epoch and the returned model is rolled back to it — so `history_[...][-1]`
describes a **different** model than the one you hold. Index with `best_epoch`
instead.
from highfis import HTSKClassifier
clf = HTSKClassifier(n_mfs=3, epochs=30, random_state=42)
clf.fit(X_train, y_train, x_val=X_val, y_val=y_val)
best = clf.history_["best_epoch"]
print("best epoch:", best)
print("last row (a discarded model):", round(clf.history_["train_accuracy"][-1], 3))
print("the returned model: ", round(clf.history_["train_accuracy"][best], 3))
best_epoch is an epoch index, so it lines up with the per-epoch series. Without a
validation set it is None — the last row is the returned model. If you set
eval_metrics_every above 1, the train_<metric> series are subsampled and no longer
indexed by epoch.
To track additional metrics, pass them as a list of strings to the .fit() method:
# Train the model while tracking accuracy and macro F1 score
clf.fit(
X_train, y_train,
x_val=X_val, y_val=y_val,
metrics=["accuracy", "f1_macro"]
)
# Inspect the captured metrics
print("Training Macro F1 per epoch:", clf.history_["train_f1_macro"])
print("Validation Accuracy per epoch:", clf.history_["val_accuracy"])
5. Uniform Regularization (UR)
In Takagi-Sugeno-Kang (TSK) fuzzy neural networks, mini-batch gradient descent can sometimes lead to rule starvation or rule dominance, where a subset of rules dominates the output while others are never activated. This reduces the generalization performance of the model.
Uniform Regularization (UR) resolves this by penalizing the deviation of the average rule activations from a target uniform distribution over each batch.
Mathematical Formulation
The Uniform Regularization loss \(L_{\text{UR}}\) added to the primary task loss is formulated as:
Where: * \(R\) is the number of fuzzy rules. * \(\bar{a}_r\) is the average normalized activation (firing strength) of rule \(r\) across all samples in the current mini-batch: $$ \bar{a}r = \frac{1}{B} \sum{b=1}^{B} \bar{w}_r(x_b) $$ * \(t\) is the target uniform activation level. Typically, this is set to \(1/R\), meaning every rule is expected to contribute equally on average.
Configuration Parameters
Uniform Regularization is configured using two hyperparameters on the estimator:
* ur_weight: The regularization strength (default: 0.0). If set to 0.0, no UR penalty is added to the loss function during backpropagation. However, the value of the metric is still computed and logged to history_["train_ur_loss"] for monitoring.
* ur_target: The target activation level \(t\) (default: None, which defaults to \(1/R\)).
Interpretation of UR Values
ur\(\approx 0.0\): The rule activations are perfectly and uniformly distributed across the batch. Every rule is contributing.ur\(> 0.5\): The rule activations are highly unbalanced. This suggests that either:- Only a few rules are doing all the work (dominating), while the others are starved (inactive).
- Many rules are activating in a highly correlated way, reducing the diversity of the rule base.
Scientific Reference
For more details on the rationale and effectiveness of Uniform Regularization, refer to:
Y. Cui, D. Wu and J. Huang, "Optimize TSK Fuzzy Systems for Classification Problems: Minibatch Gradient Descent With Uniform Regularization and Batch Normalization," in IEEE Transactions on Fuzzy Systems, vol. 28, no. 12, pp. 3065-3075, Dec. 2020, doi: 10.1109/TFUZZ.2020.2967282.