Production-grade end-to-end MLOps pipeline for chicken disease detection from fecal images. Transfer learning with VGG16 / ResNet50 / EfficientNetB0 / MobileNetV2, FastAPI, MLflow, DVC, Grad-CAM, Evidently AI, Docker, and AWS ECR + EC2.
Coccidiosis is a parasitic intestinal disease in poultry caused by Eimeria protozoa, responsible for significant economic losses worldwide. Manual diagnosis from fecal images requires expert knowledge and cannot scale to large flocks. This system automates binary classification (Healthy vs Coccidiosis) using transfer learning with model-specific preprocessing and a full MLOps production pipeline.
Chicken Fecal Images — RGB photographs of chicken fecal matter for binary disease classification.
| Coccidiosis | Healthy | Total | |
|---|---|---|---|
| Total | 195 | 195 | 390 |
| Train (70%) | 136 | 136 | 272 |
| Val (15%) | 29 | 29 | 58 |
| Test (15%) | 30 | 30 | 60 |
- Perfectly balanced classes (1:1 ratio) — no class weighting needed
- Fixed 70/15/15 split,
random.seed(42)— bitwise reproducible across machines - Source: private AWS S3 bucket (
chicken-data-for-classification) — downloaded in Stage 1 - Input size: 224×224×3 (RGB), resized at preprocessing
| Model | Accuracy | F1 Macro | AUC-ROC | Params | Train Time |
|---|---|---|---|---|---|
| MobileNetV2 | 98.3% | 0.9833 | 0.9933 | 3.4M | 47.5s |
| ResNet50 | 98.3% | 0.9833 | 0.9900 | 25.6M | 153.0s |
| EfficientNetB0 | 96.7% | 0.9667 | 0.9978 | 5.3M | 58.8s |
| VGG16 | 91.7% | 0.9161 | 0.9678 | 138.4M | 347.7s |
All models: frozen backbone, Adam lr=1e-4, EarlyStopping patience=7, 70/15/15 split, seed=42. Full results:
artifacts/model_comparison/results.json
AWS S3 (data.zip)
│
▼
[Stage 1] Data Ingestion → artifacts/data_ingestion/Chicken-fecal-images/
│
▼
[Stage 2] Data Pipeline → train/ val/ test/ (70/15/15 seeded split, seed=42)
│
▼
[Stage 3] Model Training → artifacts/training/{MODEL}_best.h5
│ MLflow run logged per model
▼
[Stage 4] Evaluation → artifacts/evaluation/{MODEL}/metrics.json
│ confusion_matrix.png · roc_curve.png
▼
[Stage 5] Explainability → artifacts/explainability/gradcam/{MODEL}/{class}/
│ Grad-CAM heatmap overlays
▼
[Stage 6] Model Comparison → artifacts/model_comparison/comparison_results.json
│ MLflow parent run with nested child runs
▼
FastAPI Inference Service → GET / (web UI)
│ POST /predict (base64 → label + confidence)
│ GET /health (liveness probe)
│ GET /metrics (latest eval metrics)
│ GET /model/version
│ GET /docs (Swagger UI)
▼
Docker Image → AWS ECR → AWS EC2 (port 8080)
├── .github/workflows/cicd.yaml # Lint → Security → Test → Build → Deploy
├── config/config.yaml # All artifact paths
├── params.yaml # All hyperparameters
├── dvc.yaml # 5-stage DVC pipeline
├── main.py # Full training pipeline entry point
├── static/index.html # Web UI (served by FastAPI at GET /)
├── src/cnnClassifier/
│ ├── components/
│ │ ├── data_ingestion.py # S3 streaming download
│ │ ├── data_pipeline.py # 70/15/15 seeded split
│ │ ├── prepare_base_model.py # 4 backbones + 3 fine-tuning strategies
│ │ ├── model_trainer.py # Train + MLflow logging
│ │ └── evaluation.py # Full metrics + CM + ROC curve
│ ├── explainability/
│ │ └── grad_cam.py # Grad-CAM heatmaps
│ ├── experiments/
│ │ └── mlflow_tracker.py # MLflow context manager
│ ├── monitoring/
│ │ └── drift_detector.py # Evidently drift reports
│ ├── api/
│ │ ├── main.py # FastAPI app (startup model load)
│ │ ├── schemas.py # Pydantic v2 request/response models
│ │ ├── dependencies.py # Singleton model state
│ │ └── routers/
│ │ ├── predict.py # POST /predict
│ │ ├── health.py # GET /health
│ │ └── metrics.py # GET /metrics, /model/version
│ ├── pipeline/
│ │ ├── stage_01_data_ingestion.py
│ │ ├── stage_02_data_pipeline.py
│ │ ├── stage_03_model_trainer.py
│ │ ├── stage_04_evaluation.py
│ │ ├── stage_05_explainability.py
│ │ ├── stage_06_model_comparison.py
│ │ └── predict.py
│ ├── config/configuration.py
│ ├── entity/config_entity.py
│ └── utils/common.py
├── tests/
│ ├── test_data_pipeline.py # 7 unit tests
│ ├── test_api.py # 10 API tests (mocked model)
│ └── test_model_factory.py # 7 model build tests
├── research/ # Exploratory notebooks
├── documents/ # HLD, LLD, wireframe, paper
├── Dockerfile # Multi-stage build (builder + runtime)
├── docker-compose.yml # api + mlflow services
├── pyproject.toml # Ruff + Black + pytest config
├── requirements.txt
└── .env.example
Pre-trained model at model/model.h5. No retraining needed.
git clone https://github.com/pankaj2k9/MLOpsE2EBirdDiseaseClassificationDeepLearningProject.git
cd MLOpsE2EBirdDiseaseClassificationDeepLearningProject
# Create and activate conda env (Python 3.11 + TF 2.15)
conda create -n cnnclassify311 python=3.11
conda activate cnnclassify311
pip install --upgrade pip setuptools wheel
pip install -r requirements.txt # installs cnnClassifier package in editable mode
python -m uvicorn cnnClassifier.api.main:app --host 0.0.0.0 --port 8080Open http://localhost:8080 — upload a fecal image, click Classify Image.
API docs at http://localhost:8080/docs (Swagger UI).
git clone https://github.com/pankaj2k9/MLOpsE2EBirdDiseaseClassificationDeepLearningProject.git
cd MLOpsE2EBirdDiseaseClassificationDeepLearningProject
conda create -n cnnclassify311 python=3.11
conda activate cnnclassify311
pip install --upgrade pip setuptools wheel
pip install -r requirements.txt # includes -e . to install cnnClassifier packagecp .env.example .envEdit .env:
AWS_ACCESS_KEY_ID=your_access_key_here
AWS_SECRET_ACCESS_KEY=your_secret_key_here
AWS_REGION=us-east-1
MLFLOW_TRACKING_URI=mlrunsAWS credentials only needed for Stage 1 (S3 download). Inference does not require them.
python main.py VGG16 # trains VGG16 through all stagesOr run each stage individually — all commands from project root:
# Stage 1: Download data from S3
python -m cnnClassifier.pipeline.stage_01_data_ingestion
# Stage 2: Create 70/15/15 train/val/test split (seed=42)
python -m cnnClassifier.pipeline.stage_02_data_pipeline
# Stage 3: Train a model (default VGG16)
python -m cnnClassifier.pipeline.stage_03_model_trainer # VGG16
MODEL_NAME=ResNet50 python -m cnnClassifier.pipeline.stage_03_model_trainer
# Stage 4: Evaluate on held-out test set
python -m cnnClassifier.pipeline.stage_04_evaluation
# Stage 5: Generate Grad-CAM explanations
python -m cnnClassifier.pipeline.stage_05_explainability
# Stage 6: Train + evaluate all 4 models, log to MLflow
python -m cnnClassifier.pipeline.stage_06_model_comparisonMLFLOW_ALLOW_FILE_STORE=true mlflow ui --backend-store-uri mlruns
# open http://localhost:5000dvc repro # re-runs only invalidated stages
dvc metrics show # compare metrics across git commits# 1. Place data manually (skip S3)
mkdir -p artifacts/data_ingestion
# copy data.zip → artifacts/data_ingestion/data.zip
# extract → artifacts/data_ingestion/Chicken-fecal-images/
# 2. Commit outputs to DVC cache
dvc commit
# 3. One-time setup — create local remote and push
mkdir ../dvc-storage
dvc remote add -d localremote ../dvc-storage
dvc push
# On a fresh clone (no AWS credentials needed)
dvc pull
dvc repro --downstream data_pipeline # skips S3 data_ingestion stageThe raw dataset (
Chicken-fecal-images, 390 fecal images) is downloaded from private S3 duringstage_01_data_ingestion. For fully offline testing, placedata.zipand extractedChicken-fecal-images/underartifacts/data_ingestion/, rundvc commit, then push to local remote.
Switch back to S3 remote:
dvc remote default myS3remote
Run drift detection first (requires reference + current batch):
python -c "
from cnnClassifier.monitoring.drift_detector import DriftDetector
from cnnClassifier.config.configuration import ConfigurationManager
config = ConfigurationManager().get_monitoring_config()
detector = DriftDetector(config)
# detector.build_reference(ref_generator) # run once after deployment
# detector.detect(current_generator) # run periodically
"Reports saved to artifacts/monitoring/reports/drift_report_{timestamp}.html
Open locally:
# macOS
open artifacts/monitoring/reports/$(ls -t artifacts/monitoring/reports/*.html | head -1)
# or any browser
python -m http.server 8888 --directory artifacts/monitoring/reports
# → http://localhost:8888python -m uvicorn cnnClassifier.api.main:app --host 0.0.0.0 --port 8080
# → http://localhost:8080 (web UI)
# → http://localhost:8080/docs (Swagger UI)| Method | Path | Description |
|---|---|---|
GET |
/ |
Web UI (upload image, view prediction) |
POST |
/predict |
{"image": "<base64>"} → {label, confidence, class_probabilities} |
GET |
/health |
{status, model_loaded, model_name, model_version} |
GET |
/metrics |
Latest evaluation metrics JSON |
GET |
/model/version |
Model name and version |
GET |
/docs |
Swagger UI |
GET |
/redoc |
ReDoc UI |
# Encode image to base64
B64=$(base64 -i path/to/image.jpg)
curl -X POST http://localhost:8080/predict \
-H "Content-Type: application/json" \
-d "{\"image\": \"$B64\"}"Response:
{
"prediction": {
"label": "Coccidiosis",
"confidence": 0.923,
"class_probabilities": {"Coccidiosis": 0.923, "Healthy": 0.077}
},
"model_name": "VGG16",
"model_version": "2.0.0"
}Key settings in params.yaml:
SEED: 42
TRAIN_RATIO: 0.70
VAL_RATIO: 0.15
TEST_RATIO: 0.15
IMAGE_SIZE: [224, 224, 3]
BATCH_SIZE: 16
EPOCHS: 30
LEARNING_RATE: 0.0001
OPTIMIZER: adam
FINETUNE_STRATEGY: frozen # frozen | partial | full
UNFREEZE_LAYERS: 20 # used when strategy=partial
EARLY_STOPPING_PATIENCE: 7
REDUCE_LR_PATIENCE: 3
REDUCE_LR_FACTOR: 0.3
MODELS:
- VGG16
- ResNet50
- EfficientNetB0
- MobileNetV2pytest tests/ -v --cov=src/cnnClassifier --cov-report=term-missingLint and format:
ruff check src/ tests/
black --check src/ tests/Security scan:
bandit -r src/ -lldocker build --target runtime -t bird-disease-classifier .docker run -p 8080:8080 bird-disease-classifierdocker run -p 8080:8080 \
-e AWS_ACCESS_KEY_ID=your_key \
-e AWS_SECRET_ACCESS_KEY=your_secret \
-e AWS_REGION=us-east-1 \
bird-disease-classifierdocker-compose up
# API → http://localhost:8080
# MLflow UI → http://localhost:5000- ECR — Docker image registry
- EC2 — compute (Ubuntu, t2.medium or higher)
AmazonEC2ContainerRegistryFullAccess
AmazonEC2FullAccess
| Secret | Value |
|---|---|
AWS_ACCESS_KEY_ID |
IAM access key |
AWS_SECRET_ACCESS_KEY |
IAM secret key |
AWS_REGION |
e.g. us-east-1 |
AWS_ECR_LOGIN_URI |
e.g. 123456789.dkr.ecr.us-east-1.amazonaws.com |
ECR_REPOSITORY_NAME |
e.g. bird-disease-classifier |
sudo apt-get update -y && sudo apt-get upgrade -y
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
sudo usermod -aG docker ubuntu
newgrp dockerRegister EC2 as GitHub Actions self-hosted runner: Settings → Actions → Runners → New self-hosted runner.
aws ecr get-login-password --region us-east-1 \
| docker login --username AWS --password-stdin <ECR_LOGIN_URI>
docker build --target runtime -t bird-disease-classifier .
docker tag bird-disease-classifier:latest <ECR_LOGIN_URI>/<REPO_NAME>:latest
docker push <ECR_LOGIN_URI>/<REPO_NAME>:latestpush to main
│
├─ [lint] ruff check + black --check
├─ [security] bandit -r src/ -ll
├─ [test] pytest + coverage upload to Codecov
│
├─ [build] docker build --target runtime → push to ECR
│
└─ [deploy] EC2 self-hosted runner
└─ docker pull
└─ docker stop old container (|| true)
└─ docker run --restart unless-stopped -p 80:8080
└─ health-check wait (12 × 10s polls on /health)
| Layer | Technology |
|---|---|
| Language | Python 3.8 |
| ML Framework | TensorFlow 2.13 / Keras |
| Models | VGG16, ResNet50, EfficientNetB0, MobileNetV2 |
| Explainability | Grad-CAM (GradientTape) |
| Experiment Tracking | MLflow 2.17 |
| Data Versioning | DVC 3.x + S3 remote |
| API | FastAPI 0.104+ + Pydantic v2 |
| Frontend | Vanilla HTML/JS (served at GET /) |
| Monitoring | Evidently AI (drift detection) |
| Containerization | Docker multi-stage (non-root runtime) |
| Registry | AWS ECR |
| Compute | AWS EC2 |
| CI/CD | GitHub Actions (lint + security + test + build + deploy) |
| Config | YAML + python-box |
| Data Source | AWS S3 |
ModuleNotFoundError: No module named 'cnnClassifier'
→ Run pip install -e . from project root. The -e . in requirements.txt does this automatically.
FileNotFoundError: config/config.yaml
→ All scripts must run from project root, not from inside src/.
model/model.h5 not found on API startup
→ Either use pre-trained model in model/ or after training: cp artifacts/training/VGG16_final.h5 model/model.h5
AWS S3 download fails with 301/403
→ Data ingestion auto-detects bucket region on redirect. Check IAM has s3:GetObject on the bucket.
Slow training on M1/M2 Mac → TF 2.13 uses legacy Keras optimizer automatically on Apple Silicon. Normal — training still works correctly.
The following are contributions built as part of this project:
- Backbone-specific preprocessing — model-specific
preprocess_inputper backbone (VGG16, ResNet50, EfficientNetB0, MobileNetV2) matching each model's ImageNet training distribution - Deterministic 70/15/15 split — physical train/val/test directories, seeded, idempotent
- Multi-model comparison pipeline — Stage 6 trains all 4 backbones under identical conditions
- Grad-CAM explainability — full GradientTape implementation for all 4 backbones
- MLflow experiment tracking — context-manager wrapper, nested runs, best-run retrieval
- Evidently drift monitoring — embedding-based DataDrift + TargetDrift reports
- FastAPI migration — 4 endpoints, Pydantic validation, singleton model state, startup lifecycle
- Web UI —
static/index.htmlwith drag-drop upload, probability bars, served atGET / - Multi-stage Docker — builder + non-root runtime user,
HEALTHCHECK - Real CI/CD — Ruff, Bandit, pytest-cov gates; EC2 health-check wait on deploy
- System design docs — HLD, LLD, wireframe, IEEE research paper
- Optuna hyperparameter search
- ViT-B/16 baseline comparison
- Model calibration (temperature scaling)
- Multi-class extension (Salmonella, Newcastle disease)
- Kubernetes (EKS) canary deployment
- Prometheus + Grafana operational dashboard
- Albumentations advanced augmentation
Pankaj Kumar Pramanik B.Sc. (Honours) Data Science and AI — IIT Guwahati
GitHub: https://github.com/pankaj2k9