<jupyter_start><jupyter_text><jupyter_code>%tensorflow_version 2.x 
import tensorflow as tf<jupyter_output><empty_output><jupyter_text>PremessaUtilizzeremo lo stesso training loop e struttura del codice definita nel notebook precedente (https://bit.ly/2lno7O5), ma metteremo a confronto le performance della rete di classificazione implementata mediante layer completamente connnessi e quella implementata con layer convoluzionali. Fully Connected cv CNN per la classificazione di immaginiRiprendiamo la struttura della pipeline di ML definita nel precedente notebook:- Ottenere ed Analizzare i dati- Definire la pipeline di input- Definire il modello- Definire le metriche- Definire il training loop- Allenare il modello e misurare le metriche durante ed alla fine di ogni epoca- Selezionare il modello migliore (basandosi sulla metrica di validation)- Misurare le performance sullo split di testAbbiamo già implementato tutti i punti qui descritti, ma basandoci sull'idea che il modello da definire ed utilizzare fosse fully connected.Per cui abbiamo:- definito la pipeline di input per produrre immagini "flat" (`32*32*3`)- definito il modello di classificazione usando solo layer FCPer poter utilizzare un modello basato su layer CNN, dobbiamo modificare:- la pipeline: per produrre immagini, quindi tensori `(32,32,3)`- il modello: deve essere in grado di accettare immagini grandi almeno `(32,32,3)`Riprendiamo **tutto** il codice definito nel precedente notebook, e lo utilizziamo come base di partenza per **mettere a confronto** le due soluzioni (FC vs CNN). La pipeline di inputUtilizziamo nuovamente il dataset Cifar10, quindi installiamo TensorFlow Datasets e riutilizziamo il codice già scritto.<jupyter_code>!pip install --upgrade tensorflow_datasets<jupyter_output><empty_output><jupyter_text>Ora, senza dilungarci oltre, otteniamo il dataset e definitiamo la funzione `transform` da mappare agli elementi del dataset, in modo tale da creare **l'input per il modello fc**.<jupyter_code>import tensorflow_datasets as tfds
data, info = tfds.load("cifar10", with_info=True, split=tfds.Split.ALL)
def transform(row):
  # trasformare i dati da uint a float
  row["image"] = tf.image.convert_image_dtype(row["image"], dtype=tf.float32)
  # 1-hot
  row["label"] = tf.one_hot(row["label"], depth=10, on_value=1, off_value=0)
  # [-1,1] range
  row["image"] = (row["image"] - 0.5) * 2.
  # flatten
  row["image"] = tf.reshape(row["image"], (-1,))
  return row

# Input for the fully connected model
dataset_fc = data.map(transform)

# split, batch, prefetch (FC)
train_fc = dataset_fc.take(50000).batch(32).prefetch(1)
validation_fc = dataset_fc.skip(50000).take(5000).batch(32).prefetch(1)
test_fc = dataset_fc.skip(50000 + 5000).take(5000).batch(32).prefetch(1)<jupyter_output><empty_output><jupyter_text>Definiamo ora **l'input per il modello conovluzionale**.Anche in questo caso, vogliamo effettuare la stessa trasformazione sulle label (codifica one-hot) e lo stesso scaling nel range [-1,1] per i valori di input dell'immagine.Possiamo quindi riutilizzare l'oggetto `dataset_fc`, cambiando semplicemente la forma da `(32*32*3)` all'originaria `(32,32,3`).<jupyter_code>def undo_flattening(row):
  row["image"] = tf.reshape(row["image"], (32,32,3))
  return row

# Input for the convolutional model
dataset_cnn = dataset_fc.map(undo_flattening)

# split, batch, prefetch (FC)
train_cnn = dataset_cnn.take(50000).batch(32).prefetch(1)
validation_cnn = dataset_cnn.skip(50000).take(5000).batch(32).prefetch(1)
test_cnn = dataset_cnn.skip(50000 + 5000).take(5000).batch(32).prefetch(1)<jupyter_output><empty_output><jupyter_text>Definizione modelliRi-utilizziamo il modello completamente connesso precedentemente definito, e definiamo un modello convoluzionale in grado di accettare immagini `32x32x3` in input. Modello Completamente Connesso<jupyter_code>inputs = tf.keras.layers.Input(shape=(32*32*3))
net = tf.keras.layers.Dense(512, activation=tf.nn.relu)(inputs)
net = tf.keras.layers.Dense(256, activation=tf.nn.relu)(net)
net = tf.keras.layers.Dense(128, activation=tf.nn.relu)(net)
out = tf.keras.layers.Dense(10)(net)
model_fc = tf.keras.Model(inputs=inputs, outputs=out)
model_fc.summary()<jupyter_output><empty_output><jupyter_text>Definizione Rete Neurale ConvoluzionaleLa struttura della rete, esattamente come per il caso FC, è arbitraria.Dato che il nostro obiettivo è quello di confrontare le performance (in termini di numero di parametri e metriche misurate) dei due modelli, cerchiamo di definire la CNN in modo "simile" alla rete FC.<jupyter_code># Begin definition: feature extractor

