vaisagan commited on
Commit
659083c
Β·
verified Β·
1 Parent(s): b4bda84

Upload src/data/dataset.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. src/data/dataset.py +182 -0
src/data/dataset.py ADDED
@@ -0,0 +1,182 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ UTKFace PyTorch Dataset.
3
+
4
+ Filename format: [age]_[gender]_[race]_[datetime].jpg
5
+ age : 0-116
6
+ gender : 0=Male 1=Female
7
+ race : 0=White 1=Black 2=Asian 3=Indian 4=Others
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import os
13
+ import random
14
+ from pathlib import Path
15
+ from typing import List, Optional, Tuple, Union
16
+
17
+ import numpy as np
18
+ import torch
19
+ from PIL import Image
20
+ from torch.utils.data import Dataset
21
+ from torchvision import transforms
22
+
23
+
24
+ # ── augmentation presets ───────────────────────────────────────────────────
25
+
26
+ def train_transforms(img_size: int = 224) -> transforms.Compose:
27
+ return transforms.Compose([
28
+ transforms.Resize((img_size + 20, img_size + 20)),
29
+ transforms.RandomCrop(img_size),
30
+ transforms.RandomHorizontalFlip(),
31
+ transforms.ColorJitter(brightness=0.3, contrast=0.3, saturation=0.2, hue=0.05),
32
+ transforms.RandomRotation(10),
33
+ transforms.ToTensor(),
34
+ transforms.Normalize(mean=[0.485, 0.456, 0.406],
35
+ std=[0.229, 0.224, 0.225]),
36
+ ])
37
+
38
+
39
+ def eval_transforms(img_size: int = 224) -> transforms.Compose:
40
+ return transforms.Compose([
41
+ transforms.Resize((img_size, img_size)),
42
+ transforms.ToTensor(),
43
+ transforms.Normalize(mean=[0.485, 0.456, 0.406],
44
+ std=[0.229, 0.224, 0.225]),
45
+ ])
46
+
47
+
48
+ # ── dataset class ──────────────────────────────────────────────────────────
49
+
50
+ class UTKFaceDataset(Dataset):
51
+ """
52
+ Returns (image_tensor, gender_label, age_normalised)
53
+ gender_label : int 0=Male 1=Female
54
+ age_normalised : float in [0, 1] (age / MAX_AGE)
55
+ """
56
+
57
+ MAX_AGE = 90.0
58
+
59
+ def __init__(
60
+ self,
61
+ root_dir: "Union[str, Path]",
62
+ split: str = "train",
63
+ target_races: Optional[List[int]] = None,
64
+ min_age: int = 1,
65
+ max_age: int = 90,
66
+ train_ratio: float = 0.80,
67
+ val_ratio: float = 0.10,
68
+ img_size: int = 224,
69
+ seed: int = 42,
70
+ ) -> None:
71
+ self.root_dir = Path(root_dir)
72
+ self.split = split
73
+ self.target_races = set(target_races) if target_races else None
74
+ self.min_age = min_age
75
+ self.max_age = max_age
76
+ self.img_size = img_size
77
+
78
+ self.transform = train_transforms(img_size) if split == "train" else eval_transforms(img_size)
79
+
80
+ samples = self._scan()
81
+ samples = self._filter(samples)
82
+
83
+ random.seed(seed)
84
+ random.shuffle(samples)
85
+
86
+ n = len(samples)
87
+ n_train = int(n * train_ratio)
88
+ n_val = int(n * val_ratio)
89
+
90
+ if split == "train":
91
+ self.samples = samples[:n_train]
92
+ elif split == "val":
93
+ self.samples = samples[n_train: n_train + n_val]
94
+ else: # test
95
+ self.samples = samples[n_train + n_val:]
96
+
97
+ # ── private helpers ────────────────────────────────────────────────────
98
+
99
+ def _scan(self) -> List[Tuple[Path, int, int, int]]:
100
+ """Return list of (path, age, gender, race)."""
101
+ records: List[Tuple[Path, int, int, int]] = []
102
+ for p in self.root_dir.glob("*.jpg"):
103
+ parts = p.stem.split("_")
104
+ if len(parts) < 3:
105
+ continue
106
+ try:
107
+ age = int(parts[0])
108
+ gender = int(parts[1])
109
+ race = int(parts[2])
110
+ except ValueError:
111
+ continue
112
+ records.append((p, age, gender, race))
113
+ return records
114
+
115
+ def _filter(self, records: List[Tuple[Path, int, int, int]]) -> List[Tuple[Path, int, int, int]]:
116
+ out = []
117
+ for p, age, gender, race in records:
118
+ if age < self.min_age or age > self.max_age:
119
+ continue
120
+ if gender not in (0, 1):
121
+ continue
122
+ if self.target_races and race not in self.target_races:
123
+ continue
124
+ out.append((p, age, gender, race))
125
+ return out
126
+
127
+ # ── public API ─────────────────────────────────────────────────────────
128
+
129
+ def __len__(self) -> int:
130
+ return len(self.samples)
131
+
132
+ def __getitem__(self, idx: int) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
133
+ path, age, gender, _ = self.samples[idx]
134
+ img = Image.open(path).convert("RGB")
135
+ img = self.transform(img)
136
+ gender_t = torch.tensor(gender, dtype=torch.long)
137
+ age_t = torch.tensor(age / self.MAX_AGE, dtype=torch.float32)
138
+ return img, gender_t, age_t
139
+
140
+ def class_weights(self) -> torch.Tensor:
141
+ """Return balanced class weights for gender (0=Male, 1=Female)."""
142
+ counts = [0, 0]
143
+ for _, _, gender, _ in self.samples:
144
+ counts[gender] += 1
145
+ total = sum(counts)
146
+ weights = torch.tensor([total / (2 * c) for c in counts], dtype=torch.float32)
147
+ return weights
148
+
149
+ @staticmethod
150
+ def denorm_age(age_norm: float, max_age: float = 90.0) -> int:
151
+ return round(float(age_norm) * max_age)
152
+
153
+
154
+ def build_dataloaders(cfg) -> dict:
155
+ """Build train / val / test DataLoaders from config."""
156
+ from torch.utils.data import DataLoader
157
+
158
+ common = dict(
159
+ root_dir = cfg.UTKFACE_DIR,
160
+ target_races = cfg.TARGET_RACES,
161
+ min_age = cfg.MIN_AGE,
162
+ max_age = cfg.MAX_AGE,
163
+ train_ratio = cfg.TRAIN_RATIO,
164
+ val_ratio = cfg.VAL_RATIO,
165
+ img_size = cfg.IMG_SIZE,
166
+ seed = cfg.SEED,
167
+ )
168
+
169
+ loaders = {}
170
+ for split in ("train", "val", "test"):
171
+ ds = UTKFaceDataset(split=split, **common)
172
+ loaders[split] = DataLoader(
173
+ ds,
174
+ batch_size = cfg.BATCH_SIZE,
175
+ shuffle = (split == "train"),
176
+ num_workers = cfg.NUM_WORKERS,
177
+ pin_memory = True,
178
+ drop_last = (split == "train"),
179
+ )
180
+ print(f"[dataset] {split:5s}: {len(ds):,} samples")
181
+
182
+ return loaders