تنفيذ StyleGAN1 من الصفر

مقدمة

يتناول هذا المقال أحد أفضل نماذج الشبكات المنافسة في الأنماط (GANs) في الوقت الحالي، StyleGAN من الورقة البحثية بنية مولدة تعتمد على الأسلوب للشبكات المنافقة المولدة، سنقوم بإنشاء تنفيذ نظيف وبسيط وقابل للقراءة له باستخدام PyTorch، و سنحاول ت reproduction الورقة الأصلية بقدر الإمكان، لذا إذا قرأت الورقة، يجب أن يكون التنفيذ مشابهًا تقريبًا.

ال一套 بيانات الذي سنستخدمه في هذا الموقع هو هذا مجموعة بيانات من Kaggle والذي يحتوي على 16240 قطعة من الملابس العلوية للنساء بdziel 256*192.

المتطلبات المسبقة

قبل أن تغوص في العمل مع StyleGAN باستخدام PyTorch، تأكد من أنك ت满足 المتطلبات التالية:

  • المعرفة الأساسية للتعلم العميق
    فهم للشبكات العصبية المت 卷积ية (CNNs).
    ألف مع الشبكات المنافقة المولدة (GANs)، بما في ذلك المفاهيم مثل المولد، المميز، و الخسارة المنافقة.

  • متطلبات الأجهزة
    وحدة معالجة رسومية قوية (مستحسن NVIDIA) للتدريب والاستدلال بشكل أسرع.
    تثبيت أداة CUDA ل تسريع وحدة المعالجة الرسومية (cuda و cudnn).

  • الإلمام بـ StyleGAN
    من المفيد أن تكون قد قرأت الأوراق الأصلية لـ StyleGAN أو StyleGAN2 لفهم تحسينات البنية والمفاهيم الرئيسية.

تحميل جميع التبعيات التي نحتاجها

سنقوم أولاً باستيراد مكتبة torch لأننا سنستخدم PyTorch، ومن هناك سنستورد nn. سي帮助我们创建和训练 الشبكات، وسي permit нам استيراد optim، حزمة تنفذ العديد من خوارزميات التحسين (مثل sgd، adam،…). من torchvision سنستورد datasets وtransforms لتحضير البيانات وتطبيق بعض التحولات.

سنستورد functional كـ F من torch.nn لزيادة دقة الصور باستخدام interpolate، DataLoader من torch.utils.data لإنشاء أحجام مجموعات صغيرة، save_image من torchvision.utils ل保存 بعض العينات المزيفة، وlog2 من math لأننا نحتاج تمثيل عكسي لقوة 2 لتنفيذ حجم المجموعة الصغيرة التكيفي بناءً على دقة المخرجات، NumPy للرياضيات الخطية، os للتفاعل مع نظام التشغيل، tqdm لعرض شريط التقدم، وأخيرًا matplotlib.pyplot لعرض النتائج ومقارنتها بالONES الحقيقية.

import torch
from torch import nn, optim
from torchvision import datasets, transforms
import torch.nn.functional as F
from torch.utils.data import DataLoader
from torchvision.utils import save_image
from math import log2
import numpy as np
import os
from tqdm import tqdm
import matplotlib.pyplot as plt

المعلمات الفائقة

  • تهيئة DATASET بمسار الصور الحقيقية.
  • تحديد بدء التدريب بحجم صورة 8×8.
  • تهيئة الجهاز باستخدام Cuda إذا كان متاحًا وCPU要不然،率和 التعلم بـ 0.001.
  • حجم المجموعة سيكون مختلفًا بناءً على دقة الصور التي نريد إنشائها، لذا ن initialize BATCH_SIZES بحجم قائمة الأعداد، يمكنك تغييرها بناءً على VRAM الخاص بك.
  • تهيئة image_size بـ 128 وCHANNELS_IMG بـ 3 لأننا سن generate صور RGB بحجم 128 × 128.
  • في الورقة الأصلية، قاموا بتهيئة Z_DIM، W_DIM، وIN_CHANNELS بـ 512، ولكنني أهيئها بـ 256 بدلاً من ذلك لاستخدام أقل VRAM وتقليل وقت التدريب. قد نحصل على نتائج أفضل حتى لو ضاعفناها.
  • للمodel StyleGAN يمكننا استخدام أي من وظائف الخسارة الخاصة بـ GAN التي نريدها، لذا استخدمت WGAN-GP من الورقة Improved Training of Wasserstein GANs. تحتوي هذه الخسارة على معلمة تسمى λ ومن الشائع إعداد λ = 10.
  • تهيئة PROGRESSIVE_EPOCHS بـ 30 لكل حجم صورة.
