| |
| |
| |
| |
|
|
| import os, random |
| import numpy as np |
| import torch |
| import torch.nn as nn |
| import torch.nn.functional as F |
| from torch.utils.data import DataLoader, Dataset, Subset |
| from PIL import Image |
| import torchvision.transforms as T |
|
|
| |
| DATA_ROOT = "/content/data/casia-webface" |
| CKPT_ROOT = "/content/drive/MyDrive/dataset/checkpoints_sw357_conv10_imgsign" |
|
|
| |
| BATCH_SIZE = 16 |
| LR = 1e-4 |
| MAX_PAIRS = 300 |
| NUM_WORKERS = 8 |
| WINDOW_SIZE = 11 |
| THRESHOLD = 8 |
| EMB_DIM = 1024 |
| NUM_EPOCHS = 50 |
| WARMUP_EPOCHS = 5 |
|
|
|
|
| |
| |
| |
| class SWBlock(nn.Module): |
| def __init__(self, in_ch, out_ch, window_sizes=[3, 5, 7]): |
| super().__init__() |
| self.window_sizes = window_sizes |
| n_diff = sum(w * w - 1 for w in window_sizes) |
| n_input = n_diff * in_ch |
| self.fc = nn.Sequential( |
| nn.Linear(n_input, 64), |
| nn.ReLU(inplace=True), |
| nn.Linear(64, out_ch), |
| ) |
|
|
| def forward(self, x): |
| B, C, H, W = x.shape |
| diffs = [] |
| for ws in self.window_sizes: |
| pad = ws // 2 |
| x_pad = F.pad(x, [pad, pad, pad, pad], mode='reflect') |
| patches = x_pad.unfold(2, ws, 1).unfold(3, ws, 1) |
| center = x.unsqueeze(-1).unsqueeze(-1) |
| diff = center - patches |
| mid = ws // 2 |
| mask = torch.ones(ws, ws, dtype=torch.bool, device=x.device) |
| mask[mid, mid] = False |
| diff = diff[:, :, :, :, mask] |
| diffs.append(diff) |
| diffs = torch.cat(diffs, dim=-1) |
| B, C, H, W, N = diffs.shape |
| diffs = diffs.permute(0, 2, 3, 1, 4).reshape(B * H * W, C * N) |
| out = self.fc(diffs) |
| out = out.reshape(B, H, W, -1).permute(0, 3, 1, 2) |
| return out |
|
|
|
|
| |
| |
| |
| |
| class IMGNet(nn.Module): |
| def __init__(self, emb_dim=EMB_DIM): |
| super().__init__() |
| self.sw1 = SWBlock(3, 32, window_sizes=[3, 5, 7]) |
| self.bn1 = nn.BatchNorm2d(32) |
| self.conv2 = nn.Conv2d(32, 64, 3, stride=1, padding=1, bias=False); self.bn2 = nn.BatchNorm2d(64) |
| self.conv3 = nn.Conv2d(64, 64, 3, stride=2, padding=1, bias=False); self.bn3 = nn.BatchNorm2d(64) |
| self.conv4 = nn.Conv2d(64, 128, 3, stride=1, padding=1, bias=False); self.bn4 = nn.BatchNorm2d(128) |
| self.conv5 = nn.Conv2d(128, 128, 3, stride=1, padding=1, bias=False); self.bn5 = nn.BatchNorm2d(128) |
| self.conv6 = nn.Conv2d(128, 128, 3, stride=2, padding=1, bias=False); self.bn6 = nn.BatchNorm2d(128) |
| self.conv7 = nn.Conv2d(128, 256, 3, stride=1, padding=1, bias=False); self.bn7 = nn.BatchNorm2d(256) |
| self.conv8 = nn.Conv2d(256, 256, 3, stride=1, padding=1, bias=False); self.bn8 = nn.BatchNorm2d(256) |
| self.conv9 = nn.Conv2d(256, 256, 3, stride=2, padding=1, bias=False); self.bn9 = nn.BatchNorm2d(256) |
| self.conv10 = nn.Conv2d(256, 256, 3, stride=1, padding=1, bias=False); self.bn10 = nn.BatchNorm2d(256) |
| self.gap = nn.AdaptiveAvgPool2d(1) |
| self.fc = nn.Linear(256, emb_dim) |
| self.bn = nn.BatchNorm1d(emb_dim) |
|
|
| def forward(self, x): |
| x = F.relu(self.bn1(self.sw1(x))) |
| x = F.relu(self.bn2(self.conv2(x))) |
| x = F.relu(self.bn3(self.conv3(x))) |
| x = F.relu(self.bn4(self.conv4(x))) |
| x = F.relu(self.bn5(self.conv5(x))) |
| x = F.relu(self.bn6(self.conv6(x))) |
| x = F.relu(self.bn7(self.conv7(x))) |
| x = F.relu(self.bn8(self.conv8(x))) |
| x = F.relu(self.bn9(self.conv9(x))) |
| x = F.relu(self.bn10(self.conv10(x))) |
| x = self.gap(x).view(x.size(0), -1) |
| return self.bn(self.fc(x)) |
|
|
| def n_params(self): |
| return sum(p.numel() for p in self.parameters()) |
|
|
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| def img_sign_score(E1, E2, beta=10.0): |
| kernel = torch.ones(1, 1, WINDOW_SIZE, device=E1.device) |
| agreement = (torch.tanh(beta * E1 * E2) + 1) / 2 |
| soft_match = F.conv1d(agreement.unsqueeze(1), kernel, stride=1).squeeze(1) |
| gate = torch.sigmoid(50.0 * (soft_match - THRESHOLD + 0.5)) |
| return gate.mean(dim=1) |
|
|
|
|
| |
| |
| |
| def contrastive_loss(E1_s, E2_s, E1_d, E2_d): |
| device = E1_s.device if E1_s.shape[0] > 0 else E1_d.device |
| ls = ld = torch.tensor(0.0, device=device) |
| if E1_s.shape[0] > 0: |
| ls = ((1.0 - img_sign_score(E1_s, E2_s)) ** 2).mean() |
| if E1_d.shape[0] > 0: |
| ld = (img_sign_score(E1_d, E2_d) ** 2).mean() |
| return ls + ld, ls.item(), ld.item() |
|
|
|
|
| |
| |
| |
| class PairDataset(Dataset): |
| def __init__(self, root_dir, img_size=112, max_pairs_per_identity=300, augment=False): |
| self.img_size = img_size |
| self.augment = augment |
| print(f"Loading dataset from: {root_dir}") |
| identities = [d for d in os.listdir(root_dir) |
| if os.path.isdir(os.path.join(root_dir, d))] |
| self.identity_images = {} |
| for idx, identity in enumerate(identities): |
| path = os.path.join(root_dir, identity) |
| images = [os.path.join(path, f) for f in os.listdir(path) |
| if f.lower().endswith(('.jpg', '.png', '.jpeg'))] |
| if len(images) >= 2: |
| self.identity_images[identity] = images |
| if (idx + 1) % 1000 == 0: |
| print(f" scanning... {idx+1}/{len(identities)}") |
| self.identity_list = list(self.identity_images.keys()) |
| self.pos_pairs = [] |
| for identity, images in self.identity_images.items(): |
| n = min(max_pairs_per_identity, len(images)) |
| for _ in range(n): |
| i, j = random.sample(range(len(images)), 2) |
| self.pos_pairs.append((images[i], images[j])) |
| self.n_neg = len(self.pos_pairs) |
| print(f"Identities : {len(self.identity_list)}") |
| print(f"Pos pairs : {len(self.pos_pairs)}") |
| print(f"Total : {len(self)}") |
|
|
| def __len__(self): |
| return len(self.pos_pairs) + self.n_neg |
|
|
| def _load(self, path): |
| img = Image.open(path).convert('RGB') |
| img = img.resize((self.img_size, self.img_size), Image.BILINEAR) |
| arr = np.array(img, dtype=np.float32) / 255.0 |
| t = torch.from_numpy(arr).permute(2, 0, 1) |
| if self.augment: |
| aug = T.Compose([ |
| T.RandomHorizontalFlip(p=0.5), |
| T.ColorJitter(brightness=0.3, contrast=0.3, saturation=0.2), |
| T.RandomRotation(degrees=10), |
| T.RandomGrayscale(p=0.1), |
| T.GaussianBlur(kernel_size=3, sigma=(0.1, 1.0)), |
| T.RandomErasing(p=0.2, scale=(0.02, 0.1)), |
| ]) |
| t = aug(t) |
| return t |
|
|
| def _random_negative(self): |
| id1, id2 = random.sample(self.identity_list, 2) |
| return random.choice(self.identity_images[id1]), random.choice(self.identity_images[id2]) |
|
|
| def __getitem__(self, idx): |
| if idx < len(self.pos_pairs): |
| p1, p2 = self.pos_pairs[idx] |
| return self._load(p1), self._load(p2), torch.tensor(1) |
| p1, p2 = self._random_negative() |
| return self._load(p1), self._load(p2), torch.tensor(0) |
|
|
|
|
| |
| |
| |
| def train(model, train_loader, val_loader, device, name): |
| ckpt_dir = os.path.join(CKPT_ROOT, name) |
| os.makedirs(ckpt_dir, exist_ok=True) |
| resume_path = os.path.join(ckpt_dir, "last_checkpoint.pth") |
|
|
| optimizer = torch.optim.Adam(model.parameters(), lr=LR, weight_decay=1e-5) |
| warmup_scheduler = torch.optim.lr_scheduler.LambdaLR( |
| optimizer, lambda ep: (ep + 1) / WARMUP_EPOCHS if ep < WARMUP_EPOCHS else 1.0) |
| cosine_scheduler = torch.optim.lr_scheduler.CosineAnnealingLR( |
| optimizer, T_max=NUM_EPOCHS - WARMUP_EPOCHS, eta_min=1e-6) |
|
|
| start_epoch = 0 |
| best_val = float('inf') |
| if os.path.exists(resume_path): |
| try: |
| ckpt = torch.load(resume_path, map_location=device, weights_only=False) |
| model.load_state_dict(ckpt['model']) |
| optimizer.load_state_dict(ckpt['optimizer']) |
| start_epoch = ckpt['epoch'] + 1 |
| best_val = ckpt.get('best_val', float('inf')) |
| print(f" [{name}] Resumed from epoch {start_epoch}") |
| except RuntimeError: |
| print(f" [{name}] Checkpoint tidak kompatibel, training dari awal") |
| else: |
| print(f" [{name}] Training dari awal...") |
|
|
| for epoch in range(start_epoch, NUM_EPOCHS): |
| model.train() |
| t_loss = t_s = t_d = 0.0; n = 0 |
|
|
| for batch_idx, (img1, img2, labels) in enumerate(train_loader): |
| img1=img1.to(device); img2=img2.to(device); labels=labels.to(device) |
| optimizer.zero_grad() |
| E1, E2 = model(img1), model(img2) |
| sm, dm = labels == 1, labels == 0 |
| loss, ls, ld = contrastive_loss(E1[sm], E2[sm], E1[dm], E2[dm]) |
| if loss.item() > 0: |
| loss.backward() |
| nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0) |
| optimizer.step() |
| t_loss += loss.item(); t_s += ls; t_d += ld; n += 1 |
|
|
| if batch_idx == 0: |
| print(f" [{name}] Epoch {epoch+1} dimulai...") |
| if (batch_idx + 1) % 100 == 0: |
| with torch.no_grad(): |
| s_mean = img_sign_score(E1[sm], E2[sm]).mean().item() if sm.sum() > 0 else 0.0 |
| d_mean = img_sign_score(E1[dm], E2[dm]).mean().item() if dm.sum() > 0 else 0.0 |
| print(f" [{name}] batch {batch_idx+1}/{len(train_loader)} " |
| f"loss={loss.item():.4f} | sign same={s_mean:.3f} diff={d_mean:.3f}") |
|
|
| if epoch < WARMUP_EPOCHS: |
| warmup_scheduler.step(); current_lr = warmup_scheduler.get_last_lr()[0] |
| else: |
| cosine_scheduler.step(); current_lr = cosine_scheduler.get_last_lr()[0] |
|
|
| model.eval() |
| v_loss = 0.0; nv = 0 |
| with torch.no_grad(): |
| for img1, img2, labels in val_loader: |
| img1=img1.to(device); img2=img2.to(device); labels=labels.to(device) |
| E1,E2=model(img1),model(img2); sm=labels==1; dm=labels==0 |
| loss,_,_=contrastive_loss(E1[sm],E2[sm],E1[dm],E2[dm]) |
| v_loss+=loss.item(); nv+=1 |
| avg_v = v_loss / max(nv, 1) |
|
|
| print(f" [{name}] Epoch {epoch+1:02d}/{NUM_EPOCHS} | " |
| f"Train {t_loss/n:.4f} (same={t_s/n:.4f} diff={t_d/n:.4f}) | " |
| f"Val {avg_v:.4f} | LR {current_lr:.6f}") |
|
|
| if avg_v < best_val: |
| best_val = avg_v |
| best_path = os.path.join(ckpt_dir, f"best_model_epoch{epoch+1}.pth") |
| torch.save(model.state_dict(), best_path) |
| print(f" [{name}] -> best saved: best_model_epoch{epoch+1}.pth (val={best_val:.4f})") |
|
|
| torch.save({ |
| 'epoch': epoch, 'model': model.state_dict(), |
| 'optimizer': optimizer.state_dict(), |
| 'val_loss': avg_v, 'best_val': best_val, |
| }, resume_path) |
|
|
| torch.save(model.state_dict(), os.path.join(ckpt_dir, "final_model.pth")) |
| print(f" [{name}] Training selesai!") |
|
|
|
|
| |
| |
| |
| def main(): |
| device = torch.device("cuda" if torch.cuda.is_available() else "cpu") |
| print(f"Device : {device}") |
| if torch.cuda.is_available(): |
| print(f"GPU : {torch.cuda.get_device_name(0)}") |
|
|
| os.makedirs(CKPT_ROOT, exist_ok=True) |
|
|
| dev_str = 'cuda' if torch.cuda.is_available() else 'cpu' |
| print("\nLoading dataset...") |
| train_dataset = PairDataset(DATA_ROOT, max_pairs_per_identity=MAX_PAIRS, augment=True) |
| val_dataset = PairDataset(DATA_ROOT, max_pairs_per_identity=MAX_PAIRS, augment=False) |
| total = len(train_dataset) |
| indices = list(range(total)) |
| random.seed(42); random.shuffle(indices) |
| val_size = int(total * 0.1) |
| val_idx, train_idx = indices[:val_size], indices[val_size:] |
| pin = (device.type == "cuda") |
| train_loader = DataLoader(Subset(train_dataset, train_idx), batch_size=BATCH_SIZE, |
| shuffle=True, num_workers=NUM_WORKERS, pin_memory=pin, |
| drop_last=True) |
| val_loader = DataLoader(Subset(val_dataset, val_idx), batch_size=BATCH_SIZE, |
| shuffle=False, num_workers=NUM_WORKERS, pin_memory=pin) |
| print(f"Train: {len(train_idx)} | Val: {len(val_idx)} | Batch/epoch: {len(train_idx)//BATCH_SIZE}") |
|
|
| name = "SW357_conv10_imgsign" |
| model = IMGNet(emb_dim=EMB_DIM).to(device) |
| print(f"Parameters : {model.n_params():,} (~{model.n_params()*4/1024/1024:.2f} MB)") |
| print(f"Loss : IMG Sign MSE (same→1.0, diff→0.0) — tanpa amplitude") |
| print(f"Checkpoint : {CKPT_ROOT}") |
|
|
| train(model, train_loader, val_loader, device, name) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|