Skip to content

bulanik.anfis

anfis

Grid-partition first-order Sugeno ANFIS (Jang 1993) with hybrid or gradient training.

ANFIS fits a first-order Sugeno system to numeric data. Every input has its own Gaussian or generalized-bell membership functions; the rules are the grid of all combinations of one MF per input (in itertools.product order); AND is the product; and rule \(r\) has a linear consequent \(p_r \cdot x + r_r\). For an input vector \(x\) the output is

\[f(x) = \sum_r \bar w_r(x)\,(p_r \cdot x + r_r), \qquad \bar w_r = \frac{w_r}{\sum_s w_s}, \qquad w_r = \prod_i \mu_{i, k_i(r)}(x_i),\]

with \(\mu(x) = \exp\bigl(-(x - c)^2 / (2\sigma^2)\bigr)\) for gaussian and \(\mu(x) = 1 / (1 + |(x - c)/a|^{2b})\) for bell. After training, to_sugeno exports an equivalent SugenoFIS with and_method="prod".

Firing strengths are computed in the log domain (\(\log w_r = \sum_i \log \mu\), then a max-shifted softmax), so far-away samples never give 0/0. The result equals the plain ratio \(w_r / \sum w\) up to rounding, which is what the exported SugenoFIS computes.

Training is deterministic; there is no random initialisation:

  • hybrid: least squares (ridge normal equation, pure Python) for all consequents, then one batch gradient step on the premise parameters;
  • gradient: one batch gradient step on all parameters per epoch.

Guard: after every step widths (Gaussian \(\sigma\), bell \(a\)) are floored at MIN_WIDTH * span of their input and bell \(b\) at MIN_B. A step that would raise the training RMSE, or make it or any parameter non-finite, is halved until it does not, and dropped after 60 halvings, so lr is the largest step tried and not the step taken. A fit that dropped any step warns once (RuntimeWarning) with the count.

The least-squares step works on inputs normalised to \([0, 1]\) over each input's range and maps the solution back, so it does not depend on the input offset or scale. The gradient step lr is not scale-invariant.

predict outside the trained ranges extrapolates and warns (ExtrapolationWarning).

Examples:

>>> from bulanik.anfis import ANFIS
>>> X = [[i / 10] for i in range(21)]
>>> y = [x[0] ** 2 for x in X]
>>> model = ANFIS(1, 3, ranges=[(0, 2)])
>>> history = model.fit(X, y, epochs=5)
>>> history[-1] < 0.02
True
>>> round(model.predict([[1.0]])[0], 1)
1.0
References

Jang (1993): "ANFIS: adaptive-network-based fuzzy inference system", IEEE Transactions on Systems, Man, and Cybernetics.

MIN_WIDTH module-attribute

MIN_WIDTH = 0.001

Training floors a width (Gaussian sigma, bell a) at MIN_WIDTH * (hi - lo) of its input.

MIN_B module-attribute

MIN_B = 0.01

Training floors the bell exponent b at MIN_B.

ExtrapolationWarning

Bases: RuntimeWarning

ANFIS.predict was asked about inputs outside the trained ranges.

A RuntimeWarning subclass, emitted once per call and naming the first offending sample. The prediction is still returned (the consequents extrapolate linearly, so it can be far off); the SugenoFIS exported by to_sugeno instead rejects such inputs.

Examples:

>>> import warnings
>>> from bulanik.anfis import ANFIS, ExtrapolationWarning
>>> model = ANFIS(1, 2, ranges=[(0, 2)])
>>> with warnings.catch_warnings(record=True) as caught:
...     warnings.simplefilter("always")
...     _ = model.predict([[3.0]])
>>> caught[0].category is ExtrapolationWarning
True

ANFIS

ANFIS(n_inputs: int, n_mfs: int | Sequence[int] = 2, *, mf: str = 'gaussian', ranges: Sequence[tuple[float, float]] | None = None, allow_large: bool = False)

Grid-partition first-order Sugeno ANFIS (Jang 1993).