DATASET                 = "Women clothes"
START_TRAIN_AT_IMG_SIZE = 8 #الباحثون يبدأون من صور بحجم 8x8 بدلاً من 4x4
DEVICE                  = "cuda" if torch.cuda.is_available() else "cpu"
LEARNING_RATE           = 1e-3
BATCH_SIZES             = [256, 128, 64, 32, 16, 8]
CHANNELS_IMG            = 3
Z_DIM                   = 256
W_DIM                   = 256
IN_CHANNELS             = 256
LAMBDA_GP               = 10
PROGRESSIVE_EPOCHS      = [30] * len(BATCH_SIZES)

الحصول على محمل البيانات

الآن دعونا ننشئ دالة get_loader ل:

  • تطبيق بعض التحويلات على الصور (تغيير حجم الصور إلى الدقة التي نريدها، تحويلها إلى مصفوفات، ثم تطبيق بعض التعزيزات، وأخيرًا ت规范化ها بحيث تكون جميع البكسلات بين -1 و1).
  • تحديد حجم المجموعة الحالية باستخدام القائمة BATCH_SIZES، وتحديد المؤشر كعدد صحيح تمثيل العكس لأس جبري 2 لحجم الصورة/4. وهذا بالفعل كيفية تنفيذ حجم المجموعة الصغيرة التكيفي بناءً على دقة المخرجات.
  • تحضير مجموعة البيانات باستخدام ImageFolder لأنها منظمة بشكل جيد بالفعل.
  • يتم إنشاء_sizes المini-批次 باستخدام DataLoader والذي يأخذ المجموعة البيانية وال尺寸 الم batching مع تحريك البيانات.
  • في النهاية، يتم إرجاع المLoader والمجموعة البيانية.
def get_loader(image_size):
    transform = transforms.Compose(
        [
            transforms.Resize((image_size, image_size)),
            transforms.ToTensor(),
            transforms.RandomHorizontalFlip(p=0.5),
            transforms.Normalize(
                [0.5 for _ in range(CHANNELS_IMG)],
                [0.5 for _ in range(CHANNELS_IMG)],
            ),
        ]
    )
    batch_size = BATCH_SIZES[int(log2(image_size / 4))]
    dataset = datasets.ImageFolder(root=DATASET, transform=transform)
    loader = DataLoader(
        dataset,
        batch_size=batch_size,
        shuffle=True,
    )
    return loader, dataset

تطبيق النماذج

الآن دعونا نطبق نموذج StyleGAN1 للمنشئ والمدمر (ProGAN و StyleGAN1 لهما نفس بنية المدمر) مع الصفات الرئيسية من الورقة البحثية. سنحاول جعل التطبيق مختصراً ولكن في نفس الوقت يمكن فهمه وتفسيره. تحديداً النقاط الرئيسية:

  • شبكة تعيين الضوضاء
  • الت normalize التكيفي للنموذج الفردي (AdaIN)
  • النمو التدريجي

في هذا الدليل، سنقوم بإنشاء صور باستخدام StyleGAN1، و لن نقوم بتحقيق خلط الأنماط والتنوع العشوائي، ولكن لا يجب أن يكون من الصعب القيام بذلك.

دعونا نحدد متغيراً باسم factors يحتوي على الأرقام التي ستضرب في IN_CHANNELS للحصول على عدد القنوات الذي نريده في كل دقة صورة.

factors = [1, 1, 1, 1, 1 / 2, 1 / 4, 1 / 8, 1 / 16, 1 / 32]

شبكة تعيين الضوضاء

شبكة تعيين الضوضاء تأخذ Z وتمرره عبر ثمانية طبقات متصلة بالكامل مفصولة ببعض التنشيط. ولا تنسى تساوي معدل التعلم كما يفعلون في ProGAN (ProGAN وStyleGan من تأليف نفس الباحثين).

