Skip to main content

EFM — Explainable Fuzzy Machines

EFM is a lightweight PyTorch and scikit-learn library for tabular machine learning.

The public API is two estimators. A string aggregator selects the internals: differentiable fuzzy rules (GPU, gradient descent) or a closed-form rule book (CPU, spectral-floored ridge). Both print IF-THEN logic.

from efm import EFMRegressor, EFMClassifier

EFMRegressor(aggregator="gaussian")          # Gaussian template matching
EFMRegressor(aggregator="student_t")         # Heavy-tailed templates
EFMRegressor(aggregator="residual")          # Affine leaves + residual CART
EFMRegressor(aggregator="residual", n_rounds=3)
EFMClassifier(aggregator="hyperplane")       # Piecewise-linear evidence
EFMClassifier(aggregator="piecewise")        # Two-piece linear GAM
EFMClassifier(aggregator="state_coupled")    # Recurrent rule state

Why EFM?

Tree ensembles are accurate and opaque. GAMs such as EBM give shape plots, not thresholds. EFM learns explicit, human-readable rules.

Ground truth: IF x0 > 0.5 AND x1 < 0 THEN +3.0 …

EBM — importances and pairwise heatmaps, no symbolic cuts.

x0 (0.38) | x1 (0.36) | x0 × x1 (0.26)

RuleFit — thresholds, but dense overlapping trees.

x0 <= 0.171 and x1 > 0.690 | x0 > 1.661 and x1 > -0.191 and x4 <= 0.859

EFM — sparse literals after L1 masking (differentiable) or a jointly refit affine book (closed-form).

Rule 1: x0 > +0.460 AND x1 < -0.021
Rule 2: x2 > +1.035 AND x5 < +0.249

Differentiable path

Fuzzy aggregators (student_t, gaussian, hyperplane, state_coupled) train an nn.Module:

x
│
├─ fuzzy rule layer     thresholds, inequality signs, feature masks
├─ log-margin tensor
├─ evidence aggregator
├─ rule firing strengths
└─ linear head          regression value or class logits

Training uses AdamW, a validation split, and early stopping. Typical fit time is seconds on a GPU.


Closed-form path

These aggregators skip PyTorch. Coefficients are a single spectral-floored ridge (SFR) fit.

aggregator What you get
"affine" Boosted affine rectangles, jointly refit
"residual" Affine book + residual CART × n_rounds + path smoothing
"piecewise" Additive two-piece linear GAM
"split_linear" IF vᵀx ≤ t THEN affine ELSE affine

n_rounds=2 (default) or 3 is the only extra knob for "residual". Prefer "piecewise" on additive classification; "residual" / "affine" on region-heavy tables. Do not mix "piecewise" and "residual" in one coefficient vector.


Aggregators

Aggregator Kind Idea
"student_t" fuzzy Heavy-tailed template matching in log-margin space
"gaussian" fuzzy Gaussian template matching
"hyperplane" fuzzy Sum of learned ReLU hyperplanes
"state_coupled" fuzzy Short linear recurrence over rule state
"affine" closed Jointly refit affine rectangles
"residual" closed Residual CART on an affine book
"piecewise" closed Two linear pieces per coordinate
"split_linear" closed One split direction, two affines

Aliases ("SMTE", "GMTE", "HYP", "SC") still resolve to the fuzzy names.


Installation

pip install efm

From a checkout:

git clone https://github.com/EmotionEngineer/efm.git
cd efm
pip install -e .
pip install -e ".[dev]"   # pytest, ruff, mypy
pytest tests -v

Quick start: regression

from sklearn.datasets import load_diabetes
from sklearn.model_selection import train_test_split
from efm import EFMRegressor

data = load_diabetes(as_frame=True)
X_train, X_test, y_train, y_test = train_test_split(
    data.data, data.target, test_size=0.2, random_state=0,
)

model = EFMRegressor(
    aggregator="student_t",
    n_rules=64,
    epochs=200,
    random_state=0,
)
model.fit(X_train, y_train)
print(model.predict(X_test)[:5])
print(model.explain(top_k=5))

No GPU, no epoch loop:

model = EFMRegressor(aggregator="residual", n_rounds=2, random_state=0)
model.fit(X_train, y_train)
print(model.explain(top_k=8))

Quick start: classification

kappa_gating=True scales the steepness of each fuzzy boundary so L1 masking can drop noise features instead of cancelling inside the log-ratio.

import pandas as pd
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from efm import EFMClassifier

X_raw, y_raw = make_classification(
    n_samples=1000, n_features=20, n_informative=4, n_redundant=0, random_state=0,
)
X = pd.DataFrame(X_raw, columns=[f"feat_{i}" for i in range(20)])

