ישום StyleGAN1 מלמטה למעלה

הקדמה

מאמר זה עוסק באחד מהרשתות GAN הטובות ביותר כיום, StyleGAN, מתוך המאמר ארכיטקטורת יוצר מושכת מבוססת סגנון עבור רשתות יוצר-יריב, נעשה המימוש הנקי, הגולמי והקריא שלו באמצעות PyTorch, וננסה לשחזר את המאמר המקורי ככל האפשר, אז אם קראתם את המאמר, היישום צריך להיות כמעט זהה.

אוסף הנתונים שנשתמש בו בבלוג זה הוא אוסף הנתונים מ-Kaggle שמכיל 16,240 בגדי עליונים לנשים ברזולוציה של 256*192.

דרישות מוקדמות

לפני שתתחילו לעבוד עם StyleGAN באמצעות PyTorch, ודאו שיש לכם את הדרישות הבאות:

  • ידע בסיסי בלמידה עמוקה
    הבנה של רשתות נוירונים קונבולוציוניות (CNNs).
    אינטימיות עם רשתות יוצר-יריב (GANs), כולל תפיסות כמו היוצר, התוקף ואובדן היריב.

  • דרישות חומרה
    GPU חזק (מומלץ NVIDIA) לאימון מהיר יותר ולהפעלה.
    התקנת ערכת הכלים CUDA להאצת GPU (cuda ו-cudnn).

  • ידיעה של StyleGAN
    זה יכול לעזור לקרוא את המאמרים המקוריים של StyleGAN או StyleGAN2 כדי להבין שיפורים בארכיטקטורה ומושגים מרכזיים.

טעינת כל התלויות הנחוצות לנו