دعونا أولاً نبني فئة باسم WSLinear (الخطي المقياس الموزن) التي ستعتمد من nn.Module.

  • في جزء init نرسل in_features وout_channels. ننشئ طبقة خطية، ثم نحدد المقياس الذي سيكون مساوياً لجذر 2 مقسوماً على in_features، ننسخ تحيز الطبقة الحالية إلى متغير لأننا لا نريد أن يتم تقييس تحيز الطبقة الخطية، ثم نزيله، وأخيراً نقوم بتهيئة الطبقة الخطية.
  • في الجزء forward، نرسل x وكل ما سنفعله هو ضرب x في المقياس وإضافة التحيز بعد إعادة تشكيله.
class WSLinear(nn.Module):
    def __init__(
        self, in_features, out_features,
    ):
        super(WSLinear, self).__init__()
        self.linear = nn.Linear(in_features, out_features)
        self.scale = (2 / in_features)**0.5
        self.bias = self.linear.bias
        self.linear.bias = None

        # تهيئة الطبقة الخطية
        nn.init.normal_(self.linear.weight)
        nn.init.zeros_(self.bias)

    def forward(self, x):
        return self.linear(x * self.scale) + self.bias

الآن دعونا ننشئ فئة MappingNetwork.

  • في الجزء init نرسل z_dim وw_din، ونحدد شبكة التمثيل التي تبدأ بتنعيم z_dim، تليها ثماني طبقات من WSLInear وReLU كوظائف تنشيط.
  • في الجزء forward، نعيد شبكة التمثيل.

class MappingNetwork(nn.Module):
    def __init__(self, z_dim, w_dim):
        super().__init__()
        self.mapping = nn.Sequential(
            PixelNorm(),
            WSLinear(z_dim, w_dim),
            nn.ReLU(),
            WSLinear(w_dim, w_dim),
            nn.ReLU(),
            WSLinear(w_dim, w_dim),
            nn.ReLU(),
            WSLinear(w_dim, w_dim),
            nn.ReLU(),
            WSLinear(w_dim, w_dim),
            nn.ReLU(),
            WSLinear(w_dim, w_dim),
            nn.ReLU(),
            WSLinear(w_dim, w_dim),
            nn.ReLU(),
            WSLinear(w_dim, w_dim),
        )

    def forward(self, x):
        return self.mapping(x)

التنعيم التكيفي للطبقة (AdaIN)

الآن دعونا ننشئ فئة AdaIN

  • في الجزء init نرسل القنوات، w_dim، ون�始化 instance_norm الذي سيكون الجزء الخاص بتنعيم الحالة، ون�始化 style_scale وstyle_bias التي ستكون الأجزاء التكيفية مع WSLinear التي ترسم شبكة карт_noise W إلى القنوات.
  • في عملية forward، نرسل x، نطبق تنعيم الحالة عليه، ونرجع style_sclate * x + style_bias.

class AdaIN(nn.Module):
    def __init__(self, channels, w_dim):
        super().__init__()
        self.instance_norm = nn.InstanceNorm2d(channels)
        self.style_scale = WSLinear(w_dim, channels)
        self.style_bias = WSLinear(w_dim, channels)

    def forward(self, x, w):
        x = self.instance_norm(x)
        style_scale = self.style_scale(w).unsqueeze(2).unsqueeze(3)
        style_bias = self.style_bias(w).unsqueeze(2).unsqueeze(3)
        return style_scale * x + style_bias

حقن الضوضاء

الآن دعونا ننشئ فئة InjectNoise لحقن الضوضاء في المولد

  • في الجزء init نرسل القنوات ون�始化 الوزن من توزيع طبيعي عشوائي ونستخدم nn.Parameter حتى يمكن تحسين هذه الأوزان
  • في الجزء forward، نرسل صورة x ونرجعها مع إضافة ضوضاء عشوائية
class InjectNoise(nn.Module):
    def __init__(self, channels):
        super().__init__()
        self.weight = nn.Parameter(torch.zeros(1, channels, 1, 1))

    def forward(self, x):
        noise = torch.randn((x.shape[0], 1, x.shape[2], x.shape[3]), device=x.device)
        return x + self.weight * noise

فئات مفيدة