The structure and the formulas are given in the module description. Initial centres are linspace(lo, hi, n) for each input with \(n\) MFs; Gaussian \(\sigma\) and bell \(a\) are \((hi - lo) / (2 (n - 1))\) (or \((hi - lo) / 2\) for one MF), bell \(b\) is 2, and all consequents are 0. The history attribute holds the RMSE per epoch of the last fit.

More than 27 rules emits a UserWarning (training cost grows with the rule count); more than 1024 raises ValueError unless allow_large=True.

Parameters:

Name Type Description Default
n_inputs int

Number of inputs, an integer of at least 1.

required
n_mfs int | Sequence[int]

Membership functions per input: one integer for every input, or one per input.

2
mf str

Membership function family, "gaussian" or "bell".

'gaussian'
ranges Sequence[tuple[float, float]] | None

Each input's (lo, hi) with lo < hi. When None they are taken from the data at the first fit.

None
allow_large bool

Allow more than 1024 grid rules.

False

Raises:

Type Description
ValueError

If n_inputs is not an integer of at least 1, n_mfs is not a positive integer (or n_inputs of them), mf is unknown, ranges has the wrong length or an invalid pair, or the grid has more than 1024 rules without allow_large.

Attributes:

Name Type Description
history list[float]

The training RMSE after each epoch of the last fit (empty before the first).

Examples:

>>> from bulanik.anfis import ANFIS
>>> model = ANFIS(2, [2, 3], mf="bell", ranges=[(0, 1), (0, 5)])
>>> model.n_inputs, model.n_rules, model.mf
(2, 6, 'bell')
>>> ANFIS(11, 3)
Traceback (most recent call last):
...
ValueError: grid partition gives 177147 rules (> 1024); pass allow_large=True to build it anyway
References

Jang (1993): "ANFIS: adaptive-network-based fuzzy inference system", IEEE Transactions on Systems, Man, and Cybernetics.

n_inputs property

n_inputs: int

Number of inputs.

n_rules property

n_rules: int

Number of grid rules, the product of the MF counts.

mf property

mf: str

Membership function family: "gaussian" or "bell".

ranges property

ranges: list[tuple[float, float]] | None

Each input's (lo, hi), or None before the first fit without ranges.

Examples:

>>> from bulanik.anfis import ANFIS
>>> ANFIS(1, 2).ranges is None
True
>>> ANFIS(1, 2, ranges=[(0, 1)]).ranges
[(0.0, 1.0)]

param_layout

param_layout() -> list[tuple[str, int, int]]

A (kind, input, index) tag per parameter, in get_params order.

Premise parameters come first, input by input and MF by MF: Gaussian ("c", i, k), ("sigma", i, k); bell ("a", i, k), ("b", i, k), ("c", i, k) for input i and MF k. Consequents follow, rule by rule: ("p", i, r) for the coefficient of input i in rule r, then ("r", -1, r) for its constant.

Returns:

Type Description
list[tuple[str, int, int]]

A list with one tag per parameter.

Examples:

>>> from bulanik.anfis import ANFIS
>>> ANFIS(1, 2).param_layout()
[('c', 0, 0), ('sigma', 0, 0), ('c', 0, 1), ('sigma', 0, 1), ('p', 0, 0), ('r', -1, 0), ('p', 0, 1), ('r', -1, 1)]

get_params

get_params() -> list[float]

All parameters as a flat list.

Premise parameters (input-major, MF-major) come first, then the consequents (rule-major, [p_1, ..., p_n, r] per rule); see param_layout.

Returns:

Type Description
list[float]

The parameter values, a copy.

Raises:

Type Description
RuntimeError

If the input ranges are not known yet (pass ranges or call fit first).

Examples:

>>> from bulanik.anfis import ANFIS
>>> ANFIS(1, 2, ranges=[(0, 2)]).get_params()
[0.0, 1.0, 2.0, 1.0, 0.0, 0.0, 0.0, 0.0]
>>> ANFIS(1, 2).get_params()
Traceback (most recent call last):
...
RuntimeError: ANFIS has no input ranges yet: pass ranges=... or call fit first

set_params

set_params(values: Sequence[float]) -> None

