Search code examples
kerastensorflow2.0python-class

I developed a class for generating images, but I have a type problem (name 'train_step' is not defined) that I can't


Here's the complete class code with an example of its implementation.

import tensorflow as tf
import numpy as np
import time
from tensorflow.keras.applications.inception_v3 import InceptionV3
from tensorflow.keras.applications.inception_v3 import preprocess_input
from scipy.linalg import sqrtm

class ImageGAN:
    """
    Classe pour construire un GAN (Generative Adversarial Network) pour la génération d'images en utilisant TensorFlow.

    Paramètres :
        - generator : Modèle TensorFlow représentant le générateur du GAN.
        - discriminator : Modèle TensorFlow représentant le discriminateur du GAN.
        - noise_dim : Dimension du vecteur de bruit d'entrée du générateur.
        - batch_size : Taille des mini-lots pour l'entraînement.
        - epochs : Nombre d'epochs pour l'entraînement.
        - learning_rate : Taux d'apprentissage pour l'optimiseur.
        - verbose : Booléen indiquant s'il faut afficher les informations de progression à chaque epoch.
        - fid_samples : Nombre d'échantillons pour le calcul de la FID.
        - spectral_regularization : Booléen indiquant s'il faut appliquer la régularisation spectrale.
        - metric : Métrique d'évaluation à utiliser (parmi 'fid', 'inception_score', 'kid', 'mse').
        - conditional : Booléen indiquant si l'architecture doit être conditionnelle.
        - use_adamw : Booléen indiquant si l'optimiseur AdamW doit être utilisé.

    Méthodes :
        - fit : Entraîne le GAN sur les données fournies.
        - generate_images : Génère des images à l'aide du générateur entraîné.
        - calculate_metric : Calcule la métrique d'évaluation spécifiée entre les images réelles et générées.

    Exemple d'utilisation :

        # Création du générateur et du discriminateur basés sur StyleGAN2
        generator = build_stylegan2_generator(...)
        discriminator = build_stylegan2_discriminator(...)

        # Création de l'instance de la classe ImageGAN avec des options avancées
        gan = ImageGAN(generator, discriminator, noise_dim=512, batch_size=32, epochs=100, learning_rate=0.0002,
                       verbose=True, fid_samples=1000, spectral_regularization=True, metric='fid', conditional=False,
                       use_adamw=True)

        # Entraînement du GAN sur des données réelles
        gan.fit(real_images)

        # Génération d'images à l'aide du générateur entraîné
        generated_images = gan.generate_images(10)

        # Calcul de la métrique d'évaluation entre les images réelles et générées
        metric_score = gan.calculate_metric(real_images, num_samples=1000)

    """

    def __init__(self, generator, discriminator, noise_dim, batch_size=64, epochs=100, learning_rate=0.0002,
                 verbose=True, fid_samples=1000, spectral_regularization=True, metric='fid', conditional=False,
                 use_adamw=True):
        self.generator = generator
        self.discriminator = discriminator
        self.noise_dim = noise_dim
        self.batch_size = batch_size
        self.epochs = epochs
        self.learning_rate = learning_rate
        self.verbose = verbose
        self.fid_samples = fid_samples
        self.spectral_regularization = spectral_regularization
        self.metric = metric
        self.conditional = conditional
        self.use_adamw = use_adamw

    def generate_latent_noise(batch_size, noise_dim):
       return np.random.normal(size=(batch_size, noise_dim))

    def train_step(real_images, generator_optimizer, discriminator_optimizer, loss_fn, batch_size, noise_dim, spectral_regularization, discriminator):

       noise = generate_latent_noise(batch_size, noise_dim)

       with tf.GradientTape(persistent=True) as tape:

            generated_images = generator(noise, training=True)

            real_output = discriminator(real_images, training=True)
            generated_output = discriminator(generated_images, training=True)

            gen_loss = loss_fn(tf.ones_like(generated_output), generated_output)
            disc_loss_real = loss_fn(tf.ones_like(real_output), real_output)
            disc_loss_generated = loss_fn(tf.zeros_like(generated_output), generated_output)
            disc_loss = disc_loss_real + disc_loss_generated

            if spectral_regularization:
                disc_grads = tape.gradient(disc_loss, discriminator.trainable_variables)
                disc_grads_norm = [tf.norm(grad) for grad in disc_grads]
                disc_penalty = 10.0 * tf.reduce_mean(tf.square(disc_grads_norm))
                disc_loss += disc_penalty

       gen_gradients = tape.gradient(gen_loss, generator.trainable_variables)
       disc_gradients = tape.gradient(disc_loss, discriminator.trainable_variables)

       generator_optimizer.apply_gradients(zip(gen_gradients, generator.trainable_variables))
       discriminator_optimizer.apply_gradients(zip(disc_gradients, discriminator.trainable_variables))

       return gen_loss, disc_loss

    def fit(self, real_images):
        """
        Entraîne le GAN sur des données réelles (images réelles).

        Paramètres :
            - real_images : Tensor ou tableau numpy contenant les images réelles pour l'entraînement.

        Retourne :
            Rien.

        """

        # Préparation des données
        real_images = tf.data.Dataset.from_tensor_slices(real_images).shuffle(len(real_images)).batch(self.batch_size)

        # Fonction de perte pour le générateur et le discriminateur
        if self.conditional:
            loss_fn = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)
        else:
            loss_fn = tf.keras.losses.BinaryCrossentropy(from_logits=True)

        # Optimiseurs pour le générateur et le discriminateur
        if self.use_adamw:
            generator_optimizer = tf.keras.optimizers.AdamW(self.learning_rate)
            discriminator_optimizer = tf.keras.optimizers.AdamW(self.learning_rate)
        else:
            generator_optimizer = tf.keras.optimizers.Adam(self.learning_rate)
            discriminator_optimizer = tf.keras.optimizers.Adam(self.learning_rate)

        # Entraînement du GAN sur plusieurs epochs
        for epoch in range(self.epochs):
            start_time = time.time()

            gen_loss_avg = tf.keras.metrics.Mean()
            disc_loss_avg = tf.keras.metrics.Mean()

            for real_batch in real_images:
                gen_loss, disc_loss = train_step(real_batch, generator_optimizer, discriminator_optimizer, loss_fn,
                                                 self.batch_size, self.noise_dim, self.spectral_regularization, self.discriminator)
                gen_loss_avg.update_state(gen_loss)
                disc_loss_avg.update_state(disc_loss)

            duration = time.time() - start_time

            if self.verbose:
                print(f"Epoch {epoch+1}/{self.epochs}, Gen Loss: {gen_loss_avg.result():.4f}, Disc Loss: {disc_loss_avg.result():.4f}, Duration: {duration:.2f} seconds")


    def generate_images(self, num_images):
        """
        Génère des images à l'aide du générateur entraîné.

        Paramètres :
            - num_images : Nombre d'images à générer.

        Retourne :
            - Tensor : Images générées.

        """

        noise = generate_latent_noise(num_images, self.noise_dim)
        generated_images = self.generator.predict(noise)
        return generated_images

    def calculate_metric(self, real_images, num_samples=None, metric=None):
        """
        Calcule la métrique d'évaluation spécifiée entre les images réelles et générées.

        Paramètres :
            - real_images : Tensor ou tableau numpy contenant les images réelles.
            - num_samples : Nombre d'échantillons à utiliser pour le calcul de la métrique.
            - metric : Métrique d'évaluation à utiliser (parmi 'fid', 'inception_score', 'kid', 'mse').


        Retourne :
            - float : Score de la métrique.

        """

        if num_samples is None:
            num_samples = self.fid_samples

        if metric is None:
            metric = self.metric

        if self.metric == 'fid':
            return self.calculate_fid(self.generator, real_images, num_samples)
        elif self.metric == 'inception_score':
            return self.calculate_inception_score(self.generator, num_samples)
        elif self.metric == 'kid':
            return self.calculate_kid(self.generator, real_images, num_samples)
        elif self.metric == 'mse':
            return self.calculate_mse(self.generator, real_images, num_samples)
        else:
            raise ValueError("Metric not supported. Available metrics: 'fid', 'inception_score', 'kid', 'mse'")


    def calculate_fid(generator, real_images, num_samples):
        """
        Calcule la Fréchet Inception Distance (FID) entre les images réelles et générées.

        Paramètres :
            - real_images : Tensor ou tableau numpy contenant les images réelles.
            - num_samples : Nombre d'échantillons à utiliser pour le calcul de la FID.

        Retourne :
            - float : Score FID.

        """
        # Générer des images à l'aide du générateur entraîné
        noise = np.random.normal(size=(num_samples, generator.noise_dim))
        generated_images = generator.generator.predict(noise)


        # Chargement du modèle InceptionV3 pré-entraîné pour l'extraction des features
        inception_model = InceptionV3(include_top=False, pooling='avg', input_shape=(299, 299, 3))
        real_features = inception_model.predict(preprocess_input(real_images))
        generated_features = inception_model.predict(preprocess_input(generated_images))

        # Calcul des moyennes et matrices de covariance des features
        real_mean, real_cov = np.mean(real_features, axis=0), np.cov(real_features, rowvar=False)
        generated_mean, generated_cov = np.mean(generated_features, axis=0), np.cov(generated_features, rowvar=False)

        # Calcul de la distance FID
        diff = real_mean - generated_mean
        cov_sqrt, _ = sqrtm(real_cov.dot(generated_cov), disp=False)
        if np.iscomplexobj(cov_sqrt):
            cov_sqrt = cov_sqrt.real

        fid_score = np.dot(diff, diff) + np.trace(real_cov + generated_cov - 2 * cov_sqrt)

        return fid_score



    def calculate_inception_score(generator, num_samples):
        # Générer des images à l'aide du générateur entraîné
        noise = np.random.normal(size=(num_samples, generator.noise_dim))
        generated_images = generator.generator.predict(noise)

        # Charger le modèle InceptionV3 pré-entraîné
        inception_model = InceptionV3(include_top=False, pooling='avg', input_shape=(299, 299, 3))

        # Prétraitement des images pour le modèle InceptionV3
        generated_images = tf.image.resize(generated_images, (299, 299))
        generated_images = tf.image.grayscale_to_rgb(generated_images)
        generated_images = preprocess_input(generated_images)

        # Calculer les probabilités de chaque classe pour les images générées
        inception_scores = inception_model.predict(generated_images)
        inception_scores = tf.nn.softmax(inception_scores)

        # Calculer l'entropie croisée pour obtenir l'Inception Score
        entropy = -tf.reduce_sum(inception_scores * tf.math.log(inception_scores), axis=1)
        inception_score = tf.exp(tf.reduce_mean(entropy))
        return inception_score.numpy()


    def calculate_kid(generator, real_images, num_samples):
        # Générer des images à l'aide du générateur entraîné
        noise = np.random.normal(size=(num_samples, generator.noise_dim))
        generated_images = generator.generator.predict(noise)

        # Charger le modèle InceptionV3 pré-entraîné pour les features
        inception_model = InceptionV3(include_top=False, pooling='avg', input_shape=(299, 299, 3))

        # Prétraitement des images pour le modèle InceptionV3
        real_images = tf.image.resize(real_images, (299, 299))
        real_images = tf.image.grayscale_to_rgb(real_images)
        real_images = preprocess_input(real_images)

        generated_images = tf.image.resize(generated_images, (299, 299))
        generated_images = tf.image.grayscale_to_rgb(generated_images)
        generated_images = preprocess_input(generated_images)

        # Extraire les features pour les images réelles et générées
        real_features = inception_model.predict(real_images)
        generated_features = inception_model.predict(generated_images)

        # Calculer la distance euclidienne moyenne entre les features réelles et générées
        distances = tf.norm(real_features[:, np.newaxis] - generated_features, axis=2)
        kid = tf.reduce_mean(distances).numpy()
        return kid

    def calculate_mse(generator, real_images, num_samples):
        # Générer des images à l'aide du générateur entraîné
        noise = np.random.normal(size=(num_samples, generator.noise_dim))
        generated_images = generator.generator.predict(noise)

        # Calculer la Mean Squared Error (MSE) entre les images réelles et générées
        mse = tf.reduce_mean(tf.square(real_images - generated_images)).numpy()
        return mse