inputs = tf.keras.layers.Input(shape=(32,32,3))
net = tf.keras.layers.Conv2D(32, (5,5), strides=(2,2), padding='same', activation=tf.nn.relu)(inputs)
# padding = same -> output side = input_side / stride = 16
# output shape = (16,16,32)
net = tf.keras.layers.Conv2D(64, (5,5), strides=(2,2), padding='same', activation=tf.nn.relu)(net)
# output shape = (8,8,64)
net = tf.keras.layers.Conv2D(128, (5,5), strides=(2,2), padding='same', activation=tf.nn.relu)(net)
# output size = (4, 4, 128)

# Classification layer: flatten the (4,4,128) tensor in a (4*4*128) tensor
net = tf.keras.layers.Flatten()(net)
# End definition: feature extractor

# Classification layer
out = tf.keras.layers.Dense(10)(net)

# building the whole model
model_cnn = tf.keras.Model(inputs=inputs, outputs=out)
model_cnn.summary()<jupyter_output><empty_output><jupyter_text>DifferenzeIl numero di parametri della rete FC è 1,738,890 mentre la CNN ha solo 279,114 parametri.La rete neurale convoluzionale ha un numero di parametri di \~6 volte inferiore, il ché significa **\~600%** di parametri in meno.Il numero di parametri della CNN aumenta all'aumentare della profondita, ma aumenta solo perché abbiamo arbitrariamente deciso di mettere più parametri apprendibili (numero di filtri) nei layer "deep" della rete.La rete completamente connessa, invece, ha un numero di parametri che diminuisce layer dopo layer (perché abbiamo definito l'archiettura in questo modo), ma **il solo layer di input** ha più parametri di tutta la CNN. Definizione e riuso di oggetti KerasLa CNN è in tutto e per tutto un classificatore, quindi possiamo riutilizzare la categorical cross-entropy loss.Possiamo quindi definire un oggetto callable (una Keras loss) e ritutilizzarla per il train di entrambi i modelli. Alla fine, la keras loss altro non fa che mettere in relazione l'output prodotto dalla rete e la predizione attesa; non avendo alcuno stato al suo interno possiamo utilizzarla senza alcun problema per il train di due modelli.Lo stesso ragionamento si può applicare anche all'ottimizzatore (solo finché utilizziamo SGD ed altri ottimizzatori senza variabili), deve solo applicare la regola di aggiornamento e non ha alcuno stato.È invece **sbagliato** riutilizzare la stessa `tf.GradientTape` dato che questa tiene traccia di quanto accade all'interno dello step di train ed il suo contenuto **viene distrutto** nel momento in cui viene invocato il metodo `.gradient`.Ed è **sbagliato** anche riutilizzare gli stessi oggetti `tf.keras.metric` in quanto anch'essi dotati di uno stato (e lo stato e relativo alle performance dello specifico modello).<jupyter_code># Loss is a callable object
loss = tf.keras.losses.CategoricalCrossentropy(from_logits=True)

# Metrics
accuracy_cnn = tf.keras.metrics.Accuracy()
mean_loss_cnn = tf.keras.metrics.Mean(name="loss")

accuracy_fc = tf.keras.metrics.Accuracy()
mean_loss_fc = tf.keras.metrics.Mean(name="loss")

# Define the optimizer
optimizer = tf.keras.optimizers.SGD(learning_rate=1e-2)

def compute_loss(input_samples, model):
    predictions = model(input_samples["image"])
    loss_value = loss(input_samples["label"], predictions)
    return loss_value

@tf.function
def train_step(input_samples, model):

  with tf.GradientTape() as tape:
    loss_value = compute_loss(input_samples, model)

  gradient = tape.gradient(loss_value, model.trainable_variables)
  optimizer.apply_gradients(zip(gradient, model.trainable_variables))

  return loss_value

