Interactive adversarial ML demo for MNIST digit classification. The browser shows FGSM on real test-set digits, and the Python tooling evaluates the same exported CNN against random noise, FGSM, and PGD.
Live demo | Course presentation
This project was built for CS38300 Ethics in Machine Learning. The goal is to make adversarial examples visible: start with a real MNIST test-set image, compute an input-gradient perturbation, and watch a compact CNN's prediction change as epsilon increases.
- The selected digit images are real MNIST test-set samples from
public/mnist_samples.json. - The browser loads exported CNN weights from
public/model_weights.json. - FGSM is computed in TensorFlow.js with the gradient of the loss with respect to the input image.
- Model metrics are exported to
public/model_metrics.json. scripts/evaluate_attacks.pyloads the exported weights and compares random noise, FGSM, and PGD over multiple epsilon values.- Attack results are written to
public/attack_results.json,public/attack_results.md, andpublic/examples/attack_comparison.png. scripts/train_mnist.pytrains the model and exports the public JSON assets used by the browser.
- This is an educational demo, not a robust adversarial-ML benchmark.
- The model is a compact two-epoch CNN, not an adversarially trained classifier.
- The UI shows one curated test sample per digit so the attack is easy to inspect.
- The offline evaluation uses a fixed 2,000-example subset by default so it can run quickly on a laptop.
Architecture:
Input 28 x 28 x 1
Conv2D(32, 3x3, ReLU, same padding)
MaxPool(2x2)
Conv2D(64, 3x3, ReLU, same padding)
MaxPool(2x2)
Dense(128, ReLU)
Dropout(0.25 during training)
Dense(10, Softmax)
Current exported FGSM evaluation from the full MNIST test set:
| FGSM epsilon | Test accuracy |
|---|---|
| 0.00 | 98.54% |
| 0.05 | 95.85% |
| 0.10 | 88.81% |
| 0.15 | 74.43% |
| 0.20 | 52.65% |
| 0.25 | 28.92% |
| 0.30 | 11.62% |
The comparison below was generated with:
python3 scripts/evaluate_attacks.py --limit 2000 --pgd-steps 10It uses the same exported weights as the browser demo. Random noise is included as a sanity check, not as an adversarial method.
| Attack | 0.00 | 0.05 | 0.10 | 0.15 | 0.20 | 0.25 | 0.30 |
|---|---|---|---|---|---|---|---|
| Random noise | 97.90% | 97.80% | 97.95% | 97.80% | 97.90% | 97.75% | 97.55% |
| FGSM | 97.90% | 94.35% | 86.25% | 68.85% | 47.50% | 24.00% | 9.30% |
| PGD | 97.90% | 94.00% | 79.90% | 47.25% | 9.85% | 0.15% | 0.00% |
The model first classifies the selected test-set digit with high confidence.
The app computes:
x_adv = clip(x + epsilon * sign(grad_x loss(model, x, y)))
The perturbation view shows which pixels are brightened or darkened.
At higher epsilon values, the adversarial image can still look like the original digit to a human while the classifier's top prediction changes.
npm install
npm run devOpen http://localhost:3000.
GitHub Actions runs TypeScript checks, a production build, and the Python tests on pushes and pull requests.
The app does not need Python to run. Python is only needed if you want to retrain/export the model assets.
python3 -m pip install numpy pillow pytest torch
python3 scripts/train_mnist.py
python3 scripts/evaluate_attacks.py --limit 2000 --pgd-steps 10
python3 -m pytest -qThe script writes:
public/model_weights.jsonpublic/model_metrics.jsonpublic/mnist_samples.jsonpublic/attack_results.jsonpublic/attack_results.mdpublic/examples/attack_comparison.png
- Next.js
- React
- TypeScript
- Tailwind CSS
- TensorFlow.js
- PyTorch
- GitHub Actions
Goodfellow et al. (2015). Explaining and Harnessing Adversarial Examples. ICLR. arXiv:1412.6572