from tensorflow.keras.layers import Input, Dense, Reshape, Flatten, Conv2D, Conv2DTranspose, LeakyReLU, BatchNormalization
from tensorflow.keras.models import Model


def build_stylegan2_generator(noise_dim):
    # Ici, nous construisons une architecture simple de générateur basée sur StyleGAN2
    # pour générer des images de 28x28 en noir et blanc (pour MNIST).
    # (Ceci est une implémentation fictive et n'est pas une vraie architecture StyleGAN2).

    inputs = Input(shape=(noise_dim,))
    x = Dense(7*7*128)(inputs)
    x = LeakyReLU(alpha=0.2)(x)
    x = Reshape((7, 7, 128))(x)
    x = Conv2DTranspose(64, kernel_size=4, strides=2, padding='same')(x)
    x = LeakyReLU(alpha=0.2)(x)
    x = Conv2DTranspose(1, kernel_size=4, strides=2, padding='same', activation='sigmoid')(x)

    generator = Model(inputs, x)
    return generator

def build_stylegan2_discriminator():
    # Ici, nous construisons une architecture simple de discriminateur basée sur StyleGAN2
    # pour discriminer les images de 28x28 en noir et blanc (pour MNIST).
    # (Ceci est une implémentation fictive et n'est pas une vraie architecture StyleGAN2).

    inputs = Input(shape=(28, 28, 1))
    x = Conv2D(64, kernel_size=4, strides=2, padding='same')(inputs)
    x = LeakyReLU(alpha=0.2)(x)
    x = Conv2D(128, kernel_size=4, strides=2, padding='same')(x)
    x = LeakyReLU(alpha=0.2)(x)
    x = Flatten()(x)
    x = Dense(1)(x)

    discriminator = Model(inputs, x)
    return discriminator