ראשית נכניס את torch מכיוון שנשתמש ב PyTorch, ומשם נכניס nn. זה יעזור לנו ליצור ולאמן את הרשתות, וגם יאפשר לנו לכניס את 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 כדי להראות את התוצאות ולהשוות אותם לאמיתיים.

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.
  • גודל החלק היה שונה בהתאם לרזולוציה של התמונות שאנחנו רוצים ליצור, אז אנחנו מתחילים את BATCH_SIZES על ידי רשימה של מספרים, אתה יכול לשנות אותם בהתאם ל-VRAM שלך.
  • להתחיל את image_size ב-128 ו-CHANNELS_IMG ב-3 כי אנחנו ניצור תמונות RGB בגודל 128 על 128.
  • במאמר המקורי, הם מתאימים את Z_DIM, W_DIM ו-IN_CHANNELS ל-512, אבל אני מתאים אותם ל-256 במקום, כדי להקטין את שימוש ב-VRAM ולהאיץ את האימון. ייתכן שאפילו נוכל להשיג תוצאות טובות יותר אם נכפיל אותם.
  • ל-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 של image_size/4. וזו למעשה הדרך שבה אנחנו מיישמים את גודל המיניבאטץ' האדפטיבי התלוי ברזולוציית היצוא.
  • להכין את המערכת באמצעות ImageFolder כי היא כבר מאורגנת בדרך נחמדה.
  • יצור גודל מיני-לוחית באמצעות DataLoader שלוקח את המערכת וגודל הלוחית עם ערבוב הנתונים.
  • לבסוף, חזיר את הטעינה והמערכת.
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 יש אותו חיזוק של המבחן) עם התכונות המרכזיות מהמאמר. ננסה לעשות את היישום קומפקטי אבל גם לשמור על קריאות והבנה. במיוחד, הנקודות המרכזיות:

  • רשת מיפוי רעש
  • נורמליזציה מותאמת למשתנים (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 Mapping Network 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 ואחרים, הם משתמשים באותה ארכיטקטורת מפלט כמו ב-ProGAN, גודל מיניבאטץ' אדפטיבי, מקדמים וכו'. כך שיש הרבה כיתות שנשארות אותו דבר כמו ביישום 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

בקטע הקוד למטה ניתן למצוא את הכיתה Discriminator שהיא זהה לזו ב-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)

        כאן אנחנו עובדים בדרך חזרה מגורמים בגלל המבחן
        כך שהמבחן צריך להיות מוארך מהיוצר. אז הבלוק הראשון prog_block ו
        שכבת 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
        עשינו זאת כדי ל"האריך" את הראשוני_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
        )  דיגום באמצעות מינימום פול

        זה הבלוק לגודל קלט 4x4
        self.final_block = nn.Sequential(
            ++1 ל-in_channels כי אנחנו מצרפים מ-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"""
        אלפא צריכה להיות סקלר בתוך [0, 1], ו-yscale.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])
        )
        לוקחים את ה-stdev לכל דוגמה (על כל הערוצים, והפיקסלים) ואז חוזרים עליו
        לערוץ יחיד ומצרפים אותו עם התמונה. בדרך זו המבחן
        יקבל מידע על השינוי בלהק/תמונה
        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_layer
        מהגודל הקודם/קטן יותר שבמקרה שלנו מתקשר ל ++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, אז אנחנו נורמלים אותו עם adain1, ושוב אנחנו עוברים אותו דרך conv2 אז ל inject_noise2 עם leaky ואנחנו נורמלים אותו עם 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’ על ידי טנזור קבוע של 4 x 4 (x 512 ערוצים לפי המאמר המקורי, ו-256 במקרה שלנו) שעובר סבב של המחולל, ממופה על ידי ‘MappingNetwork’, initial_adain1, initial_adain2 על ידי AdaIN, initial_noise1, initial_noise2 על ידי InjectNoise, initial_conv על ידי שכבת קונבולוציה שממפה in_channels לעצמה, leaky על ידי Leaky ReLU עם שיפוע של 0.2, initial_rgb על ידי WSConv2d שממפה in_channels ל img_channels שהוא 3 ל-RGB, prog_blocks על ידי ModuleList() שיכיל את כל הבלוקים הפרוגרסיביים (אנו מציינים את ערוצי הקלט/פלט של הקונבולוציה על ידי כפל in_channels שהוא 512 במאמר ו-256 במקרה שלנו עם גורמים), ו-rgb_blocks על ידי ModuleList() שיכיל את כל בלוקי ה-RGB.
  • כדי להדהד שכבות חדשות (רכיב מקורי של ProGAN), אנו מוסיפים את חלק הfade_in, אותו אנו שולחים alpha, scaled, ו-generated, ואנו מחזירים [tanh(alpha∗generated+(1−alpha)∗upscale)], הסיבה לשימוש ב-tanh היא שזה יהיה הפלט (התמונה המיוצרת) ואנו רוצים שהפיקסלים יהיו בטווח בין 1 ל–1.
  • בחלק הקדימי, אנחנו שולחים את הרעש (Z_dim), את הערך alpha שעומד להתבהר בהדרגה במהלך האימון (alpha הוא בין 0 ל1), ושלבים שהוא מספר הרזולוציה הנוכחי שאנחנו עובדים איתו, אנחנו מעבירים x למפה כדי לקבל את הוקטור הרעש הבינאמצעי W, אנחנו מעבירים starting_constant לinitial_noise1, מיישם אותו ולW initial_adain1, אז אנחנו מעבירים אותו לinitial_conv, ושוב אנחנו מוסיפים initial_noise2 לו עם leaky כתפוקת, ומיישם אותו ולW initial_adain2. אז בודקים אם steps = 0, אם כן, כל מה שאנחנו רוצים לעשות זה לרוץ אותו דרך initial RGB וסיימנו, אחרת, אנחנו חוזרים על מספר השלבים, ובכל לולאה אנחנו מגדילים (upscaled) ומריצים דרך הבלוק הפרוגרסיבי שמתאים לרזולוציה הזו (out). בסוף, אנחנו מחזירים fade_in שלוקח alpha, 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):
        # alpha צריך להיות סקלר בין [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_layers
        # (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, ואנחנו לוקחים רק את התמונות מכיוון שאנחנו לא צריכים תווית.

אז אנחנו מתקינים את האימון עבור האותנטיקטור\מבקר כאשר אנחנו רוצים למקסם E(מבקר(אמיתי)) – E(מבקר(מזויף)). המשוואה הזו אומרת כמה המבקר יכול להבחין בין תמונות אמיתיות ומזויפות.

לאחר מכן, אנחנו מגדירים את האימון עבור הגנרטור כשאנחנו רוצים למקסם 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 שלנו.

אנחנו מתחילים על ידי איתוח הגנרטור, המפריד/המבקר והאופטימיזרים, אז מכניסים את הגנרטור והמבקר למצב אימון, אז חוזרים על 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()

# מתחילים בשלב שמתאים לגודל התמונה שהגדרנו בהגדרות
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*x 128.

מסקנה

במאמר הזה, אנחנו יוצרים יישום נקי, פשוט וקריא של StyleGAN1 מלמטה למעלה באמצעות PyTorch. אנחנו משכפלים את המאמר המקורי ככל שניתן, כך שאם תקרא את המאמר, היישום יהיה כמעט זהה.

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