| import csv
|
| import glob
|
| import json
|
| import os
|
| import sys
|
| import time
|
| import warnings
|
|
|
| warnings.filterwarnings("ignore", category=UserWarning, module="PIL.TiffImagePlugin")
|
| import logging
|
|
|
| logging.getLogger("PIL").setLevel(logging.ERROR)
|
| from PIL import ImageFile
|
|
|
| ImageFile.LOAD_TRUNCATED_IMAGES = True
|
|
|
| import numpy as np
|
| import timm
|
| import torch
|
| import torch.nn as nn
|
| import torch.nn.functional as F
|
| import torch.optim as optim
|
| from datasets import load_dataset
|
| from safetensors.torch import save_file
|
| from torch.utils.data import DataLoader
|
|
|
|
|
| MODEL_NAME = "convnextv2_nano.fcmae_ft_in1k"
|
| BATCH_SIZE = 64
|
| NUM_WORKERS = 8
|
| DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
|
|
| EPOCHS = 30
|
| LEARNING_RATE = 5e-4
|
| SAVE_DIR = "./checkpoints_1bit"
|
| LOG_DIR = "./logs"
|
|
|
|
|
| TEMPERATURE = 4.0
|
| ALPHA = 0.9
|
|
|
|
|
|
|
|
|
|
|
| class BinarySTE(torch.autograd.Function):
|
| @staticmethod
|
| def forward(ctx, weight):
|
| ctx.save_for_backward(weight)
|
| return torch.where(weight == 0, torch.ones_like(weight), torch.sign(weight))
|
|
|
| @staticmethod
|
| def backward(ctx, grad_output):
|
| (weight,) = ctx.saved_tensors
|
| grad_input = grad_output.clone()
|
| grad_input[weight.abs() > 1.0] = 0
|
| return grad_input
|
|
|
|
|
| def binarize_weight(weight):
|
| if weight.dim() == 4:
|
| scale = weight.abs().mean(dim=(1, 2, 3), keepdim=True)
|
| elif weight.dim() == 2:
|
| scale = weight.abs().mean(dim=1, keepdim=True)
|
| else:
|
| scale = weight.abs().mean()
|
| binary_w = BinarySTE.apply(weight)
|
| return binary_w * scale
|
|
|
|
|
| class BinaryConv2d(nn.Conv2d):
|
| def forward(self, input):
|
| bw = binarize_weight(self.weight).to(input.dtype)
|
| bias = self.bias.to(input.dtype) if self.bias is not None else None
|
| return F.conv2d(input, bw, bias, self.stride, self.padding, self.dilation, self.groups)
|
|
|
|
|
| class BinaryLinear(nn.Linear):
|
| def forward(self, input):
|
| bw = binarize_weight(self.weight).to(input.dtype)
|
| bias = self.bias.to(input.dtype) if self.bias is not None else None
|
| return F.linear(input, bw, bias)
|
|
|
|
|
| def replace_layers_with_1bit(model):
|
| for name, module in model.named_children():
|
| if isinstance(module, nn.Conv2d) and "stem" not in name and "head" not in name:
|
| bin_conv = BinaryConv2d(
|
| module.in_channels,
|
| module.out_channels,
|
| module.kernel_size,
|
| module.stride,
|
| module.padding,
|
| module.dilation,
|
| module.groups,
|
| module.bias is not None,
|
| )
|
| bin_conv.weight.data.copy_(module.weight.data)
|
| if module.bias is not None:
|
| bin_conv.bias.data.copy_(module.bias.data)
|
| setattr(model, name, bin_conv)
|
| elif isinstance(module, nn.Linear) and "head" not in name and "classifier" not in name:
|
| bin_linear = BinaryLinear(
|
| module.in_features, module.out_features, module.bias is not None
|
| )
|
| bin_linear.weight.data.copy_(module.weight.data)
|
| if module.bias is not None:
|
| bin_linear.bias.data.copy_(module.bias.data)
|
| setattr(model, name, bin_linear)
|
| else:
|
| replace_layers_with_1bit(module)
|
|
|
|
|
|
|
| def kd_loss_fn(student_logits, teacher_logits, labels, T=TEMPERATURE, alpha=ALPHA):
|
| hard_loss = F.cross_entropy(student_logits, labels)
|
| soft_targets = F.softmax(teacher_logits / T, dim=1)
|
| student_log_probs = F.log_softmax(student_logits / T, dim=1)
|
| soft_loss = F.kl_div(student_log_probs, soft_targets, reduction="batchmean") * (T * T)
|
| return alpha * soft_loss + (1.0 - alpha) * hard_loss
|
|
|
|
|
| def export_huggingface_1bit(model, save_dir="./models/hf_1bit_model"):
|
| print("\n [1-Bit] κ·Ήνμ λΉνΈ ν¨νΉ(Bit-packing) μΆμΆμ μμν©λλ€...")
|
| os.makedirs(save_dir, exist_ok=True)
|
| export_state_dict = {}
|
|
|
| for name, module in model.named_modules():
|
| if isinstance(module, (torch.nn.Conv2d, torch.nn.Linear)):
|
| if hasattr(module, "weight") and module.weight is not None:
|
|
|
| weight = module.weight.data
|
|
|
|
|
| if weight.dim() == 4:
|
| scale = weight.abs().mean(dim=(1, 2, 3), keepdim=True)
|
| elif weight.dim() == 2:
|
| scale = weight.abs().mean(dim=1, keepdim=True)
|
| else:
|
| scale = weight.abs().mean()
|
|
|
| export_state_dict[f"{name}.scale"] = scale.to(torch.float16)
|
|
|
|
|
| binary_mask = (weight > 0).cpu().numpy()
|
|
|
|
|
| packed_bits = np.packbits(binary_mask)
|
| export_state_dict[f"{name}.weight_packed"] = torch.from_numpy(packed_bits)
|
|
|
| if hasattr(module, "bias") and module.bias is not None:
|
| export_state_dict[f"{name}.bias"] = module.bias.data.to(torch.float16)
|
|
|
|
|
| elif "norm" in name.lower() or isinstance(module, torch.nn.LayerNorm):
|
| if hasattr(module, "weight") and module.weight is not None:
|
| export_state_dict[f"{name}.weight"] = module.weight.to(torch.float16)
|
| if hasattr(module, "bias") and module.bias is not None:
|
| export_state_dict[f"{name}.bias"] = module.bias.to(torch.float16)
|
|
|
| config = {"architectures": ["ConvNeXtV2ForImageClassification"], "quantization": "1-Bit_Packed"}
|
| with open(os.path.join(save_dir, "config.json"), "w") as f:
|
| json.dump(config, f)
|
|
|
| safetensors_path = os.path.join(save_dir, "model.safetensors")
|
| save_file(export_state_dict, safetensors_path)
|
|
|
|
|
| print("=" * 50)
|
| print(f"1-Bit λΉνΈ ν¨νΉ ν¬λ§· μ μ₯ μλ£! (μμΉ: {save_dir})")
|
| print(f"μ€μ λμ€ν¬ μ°¨μ§ μ©λ: {os.path.getsize(safetensors_path) / (1024**2):.2f} MB")
|
| print("=" * 50)
|
|
|
|
|
|
|
| _dummy_model = timm.create_model(MODEL_NAME, pretrained=False)
|
| data_config = timm.data.resolve_model_data_config(_dummy_model)
|
| transform_val = timm.data.create_transform(**data_config, is_training=False)
|
| transform_train = timm.data.create_transform(**data_config, is_training=True)
|
| del _dummy_model
|
|
|
|
|
| def collate_fn_train(examples):
|
| return torch.stack(
|
| [transform_train(ex["image"].convert("RGB")) for ex in examples]
|
| ), torch.tensor([ex["label"] for ex in examples])
|
|
|
|
|
| def collate_fn_val(examples):
|
| return torch.stack(
|
| [transform_val(ex["image"].convert("RGB")) for ex in examples]
|
| ), torch.tensor([ex["label"] for ex in examples])
|
|
|
|
|
|
|
| def main():
|
| print("[Phase 3] 1-Bit Binary CNN + μ§μ μ¦λ₯(KD) νμ΅ μμ!")
|
| os.makedirs(SAVE_DIR, exist_ok=True)
|
| os.makedirs(LOG_DIR, exist_ok=True)
|
| csv_file_path = os.path.join(LOG_DIR, "training_log_1bit.csv")
|
|
|
|
|
| if not os.path.exists(csv_file_path):
|
| with open(csv_file_path, mode="w", newline="") as f:
|
| writer = csv.writer(f)
|
| writer.writerow(["Epoch", "Train_KD_Loss", "Val_Accuracy", "Learning_Rate", "Time_sec"])
|
|
|
|
|
| print("FP16 μ μλ λͺ¨λΈ μ€λΉ μ€...")
|
| teacher_model = timm.create_model(MODEL_NAME, pretrained=True)
|
| teacher_model = teacher_model.bfloat16().to(DEVICE)
|
| teacher_model.eval()
|
| for param in teacher_model.parameters():
|
| param.requires_grad = False
|
|
|
|
|
| print("1-Bit νμ λͺ¨λΈ μ€λΉ μ€...")
|
| student_model = timm.create_model(MODEL_NAME, pretrained=True)
|
| replace_layers_with_1bit(student_model)
|
| student_model = student_model.bfloat16().to(DEVICE)
|
|
|
|
|
| print("ImageNet λ°μ΄ν°μ
λ‘λ μ€...")
|
| hf_dataset = load_dataset("ILSVRC/imagenet-1k")
|
| train_loader = DataLoader(
|
| hf_dataset["train"],
|
| batch_size=BATCH_SIZE,
|
| shuffle=True,
|
| num_workers=NUM_WORKERS,
|
| pin_memory=True,
|
| prefetch_factor=4,
|
| collate_fn=collate_fn_train,
|
| )
|
| val_loader = DataLoader(
|
| hf_dataset["validation"],
|
| batch_size=BATCH_SIZE,
|
| shuffle=False,
|
| num_workers=NUM_WORKERS,
|
| pin_memory=True,
|
| prefetch_factor=4,
|
| collate_fn=collate_fn_val,
|
| )
|
|
|
|
|
| optimizer = optim.Adam(student_model.parameters(), lr=LEARNING_RATE, weight_decay=1e-5)
|
| scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=EPOCHS)
|
|
|
|
|
| start_epoch = 1
|
| checkpoints = glob.glob(os.path.join(SAVE_DIR, "qat_1bit_epoch_*.pth"))
|
|
|
| if checkpoints:
|
| latest_ckpt = max(checkpoints, key=os.path.getctime)
|
| epoch_str = latest_ckpt.split("_epoch_")[-1].split(".pth")[0]
|
| start_epoch = int(epoch_str) + 1
|
|
|
| print(f"\n[Auto-Resume] 체ν¬ν¬μΈνΈ λ°κ²¬! κΈ°μ‘΄ νμ λͺ¨λΈμ λΆλ¬μ΅λλ€: {latest_ckpt}")
|
| checkpoint = torch.load(latest_ckpt, map_location=DEVICE)
|
|
|
| if "model_state_dict" in checkpoint:
|
| student_model.load_state_dict(checkpoint["model_state_dict"])
|
| optimizer.load_state_dict(checkpoint["optimizer_state_dict"])
|
| scheduler.load_state_dict(checkpoint["scheduler_state_dict"])
|
| print("1-Bit νμ κ°μ€μΉ, μ΅ν°λ§μ΄μ , μ€μΌμ€λ¬ 볡ꡬ μλ£!")
|
| else:
|
| student_model.load_state_dict(checkpoint)
|
|
|
|
|
| try:
|
| for epoch in range(start_epoch, EPOCHS + 1):
|
| epoch_start_time = time.time()
|
| print(f"\n[Epoch {epoch}/{EPOCHS}] 1-Bit νμ΅ + μ μλ μ§λ μ€...")
|
| student_model.train()
|
| train_loss = 0.0
|
|
|
| for i, (images, labels) in enumerate(train_loader):
|
| images = images.to(DEVICE, dtype=torch.bfloat16)
|
| labels = labels.to(DEVICE)
|
|
|
| optimizer.zero_grad()
|
|
|
|
|
| with torch.no_grad():
|
| teacher_logits = teacher_model(images)
|
|
|
|
|
| student_logits = student_model(images)
|
| loss = kd_loss_fn(student_logits, teacher_logits, labels)
|
|
|
| loss.backward()
|
| torch.nn.utils.clip_grad_norm_(student_model.parameters(), max_norm=1.0)
|
| optimizer.step()
|
|
|
| train_loss += loss.item()
|
|
|
| if i % 500 == 0:
|
| print(f" Step [{i}/{len(train_loader)}] KD Loss: {loss.item():.4f}")
|
|
|
| scheduler.step()
|
| avg_train_loss = train_loss / len(train_loader)
|
|
|
|
|
| student_model.eval()
|
| correct, total = 0, 0
|
| print(f" [Epoch {epoch}] 1-Bit νμ λͺ¨λΈ μ νλ νκ° μ€...")
|
| with torch.no_grad():
|
| for images, labels in val_loader:
|
| images = images.to(DEVICE, dtype=torch.bfloat16)
|
| labels = labels.to(DEVICE)
|
|
|
| outputs = student_model(images)
|
| _, predicted = outputs.max(1)
|
| total += labels.size(0)
|
| correct += predicted.eq(labels).sum().item()
|
|
|
| acc = 100.0 * correct / total
|
| epoch_time = time.time() - epoch_start_time
|
| current_lr = scheduler.get_last_lr()[0]
|
| print(f" Epoch {epoch} 1-Bit Top-1 Accuracy: {acc:.2f} % (Time: {epoch_time:.1f}s)")
|
|
|
|
|
| save_path = os.path.join(SAVE_DIR, f"qat_1bit_epoch_{epoch}.pth")
|
| torch.save(
|
| {
|
| "epoch": epoch,
|
| "model_state_dict": student_model.state_dict(),
|
| "optimizer_state_dict": optimizer.state_dict(),
|
| "scheduler_state_dict": scheduler.state_dict(),
|
| "acc": acc,
|
| },
|
| save_path,
|
| )
|
|
|
|
|
| with open(csv_file_path, mode="a", newline="") as f:
|
| writer = csv.writer(f)
|
| writer.writerow(
|
| [
|
| epoch,
|
| f"{avg_train_loss:.4f}",
|
| f"{acc:.2f}",
|
| f"{current_lr:.6f}",
|
| f"{epoch_time:.1f}",
|
| ]
|
| )
|
| print("\n 30 μν νμ΅μ΄ λͺ¨λ μ’
λ£λμμ΅λλ€. μ΅μ’
λͺ¨λΈ μΆμΆμ μμν©λλ€.")
|
| export_huggingface_1bit(student_model)
|
| except KeyboardInterrupt:
|
| print("\nνμ΅ κ°μ μ€λ¨! μ§ν μν©μ μμ νκ² μ μ₯λμμ΅λλ€.")
|
| sys.exit(0)
|
|
|
|
|
| if __name__ == "__main__":
|
| import multiprocessing
|
|
|
| multiprocessing.freeze_support()
|
| main()
|
|
|