Vai al contenuto principale

Implementazione del Neural Style Transfer con TensorFlow 2.0

In questo tutorial imparerai a implementare applicazioni potenti come Prisma utilizzando TensorFlow 2.0.
· 11 min leggi

Scopri con l'IA

Le reti neurali convoluzionali sono la tipologia più potente di reti neurali per la classificazione e l’analisi delle immagini. Le loro applicazioni hanno superato molti limiti e si sono rivelate l’elemento cruciale di molte applicazioni di deep learning che vediamo oggi. A un livello molto alto, le CNN possono apprendere rappresentazioni interne a livello di feature delle immagini a loro fornite. È questo a renderle così potenti. Risulta che questa caratteristica saliente delle CNN non è utile solo per compiti come la classificazione delle immagini, ma anche per la costruzione di immagini. Applicazioni come Deep Dream e Neural Style Transfer compongono immagini in base alle attivazioni dei layer all’interno delle CNN e alle feature da esse estratte.

In questo tutorial studierai come funziona il Neural Style Transfer e come può essere implementato usando TensorFlow 2.0.

Nota: per seguire questo tutorial, è necessario sapere come funzionano le CNN. Se cerchi risorse per un rapido ripasso sulle CNN, prova le seguenti:

Che cos’è il Neural Style Transfer?

Per chi è alle prime armi nel campo della computer vision e del deep learning, il termine neural style transfer può risultare un po’ intimidatorio. Per comprendere ogni componente del termine, considera le due immagini seguenti:

neural style transfer 1

Nel contesto del neural style transfer, l’immagine a sinistra è detta immagine di contenuto, mentre quella a destra è detta immagine di stile. L’obiettivo è stilizzare una delle immagini (in questo caso quella a sinistra) usando l’altra (quella a destra). Questo compone le ultime due parole del termine: style transfer. Per eseguire il processo, una rete neurale (CNN) viene addestrata a ottimizzare una funzione di loss personalizzata, da cui la prima parola: neural. Quando le due immagini sopra vengono fuse tramite neural style transfer, il risultato finale appare così (a destra) -

neural style transfer 2

Potresti chiederti a cosa serva il neural style transfer. Immagina di avere l’immagine di un disegno originariamente realizzato da Vincent van Gogh. Vuoi vedere come quel disegno si tradurrebbe in uno dei tuoi. Questa è una delle applicazioni in cui il neural style transfer trova impiego. Un altro esempio sono diverse app di filtri fotografici come Prisma, che ti permettono di eseguire il neural style transfer con un’interfaccia fluida.

Separazione tra contenuto e stile

Finora, l’idea che potresti aver colto sul neural style transfer è che il processo consiste nel combinare il contenuto di un’immagine con lo stile di un’altra. In effetti, è esattamente così. In questa sezione, cerchiamo di capire cosa si intende per contenuto e stile nel contesto delle CNN.

Una CNN è spesso un insieme di diversi strati convoluzionali e di pooling. I layer convoluzionali sono responsabili dell’estrazione di feature altamente complesse da un’immagine, mentre i layer di pooling scartano informazioni spaziali dettagliate che non sono rilevanti per un problema di classificazione. L’effetto è che questo aiuta la CNN a imparare il contenuto di una data immagine piuttosto che elementi specifici come colore, texture e così via. Man mano che si va in profondità in una CNN, la complessità delle feature aumenta e i layer convoluzionali più profondi sono spesso indicati come rappresentazioni del contenuto.

Un esempio di stile è qualsiasi aspetto specifico delle proprietà dell’immagine come texture, colore e simili, ad esempio l’uso evidente di un determinato colore in un disegno. Ora la domanda è: come si estrae lo stile di un’immagine? Questo si fa calcolando le correlazioni tra i layer convoluzionali. Le correlazioni ci danno una misura di quanto siano simili/collegate due o più variabili. Per capirlo, considera un layer convoluzionale addestrato composto da diverse mappe di attivazione (feature map). Per ogni feature map puoi misurare quanto fortemente le feature da essa rilevate si relazionano con le altre feature map nello stesso layer. Questo ti fornisce una stima di aspetti come: un certo colore rilevato nella prima feature map è simile a un colore in un’altra mappa? Ci sono forme comuni tra le feature map? Questi tratti/somiglianze definiscono lo stile di un’immagine. Il processo di misurazione della somiglianza tra i contenuti di diverse feature map all’interno di un layer convoluzionale aiuta le reti a imparare una rappresentazione multi-scala dell’immagine che mette a fuoco caratteristiche spaziali come texture e colore.

