Generate realistic handwritten digits using classical : Kernel Density Estimation + PCA + Rejection Sampling. A lightweight alternative to GANs that's 40ร faster to train and 10ร smaller.
๐ฎ Live Demo | ๐ Documentation | ๐ค Contributing
- ๐ฏ Conditional Generation: Choose exactly which digit to generate (0-9).
- ๐จ High Quality: Rejection sampling + image cleaning for artifact-free results.
- ๐พ Lightweight: Models are 5-15 MB (10-100ร smaller than GANs).
- ๐ฌ Classical ML: Uses PCA + KDE instead of neural networks.
- โ๏ธ Auto-Tuned: Bandwidth optimization via cross-validation.
- ๐ Web Interface: Real-time generation in your browser.
- PCA: Dimensionality reduction (784D โ 50D) retaining ~82% variance.
- KDE: Kernel Density Estimation with Gaussian kernel.
- Rejection Sampling: Three quality levels (Light/Medium/Strict).
- Image Cleaning: Bilateral denoising + morphological operations.
- Two Architectures: Global (single model) vs Conditional (one per digit).
100 unique digits generated with our conditional model
Before and after: image cleaning
- Python 3.8+
- pip
- 2GB RAM minimum
# Clone the repository
git clone https://github.com/sofianebeloucif/ThisNumberDoesNotExist.git
cd ThisNumberDoesNotExist
# Create virtual environment (recommended)
python -m venv venv
source venv/bin/activate # Linux/Mac
# or
venv\Scripts\activate # Windows
# Install dependencies
pip install -r requirements.txt# Launch Jupyter
jupyter notebook notebooks/train_and_compare.ipynbThe notebook will:
- โ Auto-optimize bandwidth via 5-fold cross-validation
- โ Train both Global and Conditional generators
- โ Compare performance (speed, size, quality)
- โ Generate visualizations
- โ
Save models to
models/
โฑ๏ธ Training time: ~3-5 minutes on CPU
cd app
python app.pyOpen: http://localhost:5000
- Mode Selection: Global (random) or Conditional (choose digit)
- Digit Picker: Select 0-9 (conditional mode only)
- Rejection Sampling: Toggle quality filtering
- Image Cleaning: Remove artifacts (light/medium/aggressive)
from src.generator import GlobalGenerator, ConditionalGenerator
# --- Global Generator ---
global_gen = GlobalGenerator.load('models/global_generator.pkl')
# Generate 10 random digits
images = global_gen.generate(
n_samples=10,
use_rejection=True,
percentile=25,
clean_images=True,
cleaning_method='medium'
)
# --- Conditional Generator ---
cond_gen = ConditionalGenerator.load('models/conditional_generator.pkl')
# Generate 10 sevens
sevens = cond_gen.generate(
digit=7,
n_samples=10,
use_rejection=True,
percentile=25,
clean_images=True,
cleaning_method='medium'
)
# Generate all digits (10 of each)
all_digits = cond_gen.generate_all(n_samples_per_digit=10)MNIST (60k images, 28ร28)
โ
[ PCA: 784D โ 50D ] (~82% variance retained)
โ
[ KDE: Density Estimation ] (Gaussian kernel, optimized bandwidth)
โ
[ Sampling + Rejection ] (Filter by log-likelihood)
โ
[ PCA Inverse: 50D โ 784D ]
โ
[ Image Cleaning ] (Denoise + threshold + morphology)
โ
Generated Image (28ร28)
| Architecture | Description | Model Size | Training Time | Use Case |
|---|---|---|---|---|
| ๐ Global | Single KDE for all digits | ~5 MB | ~3s | Random generation |
| ๐ฏ Conditional | 10 KDE (one per digit) | ~15 MB | ~10s | Targeted generation |
Improve generation quality by filtering samples based on log-likelihood.
| Level | Percentile | Acceptance Rate | Speed |
|---|---|---|---|
| ๐ข Light | 10% | ~85% | Fast โก |
| ๐ก Medium | 25% | ~65% | Normal |
| ๐ด Strict | 50% | ~45% | Slower |
Formula: $$ \text{Accept if: } \log p(x) \geq \text{threshold}_{\text{percentile}} $$
Where
Post-process generated images to eliminate artifacts.
| Method | Pipeline | Effect | Speed |
|---|---|---|---|
| ๐ข Light | Threshold (0.2) | Minimal cleanup | Fast |
| ๐ก Medium | Threshold (0.25) + Small components removal | Balanced | Normal |
| ๐ด Aggressive | Bilateral denoise + Threshold (0.3) + Morphology | Maximum quality but risk of degradation | Slower |
Recommended: Medium for general use, Aggressive if many artifacts persist.
Kernel Density Estimation (KDE) $$ \hat{f}(x) = \frac{1}{nh} \sum_{i=1}^{n} K\left(\frac{x - x_i}{h}\right) $$
Where:
-
$K$ is the Gaussian kernel -
$h$ is the bandwidth (auto-optimized via grid search) -
$n$ is the number of training samples
Cross-Validation for Bandwidth
bandwidths = np.linspace(0.5, 2.5, 10)
grid = GridSearchCV(KernelDensity(), {'bandwidth': bandwidths}, cv=5)
grid.fit(data)
optimal_bandwidth = grid.best_params_['bandwidth']Similar to terrain generation, our model maps the latent space into "digit biomes":
if log_density < threshold_10% โ Reject
elif log_density < threshold_25% โ Accept (Light)
elif log_density < threshold_50% โ Accept (Medium)
else โ Accept (Strict)
See Technical Documentation for deep dive.
Contributions welcome! See CONTRIBUTING.md.
- Fashion-MNIST support
- CIFAR-10 (color images)
- FID/IS metrics
- Docker container
- Latent space interpolation
- Style transfer
- Multi-modal generation (digits + letters)
- Mobile app (iOS/Android)
This project is licensed under the MIT License - see LICENSE for details.
Sofiane Beloucif
- ๐ Portfolio: sofianebeloucif.com
- ๐ผ GitHub: @SofianeBeloucif
- ๐ Live Demo: ThisNumberDoesNotExist
- Yann LeCun for MNIST dataset: http://yann.lecun.com/exdb/mnist/
- scikit-learn team for excellent ML tools: https://scikit-learn.org/
- ThisPersonDoesNotExist for inspiration: https://thispersondoesnotexist.com/
- Sebastian Lague's procedural generation tutorials
If you use this project in your research, please cite:
@misc{thisnumberdoesnotexist2024,
author = {Beloucif, Sofiane},
title = {This Number Does Not Exist: MNIST Generation with PCA + KDE},
year = {2024},
publisher = {GitHub},
url = {https://github.com/sofianebeloucif/ThisNumberDoesNotExist}
}Made with โค๏ธ and lots of โ
๐ฎ Try the Demo โข ๐ Read the Docs โข ๐ Report Bug
