Overview
A video-based binary classifier that labels any clip as Normal or Suspicious. Trained end-to-end on the UCF-Crime dataset — 1,950 annotated surveillance videos across 13 crime categories (Abuse, Arrest, Arson, Assault, Burglary, Explosion, Fighting, Road Accidents, Robbery, Shooting, Shoplifting, Stealing, Vandalism) plus matched normal footage.
Architecture
Raw video
↓
ResNet50 (frozen, ImageNet) ← feature extractor, 25M params
↓ 16 frames × 2048-dim vectors
LayerNorm + Dense(256) + Dropout(0.4)
↓
Bidirectional GRU (64 units) ← temporal modelling
↓
Temporal Attention ← learned frame weighting
↓
Dense(64) + Dropout(0.6)
↓
Sigmoid → P(suspicious)
Head parameters: 660,738 (2.52 MB)
Backbone: Frozen ResNet50 — features extracted once and cached, not retrained
Code Snippet 1
import requests
with open(r"FILE_PATH", "rb") as f:
res = requests.post(url, files={"file": f})
print(res.json())
Code Snippet 2
!pip install suspectiq
from suspectiq import SuspectIQ
detector = SuspectIQ()
result = detector.predict(r"FILE_PATH")
print(result)
Training Details
Setting Value Framework TensorFlow 2.19.0 / Keras 3 GPU 2× Tesla T4 (Kaggle) Loss Focal Loss (α=0.60, γ=2.0) Optimiser Adam + gradient clipping (clipnorm=1.0) LR schedule ReduceLROnPlateau on val_AUC, min 1e-5 Early stopping Patience 6 on val_AUC, restore best weights Epochs run 19 (best at epoch 13) Batch size 32 Sequence length 16 frames per 2-second clip Threshold 0.325 (macro-F1 tuned on validation set)
Dataset Splits
Split Clips Normal Suspicious Train 1,328 680 (51.2%) 648 (48.8%) Val 332 170 (51.2%) 162 (48.8%) Test 290 150 (51.7%) 140 (48.3%)
Splits are at video level — no frame-level leakage between train and val. Val is a stratified 20% carve-out of the training pool.
Results
Metric Value Test accuracy 90.3% Test AUC-ROC 0.930 Macro F1 0.903 Normal precision / recall 89.6% / 92.0% Suspicious precision / recall 91.2% / 88.6% False alarms (FP) 12 / 150 normal clips Misses (FN) 16 / 140 suspicious clips
Inference
Supports both recorded video and live streams (webcam, RTSP, RTMP) via a sliding window with configurable overlap:
1 model = tf . keras . models . load_model ( 'best_model.keras' )
2 # No custom_objects needed — @register_keras_serializable handles it
Latency: ~135 ms per 2-second window on T4 GPU (after tf.function warm-up)
Overlap: 50% default — events spanning clip boundaries are always fully captured
Fixes Applied Across Versions
Issue Fix Class weight collapse (v1) Focal Loss α=0.60 Backbone overfitting ResNet50 frozen throughout Slow extraction Batched GPU extraction (~8× faster) Threshold bias Macro-F1 tuning instead of suspicious-only F1 Custom class serialisation @register_keras_serializableTraining resume after crash Epoch history + weight checkpoint Feature extraction resume Incremental partial .npz checkpoint every 200 clips Dataset path resolution BFS auto-discovery + 5-layout fallback Leakage Video-level splits confirmed