Replace every parameter (layout as in get_params).

Values must be finite numbers (not bools). Widths (sigma, bell a) must be at least MIN_WIDTH * (hi - lo) of their input and bell b at least MIN_B, the same floors that training keeps.

Parameters:

Name Type Description Default
values Sequence[float]

The new parameter values.

required

Raises:

Type Description
RuntimeError

If the input ranges are not known yet.

ValueError

If the count is wrong, a value is a bool, not finite, or below its floor.

Examples:

>>> from bulanik.anfis import ANFIS
>>> model = ANFIS(1, 2, ranges=[(0, 2)])
>>> params = model.get_params()
>>> params[-2:] = [3.0, 1.0]          # last rule: y = 3x + 1
>>> model.set_params(params)
>>> model.get_params()[-2:]
[3.0, 1.0]
>>> model.set_params([1.0])
Traceback (most recent call last):
...
ValueError: expected 8 parameters, got 1

predict

predict(X: Sequence[Sequence[float]]) -> list[float]

Crisp outputs for the rows of X.

The model is only trained inside ranges. An input outside them still gets a prediction (the consequents extrapolate linearly, so it can be far off), but one ExtrapolationWarning (a RuntimeWarning) names the first offending sample. The SugenoFIS exported by to_sugeno instead rejects such inputs.

Parameters:

Name Type Description Default
X Sequence[Sequence[float]]

Samples, each a sequence of n_inputs finite numbers.

required

Returns:

Type Description
list[float]

One prediction per sample.

Raises:

Type Description
RuntimeError

If the input ranges are not known yet.

ValueError

If X is empty, a sample has the wrong length, a value is not finite, or no rule fires at a sample (every firing strength underflows).

Examples:

>>> from bulanik.anfis import ANFIS
>>> model = ANFIS(1, 2, ranges=[(0, 2)])
>>> model.predict([[0.5], [1.5]])      # untrained: every consequent is 0
[0.0, 0.0]

rmse

rmse(X: Sequence[Sequence[float]], y: Sequence[float]) -> float

Root mean squared error of predict against y.

Warns on extrapolation like predict.

Parameters:

Name Type Description Default
X Sequence[Sequence[float]]

Samples, each a sequence of n_inputs finite numbers.

required
y Sequence[float]

One finite target per sample.

required

Returns:

Type Description
float

The RMSE.

Raises:

Type Description
RuntimeError

If the input ranges are not known yet.

ValueError

If X is empty, the lengths differ, a value is not finite, no rule fires at a sample (every firing strength underflows), or the error overflows.

Examples:

>>> from bulanik.anfis import ANFIS
>>> ANFIS(1, 2, ranges=[(0, 2)]).rmse([[0.5], [1.5]], [3.0, 4.0])   # all-zero model
3.535...

premise_gradient

premise_gradient(X: Sequence[Sequence[float]], y: Sequence[float]) -> list[float]

Gradient of the mean squared error with respect to the premise parameters.

In get_params order, premise parameters only (the consequents are solved by least squares in hybrid training).

Parameters:

Name Type Description Default
X Sequence[Sequence[float]]

Samples, each a sequence of n_inputs finite numbers.

required
y Sequence[float]

One finite target per sample.

required

Returns:

Type Description
list[float]

One partial derivative per premise parameter.

Raises:

Type Description
RuntimeError

If the input ranges are not known yet.

ValueError

If X is empty, the lengths differ, a value is not finite, or no rule fires at a sample (every firing strength underflows).

Examples:

>>> from bulanik.anfis import ANFIS
>>> model = ANFIS(1, 2, ranges=[(0, 2)])
>>> grad = model.premise_gradient([[0.5], [1.5]], [3.0, 4.0])
>>> len(grad)
4

fit

fit(X: Sequence[Sequence[float]], y: Sequence[float], *, epochs: int = 50, method: str = 'hybrid', lr: float = 0.01, ridge: float = 1e-08) -> list[float]

Train for epochs epochs; return (and store as history) the RMSE per epoch.

