Configure K-Step
The K-Step is the clustering stage of the KFC Procedure.
Its job is to construct several candidate partitions of the same input data by
running one BregmanKMeans model for every configured Bregman divergence.
The implementation is provided by:
Most users configure it indirectly through:
using the arguments:
Basic configuration
A minimal K-Step configuration uses one divergence:
from kfc_procedure import KFCRegressor
model = KFCRegressor(
divergences=["euclidean"],
local_model="ridge",
combiner="mean",
n_clusters=3,
random_state=42,
)
During fitting, KFC creates a KStep equivalent to:
KStep(
divergences=["euclidean"],
divergences_params={},
n_clusters=3,
max_iter=300,
tol=1e-4,
random_state=42,
)
The K-Step then fits one BregmanKMeans instance.
Why several divergences?
Ordinary K-means assumes squared-Euclidean geometry.
KFC does not commit to a single geometry. Instead, it can fit several clusterings independently:
┌─ Euclidean ─────► partition 1
│
Input X ─────────────────┼─ GKL ───────────► partition 2
│
├─ Logistic ──────► partition 3
│
└─ Itakura-Saito ─► partition 4
Each partition is passed independently to the F-Step.
The K-Step itself does not attempt to reconcile or vote between cluster assignments.
Mathematical view
For divergences
the K-Step fits
where \(K\) is the configured number of clusters.
Each model produces its own cluster assignment
Therefore the same observation can belong to different clusters under different divergences.
Built-in divergences
The package currently registers four divergences.
| Name | Class | Exponential-family interpretation | Domain |
|---|---|---|---|
euclidean |
SquaredEuclidean |
Gaussian | \(\mathbb{R}^d\) |
gkl |
GKLDivergence |
Poisson | \((0,\infty)^d\) |
logistic |
LogisticLoss |
Bernoulli / Binomial | \((0,1)^d\) |
is |
ItakuraSaito |
Exponential / Gamma | \((0,\infty)^d\) |
Importing the divergence package registers these names with
BregmanDivergenceFactory.
Squared Euclidean
Use:
The generator is
and the divergence is
Its domain is unrestricted:
This is the safest starting point for arbitrary numerical features.
model = KFCRegressor(
divergences=["euclidean"],
local_model="ridge",
combiner="mean",
random_state=42,
)
Generalized KL
Use:
The implementation uses
The required domain is
Example:
Warning
Zero and negative feature values are rejected by the domain check.
Logistic divergence
Use:
The implementation corresponds to the Bernoulli/Binomial Bregman divergence:
Its domain is strictly
Example:
model = KFCClassifier(
divergences=["logistic"],
local_model="logistic_regression",
combiner="majority_vote",
)
Warning
Values equal to 0 or 1 are outside the domain.
Itakura–Saito
Use:
The implementation uses
Its required domain is
Example:
Using all divergences together
A common KFC configuration is:
Because every divergence receives the same input matrix, the data must satisfy the intersection of all selected domains.
For all four built-ins, that means:
A practical preprocessing strategy is:
from sklearn.preprocessing import MinMaxScaler
eps = 1e-6
scaler = MinMaxScaler(
feature_range=(eps, 1.0 - eps),
)
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)
Then:
model = KFCRegressor(
divergences=[
"euclidean",
"gkl",
"logistic",
"is",
],
local_model="ridge",
combiner="weighted_mean",
n_clusters=3,
random_state=42,
)
model.fit(X_train, y_train)
Domain validation
BregmanKMeans.fit() validates the training data before clustering.
It rejects:
NaN,- infinite values,
- values outside the selected divergence's domain.
The validation logic raises errors such as:
or:
Prediction also evaluates the divergence against fitted centroids, so new data must remain inside the same valid domain.
Important
Apply exactly the same preprocessing to training and inference data.
n_clusters
The number of clusters is configured with:
and the same value is used for every divergence.
Example:
model = KFCRegressor(
divergences=[
"euclidean",
"gkl",
],
local_model="ridge",
combiner="mean",
n_clusters=5,
)
This fits:
Effect on the F-Step
If there are \(M\) divergences and \(K\) clusters, the next stage can fit up to
local predictive models.
So increasing n_clusters increases model locality and model count.
Choosing the number of clusters
A smaller value gives:
A larger value gives:
For classification, very large values can create clusters containing only one class, which may make some local classifiers impossible to fit.
The current KFCProcedure does not automatically tune n_clusters.
Evaluate it externally if it is an important hyperparameter.
Number of samples constraint
BregmanKMeans.fit() explicitly checks:
Therefore:
must hold for the internal K-Step training subset.
Remember that KFC uses only approximately half of the outer training dataset for K-Step/F-Step fitting.
How Bregman K-means works
Each divergence-specific model uses a Lloyd-style iterative algorithm.
For one initialization:
initialize centroids
│
▼
compute divergence to every centroid
│
▼
assign each sample to nearest centroid
│
▼
recompute centroids
│
▼
measure distortion
│
├── converged? -> stop
│
└── no -> repeat
The assignment step is:
The implementation updates each cluster centroid using the arithmetic mean of the assigned samples.
Centroid update
For cluster \(k\), the implementation computes
This arithmetic-mean update is used for every built-in divergence in the
current BregmanKMeans implementation.
Empty clusters
During an iteration, a cluster may receive no samples.
The implementation handles this case by reinitializing that centroid with a random training observation:
This prevents division-by-zero failures and lets the Lloyd iterations continue.
Initialization
BregmanKMeans chooses initial centroids by sampling data points without
replacement:
The code comments refer to this as a k-means++-style initialization, but the current implementation is specifically uniform random sampling of training observations, not the probability-weighted k-means++ seeding algorithm.
Current source behavior
Treat the initialization as random data-point initialization.
Multiple initializations
BregmanKMeans has:
by default.
For every initialization it runs the full Lloyd loop and retains the solution with the smallest final average distortion.
Conceptually:
initialization 1 -> distortion d1
initialization 2 -> distortion d2
...
initialization 10 -> distortion d10
│
▼
keep minimum
K-Step limitation
KStep does not currently expose an n_init parameter.
It creates BregmanKMeans without overriding n_init, so each
divergence-specific clustering currently uses the internal default:
max_iter
Configure the maximum number of Lloyd iterations per initialization with:
Example:
model = KFCRegressor(
divergences=["euclidean"],
local_model="ridge",
combiner="mean",
n_clusters=3,
max_iter=500,
)
This value is forwarded by KStep to each BregmanKMeans.
tol
The convergence tolerance defaults to:
After each iteration, the implementation computes relative distortion change:
The run stops when this is below tol.
Example:
A smaller tolerance can require more iterations before convergence.
Distortion and inertia_
For the current Bregman implementation, distortion is the average minimum divergence to a centroid:
The best run stores this value in:
Despite the familiar inertia_ name, this value is not necessarily the
classical Euclidean sum of squared distances.
It is the selected model's average Bregman distortion in the current implementation.
Memory-safe distortion calculation
During fitting, distortion is calculated in blocks:
through _distortion_stream().
This avoids allocating one very large sample-by-centroid distance matrix solely for the distortion calculation.
The assignment step itself still computes the full distance matrix for the current Lloyd iteration.
random_state
Use:
for reproducible centroid initialization and empty-cluster reinitialization.
model = KFCClassifier(
divergences=["euclidean"],
local_model="logistic_regression",
combiner="majority_vote",
n_clusters=3,
random_state=42,
)
The same K-Step random seed is passed to every divergence-specific
BregmanKMeans.
This means each clustering model uses reproducible pseudo-random initializations when the same data and configuration are reused.
Multiple divergences are fitted independently
The K-Step loops through:
There is no interaction between divergence-specific cluster models.
Therefore:
Euclidean clustering ─┐
GKL clustering ───────┼── independent
Logistic clustering ──┤
IS clustering ────────┘
The outputs are combined only later through the F-Step and C-Step.
Fitted attributes
After KFC has been fitted:
contains the fitted K-Step.
Its primary fitted attributes are:
| Attribute | Description |
|---|---|
models_ |
dictionary of fitted BregmanKMeans models |
clusters_ |
training cluster labels for each divergence |
Inspect fitted models
For string-based divergences, you may see:
Inspect one fitted model:
euclidean_model = kstep.models_["euclidean"]
print(euclidean_model.cluster_centers_)
print(euclidean_model.labels_)
print(euclidean_model.inertia_)
print(euclidean_model.n_iter_)
Inspect training cluster assignments
for divergence, labels in model.kstep_.clusters_.items():
print(
divergence,
labels.shape,
set(labels),
)
Example:
The labels are later used by the F-Step to fit one local predictive model per cluster.
Predict cluster assignments
After fitting:
returns a dictionary:
Each array has shape:
Example:
clusters = model.kstep_.predict(X_test)
for divergence, labels in clusters.items():
print(
divergence,
labels[:10],
)
Distance to centroids
The underlying BregmanKMeans also implements:
which returns the divergence from every sample to every centroid.
Example:
With three clusters:
and:
The predicted cluster is the minimum-distance column.
Using a divergence object directly
KStep accepts either:
or instances of:
Example:
from kfc_procedure.core.clustering.divergences import (
SquaredEuclidean,
)
div = SquaredEuclidean()
model = KFCRegressor(
divergences=[div],
local_model="ridge",
combiner="mean",
)
For a divergence instance, _resolve() simply returns the object rather than
creating one through the factory.
Naming of divergence objects
When a string is supplied, the K-Step dictionary key is simply the lower-case string:
When a divergence object is supplied, the key comes from its name attribute
when available.
For the built-ins, object names include:
SquaredEuclidean.name -> "Euclidean"
GKLDivergence.name -> "GKL"
LogisticLoss.name -> "Logit"
ItakuraSaito.name -> "Ita"
so direct objects can lead to keys such as:
rather than the factory identifiers:
Object names and divergences_params
If you pass divergence instances, the K-Step does not use
divergences_params to reconfigure them.
Configure the object itself before supplying it.
divergences_params
When divergences are supplied by string, K-Step resolves each one as:
params = self.divergences_params.get(name, {})
divergence = BregmanDivergenceFactory.create(
div,
**params,
)
Example:
model = KFCRegressor(
divergences=[
"euclidean",
"gkl",
],
divergences_params={
"euclidean": {
"validate_domain": True,
},
"gkl": {
"validate_domain": True,
},
},
local_model="ridge",
combiner="mean",
)
The built-in divergences currently have very few practical constructor
hyperparameters, so divergences_params becomes more useful when adding
custom divergence implementations.
Custom divergence
A custom divergence must subclass:
and implement at least:
A minimal structure is:
import numpy as np
from kfc_procedure.core.clustering.divergences import (
BaseBregmanDivergence,
)
class MyDivergence(BaseBregmanDivergence):
name = "my_divergence"
family = "custom"
def in_domain(self, X):
return np.isfinite(X).all()
def phi(self, X):
X = np.asarray(X, dtype=float)
# return one generator value per sample
...
def grad_phi(self, X):
X = np.asarray(X, dtype=float)
# return gradient with same feature dimension
...
Then pass an instance:
The base class provides a generic pairwise Bregman distance() implementation.
Registering a custom divergence
For factory-based use, register a subclass with
BregmanDivergenceFactory.
Conceptually:
from kfc_procedure.core.clustering.divergences import (
BregmanDivergenceFactory,
)
@BregmanDivergenceFactory.register("my_divergence")
class MyDivergence(BaseBregmanDivergence):
...
Then it can be selected by name:
Current verbose behavior
KStep exposes:
and BregmanKMeans itself also has a verbose argument that can print
iteration diagnostics such as:
However, in the current source, KStep.fit() creates BregmanKMeans without
passing its own verbose value:
model = BregmanKMeans(
divergence=divergence,
n_clusters=self.n_clusters,
max_iter=self.max_iter,
tol=self.tol,
random_state=self.random_state,
)
Therefore:
does not currently enable the underlying Bregman K-means iteration output.
Current implementation limitation
The verbose field exists on KStep, but is not forwarded to
BregmanKMeans in the current source version.
Parameter summary
| Parameter | Default | Effect |
|---|---|---|
divergences |
required | clustering geometries to fit |
divergences_params |
{} |
constructor kwargs for string divergences |
n_clusters |
3 |
clusters per divergence |
max_iter |
300 |
maximum Lloyd iterations per initialization |
tol |
1e-4 |
relative distortion convergence tolerance |
verbose |
False |
stored by KStep; not currently forwarded |
random_state |
None |
centroid initialization reproducibility |
The underlying BregmanKMeans additionally uses:
but K-Step does not currently expose this parameter.
Recommended starting configuration
For arbitrary numeric inputs:
model = KFCRegressor(
divergences=["euclidean"],
local_model="ridge",
combiner="mean",
n_clusters=3,
max_iter=300,
tol=1e-4,
random_state=42,
)
For data safely scaled into \((0,1)\):
model = KFCRegressor(
divergences=[
"euclidean",
"gkl",
"logistic",
"is",
],
local_model="ridge",
combiner="weighted_mean",
n_clusters=3,
max_iter=300,
tol=1e-4,
random_state=42,
)
Debugging K-Step
If fitting fails, check these items first.
1. Are all values finite?
Expected:
2. Does the input satisfy the divergence domain?
For GKL or Itakura–Saito:
For logistic:
3. Is n_clusters too large?
The internal K-Step subset contains only about half of the outer training data.
Ensure:
and remember that useful local models generally need substantially more than one sample per cluster.
4. Did every divergence fit?
After successful fitting:
Compare the result with the divergence list you requested.
5. Inspect distortions
for name, km in model.kstep_.models_.items():
print(
name,
"distortion=",
km.inertia_,
"iterations=",
km.n_iter_,
)
Do not directly compare distortion magnitudes across fundamentally different divergences as if they were on a common metric scale.
K-Step and F-Step connection
The K-Step returns cluster labels:
The F-Step then uses those labels to route observations to their local models:
The K-Step does not itself make supervised predictions.
Its output is the routing structure used by the F-Step.
Mental model
K-Step asks several clustering geometries to provide several plausible partitions of the same observations.