edge-sign / src /base_train_1bit_kd.py
gyann's picture
Deploy Edge-Sign (Direction A redesign) β€” detection+tracking+recognition+Q&A
76ec265 verified
Raw
History Blame Contribute Delete
14.1 kB
import csv
import glob
import json
import os
import sys
import time
import warnings
warnings.filterwarnings("ignore", category=UserWarning, module="PIL.TiffImagePlugin")
import logging # noqa: E402
logging.getLogger("PIL").setLevel(logging.ERROR)
from PIL import ImageFile # noqa: E402
ImageFile.LOAD_TRUNCATED_IMAGES = True
import numpy as np # noqa: E402 # πŸ’‘ λΉ„νŠΈ νŒ¨ν‚Ήμ„ μœ„ν•΄ numpyκ°€ ν•„μš”ν•©λ‹ˆλ‹€!
import timm # noqa: E402
import torch # noqa: E402
import torch.nn as nn # noqa: E402
import torch.nn.functional as F # noqa: E402
import torch.optim as optim # noqa: E402
from datasets import load_dataset # noqa: E402
from safetensors.torch import save_file # noqa: E402 # πŸ’‘ safetensors μΆ”κ°€!
from torch.utils.data import DataLoader # noqa: E402
# 1. ν™˜κ²½ 및 μ„€μ •
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"
# KD (지식 증λ₯˜) ν•˜μ΄νΌνŒŒλΌλ―Έν„°
TEMPERATURE = 4.0
ALPHA = 0.9
# 2. 1-Bit Binarization & μ»€μŠ€ν…€ λ ˆμ΄μ–΄
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)
# 3. 지식 증λ₯˜ (KD) 손싀 ν•¨μˆ˜
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:
# 1. μ›λž˜ κ°€μ€‘μΉ˜(FP16)λ₯Ό κ°€μ Έμ˜΅λ‹ˆλ‹€.
weight = module.weight.data
# 2. 채널별 μŠ€μΌ€μΌ(Scale) 계산 (볡원을 μœ„ν•΄ λ”°λ‘œ μ €μž₯ν•΄μ•Ό 함)
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)
# 3. 이진화 (+1 / -1) 및 Boolean 마슀크(0κ³Ό 1)둜 λ³€ν™˜
binary_mask = (weight > 0).cpu().numpy()
# 4. πŸ”₯ λΉ„νŠΈ νŒ¨ν‚Ή (8개의 λΆˆλ¦¬μ–Έ 값을 1개의 uint8 μ •μˆ˜λ‘œ μ••μΆ•!)
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)
# Normalization λ ˆμ΄μ–΄λŠ” κ·ΈλŒ€λ‘œ 보쑴
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)
# 1500만 νŒŒλΌλ―Έν„°κ°€ 1.8MB둜 μ••μΆ•λ˜λŠ” 기적을 눈으둜 ν™•μΈν•˜μ„Έμš”!
print("=" * 50)
print(f"1-Bit λΉ„νŠΈ νŒ¨ν‚Ή 포맷 μ €μž₯ μ™„λ£Œ! (μœ„μΉ˜: {save_dir})")
print(f"μ‹€μ œ λ””μŠ€ν¬ μ°¨μ§€ μš©λŸ‰: {os.path.getsize(safetensors_path) / (1024**2):.2f} MB")
print("=" * 50)
# 4. μ „μ²˜λ¦¬ 및 λ°μ΄ν„°λ‘œλ” μ„€μ • (μœ„μΉ˜ 정상화)
_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])
# 5. 메인 ν•™μŠ΅ 루프
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")
# 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"])
# 1. 16λΉ„νŠΈ μ„ μƒλ‹˜ λͺ¨λΈ λ‘œλ“œ (ν•™μŠ΅ X)
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
# 2. 1λΉ„νŠΈ 학생 λͺ¨λΈ λ‘œλ“œ (ν•™μŠ΅ O)
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)
# μ˜€ν†  리쀌 (Auto-Resume) 둜직
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)
# 4. KD 에폭 루프
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)
# ν•™μƒμ˜ 예츑 및 KD 손싀 계산
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)
# 5. 학생 혼자 평가 (Validation)
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)")
# 6. 체크포인트 μ €μž₯
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,
)
# CSV λ‘œκΉ…
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()