Fuzzy classification¶
Mamdani and TSK answer with a number. When the answer is a label — approve or decline, fraud or clean, setosa or virginica — the natural fuzzy model is a rule base whose consequents are class labels, each carrying a certainty factor:
Inference is a vote. Every rule contributes its firing strength times its certainty to its own class, and the strongest class wins. Because the vote is just arithmetic over rules you can read, the model explains every prediction it makes.
Writing a classifier by hand¶
import fuzzytool as fz
score = fz.Variable("score", (300, 850), terms=["poor", "fair", "good"],
shoulders=True)
dti = fz.Variable("dti", (0, 50), terms=["low", "moderate", "high"],
shoulders=True)
clf = fz.FuzzyClassifier()
clf.rule(score["poor"] | dti["high"], "decline", weight=0.9)
clf.rule(score["fair"] & dti["moderate"], "review", weight=0.6)
clf.rule(score["good"] & dti["low"], "approve", weight=0.95)
clf(score=810, dti=8) # -> 'approve'
The rule weight is the certainty factor: how much you trust that rule.
A rule scraped from an ambiguous region should not outvote a rule backed by a
clean one, and the weight is what encodes that.
Learning one from data¶
chi implements the classic Chi-Yuen-Pedrycz method:
each sample votes for the (partition cell, class) pair it matches best, each
cell predicts its winning class, and the certainty factor comes out of how
lopsided that cell's vote was.
import numpy as np
from fuzzytool.learn import chi
rng = np.random.default_rng(0)
X = rng.uniform(0, 10, size=(300, 2))
y = np.where(X[:, 0] + X[:, 1] > 10, "high", "low")
inputs = [fz.Variable(f"x{j}", (0, 10), terms=["lo", "mid", "hi"], shoulders=True)
for j in range(2)]
clf = chi(X, y, inputs)
clf.score(y, x0=X[:, 0], x1=X[:, 1]) # accuracy
print(clf.summary()) # the whole model, in words
Rules from pure cells come out with certainty near 1; rules from cells where the
classes overlap come out near 0. min_certainty=0.5 drops the latter, trading a
little accuracy for a much shorter, more honest rule base.
Probabilities and the reject option¶
clf.predict(x0=X[:, 0], x1=X[:, 1]) # labels, one per sample
clf.predict_proba(x0=X[:, 0], x1=X[:, 1]) # confidence shares, rows sum to 1
predict_proba returns the normalized vote — a confidence share, not a
calibrated probability. Say so when you report it.
What happens where no rule fires is a policy you choose:
on_no_rule |
Behavior |
|---|---|
"majority" (default) |
predict the rule base's most-supported class |
"none" |
predict None — an explicit reject option |
"raise" |
fail loudly |
"none" is usually the right choice in a decision pipeline: a fuzzy classifier
that admits "this input is outside anything I was built for" is more useful than
one that guesses.
Aggregation: winner-takes-all or additive¶
fz.FuzzyClassifier(aggregation="max") # the single most confident rule decides
fz.FuzzyClassifier(aggregation="sum") # weak rules that agree add up
"max" is Ishibuchi's classic single-winner model and is more robust to a badly
estimated certainty factor. "sum" lets a group of agreeing weak rules outvote
one strong rule, which helps when the partition is fine-grained.
Explaining a prediction¶
{'output': 'high',
'fired_rules': [
{'index': 5, 'rule': 'IF (x0 is hi and x1 is mid) THEN high',
'firing': 0.48, 'share': 0.71},
{'index': 2, 'rule': 'IF (x0 is mid and x1 is mid) THEN low',
'firing': 0.20, 'share': 0.29}]}
That is the whole model's reasoning for that one input: which rules spoke, how loudly, and how much of the decision each accounts for.
In a scikit-learn pipeline¶
from fuzzytool.integrations.sklearn import ChiClassifier
from sklearn.model_selection import cross_val_score
cross_val_score(ChiClassifier(inputs), X, y, cv=5)
ChiClassifier learns the rule base in fit and exposes the learned system as
clf.system_, so a cross-validated model is still one you can print, audit and
save. FuzzySystemClassifier does the same for a classifier you wrote by hand.
See also: Auditing a rule base, Rule learning.