AFK-MC²: Adaptive Fast k-MC²
Overview
AFK-MC² (Adaptive Fast k-Markov Chain Monte Carlo squared) is a k-means++ initialization algorithm that uses Markov Chain Monte Carlo (MCMC) sampling to approximate the D² distribution without computing all pairwise distances.
The algorithm achieves speedup by:
Using MCMC to sample from the D² distribution
Computing only distances needed for the Markov chain
Adapting chain length based on data characteristics
When to use:
Medium to large datasets (1,000 < n < 100,000)
When you want a simpler alternative to RS-k-means++
Good balance between speed and implementation simplicity
When FAISS is not available or desired
Key advantages:
✅ Sublinear time complexity per center
✅ No need for auxiliary data structures (unlike RS-k-means++)
✅ Provable approximation guarantees
✅ Simple to understand and implement
Algorithm Details
AFK-MC² uses a Markov chain to sample from the D² distribution:
Start at a random point
Run a Markov chain for
mstepsAccept/reject based on D² probabilities
Return the final state as the sampled center
Key insight: A Markov chain with proper transition probabilities converges to the D² distribution. We don’t need to compute all distances, only those visited by the chain.
Complexity:
Per center: \(O(m \\cdot d)\) where m is the chain length
Total: \(O(k \\cdot m \\cdot d)\) vs \(O(nkd)\) for standard k-means++
Typically \(m \\ll n\), giving significant speedup
Python API
- kmeans_seeding.afkmc2(X, n_clusters, *, chain_length=200, index_type='Flat', random_state=None)[source]
Initialize cluster centers using AFK-MC² MCMC sampling.
- Parameters:
X (array-like of shape (n_samples, n_features)) – Training data
n_clusters (int) – Number of clusters to initialize
chain_length (int, optional) – Length of the Markov chain per center. Longer chains give better quality but take more time. Default: 200
index_type (str, optional) – FAISS index type for final label assignment (not used in sampling). Options:
'Flat','LSH','HNSW'Default:'Flat'random_state (int, optional) – Random seed for reproducibility
- Returns:
Initial cluster centers
- Return type:
ndarray of shape (n_clusters, n_features)
- Raises:
ValueError – If n_samples < n_clusters or invalid parameters
Examples:
Basic usage:
from kmeans_seeding import afkmc2 import numpy as np X = np.random.randn(10000, 50) centers = afkmc2(X, n_clusters=100)
With custom chain length:
# Faster, slightly lower quality centers = afkmc2(X, n_clusters=100, chain_length=100, random_state=42) # Higher quality, slower centers = afkmc2(X, n_clusters=100, chain_length=500, random_state=42)
Complete clustering pipeline:
from kmeans_seeding import afkmc2 from sklearn.cluster import KMeans # Initialize with AFK-MC² centers = afkmc2(X, n_clusters=100, chain_length=200, random_state=42) # Run k-means kmeans = KMeans(n_clusters=100, init=centers, n_init=1) labels = kmeans.fit_predict(X)
How It Works
Markov Chain Construction
AFK-MC² constructs a Markov chain with the following properties:
States: All points in the dataset \(X\)
Transition probability: From point \(x\) to \(y\):
\[\begin{split}P(x \\to y) = \\frac{1}{2n} + \\frac{1}{2} \\cdot \\frac{\\Delta(y, S)}{\\sum_{z} \\Delta(z, S)}\end{split}\]Stationary distribution: The D² distribution (after convergence)
Intuition: The chain randomly walks through the dataset, with bias toward points far from existing centers.
Sampling Process
For each center \(c_i\):
Start at a random point \(x_0\)
For \(t = 1\) to
chain_length:Propose: Move to random point \(y\)
Accept: Based on Metropolis-Hastings criterion
Update: \(x_t = y\) if accepted, else \(x_t = x_{t-1}\)
Return \(x_{\\text{chain\_length}}\) as the new center
Key advantage: Only compute distances for points visited by the chain (≪ n points).
Parameter Tuning
chain_length
The most important parameter controlling quality vs speed tradeoff:
- Low (50-100):
Fastest execution
Chain may not fully converge
Slightly lower clustering quality
Use for quick experiments or when speed is critical
- Medium (200-300) [Recommended]:
Good balance
Chain typically converges
Near-optimal quality
Default: 200
- High (500-1000):
Best quality
Slower execution
Diminishing returns beyond ~500
Use when quality is paramount
# Fast mode
centers = afkmc2(X, n_clusters=100, chain_length=100)
# Balanced mode (recommended)
centers = afkmc2(X, n_clusters=100, chain_length=200)
# Quality mode
centers = afkmc2(X, n_clusters=100, chain_length=500)
Rule of thumb: Use chain_length ≈ O(log k) as a baseline, then scale based on quality needs.
Adaptive Chain Length
For different numbers of clusters:
import numpy as np
def adaptive_chain_length(k):
\"\"\"Suggest chain length based on number of clusters.\"\"\"
return max(100, int(200 * np.log(k + 1)))
k = 500
centers = afkmc2(X, n_clusters=k,
chain_length=adaptive_chain_length(k))
Performance Characteristics
Time Complexity
Operation |
AFK-MC² |
Standard k-means++ |
|---|---|---|
Per center |
\(O(m \\cdot d)\) |
\(O(n \\cdot d)\) |
Total initialization |
\(O(k \\cdot m \\cdot d)\) |
\(O(n \\cdot k \\cdot d)\) |
Speedup |
\(n / m\) |
1× |
- Example: For n=100,000, k=100, m=200, d=50:
AFK-MC²: ~1 million ops
k-means++: ~500 million ops
Speedup: ~500×
Quality vs Speed Tradeoff
import numpy as np
import time
from sklearn.cluster import KMeans
from kmeans_seeding import afkmc2
X = np.random.randn(50000, 100)
chain_lengths = [50, 100, 200, 500, 1000]
for m in chain_lengths:
start = time.time()
centers = afkmc2(X, n_clusters=200, chain_length=m)
init_time = time.time() - start
# Measure quality (k-means cost)
kmeans = KMeans(n_clusters=200, init=centers, n_init=1, max_iter=1)
kmeans.fit(X)
cost = kmeans.inertia_
print(f"m={m:4d}: {init_time:.3f}s, cost={cost:.2e}")
Comparison with Other Algorithms
vs Standard k-means++
- Advantages:
Much faster (10-100× speedup)
Sublinear time per center
Similar clustering quality
- Disadvantages:
Randomness from MCMC (need longer chains for consistency)
Theoretical guarantees are probabilistic
vs RS-k-means++
- Advantages:
Simpler: no auxiliary data structures needed
More predictable behavior
Easier to tune (only one main parameter)
- Disadvantages:
Slightly slower for very large datasets
Less flexible (no index type options)
Lower quality with short chain lengths
Recommendation: Use AFK-MC² when simplicity is valued and dataset size is moderate (< 100K points).
Practical Tips
Start with default parameters:
centers = afkmc2(X, n_clusters=k) # Use defaults
Scale chain_length with k:
m = int(200 * np.log(k + 1)) centers = afkmc2(X, n_clusters=k, chain_length=m)
Normalize your data:
from sklearn.preprocessing import StandardScaler X_scaled = StandardScaler().fit_transform(X) centers = afkmc2(X_scaled, n_clusters=k)
Use multiple initializations:
from sklearn.cluster import KMeans best_inertia = float('inf') best_labels = None for seed in range(10): centers = afkmc2(X, n_clusters=k, random_state=seed) kmeans = KMeans(n_clusters=k, init=centers, n_init=1) labels = kmeans.fit_predict(X) if kmeans.inertia_ < best_inertia: best_inertia = kmeans.inertia_ best_labels = labels
Batch processing for very large datasets:
# Sample subset for initialization n_sample = min(50000, len(X)) indices = np.random.choice(len(X), n_sample, replace=False) X_sample = X[indices] # Initialize on sample centers = afkmc2(X_sample, n_clusters=k) # Use on full dataset kmeans = KMeans(n_clusters=k, init=centers, n_init=1) labels = kmeans.fit_predict(X)
Theoretical Background
AFK-MC² provides an \(O(\\log k)\) approximation to the optimal k-means cost with high probability, assuming the Markov chain is run for \(O(\\log k)\) steps.
Approximation guarantee:
with probability at least \(1 - 1/k\), where:
\(\\Phi(S)\) is the k-means cost for centers \(S\)
\(\\Phi_k(X)\) is the optimal k-means cost
Key result: The chain converges to the D² distribution in \(O(\\log n)\) steps (mixing time).
References
Bachem, O., Lucic, M., Hassani, H., & Krause, A. (2016). “Approximate k-means++ in sublinear time.” AAAI Conference on Artificial Intelligence.
Bachem, O., Lucic, M., & Krause, A. (2016). “Distributed and provably good seedings for k-means in constant rounds.” ICML 2017.
See Also
RS-k-means++: Rejection Sampling - Faster alternative using rejection sampling
Fast-LSH k-means++ - LSH-based approach
Algorithm Comparison - Detailed algorithm comparison