Download train.py from DonkeySmall/Face-Upscale: direct link, hf CLI and curl.
- Browser
- Download file 17.6 kB
-
https://huggingface.co/DonkeySmall/Face-Upscale/resolve/main/train.py
- Command line
-
hf download hf://DonkeySmall/Face-Upscale/train.py
-
curl -L -o train.py https://huggingface.co/DonkeySmall/Face-Upscale/resolve/main/train.py
17.6 kB
| import os | |
| os.environ["CUDA_VISIBLE_DEVICES"] = "1" | |
| import argparse | |
| import random | |
| import time | |
| from pathlib import Path | |
| import io | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| from torch.utils.data import Dataset, DataLoader | |
| from tqdm import tqdm | |
| from torchvision import transforms | |
| import torchvision.transforms.functional as TF | |
| from torchvision.transforms import InterpolationMode | |
| from PIL import Image, ImageChops, ImageFilter | |
| import torchvision.models as models | |
| from model import UpscalerTransformer | |
| IMG_EXTENSIONS = { | |
| ".png", | |
| ".jpg", | |
| ".jpeg", | |
| ".bmp", | |
| ".tif", | |
| ".tiff", | |
| ".webp", | |
| } | |
| def find_images(folder): | |
| folder = Path(folder) | |
| if not folder.is_dir(): | |
| raise FileNotFoundError(f"Папка с датасетом не найдена: {folder}") | |
| files = [] | |
| for p in folder.rglob("*"): | |
| if p.suffix.lower() in IMG_EXTENSIONS: | |
| files.append(str(p)) | |
| return sorted(files) | |
| def split_dataset(paths, val_split, seed): | |
| paths = sorted(paths) | |
| if len(paths) == 0: | |
| return [], [] | |
| if len(paths) == 1: | |
| return paths, paths | |
| rng = random.Random(seed) | |
| rng.shuffle(paths) | |
| if val_split <= 0: | |
| return paths, paths | |
| val_count = max(1, int(len(paths) * val_split)) | |
| val_paths = paths[:val_count] | |
| train_paths = paths[val_count:] | |
| return train_paths, val_paths | |
| import torch | |
| import torch.nn as nn | |
| def _load_vgg16(): | |
| try: | |
| weights = models.VGG16_Weights.IMAGENET1K_V1 | |
| return models.vgg16(weights=weights) | |
| except Exception: | |
| return models.vgg16(pretrained=True) | |
| class VGGPerceptualLoss(nn.Module): | |
| """ | |
| Perceptual loss на признаках VGG16. | |
| Использует признаки до relu3_3. | |
| """ | |
| def __init__(self): | |
| super().__init__() | |
| vgg = _load_vgg16() | |
| # VGG16 features: | |
| # 0 conv1_1, 1 relu, 2 conv1_2, 3 relu, | |
| # 4 maxpool, | |
| # 5 conv2_1, 6 relu, 7 conv2_2, 8 relu, | |
| # 9 maxpool, | |
| # 10 conv3_1, 11 relu, 12 conv3_2, 13 relu, 14 conv3_3, 15 relu | |
| self.features = nn.Sequential(*list(vgg.features.children())[:16]) | |
| self.features.eval() | |
| for p in self.features.parameters(): | |
| p.requires_grad_(False) | |
| self.register_buffer( | |
| "mean", | |
| torch.tensor([0.485, 0.456, 0.406]).view(1, 3, 1, 1), | |
| ) | |
| self.register_buffer( | |
| "std", | |
| torch.tensor([0.229, 0.224, 0.225]).view(1, 3, 1, 1), | |
| ) | |
| def train(self, mode: bool = True): | |
| # Всегда держим VGG в eval-режиме. | |
| return super().train(False) | |
| def forward(self, sr, hr): | |
| sr = (sr - self.mean) / self.std | |
| hr = (hr - self.mean) / self.std | |
| sr_features = self.features(sr) | |
| hr_features = self.features(hr) | |
| return F.l1_loss(sr_features, hr_features) | |
| class FFTLoss(nn.Module): | |
| """ | |
| FFT loss помогает восстанавливать высокие частоты: | |
| резкость, мелкие детали, текстуру. | |
| """ | |
| def forward(self, sr, hr): | |
| sr_fft = torch.fft.rfft2(sr, norm="ortho") | |
| hr_fft = torch.fft.rfft2(hr, norm="ortho") | |
| sr_mag = torch.log1p(torch.abs(sr_fft)) | |
| hr_mag = torch.log1p(torch.abs(hr_fft)) | |
| return F.l1_loss(sr_mag, hr_mag) | |
| class SRDataset(Dataset): | |
| def __init__( | |
| self, | |
| image_paths, | |
| hr_size=256, | |
| lr_size=128, | |
| train=True, | |
| ): | |
| self.image_paths = image_paths | |
| self.hr_size = hr_size | |
| self.lr_size = lr_size | |
| self.train = train | |
| def __len__(self): | |
| return len(self.image_paths) | |
| def __getitem__(self, idx): | |
| img_hr = Image.open(self.image_paths[idx]).convert("RGB") | |
| if self.train: | |
| rnd = random.randint(0, 192) | |
| area = (rnd, rnd, 1024 - rnd, 1024 - rnd) | |
| img_hr = img_hr.crop(area) | |
| img_hr = img_hr.resize((self.hr_size, self.hr_size), Image.Resampling.LANCZOS) | |
| if random.randint(0, 1) == 0: | |
| img_hr = img_hr.transpose(Image.FLIP_LEFT_RIGHT) | |
| if random.randint(0, 1) == 0: | |
| random_choice = random.randint(0, 2) | |
| if random_choice == 0: | |
| img_hr = img_hr.transpose(Image.Transpose.ROTATE_90) | |
| elif random_choice == 1: | |
| img_hr = img_hr.transpose(Image.Transpose.ROTATE_180) | |
| elif random_choice == 2: | |
| img_hr = img_hr.transpose(Image.Transpose.ROTATE_270) | |
| if random.randint(0, 1) == 0: | |
| rnd_x = random.randint(-self.hr_size // 3, self.hr_size // 3) | |
| rnd_y = random.randint(-self.hr_size // 3, self.hr_size // 3) | |
| img_hr = ImageChops.offset(img_hr, xoffset=rnd_x, yoffset=rnd_y) | |
| degradation_type = random.randint(0, 1) | |
| resampling_mode = random.randint(0, 5) | |
| if degradation_type == 0: | |
| intermediate_size = random.randint(self.lr_size // 4, self.lr_size) | |
| img_lr = img_hr.resize((intermediate_size, intermediate_size), resampling_mode) | |
| img_lr = img_lr.resize((self.lr_size, self.lr_size), Image.Resampling.LANCZOS) | |
| elif degradation_type == 1: | |
| img_lr = img_hr.resize((self.lr_size, self.lr_size), resampling_mode) | |
| if random.randint(0, 1) == 0: | |
| buffer = io.BytesIO() | |
| img_lr.save(buffer, format="JPEG", quality=random.randint(20, 80), optimize=True) | |
| buffer.seek(0) | |
| img_lr = Image.open(buffer) | |
| img_lr.load() | |
| else: | |
| img_hr = img_hr.resize((self.hr_size, self.hr_size), Image.Resampling.LANCZOS) | |
| img_lr = img_hr.resize((self.lr_size, self.lr_size), Image.Resampling.LANCZOS) | |
| hr = TF.to_tensor(img_hr) | |
| lr = TF.to_tensor(img_lr) | |
| return lr, hr | |
| def train_one_epoch( | |
| model, | |
| loader, | |
| optimizer, | |
| scaler, | |
| device, | |
| use_amp, | |
| args, | |
| perceptual_loss, | |
| fft_loss, | |
| ): | |
| model.train() | |
| total_loss = 0.0 | |
| num_samples = 0 | |
| pbar = tqdm(loader, desc="Training", leave=False) | |
| for lr, hr in pbar: | |
| lr = lr.to(device, non_blocking=True) | |
| hr = hr.to(device, non_blocking=True) | |
| optimizer.zero_grad(set_to_none=True) | |
| if use_amp: | |
| with torch.cuda.amp.autocast(): | |
| sr = model(lr) | |
| loss = args.w_l1 * F.l1_loss(sr.float(), hr) | |
| else: | |
| sr = model(lr) | |
| loss = args.w_l1 * F.l1_loss(sr, hr) | |
| sr_f = sr.float() | |
| if perceptual_loss is not None: | |
| loss = loss + args.w_perceptual * perceptual_loss(sr_f, hr) | |
| if fft_loss is not None: | |
| loss = loss + args.w_fft * fft_loss(sr_f, hr) | |
| if use_amp: | |
| scaler.scale(loss).backward() | |
| scaler.unscale_(optimizer) | |
| torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) | |
| scaler.step(optimizer) | |
| scaler.update() | |
| else: | |
| loss.backward() | |
| torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) | |
| optimizer.step() | |
| total_loss += loss.item() * lr.size(0) | |
| num_samples += lr.size(0) | |
| avg_loss = total_loss / max(1, num_samples) | |
| pbar.set_postfix(loss=f"{avg_loss:.4f}") | |
| return total_loss / max(1, num_samples) | |
| def evaluate(model, loader, device): | |
| model.eval() | |
| total_psnr = 0.0 | |
| count = 0 | |
| pbar = tqdm(loader, desc="Evaluating", leave=False) | |
| for lr, hr in pbar: | |
| lr = lr.to(device, non_blocking=True) | |
| hr = hr.to(device, non_blocking=True) | |
| sr = model(lr) | |
| sr = torch.clamp(sr.float(), 0.0, 1.0) | |
| mse = torch.mean((sr - hr) ** 2, dim=(1, 2, 3)) | |
| psnr = 10.0 * torch.log10(1.0 / mse.clamp_min(1e-12)) | |
| total_psnr += psnr.sum().item() | |
| count += lr.size(0) | |
| # Обновляем прогресс-бар текущим средним PSNR | |
| avg_psnr = total_psnr / max(1, count) | |
| pbar.set_postfix(psnr=f"{avg_psnr:.2f} dB") | |
| return total_psnr / max(1, count) | |
| def parse_args(): | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument( | |
| "--data_dir", | |
| type=str, | |
| required=True, | |
| help="Папка с HR-изображениями.", | |
| ) | |
| parser.add_argument( | |
| "--output_dir", | |
| type=str, | |
| default="runs_quality", | |
| help="Куда сохранять чекпоинты.", | |
| ) | |
| parser.add_argument( | |
| "--resume", | |
| type=str, | |
| default="", | |
| help="Путь к last.pth или best.pth для возобновления обучения.", | |
| ) | |
| parser.add_argument( | |
| "--reset_scheduler", | |
| action="store_true", | |
| help="Пересчитать scheduler под новый --epochs при возобновлении.", | |
| ) | |
| parser.add_argument("--epochs", type=int, default=300) | |
| parser.add_argument("--batch_size", type=int, default=4) | |
| parser.add_argument("--lr", type=float, default=0.0001) | |
| parser.add_argument("--weight_decay", type=float, default=0.00001) | |
| # Параметры модели. | |
| # Для качества лучше больше, чем для скорости. | |
| parser.add_argument("--dim", type=int, default=512) | |
| parser.add_argument("--depth", type=int, default=8) | |
| parser.add_argument("--heads", type=int, default=8) | |
| parser.add_argument("--window", type=int, default=16) | |
| # Датасет. | |
| parser.add_argument("--hr_size", type=int, default=256) | |
| parser.add_argument("--lr_size", type=int, default=128) | |
| parser.add_argument("--val_split", type=float, default=0.05) | |
| parser.add_argument("--num_workers", type=int, default=4) | |
| parser.add_argument("--seed", type=int, default=42) | |
| # Loss weights. | |
| parser.add_argument("--w_l1", type=float, default=1.0) | |
| parser.add_argument("--w_perceptual", type=float, default=0.1) | |
| parser.add_argument("--w_fft", type=float, default=0.1) | |
| # Если нужно отключить тяжёлые лоссы. | |
| parser.add_argument( | |
| "--no_perceptual", | |
| action="store_true", | |
| help="Отключить VGG perceptual loss.", | |
| ) | |
| parser.add_argument( | |
| "--no_fft", | |
| action="store_true", | |
| help="Отключить FFT loss.", | |
| ) | |
| return parser.parse_args() | |
| def main(): | |
| args = parse_args() | |
| out_dir = Path(args.output_dir) | |
| out_dir.mkdir(parents=True, exist_ok=True) | |
| device = torch.device("cuda" if torch.cuda.is_available() else "cpu") | |
| print(f"Device: {device}") | |
| torch.backends.cudnn.benchmark = True | |
| paths = find_images(args.data_dir) | |
| print(f"Найдено изображений: {len(paths)}") | |
| train_paths, val_paths = split_dataset(paths, args.val_split, args.seed) | |
| print(f"Train: {len(train_paths)}, Val: {len(val_paths)}") | |
| train_dataset = SRDataset( | |
| image_paths=train_paths, | |
| hr_size=args.hr_size, | |
| lr_size=args.lr_size, | |
| train=True, | |
| ) | |
| val_dataset = SRDataset( | |
| image_paths=val_paths, | |
| hr_size=args.hr_size, | |
| lr_size=args.lr_size, | |
| train=False, | |
| ) | |
| pin_memory = device.type == "cuda" | |
| persistent_workers = args.num_workers > 0 | |
| train_loader = DataLoader( | |
| train_dataset, | |
| batch_size=args.batch_size, | |
| shuffle=True, | |
| num_workers=args.num_workers, | |
| pin_memory=pin_memory, | |
| persistent_workers=persistent_workers, | |
| ) | |
| val_loader = DataLoader( | |
| val_dataset, | |
| batch_size=args.batch_size, | |
| shuffle=False, | |
| num_workers=args.num_workers, | |
| pin_memory=pin_memory, | |
| persistent_workers=persistent_workers, | |
| ) | |
| model = UpscalerTransformer( | |
| dim=args.dim, | |
| depth=args.depth, | |
| num_heads=args.heads, | |
| window_size=args.window, | |
| ).to(device) | |
| optimizer = torch.optim.AdamW( | |
| model.parameters(), | |
| lr=args.lr, | |
| weight_decay=args.weight_decay, | |
| ) | |
| scheduler = torch.optim.lr_scheduler.CosineAnnealingLR( | |
| optimizer, | |
| T_max=args.epochs, | |
| eta_min=1e-6, | |
| ) | |
| use_amp = device.type == "cuda" | |
| scaler = torch.cuda.amp.GradScaler(enabled=use_amp) | |
| perceptual_loss = None | |
| if not args.no_perceptual: | |
| try: | |
| print("Загрузка VGG perceptual loss...") | |
| perceptual_loss = VGGPerceptualLoss().to(device).eval() | |
| print("VGG perceptual loss включён.") | |
| except Exception as e: | |
| print(f"Не удалось загрузить VGG perceptual loss: {e}") | |
| print("Продолжаю без perceptual loss.") | |
| perceptual_loss = None | |
| fft_loss = None | |
| if not args.no_fft: | |
| fft_loss = FFTLoss().to(device) | |
| print("FFT loss включён.") | |
| start_epoch = 1 | |
| best_psnr = -float("inf") | |
| if args.resume: | |
| resume_path = Path(args.resume) | |
| if not resume_path.exists(): | |
| raise FileNotFoundError(f"Чекпоинт для возобновления не найден: {resume_path}") | |
| print(f"Возобновление обучения из: {resume_path}") | |
| ckpt = torch.load(resume_path, map_location=device) | |
| if isinstance(ckpt, dict): | |
| # Веса модели | |
| model.load_state_dict(ckpt.get("model", ckpt)) | |
| # Оптимизатор | |
| if "optimizer" in ckpt: | |
| optimizer.load_state_dict(ckpt["optimizer"]) | |
| # Scheduler | |
| if ( | |
| not args.reset_scheduler | |
| and "scheduler" in ckpt | |
| and ckpt["scheduler"] is not None | |
| ): | |
| scheduler.load_state_dict(ckpt["scheduler"]) | |
| elif "epoch" in ckpt: | |
| # Если scheduler не был сохранён или сброшен, | |
| # восстанавливаем его позицию вручную. | |
| for _ in range(ckpt["epoch"]): | |
| scheduler.step() | |
| # AMP scaler | |
| if use_amp and "scaler" in ckpt and ckpt["scaler"] is not None: | |
| scaler.load_state_dict(ckpt["scaler"]) | |
| start_epoch = ckpt.get("epoch", 0) + 1 | |
| best_psnr = ckpt.get("best_psnr", ckpt.get("val_psnr", -float("inf"))) | |
| else: | |
| # Если это просто state_dict модели | |
| model.load_state_dict(ckpt) | |
| print(f"Продолжаю с эпохи: {start_epoch}") | |
| print(f"Лучший PSNR на данный момент: {best_psnr:.3f} dB") | |
| config = { | |
| "dim": args.dim, | |
| "depth": args.depth, | |
| "heads": args.heads, | |
| "window": args.window, | |
| "hr_size": args.hr_size, | |
| "lr_size": args.lr_size, | |
| } | |
| if start_epoch > args.epochs: | |
| print("Обучение уже завершено для указанного --epochs.") | |
| print("Увеличь --epochs, если хочешь дообучить модель.") | |
| return | |
| for epoch in range(start_epoch, args.epochs + 1): | |
| t0 = time.time() | |
| train_loss = train_one_epoch( | |
| model=model, | |
| loader=train_loader, | |
| optimizer=optimizer, | |
| scaler=scaler, | |
| device=device, | |
| use_amp=use_amp, | |
| args=args, | |
| perceptual_loss=perceptual_loss, | |
| fft_loss=fft_loss, | |
| ) | |
| val_psnr = evaluate(model, val_loader, device) | |
| scheduler.step() | |
| state = { | |
| "epoch": epoch, | |
| "model": model.state_dict(), | |
| "optimizer": optimizer.state_dict(), | |
| "scheduler": scheduler.state_dict(), | |
| "scaler": scaler.state_dict() if use_amp else None, | |
| "train_loss": train_loss, | |
| "val_psnr": val_psnr, | |
| "best_psnr": max(best_psnr, val_psnr), | |
| "config": config, | |
| } | |
| torch.save(state, out_dir / "last.pth") | |
| if val_psnr > best_psnr: | |
| best_psnr = val_psnr | |
| torch.save(state, out_dir / "best.pth") | |
| dt = time.time() - t0 | |
| print( | |
| f"Epoch {epoch:03d} | " | |
| f"loss: {train_loss:.6f} | " | |
| f"val PSNR: {val_psnr:.3f} dB | " | |
| f"best PSNR: {best_psnr:.3f} dB | " | |
| f"time: {dt:.2f}s" | |
| ) | |
| if __name__ == "__main__": | |
| main() |