قام المؤلفون ببناء StyleGAN على تنفيذ ProGAN الرسمي من قبل Karras وآخرين، يستخدمون نفس بنية الم discriminator، حجم المجموعات الصغيرة التكيفي، المعلمات، إلخ. لذا هناك الكثير من الفئات التي تظل نفسها من تنفيذ ProGAN.

في هذا القسم، سننشئ الفئات التي لا تتغير من بنية ProGAN.

في المقطع البرمجي أدناه يمكنك العثور على فئة WSConv2d (طبقة التجميع الموزونة المقياس) لتحقيق المعدل المتساوٍ للتعلم في طبقات التجميع.

class WSConv2d(nn.Module):
    def __init__(
        self, in_channels, out_channels, kernel_size=3, stride=1, padding=1
    ):
        super(WSConv2d, self).__init__()
        self.conv = nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding)
        self.scale = (2 / (in_channels * (kernel_size ** 2))) ** 0.5
        self.bias = self.conv.bias
        self.conv.bias = None

        #�始化 طبقة التجميع
        nn.init.normal_(self.conv.weight)
        nn.init.zeros_(self.bias)

    def forward(self, x):
        return self.conv(x * self.scale) + self.bias.view(1, self.bias.shape[0], 1, 1)

في المقطع البرمجي أدناه يمكنك العثور على فئة PixelNorm لتنعيم Z قبل شبكة 매핑 الضوضاء.

class PixelNorm(nn.Module):
    def __init__(self):
        super(PixelNorm, self).__init__()
        self.epsilon = 1e-8

    def forward(self, x):
        return x / torch.sqrt(torch.mean(x ** 2, dim=1, keepdim=True) + self.epsilon)   

في المقطع البرمجي أدناه يمكنك العثور على فئة ConvBlock التي ستساعدنا في إنشاء المميز.

class ConvBlock(nn.Module):
    def __init__(self, in_channels, out_channels):
        super(ConvBlock, self).__init__()
        self.conv1 = WSConv2d(in_channels, out_channels)
        self.conv2 = WSConv2d(out_channels, out_channels)
        self.leaky = nn.LeakyReLU(0.2)

    def forward(self, x):
        x = self.leaky(self.conv1(x))
        x = self.leaky(self.conv2(x))
        return x

في المقطع البرمجي أدناه يمكنك العثور على فئة Discriminatowich وهي نفسها كما في ProGAN.