Un punto fondamentale da tenere a mente è che, durante l’applicazione del neural style transfer, devi anche assicurarti che il contenuto di un’immagine venga preservato insieme allo stile desiderato dell’altra immagine. Vedrai come si fa in una sezione successiva. Nella prossima sezione imparerai come estrarre le feature da un’immagine (immagine di contenuto) e calcolare la content loss.

Quantificare l’immagine di contenuto e calcolare la Content Loss

L’algoritmo di neural style transfer è stato introdotto per la prima volta da Gatys et al. nel loro paper del 2015, A Neural Algorithm of Artistic Style. Questo tutorial fa però riferimento a Image Style Transfer Using Convolutional Neural Networks, che è una sorta di continuazione del paper precedente.

Secondo il paper Image Style Transfer Using Convolutional Neural Networks, viene impiegata un’architettura CNN VGG-19 per estrarre sia le feature di contenuto sia quelle di stile rispettivamente dalle immagini di contenuto e di stile. Per ottenere le feature di contenuto, si usa il secondo layer convoluzionale del quarto blocco (di layer convoluzionali). Per comodità, gli autori lo hanno chiamato conv4_2. Una volta ottenute le feature di contenuto, dovrai confrontarle con un’immagine target per misurare la content loss. Cos’è un’immagine target? Perché qui è necessario calcolare la content loss? Facciamo un passo indietro e concentriamoci su queste due domande.

Per combinare le feature di contenuto e di stile in un’unica immagine, dovrai partire da un’immagine target, che può essere una semplice immagine vuota o una copia dell’immagine di contenuto. Ora, per apprendere efficacemente sia le feature di contenuto sia quelle di stile usando una CNN, ti serve una funzione di loss personalizzata, che ottimizzerai per ottenere un’immagine stilizzata fluida costruita a partire dalle immagini di contenuto e di stile. Questa funzione di loss personalizzata è essenzialmente un’unione di due loss differenti:

  • Content loss, che fa sì che la quantità complessiva di contenuto venga preservata.
  • Style loss, che regola la quantità di stile trasferita all’immagine target.

Ora che hai un’idea delle domande di cui sopra, torniamo alla content loss e definiamola. La content loss è definita come segue -

dove $T_c$ si riferisce all’immagine target e $C_c$ all’immagine di contenuto. Quindi, ciò che fa questa funzione di loss è darti una misura di quanto distano tra loro le feature delle immagini di contenuto e target. La rete punterà a minimizzare questa loss. Nella prossima sezione vedrai come determinare la style loss.

La Style Loss e la matrice di Gram

Per determinare la style loss, il paper indica di prendere le rappresentazioni (in sostanza numeri) dai seguenti layer e ottenere le matrici di Gram delle feature map all’interno di questi layer.

'conv1_1'
'conv2_1'
'conv3_1'
'conv4_1'
'conv5_1'

Il vantaggio di utilizzare numerosi layer per definire la style loss è che consente di apprendere una rappresentazione multi-scala dello stile contenuto in un’immagine. Le matrici di Gram aiutano a determinare quanto siano simili le feature tra le feature map all’interno di un singolo layer convoluzionale. Questo riflette ulteriormente informazioni non localizzate sull’immagine, che ne plasmano lo stile. Una matrice di Gram in questo contesto si calcola nel modo seguente.

