Feature Selection Toolbox FST4
UTIA · Czech Academy of Sciences

ÚTIA — Institute of Information Theory and Automation, Czech Academy of Sciences

FST4 · Feature Selection Toolbox 4.2

Find the features that matter — reproducibly, on any platform, at any dimensionality. A standalone C++ library and a single-binary command-line tool for choosing, out of everything you measured, the subset your model should actually use.

  • header-only C++17 · zero mandatory dependencies
  • 15 search algorithms × 18 criteria × 8 metrics
  • bit-identical results at any --threads
  • same --seed ⇒ same result on every OS
  • plug in your own criterion — any language
./fst4 --data data/wdbc_30.trn \
       --criterion wrapper-knn --knn-k 3 \
       --splitter 5050 --splitter2 cv \
       --search sffs \
       --validate same

One command, four decisions: what to optimize (3-NN accuracy), how to estimate it honestly (inner cross-validation on one half, the other half walled off), how to explore (floating search), and the final honesty check (the untouched holdout). Load a dataset, run the search on the selection half, and report the chosen subset with its holdout validation — the complete textbook protocol.

01 · The problem

Why select features at all

Most datasets measure far more variables than the signal needs. Feature selection picks the subset worth keeping — and unlike projection methods (PCA and friends), the selected variables remain the original, interpretable measurements. Nothing is mixed, rotated, or renamed. Three reasons to care, in decreasing order of frequency:

Generalization

With limited samples, models fit noise in irrelevant variables. Removing them is regularization you can inspect — and explain.

Cost

Features can be expensive to acquire: sensors, lab tests, compute. A model that needs 10 of 500 inputs is a different product from one that needs all 500.

Insight

Which variables carry the signal is often the actual research question. The subset is the answer, not a preprocessing step.

The catch: subsets live in a space of 2D candidates, every candidate must be scored, and every score is only an estimate made from finite data. Feature selection is therefore three problems wearing one coat — a search problem, a scoring problem, and a statistical-honesty problem. FST4 treats all three as first-class.

02 · The design

One idea, four axes

FST4 is built around one organizing idea: feature selection is a search over subsets, scored by a criterion, estimated on data splits, with results collected by trackers. The four axes compose freely — any search runs any criterion on any splitting protocol — and the command line mirrors them one flag each.

DATA TRN · ARFF · CSV SCALING void · to01 · white SPLITTING cv · 5050 · holdout… nests: --splitter2 SELECTION LOOP SEARCH sffs · bb-fast · montecarlo… CRITERION wrapper-knn · bhattacharyya… subset value --evaluator · --threads N (results identical) TRACKERS best-k · stability… VALIDATION & REPORT criterion— what to optimize search— how to explore 2ᴰ subsets splits— how estimates stay honest trackers— what happens to the results
The composition. Every FST4 run is one path through this diagram; each box is a flag with interchangeable values. The selection loop never touches the outer holdout — that is reserved for --validate.

Everything runs from one binary with no mandatory external dependencies. ./fst4 --list prints the full component registry with validity notes; every misuse produces an actionable error, not a silent fallback. The same components are equally usable as a C++ template library — the CLI is a thin composition layer over public seams.

03 · The choice that matters most

What you optimize beats how you search

Newcomers to feature selection agonize over search algorithms. The evidence says: agonize over the criterion instead. Search algorithms differ in how hard they push the objective; criteria differ in what is being pushed. A perfect search of the wrong objective returns a confidently wrong subset.

FST4 makes this concrete. Take one dataset, one search (floating), one classifier (3-NN) — and change only the optimized metric from plain accuracy to balanced accuracy. The two runs select almost disjoint subsets: cross-scenario similarity IATI ≈ 0.14 on the diagnostic data of demos/demo14.cfg. Same everything, different question, different answer. Choose the question first.

Filters: fast, classifier-free, hard to fool

Filter criteria score subsets from data statistics alone — no classifier is trained, evaluations cost microseconds, and there is no classifier to overfit. All five are cardinality-monotonic, which is exactly what the optimal searches of section 05 need:

