bulanik.sezgi
sezgi ¶
sezgi bridge: nonlinear and fuzzy BWM, and an ANFIS trainer, as sezgi problems.
Needs the sezgi extra (pip install "bulanik[sezgi]"). sezgi is a
metaheuristic optimisation library; here its problems are minimised without
derivatives.
BWMProblem- Rezaei's (2015) nonlinear best-worst method.FuzzyBWMProblem- Guo & Zhao's (2017) fuzzy best-worst method.ANFISProblem/train_anfis- derivative-free training of anANFIS.
sezgi has no single-objective constraint API, so every constraint is a
penalty inside evaluate (minimised). The default algorithm everywhere is
sezgi.DifferentialEvolution(pop_size=40, variant="jde"); pass any other
sezgi algorithm as algorithm=. Runs are deterministic for a fixed seed and
budget (the number of objective evaluations).
The two consistency_ratio fields differ
In the BWMResult returned by
BWMProblem.solve the consistency_ratio is Rezaei's (2015) ratio
\(\xi / CI(a_{BW})\), where \(CI\) is
bwm_consistency_index of the
best-over-worst comparison \(a_{BW}\). It lies in \([0, 1]\) at the
optimum and is analogous to (not interchangeable with) AHP's
consistency ratio. When \(a_{BW} = 1\) the index is 0: the ratio is then
0.0 if \(\xi \le 10^{-6}\) (consistent) and None otherwise, with an
explanation in extras["consistency_note"]. FuzzyBWMProblem.solve
reports \(k / CI\) with Guo & Zhao's
fuzzy_bwm_consistency_index,
which is never 0. By contrast, bwm_linear in
bulanik.lumix stores the linear model's optimum \(\xi^L\) itself under the
same field name. Read operators["consistency"] ("rezaei2015_ci",
"guo_zhao2017_ci" or "xi_L") before comparing results.
Examples:
>>> from bulanik.sezgi import BWMProblem
>>> problem = BWMProblem({"price": 1, "quality": 2, "speed": 8},
... {"price": 8, "quality": 4, "speed": 1})
>>> r = problem.solve(budget=3000, seed=1)
>>> {k: round(v, 2) for k, v in r.weights.items()}
{'price': 0.62, 'quality': 0.31, 'speed': 0.08}
BWMProblem ¶
BWMProblem(best_to_others: Mapping[str, float], others_to_worst: Mapping[str, float], criteria: Sequence[str] | None = None, *, best: str | None = None, worst: str | None = None, penalty: float = 10.0)
Bases: Problem
Rezaei's (2015, Omega 53:49-57) nonlinear best-worst method.
With best criterion \(B\), worst criterion \(W\) and comparisons \(a_{Bj}\), \(a_{jW}\) the model is
evaluate(x) is the largest of those deviations plus
penalty * |sum(x) - 1|; the ratios are scale-invariant, so the penalty only
fixes the scale. Search space: Float(1e-6, 1, n).
solve().consistency_ratio is \(\xi / CI\), not \(\xi^L\)
Here consistency_ratio is Rezaei's (2015) ratio \(\xi / CI(a_{BW})\), a
normalised ratio, analogous to (not interchangeable with) AHP's
consistency ratio; Rezaei's acceptance thresholds differ. It is not
the quantity bwm_linear reports under
the same field name, which is the linear model's \(\xi^L\) itself. For
\(a_{BW} = 1\) the index \(CI\) is 0, so the ratio is 0.0 when
\(\xi \le 10^{-6}\) and None (with a note in
extras["consistency_note"]) otherwise.
Attributes:
| Name | Type | Description |
|---|---|---|
criteria |
Criterion names in order. |
|
best |
The best criterion. |
|
worst |
The worst criterion. |
|
best_to_others |
Validated best-to-others comparisons by criterion. |
|
others_to_worst |
Validated others-to-worst comparisons by criterion. |
|
penalty |
Weight of the sum-to-one penalty. |
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
best_to_others
|
Mapping[str, float]
|
Comparison of the best criterion over each criterion. |
required |
others_to_worst
|
Mapping[str, float]
|
Comparison of each criterion over the worst criterion. |
required |
criteria
|
Sequence[str] | None
|
Criterion names, fixing the order. Defaults to the order of
|
None
|
best
|
str | None
|
The best criterion. Inferred as the unique one with comparison 1
when |
None
|
worst
|
str | None
|
The worst criterion, inferred likewise. |
None
|
penalty
|
float
|
Weight of the penalty for violating the sum-to-one constraint (positive). |
10.0
|
Raises:
| Type | Description |
|---|---|
ValueError
|
For invalid comparison vectors (see
|
Examples:
>>> from bulanik.sezgi import BWMProblem
>>> problem = BWMProblem({"a": 1, "b": 2, "c": 8}, {"a": 8, "b": 4, "c": 1})
>>> problem.best, problem.worst
('a', 'c')
>>> round(problem.deviation([8, 4, 1]), 6) # perfectly consistent, any scale
0.0
>>> round(problem.deviation([0.6, 0.3, 0.1]), 6)
2.0
References
Rezaei (2015), Omega 53, 49-57.
deviation ¶
deviation(x: Sequence[float]) -> float
The BWM objective \(\xi\) at weights x, without the penalty.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
Sequence[float]
|
One positive weight per criterion, in criteria order (need not sum to 1). |
required |
Returns:
| Type | Description |
|---|---|
float
|
The largest deviation \(\max(|w_B/w_j - a_{Bj}|, |w_j/w_W - a_{jW}|)\). |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
solve ¶
solve(algorithm: Any = None, *, budget: int = 20000, seed: int = 0) -> BWMResult
Minimise with sezgi and return weights rescaled to sum to 1.
consistency_ratio is \(\xi /\)
bwm_consistency_index\((a_{BW})\),
with \(\xi\) recomputed at the rescaled weights. For \(a_{BW} = 1\) that
index is 0: the ratio is then 0.0 when \(\xi \le 10^{-6}\)
(consistent) and None otherwise, with extras["consistency_note"]
saying why. This differs from
bwm_linear, whose consistency_ratio is
\(\xi^L\) itself. extras also holds evals_used and best_f.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
algorithm
|
Any
|
A sezgi algorithm instance; |
None
|
budget
|
int
|
Number of objective evaluations (integer, at least 1). |
20000
|
seed
|
int
|
Random seed, making the run reproducible. |
0
|
Returns:
| Type | Description |
|---|---|
BWMResult
|
A |
BWMResult
|
|
BWMResult
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
>>> from bulanik.sezgi import BWMProblem
>>> problem = BWMProblem({"a": 1, "b": 2, "c": 8}, {"a": 8, "b": 4, "c": 1})
>>> r = problem.solve(budget=3000, seed=1)
>>> r.ranking, r.xi < 0.01, r.consistency_ratio < 0.01
(('a', 'b', 'c'), True, True)
With \(a_{BW} = 1\) the consistency index is 0, so an inconsistent set of comparisons has no ratio:
>>> p = BWMProblem({"a": 1, "b": 1, "c": 3}, {"a": 1, "b": 1, "c": 2},
... best="a", worst="b")
>>> r = p.solve(budget=2000, seed=1)
>>> r.consistency_ratio is None
True
>>> r.extras["consistency_note"]
"the comparisons are inconsistent (xi = 1.00213) but Rezaei's consistency index is 0 for a_BW = 1, so no consistency ratio exists"
FuzzyBWMProblem ¶
FuzzyBWMProblem(best_to_others: Mapping[str, TFN], others_to_worst: Mapping[str, TFN], criteria: Sequence[str] | None = None, *, best: str | None = None, worst: str | None = None, penalty: float = 10.0)
Bases: Problem
Guo & Zhao's (2017, Knowledge-Based Systems 121:23-31) fuzzy best-worst method.
The weights are triangular numbers \(\tilde w_j = (l_j, m_j, u_j)\). With fuzzy comparisons \(\tilde a_{Bj} = (l_{Bj}, m_{Bj}, u_{Bj})\), the model is \(\min k\) subject to, for \(j \ne B\),
(since \(\tilde w_B / \tilde w_j = (l_B/u_j, m_B/m_j, u_B/l_j)\)), the same rows
for \(\tilde w_j / \tilde w_W\) with \(\tilde a_{jW}\) for \(j \ne W\), the
normalisation \(\sum_j R(\tilde w_j) = 1\) with the graded mean
\(R = (l + 4m + u)/6\), and \(l \le m \le u\). evaluate is the largest
|row| / denominator plus
penalty * (|sum_j R_j - 1| + sum_j [max(0, l_j - m_j) + max(0, m_j - u_j)]),
where both ordering terms are summed over the criteria. Search
space: Float(1e-6, 1, 3n), \((l, m, u)\) per criterion.
Comparisons are TFN with \(l > 0\) and \(m \ge 1\),
e.g. from FUZZY_BWM_TFN; best and worst
are found by their TFN(1, 1, 1) comparison.
Attributes:
| Name | Type | Description |
|---|---|---|
criteria |
Criterion names in order. |
|
best |
The best criterion. |
|
worst |
The worst criterion. |
|
best_to_others |
Fuzzy best-to-others comparisons by criterion. |
|
others_to_worst |
Fuzzy others-to-worst comparisons by criterion. |
|
penalty |
Weight of the normalisation and ordering penalty. |
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
best_to_others
|
Mapping[str, TFN]
|
Fuzzy comparison of the best criterion over each one. |
required |
others_to_worst
|
Mapping[str, TFN]
|
Fuzzy comparison of each criterion over the worst. |
required |
criteria
|
Sequence[str] | None
|
Criterion names, fixing the order. Defaults to the order of
|
None
|
best
|
str | None
|
The best criterion; inferred from |
None
|
worst
|
str | None
|
The worst criterion; inferred likewise. |
None
|
penalty
|
float
|
Penalty weight (positive). |
10.0
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If a comparison is not a TFN. |
ValueError
|
If a TFN has \(l \le 0\) or \(m < 1\), the vectors are invalid
(see |
Examples:
>>> from bulanik.scales import FUZZY_BWM_TFN as F
>>> from bulanik.sezgi import FuzzyBWMProblem
>>> problem = FuzzyBWMProblem({"a": F["EI"], "b": F["WI"], "c": F["AI"]},
... {"a": F["AI"], "b": F["FI"], "c": F["EI"]})
>>> problem.best, problem.worst, problem.space().__class__.__name__
('a', 'c', 'Float')
References
Guo & Zhao (2017), Knowledge-Based Systems 121, 23-31.
deviation ¶
deviation(x: Sequence[float]) -> float
The fuzzy BWM objective \(k\) at x, without the penalty.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
x
|
Sequence[float]
|
The triples \((l, m, u)\) of all criteria, flattened in criteria order (length \(3n\), positive components). |
required |
Returns:
| Type | Description |
|---|---|
float
|
The largest row deviation. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
solve ¶
solve(algorithm: Any = None, *, budget: int = 100000, seed: int = 0) -> BWMResult
Minimise with sezgi; return fuzzy weights rescaled so \(\sum R = 1\).
Each best triple is sorted so that \(l \le m \le u\) (a no-op unless the
search ended a hair inside the ordering penalty) and every triple is
scaled by \(1 / \sum R\), which leaves \(k\) unchanged. xi is \(k\)
recomputed at the returned weights, weights are the graded means
\(R(\tilde w_j)\), fuzzy_weights the TFNs and consistency_ratio is
\(k /\)
fuzzy_bwm_consistency_index\((\tilde a_{BW})\):
a ratio to the index, like BWMProblem's and
unlike bwm_linear's, which is \(\xi^L\)
itself. The fuzzy index is never 0, so the ratio is always a number.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
algorithm
|
Any
|
A sezgi algorithm instance; |
None
|
budget
|
int
|
Number of objective evaluations (integer, at least 1). |
100000
|
seed
|
int
|
Random seed, making the run reproducible. |
0
|
Returns:
| Type | Description |
|---|---|
BWMResult
|
A |
BWMResult
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
>>> from bulanik.scales import FUZZY_BWM_TFN as F
>>> from bulanik.sezgi import FuzzyBWMProblem
>>> problem = FuzzyBWMProblem({"a": F["EI"], "b": F["WI"], "c": F["AI"]},
... {"a": F["AI"], "b": F["FI"], "c": F["EI"]})
>>> r = problem.solve(budget=20000, seed=1)
>>> r.ranking, {k: round(v, 2) for k, v in r.weights.items()}
(('a', 'b', 'c'), {'a': 0.51, 'b': 0.35, 'c': 0.14})
>>> round(sum(r.weights.values()), 6)
1.0
>>> r.operators["consistency"]
'guo_zhao2017_ci'
ANFISProblem ¶
ANFISProblem(anfis: ANFIS, X: Sequence[Sequence[float]], y: Sequence[float], *, coef_bound: float = 10.0)
Bases: Problem
Training RMSE of an ANFIS as a sezgi problem.
The space is Float(0, 1, D) over all \(D\) parameters;
decode maps each \(z\) affinely to its
parameter's bounds (span = hi - lo of its input): gaussian \(c\) in
[lo, hi], \(\sigma\) in [span/100, span]; bell \(a\) in [span/100, span], \(b\)
in [0.5, 5], \(c\) in [lo, hi]; consequents in [-coef_bound, coef_bound].
evaluate sets the decoded parameters on anfis (this instance is the
working copy and is mutated) and returns the RMSE, or inf when no rule
fires or the output overflows. The ANFIS needs its ranges (pass ranges=
when building it; train_anfis infers them
from the data when it has none).
Attributes:
| Name | Type | Description |
|---|---|---|
anfis |
The working ANFIS whose parameters |
|
coef_bound |
Bound of the consequent coefficients. |
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
anfis
|
ANFIS
|
An |
required |
X
|
Sequence[Sequence[float]]
|
Training inputs, one row per sample. |
required |
y
|
Sequence[float]
|
Training targets. |
required |
coef_bound
|
float
|
Consequent coefficients are searched in
|
10.0
|
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
RuntimeError
|
If the ANFIS has no input ranges. |
ValueError
|
If |
Examples:
>>> from bulanik.anfis import ANFIS
>>> from bulanik.sezgi import ANFISProblem
>>> X = [[i / 10] for i in range(11)]
>>> y = [2 * x[0] + 1 for x in X]
>>> problem = ANFISProblem(ANFIS(1, 2, ranges=[(0, 1)]), X, y)
>>> problem.dim
8
>>> 0.0 <= problem.evaluate([0.5] * problem.dim) < 10
True
decode ¶
decode(z: Sequence[float]) -> list[float]
Map z in \([0, 1]^D\) to the ANFIS parameter vector.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
z
|
Sequence[float]
|
\(D\) numbers in [0, 1]. |
required |
Returns:
| Type | Description |
|---|---|
list[float]
|
The parameters in the order of |
list[float]
|
|
list[float]
|
bounds. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
Examples:
>>> from bulanik.anfis import ANFIS
>>> from bulanik.sezgi import ANFISProblem
>>> X = [[0.0], [1.0]]
>>> p = ANFISProblem(ANFIS(1, 1, ranges=[(0, 1)]), X, [0.0, 1.0], coef_bound=2)
>>> p.decode([0.0] * p.dim)
[0.0, 0.01, -2.0, -2.0]
>>> p.decode([1.0] * p.dim)
[1.0, 1.0, 2.0, 2.0]
ANFISTrainResult
dataclass
¶
ANFISTrainResult(anfis: ANFIS, rmse: float, evals_used: int)
Outcome of train_anfis.
Attributes:
| Name | Type | Description |
|---|---|---|
anfis |
ANFIS
|
The trained copy of the ANFIS. |
rmse |
float
|
Its training root-mean-square error. |
evals_used |
int
|
Number of objective evaluations sezgi used. |
train_anfis ¶
train_anfis(anfis: ANFIS, X: Sequence[Sequence[float]], y: Sequence[float], *, algorithm: Any = None, budget: int = 20000, seed: int = 0, coef_bound: float = 10.0) -> ANFISTrainResult
Train a deep copy of anfis with sezgi; anfis itself is untouched.
An ANFIS built without ranges gets them from the min and max of each
column of X on the copy, as
ANFIS.fit does (a constant column raises
ValueError). Use this derivative-free trainer instead of fit when you
want a global search over all membership and consequent parameters.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
anfis
|
ANFIS
|
The ANFIS whose structure is trained (not modified). |
required |
X
|
Sequence[Sequence[float]]
|
Training inputs, one row per sample. |
required |
y
|
Sequence[float]
|
Training targets. |
required |
algorithm
|
Any
|
A sezgi algorithm instance; |
None
|
budget
|
int
|
Number of objective evaluations (integer, at least 1). |
20000
|
seed
|
int
|
Random seed, making the run reproducible. |
0
|
coef_bound
|
float
|
Consequent coefficients are searched in
|
10.0
|
Returns:
| Type | Description |
|---|---|
ANFISTrainResult
|
An |
Raises:
| Type | Description |
|---|---|
TypeError
|
If |
ValueError
|
If the data or |
RuntimeError
|
If no finite RMSE is found. |
Examples:
>>> from bulanik.anfis import ANFIS
>>> from bulanik.sezgi import train_anfis
>>> X = [[i / 10] for i in range(11)]
>>> y = [2 * x[0] + 1 for x in X]
>>> model = ANFIS(1, 2)
>>> res = train_anfis(model, X, y, budget=2000, seed=0)
>>> res.rmse < 0.1, res.evals_used, model.ranges is None
(True, 2000, True)