Considera che la dimensione di un’immagine convoluta sia 8x8x16, il che indica principalmente che ha 16 feature map. Ora ti interessa trovare le somiglianze tra le feature in queste feature map usando una matrice di Gram. Per farlo, appiattisci le prime due dimensioni dell’immagine convoluta e trasformale in un vettore 1D. Le dimensioni, se appiattite in questo caso, conterrebbero 64 elementi nel vettore 1D. Questo si ripete per tutte le feature map. Ottieni così una matrice finale di dimensione 16x64 (chiamiamola matrice A). Questa matrice viene poi trasposta (diventa 64x16) e moltiplicata per la matrice A. Una matrice di Gram, in questo caso, avrà dimensione 16x16. Un particolare valore nella matrice di Gram indica le somiglianze tra le feature map. Visivamente, questo processo assomiglia al seguente:

style loss and gram matrix

Per calcolare la style loss tra l’immagine target e quella di stile, si prendono le distanze quadratiche medie tra le loro matrici di Gram a ciascuno dei blocchi di layer convoluzionali. Formalmente, questo può essere definito come segue:

dove T(s,i) è la matrice di Gram dell’immagine target calcolata al blocco i e S(s,i) è la matrice di Gram dell’immagine di stile calcolata al blocco i. Con wi puoi assegnare pesi personalizzati ai diversi blocchi convoluzionali per ottenere una rappresentazione dettagliata dello stile. Infine, $a$ è una costante che tiene conto dei valori in ciascun layer all’interno dei blocchi. Mettiamo insieme le due loss per definire la loss totale che la rete ottimizza nel processo di neural style transfer.

Definire la Loss Totale

La loss totale ora è solo una questione di somma con pesi personalizzati -

dove $\alpha$ indica il peso del contenuto e $\beta$ indica il peso dello stile. Questo ti aiuta a mantenere un buon equilibrio tra il contenuto e la quantità di stile trasferita all’immagine target. L’effetto di diverse combinazioni di $\alpha$ e $\beta$ è mostrato nel paper ed è consigliabile darci un’occhiata. Ora hai tutti i pezzi per implementare il neural style transfer. Procediamo nella prossima sezione.

Neural Style Transfer in pratica

Per la parte di implementazione userai TensorFlow 2.0. Include molte aggiunte straordinarie ed è uno degli aggiornamenti più completi della libreria fino a oggi. Se ti interessa conoscerne alcune, puoi leggere questo articolo.

L’ultima versione di TensorFlow (al momento della stesura di questo tutorial) è 2.0.0-beta0. Se non l’hai ancora installata, procedi prima all’installazione seguendo le istruzioni riportate qui. Inizierai importando i pacchetti necessari e le immagini di contenuto e di stile.

# Packages
import tensorflow as tf
from tensorflow.keras.applications.vgg19 import preprocess_input
from tensorflow.keras.models import Model
import matplotlib.pyplot as plt
import numpy as np
np.random.seed(7)

%matplotlib inline

print(tf.__version__)
2.0.0-beta0
# Load the content and style images
content = plt.imread('Content.jpeg')
style = plt.imread('Style.jpg')

# Display the images
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(20, 10))

# Content and style images side-by-side
ax1.imshow(content)
ax1.set_title('Content Image')
ax2.imshow(style)
ax2.set_title('Style Image')
plt.show()
immagini di contenuto e stile

Nel prossimo passaggio, scriverai una funzione di supporto che caricherà le due immagini in array di numeri (perché i computer capiscono solo numeri) e le ridimensionerà per renderle compatibili con il modello.

def load_image(image):
  image = plt.imread(image)
  img = tf.image.convert_image_dtype(image, tf.float32)
  img = tf.image.resize(img, [400, 400])
  # Shape -> (batch_size, h, w, d)
  img = img[tf.newaxis, :]
  return img
# Use load_image of content and style images
content = load_image('Content.jpeg')
style = load_image('Style.jpg')

# Verify the shapes
content.shape, style.shape
(TensorShape([1, 400, 400, 3]), TensorShape([1, 400, 400, 3]))

Le immagini sono state ridimensionate. Ora caricherai un modello VGG19 pre-addestrato per estrarre le feature. Poiché userai il modello per l’estrazione di feature, non avrai bisogno della parte di classificazione del modello.

vgg = tf.keras.applications.VGG19(include_top=False, weights='imagenet')
vgg.trainable = False
Downloading data from https://github.com/fchollet/deep-learning-models/releases/download/v0.1/vgg19_weights_tf_dim_ordering_tf_kernels_notop.h5
80142336/80134624 [==============================] - 5s 0us/step

