This project builds a system to detect AI-generated images using the CIFAKE dataset, which contains:
- REAL images: sampled from CIFAR-10
- FAKE images: generated by Stable Diffusion
We compare a classical computer vision baseline (HOG + Linear SVM) against a deep learning approach (CNN), then extend the CNN with adversarial robustness and expose it via a FastAPI backend that accepts image uploads and returns REAL/FAKE predictions. A React + TypeScript frontend provides a modern upload UI with model selection, explanations, and lightweight analytics (history + session stats).
Tech stack:
- ML / Training: Python, PyTorch, scikit-learn
- Backend: FastAPI, Uvicorn
- Frontend: React + TypeScript (single-page upload UI)
- Infrastructure / Deployment: Docker, Docker Compose, MongoDB Atlas, AWS EC2 (hosting), optional S3 for model storage
This will start both the FastAPI backend and the React frontend so you can upload any image and get a REAL/FAKE prediction.
-
Create & activate virtualenv
From the project root:
python -m venv .venv source .venv/bin/activate # macOS / Linux # .venv\Scripts\activate # Windows
-
Install Python dependencies
pip install -r ml/requirements-ml.txt
-
Download CIFAKE dataset
export KAGGLE_API_TOKEN="KGAT_..." # your Kaggle token python ml/download_cifake.py
This prints a
Path to dataset files: .... Optionally:export CIFAKE_ROOT="/full/path/printed/by/download_cifake.py"
-
Start the FastAPI backend
From the project root:
uvicorn backend.main:app --reload
- Health check: open
http://127.0.0.1:8000/healthin your browser.
- Health check: open
-
Start the React frontend
In a new terminal (backend still running):
cd frontend npm install # first time only npm run dev
Then open the URL shown in the terminal, typically:
http://localhost:5173/ -
Use the app
- Click "Choose File" or drag-and-drop an image into the upload area.
- (Optional) Select which model to use (adversarially trained
cnn_advor baselinecnn_v1). - Click "Analyze Image".
- The app calls
POST /predict(for the label + confidence) and thenPOST /explain(for an LLM-backed or rule-based explanation, depending on your config) and shows:- Label: REAL or FAKE
- Confidence: model probability
- Why this result? short explanation based on label + confidence
- Recent predictions: a small history list
- Session stats: counts by label and by model used
AI-Image-Detection/
ml/
baseline_hog_svm.py # classical HOG + SVM baseline on CIFAKE
dataset.py # PyTorch dataset + dataloaders for CIFAKE
download_cifake.py # download CIFAKE via kagglehub
train_cnn.py # train SimpleCNN on CIFAKE, saves cnn_v1.pth
eval_cnn.py # evaluate baseline CNN on test set
train_cnn_adv.py # adversarially train CNN with FGSM examples
eval_cnn_adv.py # evaluate adversarially trained CNN
fgsm_attack.py # FGSM attack on baseline CNN
fgsm_attack_adv.py # FGSM attack on adversarially trained CNN
inference.py # shared inference helpers for backend
test_inference.py # quick check for inference
requirements-ml.txt # Python deps for ML + backend
backend/
main.py # FastAPI inference service (REST API)
frontend/ # React + TS web app (image upload UI)
weights/ # model checkpoints (cnn_v1.pth, cnn_adv.pth) – gitignored
.gitignore
README.md
From the project root:
python -m venv .venv
source .venv/bin/activate # macOS / Linux
# .venv\Scripts\activate # Windowspip install -r ml/requirements-ml.txt(Contains: torch, torchvision, numpy, scikit-learn, matplotlib,
Pillow, scikit-image, kagglehub, fastapi, uvicorn, etc.)
In shell:
export KAGGLE_API_TOKEN="KGAT_..."From the project root:
python ml/download_cifake.pyThis uses kagglehub to download CIFAKE and prints something like:
Path to dataset files: /Users/<user>/.cache/kagglehub/datasets/birdy654/cifake-real-and-ai-generated-synthetic-images/versions/3
We call this CIFAKE_ROOT. It should contain:
CIFAKE_ROOT/
train/
REAL/
FAKE/
test/
REAL/
FAKE/
Optionally set:
export CIFAKE_ROOT="/Users/<user>/.cache/kagglehub/datasets/birdy654/cifake-real-and-ai-generated-synthetic-images/versions/3"ml/dataset.py reads CIFAKE_ROOT (or falls back to the kagglehub cache path).
Script: ml/baseline_hog_svm.py
This script implements a classical HOG + SVM baseline:
- Loads images from
CIFAKE_ROOT/train/REALandCIFAKE_ROOT/train/FAKE - Converts to grayscale and resizes to 64×64
- Extracts Histogram of Oriented Gradients (HOG) features
- Splits data into 80% train / 20% test
- Trains a LinearSVC on the HOG features
- Prints:
- precision, recall, F1, accuracy per class
- confusion matrix
To run:
python ml/baseline_hog_svm.pyApproximate performance:
- Accuracy ≈ 74%
- Balanced F1-scores for REAL and FAKE
This is our interpretable classical benchmark.
Script: ml/dataset.py
Uses torchvision.datasets.ImageFolder to read:
CIFAKE_ROOT/train/REALandCIFAKE_ROOT/train/FAKECIFAKE_ROOT/test/REALandCIFAKE_ROOT/test/FAKE
Transforms:
- Resize to 32×32
- Random horizontal flip (train only)
- Convert to tensor
- Normalize RGB channels to roughly zero mean / unit-ish variance
Provides:
from ml.dataset import get_dataloaders
train_loader, test_loader = get_dataloaders(batch_size=256)You can confirm dataset loading with:
python ml/dataset.pyScript: ml/train_cnn.py
Trains a small CNN (SimpleCNN) on CIFAKE:
- 3 convolutional layers with ReLU + max pooling
- Adaptive average pooling to a 128-d feature vector
- Final linear layer → 2 logits (FAKE vs REAL)
- Loss: cross-entropy
- Optimizer: Adam (lr = 1e-3)
- Device:
cuda/mps/ CPU
To run:
cd ml
python train_cnn.pyDuring training, it prints train/test loss and accuracy per epoch and saves the best-performing model weights to:
weights/cnn_v1.pth
On our full run (50 epochs), the best epoch reached:
- Test accuracy ≈ 96.5% on 20k test images
Script: ml/fgsm_attack.py
This script evaluates how vulnerable the baseline CNN (cnn_v1.pth) is to a
Fast Gradient Sign Method (FGSM) attack.
High-level steps:
- Load the trained baseline CNN (
cnn_v1.pth) - Evaluate clean test accuracy
- Generate FGSM adversarial examples with perturbation
epsilon = 0.03:- Enable gradients on the input images
- Take gradient of loss w.r.t. inputs
- Add
epsilon * sign(gradient)to each pixel, then clamp back to valid range
- Re-evaluate accuracy on adversarial examples
To run:
cd ml
python fgsm_attack.pyRepresentative results:
- Clean accuracy ≈ 0.9648
- FGSM (eps=0.03) accuracy ≈ 0.29
- Accuracy drop ≈ 0.67
This shows the baseline CNN is highly vulnerable to even small, structured noise.
Script: ml/train_cnn_adv.py
We perform adversarial training to improve robustness:
- For each minibatch, generate FGSM examples on the fly (same epsilon)
- Compute loss on both clean and adversarial images and backpropagate
- Train for 10 epochs and track validation accuracy on clean data
- Save best model as
cnn_adv.pth
Representative training log (summarized):
- Clean train accuracy grows from ~80% → ~93%
- Adversarial train accuracy from ~65% → ~79%
- Validation accuracy ends around 92–93%
To run:
cd ml
python train_cnn_adv.pyScript: ml/eval_cnn_adv.py
Loads cnn_adv.pth and evaluates on the clean test set:
cd ml
python eval_cnn_adv.pyRepresentative results:
- Clean accuracy ≈ 93%
- Balanced precision/recall for REAL and FAKE
Script: ml/fgsm_attack_adv.py
Runs the same FGSM attack on the adversarially trained model:
cd ml
python fgsm_attack_adv.pyRepresentative results:
- Clean accuracy ≈ 0.93
- FGSM (eps=0.03) accuracy ≈ 0.78
- Accuracy drop ≈ 0.15
So the adversarially trained CNN sacrifices a bit of clean accuracy but is much more robust under attack.
Script: ml/inference.py
Provides a clean, reusable interface for running inference with our models.
Main pieces:
get_device()– pickscuda/mps/ CPUload_model(model_version="cnn_adv")– rebuildsSimpleCNNand loadsweights/cnn_v1.pthorweights/cnn_adv.pthpreprocess_image(pil_image)– resizes to 32×32, converts to tensor, normalizes channelspredict_image(pil_image, model=None, model_version="cnn_adv")– returns:
{
"label": "REAL" or "FAKE",
"probability": float, # softmax confidence for predicted class
"model_version": "cnn_v1" or "cnn_adv"
}You can confirm this with:
cd ml
python test_inference.pywhich loads a REAL and FAKE test image and prints predictions.
File: backend/main.py
We expose the model via a simple REST API powered by FastAPI + Uvicorn.
-
GET /health– health check{ "status": "ok", "model_version": "cnn_adv" } -
POST /predict– image upload and prediction- Expects form-data field
filewith an image (PNG/JPEG, etc.) - Uses
ml.inference.predict_imageunder the hood - Returns JSON:
{ "label": "REAL", "probability": 0.86, "model_version": "cnn_adv" }You can choose the model version (e.g.
cnn_advvscnn_v1) from the frontend UI; the backend will respect that selection when running inference. - Expects form-data field
-
POST /explain– image upload + explanation- Accepts the same image file field (
file) as/predict, plus query params for model and optional LLM provider. - Returns label, probability, model version, a confidence bucket, a human-readable explanation, and (when applicable) which LLM/provider generated it.
- Accepts the same image file field (
-
POST /explain– generate a human-readable explanation directly from an uploaded image.-
Accepts form-data:
curl -X POST "http://127.0.0.1:8000/explain?model=cnn_adv&llm_provider=openai" \ -F "file=@/full/path/to/some/test_image.png"
-
Query parameters:
model– which CNN to use:cnn_v1(baseline) orcnn_adv(adversarially trained). Defaults to the backendMODEL_VERSIONenv var.llm_provider(optional) –openaiorgemini. If omitted, the backend will choose a default provider based on environment variables.
-
Response shape (Pydantic
ExplanationResponse):{ "label": "FAKE", "probability": 0.9957, "model_version": "cnn_adv", "confidence_bucket": "very_high", "explanation": "…human-readable paragraph…", "llm_provider": "openai", "llm_model": "gpt-4o-mini" } -
Under the hood, the backend:
- Runs the same CNN inference as
/predict. - Buckets the confidence into
very_high,high,medium, orlow. - Tries to generate an explanation with LangChain + an LLM (OpenAI or Gemini) when API keys are configured.
- Falls back to a rule-based explanation if no LLM is available or if the LLM call fails.
- Runs the same CNN inference as
-
The backend uses LangChain to optionally call an LLM for explanations. Configuration is driven by environment variables:
OPENAI_API_KEY– if set, enables OpenAI viaChatOpenAI.GOOGLE_API_KEY– if set, enables Gemini viaChatGoogleGenerativeAI.EXPLAIN_DEFAULT_LLM– default provider when the frontend does not passllm_provider:openai– prefer OpenAI when available.gemini– prefer Gemini (if allowed/enabled).none– never call an LLM by default; always use rule-based explanations.
EXPLAIN_ENABLE_GEMINI– feature flag (true/false) to allow Gemini usage in this deployment.OPENAI_MODEL– OpenAI model name (default:gpt-4o-mini).GEMINI_MODEL– Gemini model name (default:gemini-2.5-flash).
If no provider is usable (no keys configured or none is specified), /explain always returns the rule-based explanation but still includes the confidence bucket and model metadata.
When configured with a MongoDB connection string (e.g. a MongoDB Atlas cluster), the backend logs each explanation request to a predictions collection. Each document records:
model_version– which model handled the request (cnn_v1orcnn_adv)labelandprobability– the prediction and confidenceconfidence_bucket– one ofvery_high,high,medium,lowllm_provider/llm_model– which LLM (if any) generated the explanation- timestamps and basic request metadata
This makes it easy to compute offline metrics (accuracy over time, model drift, per-model comparisons) and to debug individual predictions without storing the raw image data.
From the project root (with virtualenv activated):
uvicorn backend.main:app --reloadThen:
- Visit
http://127.0.0.1:8000/healthin a browser → should return status OK - Call the predict endpoint with
curl:
curl -X POST "http://127.0.0.1:8000/predict" \
-F "file=@/full/path/to/some/test_image.png"Expected output (example):
{"label":"REAL","probability":0.8645,"model_version":"cnn_adv"}This backend is what the React frontend consumes.
Folder: frontend/
This is a Vite-powered React + TypeScript single-page app that lets a user:
- Upload an image via file picker or drag-and-drop
- Choose which model to use (adversarially trained Adv-CNN vs baseline CNN)
- Send the image to the FastAPI backend
/predictendpoint - Call
/explainto get a short explanation for the result - View a list of recent predictions for the current session
- See lightweight session stats (total images, REAL vs FAKE counts, usage per model)
By default, the frontend expects the backend to be running at:
http://127.0.0.1:8000
and calls:
POST /predict
with the uploaded file in a file form field.
Requirements:
- Node.js 20.19+ or 22.12+ (Vite 7 requirement)
- npm (comes with Node)
From the project root:
cd frontend
npm install # if you haven't already
npm run devThen open the URL printed in the terminal, typically:
http://localhost:5173/
Make sure the backend is running first (uvicorn backend.main:app --reload), so the Analyze button can reach /predict.
Already implemented on top of the core ML + backend work:
- Modern upload UI
- Glassmorphism-style card with a soft glow that follows the cursor
- Drag-and-drop or file picker upload
- Inline image preview before sending to the backend
- Model selection
- Toggle between adversarially trained
cnn_advand baselinecnn_v1
- Toggle between adversarially trained
- Result explanation (LLM + rule-based fallback)
- "Why this result?" panel driven by the
/explainendpoint. - When an LLM provider is configured and selected, the explanation comes from LangChain + an LLM (OpenAI or Gemini); otherwise it falls back to the rule-based text.
- "Why this result?" panel driven by the
- LLM provider selector
- Dropdown to choose between OpenAI, Gemini, or a purely rule-based explanation.
- UI displays a small "generated by …" line under the explanation showing which provider/model was used.
- Prediction history
- Recent predictions list (label, confidence, model version, timestamp), persisted in
localStorageso it survives refreshes.
- Recent predictions list (label, confidence, model version, timestamp), persisted in
- Session stats
- Total images analyzed this session
- REAL vs FAKE counts
- How many times each model was used
In addition to the local dev workflow (uvicorn + Vite), the app can be run end-to-end via Docker.
From the project root (after ensuring weights/ contains cnn_v1.pth and cnn_adv.pth):
docker compose build backend frontend
docker compose up backend frontendThis will start:
- FastAPI backend on
http://localhost:8000 - React frontend on
http://localhost:5173
The frontend is configured to talk to the backend via the VITE_BACKEND_URL environment variable (set in frontend/.env.local for local dev or via a Docker environment file such as .env.docker).
For a cloud deployment, you can:
-
Launch an Ubuntu EC2 instance with Docker and the Docker Compose plugin installed.
-
Clone this repository onto the instance and copy the trained weights into
weights/. -
Create an
.env.dockerfile at the project root with yourOPENAI_API_KEY,GOOGLE_API_KEY, MongoDB connection string, and any other config. -
Run:
docker compose build backend frontend docker compose up -d backend frontend
The app will then be accessible via the EC2 instance’s public IP on ports 8000 (backend API) and 5173 (frontend UI), subject to your security group configuration.
This README serves as both a project log and a guide to running and extending AI Image Detector (CIFAKE – REAL vs FAKE).