class Discriminator(nn.Module):
    def __init__(self, in_channels, img_channels=3):
        super(Discriminator, self).__init__()
        self.prog_blocks, self.rgb_layers = nn.ModuleList([]), nn.ModuleList([])
        self.leaky = nn.LeakyReLU(0.2)

        هنا نعمل بطرق مقلوبة من العوامل لأن الميزان
        يجب أن يكون مت_symmetric من المصدر. لذا فإن أول block prog و
        الطبقة_rgb التي سنضيفها ستعمل ل حجم المدخلات 1024x1024، ثم 512->256-> إلخ
        for i in range(len(factors) - 1, 0, -1):
            conv_in = int(in_channels * factors[i])
            conv_out = int(in_channels * factors[i - 1])
            self.prog_blocks.append(ConvBlock(conv_in, conv_out))
            self.rgb_layers.append(
                WSConv2d(img_channels, conv_in, kernel_size=1, stride=1, padding=0)
            )

        ربما اسم غامض "initial_rgb" هذه مجرد طبقة_RGB للمدخل حجم 4x4
        قمت بذلك لت "__reflect__" طبقة_rgb الأولية للمولد
        self.initial_rgb = WSConv2d(
            img_channels, in_channels, kernel_size=1, stride=1, padding=0
        )
        self.rgb_layers.append(self.initial_rgb)
        self.avg_pool = nn.AvgPool2d(
            kernel_size=2, stride=2
        )  التقليل باستخدام swimming pool المتوسط

        هذا هو blok لحجم المدخل 4x4
        self.final_block = nn.Sequential(
            +1 إلى في القنوات لأننا نربط من MiniBatch std
            WSConv2d(in_channels + 1, in_channels, kernel_size=3, padding=1),
            nn.LeakyReLU(0.2),
            WSConv2d(in_channels, in_channels, kernel_size=4, padding=0, stride=1),
            nn.LeakyReLU(0.2),
            WSConv2d(
                in_channels, 1, kernel_size=1, padding=0, stride=1
            ),  نستخدم هذا بدلاً من الطبقة الخطية
        )

    def fade_in(self, alpha, downscaled, out):
        """Used to fade in downscaled using avg pooling and output from CNN"""
        alpha يجب أن يكون عدد صحيح بين [0، 1]، و upscale.shape == generated.shape
        return alpha * out + (1 - alpha) * downscaled

    def minibatch_std(self, x):
        batch_statistics = (
            torch.std(x, dim=0).mean().repeat(x.shape[0], 1, x.shape[2], x.shape[3])
        )
        نأخذ std لكل مثال (عبر جميع القنوات، و البكسلات) ثم نكرره
        لقناة واحدة و نربطها بالصورة. بهذه الطريقة سيحصل الميزان
        على معلومات عن التباين في المجموعة/الصورة
        return torch.cat([x, batch_statistics], dim=1)

    def forward(self, x, alpha, steps):
        حيث يجب أن نبدأ في قائمة prog_blocks، ربما يكون محيرًا قليلاً ولكن
        الأخير هو للـ 4x4. على سبيل المثال، لنفترض أن steps=1، إذن يجب أن نبدأ
        في الثاني من الآخر لأن حجم المدخل سيكون 8x8. إذا كانت steps==0 نستخدم
        الكتلة النهائية
        cur_step = len(self.prog_blocks) - steps

        التحويل من rgb كخطوة أولية، سيتم تحديد ذلك بناءً على
        حجم الصورة (لكل منها طبقة_rgb خاصة بها)
        out = self.leaky(self.rgb_layers[cur_step](x))

        if steps == 0:  أي، الصورة بحجم 4x4
            out = self.minibatch_std(out)
            return self.final_block(out).view(out.shape[0], -1)

        لأن prog_blocks قد تغير القنوات، لالتقليل نستخدم طبقة_rgb من الحجم السابق/الأصغر
        الذي في حالتنا يتناسب مع +1 في الترقيم
        downscaled = self.leaky(self.rgb_layers[cur_step + 1](self.avg_pool(x)))
        out = self.avg_pool(self.prog_blocks[cur_step](out))

        يتم fade_in أولاً بين المقلص و المدخل
        هذا عكس المولد
        out = self.fade_in(alpha, downscaled, out)

        for step in range(cur_step + 1, len(self.prog_blocks)):
            out = self.prog_blocks[step](out)
            out = self.avg_pool(out)

        out = self.minibatch_std(out)
        return self.final_block(out).view(out.shape[0], -1)

مولد

في架构 المولد، لدينا بعض الأنماط التي تكرر، لذا دعونا أولاً إنشاء فئة لها لجعل كودنا نظيفاً قدر الإمكان، دعونا نسمي الفئة GenBlock التي سترث من nn.Module.

  • في جزء init نرسل in_channels، out_channels، و w_dim، ثم ن�始化 conv1 بـ WSConv2d التي تحول in_channels إلى out_channels، وconv2 بـ WSConv2d التي تحول out_channels إلى out_channels، وleaky بـ Leaky ReLU مع انحدار 0.2 كما هو المستخدم في الورقة، وinject_noise1، وinject_noise2 بـ InjectNoise، وadain1، وadain2 بـ AdaIN
  • في الجزء forward، نرسل x، ونمرره من خلال conv1 ثم إلى inject_noise1 مع leaky، ثم ن normalizeه مع adain1، ومرة أخرى نمرره من خلال conv2 ثم إلى inject_noise2 مع leaky ون normalizeه مع adain2. وأخيراً، نعيد x.
class GenBlock(nn.Module):
    def __init__(self, in_channels, out_channels, w_dim):
        super(GenBlock, self).__init__()
        self.conv1 = WSConv2d(in_channels, out_channels)
        self.conv2 = WSConv2d(out_channels, out_channels)
        self.leaky = nn.LeakyReLU(0.2, inplace=True)
        self.inject_noise1 = InjectNoise(out_channels)
        self.inject_noise2 = InjectNoise(out_channels)
        self.adain1 = AdaIN(out_channels, w_dim)
        self.adain2 = AdaIN(out_channels, w_dim)

    def forward(self, x, w):
        x = self.adain1(self.leaky(self.inject_noise1(self.conv1(x))), w)
        x = self.adain2(self.leaky(self.inject_noise2(self.conv2(x))), w)
        return x