criterionmodelnotes
bhattacharyyaGaussiantwo-class; the recommended default filter
divergenceGaussiantwo-class; can breach monotonicity numerically on some data — prefer Bhattacharyya
gmahalanobisGaussiantwo-class generalized Mahalanobis distance
multinom-bhattacharyyamultinomialcount/text data, any class count
multinom-mimultinomialmutual information for count/text data

Wrappers: optimize the model you will deploy

Wrapper criteria train and test an actual predictor on every candidate subset — slower, but they measure the thing you ultimately ship. FST4 carries a full model zoo, implemented from scratch in the library and deterministic by design:

criterionmodelnotes
wrapper-knnk-nearest neighbours--knn-k, L1/L2/Lp distances
wrapper-bayesGaussian Bayesper-class covariances, any class count
wrapper-ldalinear discriminantpooled covariance, closed form, parameterless
wrapper-logregmultinomial logistic regressiondeterministic Newton training, RNG-free
wrapper-linsvmlinear SVMdual coordinate descent, RNG-free, one-vs-rest
wrapper-rbfsvmRBF-kernel SVMnative, deterministic; built-in C/γ tuning (--rbfsvm-optimize)
wrapper-svmkernel SVM (poly/sigmoid too)via the optional LibSVM drop-in
wrapper-rfRandom ForestCART/Gini + bagging; deterministic by construction
wrapper-multinommultinomial naive Bayescount/text data
wrapper-regressorweighted-neighbours regressionregression pipeline
wrapper-ridgeridge regressionclosed form; criterion defaults to R²
wrapper-externyour own modelany language, persistent-process protocol — section 06

Metrics: ask the question you actually care about

Every wrapper optimizes plain accuracy by default, but imbalanced problems deserve better. --wrapper-metric switches the optimized quantity to balanced-accuracy, f1, f1-macro, or exact-rank auc (binary exact; multiclass as one-vs-rest macro AUC) for classification, and r2, neg-rmse, neg-mae for regression. --class-weights 1,5 injects asymmetric error costs — a missed positive can cost five times a false alarm, and selection will pick features accordingly.

criterion on selection data (train axis) accuracy on untouched holdout (validated axis)
schematic d★ — the size actually worth deploying optimism gap = overfitting criterion keeps climbing… …while true performance has already turned subset size d → score (normalized) →
Why the criterion is not the truth. Every criterion value is an estimate from finite data; pushed hard enough it rewards subsets that exploit the particular sample. FST4 prints this exact curve for any single-search run — the best-per-size criterion value, with holdout re-validation of each subset when --validate is given — via --report detailed, so you see where your own d★ sits instead of guessing.

Honest guidance — filter or wrapper?

Filters are orders of magnitude faster and immune to classifier overfitting, but they optimize a proxy. Wrappers optimize deployed performance — and can over-optimize it when samples are few and features many. When samples are scarce, prefer filters or regularized criteria, and distrust any criterion value not confirmed on a holdout. The nested-split protocol and --validate exist exactly for this.

05 · The big gun

Stochastic sequential search at 10⁴ features

At D ≈ 10 000, classic sequential search dies of arithmetic: every floating-search step sweeps all D candidates, so a single step costs thousands of criterion evaluations — with a wrapper criterion, that is the end of the story. The usual retreat is to give up on search altogether and rank features individually. FST4 offers a stronger option.

The budgeted stochastic sequential search (--evaluator sampled) replaces each step's full sweep with a fixed-size, adaptively informed sample: online per-feature statistics — collected free of charge from every criterion evaluation the search already pays for — feed a softmax proposal distribution (with a guaranteed exploration floor) that nominates --step-cap candidates per forward step and --step-cap-backward per backward step. Step cost is set by the budget, not by D. A frontier bound (--sffs-delta) keeps the floating expansion near the target size instead of wandering toward full dimensionality. The method and its evaluation are described in Somol and Grim, Stochastic Sequential Search in Very-High-Dimensional Feature Selection, arXiv:2608.01502 (2026).