Training is deterministic and continues from the current parameters. hybrid solves the consequents by ridge least squares before the first epoch, then each epoch takes one gradient step on the premise and solves the consequents again; gradient takes one gradient step on all parameters per epoch. Each history entry is the RMSE of the model as it stands after that epoch, so the last one is the RMSE of the returned model. If the model has no ranges yet they are taken from X.

The least-squares step normalises every input to \([0, 1]\) over its range, so it is invariant to input offset and scale; ridge applies to the normalised coefficients. If the normal matrix is singular to working precision (for example ridge=0 with a rank-deficient design), the ridge is raised automatically (10x per retry, from at least 1e-12 times the largest diagonal entry) instead of failing. The gradient step lr is not scale-invariant: rescaling an input changes the effective step.

A step that would raise the training RMSE, or make it or a parameter non-finite, is halved until it does not, and dropped after 60 halvings (or at once for a non-finite gradient). The next epoch starts from twice the step taken, up to lr. So the gradient method's history never rises, and neither does the hybrid history apart from the small effect of ridge. A step shrunk that far no longer changes the parameters, so a dropped step means lr is far too large; a fit that dropped any emits one RuntimeWarning with the count.

Parameters:

Name Type Description Default
X Sequence[Sequence[float]]

Training samples, each a sequence of n_inputs finite numbers.

required
y Sequence[float]

One finite target per sample.

required
epochs int

Number of epochs, an integer of at least 1.

50
method str

"hybrid" or "gradient".

'hybrid'
lr float

Largest gradient step tried per epoch, positive.

0.01
ridge float

Ridge penalty of the least-squares step, non-negative.

1e-08

Returns:

Type Description
list[float]

The RMSE after each epoch (a copy of history).

Raises:

Type Description
ValueError

If an option is invalid (method, epochs, lr, ridge), X is empty, a sample has the wrong length, the lengths of X and y differ, a value is not finite, an input is constant in X while ranges is unknown, or no rule fires at a sample (every firing strength underflows).

Examples:

>>> from bulanik.anfis import ANFIS
>>> X = [[i / 10] for i in range(21)]
>>> y = [x[0] ** 2 for x in X]
>>> model = ANFIS(1, 3, ranges=[(0, 2)])
>>> model.rmse(X, y) > 1
True
>>> history = model.fit(X, y, epochs=5)
>>> len(history), history == model.history
(5, True)
>>> round(model.rmse(X, y), 2)
0.01
>>> tuned = ANFIS(1, 3, mf="bell", ranges=[(0, 2)])
>>> losses = tuned.fit(X, y, epochs=3, method="gradient", lr=0.05)
>>> losses[-1] < losses[0]
True

to_sugeno

to_sugeno(input_names: Sequence[str] | None = None, output_name: str = 'y') -> SugenoFIS

An equivalent SugenoFIS (and_method="prod").

One variable per input over its range with terms mf1..mfn, one Linear output term r1..rR per rule. It reproduces predict inside the ranges (the FIS rejects inputs outside). Where every membership underflows to 0 in plain floats the two differ: ANFIS still predicts (its firing is normalised in the log domain) while the FIS raises NoRuleFiredError.

Parameters:

Name Type Description Default
input_names Sequence[str] | None

Names for the input variables; defaults to x1, x2, ...

None
output_name str

Name of the output variable.

'y'

Returns:

Type Description
SugenoFIS

The exported fuzzy inference system.

Raises:

Type Description
RuntimeError

If the input ranges are not known yet.

ValueError

If input_names has the wrong length, or a name is not a valid rule name.

Examples:

>>> from bulanik.anfis import ANFIS
>>> X = [[i / 10] for i in range(21)]
>>> y = [x[0] ** 2 for x in X]
>>> model = ANFIS(1, 3, ranges=[(0, 2)])
>>> _ = model.fit(X, y, epochs=5)
>>> fis = model.to_sugeno(["x"])
>>> print(fis.rules[0])
IF x IS mf1 THEN y IS r1
>>> abs(fis.evaluate({"x": 1.0})["y"] - model.predict([[1.0]])[0]) < 1e-9
True