ayushpfullstack commited on
Commit
4bdca69
·
verified ·
1 Parent(s): e276eca

Upload 5 files

Browse files
Files changed (5) hide show
  1. Dockerfile +23 -0
  2. checkpoints/best_age_model.pth +3 -0
  3. main.py +170 -0
  4. requirements.txt +7 -0
  5. server.py +89 -0
Dockerfile ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use a lightweight Python image
2
+ FROM python:3.9-slim
3
+
4
+ WORKDIR /code
5
+
6
+ # 1. Install CPU-only PyTorch (Critical for Free Tier to avoid 3GB bloat)
7
+ RUN pip install --no-cache-dir torch torchvision --index-url https://download.pytorch.org/whl/cpu
8
+
9
+ # 2. Copy requirements and install dependencies
10
+ COPY ./requirements.txt /code/requirements.txt
11
+ RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
12
+
13
+ # 3. Create checkpoints directory and set permissions
14
+ # We allow any user to write here to avoid permission errors on some clouds
15
+ RUN mkdir -p /code/checkpoints && chmod -R 777 /code/checkpoints
16
+
17
+ # 4. Copy your code and the TRAINED MODEL
18
+ COPY server.py /code/server.py
19
+ COPY checkpoints/ /code/checkpoints/
20
+
21
+ # 5. Start the Server
22
+ # Port 7860 is the default for Hugging Face Spaces
23
+ CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "7860"]
checkpoints/best_age_model.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6123e7653e7de692f0c0509b1429c5978709fc3ab0595059978e6c0d17c2cdc5
3
+ size 45050069
main.py ADDED
@@ -0,0 +1,170 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import glob
3
+ import torch
4
+ import torch.nn as nn
5
+ import torch.optim as optim
6
+ from torch.utils.data import Dataset, DataLoader, random_split
7
+ from torchvision import models, transforms as T
8
+ from PIL import Image
9
+ from tqdm import tqdm # Progress bar
10
+
11
+ # =================CONFIGURATION=================
12
+ # Update this path based on your screenshot
13
+ DATA_DIR = "dataset/UTKFace"
14
+ CHECKPOINT_DIR = "checkpoints"
15
+ BATCH_SIZE = 64
16
+ EPOCHS = 10
17
+ LEARNING_RATE = 1e-4
18
+ DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
19
+ # ===============================================
20
+
21
+ # We must use a tiny class for Data because PyTorch requires it
22
+ # But we keep it minimal (just loading logic)
23
+ class SimpleFaceDataset(Dataset):
24
+ def __init__(self, root_dir, transform=None):
25
+ self.transform = transform
26
+ # Find all files ending in .jpg (some in your screenshot have .chip.jpg)
27
+ self.files = glob.glob(os.path.join(root_dir, "*.jpg*"))
28
+ print(f"Dataset: Found {len(self.files)} images.")
29
+
30
+ def __len__(self):
31
+ return len(self.files)
32
+
33
+ def __getitem__(self, idx):
34
+ filepath = self.files[idx]
35
+ filename = os.path.basename(filepath)
36
+
37
+ # 1. Parse Age from Filename
38
+ # Example: "26_0_1_2017.jpg" -> "26" is the first part
39
+ try:
40
+ age = int(filename.split('_')[0])
41
+ except:
42
+ age = 25 # Default safe fallback
43
+
44
+ # 2. Load Image
45
+ image = Image.open(filepath).convert('RGB')
46
+
47
+ if self.transform:
48
+ image = self.transform(image)
49
+
50
+ # Return image and age as a Float (needed for regression)
51
+ return image, torch.tensor(age, dtype=torch.float32)
52
+
53
+ def get_model():
54
+ """Loads ResNet18 and changes output to 1 number (Age)."""
55
+ print("Loading ResNet18...")
56
+ model = models.resnet18(weights="DEFAULT")
57
+
58
+ # Change the final layer
59
+ # Old: Output 1000 classes (ImageNet)
60
+ # New: Output 1 number (Age)
61
+ num_features = model.fc.in_features
62
+ model.fc = nn.Sequential(
63
+ nn.Linear(num_features, 128),
64
+ nn.ReLU(),
65
+ nn.Dropout(0.2), # Helps prevent memorization
66
+ nn.Linear(128, 1)
67
+ )
68
+ return model.to(DEVICE)
69
+
70
+ def train_one_epoch(model, loader, criterion, optimizer):
71
+ model.train()
72
+ running_loss = 0.0
73
+ total_error = 0.0 # To track how many years we are off by
74
+ count = 0
75
+
76
+ loop = tqdm(loader, desc="Training", leave=False)
77
+ for images, ages in loop:
78
+ images, ages = images.to(DEVICE), ages.to(DEVICE)
79
+
80
+ # Forward
81
+ predictions = model(images).squeeze() # remove extra dim
82
+ loss = criterion(predictions, ages)
83
+
84
+ # Backward
85
+ optimizer.zero_grad()
86
+ loss.backward()
87
+ optimizer.step()
88
+
89
+ # Stats
90
+ running_loss += loss.item()
91
+ # Calculate Absolute Error (e.g., predicted 25, real 30 -> error 5)
92
+ total_error += torch.abs(predictions - ages).sum().item()
93
+ count += ages.size(0)
94
+
95
+ loop.set_description(f"Loss: {loss.item():.2f}")
96
+
97
+ avg_loss = running_loss / len(loader)
98
+ avg_mae = total_error / count # Mean Absolute Error
99
+ return avg_loss, avg_mae
100
+
101
+ def evaluate(model, loader, criterion):
102
+ model.eval()
103
+ running_loss = 0.0
104
+ total_error = 0.0
105
+ count = 0
106
+
107
+ with torch.no_grad():
108
+ for images, ages in loader:
109
+ images, ages = images.to(DEVICE), ages.to(DEVICE)
110
+
111
+ predictions = model(images).squeeze()
112
+ loss = criterion(predictions, ages)
113
+
114
+ running_loss += loss.item()
115
+ total_error += torch.abs(predictions - ages).sum().item()
116
+ count += ages.size(0)
117
+
118
+ avg_loss = running_loss / len(loader)
119
+ avg_mae = total_error / count
120
+ return avg_loss, avg_mae
121
+
122
+ # ================= MAIN SCRIPT =================
123
+ if __name__ == "__main__":
124
+ # 1. Setup Transforms (Resize to 200x200 standard)
125
+ transform = T.Compose([
126
+ T.Resize((200, 200)),
127
+ T.ToTensor(),
128
+ T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
129
+ ])
130
+
131
+ # 2. Setup Data
132
+ full_dataset = SimpleFaceDataset(DATA_DIR, transform=transform)
133
+
134
+ # 80/20 Split
135
+ train_size = int(0.8 * len(full_dataset))
136
+ val_size = len(full_dataset) - train_size
137
+ train_ds, val_ds = random_split(full_dataset, [train_size, val_size])
138
+
139
+ train_loader = DataLoader(train_ds, batch_size=BATCH_SIZE, shuffle=True, num_workers=4)
140
+ val_loader = DataLoader(val_ds, batch_size=BATCH_SIZE, num_workers=4)
141
+
142
+ # 3. Setup Model, Loss, Optimizer
143
+ model = get_model()
144
+ # MSELoss is standard for predicting numbers (Regression)
145
+ criterion = nn.MSELoss()
146
+ optimizer = optim.Adam(model.parameters(), lr=LEARNING_RATE)
147
+
148
+ os.makedirs(CHECKPOINT_DIR, exist_ok=True)
149
+
150
+ print(f"Starting training on {DEVICE}...")
151
+
152
+ best_mae = float('inf') # Lower is better
153
+
154
+ for epoch in range(EPOCHS):
155
+ print(f"\nEpoch {epoch+1}/{EPOCHS}")
156
+
157
+ train_loss, train_mae = train_one_epoch(model, train_loader, criterion, optimizer)
158
+ print(f"Train Loss: {train_loss:.2f} | Avg Error: {train_mae:.1f} years")
159
+
160
+ val_loss, val_mae = evaluate(model, val_loader, criterion)
161
+ print(f"Val Loss: {val_loss:.2f} | Avg Error: {val_mae:.1f} years")
162
+
163
+ # Save Best
164
+ if val_mae < best_mae:
165
+ best_mae = val_mae
166
+ save_path = os.path.join(CHECKPOINT_DIR, "best_age_model.pth")
167
+ torch.save(model.state_dict(), save_path)
168
+ print(f"🔥 Model saved! (Off by only {val_mae:.1f} years on avg)")
169
+
170
+ print("Done!")
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ python-multipart
4
+ torch
5
+ torchvision
6
+ pillow
7
+ tqdm
server.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
+ import os
3
+ import glob
4
+ import torch
5
+ import torch.nn as nn
6
+ from torchvision import models, transforms as T
7
+ from PIL import Image
8
+ from fastapi import FastAPI, UploadFile, File, HTTPException
9
+ import uvicorn
10
+
11
+ app = FastAPI()
12
+
13
+ # =================CONFIGURATION=================
14
+ CHECKPOINT_PATH = "checkpoints/best_age_model.pth"
15
+ DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
16
+ # ===============================================
17
+
18
+ # 1. Define the Model Architecture (Must match training EXACTLY)
19
+ def get_model():
20
+ # Load standard ResNet18 structure
21
+ model = models.resnet18(weights=None)
22
+
23
+ # Reconstruct the final layer exactly as we defined in training
24
+ num_features = model.fc.in_features
25
+ model.fc = nn.Sequential(
26
+ nn.Linear(num_features, 128),
27
+ nn.ReLU(),
28
+ nn.Dropout(0.2),
29
+ nn.Linear(128, 1)
30
+ )
31
+ return model
32
+
33
+ # 2. Load Weights
34
+ print(f"Initializing model on {DEVICE}...")
35
+ model = get_model()
36
+
37
+ if os.path.exists(CHECKPOINT_PATH):
38
+ print(f"Loading weights from {CHECKPOINT_PATH}...")
39
+ # Load the state dictionary
40
+ state_dict = torch.load(CHECKPOINT_PATH, map_location=DEVICE)
41
+ model.load_state_dict(state_dict)
42
+ model.to(DEVICE)
43
+ model.eval() # Important: Turn off Dropout for inference
44
+ print("✅ Model loaded successfully!")
45
+ else:
46
+ print(f"⚠️ Warning: Checkpoint not found at {CHECKPOINT_PATH}")
47
+ print("Inference will use random weights (garbage output).")
48
+
49
+ # 3. Define Transforms (Must match training)
50
+ transform = T.Compose([
51
+ T.Resize((200, 200)),
52
+ T.ToTensor(),
53
+ T.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
54
+ ])
55
+
56
+ @app.get("/")
57
+ def home():
58
+ return {"status": "Age Predictor API is running"}
59
+
60
+ @app.post("/predict")
61
+ async def predict_age(file: UploadFile = File(...)):
62
+ """
63
+ Accepts an image file and returns predicted age.
64
+ """
65
+ try:
66
+ # 1. Read and Process Image
67
+ content = await file.read()
68
+ image = Image.open(io.BytesIO(content)).convert("RGB")
69
+
70
+ # 2. Transform
71
+ input_tensor = transform(image).unsqueeze(0).to(DEVICE)
72
+
73
+ # 3. Inference
74
+ with torch.no_grad():
75
+ prediction = model(input_tensor).squeeze()
76
+ predicted_age = prediction.item()
77
+
78
+ # 4. Return Result
79
+ return {
80
+ "filename": file.filename,
81
+ "predicted_age": round(predicted_age, 1) # Round to 1 decimal
82
+ }
83
+
84
+ except Exception as e:
85
+ raise HTTPException(status_code=500, detail=str(e))
86
+
87
+ if __name__ == "__main__":
88
+ # Host 0.0.0.0 is needed for cloud environments/Docker
89
+ uvicorn.run(app, host="0.0.0.0", port=8000)