X_train, X_test, y_train, y_test = train_test_split(
    X, y_raw, test_size=0.2, stratify=y_raw, random_state=0,
)

model = EFMClassifier(
    aggregator="student_t",
    n_rules=32,
    epochs=150,
    mask_l1=1e-3,
    kappa_gating=True,
    random_state=0,
)
model.fit(X_train, y_train)
print(model.predict(X_test)[:5])
print(model.explain(top_k=3, mask_threshold=0.10))

Additive closed-form GAM:

model = EFMClassifier(aggregator="piecewise", random_state=0)
model.fit(X_train, y_train)
print(model.explain())

Bare PyTorch

Sklearn wrappers handle preprocessing, a validation split, and target scaling. The module itself is EFM (fuzzy aggregators only).

import torch
from efm import EFM

model = EFM(
    input_dim=10,
    n_rules=32,
    n_classes=3,
    aggregator="student_t",
    kappa_gating=True,
)
x = torch.randn(8, 10)
y = torch.randint(0, 3, size=(8,))
loss = model.loss_batch(x, y)
loss.backward()

Explanations

print(model.explain(top_k=10, mask_threshold=0.10))
fig = model.plot_rules(top_k=10)          # fuzzy aggregators
fig.savefig("rules.png", dpi=150)
Rule #01  [id=12, importance=0.8342, n_conds=3, ν=3.91]
    bmi                            > +0.418  [ev=+, mask=0.62]
    bp                             > -0.055  [ev=+, mask=0.44]
    s5                             > +0.276  [ev=−, mask=0.31]
  • Thresholds are in the transformed space (standardized numerics, one-hot categoricals).
  • Pass model.preprocessor_.get_feature_names_out() if you need those names explicitly.
  • Closed-form aggregators implement explain() only (plot_rules is fuzzy-specific).

Parameters

Shared:

Parameter Description
aggregator See the table above
random_state Seed
val_fraction Hold-out fraction when X_val is omitted

Fuzzy:

Parameter Description
n_rules Number of fuzzy rules
n_planes Planes per rule (hyperplane)
steps Recurrence depth (state_coupled)
epochs, lr, batch_size, patience AdamW training
mask_l1, weight_decay Sparsity and decay
device "cpu", "cuda", or None (auto)
kappa_gating Steeper literals so masks prune noise. Default False

Closed-form:

Parameter Description
n_rounds Residual CART rounds for "residual" (2 or 3)
n_trees Affine boosting rounds ("affine", "residual")
smooth Path-membership temperature ("residual", default 0.08)
min_leaf Minimum samples per affine leaf

Validation split

model.fit(X_train, y_train, X_val=X_valid, y_val=y_valid)

If X_val is omitted, a split of size val_fraction is taken from the training data (fuzzy path). Closed-form books fit on the full training matrix.


Saving

import joblib
joblib.dump(model, "efm_model.joblib")

Fuzzy module weights:

torch.save(model.module_.state_dict(), "efm_state.pt")

Development

pytest tests -v
ruff check efm tests
python -m build

Limitations

  • With kappa_gating=False, masks sit inside the fuzzy subset and can train slowly on small data. Use kappa_gating=True when feature selection must be sharp.
  • High-cardinality categoricals expand into many one-hot columns.
  • Explanation thresholds are post-preprocessing.
  • "affine" and "residual" classifiers are binary in the library. "piecewise" and "split_linear" classifiers are binary too (ŷ = σ(·) vs the max label). For multiclass use a fuzzy aggregator, or wrap a closed book in one-vs-rest (see examples/synthetic.ipynb).

License

MIT.

Metadata

Release files for efm 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for efm 0.2.0
File Size Uploaded Provenance
efm-0.2.0.tar.gz 28.6 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for efm 0.2.0
File Interpreter ABI Platform
efm-0.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 52.7 kB

Release files / efm-0.2.0.tar.gz

Download URL efm-0.2.0.tar.gz
Size 28.6 kB
Tags Source
SHA-256 checksum
How to use checksums
e032dbb7b36a4887c16bd41fa991d81bfaf68b09a94eafee483f8e26ae99ef96
BLAKE2b-256 checksum
How to use checksums
01d10cd6b721f9ed958f402a6ccb25bf240b98834d7196c96a94b77c6caac686
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.1.0 CPython/3.10.11

Release files / efm-0.2.0-py3-none-any.whl

Download URL efm-0.2.0-py3-none-any.whl
Size 24.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
08e24cae243c14cce5237a23cee2c1c2469884f5995ca470743f32d92faf7134
BLAKE2b-256 checksum
How to use checksums
09afed1dab91fc3c463194f8c8f66e33a17d9eeb6c3f41ec0b47124e2b7b07eb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.1.0 CPython/3.10.11

Security for efm 0.2.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page