Gli autori hanno specificato i nomi dei blocchi usati per estrarre le feature di contenuto e di stile. Il modello VGG19 che hai appena caricato adotta la stessa convenzione di denominazione. Per sfruttare questo, per prima cosa stamperai i nomi di tutti i layer presenti nella rete.

# Print the layer names for convenience
for layer in vgg.layers:
  print(layer.name)
input_1
block1_conv1
block1_conv2
block1_pool
block2_conv1
block2_conv2
block2_pool
block3_conv1
block3_conv2
block3_conv3
block3_conv4
block3_pool
block4_conv1
block4_conv2
block4_conv3
block4_conv4
block4_pool
block5_conv1
block5_conv2
block5_conv3
block5_conv4
block5_pool

Ti interessano però i seguenti per ottenere le feature di stile:

'conv1_1'
'conv2_1'
'conv3_1'
'conv4_1'
'conv5_1'

Per le feature di contenuto, ti servirà conv4_2. Le salverai in variabili di conseguenza.

# Content layer
content_layers = ['block4_conv2']

# Style layer
style_layers = ['block1_conv1',
                'block2_conv1',
                'block3_conv1',
                'block4_conv1',
                'block5_conv1']


num_content_layers = len(content_layers)
num_style_layers = len(style_layers)

Ora crea un modello VGG personalizzato composto dai layer specificati. Questo ti aiuterà a eseguire forward pass sulle immagini ed estrarre le feature necessarie lungo il percorso.

def mini_model(layer_names, model):

  outputs = [model.get_layer(name).output for name in layer_names]

  model = Model([vgg.input], outputs)
  return model

Definire una matrice di Gram è semplicissimo in TensorFlow e puoi farlo così:

# Gram matrix
def gram_matrix(tensor):
  temp = tensor
  temp = tf.squeeze(temp)
  fun = tf.reshape(temp, [temp.shape[2], temp.shape[0]*temp.shape[1]])
  result = tf.matmul(temp, temp, transpose_b=True)
  gram = tf.expand_dims(result, axis=0)
  return gram

Ora definirai un modello personalizzato usando la funzione mini_model(). Servirà a restituire le feature di contenuto e di stile dalle rispettive immagini.

 class Custom_Style_Model(tf.keras.models.Model):
  def __init__(self, style_layers, content_layers):
    super(Custom_Style_Model, self).__init__()
    self.vgg =  mini_model(style_layers + content_layers, vgg)
    self.style_layers = style_layers
    self.content_layers = content_layers
    self.num_style_layers = len(style_layers)
    self.vgg.trainable = False

  def call(self, inputs):
    # Scale back the pixel values
    inputs = inputs*255.0
    # Preprocess them with respect to VGG19 stats
    preprocessed_input = preprocess_input(inputs)
    # Pass through the mini network
    outputs = self.vgg(preprocessed_input)
    # Segregate the style and content representations
    style_outputs, content_outputs = (outputs[:self.num_style_layers],
                                      outputs[self.num_style_layers:])

    # Calculate the gram matrix for each layer
    style_outputs = [gram_matrix(style_output)
                     for style_output in style_outputs]

    # Assign the content representation and gram matrix in
    # a layer by layer fashion in dicts
    content_dict = {content_name:value
                    for content_name, value
                    in zip(self.content_layers, content_outputs)}

    style_dict = {style_name:value
                  for style_name, value
                  in zip(self.style_layers, style_outputs)}

    return {'content':content_dict, 'style':style_dict}

Ora che hai definito il modello personalizzato, usiamolo sulle immagini per ottenere rispettivamente le feature di contenuto e di stile -

# Note that the content and style images are loaded in
# content and style variables respectively
extractor = Custom_Style_Model(style_layers, content_layers)
style_targets = extractor(style)['style']
content_targets = extractor(content)['content']


In the paper, optimization was done using the L-BFGS algorithm, but you can use Adam also.
opt = tf.optimizers.Adam(learning_rate=0.02)