# 10,105-feature text data, target d=500, 8 threads — minutes, not days:
./fst4 --config demos/demo37t.cfg
# the campaign protocol behind it, spelled out:
./fst4 --data data/reuters_apte.arff --criterion multinom-bhattacharyya \
       --search sffs --target-d 500 --sffs-delta 20 \
       --evaluator sampled --step-cap 100 --step-cap-backward 50 \
       --threads 8 --validate multinom

Two shipped protocols mark the scale: 5 000-feature handwriting data searched to d = 300 (demos/demo36.cfg), and 10 105-feature text categorization searched to d = 500 in minutes on 8 threads (demos/demo37t.cfg) — threaded runs bit-identical to sequential at the same --seed, for any thread count.

The duality: stochastic search vs. DAF ranking

FST4's other high-dimensional instrument is DAF (Dependency-Aware Feature ranking): run montecarlo probes with the feature-stats tracker, and every feature is ranked by its average contribution across thousands of random subsets (--daf-rank, pairwise statistics via --daf-pairs). Unlike per-feature ranking, DAF sees features in company, yet it stays blunt enough to be extremely robust. The two methods form a genuine duality:

Budgeted stochastic search

The stronger optimizer. Actively constructs a subset; with budget it decisively out-scores rankings on the criterion. The choice when you can validate and want the best subset the data can support.

DAF ranking

The robust one. Averages over thousands of probes; stable across data resamples and seeds, hard to overfit, cheap. The safer default when samples are scarce relative to dimensionality.

budgeted stochastic SFFS (--evaluator sampled) DAF ranking (montecarlo + feature-stats) BIF (individual ranking)
schematic BIF — one ranking pass, interaction-blind DAF — fast rise, robust plateau stochastic SFFS — keeps climbing given budget, the search pulls ahead — on the criterion (train) axis criterion-evaluation budget (log, schematic) → criterion value (train axis) →
Two regimes, one honest caveat. The y-axis is the optimized criterion, estimated on selection data. When samples are scarce relative to dimensionality, the strongest optimizer of an estimate can overfit it — the holdout, not the criterion, has the last word. Run both, compare on --validate, and let DAF's robustness arbitrate ties. FST4 makes the comparison a one-liner (--compare-config, section 07).

06 · Open the loop

Bring your own criterion — any language

Everything so far used FST4's built-in models. But the most consequential capability is the generic one: FST4 will optimize a criterion it has never heard of. Your model, your metric, your cost function — in Python, R, awk, or a compiled binary — plugs into every search, splitter, tracker and thread of the toolbox through a persistent-process line protocol on stdin/stdout. Three escalating layers:

fst4 any search · any splitter · any tracker your tool Python · R · awk · anything with stdin/stdout 1 · wrapper-extern — your classifier inside the loop TRAIN rows … / TEST rows … → ← predicted labels 2 · extern-value — your criterion inside the loop DATA — training part, once per split → VALUE d f₁ … f_d — per evaluation, indices only → ← the criterion value (one number) your tool holds sufficient statistics — answers in O(d), caching and cost-aware measures possible 3 · extern-value --extern-no-data — pure set-function optimization VALUE d f₁ … f_d — no data at all → ← f(S) FST4 as a generic subset optimizer over an abstract feature universe
The escalation. Layer 1 swaps the classifier; layer 2 swaps the whole criterion while FST4 still manages data splits for you (the DATA block is re-sent per split, so honest estimation survives the bridge); layer 3 drops data entirely — any set function can be searched, floated, tracked, and Branch-&-Bounded.
# your classifier in the loop (reference tool: a nearest-centroid model in awk):
./fst4 --data data/wdbc_30.trn --criterion wrapper-extern \
       --extern-cmd "awk -f tests/extern_centroid.awk" \
       --splitter 5050 --splitter2 cv --search sfs --target-d 3 --validate same

# your criterion in the loop — and, asserted monotonic, under PROVABLY OPTIMAL search:
./fst4 --data data/wdbc_30.trn --criterion extern-value \
       --extern-cmd "awk -f tests/extern_value_meandiff.awk" \
       --extern-monotonic --search bb-improved --target-d 3

