Accéder au contenu principal

Implémenter le transfert de style neuronal avec TensorFlow 2.0

Dans ce tutoriel, vous apprendrez à implémenter des applications puissantes comme Prisma avec TensorFlow 2.0.
Actualisé 19 sept. 2026  · 11 min lire

Explorer avec l’IA

ChatGPTClaudePerplexity

Les réseaux de neurones convolutionnels sont la famille la plus performante pour la classification et l’analyse d’images. Leurs applications ont franchi de nombreuses limites et constituent l’élément clé de bon nombre d’applications de deep learning que nous utilisons aujourd’hui. À haut niveau, les CNN apprennent des représentations internes de caractéristiques à partir des images qu’on leur fournit. C’est ce qui fait leur puissance. Il se trouve que cette propriété n’est pas seulement utile pour la classification d’images, mais aussi pour la synthèse d’images. Des applications comme Deep Dream et le transfert de style neuronal composent des images à partir des activations de couches dans des CNN et des caractéristiques qu’elles extraient.

Dans ce tutoriel, vous allez étudier le fonctionnement du transfert de style neuronal et voir comment l’implémenter avec TensorFlow 2.0.

Remarque : pour suivre ce tutoriel, vous devez comprendre le fonctionnement des CNN. Si vous cherchez une mise à niveau rapide, essayez les ressources suivantes :

Qu’est-ce que le transfert de style neuronal ?

Pour celles et ceux qui débutent en vision par ordinateur et en deep learning, le terme neural style transfer peut paraître intimidant. Pour en comprendre chaque composant, considérez les deux images suivantes :

neural style transfer 1

Dans le contexte du transfert de style, l’image de gauche est appelée image de contenu et celle de droite image de style. L’objectif est de styliser une image (ici, celle de gauche) à partir d’une autre (celle de droite). C’est ce qui constitue les deux derniers mots du terme – style transfer. Pour réaliser ce processus, un réseau neuronal (CNN) est entraîné à optimiser une fonction de perte personnalisée : d’où le premier mot – neural. Lorsque les deux images ci‑dessus sont fusionnées par transfert de style, le rendu final ressemble à ceci (à droite) :

neural style transfer 2

À quoi sert le transfert de style neuronal ? Imaginez que vous ayez l’image d’un dessin réalisé à l’origine par Vincent van Gogh. Vous souhaitez voir comment ce style se traduirait sur l’un de vos propres dessins. C’est un cas d’usage typique. Autre exemple : des applications de filtres photo comme Prisma, qui vous permettent de réaliser un transfert de style via une interface fluide.

Isoler contenu et style

Jusqu’ici, vous avez compris que le transfert de style consiste à combiner le contenu d’une image avec le style d’une autre. C’est tout à fait exact. Dans cette section, voyons ce que recouvrent précisément contenu et style dans le contexte des CNN.

Un CNN est souvent constitué de plusieurs couches convolutionnelles et de pooling. Les couches convolutionnelles extraient des caractéristiques très complexes d’une image donnée, tandis que les couches de pooling éliminent des informations spatiales détaillées qui ne sont pas pertinentes pour la classification. Cela aide le réseau à apprendre le contenu d’une image plutôt que des éléments spécifiques comme la couleur, la texture, etc. Plus on va en profondeur, plus la complexité des caractéristiques augmente ; les couches convolutionnelles profondes sont souvent appelées représentations de contenu.

Le style correspond à tout ce qui relève de propriétés spécifiques de l’image comme la texture, la couleur, etc. — par exemple l’usage prononcé d’une teinte particulière dans un tableau. Comment extraire le style d’une image ? En calculant des corrélations entre les cartes de caractéristiques des couches convolutionnelles. Les corrélations mesurent à quel point deux (ou plusieurs) variables sont liées. Considérez une couche convolutionnelle apprise composée de plusieurs cartes de caractéristiques : pour chacune, on peut mesurer à quel point les motifs détectés se rapprochent de ceux des autres cartes de la même couche. Cela renseigne sur des points comme : une couleur détectée sur la première carte est‑elle proche d’une couleur sur une autre ? Des formes sont‑elles communes entre cartes ? Ces traits/similarités définissent le style d’une image. Mesurer la similarité entre les contenus de plusieurs cartes au sein d’une couche aide le réseau à apprendre une représentation multi‑échelle centrée sur des aspects spatiaux comme la texture et la couleur.