الآن لدينا كل ما نحتاجه لإنشاء المولد.

  • في الجزء init دعونا ن�始化 ‘starting_constant’ بTensor ثابت 4×4 (x 512 قناة في الورقة الأصلية، و 256 في حالتنا) والذي يمر بiteración من الجينيراتور، ويتم تعيينه بواسطة ‘MappingNetwork’, initial_adain1، initial_adain2 بواسطة AdaIN، initial_noise1، initial_noise2 بواسطة InjectNoise، initial_conv بواسطة طبقة التراكم التي تربط في_channels مع نفسه، leaky بواسطة Leaky ReLU مع انحدار 0.2، initial_rgb بواسطة WSConv2d التي تربط في_channels مع img_channels والتي تكون 3 للRGB، prog_blocks بواسطة ModuleList() والذي س يحتوي على جميع الكتل التقدمية (ن indicate مدخل/مخرج القنوات التراكمية عن طريق ضرب في_channels والذي هو 512 في الورقة و 256 في حالتنا في عوامل)، و rgb_blocks بواسطة ModuleList() والذي س يحتوي على جميع الكتل RGB.
  • لإدخال طبقات جديدة (جزء أصل ProGAN)، نضيف الجزء fade_in، والذي نرسل إليه alpha، scaled، و generated، ونقوم بإرجاع [tanh(alpha∗generated+(1−alpha)∗upscale)]، السبب في استخدامنا tanh هو أن سيكون الناتج (الصورة المحتلة) ونريد أن تكون القيم بين 1 و -1.
  • في الجزء الأمامي، نرسل الضجيج (Z_dim)، والقيمة ألفا التي ستظهر تدريجياً أثناء التدريب (ألفا بين 0 و1)، وخطوات وهي عدد الدقة الحالية التي نعمل معها، نمرر x في الخريطة ل��取 المتجه الضجيج الوسيط W، نمرر starting_constant إلى initial_noise1، ونطبقه عليها و على W initial_adain1، ثم نمرره في initial_conv، مرة أخرى نضيف initial_noise2 له مع وظيفة نشاط leaky، ونطبقه عليها و على W initial_adain2. ثم نتحقق مما إذا كانت الخطوات تساوي 0، إذا كانت كذلك، فكل ما نريد القيام به هو تشغيلها عبر RGB الأولي ونكون انتهينا، وإلا، نكرر عدد الخطوات، وفي كل حلقة ن放大 (upscaled) ونمرر عبر الكتلة التدريجية التي correspond إلى تلك الدقة (out). في النهاية، نعيد fade_in التي تأخذ ألفا، final_out، وfinal_upscaled بعد نقلهما إلى RGB.
class Generator(nn.Module):
    def __init__(self, z_dim, w_dim, in_channels, img_channels=3):
        super(Generator, self).__init__()
        self.starting_constant = nn.Parameter(torch.ones((1, in_channels, 4, 4)))
        self.map = MappingNetwork(z_dim, w_dim)
        self.initial_adain1 = AdaIN(in_channels, w_dim)
        self.initial_adain2 = AdaIN(in_channels, w_dim)
        self.initial_noise1 = InjectNoise(in_channels)
        self.initial_noise2 = InjectNoise(in_channels)
        self.initial_conv = nn.Conv2d(in_channels, in_channels, kernel_size=3, stride=1, padding=1)
        self.leaky = nn.LeakyReLU(0.2, inplace=True)

        self.initial_rgb = WSConv2d(
            in_channels, img_channels, kernel_size=1, stride=1, padding=0
        )
        self.prog_blocks, self.rgb_layers = (
            nn.ModuleList([]),
            nn.ModuleList([self.initial_rgb]),
        )

        for i in range(len(factors) - 1):  # -1 لمنع خطأ الفهرس بسبب factors[i+1]
            conv_in_c = int(in_channels * factors[i])
            conv_out_c = int(in_channels * factors[i + 1])
            self.prog_blocks.append(GenBlock(conv_in_c, conv_out_c, w_dim))
            self.rgb_layers.append(
                WSConv2d(conv_out_c, img_channels, kernel_size=1, stride=1, padding=0)
            )

    def fade_in(self, alpha, upscaled, generated):
        # يجب أن يكون ألفا عدد صحيح بين [0, 1]، و upscale.shape == generated.shape
        return torch.tanh(alpha * generated + (1 - alpha) * upscaled)

    def forward(self, noise, alpha, steps):
        w = self.map(noise)
        x = self.initial_adain1(self.initial_noise1(self.starting_constant), w)
        x = self.initial_conv(x)
        out = self.initial_adain2(self.leaky(self.initial_noise2(x)), w)

        if steps == 0:
            return self.initial_rgb(x)

        for step in range(steps):
            upscaled = F.interpolate(out, scale_factor=2, mode="bilinear")
            out = self.prog_blocks[step](upscaled, w)

        # عدد القنوات في upscale سيظل نفسه، بينما
        # out التي انتقلت عبر prog_blocks قد تتغير. لضمان
        # يمكننا تحويل كلاهما إلى rgb نستخدم طبقات rgb مختلفة
        # (steps-1) و steps لـ upscaled، out على التوالي
        final_upscaled = self.rgb_layers[steps - 1](upscaled)
        final_out = self.rgb_layers[steps](out)
        return self.fade_in(alpha, final_upscaled, final_out)