def measure_metrics(input_samples, model, id):
  predicted_labels = tf.argmax(model(input_samples["image"]), axis=1)
  if id == "cnn":
      accuracy_cnn.update_state(tf.argmax(input_samples["label"], axis=1), predicted_labels)
      mean_loss_cnn.update_state(compute_loss(input_samples, model))
  else:
    accuracy_fc.update_state(tf.argmax(input_samples["label"], axis=1), predicted_labels)
    mean_loss_fc.update_state(compute_loss(input_samples, model))<jupyter_output><empty_output><jupyter_text>LoggingDato che vogliamo confrontare le performance del modello FC e del modello CNN, è necessario creare il corretto numero di `FileWriter' ed usarli correttamente.Dato che TensorBoard utilizza la struttura delle cartelle per creare curve differenti sullo stesso grafico, possiamo definire sei diversi writer nelle directory corrette:<jupyter_code># FC writers
train_writer_fc = tf.summary.create_file_writer("logs/train/fc")
validation_writer_fc = tf.summary.create_file_writer("logs/validation/fc")
test_writer_fc = tf.summary.create_file_writer("logs/test/fc")

# CNN writers
train_writer_cnn = tf.summary.create_file_writer("logs/train/cnn")
validation_writer_cnn = tf.summary.create_file_writer("logs/validation/cnn")
test_writer_cnn = tf.summary.create_file_writer("logs/test/cnn")<jupyter_output><empty_output><jupyter_text>Training loopSiamo ora pronti per definire il training loop.La funzione accetterà il modello, il dataset, e l'ID ("cnn" o "fc") corretto ed allenerà il modello per il numero desiderato di epoche.Dato che vogliamo fare due train distinti, ma plottarli sullo stesso grafico come se fossero stati eseguiti in parallelo, dobbiamo ricordarci di azzerare la variable `global_step` e `epoch_counter` al termine del primo training.<jupyter_code>global_step = tf.Variable(0, dtype=tf.int64, trainable=False)
epoch_counter = tf.Variable(0, dtype=tf.int64, trainable=False)

def train_loop(num_epochs, model, dataset, id):
  if id == "cnn":
    mean_loss = mean_loss_cnn
    accuracy = accuracy_cnn
    train_writer = train_writer_cnn
  else:
    mean_loss = mean_loss_fc
    accuracy = accuracy_fc
    train_writer = train_writer_fc

  # Loop
  for epoch in tf.range(epoch_counter, num_epochs):
    for input_samples in dataset:
      loss_value = train_step(input_samples, model)
      measure_metrics(input_samples, model, id)
      global_step.assign_add(1)

      if tf.equal(tf.math.mod(global_step, 100), 0):
        mean_loss_value = mean_loss.result() 
        accuracy_value = accuracy.result()
        mean_loss.reset_states()
        accuracy.reset_states()
        tf.print(f"[{global_step.numpy()}] loss value: ", mean_loss_value," - train acc: ", accuracy_value)
        with train_writer.as_default():
          tf.summary.scalar("loss", mean_loss_value, step=global_step)
          tf.summary.scalar("accuracy", accuracy_value, step=global_step)
          tf.summary.image("images", tf.reshape(input_samples["image"], (-1, 32,32,3)), step=global_step, max_outputs=5)
    # end of epoch: measure performance on validation set and log the values on tensorboard
    tf.print(f"Epoch {epoch.numpy() + 1 } completed")
    epoch_counter.assign(epoch + 1)
    # TODO: insert validation code here<jupyter_output><empty_output><jupyter_text>Tensorboard e trainingLanciamo tensorboard e subito dopo invochiamo la funzione di train: prima sul modello fc, poi resettiamo le due variabili globali, e infine alleniamo il modello cnn.<jupyter_code>%load_ext tensorboard
%tensorboard --logdir logs
global_step.assign(0)
epoch_counter.assign(0)
train_loop(num_epochs=5, model=model_fc, dataset=train_fc, id="fc")

print("#### END FC MODEL TRAINING ####")
global_step.assign(0)
epoch_counter.assign(0)
train_loop(num_epochs=5, model=model_cnn, dataset=train_cnn, id="cnn")
print("#### END CNN MODEL TRAINING ####")<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.09865283239896243, 'fraction_numerical': 0.02091875156890637, 'mean_word_length': 5.920671685002895, 'pattern_counts': {'":': 2, '<': 41, '<?xml version=': 0, '>': 42, 'https://': 1, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '10', 'dir': 'jupyter-structured-clean-dedup', 'id': '138304', 'license': 'CC-BY-4.0', 'n_tokens_mistral': 4170, 'n_tokens_neox': 4026, 'n_words': 1142, 'path': 'Introduzione_alla_reti_neurali_convoluzionali.ipynb', 'repo_name': 'galeone/italian-machine-learning-course', 'size': '11951'}