Definiamo ora i pesi complessivi di contenuto e stile e anche i pesi per ciascuna rappresentazione di stile come discusso in precedenza. Nota che si tratta di iperparametri su cui vale la pena sperimentare.

 # Custom weights for style and content updates
style_weight=100
content_weight=10

# Custom weights for different style layers
style_weights = {'block1_conv1': 1.,
                 'block2_conv1': 0.8,
                 'block3_conv1': 0.5,
                 'block4_conv1': 0.3,
                 'block5_conv1': 0.1}

Arriva ora la parte più cruciale, che rende il processo di neural style transfer molto più interessante: la funzione di loss.

# The loss function to optimize
def total_loss(outputs):
    style_outputs = outputs['style']
    content_outputs = outputs['content']
    style_loss = tf.add_n([style_weights[name]*tf.reduce_mean((style_outputs[name]-style_targets[name])**2)
                           for name in style_outputs.keys()])
    # Normalize
    style_loss *= style_weight / num_style_layers

    content_loss = tf.add_n([tf.reduce_mean((content_outputs[name]-content_targets[name])**2)
                             for name in content_outputs.keys()])
    # Normalize
    content_loss *= content_weight / num_content_layers
    loss = style_loss + content_loss
    return loss

Ora scriverai un’altra funzione che:

  • Calcola i gradienti della funzione di loss appena definita.
  • Usa questi gradienti per aggiornare l’immagine target.

Con GradientTape puoi sfruttare l’autodifferenziazione, che consente di calcolare i gradienti di una funzione in base alla sua composizione. Userai anche il decorator tf.function per velocizzare le operazioni. Leggi di più qui.

@tf.function()
def train_step(image):
  with tf.GradientTape() as tape:
    # Extract the features
    outputs = extractor(image)
    # Calculate the loss
    loss = total_loss(outputs)
  # Determine the gradients of the loss function w.r.t the image pixels
  grad = tape.gradient(loss, image)
  # Update the pixels
  opt.apply_gradients([(grad, image)])
  # Clip the pixel values that fall outside the range of [0,1]
  image.assign(tf.clip_by_value(image, clip_value_min=0.0, clip_value_max=1.0))

L’unico passo rimanente prima di addestrare la rete è definire l’immagine target. Per l’immagine target userai proprio l’immagine di contenuto.

target_image = tf.Variable(content)

Ora sei assolutamente pronto per addestrare la rete.

epochs = 10
steps_per_epoch = 100

step = 0
for n in range(epochs):
  for m in range(steps_per_epoch):
    step += 1
    train_step(target_image)
  plt.imshow(np.squeeze(target_image.read_value(), 0))
  plt.title("Train step: {}".format(step))
  plt.show()
WARNING: Logging before flag parsing goes to stderr.
W0617 16:21:34.491543 140709216896896 deprecation.py:323] From /usr/local/lib/python3.6/dist-packages/tensorflow/python/ops/math_grad.py:1205: add_dispatch_support.<locals>.wrapper (from tensorflow.python.ops.array_ops) is deprecated and will be removed in a future version.
Instructions for updating:
Use tf.where in 2.0, which has the same broadcast rule as np.where 

Ti consigliamo di sperimentare con i pesi di contenuto e di stile per osservare i cambiamenti nell’immagine target.

Considerazioni finali

Il paper a cui abbiamo fatto riferimento qui nel tutorial non menziona un’ulteriore ottimizzazione della qualità dell’immagine costruita. Come probabilmente hai osservato nei risultati sopra, serve una certa morbidezza nella relazione spaziale tra contenuto complessivo e stile dell’immagine ottenuta. Per affrontare questo problema è stata introdotta la total_variantional_loss, analoga all’uso della regolarizzazione. Ad alto livello, la total_variational_loss penalizza gli artefatti ad alta frequenza introdotti dall’algoritmo originale di neural style transfer. Dai un’occhiata a questo tutorial se ti interessa implementarla. Di seguito alcune delle risorse a cui si è fatto riferimento per la stesura di questo tutorial:

Se vuoi affinare le tue competenze di deep learning, potresti consultare i seguenti corsi e tutorial di DataCamp:

Argomenti

Correlato