That second command deserves a double take: --extern-monotonic asserts that your criterion is cardinality-monotonic, which admits the Branch & Bound family — guaranteed-optimal subsets of a criterion FST4 cannot even inspect. The guarantee then rests entirely on your assertion; without the flag, the optimal searches refuse and tell you why. Reference implementations of all three protocol layers ship in tests/ as one-page awk scripts — the protocol is small enough to implement over coffee.

Loud caveats — by design

--extern-cmd is user-supplied code execution through the shell: run only commands you trust. FST4's reproducibility guarantees end at the protocol boundary — beyond it, determinism belongs to your tool. The label-only classifier protocol cannot provide scores, so AUC is unavailable there (confusion-based metrics work).

07 · Trust, but verify

Machinery for honest answers

Feature selection is uniquely exposed to self-deception: the same data that chooses the subset praises it. FST4's estimation machinery is built to keep the praise and the choice apart.

Splits that nest

The splitter axis (5050, cv, holdout, resub, leave1out, randrand/randfix) defines the estimation protocol, and splitters nest: with --splitter 5050 --splitter2 cv, the criterion runs on inner cross-validation folds of the outer training half while the outer holdout stays untouched for --validate — selection never sees the data it is judged on. Scaling (--scaler to01|white) normalizes from training-part statistics only when you ask for it with --scaler-fit train; the default fits the transform on the whole dataset before splitting, the FST3 convention. --validate knn,bayes,lda then reports holdout performance under classifiers that took no part in selection — the cheapest honesty check there is.

Trackers: results as evidence, not trophies

Trackers observe every evaluated subset. The default keeps the best-k distinct subsets; regularizer prefers a smaller or cheaper subset within a criterion margin of the best — a direct antidote to overfitting-by-greed; stabileval measures selection stability across repeated splits (--repeat-over-splits): if ten resamples of your data select ten disjoint subsets, no single run should be trusted, and now you know. --compare-config runs a second scenario and quantifies the similarity of two whole selection processes (IATI/ICW/IANHI) — the instrument behind the metric-flip experiment of section 03.

08 · The contract

Reproducibility is a specification, not luck

Randomized feature selection has a credibility problem: rerun someone's experiment and you get a different subset. FST4 treats determinism as a contract with a test suite:

  • One owned PRNG. All randomness flows through FST4's own generator with identical streams on Windows, macOS and Linux — the same --seed yields the same splits, the same sampled candidates, the same subsets, on every platform. An unseeded run equals --seed 1: even the default is reproducible.
  • Structural determinism. Components needing training-time randomness derive it structurally — the Random Forest seeds each training run from a hash of (candidate subset, seed, split), making every evaluation a pure function of its inputs. Logistic regression, both native SVMs, LDA and ridge are RNG-free outright.
  • Threading changes wall time, never answers. Threaded runs are bit-identical to sequential at matched seed for any thread count — across the sequential, floating, Monte Carlo, exhaustive, and all four Branch & Bound searches, and the stochastic sampled evaluator. --threads 8 is free of scientific cost.
  • A golden gate. A 112-scenario golden-output suite pins the entire CLI surface byte-for-byte (106 scenarios in the default run plus a slow tier for the hours-long protocols). If a result can drift, the gate fails.

The single deliberate exception is the external bridge — beyond the protocol line, determinism is your tool's job.

09 · From experiment to pipeline

Reports for humans, streams for machines

--report detailed upgrades any run into a small research report: the best-per-size criterion curve with holdout re-validation of every subset (the overfitting curve of section 03), tabular ROC and precision–recall curves of the validating classifier, confusion matrices, and any hyperparameters the run tuned. --curves-svg renders those curves as a self-contained picture — no plotting library, nothing fetched from anywhere — and keeps the criterion and the holdout on separate axes, so the overfitting panel cannot be misread as one quantity. --dump-scores writes the per-sample holdout predictions and class scores, for error analysis or an ROC variant of your own. For automation, the CLI is pipe-clean by design in both directions: with --output json or csv, all prose goes to stderr and stdout carries only the result — ./fst4 … --output json | jq just works — while --data - takes the dataset from standard input, so fst4 sits in the middle of a pipeline like any other filter: zcat data.trn.gz | ./fst4 --data - --format trn … | jq. --progress json emits one JSON line per improved solution for live dashboards, config files capture whole scenarios (--config demos/demo10.cfg), and --seed makes every pipeline rerun byte-comparable to the last.

