text: import torch
  from torch import nn
  torch.manual_seed(0)
  
  
  def get_generator_block(input_dim, output_dim, is_norm=False,):
      '''
      Function for returning a block of the generator's neural network
      given input and output dimensions.
      Parameters:
          input_dim: the dimension of the input vector, a scalar
          output_dim: the dimension of the output vector, a scalar
      Returns:
          a generator neural network layer, with a linear transformation 
            followed by a batch normalization and then a relu activation
      '''
      if is_norm:
        print("Applying Batch Normalization")
        model = nn.Sequential(
            nn.Linear(input_dim, output_dim),
            nn.BatchNorm1d(output_dim),
            nn.ReLU(inplace=True),
            )
      else:
        print("Without Batch Normalization")
        model = nn.Sequential(
            nn.Linear(input_dim, output_dim),
            nn.ReLU(inplace=True),
            )
      
      return model
  
  
  class LinearGenerator(nn.Module):
      '''
      Generator Class
      Values:
          z_dim: the dimension of the noise vector, a scalar
          im_dim: the dimension of the images, fitted for the dataset used, a scalar
            (MNIST images are 28 x 28 = 784 so that is your default)
          hidden_dim: the inner dimension, a scalar
      '''
      def __init__(self, z_dim=10, im_dim=784, hidden_dim=128, is_norm=False):
          super(LinearGenerator, self).__init__()
          # Build the neural network
          self.gen = nn.Sequential(
              get_generator_block(z_dim, hidden_dim,is_norm),
              get_generator_block(hidden_dim, hidden_dim * 2, is_norm),
              get_generator_block(hidden_dim * 2, hidden_dim * 4, is_norm),
              get_generator_block(hidden_dim * 4, hidden_dim * 8, is_norm),
              nn.Linear(hidden_dim * 8, im_dim),
              nn.Sigmoid()
          )
      def forward(self, noise):
          '''
          Function for completing a forward pass of the generator: Given a noise tensor, 
          returns generated images.
          Parameters:
              noise: a noise tensor with dimensions (n_samples, z_dim)
          '''
          return self.gen(noise)
      
      # Needed for grading
      def get_gen(self):
          '''
          Returns:
              the sequential model
          '''
          return self.gen
  
  
  def get_noise(n_samples, z_dim, device='cpu'):
      '''
      Function for creating noise vectors: Given the dimensions (n_samples, z_dim),
      creates a tensor of that shape filled with random numbers from the normal distribution.
      Parameters:
          n_samples: the number of samples to generate, a scalar
          z_dim: the dimension of the noise vector, a scalar
          device: the device type
      '''
      return torch.randn(n_samples,z_dim,device=device)
  
  def get_discriminator_block(input_dim, output_dim):
      '''
      Discriminator Block
      Function for returning a neural network of the discriminator given input and output dimensions.
      Parameters:
          input_dim: the dimension of the input vector, a scalar
          output_dim: the dimension of the output vector, a scalar
      Returns:
          a discriminator neural network layer, with a linear transformation 
            followed by an nn.LeakyReLU activation with negative slope of 0.2 
            (https://pytorch.org/docs/master/generated/torch.nn.LeakyReLU.html)
      '''
      return nn.Sequential(
           nn.Linear(input_dim, output_dim), #Layer 1
           nn.LeakyReLU(0.2, inplace=True)
      )
  
  class LinearDiscriminator(nn.Module):
      '''
      Discriminator Class
      Values:
          im_dim: the dimension of the images, fitted for the dataset used, a scalar
              (MNIST images are 28x28 = 784 so that is your default)
          hidden_dim: the inner dimension, a scalar
      '''
      def __init__(self, im_dim=784, hidden_dim=128):
          super(LinearDiscriminator, self).__init__()
          self.disc = nn.Sequential(
              get_discriminator_block(im_dim, hidden_dim * 4),
              get_discriminator_block(hidden_dim * 4, hidden_dim * 2),
              get_discriminator_block(hidden_dim * 2, hidden_dim),
              nn.Linear(hidden_dim, 1)
          )
  
      def forward(self, image):
          '''
          Function for completing a forward pass of the discriminator: Given an image tensor, 
          returns a 1-dimension tensor representing fake/real.
          Parameters:
              image: a flattened image tensor with dimension (im_dim)
          '''
          return self.disc(image)
      
      # Needed for grading
      def get_disc(self):
          '''
          Returns:
              the sequential model
          '''
          return self.disc
  
  
  def linear_disc_loss(gen, disc, criterion, real, num_images, z_dim, device):
      '''
      Return the loss of the discriminator given inputs.
      Parameters:
          gen: the generator model, which returns an image given z-dimensional noise
          disc: the discriminator model, which returns a single-dimensional prediction of real/fake
          criterion: the loss function, which should be used to compare 
                 the discriminator's predictions to the ground truth reality of the images 
                 (e.g. fake = 0, real = 1)
          real: a batch of real images
          num_images: the number of images the generator should produce, 
                  which is also the length of the real images
          z_dim: the dimension of the noise vector, a scalar
          device: the device type
      Returns:
          disc_loss: a torch scalar loss value for the current batch
      '''
  
      fake_noise = get_noise(num_images, z_dim, device=device)
      fake = gen(fake_noise)
      disc_fake_pred = disc(fake.detach())
      disc_fake_loss = criterion(disc_fake_pred, torch.zeros_like(disc_fake_pred))
      disc_real_pred = disc(real)
      disc_real_loss = criterion(disc_real_pred, torch.ones_like(disc_real_pred))
      disc_loss = (disc_fake_loss + disc_real_loss) / 2
      return disc_loss
  
  
  def linear_gen_loss(gen, disc, criterion, num_images, z_dim, device):
      '''
      Return the loss of the generator given inputs.
      Parameters:
          gen: the generator model, which returns an image given z-dimensional noise
          disc: the discriminator model, which returns a single-dimensional prediction of real/fake
          criterion: the loss function, which should be used to compare 
                 the discriminator's predictions to the ground truth reality of the images 
                 (e.g. fake = 0, real = 1)
          num_images: the number of images the generator should produce, 
                  which is also the length of the real images
          z_dim: the dimension of the noise vector, a scalar
          device: the device type
      Returns:
          gen_loss: a torch scalar loss value for the current batch
      '''
      
      fake_noise = get_noise(num_images, z_dim, device=device)
      fake = gen(fake_noise)
      disc_fake_pred = disc(fake)
      gen_loss = criterion(disc_fake_pred, torch.ones_like(disc_fake_pred))
      return gen_loss
  
  
  
  
  
  