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
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.
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'
|
ranges
|
Sequence[tuple[float, float]] | None
|
Each input's |
None
|
allow_large
|
bool
|
Allow more than 1024 grid rules. |
False
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Attributes:
| Name | Type | Description |
|---|---|---|
history |
list[float]
|
The training RMSE after each epoch of the last |
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.
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 |
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 |
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 |
required |
Returns:
| Type | Description |
|---|---|
list[float]
|
One prediction per sample. |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If the input ranges are not known yet. |
ValueError
|
If |
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 |
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 |
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 |
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 |
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 |
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'
|
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 |
Raises:
| Type | Description |
|---|---|
ValueError
|
If an option is invalid ( |
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 |
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 |
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