A multi-label image classifier that detects NSFW/harmful content across 4 categories: alcohol, drugs, sexual, and extremism. Built with TensorFlow/Keras on top of EfficientNetB0, trained on ~79 k images.
The trained model is freely available on Hugging Face Hub — no sign-in required.
pip install huggingface_hub tensorflow pillowimport tensorflow as tf, numpy as np, json
from huggingface_hub import hf_hub_download
from PIL import Image
REPO = "damilareisaac/parental-control-efficientnet-b0"
# Downloads and caches model + metadata (~44 MB, first run only)
model_path = hf_hub_download(REPO, "parental_control_b0.keras")
meta = json.load(open(hf_hub_download(REPO, "model_metadata.json")))
model = tf.keras.models.load_model(model_path)
img = Image.open("image.jpg").convert("RGB").resize(tuple(meta["input_size"]))
arr = np.expand_dims(np.array(img, dtype=np.float32), 0)
scores = model.predict(arr)[0]
for label, score in zip(meta["labels"], scores):
flagged = score > meta["optimal_thresholds"][label]
print(f"{label:<12} {score:.3f} {'⚠️ FLAGGED' if flagged else '✅ ok'}")| Label | Val Accuracy | Val Loss |
|---|---|---|
| alcohol | 99.6% | — |
| extremism | 99.5% | — |
| sexual | 99.0% | — |
| drugs | 98.2% | — |
| Overall best val_loss | — | 0.0948 |
| File | Size | Purpose |
|---|---|---|
parental_control_b0.keras |
~44 MB | Full model — inference & fine-tuning |
model_metadata.json |
< 1 KB | Labels, thresholds, input spec |
training_history.png |
197 KB | Loss & accuracy curves |
threshold_calibration.png |
81 KB | Per-label threshold calibration |
| Property | Value |
|---|---|
| Base | EfficientNetB0 (ImageNet pre-trained) |
| Total parameters | 4,383,655 |
| Input size | 224 × 224 × 3 (RGB) |
| Output | 4 independent sigmoid scores (multi-label) |
| Labels | alcohol, drugs, sexual, extremism |
| Precision | mixed_float16 during training |
Training uses a two-phase fine-tuning approach:
| Phase | Epochs | Layers trained | Learning rate |
|---|---|---|---|
| 1 — Head only | 20 | Custom classification head | 1e-3 |
| 2 — Fine-tune | 40 | Head + top 30% of EfficientNetB0 backbone | 2e-5 |
Loss is weighted binary cross-entropy with higher penalties for harder/rarer classes:
| Label | Weight |
|---|---|
| alcohol | 1.5 |
| drugs | 1.5 |
| sexual | 2.0 |
| extremism | 2.5 |
~79 k images from Kaggle sofialitvin/dataset-images:
| Class | Images |
|---|---|
| normal | 23,332 |
| alcohol | 13,837 |
| tobacco (→ merged into drugs) | 14,025 |
| sexual | 14,178 |
| drugs | 5,649 |
| extremism | 7,912 |
tobaccoimages are merged into thedrugslabel during training.
brew install python@3.12 # macOS (Homebrew)
sudo apt install python3.12 # Ubuntu/Debian
winget install Python.Python.3.12 # WindowsPython 3.12 is required for
tensorflow-metalcompatibility on Apple Silicon. Python 3.11+ works fine on Linux/Windows.
Apple Silicon (M-series Mac)
python3.12 -m venv venv
source venv/bin/activate
pip install "tensorflow==2.18.0" tensorflow-metal
pip install scikit-learn matplotlib pillow pandas kaggleNVIDIA GPU (Linux / Windows)
python3 -m venv venv
source venv/bin/activate # Linux
# venv\Scripts\activate # Windows
pip install tensorflow[and-cuda] # pulls CUDA deps automatically
pip install scikit-learn matplotlib pillow pandas kaggleCPU only (any platform)
python3 -m venv venv
source venv/bin/activate
pip install tensorflow
pip install scikit-learn matplotlib pillow pandas kaggle# Requires a Kaggle API token at ~/.kaggle/kaggle.json
kaggle datasets download -d sofialitvin/dataset-images -p dataset/ --unzipEither edit the two constants at the top of train.py:
DATA_DIR = "/path/to/dataset/DATASET_IMAGES"
WORKING_DIR = "/path/to/output"Or pass them as environment variables (no code change needed):
export DATA_DIR="/path/to/dataset/DATASET_IMAGES"
export WORKING_DIR="/path/to/output"
python train.pysource venv/bin/activate
python train.pyResumable — if interrupted, re-running train.py picks up from the last completed phase automatically.
The script auto-detects the best available accelerator at startup via setup_gpu() and configures training accordingly. No code changes are required when switching hardware.
startup
│
├─ arm64 macOS? ──yes──► tensorflow-metal installed? ──yes──► Metal GPU (/GPU:0)
│ └──no──► CPU (with warning)
│
├─ CUDA GPU present? ──yes──► 1 GPU? ──► OneDeviceStrategy (/gpu:0)
│ └─ >1 GPU? ─► MirroredStrategy (all GPUs)
│
└─ nothing found ──► CPU fallback (warning logged)
Mixed-precision (float16 compute, float32 weights) is enabled automatically on all GPU paths for a significant speed boost.
Requires TF 2.18 + tensorflow-metal 1.2.0 (TF 2.21+ is not yet supported by tensorflow-metal):
pip install "tensorflow==2.18.0" tensorflow-metalWhat happens at runtime:
- The Metal plugin is auto-registered when TensorFlow is imported — no explicit
importneeded. - The GPU appears as
/physical_device:GPU:0with device nameMETAL. - Memory growth is enabled automatically so the GPU doesn't pre-allocate all unified memory.
mixed_float16is applied — Metal supports float16 natively, giving ~2× throughput over float32.
Sleep prevention (macOS): The laptop will go to sleep during long runs unless you run:
caffeinate -disum -w $(pgrep -f "python train.py") &
-dprevents display sleep,-iidle sleep,-sAC sleep,-ukeeps user session active.
Works on Linux and Windows with a CUDA-capable GPU (Kepler/GTX 700+):
pip install tensorflow[and-cuda] # installs CUDA/cuDNN automaticallyWhat happens at runtime:
mixed_float16is applied. Full benefit requires Volta architecture (GTX 1080 Ti / V100) or newer — older cards run float16 but without tensor core acceleration.- Memory growth is enabled — the GPU allocates VRAM on demand rather than reserving it all upfront.
- Single GPU:
OneDeviceStrategy(/gpu:0)— all training on one device. - Multiple GPUs:
MirroredStrategyis selected automatically. Gradients are synchronised across all devices at each step; effective batch size scales with the number of GPUs.
Minimum VRAM requirements at BATCH_SIZE = 128:
| GPU VRAM | Batch size |
|---|---|
| 4 GB | Reduce to 32–64 |
| 8 GB | 64–128 |
| 12 GB+ | 128 (default) |
| 24 GB+ | 256+ for faster training |
If you hit OOM errors, reduce
BATCH_SIZEat the top oftrain.py.
Colab provides a free NVIDIA T4 (16 GB VRAM). No local install needed:
- Upload the dataset to Google Drive.
- Open a new Colab notebook and mount Drive:
from google.colab import drive drive.mount("/content/drive")
- Clone this repo and set env vars:
!git clone https://github.com/grindqueue/thesis_model_train.git %cd thesis_model_train !pip install scikit-learn matplotlib pillow pandas
import os os.environ["DATA_DIR"] = "/content/drive/MyDrive/DATASET_IMAGES" os.environ["WORKING_DIR"] = "/content/drive/MyDrive/output"
- Run training:
!python train.py
Colab disconnects after ~12 hrs of inactivity. Training is resumable — re-run
train.pyto continue from the last completed phase.
No extra packages. Set a smaller batch size to avoid excessive RAM use:
BATCH_SIZE = 32 # reduce if RAM < 16 GBCPU training is very slow (see timing table above). Use Colab or a cloud GPU instead for full training runs.
Check the log output at the start of training:
# Apple Silicon
INFO GPUs found : ['/physical_device:GPU:0']
INFO Apple Silicon detected — tensorflow-metal plugin loaded
INFO mixed_float16 precision enabled
INFO Using GPU: /physical_device:GPU:0 [Metal (Apple Silicon)]
# NVIDIA single GPU
INFO GPUs found : ['/physical_device:GPU:0']
INFO mixed_float16 precision enabled
INFO Using GPU: /physical_device:GPU:0 [CUDA]
# NVIDIA multi-GPU
INFO GPUs found : ['/physical_device:GPU:0', '/physical_device:GPU:1']
INFO mixed_float16 precision enabled
INFO Multi-GPU: MirroredStrategy across 2 GPUs
# No GPU
WARNING No GPU found — running on CPU (will be slow)
Both phases together (20 + 40 epochs, ~79 k images):
| Hardware | Time per epoch | Total (60 epochs) |
|---|---|---|
| Apple M3 / M4 (Metal, batch 128) | ~4 min | ~4 hrs |
| Apple M1 / M2 (Metal, batch 128) | ~6–8 min | ~6–8 hrs |
| NVIDIA RTX 4090 (CUDA, batch 128) | ~2–3 min | ~2–3 hrs |
| NVIDIA RTX 3080 (CUDA, batch 128) | ~4–5 min | ~4–5 hrs |
| NVIDIA T4 (Google Colab, batch 64) | ~8–10 min | ~8–10 hrs |
| Modern CPU only (batch 32) | ~40–60 min | ~2–3 days |
| Technique | How | Effect |
|---|---|---|
| Larger batch size | Set BATCH_SIZE = 128 or higher |
2–4× fewer steps/epoch |
| Enable GPU | See GPU Support above | 10–50× vs CPU |
| Fewer epochs | Reduce PHASE1_EPOCHS / PHASE2_EPOCHS |
Linear reduction |
| Google Colab (free T4) | Upload dataset to Google Drive, run there | Free GPU |
All outputs are written to WORKING_DIR/:
output/
checkpoints/
phase1_best.keras ← best Phase 1 weights
phase2_best.keras ← best Phase 2 weights (primary result)
exported_model/
parental_control_b0.keras ← full exported model (~44 MB)
model_metadata.json ← labels, thresholds, input spec
training_history.png
threshold_calibration.png
logs/
training.log
train_clean.csv / val_clean.csv / test_clean.csv