imgnetV1 / training.py
imghost11's picture
Upload 6 files
93f9bb1 verified
Raw
History Blame Contribute Delete
14.5 kB
# train_sw357_conv10_imgsign_a100.py
# SW357 + Conv10 (1SW+10Conv) — IMG Sign Score MSE loss
# TIDAK ada AMP/amplitude — murni sign pattern matching
# Loss: MSE (same→1.0, diff→0.0) via IMG Sign score
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
# ── PATH CONFIG ────────────────────────────────────
DATA_ROOT = "/content/data/casia-webface"
CKPT_ROOT = "/content/drive/MyDrive/dataset/checkpoints_sw357_conv10_imgsign"
# ── HYPERPARAMS ────────────────────────────────────
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
# ============================================================
# SW BLOCK
# ============================================================
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
# ============================================================
# IMGNET — SW357 + Conv10 (1SW+10Conv, 10.58MB)
# Resolusi: 112→56→56→28→28→28→14→14→7→7
# ============================================================
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())
# ============================================================
# IMG SIGN SCORE — murni sign pattern, tanpa amplitude
#
# soft_match = tanh(β × E1 × E2) → soft sign agreement per dim
# gate = sigmoid(50 × (soft_match_sum - threshold + 0.5))
# img_sign = mean(gate) over all windows
#
# Tidak ada rel_sim, tidak ada amplitude comparison
# ============================================================
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) # mean over windows
# ============================================================
# MSE LOSS — same→1.0, diff→0.0
# ============================================================
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()
# ============================================================
# DATASET (tanpa MTCNN)
# ============================================================
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)
# ============================================================
# TRAINING LOOP
# ============================================================
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!")
# ============================================================
# MAIN
# ============================================================
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()