La convolution est une opération fondamentale dans les réseaux de neurones convolutifs (CNN). Elle consiste à appliquer un filtre (ou noyau) sur une entrée pour produire une carte de caractéristiques. Les principales différences entre la convolution 1D et 2D résident dans la dimensionnalité de l'entrée et la manière dont le filtre glisse.
Convolution 1D (conv1d)
La convolution 1D est principalement utilisée pour traiter des données séquentielles, telles que celles rencontrées en traitement du langage naturel (NLP). Elle applique un filtre qui glisse le long d'une seule dimension (longuuer ou largeur).
Paramètres de tf.layers.conv1d :
inputs: Le tenseur d'entrée. Pourchannels_last, la forme typique est[batch_size, sequence_length, channels].filters: Le nombre de filtres (noyaux) à utiliser, déterminant la profondeur de la sortie.kernel_size: La taille du noyau de convolution le long de la dimension spatiale (la longueur de la séquence).strides: Le pas de déplacement du noyau.padding: Définit comment gérer les bords de l'entrée.'valid'ne fait aucun padding, tandis que'same'ajoute du padding pour que la sortie ait la même longueur que l'entrée (si le stride est de 1).data_format: Spécifie l'ordre des dimensions :'channels_last'(par défaut) ou'channels_first'.activation: La fonction d'activation à appliquer après la convolution.use_bias: Indique si un biais doit être ajouté après la convolution.- Autres paramètres : Initialiseurs pour le noyau et le biais, régulariseurs, contraintes, etc.
Exemple d'utiilsation :
import tensorflow as tf
# Entrée de forme (batch_size, sequence_length, channels)
# Exemple : 32 échantillons, longueur de séquence 512, 1024 caractéristiques par pas de temps
input_data = tf.random.normal([32, 512, 1024])
# Convolution 1D avec 1 filtre, taille de noyau 512, padding 'same'
# La taille du noyau (kernel_size) ici est interprétée comme la longueur de la fenêtre glissante.
# La sortie aura une forme (batch_size, sequence_length, num_filters)
output_conv1d = tf.keras.layers.Conv1D(
filters=1,
kernel_size=512, # Noyau de 512 éléments le long de la dimension de séquence
strides=1,
padding='same',
data_format='channels_last'
)(input_data)
print(output_conv1d.shape) # Exemple de sortie : (32, 512, 1)
Dans cet exemple, le kernel_size=512 signifie que le noyau a une longueur de 512. Cependant, comme il s'agit d'une convolution sur la dimension de la séquence (longueur 512), le noyau glisse sur cette dimension. La sortie résultante a une forme (32, 512, 1), où 1 est le nombre de filtres.
Convolution 2D (conv2d)
La convolution 2D est couramment utilisée dans le traitement d'images et la vision par ordinateur. Elle applique un filtre qui glisse à la fois sur la hauteur et la largeur de l'entrée.
Paramètres de tf.layers.conv2d :
inputs: Le tenseur d'entrée. Pourchannels_last, la forme typique est[batch_size, height, width, channels].filters: Le nombre de filtres à utiliser.kernel_size: La taille du noyau (hauteur, largeur). Peut être un entier unique pour des noyaux carrés.strides: Le pas de déplacement du noyau le long de la hauteur et de la largeur. Peut être un entier unique.padding:'valid'ou'same'.data_format:'channels_last'(par défaut) ou'channels_first'.activation: Fonction d'activation.use_bias: Indique si un biais doit être ajouté.- Autres paramètres : Similaires à
conv1d.
Exemple d'utilisation :
import tensorflow as tf
# Entrée de forme (batch_size, height, width, channels)
# Exemple : 1 échantillon, image 3x3, 5 canaux
input_image = tf.random.normal([1, 3, 3, 5])
# Convolution 2D avec 1 filtre, noyau 1x1, strides 1x1, padding 'same'
output_conv2d = tf.keras.layers.Conv2D(
filters=1,
kernel_size=[1, 1], # Noyau de 1x1 pixel
strides=[1, 1],
padding='same',
data_format='channels_last'
)(input_image)
print(output_conv2d.shape) # Sortie : (1, 3, 3, 1)
Dans cet exemple, un noyau 1x1 glisse sur l'image 3x3. La sortie conserve les dimensions spatiales 3x3 et a une profondeur de 1 (dictée par filters=1).
Calcul de la taille de sortie
La taille de sortie d'une couche de convolution dépend de la taille de l'entrée, de la taille du noyau, du pas et du padding.
Pour une dimension donnée (par exemple, la largeur W), avec une taille de noyau F, un pas S et un padding P (où P=0 pour 'valid' et une valeur calculée pour 'same'), la formule générale est :
$$N = \lfloor \frac{W - F + 2P}{S} \rfloor + 1$$ ### Padding 'SAME' vs 'VALID'
- Padding 'VALID' : Le noyau ne glisse que sur les parties de l'entrée où il peut être entièrement contenu. La formule devient : $N = \lfloor \frac{W - F + 1}{S} \rfloor$.
- Padding 'SAME' : Le padding est ajouté de manière à ce que la sortie ait la même taille que l'entrée si le pas est de 1. La taille de sortie est calculée comme $N = \lceil \frac{W}{S} \rceil$ (arrondi à l'entier supérieur).
Exemple avec padding 'SAME' :
import tensorflow as tf
input_shape = [64, 32, 32, 3] # (batch, height, width, channels)
input_tensor = tf.random.normal(input_shape)
# Exemple 1: kernel_size=3, strides=2
conv_same_1 = tf.keras.layers.Conv2D(
filters=64, kernel_size=3, strides=2, padding='same'
)(input_tensor)
# Taille de sortie attendue : ceil(32/2) = 16
print(f"SAME, kernel=3, stride=2: {conv_same_1.shape}") # (64, 16, 16, 64)
# Exemple 2: kernel_size=5, strides=2
conv_same_2 = tf.keras.layers.Conv2D(
filters=64, kernel_size=5, strides=2, padding='same'
)(input_tensor)
# Taille de sortie attendue : ceil(32/2) = 16 (la taille du noyau n'affecte pas la sortie avec padding='same' et stride=2)
print(f"SAME, kernel=5, stride=2: {conv_same_2.shape}") # (64, 16, 16, 64)
Exemple avec padding 'VALID' :
import tensorflow as tf
input_shape = [64, 32, 32, 3] # (batch, height, width, channels)
input_tensor = tf.random.normal(input_shape)
# Exemple 1: kernel_size=3, strides=2
conv_valid_1 = tf.keras.layers.Conv2D(
filters=64, kernel_size=3, strides=2, padding='valid'
)(input_tensor)
# Taille de sortie attendue : floor((32 - 3 + 1) / 2) = floor(30 / 2) = 15
print(f"VALID, kernel=3, stride=2: {conv_valid_1.shape}") # (64, 15, 15, 64)
# Exemple 2: kernel_size=5, strides=2
conv_valid_2 = tf.keras.layers.Conv2D(
filters=64, kernel_size=5, strides=2, padding='valid'
)(input_tensor)
# Taille de sortie attendue : floor((32 - 5 + 1) / 2) = floor(28 / 2) = 14
print(f"VALID, kernel=5, stride=2: {conv_valid_2.shape}") # (64, 14, 14, 64)
L'utilisation de noyaux 1x1 dans les convolutions 2D peut servir à réduire ou augmenter la dimensionnalité des canaux, permettant de capturer des caractéristiques plus complexes ou de contrôler la capacité du modèle sans affecter directement les dimensions spatiales de manière significative (lorsque le stride est de 1).
Types de Padding : 'valid', 'same', 'full'
Ces termes décrivent comment la convolution gère les bords de l'entrée.
- 'valid' : Aucune information n'est perdue, mais la sortie peut être plus petite que l'entrée.
- 'same' : Ajoute du padding de manière à ce que la sortie ait une taille spatiale égale à celle de l'entrée si le stride est 1. Le padding est généralement appliqué symétriquement.
- 'full' : Ajoute du padding de sorte que toutes les combinaisons possibles entre l'entrée et le noyau soient calculées. La sortie est généralement plus grande que l'entrée.
La visualisation suivante illustre la différence pour une convolution 1D :
Soit une entrée a = [1, 2, 3, 4, 5] et un noyau b = [8, 7, 6].
Entrée (avec padding implicite pour 'same' et 'full'):
0 0 1 2 3 4 5 0 0 (Entrée potentiellement paddée)
Noyau 6 7 8
Opérations :
6*1 + 7*2 + 8*3 = 44 (avec noyau centré sur 1) -> 'valid' partiel
6*2 + 7*3 + 8*4 = 65
6*3 + 7*4 + 8*5 = 86
Résultats :
'valid' (seulement quand le noyau est entièrement dans les données originales) : [44, 65, 86]
'same' (taille de sortie égale à l'entrée, typiquement) : [?, 44, 65, 86, ?] (ex: 23, 44, 65, 86, 59)
'full' (toutes les superpositions possibles) : [8, 23, 44, 65, 86, 59, 30]
Exmeples de formes de sortie avec tf.nn.conv2d :
import tensorflow as tf
# Utilisation de tf.nn.conv2d pour une meilleure compréhension des formes
# Note: Les tenseurs sont initialisés aléatoirement pour la démonstration
# Cas 2: Noyau 1x1, strides [1,1,1,1], padding 'VALID'
input_2 = tf.random.normal([1, 3, 3, 5]) # (batch, height, width, channels)
filter_2 = tf.random.normal([1, 1, 5, 1]) # (kernel_h, kernel_w, in_channels, out_channels)
op_2 = tf.nn.conv2d(input_2, filter_2, strides=[1, 1, 1, 1], padding='VALID')
# Forme attendue : (1, 3, 3, 1) car le noyau 1x1 ne réduit pas la dimension
# Cas 3: Noyau 3x3, strides [1,1,1,1], padding 'VALID'
input_3 = tf.random.normal([1, 3, 3, 5])
filter_3 = tf.random.normal([3, 3, 5, 1]) # Noyau 3x3
op_3 = tf.nn.conv2d(input_3, filter_3, strides=[1, 1, 1, 1], padding='VALID')
# Forme attendue : (1, 1, 1, 1) car (3 - 3 + 1) = 1 pour chaque dimension spatiale
# Cas 4: Noyau 3x3, strides [1,1,1,1], padding 'VALID' sur une plus grande entrée
input_4 = tf.random.normal([1, 5, 5, 5])
filter_4 = tf.random.normal([3, 3, 5, 1])
op_4 = tf.nn.conv2d(input_4, filter_4, strides=[1, 1, 1, 1], padding='VALID')
# Forme attendue : (1, 3, 3, 1) car (5 - 3 + 1) = 3 pour chaque dimension spatiale
# Cas 5: Noyau 3x3, strides [1,1,1,1], padding 'SAME'
input_5 = tf.random.normal([1, 5, 5, 5])
filter_5 = tf.random.normal([3, 3, 5, 1])
op_5 = tf.nn.conv2d(input_5, filter_5, strides=[1, 1, 1, 1], padding='SAME')
# Forme attendue : (1, 5, 5, 1) car 'SAME' avec stride 1 préserve les dimensions spatiales
# Cas 6: Noyau 3x3, strides [1,1,1,1], padding 'SAME', plus de filtres de sortie
input_6 = tf.random.normal([1, 5, 5, 5])
filter_6 = tf.random.normal([3, 3, 5, 7]) # 7 filtres de sortie
op_6 = tf.nn.conv2d(input_6, filter_6, strides=[1, 1, 1, 1], padding='SAME')
# Forme attendue : (1, 5, 5, 7)
# Cas 7: Noyau 3x3, strides [1,2,2,1], padding 'SAME'
input_7 = tf.random.normal([1, 5, 5, 5])
filter_7 = tf.random.normal([3, 3, 5, 7])
op_7 = tf.nn.conv2d(input_7, filter_7, strides=[1, 2, 2, 1], padding='SAME')
# Forme attendue : (1, ceil(5/2), ceil(5/2), 7) = (1, 3, 3, 7)
# Cas 8: Même que 7 mais avec batch_size > 1
input_8 = tf.random.normal([10, 5, 5, 5]) # Batch size 10
filter_8 = tf.random.normal([3, 3, 5, 7])
op_8 = tf.nn.conv2d(input_8, filter_8, strides=[1, 2, 2, 1], padding='SAME')
# Forme attendue : (10, 3, 3, 7)
init = tf.compat.v1.global_variables_initializer() # Utilisation compatible v1
with tf.compat.v1.Session() as sess:
sess.run(init)
print("Case 2 shape:", sess.run(op_2).shape)
print("Case 3 shape:", sess.run(op_3).shape)
print("Case 4 shape:", sess.run(op_4).shape)
print("Case 5 shape:", sess.run(op_5).shape)
print("Case 6 shape:", sess.run(op_6).shape)
print("Case 7 shape:", sess.run(op_7).shape)
print("Case 8 shape:", sess.run(op_8).shape)
# Les sorties correspondent aux calculs théoriques :
# Case 2 shape: (1, 3, 3, 1)
# Case 3 shape: (1, 1, 1, 1)
# Case 4 shape: (1, 3, 3, 1)
# Case 5 shape: (1, 5, 5, 1)
# Case 6 shape: (1, 5, 5, 7)
# Case 7 shape: (1, 3, 3, 7)
# Case 8 shape: (10, 3, 3, 7)