Spaces:
Runtime error
Runtime error
Backend + Frontend done
Browse files- .streamlit/config.toml +15 -0
- .vscode/settings.json +3 -0
- app.py +116 -186
- config/config.yaml +15 -10
- dvc.lock +67 -11
- dvc.yaml +18 -7
- requirements.txt +2 -2
- research/evaluation.py +163 -0
- src/cnnClassifier/components/data_ingestion.py +29 -23
- src/cnnClassifier/components/data_preparation.py +50 -0
- src/cnnClassifier/components/multi_task_model_trainer.py +161 -0
- src/cnnClassifier/config/configuration.py +30 -27
- src/cnnClassifier/entity/config_entity.py +9 -7
- src/cnnClassifier/pipeline/prediction.py +191 -0
- src/cnnClassifier/pipeline/stage_01_data_ingestion.py +3 -2
- src/cnnClassifier/pipeline/stage_02_data_preparation.py +27 -0
- src/cnnClassifier/pipeline/stage_03_multi_task_model_training.py +26 -0
.streamlit/config.toml
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[theme]
|
| 2 |
+
# Primary accent color (a professional blue, similar to your example)
|
| 3 |
+
primaryColor="#4A90E2"
|
| 4 |
+
|
| 5 |
+
# Main background color
|
| 6 |
+
backgroundColor="#F0F4F8"
|
| 7 |
+
|
| 8 |
+
# Sidebar and card background color
|
| 9 |
+
secondaryBackgroundColor="#FFFFFF"
|
| 10 |
+
|
| 11 |
+
# Default text color
|
| 12 |
+
textColor="#31333F"
|
| 13 |
+
|
| 14 |
+
# Font
|
| 15 |
+
font="sans serif"
|
.vscode/settings.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"git.ignoreLimitWarning": true
|
| 3 |
+
}
|
app.py
CHANGED
|
@@ -2,195 +2,125 @@ import streamlit as st
|
|
| 2 |
import cv2
|
| 3 |
import numpy as np
|
| 4 |
from PIL import Image
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
-
|
|
|
|
| 10 |
|
| 11 |
-
|
| 12 |
-
st.write("Detect age groups from images, videos, or a live webcam feed.")
|
| 13 |
-
st.write("This application uses an EfficientFormer-L1 model fine-tuned on the Facial Age dataset.")
|
| 14 |
-
|
| 15 |
-
# --- Helper Functions and Classes ---
|
| 16 |
-
|
| 17 |
-
@st.cache_resource
|
| 18 |
-
def load_model():
|
| 19 |
-
"""Load the age detection model pipeline."""
|
| 20 |
-
model_path = "artifacts/model_trainer/facial_age_detector_model"
|
| 21 |
-
pipe = pipeline('image-classification', model=model_path, device=0) # Use 0 for GPU
|
| 22 |
-
return pipe
|
| 23 |
-
|
| 24 |
-
@st.cache_resource
|
| 25 |
-
def load_face_detector():
|
| 26 |
-
"""Load the MTCNN face detector."""
|
| 27 |
-
return MTCNN()
|
| 28 |
-
|
| 29 |
-
def iou(boxA, boxB):
|
| 30 |
-
"""Calculate Intersection over Union."""
|
| 31 |
-
xA = max(boxA[0], boxB[0])
|
| 32 |
-
yA = max(boxA[1], boxB[1])
|
| 33 |
-
xB = min(boxA[2], boxB[2])
|
| 34 |
-
yB = min(boxA[3], boxB[3])
|
| 35 |
-
interArea = max(0, xB - xA) * max(0, yB - yA)
|
| 36 |
-
boxAArea = (boxA[2] - boxA[0]) * (boxA[3] - boxA[1])
|
| 37 |
-
boxBArea = (boxB[2] - boxB[0]) * (boxB[3] - boxB[1])
|
| 38 |
-
return interArea / float(boxAArea + boxBArea - interArea)
|
| 39 |
-
|
| 40 |
-
class EMATracker:
|
| 41 |
-
"""Exponential Moving Average Tracker for smoothing predictions."""
|
| 42 |
-
def __init__(self, alpha=0.3):
|
| 43 |
-
self.alpha = alpha
|
| 44 |
-
self.tracked_objects = {} # {track_id: {box: [], ema_preds: {}}}
|
| 45 |
-
|
| 46 |
-
def update(self, detections, id_counter):
|
| 47 |
-
# Detections are a list of face boxes
|
| 48 |
-
# Simple tracking by IOU
|
| 49 |
-
|
| 50 |
-
# Match detections to existing tracks
|
| 51 |
-
matches = {} # {track_id: det_idx}
|
| 52 |
-
used_det_indices = set()
|
| 53 |
-
|
| 54 |
-
# This is a simple greedy matching. For more robust tracking, consider Hungarian algorithm.
|
| 55 |
-
for track_id, data in self.tracked_objects.items():
|
| 56 |
-
best_iou = 0
|
| 57 |
-
best_det_idx = -1
|
| 58 |
-
for i, det_box in enumerate(detections):
|
| 59 |
-
if i in used_det_indices: continue
|
| 60 |
-
current_iou = iou(data['box'], det_box)
|
| 61 |
-
if current_iou > best_iou and current_iou > 0.3: # IOU threshold
|
| 62 |
-
best_iou = current_iou
|
| 63 |
-
best_det_idx = i
|
| 64 |
-
if best_det_idx != -1:
|
| 65 |
-
matches[track_id] = best_det_idx
|
| 66 |
-
used_det_indices.add(best_det_idx)
|
| 67 |
-
|
| 68 |
-
# Update matched tracks
|
| 69 |
-
for track_id, det_idx in matches.items():
|
| 70 |
-
self.tracked_objects[track_id]['box'] = detections[det_idx]
|
| 71 |
-
|
| 72 |
-
# Add new tracks
|
| 73 |
-
for i, det_box in enumerate(detections):
|
| 74 |
-
if i not in used_det_indices:
|
| 75 |
-
self.tracked_objects[id_counter] = {'box': det_box, 'ema_preds': defaultdict(float)}
|
| 76 |
-
id_counter += 1
|
| 77 |
-
|
| 78 |
-
# Remove old tracks (optional, for long videos)
|
| 79 |
-
|
| 80 |
-
return id_counter
|
| 81 |
-
|
| 82 |
-
def apply_ema(self, track_id, new_preds):
|
| 83 |
-
"""Applies EMA to the predictions for a given track."""
|
| 84 |
-
if track_id not in self.tracked_objects:
|
| 85 |
-
return {}
|
| 86 |
-
|
| 87 |
-
current_ema = self.tracked_objects[track_id]['ema_preds']
|
| 88 |
-
|
| 89 |
-
# Initialize if new
|
| 90 |
-
if not current_ema:
|
| 91 |
-
for pred in new_preds:
|
| 92 |
-
current_ema[pred['label']] = pred['score']
|
| 93 |
-
else:
|
| 94 |
-
# Update existing values
|
| 95 |
-
for pred in new_preds:
|
| 96 |
-
label = pred['label']
|
| 97 |
-
current_ema[label] = (self.alpha * pred['score']) + ((1 - self.alpha) * current_ema[label])
|
| 98 |
-
|
| 99 |
-
self.tracked_objects[track_id]['ema_preds'] = current_ema
|
| 100 |
-
|
| 101 |
-
# Return the top prediction from EMA
|
| 102 |
-
if not current_ema: return None
|
| 103 |
-
top_label = max(current_ema, key=current_ema.get)
|
| 104 |
-
return f"{top_label} ({current_ema[top_label]:.2f})"
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
# --- Load Models ---
|
| 108 |
try:
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
|
|
|
| 113 |
st.stop()
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
|
| 172 |
-
|
| 173 |
-
|
|
|
|
|
|
|
| 174 |
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
cap.release()
|
| 191 |
-
st.sidebar.info("Webcam stopped.")
|
| 192 |
-
|
| 193 |
-
# Add a placeholder for Video processing, which would be similar to Webcam but with a file uploader.
|
| 194 |
-
elif app_mode == "Video":
|
| 195 |
-
st.sidebar.warning("Video processing is similar to the webcam feed but processes a file. This feature is not fully implemented in this demo but follows the same logic.")
|
| 196 |
-
# You would use cv2.VideoCapture(video_path) and loop through frames.
|
|
|
|
| 2 |
import cv2
|
| 3 |
import numpy as np
|
| 4 |
from PIL import Image
|
| 5 |
+
import tensorflow as tf
|
| 6 |
+
import sys
|
| 7 |
+
import os
|
| 8 |
+
import tempfile
|
| 9 |
+
import time
|
| 10 |
+
from streamlit_option_menu import option_menu
|
| 11 |
|
| 12 |
+
# --- Page Config (Set once at the top) ---
|
| 13 |
+
st.set_page_config(page_title="Facial Analysis", page_icon="👤", layout="wide", initial_sidebar_state="expanded")
|
| 14 |
|
| 15 |
+
# --- Backend Loading (Robust and Unchanged) ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
try:
|
| 17 |
+
src_path = os.path.abspath(os.path.join(os.path.dirname(__file__), 'src'))
|
| 18 |
+
if src_path not in sys.path: sys.path.append(src_path)
|
| 19 |
+
from cnnClassifier.pipeline.prediction import PredictionPipeline
|
| 20 |
+
except ImportError:
|
| 21 |
+
st.error("FATAL: Prediction pipeline not found. Check project structure.")
|
| 22 |
st.stop()
|
| 23 |
+
try:
|
| 24 |
+
gpus = tf.config.list_physical_devices('GPU')
|
| 25 |
+
if gpus:
|
| 26 |
+
for gpu in gpus: tf.config.experimental.set_memory_growth(gpu, True)
|
| 27 |
+
except Exception: pass
|
| 28 |
+
@st.cache_resource
|
| 29 |
+
def load_pipeline():
|
| 30 |
+
return PredictionPipeline()
|
| 31 |
+
pipeline = load_pipeline()
|
| 32 |
+
|
| 33 |
+
# --- Session State for Webcam Control ---
|
| 34 |
+
if 'webcam_running' not in st.session_state: st.session_state.webcam_running = False
|
| 35 |
+
def start_webcam(): st.session_state.webcam_running = True
|
| 36 |
+
def stop_webcam(): st.session_state.webcam_running = False
|
| 37 |
+
|
| 38 |
+
# --- Sidebar UI (Clean and Themed) ---
|
| 39 |
+
with st.sidebar:
|
| 40 |
+
st.markdown("## ⚙️ Controls")
|
| 41 |
+
app_mode = option_menu(
|
| 42 |
+
menu_title=None,
|
| 43 |
+
options=["Image", "Video", "Live Feed"],
|
| 44 |
+
icons=["image", "film", "camera-video"],
|
| 45 |
+
menu_icon="cast",
|
| 46 |
+
default_index=0,
|
| 47 |
+
)
|
| 48 |
+
st.divider()
|
| 49 |
+
st.info("This app uses a multi-task EfficientNet model to predict age and gender.")
|
| 50 |
+
|
| 51 |
+
# --- Main Page Content ---
|
| 52 |
+
st.title(f"👤 Facial Demographics Analysis")
|
| 53 |
+
st.markdown(f"### Mode: {app_mode}")
|
| 54 |
+
st.divider()
|
| 55 |
+
|
| 56 |
+
if not pipeline:
|
| 57 |
+
st.error("AI Pipeline failed to load. Please check the terminal for errors.")
|
| 58 |
+
else:
|
| 59 |
+
if app_mode == "Image":
|
| 60 |
+
uploaded_file = st.file_uploader("Upload an image for analysis", type=["jpg", "jpeg", "png"])
|
| 61 |
+
if uploaded_file:
|
| 62 |
+
image = Image.open(uploaded_file).convert("RGB")
|
| 63 |
+
col1, col2 = st.columns(2)
|
| 64 |
+
with col1: st.image(image, caption='Original Image', use_column_width=True)
|
| 65 |
+
with col2:
|
| 66 |
+
with st.spinner('🔬 Analyzing...'):
|
| 67 |
+
annotated_image, predictions = pipeline.predict_image(np.array(image))
|
| 68 |
+
st.image(annotated_image, caption='Processed Image', use_column_width=True)
|
| 69 |
+
if predictions:
|
| 70 |
+
with st.expander("View Details", expanded=True):
|
| 71 |
+
for i, p in enumerate(predictions):
|
| 72 |
+
st.write(f"**Face {i+1}:** Gender: `{p['gender']}`, Age Group: `{p['age']}`")
|
| 73 |
+
else: st.warning("No faces detected.")
|
| 74 |
+
|
| 75 |
+
elif app_mode == "Video":
|
| 76 |
+
uploaded_file = st.file_uploader("Upload a video for analysis", type=["mp4", "mov", "avi"])
|
| 77 |
+
if uploaded_file:
|
| 78 |
+
tfile = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4')
|
| 79 |
+
tfile.write(uploaded_file.read())
|
| 80 |
+
cap = cv2.VideoCapture(tfile.name)
|
| 81 |
+
frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
| 82 |
+
st.info(f"Video has {frame_count} frames.")
|
| 83 |
+
if st.button("Start Video Processing", type="primary", use_container_width=True):
|
| 84 |
+
progress_bar = st.progress(0, text="Initializing...")
|
| 85 |
+
out_tfile = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4')
|
| 86 |
+
h, w = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)), int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
| 87 |
+
out = cv2.VideoWriter(out_tfile.name, cv2.VideoWriter_fourcc(*'mp4v'), cap.get(cv2.CAP_PROP_FPS), (w, h))
|
| 88 |
+
def frame_generator():
|
| 89 |
+
for _ in range(frame_count):
|
| 90 |
+
ret, frame = cap.read()
|
| 91 |
+
if not ret: break
|
| 92 |
+
yield cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
| 93 |
+
for i, annotated_frame_rgb in enumerate(pipeline.process_video_stream(frame_generator())):
|
| 94 |
+
out.write(cv2.cvtColor(annotated_frame_rgb, cv2.COLOR_RGB2BGR))
|
| 95 |
+
progress_bar.progress((i + 1) / frame_count, text=f"Processing Frame {i+1}/{frame_count}")
|
| 96 |
+
cap.release(), out.release()
|
| 97 |
+
st.success("Video processing complete!")
|
| 98 |
+
st.video(out_tfile.name)
|
| 99 |
+
with open(out_tfile.name, "rb") as f:
|
| 100 |
+
st.download_button("Download Processed Video", f, "output.mp4", "video/mp4", use_container_width=True)
|
| 101 |
+
|
| 102 |
+
elif app_mode == "Live Feed":
|
| 103 |
+
col1, col2 = st.columns(2)
|
| 104 |
+
with col1: st.button("Start Feed", on_click=start_webcam, use_container_width=True, type="primary")
|
| 105 |
+
with col2: st.button("Stop Feed", on_click=stop_webcam, use_container_width=True)
|
| 106 |
|
| 107 |
+
_, center_col, _ = st.columns([1, 2, 1])
|
| 108 |
+
with center_col:
|
| 109 |
+
FRAME_WINDOW = st.image([])
|
| 110 |
+
fps_display = st.empty()
|
| 111 |
|
| 112 |
+
if st.session_state.webcam_running:
|
| 113 |
+
cap = cv2.VideoCapture(0)
|
| 114 |
+
while st.session_state.webcam_running:
|
| 115 |
+
start_time = time.time()
|
| 116 |
+
ret, frame = cap.read()
|
| 117 |
+
if not ret: break
|
| 118 |
+
frame = cv2.flip(frame, 1)
|
| 119 |
+
annotated_frame = pipeline.process_live_frame(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
|
| 120 |
+
FRAME_WINDOW.image(annotated_frame, channels="RGB")
|
| 121 |
+
fps = 1.0 / (time.time() - start_time) if (time.time() - start_time) > 0 else 0
|
| 122 |
+
fps_display.markdown(f"<p style='text-align: center;'><b>FPS: {fps:.2f}</b></p>", unsafe_allow_html=True)
|
| 123 |
+
cap.release()
|
| 124 |
+
cv2.destroyAllWindows()
|
| 125 |
+
st.session_state.webcam_running = False
|
| 126 |
+
st.rerun()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
config/config.yaml
CHANGED
|
@@ -2,17 +2,22 @@ artifacts_root: artifacts
|
|
| 2 |
|
| 3 |
data_ingestion:
|
| 4 |
root_dir: artifacts/data_ingestion
|
| 5 |
-
dataset_name:
|
| 6 |
-
|
| 7 |
-
|
| 8 |
|
| 9 |
data_preparation:
|
| 10 |
root_dir: artifacts/data_preparation
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
| 13 |
|
| 14 |
-
|
| 15 |
-
root_dir: artifacts/
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
data_ingestion:
|
| 4 |
root_dir: artifacts/data_ingestion
|
| 5 |
+
dataset_name: "HuggingFaceM4/FairFace"
|
| 6 |
+
dataset_config: "0.25"
|
| 7 |
+
local_data_dir: artifacts/data_ingestion/dataset
|
| 8 |
|
| 9 |
data_preparation:
|
| 10 |
root_dir: artifacts/data_preparation
|
| 11 |
+
# Path to the raw dataset downloaded by the previous stage
|
| 12 |
+
raw_data_path: artifacts/data_ingestion/dataset
|
| 13 |
+
# Path where we will save the cleaned dataframe
|
| 14 |
+
cleaned_data_path: artifacts/data_preparation/fairface_cleaned.csv
|
| 15 |
|
| 16 |
+
multi_task_model_trainer:
|
| 17 |
+
root_dir: artifacts/multi_task_model_trainer
|
| 18 |
+
# The path to our cleaned CSV file from the previous stage
|
| 19 |
+
data_path: artifacts/data_preparation/fairface_cleaned.csv
|
| 20 |
+
# Where to save the final multi-task model
|
| 21 |
+
trained_model_path: artifacts/multi_task_model_trainer/facial_demographics_model
|
| 22 |
+
# The base model from Hugging Face
|
| 23 |
+
model_name: "google/efficientnet-b2"
|
dvc.lock
CHANGED
|
@@ -3,24 +3,24 @@ stages:
|
|
| 3 |
data_ingestion:
|
| 4 |
cmd: python src/cnnClassifier/pipeline/stage_01_data_ingestion.py
|
| 5 |
deps:
|
| 6 |
-
- path: config/config.yaml
|
| 7 |
-
hash: md5
|
| 8 |
-
md5: 3cea2dfb36f0a5e40dd599dad9458ca4
|
| 9 |
-
size: 609
|
| 10 |
- path: src/cnnClassifier/components/data_ingestion.py
|
| 11 |
hash: md5
|
| 12 |
-
md5:
|
| 13 |
-
size:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
- path: src/cnnClassifier/pipeline/stage_01_data_ingestion.py
|
| 15 |
hash: md5
|
| 16 |
-
md5:
|
| 17 |
-
size:
|
| 18 |
outs:
|
| 19 |
- path: artifacts/data_ingestion
|
| 20 |
hash: md5
|
| 21 |
-
md5:
|
| 22 |
-
size:
|
| 23 |
-
nfiles:
|
| 24 |
model_training:
|
| 25 |
cmd: python src/cnnClassifier/pipeline/stage_02_model_training.py
|
| 26 |
deps:
|
|
@@ -51,3 +51,59 @@ stages:
|
|
| 51 |
md5: 621f61ba7beea89c3bef7a921afdcc9d.dir
|
| 52 |
size: 183039001
|
| 53 |
nfiles: 12
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
data_ingestion:
|
| 4 |
cmd: python src/cnnClassifier/pipeline/stage_01_data_ingestion.py
|
| 5 |
deps:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
- path: src/cnnClassifier/components/data_ingestion.py
|
| 7 |
hash: md5
|
| 8 |
+
md5: 11a88b6a3a45651504f59cab654dd889
|
| 9 |
+
size: 1737
|
| 10 |
+
- path: src/cnnClassifier/config/configuration.py
|
| 11 |
+
hash: md5
|
| 12 |
+
md5: f2ee2d8b6bf946faa613a7d498bf789a
|
| 13 |
+
size: 2458
|
| 14 |
- path: src/cnnClassifier/pipeline/stage_01_data_ingestion.py
|
| 15 |
hash: md5
|
| 16 |
+
md5: ba77afc62b94ad61c5990601bc2a6f15
|
| 17 |
+
size: 945
|
| 18 |
outs:
|
| 19 |
- path: artifacts/data_ingestion
|
| 20 |
hash: md5
|
| 21 |
+
md5: 010e49306f9a2f8bd5f6235a9bb4c40a.dir
|
| 22 |
+
size: 1158773512
|
| 23 |
+
nfiles: 12
|
| 24 |
model_training:
|
| 25 |
cmd: python src/cnnClassifier/pipeline/stage_02_model_training.py
|
| 26 |
deps:
|
|
|
|
| 51 |
md5: 621f61ba7beea89c3bef7a921afdcc9d.dir
|
| 52 |
size: 183039001
|
| 53 |
nfiles: 12
|
| 54 |
+
data_preparation:
|
| 55 |
+
cmd: python src/cnnClassifier/pipeline/stage_02_data_preparation.py
|
| 56 |
+
deps:
|
| 57 |
+
- path: artifacts/data_ingestion/dataset
|
| 58 |
+
hash: md5
|
| 59 |
+
md5: 86434e33cd2b0a60b09c0624d29f1fda.dir
|
| 60 |
+
size: 579423180
|
| 61 |
+
nfiles: 8
|
| 62 |
+
- path: config/config.yaml
|
| 63 |
+
hash: md5
|
| 64 |
+
md5: bc8e095ab04797e455847fc34f3db546
|
| 65 |
+
size: 908
|
| 66 |
+
- path: src/cnnClassifier/components/data_preparation.py
|
| 67 |
+
hash: md5
|
| 68 |
+
md5: 39d7a55e908ab9b099f55c21a3019014
|
| 69 |
+
size: 2175
|
| 70 |
+
- path: src/cnnClassifier/pipeline/stage_02_data_preparation.py
|
| 71 |
+
hash: md5
|
| 72 |
+
md5: d04a9dd31ed636a27a79235a0dff46a6
|
| 73 |
+
size: 953
|
| 74 |
+
outs:
|
| 75 |
+
- path: artifacts/data_preparation
|
| 76 |
+
hash: md5
|
| 77 |
+
md5: 412fbf5339e9b82998c7617d19fce476.dir
|
| 78 |
+
size: 580361278
|
| 79 |
+
nfiles: 97699
|
| 80 |
+
multi_task_model_training:
|
| 81 |
+
cmd: python src/cnnClassifier/pipeline/stage_03_multi_task_model_training.py
|
| 82 |
+
deps:
|
| 83 |
+
- path: artifacts/data_preparation
|
| 84 |
+
hash: md5
|
| 85 |
+
md5: 412fbf5339e9b82998c7617d19fce476.dir
|
| 86 |
+
size: 580361278
|
| 87 |
+
nfiles: 97699
|
| 88 |
+
- path: config/config.yaml
|
| 89 |
+
hash: md5
|
| 90 |
+
md5: bc8e095ab04797e455847fc34f3db546
|
| 91 |
+
size: 908
|
| 92 |
+
- path: params.yaml
|
| 93 |
+
hash: md5
|
| 94 |
+
md5: ce8c137aa11f22d0901fb41485e9bfde
|
| 95 |
+
size: 239
|
| 96 |
+
- path: src/cnnClassifier/components/multi_task_model_trainer.py
|
| 97 |
+
hash: md5
|
| 98 |
+
md5: 5429e22ede43731b1806a9218c41c6d7
|
| 99 |
+
size: 7510
|
| 100 |
+
- path: src/cnnClassifier/pipeline/stage_03_multi_task_model_training.py
|
| 101 |
+
hash: md5
|
| 102 |
+
md5: 317cd31673bc55d1d73dea54cb75a8e0
|
| 103 |
+
size: 974
|
| 104 |
+
outs:
|
| 105 |
+
- path: artifacts/multi_task_model_trainer
|
| 106 |
+
hash: md5
|
| 107 |
+
md5: b641e844039bc54d5c29145dfb0bab77.dir
|
| 108 |
+
size: 1898189766
|
| 109 |
+
nfiles: 123
|
dvc.yaml
CHANGED
|
@@ -4,17 +4,28 @@ stages:
|
|
| 4 |
deps:
|
| 5 |
- src/cnnClassifier/pipeline/stage_01_data_ingestion.py
|
| 6 |
- src/cnnClassifier/components/data_ingestion.py
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
- config/config.yaml
|
|
|
|
| 8 |
outs:
|
| 9 |
-
- artifacts/
|
| 10 |
|
| 11 |
-
|
| 12 |
-
cmd: python src/cnnClassifier/pipeline/
|
| 13 |
deps:
|
| 14 |
-
- src/cnnClassifier/pipeline/
|
| 15 |
-
- src/cnnClassifier/components/
|
| 16 |
- config/config.yaml
|
| 17 |
- params.yaml
|
| 18 |
-
- artifacts/
|
| 19 |
outs:
|
| 20 |
-
- artifacts/
|
|
|
|
|
|
| 4 |
deps:
|
| 5 |
- src/cnnClassifier/pipeline/stage_01_data_ingestion.py
|
| 6 |
- src/cnnClassifier/components/data_ingestion.py
|
| 7 |
+
- src/cnnClassifier/config/configuration.py
|
| 8 |
+
outs:
|
| 9 |
+
- artifacts/data_ingestion # This output now includes the saved dataset
|
| 10 |
+
|
| 11 |
+
data_preparation: # <<< NEW STAGE
|
| 12 |
+
cmd: python src/cnnClassifier/pipeline/stage_02_data_preparation.py
|
| 13 |
+
deps:
|
| 14 |
+
- src/cnnClassifier/pipeline/stage_02_data_preparation.py
|
| 15 |
+
- src/cnnClassifier/components/data_preparation.py
|
| 16 |
- config/config.yaml
|
| 17 |
+
- artifacts/data_ingestion/dataset # Depends on the output of the first stage
|
| 18 |
outs:
|
| 19 |
+
- artifacts/data_preparation
|
| 20 |
|
| 21 |
+
multi_task_model_training: # <<< NEW STAGE
|
| 22 |
+
cmd: python src/cnnClassifier/pipeline/stage_03_multi_task_model_training.py
|
| 23 |
deps:
|
| 24 |
+
- src/cnnClassifier/pipeline/stage_03_multi_task_model_training.py
|
| 25 |
+
- src/cnnClassifier/components/multi_task_model_trainer.py
|
| 26 |
- config/config.yaml
|
| 27 |
- params.yaml
|
| 28 |
+
- artifacts/data_preparation
|
| 29 |
outs:
|
| 30 |
+
- artifacts/multi_task_model_trainer
|
| 31 |
+
|
requirements.txt
CHANGED
|
@@ -25,12 +25,12 @@ scikit-learn
|
|
| 25 |
Pillow
|
| 26 |
tqdm
|
| 27 |
imblearn
|
| 28 |
-
|
| 29 |
# Frontend and Real-time Processing
|
| 30 |
streamlit
|
| 31 |
opencv-python
|
| 32 |
mtcnn
|
| 33 |
tensorflow==2.15.0
|
| 34 |
-
|
| 35 |
# AWS Deployment
|
| 36 |
boto3
|
|
|
|
| 25 |
Pillow
|
| 26 |
tqdm
|
| 27 |
imblearn
|
| 28 |
+
seaborn
|
| 29 |
# Frontend and Real-time Processing
|
| 30 |
streamlit
|
| 31 |
opencv-python
|
| 32 |
mtcnn
|
| 33 |
tensorflow==2.15.0
|
| 34 |
+
streamlit-option-menu
|
| 35 |
# AWS Deployment
|
| 36 |
boto3
|
research/evaluation.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# research/evaluation.py
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
import pandas as pd
|
| 5 |
+
import json
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
import sys
|
| 8 |
+
import os
|
| 9 |
+
from sklearn.model_selection import train_test_split
|
| 10 |
+
from sklearn.metrics import accuracy_score, confusion_matrix, classification_report
|
| 11 |
+
import seaborn as sns
|
| 12 |
+
import matplotlib.pyplot as plt
|
| 13 |
+
from torch.utils.data import DataLoader
|
| 14 |
+
from tqdm import tqdm
|
| 15 |
+
|
| 16 |
+
# Add the project's src directory to the Python path
|
| 17 |
+
# This allows us to import our custom modules
|
| 18 |
+
src_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'src'))
|
| 19 |
+
sys.path.append(src_path)
|
| 20 |
+
|
| 21 |
+
# Now we can import our custom classes
|
| 22 |
+
from cnnClassifier.components.multi_task_model_trainer import MultiTaskEfficientNet, FairFaceDataset
|
| 23 |
+
from cnnClassifier.utils.common import read_yaml
|
| 24 |
+
from torchvision.transforms import Compose, Resize, ToTensor, Normalize
|
| 25 |
+
from transformers import AutoImageProcessor
|
| 26 |
+
|
| 27 |
+
# ==============================================================================
|
| 28 |
+
# CONFIGURATION
|
| 29 |
+
# ==============================================================================
|
| 30 |
+
# Define paths directly. We are not using the config manager.
|
| 31 |
+
MODEL_PATH = Path("artifacts/multi_task_model_trainer/facial_demographics_model")
|
| 32 |
+
DATA_PATH = Path("artifacts/data_preparation/fairface_cleaned.csv")
|
| 33 |
+
PARAMS_PATH = Path("params.yaml")
|
| 34 |
+
EVALUATION_OUTPUT_DIR = Path("artifacts/manual_evaluation")
|
| 35 |
+
EVALUATION_OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
|
| 36 |
+
|
| 37 |
+
# Load parameters
|
| 38 |
+
params = read_yaml(PARAMS_PATH)
|
| 39 |
+
IMAGE_SIZE = params.IMAGE_SIZE
|
| 40 |
+
BATCH_SIZE = params.BATCH_SIZE
|
| 41 |
+
TEST_SPLIT_SIZE = params.TEST_SPLIT_SIZE
|
| 42 |
+
RANDOM_STATE = params.RANDOM_STATE
|
| 43 |
+
|
| 44 |
+
# ==============================================================================
|
| 45 |
+
# MAIN EVALUATION LOGIC
|
| 46 |
+
# ==============================================================================
|
| 47 |
+
def evaluate_model():
|
| 48 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 49 |
+
print(f"--- Running evaluation on device: {device} ---")
|
| 50 |
+
|
| 51 |
+
# 1. Load data and prepare the test split
|
| 52 |
+
print("Loading and preparing test data...")
|
| 53 |
+
df = pd.read_csv(DATA_PATH)
|
| 54 |
+
|
| 55 |
+
label_maps = {}
|
| 56 |
+
for task in ['age', 'gender', 'race']:
|
| 57 |
+
labels = sorted(df[task].unique())
|
| 58 |
+
label_maps[f'{task}_label2id'] = {label: i for i, label in enumerate(labels)}
|
| 59 |
+
label_maps[f'{task}_id2label'] = {i: label for i, label in enumerate(labels)}
|
| 60 |
+
df[f'{task}_id'] = df[task].map(label_maps[f'{task}_label2id'])
|
| 61 |
+
|
| 62 |
+
# Use the same random_state to ensure we get the identical test split as in training
|
| 63 |
+
_, test_df = train_test_split(
|
| 64 |
+
df,
|
| 65 |
+
test_size=TEST_SPLIT_SIZE,
|
| 66 |
+
random_state=RANDOM_STATE,
|
| 67 |
+
stratify=df['age']
|
| 68 |
+
)
|
| 69 |
+
|
| 70 |
+
# 2. Create the PyTorch DataLoader
|
| 71 |
+
model_config = read_yaml(Path("config/config.yaml"))
|
| 72 |
+
base_model_name = model_config.multi_task_model_trainer.model_name
|
| 73 |
+
processor = AutoImageProcessor.from_pretrained(base_model_name)
|
| 74 |
+
_transforms = Compose([
|
| 75 |
+
Resize((IMAGE_SIZE, IMAGE_SIZE)),
|
| 76 |
+
ToTensor(),
|
| 77 |
+
Normalize(mean=processor.image_mean, std=processor.image_std)
|
| 78 |
+
])
|
| 79 |
+
|
| 80 |
+
test_dataset = FairFaceDataset(dataframe=test_df, processor=processor, transforms=_transforms)
|
| 81 |
+
test_dataloader = DataLoader(test_dataset, batch_size=BATCH_SIZE, shuffle=False)
|
| 82 |
+
|
| 83 |
+
# 3. Load the trained model
|
| 84 |
+
print(f"Loading model from: {MODEL_PATH}")
|
| 85 |
+
model = MultiTaskEfficientNet(
|
| 86 |
+
model_name=str(MODEL_PATH), # Pass the path as the model name
|
| 87 |
+
num_labels_age=len(label_maps['age_id2label']),
|
| 88 |
+
num_labels_gender=len(label_maps['gender_id2label']),
|
| 89 |
+
num_labels_race=len(label_maps['race_id2label']),
|
| 90 |
+
).to(device)
|
| 91 |
+
|
| 92 |
+
# Load the trained weights
|
| 93 |
+
model.load_state_dict(torch.load(MODEL_PATH / 'pytorch_model.bin', map_location=device))
|
| 94 |
+
model.eval()
|
| 95 |
+
|
| 96 |
+
# 4. Run predictions on the test set
|
| 97 |
+
print("Running predictions on the test set...")
|
| 98 |
+
all_preds = {'age': [], 'gender': [], 'race': []}
|
| 99 |
+
all_labels = {'age': [], 'gender': [], 'race': []}
|
| 100 |
+
|
| 101 |
+
for batch in tqdm(test_dataloader, desc="Evaluating"):
|
| 102 |
+
pixel_values = batch['pixel_values'].to(device)
|
| 103 |
+
labels = batch['labels']
|
| 104 |
+
|
| 105 |
+
with torch.no_grad():
|
| 106 |
+
outputs = model(pixel_values=pixel_values)
|
| 107 |
+
|
| 108 |
+
all_preds['age'].extend(outputs['age_logits'].argmax(1).cpu().numpy())
|
| 109 |
+
all_preds['gender'].extend(outputs['gender_logits'].argmax(1).cpu().numpy())
|
| 110 |
+
all_preds['race'].extend(outputs['race_logits'].argmax(1).cpu().numpy())
|
| 111 |
+
|
| 112 |
+
all_labels['age'].extend(labels['age'].cpu().numpy())
|
| 113 |
+
all_labels['gender'].extend(labels['gender'].cpu().numpy())
|
| 114 |
+
all_labels['race'].extend(labels['race'].cpu().numpy())
|
| 115 |
+
|
| 116 |
+
# 5. Calculate metrics, generate reports, and save artifacts
|
| 117 |
+
print("--- Evaluation Results ---")
|
| 118 |
+
metrics = {}
|
| 119 |
+
for task in ['age', 'gender', 'race']:
|
| 120 |
+
accuracy = accuracy_score(all_labels[task], all_preds[task])
|
| 121 |
+
print(f"\n--- {task.capitalize()} ---")
|
| 122 |
+
print(f"Accuracy: {accuracy:.4f}")
|
| 123 |
+
|
| 124 |
+
report_str = classification_report(
|
| 125 |
+
all_labels[task],
|
| 126 |
+
all_preds[task],
|
| 127 |
+
target_names=list(label_maps[f'{task}_id2label'].values())
|
| 128 |
+
)
|
| 129 |
+
print("Classification Report:")
|
| 130 |
+
print(report_str)
|
| 131 |
+
|
| 132 |
+
metrics[f'{task}_accuracy'] = accuracy
|
| 133 |
+
|
| 134 |
+
# Confusion Matrix
|
| 135 |
+
cm = confusion_matrix(all_labels[task], all_preds[task])
|
| 136 |
+
plt.figure(figsize=(12, 10))
|
| 137 |
+
sns.heatmap(cm, annot=True, fmt='d', xticklabels=list(label_maps[f'{task}_id2label'].values()), yticklabels=list(label_maps[f'{task}_id2label'].values()), cmap='Blues')
|
| 138 |
+
plt.title(f'Confusion Matrix for {task.capitalize()}', fontsize=16)
|
| 139 |
+
plt.ylabel('Actual', fontsize=12)
|
| 140 |
+
plt.xlabel('Predicted', fontsize=12)
|
| 141 |
+
plt.xticks(rotation=45)
|
| 142 |
+
plt.yticks(rotation=0)
|
| 143 |
+
|
| 144 |
+
cm_path = EVALUATION_OUTPUT_DIR / f'{task}_confusion_matrix.png'
|
| 145 |
+
plt.savefig(cm_path, bbox_inches='tight')
|
| 146 |
+
plt.close() # Close the plot to avoid displaying it in the console
|
| 147 |
+
print(f"Saved {task} confusion matrix to {cm_path}")
|
| 148 |
+
|
| 149 |
+
# Save metrics to a JSON file
|
| 150 |
+
metrics_path = EVALUATION_OUTPUT_DIR / "metrics.json"
|
| 151 |
+
with open(metrics_path, 'w') as f:
|
| 152 |
+
json.dump(metrics, f, indent=4)
|
| 153 |
+
print(f"\nSaved final metrics to {metrics_path}")
|
| 154 |
+
|
| 155 |
+
# Save the label maps used for this evaluation run
|
| 156 |
+
label_maps_path = EVALUATION_OUTPUT_DIR / "label_maps.json"
|
| 157 |
+
with open(label_maps_path, 'w') as f:
|
| 158 |
+
json.dump(label_maps, f, indent=4)
|
| 159 |
+
print(f"Saved label maps to {label_maps_path}")
|
| 160 |
+
|
| 161 |
+
|
| 162 |
+
if __name__ == '__main__':
|
| 163 |
+
evaluate_model()
|
src/cnnClassifier/components/data_ingestion.py
CHANGED
|
@@ -1,34 +1,40 @@
|
|
| 1 |
-
|
| 2 |
-
|
|
|
|
| 3 |
from cnnClassifier import logger
|
| 4 |
from cnnClassifier.entity.config_entity import DataIngestionConfig
|
|
|
|
| 5 |
|
| 6 |
class DataIngestion:
|
| 7 |
def __init__(self, config: DataIngestionConfig):
|
| 8 |
self.config = config
|
| 9 |
|
| 10 |
-
def
|
| 11 |
"""
|
| 12 |
-
Downloads the dataset from
|
| 13 |
-
Make sure to have your kaggle.json file in ~/.kaggle/ or set KAGGLE_USERNAME and KAGGLE_KEY env variables.
|
| 14 |
"""
|
| 15 |
try:
|
| 16 |
-
logger.info(f"Downloading dataset
|
| 17 |
-
|
| 18 |
-
#
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
zip_ref.extractall(unzip_path)
|
| 34 |
-
logger.info(f"Dataset extracted to {unzip_path}")
|
|
|
|
| 1 |
+
# src/cnnClassifier/components/data_ingestion.py
|
| 2 |
+
|
| 3 |
+
from datasets import load_dataset
|
| 4 |
from cnnClassifier import logger
|
| 5 |
from cnnClassifier.entity.config_entity import DataIngestionConfig
|
| 6 |
+
from pathlib import Path
|
| 7 |
|
| 8 |
class DataIngestion:
|
| 9 |
def __init__(self, config: DataIngestionConfig):
|
| 10 |
self.config = config
|
| 11 |
|
| 12 |
+
def download_dataset(self):
|
| 13 |
"""
|
| 14 |
+
Downloads and saves the FairFace dataset from the Hugging Face Hub.
|
|
|
|
| 15 |
"""
|
| 16 |
try:
|
| 17 |
+
logger.info(f"Downloading dataset '{self.config.dataset_name}' from Hugging Face Hub...")
|
| 18 |
+
|
| 19 |
+
# load_dataset handles everything: download, verification, and caching
|
| 20 |
+
# It returns a DatasetDict, typically with 'train' and 'validation' splits
|
| 21 |
+
fairface_dataset = load_dataset(
|
| 22 |
+
self.config.dataset_name,
|
| 23 |
+
name=self.config.dataset_config,
|
| 24 |
+
cache_dir=self.config.root_dir # Use our root_dir for caching
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
# Save the downloaded dataset to our specified artifacts directory
|
| 28 |
+
# This makes it a persistent part of our DVC pipeline
|
| 29 |
+
save_path = Path(self.config.local_data_dir)
|
| 30 |
+
fairface_dataset.save_to_disk(save_path)
|
| 31 |
+
|
| 32 |
+
logger.info(f"Dataset successfully downloaded and saved to {save_path}")
|
| 33 |
|
| 34 |
+
# Optional: Log the structure of the downloaded dataset
|
| 35 |
+
logger.info(f"Dataset splits: {list(fairface_dataset.keys())}")
|
| 36 |
+
logger.info(f"Training set features: {fairface_dataset['train'].features}")
|
| 37 |
+
|
| 38 |
+
except Exception as e:
|
| 39 |
+
logger.error(f"Failed to download or save dataset. Error: {e}")
|
| 40 |
+
raise e
|
|
|
|
|
|
src/cnnClassifier/components/data_preparation.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from datasets import load_from_disk
|
| 2 |
+
import pandas as pd
|
| 3 |
+
from cnnClassifier import logger
|
| 4 |
+
from cnnClassifier.entity.config_entity import DataPreparationConfig
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from PIL import Image # <<< ADD THIS IMPORT
|
| 7 |
+
import io # <<< ADD THIS IMPORT
|
| 8 |
+
|
| 9 |
+
class DataPreparation:
|
| 10 |
+
def __init__(self, config: DataPreparationConfig):
|
| 11 |
+
self.config = config
|
| 12 |
+
|
| 13 |
+
def create_cleaned_dataframe(self):
|
| 14 |
+
try:
|
| 15 |
+
logger.info("Loading raw dataset to create cleaned CSV...")
|
| 16 |
+
raw_dataset = load_from_disk(self.config.raw_data_path)
|
| 17 |
+
|
| 18 |
+
df_train = raw_dataset['train'].to_pandas()
|
| 19 |
+
df_val = raw_dataset['validation'].to_pandas()
|
| 20 |
+
combined_df = pd.concat([df_train, df_val], ignore_index=True)
|
| 21 |
+
|
| 22 |
+
image_dir = Path("artifacts/data_preparation/images")
|
| 23 |
+
image_dir.mkdir(parents=True, exist_ok=True)
|
| 24 |
+
|
| 25 |
+
combined_df['image_file_path'] = [
|
| 26 |
+
str(image_dir / f"{i}.jpg") for i in range(len(combined_df))
|
| 27 |
+
]
|
| 28 |
+
|
| 29 |
+
# --- IMPORTANT ---
|
| 30 |
+
# We only need the file path for the CSV, so we drop the bulky 'image' column
|
| 31 |
+
final_df_for_csv = combined_df.drop(columns=['image'])
|
| 32 |
+
|
| 33 |
+
logger.info(f"Saving cleaned metadata to {self.config.cleaned_data_path}")
|
| 34 |
+
final_df_for_csv.to_csv(self.config.cleaned_data_path, index=False)
|
| 35 |
+
|
| 36 |
+
# --- CORRECTED IMAGE SAVING LOOP ---
|
| 37 |
+
logger.info(f"Deterministically saving images to {image_dir}...")
|
| 38 |
+
for i, row in combined_df.iterrows():
|
| 39 |
+
image_path = Path(row['image_file_path'])
|
| 40 |
+
image_dict = row['image']
|
| 41 |
+
|
| 42 |
+
# Recreate the PIL Image from the dictionary's bytes data
|
| 43 |
+
pil_image = Image.open(io.BytesIO(image_dict['bytes']))
|
| 44 |
+
|
| 45 |
+
# Now save the reconstructed PIL Image
|
| 46 |
+
pil_image.save(image_path)
|
| 47 |
+
|
| 48 |
+
except Exception as e:
|
| 49 |
+
logger.error(f"Failed during data preparation. Error: {e}")
|
| 50 |
+
raise e
|
src/cnnClassifier/components/multi_task_model_trainer.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import torch.nn as nn
|
| 3 |
+
import pandas as pd
|
| 4 |
+
import numpy as np
|
| 5 |
+
from torch.utils.data import Dataset
|
| 6 |
+
from transformers import (
|
| 7 |
+
AutoImageProcessor,
|
| 8 |
+
AutoModelForImageClassification,
|
| 9 |
+
TrainingArguments,
|
| 10 |
+
Trainer
|
| 11 |
+
)
|
| 12 |
+
from torchvision.transforms import (
|
| 13 |
+
Compose,
|
| 14 |
+
Normalize,
|
| 15 |
+
RandomRotation,
|
| 16 |
+
RandomHorizontalFlip,
|
| 17 |
+
Resize,
|
| 18 |
+
ToTensor
|
| 19 |
+
)
|
| 20 |
+
from cnnClassifier.entity.config_entity import MultiTaskModelTrainerConfig
|
| 21 |
+
from cnnClassifier import logger
|
| 22 |
+
from PIL import Image
|
| 23 |
+
from sklearn.model_selection import train_test_split
|
| 24 |
+
from sklearn.metrics import accuracy_score
|
| 25 |
+
|
| 26 |
+
class MultiTaskEfficientNet(nn.Module):
|
| 27 |
+
def __init__(self, model_name, num_labels_age, num_labels_gender, num_labels_race):
|
| 28 |
+
super().__init__()
|
| 29 |
+
self.efficientnet_base = AutoModelForImageClassification.from_pretrained(model_name, ignore_mismatched_sizes=True)
|
| 30 |
+
original_classifier = self.efficientnet_base.classifier
|
| 31 |
+
feature_dim = original_classifier.in_features
|
| 32 |
+
self.efficientnet_base.classifier = nn.Identity()
|
| 33 |
+
|
| 34 |
+
self.age_classifier = nn.Linear(feature_dim, num_labels_age)
|
| 35 |
+
self.gender_classifier = nn.Linear(feature_dim, num_labels_gender)
|
| 36 |
+
self.race_classifier = nn.Linear(feature_dim, num_labels_race)
|
| 37 |
+
|
| 38 |
+
def forward(self, pixel_values, labels=None):
|
| 39 |
+
features = self.efficientnet_base.efficientnet(pixel_values)
|
| 40 |
+
pooled_features = features.last_hidden_state.mean(dim=[2, 3])
|
| 41 |
+
age_logits = self.age_classifier(pooled_features)
|
| 42 |
+
gender_logits = self.gender_classifier(pooled_features)
|
| 43 |
+
race_logits = self.race_classifier(pooled_features)
|
| 44 |
+
|
| 45 |
+
loss = None
|
| 46 |
+
if labels is not None:
|
| 47 |
+
loss_fct = nn.CrossEntropyLoss()
|
| 48 |
+
age_loss = loss_fct(age_logits, labels[:, 0])
|
| 49 |
+
gender_loss = loss_fct(gender_logits, labels[:, 1])
|
| 50 |
+
race_loss = loss_fct(race_logits, labels[:, 2])
|
| 51 |
+
loss = (2.0 * age_loss) + gender_loss + race_loss
|
| 52 |
+
|
| 53 |
+
return {"loss": loss, "age_logits": age_logits, "gender_logits": gender_logits, "race_logits": race_logits}
|
| 54 |
+
|
| 55 |
+
class FairFaceDataset(Dataset):
|
| 56 |
+
def __init__(self, dataframe, processor, transforms):
|
| 57 |
+
self.dataframe = dataframe
|
| 58 |
+
self.processor = processor
|
| 59 |
+
self.transforms = transforms
|
| 60 |
+
self.normalize = Normalize(mean=processor.image_mean, std=processor.image_std)
|
| 61 |
+
|
| 62 |
+
def __len__(self):
|
| 63 |
+
return len(self.dataframe)
|
| 64 |
+
|
| 65 |
+
def __getitem__(self, idx):
|
| 66 |
+
row = self.dataframe.iloc[idx]
|
| 67 |
+
image_path = row['image_file_path']
|
| 68 |
+
image = Image.open(image_path).convert("RGB")
|
| 69 |
+
pixel_values = self.transforms(image)
|
| 70 |
+
pixel_values = self.normalize(pixel_values)
|
| 71 |
+
|
| 72 |
+
labels = torch.tensor([row['age_id'], row['gender_id'], row['race_id']], dtype=torch.long)
|
| 73 |
+
return {"pixel_values": pixel_values, "labels": labels}
|
| 74 |
+
|
| 75 |
+
def compute_multitask_metrics(eval_pred):
|
| 76 |
+
predictions, labels = eval_pred
|
| 77 |
+
age_logits, gender_logits, race_logits = predictions['age_logits'], predictions['gender_logits'], predictions['race_logits']
|
| 78 |
+
age_preds = np.argmax(age_logits, axis=1)
|
| 79 |
+
gender_preds = np.argmax(gender_logits, axis=1)
|
| 80 |
+
race_preds = np.argmax(race_logits, axis=1)
|
| 81 |
+
age_labels, gender_labels, race_labels = labels[:, 0], labels[:, 1], labels[:, 2]
|
| 82 |
+
age_acc = accuracy_score(age_labels, age_preds)
|
| 83 |
+
gender_acc = accuracy_score(gender_labels, gender_preds)
|
| 84 |
+
race_acc = accuracy_score(race_labels, race_preds)
|
| 85 |
+
overall_acc = (age_acc + gender_acc + race_acc) / 3.0
|
| 86 |
+
return {"age_accuracy": age_acc, "gender_accuracy": gender_acc, "race_accuracy": race_acc, "overall_accuracy": overall_acc}
|
| 87 |
+
|
| 88 |
+
class MultiTaskModelTrainer:
|
| 89 |
+
def __init__(self, config: MultiTaskModelTrainerConfig):
|
| 90 |
+
self.config = config
|
| 91 |
+
self.processor = AutoImageProcessor.from_pretrained(config.model_name)
|
| 92 |
+
|
| 93 |
+
def train(self):
|
| 94 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 95 |
+
logger.info(f"Using device: {device}")
|
| 96 |
+
|
| 97 |
+
logger.info("Loading and preparing dataset from cleaned CSV...")
|
| 98 |
+
df = pd.read_csv(self.config.data_path)
|
| 99 |
+
|
| 100 |
+
label_maps = {}
|
| 101 |
+
for task in ['age', 'gender', 'race']:
|
| 102 |
+
labels = sorted(df[task].unique())
|
| 103 |
+
label_maps[f'{task}_label2id'] = {label: i for i, label in enumerate(labels)}
|
| 104 |
+
df[f'{task}_id'] = df[task].map(label_maps[f'{task}_label2id'])
|
| 105 |
+
|
| 106 |
+
num_classes_age = len(label_maps['age_label2id'])
|
| 107 |
+
num_classes_gender = len(label_maps['gender_label2id'])
|
| 108 |
+
num_classes_race = len(label_maps['race_label2id'])
|
| 109 |
+
train_df, test_df = train_test_split(df, test_size=self.config.test_split_size, random_state=self.config.random_state, stratify=df['age'])
|
| 110 |
+
|
| 111 |
+
train_transforms = Compose([
|
| 112 |
+
Resize((self.config.image_size, self.config.image_size)),
|
| 113 |
+
RandomHorizontalFlip(),
|
| 114 |
+
RandomRotation(10),
|
| 115 |
+
ToTensor(), # Normalization is now in the Dataset
|
| 116 |
+
])
|
| 117 |
+
|
| 118 |
+
val_transforms = Compose([
|
| 119 |
+
Resize((self.config.image_size, self.config.image_size)),
|
| 120 |
+
ToTensor(),
|
| 121 |
+
])
|
| 122 |
+
|
| 123 |
+
train_dataset = FairFaceDataset(dataframe=train_df, processor=self.processor, transforms=train_transforms)
|
| 124 |
+
test_dataset = FairFaceDataset(dataframe=test_df, processor=self.processor, transforms=val_transforms)
|
| 125 |
+
|
| 126 |
+
model = MultiTaskEfficientNet(model_name=self.config.model_name, num_labels_age=num_classes_age, num_labels_gender=num_classes_gender, num_labels_race=num_classes_race).to(device)
|
| 127 |
+
|
| 128 |
+
args = TrainingArguments(
|
| 129 |
+
output_dir=self.config.root_dir,
|
| 130 |
+
logging_dir=f'{self.config.root_dir}/logs',
|
| 131 |
+
evaluation_strategy="epoch",
|
| 132 |
+
learning_rate=self.config.learning_rate,
|
| 133 |
+
per_device_train_batch_size=self.config.batch_size,
|
| 134 |
+
per_device_eval_batch_size=self.config.batch_size,
|
| 135 |
+
num_train_epochs=self.config.num_train_epochs,
|
| 136 |
+
weight_decay=self.config.weight_decay,
|
| 137 |
+
save_strategy='epoch',
|
| 138 |
+
load_best_model_at_end=True,
|
| 139 |
+
metric_for_best_model="eval_overall_accuracy",
|
| 140 |
+
dataloader_num_workers=4,
|
| 141 |
+
lr_scheduler_type='cosine',
|
| 142 |
+
report_to="none"
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
class EvalTrainer(Trainer):
|
| 146 |
+
def prediction_step(self, model, inputs, prediction_loss_only, ignore_keys=None):
|
| 147 |
+
has_labels = "labels" in inputs
|
| 148 |
+
inputs = self._prepare_inputs(inputs)
|
| 149 |
+
with torch.no_grad():
|
| 150 |
+
outputs = model(**inputs)
|
| 151 |
+
loss = outputs.get("loss")
|
| 152 |
+
predictions = {"age_logits": outputs["age_logits"], "gender_logits": outputs["gender_logits"], "race_logits": outputs["race_logits"]}
|
| 153 |
+
return (loss, predictions, inputs["labels"] if has_labels else None)
|
| 154 |
+
|
| 155 |
+
trainer = EvalTrainer(model=model, args=args, train_dataset=train_dataset, eval_dataset=test_dataset, compute_metrics=compute_multitask_metrics)
|
| 156 |
+
|
| 157 |
+
trainer.train()
|
| 158 |
+
|
| 159 |
+
logger.info(f"Saving final model and processor to {self.config.trained_model_path}")
|
| 160 |
+
trainer.save_model(self.config.trained_model_path)
|
| 161 |
+
self.processor.save_pretrained(self.config.trained_model_path)
|
src/cnnClassifier/config/configuration.py
CHANGED
|
@@ -1,6 +1,10 @@
|
|
| 1 |
from cnnClassifier.constants import *
|
| 2 |
from cnnClassifier.utils.common import read_yaml, create_directories
|
| 3 |
-
from cnnClassifier.entity.config_entity import
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
|
| 5 |
class ConfigurationManager:
|
| 6 |
def __init__(
|
|
@@ -15,46 +19,45 @@ class ConfigurationManager:
|
|
| 15 |
|
| 16 |
def get_data_ingestion_config(self) -> DataIngestionConfig:
|
| 17 |
config = self.config.data_ingestion
|
| 18 |
-
|
| 19 |
create_directories([config.root_dir])
|
| 20 |
|
| 21 |
data_ingestion_config = DataIngestionConfig(
|
| 22 |
root_dir=config.root_dir,
|
| 23 |
dataset_name=config.dataset_name,
|
| 24 |
-
|
| 25 |
-
|
| 26 |
)
|
| 27 |
return data_ingestion_config
|
| 28 |
-
|
| 29 |
-
def get_data_preparation_config(self) -> DataPreparationConfig:
|
| 30 |
config = self.config.data_preparation
|
|
|
|
| 31 |
create_directories([config.root_dir])
|
| 32 |
-
|
| 33 |
data_preparation_config = DataPreparationConfig(
|
| 34 |
root_dir=config.root_dir,
|
| 35 |
-
|
| 36 |
-
|
| 37 |
)
|
| 38 |
return data_preparation_config
|
| 39 |
-
|
| 40 |
-
def
|
| 41 |
-
config = self.config.
|
| 42 |
-
data_prep_config = self.config.data_preparation
|
| 43 |
params = self.params
|
| 44 |
create_directories([config.root_dir])
|
| 45 |
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
)
|
| 60 |
-
return
|
|
|
|
| 1 |
from cnnClassifier.constants import *
|
| 2 |
from cnnClassifier.utils.common import read_yaml, create_directories
|
| 3 |
+
from cnnClassifier.entity.config_entity import (
|
| 4 |
+
DataIngestionConfig,
|
| 5 |
+
DataPreparationConfig,
|
| 6 |
+
MultiTaskModelTrainerConfig # <-- Import the new one
|
| 7 |
+
)
|
| 8 |
|
| 9 |
class ConfigurationManager:
|
| 10 |
def __init__(
|
|
|
|
| 19 |
|
| 20 |
def get_data_ingestion_config(self) -> DataIngestionConfig:
|
| 21 |
config = self.config.data_ingestion
|
|
|
|
| 22 |
create_directories([config.root_dir])
|
| 23 |
|
| 24 |
data_ingestion_config = DataIngestionConfig(
|
| 25 |
root_dir=config.root_dir,
|
| 26 |
dataset_name=config.dataset_name,
|
| 27 |
+
dataset_config=config.dataset_config,
|
| 28 |
+
local_data_dir=config.local_data_dir
|
| 29 |
)
|
| 30 |
return data_ingestion_config
|
| 31 |
+
|
| 32 |
+
def get_data_preparation_config(self) -> DataPreparationConfig: # <<< NEW METHOD
|
| 33 |
config = self.config.data_preparation
|
| 34 |
+
|
| 35 |
create_directories([config.root_dir])
|
| 36 |
+
|
| 37 |
data_preparation_config = DataPreparationConfig(
|
| 38 |
root_dir=config.root_dir,
|
| 39 |
+
raw_data_path=config.raw_data_path,
|
| 40 |
+
cleaned_data_path=config.cleaned_data_path
|
| 41 |
)
|
| 42 |
return data_preparation_config
|
| 43 |
+
|
| 44 |
+
def get_multi_task_model_trainer_config(self) -> MultiTaskModelTrainerConfig:
|
| 45 |
+
config = self.config.multi_task_model_trainer
|
|
|
|
| 46 |
params = self.params
|
| 47 |
create_directories([config.root_dir])
|
| 48 |
|
| 49 |
+
multi_task_model_trainer_config = MultiTaskModelTrainerConfig(
|
| 50 |
+
root_dir=Path(config.root_dir),
|
| 51 |
+
data_path=config.data_path,
|
| 52 |
+
trained_model_path=Path(config.trained_model_path),
|
| 53 |
+
model_name=config.model_name,
|
| 54 |
+
image_size=int(params.IMAGE_SIZE),
|
| 55 |
+
learning_rate=float(params.LEARNING_RATE),
|
| 56 |
+
batch_size=int(params.BATCH_SIZE),
|
| 57 |
+
num_train_epochs=int(params.NUM_TRAIN_EPOCHS),
|
| 58 |
+
weight_decay=float(params.WEIGHT_DECAY),
|
| 59 |
+
warmup_steps=int(params.WARMUP_STEPS),
|
| 60 |
+
test_split_size=float(params.TEST_SPLIT_SIZE),
|
| 61 |
+
random_state=int(params.RANDOM_STATE)
|
| 62 |
)
|
| 63 |
+
return multi_task_model_trainer_config
|
src/cnnClassifier/entity/config_entity.py
CHANGED
|
@@ -5,19 +5,21 @@ from pathlib import Path
|
|
| 5 |
class DataIngestionConfig:
|
| 6 |
root_dir: Path
|
| 7 |
dataset_name: str
|
| 8 |
-
|
| 9 |
-
|
| 10 |
|
| 11 |
@dataclass(frozen=True)
|
| 12 |
-
class DataPreparationConfig:
|
| 13 |
root_dir: Path
|
| 14 |
-
|
| 15 |
-
|
|
|
|
|
|
|
| 16 |
|
| 17 |
@dataclass(frozen=True)
|
| 18 |
-
class
|
| 19 |
root_dir: Path
|
| 20 |
-
data_path:
|
| 21 |
trained_model_path: Path
|
| 22 |
model_name: str
|
| 23 |
image_size: int
|
|
|
|
| 5 |
class DataIngestionConfig:
|
| 6 |
root_dir: Path
|
| 7 |
dataset_name: str
|
| 8 |
+
local_data_dir: Path
|
| 9 |
+
dataset_config: str
|
| 10 |
|
| 11 |
@dataclass(frozen=True)
|
| 12 |
+
class DataPreparationConfig: # <<< NEW DATACLASS
|
| 13 |
root_dir: Path
|
| 14 |
+
raw_data_path: Path
|
| 15 |
+
cleaned_data_path: Path
|
| 16 |
+
|
| 17 |
+
# ... (other configs are above)
|
| 18 |
|
| 19 |
@dataclass(frozen=True)
|
| 20 |
+
class MultiTaskModelTrainerConfig:
|
| 21 |
root_dir: Path
|
| 22 |
+
data_path: str
|
| 23 |
trained_model_path: Path
|
| 24 |
model_name: str
|
| 25 |
image_size: int
|
src/cnnClassifier/pipeline/prediction.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import numpy as np
|
| 4 |
+
from PIL import Image
|
| 5 |
+
from transformers import AutoImageProcessor
|
| 6 |
+
import cv2
|
| 7 |
+
from mtcnn import MTCNN
|
| 8 |
+
from pathlib import Path
|
| 9 |
+
import sys
|
| 10 |
+
import os
|
| 11 |
+
from torchvision.transforms import Compose, Resize, ToTensor, Normalize
|
| 12 |
+
from safetensors.torch import load_file as load_safetensors
|
| 13 |
+
from collections import OrderedDict
|
| 14 |
+
from scipy.spatial import distance as dist
|
| 15 |
+
|
| 16 |
+
try:
|
| 17 |
+
src_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
| 18 |
+
if src_path not in sys.path: sys.path.append(src_path)
|
| 19 |
+
from components.multi_task_model_trainer import MultiTaskEfficientNet
|
| 20 |
+
from utils.common import read_yaml
|
| 21 |
+
except ImportError as e:
|
| 22 |
+
print(f"Could not import custom modules: {e}.")
|
| 23 |
+
sys.exit(1)
|
| 24 |
+
|
| 25 |
+
class CentroidTracker:
|
| 26 |
+
def __init__(self, max_disappeared=20):
|
| 27 |
+
self.next_object_id = 0
|
| 28 |
+
self.objects = OrderedDict()
|
| 29 |
+
self.disappeared = OrderedDict()
|
| 30 |
+
self.max_disappeared = max_disappeared
|
| 31 |
+
|
| 32 |
+
def register(self, centroid, box):
|
| 33 |
+
self.objects[self.next_object_id] = {'centroid': centroid, 'box': box, 'labels': {}, 'ema_preds': {}}
|
| 34 |
+
self.disappeared[self.next_object_id] = 0
|
| 35 |
+
self.next_object_id += 1
|
| 36 |
+
|
| 37 |
+
def deregister(self, object_id):
|
| 38 |
+
del self.objects[object_id]
|
| 39 |
+
del self.disappeared[object_id]
|
| 40 |
+
|
| 41 |
+
def update(self, boxes):
|
| 42 |
+
if len(boxes) == 0:
|
| 43 |
+
for object_id in list(self.disappeared.keys()):
|
| 44 |
+
self.disappeared[object_id] += 1
|
| 45 |
+
if self.disappeared[object_id] > self.max_disappeared:
|
| 46 |
+
self.deregister(object_id)
|
| 47 |
+
return self.objects
|
| 48 |
+
|
| 49 |
+
input_centroids = np.array([(x + w // 2, y + h // 2) for (x, y, w, h) in boxes])
|
| 50 |
+
|
| 51 |
+
if len(self.objects) == 0:
|
| 52 |
+
for i in range(len(input_centroids)):
|
| 53 |
+
self.register(input_centroids[i], boxes[i])
|
| 54 |
+
else:
|
| 55 |
+
object_ids = list(self.objects.keys())
|
| 56 |
+
object_centroids = np.array([v['centroid'] for v in self.objects.values()])
|
| 57 |
+
D = dist.cdist(object_centroids, input_centroids)
|
| 58 |
+
rows = D.min(axis=1).argsort()
|
| 59 |
+
cols = D.argmin(axis=1)[rows]
|
| 60 |
+
used_rows, used_cols = set(), set()
|
| 61 |
+
for row, col in zip(rows, cols):
|
| 62 |
+
if row in used_rows or col in used_cols: continue
|
| 63 |
+
object_id = object_ids[row]
|
| 64 |
+
self.objects[object_id]['centroid'] = input_centroids[col]
|
| 65 |
+
self.objects[object_id]['box'] = boxes[col]
|
| 66 |
+
self.disappeared[object_id] = 0
|
| 67 |
+
used_rows.add(row)
|
| 68 |
+
used_cols.add(col)
|
| 69 |
+
|
| 70 |
+
unused_rows = set(range(D.shape[0])).difference(used_rows)
|
| 71 |
+
unused_cols = set(range(D.shape[1])).difference(used_cols)
|
| 72 |
+
|
| 73 |
+
if D.shape[0] >= D.shape[1]:
|
| 74 |
+
for row in unused_rows:
|
| 75 |
+
object_id = object_ids[row]
|
| 76 |
+
self.disappeared[object_id] += 1
|
| 77 |
+
if self.disappeared[object_id] > self.max_disappeared:
|
| 78 |
+
self.deregister(object_id)
|
| 79 |
+
else:
|
| 80 |
+
for col in unused_cols:
|
| 81 |
+
self.register(input_centroids[col], boxes[col])
|
| 82 |
+
return self.objects
|
| 83 |
+
|
| 84 |
+
class PredictionPipeline:
|
| 85 |
+
def __init__(self, model_path: str = "artifacts/multi_task_model_trainer/checkpoint-26873"):
|
| 86 |
+
self.device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 87 |
+
self.model_path = Path(model_path)
|
| 88 |
+
self.base_model_name = "google/efficientnet-b2"
|
| 89 |
+
params = read_yaml(Path("params.yaml"))
|
| 90 |
+
self.processor = AutoImageProcessor.from_pretrained(self.base_model_name)
|
| 91 |
+
self.transforms = Compose([Resize((params.IMAGE_SIZE, params.IMAGE_SIZE)), ToTensor(), Normalize(mean=self.processor.image_mean, std=self.processor.image_std)])
|
| 92 |
+
self.label_maps = self._load_label_maps()
|
| 93 |
+
self.model = self._load_model()
|
| 94 |
+
self.face_detector = MTCNN()
|
| 95 |
+
self.tracker = CentroidTracker()
|
| 96 |
+
print(f"--- Pipeline Initialized on device: {self.device} ---")
|
| 97 |
+
|
| 98 |
+
def _load_label_maps(self):
|
| 99 |
+
maps = {'age_id2label': {'0': '0-2', '1': '3-9', '2': '10-19', '3': '20-29', '4': '30-39', '5': '40-49', '6': '50-59', '7': '60-69', '8': 'more than 70'},
|
| 100 |
+
'gender_id2label': {'0': 'Male', '1': 'Female'}}
|
| 101 |
+
return maps
|
| 102 |
+
|
| 103 |
+
def _load_model(self):
|
| 104 |
+
num_age, num_gender, num_race = len(self.label_maps['age_id2label']), len(self.label_maps['gender_id2label']), 7
|
| 105 |
+
model = MultiTaskEfficientNet(self.base_model_name, num_age, num_gender, num_race)
|
| 106 |
+
weight_file = self.model_path / 'model.safetensors'
|
| 107 |
+
if not weight_file.exists(): weight_file = self.model_path / 'pytorch_model.bin'
|
| 108 |
+
state_dict = load_safetensors(weight_file, device="cpu") if weight_file.suffix == ".safetensors" else torch.load(weight_file, map_location="cpu")
|
| 109 |
+
model.load_state_dict(state_dict)
|
| 110 |
+
model.to(self.device)
|
| 111 |
+
model.eval()
|
| 112 |
+
return model
|
| 113 |
+
|
| 114 |
+
def _draw_predictions(self, image, box, labels):
|
| 115 |
+
x, y, w, h = [int(c) for c in box]
|
| 116 |
+
font, font_scale, font_thickness = cv2.FONT_HERSHEY_DUPLEX, 0.6, 1
|
| 117 |
+
text_color, bg_color = (255, 255, 255), (255, 75, 75)
|
| 118 |
+
text_lines = [f"Gender: {labels['gender']}", f"Age: {labels['age']}"]
|
| 119 |
+
max_width, line_height = 0, 25
|
| 120 |
+
for line in text_lines:
|
| 121 |
+
(w_text, _), _ = cv2.getTextSize(line, font, font_scale, font_thickness)
|
| 122 |
+
if w_text > max_width: max_width = w_text
|
| 123 |
+
total_height = len(text_lines) * line_height - 5
|
| 124 |
+
cv2.rectangle(image, (x, y), (x + w, y + h), bg_color, 2)
|
| 125 |
+
cv2.rectangle(image, (x-1, y - total_height), (x + max_width + 10, y), bg_color, -1)
|
| 126 |
+
for i, line in enumerate(text_lines):
|
| 127 |
+
y_text = y - total_height + (i * line_height) + 18
|
| 128 |
+
cv2.putText(image, line, (x + 5, y_text), font, font_scale, text_color, font_thickness, cv2.LINE_AA)
|
| 129 |
+
|
| 130 |
+
def _predict_for_box(self, frame, box):
|
| 131 |
+
x, y, w, h = [int(c) for c in box]
|
| 132 |
+
face_img = frame[max(0,y):min(frame.shape[0],y+h), max(0,x):min(frame.shape[1],x+w)]
|
| 133 |
+
if face_img.size == 0: return None
|
| 134 |
+
pixel_values = self.transforms(Image.fromarray(face_img)).unsqueeze(0).to(self.device)
|
| 135 |
+
with torch.no_grad(): outputs = self.model(pixel_values=pixel_values)
|
| 136 |
+
return outputs
|
| 137 |
+
|
| 138 |
+
def predict_image(self, image_array):
|
| 139 |
+
annotated_image, predictions = image_array.copy(), []
|
| 140 |
+
face_results = self.face_detector.detect_faces(image_array)
|
| 141 |
+
if not face_results: return annotated_image, predictions
|
| 142 |
+
for face in face_results:
|
| 143 |
+
if face['confidence'] < 0.9: continue
|
| 144 |
+
box = face['box']
|
| 145 |
+
outputs = self._predict_for_box(annotated_image, box)
|
| 146 |
+
if outputs:
|
| 147 |
+
age_label = self.label_maps['age_id2label'][str(outputs['age_logits'].argmax(1).item())]
|
| 148 |
+
gender_label = self.label_maps['gender_id2label'][str(outputs['gender_logits'].argmax(1).item())]
|
| 149 |
+
prediction_labels = {"age": age_label, "gender": gender_label}
|
| 150 |
+
predictions.append({**prediction_labels, 'box': box})
|
| 151 |
+
self._draw_predictions(annotated_image, box, prediction_labels)
|
| 152 |
+
return annotated_image, predictions
|
| 153 |
+
|
| 154 |
+
def process_video_stream(self, frame_generator):
|
| 155 |
+
self.tracker = CentroidTracker()
|
| 156 |
+
for frame in frame_generator:
|
| 157 |
+
face_results = self.face_detector.detect_faces(frame)
|
| 158 |
+
boxes = [tuple(face['box']) for face in face_results if face['confidence'] > 0.9]
|
| 159 |
+
tracked_objects = self.tracker.update(boxes)
|
| 160 |
+
|
| 161 |
+
for obj_id, data in tracked_objects.items():
|
| 162 |
+
# Predict only for new tracks or tracks that have just been re-found
|
| 163 |
+
if 'labels' not in data or self.tracker.disappeared[obj_id] == 0:
|
| 164 |
+
outputs = self._predict_for_box(frame, data['box'])
|
| 165 |
+
if outputs:
|
| 166 |
+
alpha = 0.3
|
| 167 |
+
current_probs = {
|
| 168 |
+
'age': outputs['age_logits'].softmax(1).cpu().numpy()[0],
|
| 169 |
+
'gender': outputs['gender_logits'].softmax(1).cpu().numpy()[0]
|
| 170 |
+
}
|
| 171 |
+
# Apply EMA smoothing
|
| 172 |
+
if not data.get('ema_preds'): data['ema_preds'] = current_probs
|
| 173 |
+
else:
|
| 174 |
+
for task in ['age', 'gender']:
|
| 175 |
+
data['ema_preds'][task] = alpha * current_probs[task] + (1 - alpha) * data['ema_preds'][task]
|
| 176 |
+
|
| 177 |
+
# Always update the label from the latest smoothed probabilities
|
| 178 |
+
if data.get('ema_preds'):
|
| 179 |
+
age_label = self.label_maps['age_id2label'][str(np.argmax(data['ema_preds']['age']))]
|
| 180 |
+
gender_label = self.label_maps['gender_id2label'][str(np.argmax(data['ema_preds']['gender']))]
|
| 181 |
+
data['labels'] = {"age": age_label, "gender": gender_label}
|
| 182 |
+
|
| 183 |
+
annotated_frame = frame.copy()
|
| 184 |
+
for obj_id, data in tracked_objects.items():
|
| 185 |
+
if 'labels' in data:
|
| 186 |
+
self._draw_predictions(annotated_frame, data['box'], data['labels'])
|
| 187 |
+
yield annotated_frame
|
| 188 |
+
|
| 189 |
+
def process_live_frame(self, frame):
|
| 190 |
+
annotated_frame, _ = self.predict_image(frame)
|
| 191 |
+
return annotated_frame
|
src/cnnClassifier/pipeline/stage_01_data_ingestion.py
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
|
|
|
|
|
| 1 |
from cnnClassifier.config.configuration import ConfigurationManager
|
| 2 |
from cnnClassifier.components.data_ingestion import DataIngestion
|
| 3 |
from cnnClassifier import logger
|
|
@@ -12,8 +14,7 @@ class DataIngestionTrainingPipeline:
|
|
| 12 |
config = ConfigurationManager()
|
| 13 |
data_ingestion_config = config.get_data_ingestion_config()
|
| 14 |
data_ingestion = DataIngestion(config=data_ingestion_config)
|
| 15 |
-
data_ingestion.
|
| 16 |
-
data_ingestion.extract_zip_file()
|
| 17 |
|
| 18 |
|
| 19 |
if __name__ == '__main__':
|
|
|
|
| 1 |
+
# src/cnnClassifier/pipeline/stage_01_data_ingestion.py
|
| 2 |
+
|
| 3 |
from cnnClassifier.config.configuration import ConfigurationManager
|
| 4 |
from cnnClassifier.components.data_ingestion import DataIngestion
|
| 5 |
from cnnClassifier import logger
|
|
|
|
| 14 |
config = ConfigurationManager()
|
| 15 |
data_ingestion_config = config.get_data_ingestion_config()
|
| 16 |
data_ingestion = DataIngestion(config=data_ingestion_config)
|
| 17 |
+
data_ingestion.download_dataset() # Call the new method
|
|
|
|
| 18 |
|
| 19 |
|
| 20 |
if __name__ == '__main__':
|
src/cnnClassifier/pipeline/stage_02_data_preparation.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# src/cnnClassifier/pipeline/stage_02_data_preparation.py
|
| 2 |
+
|
| 3 |
+
from cnnClassifier.config.configuration import ConfigurationManager
|
| 4 |
+
from cnnClassifier.components.data_preparation import DataPreparation
|
| 5 |
+
from cnnClassifier import logger
|
| 6 |
+
|
| 7 |
+
STAGE_NAME = "Data Preparation stage"
|
| 8 |
+
|
| 9 |
+
class DataPreparationTrainingPipeline:
|
| 10 |
+
def __init__(self):
|
| 11 |
+
pass
|
| 12 |
+
|
| 13 |
+
def main(self):
|
| 14 |
+
config = ConfigurationManager()
|
| 15 |
+
data_preparation_config = config.get_data_preparation_config()
|
| 16 |
+
data_preparation = DataPreparation(config=data_preparation_config)
|
| 17 |
+
data_preparation.create_cleaned_dataframe()
|
| 18 |
+
|
| 19 |
+
if __name__ == '__main__':
|
| 20 |
+
try:
|
| 21 |
+
logger.info(f">>>>>> stage {STAGE_NAME} started <<<<<<")
|
| 22 |
+
obj = DataPreparationTrainingPipeline()
|
| 23 |
+
obj.main()
|
| 24 |
+
logger.info(f">>>>>> stage {STAGE_NAME} completed <<<<<<\n\nx==========x")
|
| 25 |
+
except Exception as e:
|
| 26 |
+
logger.exception(e)
|
| 27 |
+
raise e
|
src/cnnClassifier/pipeline/stage_03_multi_task_model_training.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# src/cnnClassifier/pipeline/stage_03_multi_task_model_training.py
|
| 2 |
+
from cnnClassifier.config.configuration import ConfigurationManager
|
| 3 |
+
from cnnClassifier.components.multi_task_model_trainer import MultiTaskModelTrainer
|
| 4 |
+
from cnnClassifier import logger
|
| 5 |
+
|
| 6 |
+
STAGE_NAME = "Multi-Task Model Training stage"
|
| 7 |
+
|
| 8 |
+
class MultiTaskModelTrainingPipeline:
|
| 9 |
+
def __init__(self):
|
| 10 |
+
pass
|
| 11 |
+
|
| 12 |
+
def main(self):
|
| 13 |
+
config = ConfigurationManager()
|
| 14 |
+
multi_task_model_trainer_config = config.get_multi_task_model_trainer_config()
|
| 15 |
+
trainer = MultiTaskModelTrainer(config=multi_task_model_trainer_config)
|
| 16 |
+
trainer.train()
|
| 17 |
+
|
| 18 |
+
if __name__ == '__main__':
|
| 19 |
+
try:
|
| 20 |
+
logger.info(f">>>>>> stage {STAGE_NAME} started <<<<<<")
|
| 21 |
+
obj = MultiTaskModelTrainingPipeline()
|
| 22 |
+
obj.main()
|
| 23 |
+
logger.info(f">>>>>> stage {STAGE_NAME} completed <<<<<<\n\nx==========x")
|
| 24 |
+
except Exception as e:
|
| 25 |
+
logger.exception(e)
|
| 26 |
+
raise e
|