Point essentiel : lors d’un transfert de style, il faut aussi veiller à préserver le contenu de l’image tout en y appliquant le style souhaité. Vous verrez plus loin comment procéder. Dans la section suivante, vous apprendrez à extraire les caractéristiques d’une image (de contenu) et à calculer la perte de contenu.

Quantifier l’image de contenu et calculer la perte de contenu

L’algorithme de transfert de style neuronal a été présenté pour la première fois par Gatys et al. en 2015 : A Neural Algorithm of Artistic Style. Ce tutoriel s’appuie toutefois sur Image Style Transfer Using Convolutional Neural Networks, qui prolonge le premier article.

Selon Image Style Transfer Using Convolutional Neural Networks, on utilise une architecture VGG‑19 pour extraire à la fois les caractéristiques de contenu et de style des images correspondantes. Pour le contenu, la deuxième couche convolutionnelle du quatrième bloc est utilisée, notée conv4_2. Une fois ces caractéristiques obtenues, on les compare à une image cible pour mesurer la perte de contenu. Qu’est‑ce qu’une image cible ? Pourquoi calculer une perte de contenu ici ? Prenons du recul.

Pour combiner contenu et style dans une seule image, on part d’une image cible, qui est soit une image blanche, soit une copie de l’image de contenu. Pour apprendre efficacement contenu et style avec un CNN, on définit une fonction de perte personnalisée que l’on optimise afin d’obtenir une image stylisée harmonieuse construite à partir des images de contenu et de style. Cette perte est la combinaison de deux termes :

  • La perte de contenu, qui veille à ce que la quantité de contenu soit préservée.
  • La perte de style, qui contrôle la quantité de style transférée à l’image cible.

Revenons à la perte de contenu et définissons‑la. La perte de contenu mesure à quel point les caractéristiques des images de contenu et cible s’éloignent l’une de l’autre. Le réseau cherchera à minimiser cette perte. Dans la section suivante, vous verrez comment déterminer la perte de style.

La perte de style et la matrice de Gram

Pour déterminer la perte de style, l’article indique de prendre les représentations (des valeurs numériques) issues des couches suivantes et de calculer les matrices de Gram des cartes de caractéristiques correspondantes.

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

L’intérêt d’utiliser plusieurs couches pour définir la perte de style est d’apprendre une représentation multi‑échelle du style contenu dans une image. Les matrices de Gram évaluent la similarité des caractéristiques entre cartes au sein d’une même couche convolutionnelle. Elles capturent ainsi une information non localisée qui façonne le style de l’image. Une matrice de Gram se calcule ici de la manière suivante.

Supposons qu’une image convoluée ait la dimension 8×8×16, soit 16 cartes de caractéristiques. On cherche à mesurer les similarités entre ces cartes via une matrice de Gram. Pour cela, on aplatit les deux premières dimensions en un vecteur 1D : on obtient 64 entrées. On répète l’opération pour chaque carte. On obtient une matrice finale de dimension 16×64 (appelons‑la A). On transpose ensuite cette matrice (64×16) puis on la multiplie par A. La matrice de Gram résultante est de dimension 16×16. Chaque valeur indique la similarité entre deux cartes. Visuellement, ce processus ressemble à :

style loss and gram matrix

Pour calculer la perte de style entre l’image cible et l’image de style, on prend la moyenne des distances quadratiques entre leurs matrices de Gram à chacun des blocs convolutionnels. Formellement :

T(s,i) est la matrice de Gram de l’image cible au bloc i et S(s,i) celle de l’image de style au bloc i. Avec wi, vous pouvez pondérer différemment les blocs pour une représentation de style plus fine. Enfin, $a$ est une constante qui tient compte des valeurs dans chaque couche des blocs. Mettons maintenant les deux pertes ensemble pour définir la perte totale optimisée durant le transfert de style.

Définir la perte totale

La perte totale est simplement la somme pondérée des deux termes :

où $\alpha$ est le poids du contenu et $\beta$ le poids du style. Cela vous permet d’équilibrer le contenu et la quantité de style transférée à l’image cible. L’effet de différentes combinaisons de $\alpha$ et $\beta$ est illustré dans l’article ; n’hésitez pas à le consulter. Vous avez désormais toutes les pièces pour implémenter le transfert de style neuronal. Passons à la pratique.

