code: # Packages
  
  import matplotlib.pyplot as plt
  import numpy as np
  import os
  import tensorflow as tf
  import tensorflow.keras.layers as tfl
  
  from tensorflow.keras.preprocessing import image_dataset_from_directory
  from tensorflow.keras.layers.experimental.preprocessing import RandomFlip, RandomRotation
  
  # Creating the dataset and splitting it into training and validation sets
  
  BATCH_SIZE = 32
  IMG_SIZE = (160, 160)
  directory = "dataset/"    # One can change this directory
  train_dataset = image_dataset_from_directory(directory,
                                               shuffle=True,
                                               batch_size=BATCH_SIZE,
                                               image_size=IMG_SIZE,
                                               validation_split=0.2,
                                               subset='training',
                                               seed=42)
  validation_dataset = image_dataset_from_directory(directory,
                                               shuffle=True,
                                               batch_size=BATCH_SIZE,
                                               image_size=IMG_SIZE,
                                               validation_split=0.2,
                                               subset='validation',
                                               seed=42)
  
  # Some of the images from the training set
  
  class_names = train_dataset.class_names
  
  plt.figure(figsize=(10, 10))
  for images, labels in train_dataset.take(1):
      for i in range(9):
          ax = plt.subplot(3, 3, i + 1)
          plt.imshow(images[i].numpy().astype("uint8"))
          plt.title(class_names[labels[i]])
          plt.axis("off")
          
  #### Preprocessing and Augmentation of the data ####
  
  """
  dataset.prefetch() is an important extra step in data preprocessing.
  Using prefetch() prevents a memory bottleneck that can occur when reading from disk.
  """
  AUTOTUNE = tf.data.experimental.AUTOTUNE
  train_dataset = train_dataset.prefetch(buffer_size=AUTOTUNE)
  
  # Function for data augmentation
  
  def data_augmenter():
      '''
      Create a Sequential model composed of 2 layers
      Returns:
          tf.keras.Sequential
      '''
      
      data_augmentation = tf.keras.Sequential()
      data_augmentation.add(RandomFlip("horizontal"))
      data_augmentation.add(RandomRotation(0.2))
      
      return data_augmentation
    
  augmenter = data_augmenter()
  
  """
  # Testing the data_augmenter() function with one image
  
  data_augmentation = data_augmenter()
  
  for image, _ in train_dataset.take(1):
      plt.figure(figsize=(10, 10))
      first_image = image[0]
      for i in range(9):
          ax = plt.subplot(3, 3, i + 1)
          augmented_image = data_augmentation(tf.expand_dims(first_image, 0))
          plt.imshow(augmented_image[0] / 255)
          plt.axis('off')
  """
  
  # Since we're using a pre-trained model that was trained on the normalization 
  # values [-1,1], it's best practice to reuse that standard.
  
  preprocess_input = tf.keras.applications.mobilenet_v2.preprocess_input
  
  # Let's try to train our base model using all the layers and weights from the pretrained model.
  
  IMG_SHAPE = IMG_SIZE + (3,)
  base_model = tf.keras.applications.MobileNetV2(input_shape=IMG_SHAPE,
                                                 include_top=True,
                                                 weights='imagenet')
  
  # One can check model summary
  
  # base_model.summary()
  
  """
  Note the last 2 layers here. They are so called top layers,
  and they are responsible of the classification in the model.
  """
  # Their names
  
  # nb_layers = len(base_model.layers)
  # print(base_model.layers[nb_layers - 2].name)
  # print(base_model.layers[nb_layers - 1].name)
  
  # choosing the first batch from the tensorflow dataset to use the images, and runing 
  # it through the MobileNetV2 base model to test out the predictions on some of our images.
  
  """
  We can see that shape of the batch is (32,1000). The number 32 refers to the batch size and 
  1000 refers to the 1000 classes the model was pretrained on.
  """
  image_batch, label_batch = next(iter(train_dataset))
  feature_batch = base_model(image_batch)
  print(feature_batch.shape)
  
  # Decoding the predictions made by the model
  
  """
  The predictions returned by the base model below follow this format:
  First the class number, then a human-readable label, and last the probability of the image 
  belonging to that class. You'll notice that there are two of these returned for each image 
  in the batch - these the top two probabilities returned for that image.
  
  There's a whole lot of labels here, some of them hilariously wrong, but none of them say "alpaca."
  This is because MobileNet pretrained over ImageNet doesn't have the correct labels for alpacas,
  so when we use the full model, all we get is a bunch of incorrectly classified images.
  
  Fortunately, you can delete the top layer, which contains all the classification labels,
  and create a new classification layer.
  """
  base_model.trainable = False
  image_var = tf.Variable(image_batch)
  pred = base_model(image_var)
  
  tf.keras.applications.mobilenet_v2.decode_predictions(pred.numpy(), top=2)
  
  # Alpaca model
  
  """
  We can use a pretrained model to modify the classifier task so that it's able to recognize alpacas.
  We can achieve this in three steps:
  1- Delete the top layer (the classification layer)
        * Set include_top in base_model as False
  2- Add a new classifier layer
        * Train only one layer by freezing the rest of the network
        * A single neuron is enough to solve a binary classification problem.
  3- Freeze the base model and train the newly-created classifier layer
        * Set base model.trainable=False to avoid changing the weights and train only the new layer
        * Set training in base_model to False to avoid keeping track of statistics in the batch norm layer
  """
  def alpaca_model(image_shape=IMG_SIZE, data_augmentation=data_augmenter()):
      ''' Define a tf.keras model for binary classification out of the MobileNetV2 model
      Arguments:
          image_shape -- Image width and height
          data_augmentation -- data augmentation function
      Returns:
      Returns:
          tf.keras.model
      '''
      
      
      input_shape = image_shape + (3,)
          
      base_model = tf.keras.applications.MobileNetV2(input_shape=input_shape,
                                                     include_top=False, # <== Important!!!!
                                                     weights='imagenet') # From imageNet
      
      # Freeze the base model by making it non trainable
      base_model.trainable = False
      
      # create the input layer (Same as the imageNetv2 input size)
      inputs = tf.keras.Input(shape=input_shape) 
      
      # apply data augmentation to the inputs
      x = data_augmentation(inputs)
      
      # data preprocessing using the same weights the model was trained on
      # Already Done -> preprocess_input = tf.keras.applications.mobilenet_v2.preprocess_input
      x = preprocess_input(x) 
      
      # set training to False to avoid keeping track of statistics in the batch norm layer
      x = base_model(x, training=False) 
      
      # Add the new Binary classification layers
      # use global avg pooling to summarize the info in each channel
      x = tfl.GlobalAveragePooling2D()(x) 
      #include dropout with probability of 0.2 to avoid overfitting
      x = tfl.Dropout(0.2)(x)
          
      # create a prediction layer with one neuron (as a classifier only needs one)
      prediction_layer = tfl.Dense(1)
          
      outputs = prediction_layer(x) 
      model = tf.keras.Model(inputs, outputs)
      
      return model
  
  data_augmentation = data_augmenter()
  model2 = alpaca_model(IMG_SIZE, data_augmentation)
  
  # Compiling the model
  
  base_learning_rate = 0.01
  model2.compile(optimizer=tf.keras.optimizers.Adam(lr=base_learning_rate),
                loss=tf.keras.losses.BinaryCrossentropy(from_logits=True),
                metrics=['accuracy'])
  
  # Running the model
  
  initial_epochs = 5
  history = model2.fit(train_dataset, validation_data=validation_dataset, epochs=initial_epochs)
  
  # Plotting the training and validation accuracy
  
  acc = [0.] + history.history['accuracy']
  val_acc = [0.] + history.history['val_accuracy']
  
  loss = history.history['loss']
  val_loss = history.history['val_loss']
  
  plt.figure(figsize=(8, 8))
  plt.subplot(2, 1, 1)
  plt.plot(acc, label='Training Accuracy')
  plt.plot(val_acc, label='Validation Accuracy')
  plt.legend(loc='lower right')
  plt.ylabel('Accuracy')
  plt.ylim([min(plt.ylim()),1])
  plt.title('Training and Validation Accuracy')
  
  plt.subplot(2, 1, 2)
  plt.plot(loss, label='Training Loss')
  plt.plot(val_loss, label='Validation Loss')
  plt.legend(loc='upper right')
  plt.ylabel('Cross Entropy')
  plt.ylim([0,1.0])
  plt.title('Training and Validation Loss')
  plt.xlabel('epoch')
  plt.show()
  
  # Printing class names
  
  print(class_names)
  
  # Fine-tunning the model to increase the accuracy
  
  base_model.trainable = True
  # Let's take a look to see how many layers are in the base model
  print("Number of layers in the base model: ", len(base_model.layers))
  
  # Fine-tune from this layer onwards
  fine_tune_at = 126
  
  ##### One Way ########
  """
  for layer in base_model.layers:
      if layer.name == 'block_16_expand':
          break
      layer.trainable = False
      print('Layer ' + layer.name + ' frozen.')
  """
  ######## Other Way ######
  # Freeze all the layers before the `fine_tune_at` layer
  for layer in base_model.layers[:fine_tune_at]:
      #print('Layer ' + layer.name + ' frozen.')
      layer.trainable = None
      
  # Define a BinaryCrossentropy loss function. Use from_logits=True
  loss_function= tf.keras.losses.BinaryCrossentropy(from_logits=True)
  # Define an Adam optimizer with a learning rate of 0.1 * base_learning_rate
  optimizer = tf.keras.optimizers.Adam(learning_rate=base_learning_rate*0.1)# 0.001
  # Use accuracy as evaluation metric
  metrics=['accuracy']
  
  model2.compile(loss=loss_function,
                optimizer = optimizer,
                metrics=metrics)
  
  fine_tune_epochs = 5
  total_epochs =  initial_epochs + fine_tune_epochs
  
  history_fine = model2.fit(train_dataset,
                           epochs=total_epochs,
                           initial_epoch=history.epoch[-1],
                           validation_data=validation_dataset)
  
  # Plotting the training and validation accuracy after fine-tunning process
  
  acc += history_fine.history['accuracy']
  val_acc += history_fine.history['val_accuracy']
  
  loss += history_fine.history['loss']
  val_loss += history_fine.history['val_loss']
  plt.figure(figsize=(8, 8))
  plt.subplot(2, 1, 1)
  plt.plot(acc, label='Training Accuracy')
  plt.plot(val_acc, label='Validation Accuracy')
  plt.ylim([0, 1])
  plt.plot([initial_epochs-1,initial_epochs-1],
            plt.ylim(), label='Start Fine Tuning')
  plt.legend(loc='lower right')
  plt.title('Training and Validation Accuracy')
  
  plt.subplot(2, 1, 2)
  plt.plot(loss, label='Training Loss')
  plt.plot(val_loss, label='Validation Loss')
  plt.ylim([0, 1.0])
  plt.plot([initial_epochs-1,initial_epochs-1],
           plt.ylim(), label='Start Fine Tuning')
  plt.legend(loc='upper right')
  plt.title('Training and Validation Loss')
  plt.xlabel('epoch')
  plt.show()
  
  """
  What we should remember:
  
    - To adapt the classifier to new data: Delete the top layer, add a new 
        classification layer, and train only on that layer
    - When freezing layers, avoid keeping track of statistics (like in the 
        batch normalization layer)
    - Fine-tune the final layers of your model to capture high-level details 
        near the end of the network and potentially improve accuracy
  """
  
apis: tensorflow.keras.optimizers.Adam matplotlib.pyplot.ylabel tensorflow.Variable matplotlib.pyplot.plot matplotlib.pyplot.figure tensorflow.keras.Sequential matplotlib.pyplot.title tensorflow.keras.layers.Dense tensorflow.keras.layers.experimental.preprocessing.RandomRotation tensorflow.keras.Input tensorflow.keras.layers.Dropout tensorflow.keras.layers.experimental.preprocessing.RandomFlip matplotlib.pyplot.axis tensorflow.keras.losses.BinaryCrossentropy matplotlib.pyplot.ylim tensorflow.keras.preprocessing.image_dataset_from_directory tensorflow.keras.layers.GlobalAveragePooling2D matplotlib.pyplot.legend tensorflow.keras.applications.MobileNetV2 tensorflow.keras.Model matplotlib.pyplot.subplot matplotlib.pyplot.show matplotlib.pyplot.xlabel