# Charger le jeu de données MNIST
(train_images, _), (_, _) = tf.keras.datasets.mnist.load_data()

# Prétraitement des images
train_images = train_images.reshape(train_images.shape[0], 28, 28, 1).astype('float32')
train_images = (train_images - 127.5) / 127.5  # Mise à l'échelle des pixels entre -1 et 1

# Créer le générateur et le discriminateur
noise_dim = 100
generator = build_stylegan2_generator(noise_dim)
discriminator = build_stylegan2_discriminator()

# Créer l'instance de la classe ImageGAN avec des options avancées
gan = ImageGAN(generator, discriminator, noise_dim=noise_dim, batch_size=64, epochs=50, learning_rate=0.0002,
               verbose=True, fid_samples=1000, spectral_regularization=True, metric='inception_score',
               conditional=False, use_adamw=True)

# Entraîner le GAN sur le jeu de données MNIST
gan.fit(train_images)

# Générer des images à l'aide du générateur entraîné
n_images_to_generate = 10
generated_images = gan.generate_images(n_images_to_generate)

# Calcule la Fréchet Inception Distance (FID) entre les images réelles et générées.
fid = gan.calculate_metric(generated_images, num_samples=1000, metric='fid')

# Calculer l'Inception Score entre les images réelles et générées
inception_score = gan.calculate_metric(generated_images, num_samples=1000, metric='inception_score')