Transfert de style neuronal en pratique

Pour l’implémentation, vous utiliserez TensorFlow 2.0. Cette version apporte de nombreuses nouveautés majeures et constitue l’une des mises à jour les plus complètes de la bibliothèque à ce jour. Si vous souhaitez en découvrir quelques‑unes, consultez cet article.

La dernière version de TensorFlow (au moment de la rédaction) est 2.0.0-beta0. Si vous ne l’avez pas encore, installez‑la en suivant les instructions indiquées ici. Commencez par importer les packages nécessaires ainsi que les images de contenu et de style.

# 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()
content and style images

À l’étape suivante, vous écrirez une fonction utilitaire qui charge les deux images en tableaux numériques (les ordinateurs ne manipulent que des nombres) et les redimensionne pour les rendre compatibles avec le modèle.

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]))

Les images sont redimensionnées. Vous allez maintenant charger un modèle VGG19 pré‑entraîné pour extraire les caractéristiques. Comme il sert uniquement d’extracteur, la partie classifieur n’est pas nécessaire.

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

Les auteurs ont indiqué les noms de blocs utilisés pour extraire contenu et style. Le modèle VGG19 que vous venez de charger utilise la même convention. Pour en profiter, commencez par afficher les noms de toutes les couches du réseau.

# 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

Voici celles qui nous intéressent pour les caractéristiques de style :

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

Pour le contenu, vous aurez besoin de conv4_2. Enregistrons ces noms dans des variables.

# 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)

Créez maintenant un VGG personnalisé composé des couches spécifiées. Vous pourrez ainsi faire des passes avant sur les images et extraire les caractéristiques nécessaires au passage.

def mini_model(layer_names, model):

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

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

Définir une matrice de Gram est très simple dans TensorFlow :

# 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

Vous allez maintenant définir un modèle personnalisé via la fonction mini_model(). Il renverra les caractéristiques de contenu et de style à partir des images correspondantes.

 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}

Maintenant que le modèle personnalisé est défini, utilisons‑le sur les images pour obtenir les caractéristiques de contenu et de style :

# 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)

Définissons maintenant les poids globaux de contenu et de style, ainsi que les poids propres à chaque représentation de style, comme évoqué plus haut. Remarque : ce sont des hyperparamètres, à ajuster selon vos besoins.

 # 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}

Vient maintenant la partie cruciale qui rend le processus de transfert de style particulièrement intéressant : la fonction de perte.

# 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

Vous allez maintenant écrire une autre fonction qui :

  • calcule les gradients de la fonction de perte que vous venez de définir ;
  • utilise ces gradients pour mettre à jour l’image cible.

Avec GradientTape, vous profitez de la différenciation automatique pour calculer les gradients d’une fonction selon sa composition. Vous utiliserez aussi le décorateur tf.function pour accélérer l’exécution. Plus d’informations ici.

@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))

La seule étape restante avant l’entraînement est de définir l’image cible. Ici, nous utiliserons l’image de contenu.

target_image = tf.Variable(content)

Vous êtes désormais prêt à entraîner le réseau.

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 

Nous vous encourageons à tester différentes valeurs pour les poids de contenu et de style afin d’observer l’impact sur l’image cible.

Pour conclure

L’article de référence utilisé ici ne mentionne pas l’optimisation supplémentaire de la qualité de l’image générée. Comme on l’observe dans les résultats ci‑dessus, un certain lissage des relations spatiales entre contenu et style serait utile. Pour y remédier, on introduit la total_variational_loss, analogue à une régularisation. À haut niveau, la total_variational_loss pénalise les artéfacts haute fréquence introduits par l’algorithme original de transfert de style. Consultez ce tutoriel si vous souhaitez l’implémenter. Voici quelques ressources qui ont servi de références pour la rédaction :

Si vous souhaitez développer vos compétences en deep learning, découvrez ces cours et tutoriels DataCamp :

Sujets
Python
Apprentissage profond

En savoir plus sur Python et le deep learning

Cours

Introduction à TensorFlow en Python

4 h
56.5K
Apprenez les principes fondamentaux des réseaux neuronaux et comment créer des modèles d'apprentissage profond à l'aide de TensorFlow.
Afficher les détailsRight Arrow
Commencer Le Cours
Voir plusRight Arrow