أدوات

في المقطع الكودي أدناه يمكنك العثور على دالة generate_examples التي تأخذ المولد gen، عدد الخطوات لتحديد الحلزون الحالي، وعدد n=100. هدفت هذه الدالة هي توليد n صور وهمية وتخزينها كناتج.

def generate_examples(gen, steps, n=100):

    gen.eval()
    alpha = 1.0
    for i in range(n):
        with torch.no_grad():
            noise = torch.randn(1, Z_DIM).to(DEVICE)
            img = gen(noise, alpha, steps)
            if not os.path.exists(f'saved_examples/step{steps}'):
                os.makedirs(f'saved_examples/step{steps}')
            save_image(img*0.5+0.5, f"saved_examples/step{steps}/img_{i}.png")
    gen.train()

في المقطع الكودي أدناه يمكنك العثور على دالة gradient_penalty لخسارة WGAN-GP.

def gradient_penalty(critic, real, fake, alpha, train_step, device="cpu"):
    BATCH_SIZE, C, H, W = real.shape
    beta = torch.rand((BATCH_SIZE, 1, 1, 1)).repeat(1, C, H, W).to(device)
    interpolated_images = real * beta + fake.detach() * (1 - beta)
    interpolated_images.requires_grad_(True)

    # حساب تقييمات النقد
    mixed_scores = critic(interpolated_images, alpha, train_step)
 
    # أخذ التدرج في التقييمات بالنسبة للصور
    gradient = torch.autograd.grad(
        inputs=interpolated_images,
        outputs=mixed_scores,
        grad_outputs=torch.ones_like(mixed_scores),
        create_graph=True,
        retain_graph=True,
    )[0]
    gradient = gradient.view(gradient.shape[0], -1)
    gradient_norm = gradient.norm(2, dim=1)
    gradient_penalty = torch.mean((gradient_norm - 1) ** 2)
    return gradient_penalty

دالة التدريب

لدالة التدريب، نرسل النقد (التمييز)، المولد gen، الموزع loader، المجموعة البيانية dataset، الخطوة step، والمعامل alpha، ومحسن المولد والنقد.

نبدأ بتحويل جميع أحجام المجموعات الصغيرة التي ننشئها باستخدام DataLoader، ونأخذ فقط الصور لأننا لا نحتاج إلى علامة.

ثم نعداد التدريب للتمييز\Critic عندما نريد تحقيق E(critic(real)) – E(critic(fake)). تعني هذه المعادلة إلى أي مدى يمكن للنقد التمييز بين الصور الحقيقية والوهمية.

بعد ذلك، ن configure التدريب للمعالج عند الرغبة في تعظيم E(المحكم(المزيف)).

في النهاية، نقوم بتحديث الحلقة وقيمة ألفا لـ fade_in ون确保 أنها بين 0 و1، ثم نعيد إرجاعها.