# Calculer le Kernel Inception Distance (KID) entre les images réelles et générées
kid = gan.calculate_metric(generated_images, num_samples=1000, metric='kid')

# Calculer la Mean Squared Error (MSE) entre les images réelles et générées
mse = gan.calculate_metric(generated_images, num_samples=1000, metric='mse')


# Affichage des images générées
plt.figure(figsize=(10, 5))
for i in range(n_images_to_generate):
    plt.subplot(1, n_images_to_generate, i + 1)
    plt.imshow(generated_images[i].reshape(28, 28), cmap='gray')
    plt.axis('off')
plt.suptitle("Exemples d'images générées", fontsize=16)
plt.show()


# Afficher les résultats des métriques d'évaluation entre les images réelles et générées
print(f"Fréchet Inception Distance (FID): {fid}")
print(f"Inception Score: {inception_score}")
print(f"Kernel Inception Distance (KID): {kid}")
print(f"Mean Squared Error (MSE): {mse}")

here you'll find the error displayed by google Colab during execution

---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-3-fcaa091262ee> in <cell line: 19>()
     17 
     18 # Entraîner le GAN sur le jeu de données MNIST
---> 19 gan.fit(train_images)
     20 
     21 # Générer des images à l'aide du générateur entraîné

<ipython-input-1-d5bbfe93e45e> in fit(self, real_images)
    137 
    138             for real_batch in real_images:
--> 139                 gen_loss, disc_loss = train_step(real_batch, generator_optimizer, discriminator_optimizer, loss_fn,
    140                                                  self.batch_size, self.noise_dim, self.spectral_regularization, self.discriminator)
    141                 gen_loss_avg.update_state(gen_loss)

NameError: name 'train_step' is not defined

thank you in advance for your help. i apologize in advance for the fact that the comments are in french !! because it's my mother tongue but you can translate !?

to solve the problem, I reintroduced the code from the "generate_latent_noise" and "train_step" functions into the "fit" function - it worked! but I'm not happy with it!


Solution

  • You need to pass the self argument to call a function from another function when both are under the same class. You can find out more about this from this thread.

    However, to solve your specific case, replace this:

    def train_step(real_images, generator_optimizer, discriminator_optimizer, loss_fn, batch_size, noise_dim, spectral_regularization, discriminator):
    

    with the following (I just included the self argument here):

    def train_step(self, real_images, generator_optimizer, discriminator_optimizer, loss_fn, batch_size, noise_dim, spectral_regularization, discriminator):
    

    And then in your fit function, use self.train_step instead of train_step here:

    gen_loss, disc_loss = self.train_step(real_batch, generator_optimizer, discriminator_optimizer, loss_fn,
                                                 self.batch_size, self.noise_dim, self.spectral_regularization, self.discriminator)
    

    Additional: You need to make the same changes for your generate_latent_noise function. And change these lines under train_step function:

    line 78: generated_images = generator(noise, training=True)
    line 94: gen_gradients = tape.gradient(gen_loss, generator.trainable_variables)
    line 97: generator_optimizer.apply_gradients(zip(gen_gradients, generator.trainable_variables))
    

    with:

    line 78: generated_images = self.generator(noise, training=True)
    line 94: gen_gradients = tape.gradient(gen_loss, self.generator.trainable_variables)
    line 97: generator_optimizer.apply_gradients(zip(gen_gradients, self.generator.trainable_variables))