10 · See for yourself

A runnable tour

Every capability on this page ships as a ready-to-run config — ./fst4 --config demos/demoNN.cfg from the repository root; the demo datasets are included in data/ (see data/readme.txt). A guided selection:

configwhat it shows
demo10.cfgfilter selection + independent validation — the basic protocol
demo14.cfgbalanced vs. plain accuracy changing what gets selected (IATI ≈ 0.14)
demo15.cfg / demo16.cfgAUC-driven selection / cost-sensitive selection (--class-weights)
demo17.cfg / demo19.cfgregression selection by R² — neighbours / ridge
demo18.cfg / demo38.cfgRandom Forest vs. linear-SVM wrappers on spam data
demo29.cfgRBF-SVM wrapper with built-in C/γ tuning
demo41.cfgoptimal Branch & Bound
demo34.cfgDAF ranking at high dimensionality
demo36.cfg / demo37t.cfgbudgeted stochastic search at 5 000 and 10 105 features
demo54.cfg / demo55.cfgselection stability / process-similarity comparison
demo64.cfgyour-own-model selection via the external bridge

11 · Ninety seconds

Getting started

# build (CMake ≥ 3.x, any C++17 compiler; no external libraries needed):
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build --parallel --target fst4

# first result:
build/bin/fst4 --config demos/demo10.cfg

# what else is in the box:
build/bin/fst4 --list
build/bin/fst4 --help

From there: doc/FST4_CLI.md is the complete flag reference, the ready-to-run configs in demos/ map protocols to command lines, and the C++ API is documented in the source headers (Doxygen configuration in doc/). The library itself is header-only — C++ consumers include and go; the demo sources in demos/ show the idiom.

12 · The fine print, honestly

Scope, limits, dependencies, licensing

  • The Gaussian-model filters are two-class only, and all normal-model components fail — loudly, not silently — on singular covariances: constant features, or subsets larger than class sample counts. The multinomial filters and all wrappers handle any class count.
  • Wrapper selection costs classifier trainings by the thousand; that is what the hybrid evaluator, threading, and the budgeted stochastic search are for.
  • The optimality of Branch & Bound is exactly as good as the monotonicity of the criterion — FST4 warns when the property is not declared.
  • Dependencies: the core is header-only C++17 with zero mandatory external libraries. Building the fst4 command-line tool additionally uses CLI11 for argument parsing — vendored in-tree (BSD-3-licensed), so there is nothing to fetch, and library-only consumers carry no CLI11 obligations. Exactly two optional external libraries exist — LibSVM (enables the wrapper-svm kernel zoo; drop-in, auto-detected) and Eigen (optional swap-in for the internal matrix kernels, used to cross-verify the built-in solvers — which it matches at print precision). Everything else, Random Forest to ridge to both native SVMs, is implemented in the library itself, keeping the licensing story a one-liner.
  • License: FST4 is Copyright © 2000–present ÚTIA (Institute of Information Theory and Automation of the Czech Academy of Sciences) and dual-licensed. It is free software under the GNU Affero General Public License, version 3 (the full text ships as LICENSE): anyone, including commercial users, may use, modify and redistribute it, provided that modified versions they distribute or serve over a network stay under the AGPL with complete source. Organizations that want FST4 in closed-source products without those obligations need a commercial license from ÚTIA. LICENSING.md explains both options and the contributor license agreement. In case of interest in a commercial license, or with any licensing question, write to utia@utia.cas.cz; technical questions and pull requests: somol@utia.cas.cz, with "FST" in the subject line.
Copyright © 2000–present ÚTIA, Institute of Information Theory and Automation of the Czech Academy of Sciences · Contacts · This site sets no cookies and uses no analytics.