Coverage for anfis_toolbox / optim / hybrid.py: 100%
52 statements
« prev ^ index » next coverage.py v7.13.3, created at 2026-02-05 18:47 -0300
« prev ^ index » next coverage.py v7.13.3, created at 2026-02-05 18:47 -0300
1from __future__ import annotations
3import logging
4from dataclasses import dataclass
6import numpy as np
8from ..losses import MSELoss
9from ..model import TSKANFIS
10from .base import BaseTrainer, ModelLike
13@dataclass
14class HybridTrainer(BaseTrainer):
15 """Original Jang (1993) hybrid training: LSM for consequents + GD for antecedents.
17 Notes:
18 This trainer assumes a single-output regression head. It is not compatible with
19 :class:`~anfis_toolbox.model.TSKANFISClassifier` or the high-level
20 :class:`~anfis_toolbox.classifier.ANFISClassifier` facade.
21 """
23 learning_rate: float = 0.01
24 epochs: int = 100
25 verbose: bool = False
26 _loss_fn: MSELoss = MSELoss()
28 def init_state(self, model: ModelLike, X: np.ndarray, y: np.ndarray) -> None:
29 """Hybrid trainer doesn't maintain optimizer state; returns None."""
30 self._require_regression_model(model)
31 return None
33 def train_step(self, model: ModelLike, Xb: np.ndarray, yb: np.ndarray, state: None) -> tuple[float, None]:
34 """Perform one hybrid step on a batch and return (loss, state).
36 Equivalent to one iteration of the hybrid algorithm on the given batch.
37 """
38 model = self._require_regression_model(model)
39 Xb, yb = self._prepare_training_data(model, Xb, yb)
40 # Forward to get normalized weights
41 normalized_weights = model.forward_antecedents(Xb)
43 # Build LSM system for batch
44 ones_col = np.ones((Xb.shape[0], 1), dtype=float)
45 x_bar = np.concatenate([Xb, ones_col], axis=1)
46 A_blocks = [normalized_weights[:, j : j + 1] * x_bar for j in range(model.n_rules)]
47 A = np.concatenate(A_blocks, axis=1)
48 try:
49 regularization = 1e-6 * np.eye(A.shape[1])
50 ATA_reg = A.T @ A + regularization
51 theta = np.linalg.solve(ATA_reg, A.T @ yb.flatten())
52 except np.linalg.LinAlgError:
53 logging.getLogger(__name__).warning("Matrix singular in LSM, using pseudo-inverse")
54 theta = np.linalg.pinv(A) @ yb.flatten()
55 model.consequent_layer.parameters = theta.reshape(model.n_rules, model.n_inputs + 1)
57 # Loss and backward for antecedents only
58 y_pred = model.consequent_layer.forward(Xb, normalized_weights)
59 loss = self._loss_fn.loss(yb, y_pred)
60 dL_dy = self._loss_fn.gradient(yb, y_pred)
61 dL_dnorm_w, _ = model.consequent_layer.backward(dL_dy)
62 dL_dw = model.normalization_layer.backward(dL_dnorm_w)
63 gradients = model.rule_layer.backward(dL_dw)
64 model.membership_layer.backward(gradients)
65 model.update_membership_parameters(self.learning_rate)
66 return float(loss), state
68 def compute_loss(self, model: ModelLike, X: np.ndarray, y: np.ndarray) -> float:
69 """Compute the hybrid MSE loss on prepared data without side effects."""
70 model = self._require_regression_model(model)
71 X_arr, y_arr = self._prepare_validation_data(model, X, y)
72 normalized_weights = model.forward_antecedents(X_arr)
73 preds = model.consequent_layer.forward(X_arr, normalized_weights)
74 return float(self._loss_fn.loss(y_arr, preds))
76 @staticmethod
77 def _require_regression_model(model: ModelLike) -> TSKANFIS:
78 if not isinstance(model, TSKANFIS):
79 raise TypeError("HybridTrainer supports TSKANFIS regression models only")
80 return model