def train_fn(
    critic,
    gen,
    loader,
    dataset,
    step,
    alpha,
    opt_critic,
    opt_gen,
):
    loop = tqdm(loader, leave=True)

    for batch_idx, (real, _) in enumerate(loop):
        real = real.to(DEVICE)
        cur_batch_size = real.shape[0]


        noise = torch.randn(cur_batch_size, Z_DIM).to(DEVICE)

        fake = gen(noise, alpha, step)
        critic_real = critic(real, alpha, step)
        critic_fake = critic(fake.detach(), alpha, step)
        gp = gradient_penalty(critic, real, fake, alpha, step, device=DEVICE)
        loss_critic = (
            -(torch.mean(critic_real) - torch.mean(critic_fake))
            + LAMBDA_GP * gp
            + (0.001 * torch.mean(critic_real ** 2))
        )

        critic.zero_grad()
        loss_critic.backward()
        opt_critic.step()

        gen_fake = critic(fake, alpha, step)
        loss_gen = -torch.mean(gen_fake)

        gen.zero_grad()
        loss_gen.backward()
        opt_gen.step()

        # تحديث ألفا وضمان أقل من 1
        alpha += cur_batch_size / (
            (PROGRESSIVE_EPOCHS[step] * 0.5) * len(dataset)
        )
        alpha = min(alpha, 1)

        loop.set_postfix(
            gp=gp.item(),
            loss_critic=loss_critic.item(),
        )


    return alpha

التدريب

الآن بما أن لدينا كل شيء، دعونا ندمجها معًا لتدريب StyleGAN الخاص بنا.

نبدأ بتهيئة المعالج، المحكم/المحكم، والمحسنات، ثم نحول المعالج والمحكم إلى وضع التدريب، ثم ن loop عبر PROGRESSIVE_EPOCHS، وفي كل حلقة، نستدعي دالة التدريب عدد مرات العصر، ثم نولد بعض الصور المزيفة ونحفظها كنتيجة، باستخدام دالة generate_examples، وأخيرًا، ننتقل إلى دقة الصورة التالية.

gen = Generator(
        Z_DIM, W_DIM, IN_CHANNELS, img_channels=CHANNELS_IMG
    ).to(DEVICE)
critic = Discriminator(IN_CHANNELS, img_channels=CHANNELS_IMG).to(DEVICE)
# تهيئة المحسنات
opt_gen = optim.Adam([{"params": [param for name, param in gen.named_parameters() if "map" not in name]},
                        {"params": gen.map.parameters(), "lr": 1e-5}], lr=LEARNING_RATE, betas=(0.0, 0.99))
opt_critic = optim.Adam(
    critic.parameters(), lr=LEARNING_RATE, betas=(0.0, 0.99)
)


gen.train()
critic.train()

# تبدأ في الخطوة التي ت odpowiednik إلى حجم الصورة الذي ن 설정 في config
step = int(log2(START_TRAIN_AT_IMG_SIZE / 4))
for num_epochs in PROGRESSIVE_EPOCHS[step:]:
    alpha = 1e-5   # تبدأ بـ ألفا منخفض جدًا
    loader, dataset = get_loader(4 * 2 ** step)  
    print(f"Current image size: {4 * 2 ** step}")

    for epoch in range(num_epochs):
        print(f"Epoch [{epoch+1}/{num_epochs}]")
        alpha = train_fn(
            critic,
            gen,
            loader,
            dataset,
            step,
            alpha,
            opt_critic,
            opt_gen
        )

    generate_examples(gen, step)
    step += 1  # تقدم إلى حجم صورة التالي

النتيجة

نأمل أن تكون قادرًا على متابعة جميع الخطوات وفهم كيفية تطبيق StyleGAN بالطريقة الصحيحة. دعنا الآن نتحقق من النتائج التي نحصل عليها بعد تدريب هذا النموذج على هذا مجموع البيانات بدقة 128×128.

الخاتمة

في هذا المقال، قمنا بإنشاء تنفيذ نظيف وبسيط وقابل للقراءة من الصفر لـ StyleGAN1 باستخدام PyTorch. قمنا بتقليد الورقة الأصلية بقدر الإمكان، لذا إذا قرأت الورقة، يجب أن يكون التنفيذ مشابهًا جدًا لها تقريبًا.

Source:
https://www.digitalocean.com/community/tutorials/implementation-stylegan-from-scratch