Données d'image : chaque variable explicative est une matrice de pixels
Nous avons simplement éliminé la structure spatiale de chaque image en l'aplatissant en vecteurs unidimensionnels.
Comme ces réseaux sont invariants par rapport à l'ordre des caractéristiques, nous pourrions obtenir des résultats similaires, que nous préservions un ordre correspondant à la structure spatiale des pixels ou que nous permutions les colonnes de notre matrice de pixels.
Il serait préférable de tirer parti de notre connaissance préalable du fait que les pixels proches sont généralement liés les uns aux autres, afin de construire des modèles efficaces pour l'apprentissage à partir de données images.
Les réseaux neuronaux convolutifs (CNN) sont une puissante famille de réseaux neuronaux conçus précisément dans ce but. Les architectures basées sur les CNN sont désormais omniprésentes dans le domaine de la vision par ordinateur.
Les CNN modernes, comme on les appelle familièrement, doivent leur conception à des inspirations issues de la biologie, de la théorie des groupes et d'une bonne dose de bricolage expérimental. Outre leur efficacité en matière d'échantillonnage pour obtenir des modèles précis, les CNN ont tendance à être efficaces sur le plan du calcul, à la fois parce qu'ils nécessitent moins de paramètres que les architectures entièrement connectées et parce que les convolutions sont faciles à paralléliser sur les cœurs des GPU.
Par conséquent, les praticiens appliquent souvent les CNN chaque fois que cela est possible et, de plus en plus, ils s'imposent comme des concurrents crédibles, même pour les tâches présentant une structure de séquence unidimensionnelle, comme l'analyse de l'audio, du texte et des séries temporelles, où les réseaux neuronaux récurrents sont traditionnellement utilisés. Certaines adaptations astucieuses des CNN ont également permis de les utiliser sur des données structurées par des graphes et dans des systèmes de recommandation.
Nous allons passer en revue les opérations de base de tous les réseaux convolutifs. Cela comprend les couches convolutives elles-mêmes, les détails de détail, y compris le padding et le stride, les couches de mise en commun utilisées pour agréger les informations dans les régions spatiales adjacentes, l'utilisation de plusieurs canaux à chaque couche, et une discussion approfondie de la structure des architectures modernes. Nous conclurons ce chapitre par un exemple fonctionnel complet de LeNet, le premier réseau convolutif déployé avec succès, bien avant l'essor de l'apprentissage profond moderne.
Supposons qu'on s'intéresse à la distinction entre les chats et les chiens. Supposons que nous fassions un travail minutieux de collecte de données, en recueillant un ensemble de données annotées de photographies d'un mégapixel. Cela signifie que chaque entrée du réseau a un million de dimensions. Selon nos discussions sur le coût de paramétrage des couches entièrement connectées, même une réduction agressive à mille dimensions cachées nécessiterait une couche entièrement connectée caractérisée par $10^6 \times 10^3=10^9$ paramètres. À moins de disposer de nombreux GPU, d'un talent pour l'optimisation distribuée et d'une patience extraordinaire, l'apprentissage des paramètres de ce réseau peut s'avérer irréalisable.
On peut penser que la résolution d'un mégapixel n'est pas nécessaire. Cependant, même si nous pouvons nous contenter de cent mille pixels, notre couche cachée de taille 1000 sous-estime largement le nombre d'unités cachées nécessaires pour apprendre de bonnes représentations d'images, de sorte qu'un système pratique nécessitera encore des milliards de paramètres. De plus, l'apprentissage d'un classificateur en ajustant autant de paramètres pourrait nécessiter la collecte d'un énorme ensemble de données. Pourtant, aujourd'hui, les humains et les ordinateurs sont capables de distinguer les chats des chiens, ce qui semble contredire ces intuitions. Cela est dû au fait que les images présentent une structure riche qui peut être exploitée par les humains et les modèles d'apprentissage automatique. Les réseaux de neurones convolutifs (CNN) sont un moyen créatif que l'apprentissage automatique a adopté pour exploiter une partie de la structure connue des images naturelles.
Imaginez que vous souhaitiez détecter un objet dans une image. Il semble raisonnable que la méthode que nous utilisons pour reconnaître les objets ne soit pas pas trop sensible à l'emplacement précis de l'objet dans l'image. Idéalement, notre système devrait exploiter cette connaissance. Les cochons ne volent généralement pas et les avions ne nagent généralement pas. Néanmoins, nous devrions reconnaître un cochon s'il apparaissait en haut de l'image. Nous pouvons nous inspirer ici du jeu pour enfants "où est Charlie". Le jeu consiste en un certain nombre de scènes chaotiques débordant d'activités. Charlie apparaît quelque part dans chacune d'elles, généralement dans un endroit improbable. Le but du lecteur est de le localiser. Malgré sa tenue caractéristique, cela peut s'avérer étonnamment difficile, en raison du grand nombre de distractions. Cependant, l'apparence de Charlie ne dépend pas de l'endroit où il se trouve. Nous pourrions balayer l'image avec un détecteur de Charlie qui pourrait attribuer un score à chaque patch, indiquant la probabilité que le patch contienne Charlie. Les CNN systématisent cette idée d'invariance spatiale et l'exploitent pour apprendre des représentations utiles avec moins de paramètres.
Concrètement :
Dans les premières couches, notre réseau doit répondre de manière similaire au même patch, quel que soit l'endroit où il apparaît dans l'image. Ce principe est appelé invariance par translation.
Les premières couches du réseau doivent se concentrer sur les régions locales, sans tenir compte du contenu de l'image dans les régions éloignées. C'est le principe de localité. Finalement, ces représentations locales peuvent être agrégées pour faire des prédictions au niveau de l'image entière.
Voyons comment cela se traduit en mathématiques.
Pour commencer, nous pouvons considérer un MLP avec des images bidimensionnelles $\mathbf{X}$ comme entrées et leurs représentations cachées immédiates représentées de manière similaire comme des matrices en mathématiques et comme des tenseurs bidimensionnels en code, où les $\mathbf{X}$ et les $\mathbf{H}$ ont la même forme. Nous concevons maintenant non seulement les entrées mais aussi les représentations cachées comme possédant une structure spatiale.
Que $[\mathbf{X}]_{i, j}$ et $[\mathbf{H}]_{i, j}$ désignent le pixel à l'emplacement ($i$, $j$) dans l'image d'entrée et la représentation cachée, respectivement. Par conséquent, pour que chacune des unités cachées reçoive une entrée de chacun des pixels d'entrée, nous passerions de l'utilisation de matrices de poids (comme nous l'avons fait précédemment dans les MLP) pour représenter nos paramètres sous forme de tenseurs de poids d'ordre 4 $\mathsf{W}$. Supposons que $\mathbf{U}$ contienne des biais, nous pourrions formellement exprimer la couche entièrement connectée comme suit .
$$\begin{aligned} \left[\mathbf{H}\right]_{i, j} &= [\mathbf{U}]_{i, j} + \sum_k \sum_l[\mathsf{W}]_{i, j, k, l} [\mathbf{X}]_{k, l}\\ &= [\mathbf{U}]_{i, j} + \sum_a \sum_b [\mathsf{V}]_{i, j, a, b} [\mathbf{X}]_{i+a, j+b}. \end{aligned},$$où le passage de $\mathsf{W}$ à $\mathsf{V}$ est entièrement cosmétique pour le moment puisqu'il existe une correspondance biunivoque entre les coefficients des deux tenseurs d'ordre 4. Nous réindexons simplement les indices $(k, l)$ de sorte que $k = i+a$ et $l = j+b$. En d'autres termes, nous définissons $[\mathsf{V}]_{i, j, a, b} = [\mathsf{W}]_{i, j, i+a, j+b}$. Les indices $a$ et $b$ couvrent les décalages positifs et négatifs, couvrant l'image entière. Pour tout emplacement donné ($i$, $j$) dans la représentation cachée $[\mathbf{H}]_{i, j}$, on calcule sa valeur en faisant la somme des pixels de $x$, centrés autour de $(i, j)$ et pondérés par $[\mathsf{V}]_{i, j, a, b}$.
Cela implique qu'un déplacement de l'entrée $\mathbf{X}$ devrait simplement conduire à un déplacement de la représentation cachée $\mathbf{H}$. Ceci n'est possible que si $\mathsf{V}$ et $\mathbf{U}$ ne dépendent pas réellement de $(i, j)$, c'est-à-dire que nous avons $[\mathsf{V}]_{i, j, a, b} = [\mathbf{V}]_{a, b}$ et $\mathbf{U}$ est une constante, disons $u$. Nous pouvons donc simplifier la définition pour $\mathbf{H}$ : $$[\mathbf{H}]_{i, j} = u + \sum_a\sum_b [\mathbf{V}]_{a, b} [\mathbf{X}]_{i+a, j+b}.$$
Il s'agit d'une convolution !
Nous pondérons effectivement les pixels à $(i+a, j+b)$. au voisinage de l'emplacement $(i, j)$ avec des coefficients $[\mathbf{V}]_{a, b}$. pour obtenir la valeur $[\mathbf{H}]_{i, j}$. Notez que $[\mathbf{V}]_{a, b}$ a besoin de beaucoup moins de coefficients que $[\mathsf{V}]_{i, j, a, b}$ puisqu'il ne dépend plus de l'emplacement dans l'image.
Comme nous l'avons motivé, nous pensons que nous ne devrions pas avoir à regarder très loin de l'emplacement $(i, j)$ afin de glaner des informations pertinentes pour évaluer ce qui se passe à $[\mathbf{H}]_{i, j}$. Cela signifie qu'en dehors d'un certain intervalle $|a|> \Delta$ ou $|b| > \Delta$, nous devons définir $[\mathbf{V}]_{a, b} = 0$. De manière équivalente, nous pouvons réécrire $[\mathbf{H}]_{i, j}$ comme suit
$$[\mathbf{H}]_{i, j} = u + \sum_{a = -\Delta}^\Delta \sum_{b = -\Delta}^\Delta [\mathbf{V}]_{a, b} [\mathbf{X}]_{i+a, j+b}.$$En un mot, il s'agit d'une couche de convolution. Les réseaux neuronaux convolutifs (CNN) sont une famille particulière de réseaux neuronaux qui contiennent des couches convolutives. Dans la communauté de recherche sur l'apprentissage profond, $\mathbf{V}$ est appelé un noyau de convolution, un filtre, ou simplement les poids de la couche qui sont souvent des paramètres à calculer par apprentissage par minimisation d'une fonction de perte. Lorsque la région locale est petite, la différence par rapport à un réseau entièrement connecté peut être spectaculaire. Alors qu'auparavant, nous aurions pu avoir besoin de milliards de paramètres pour représenter une seule couche dans un réseau de traitement d'image, nous n'avons maintenant besoin que de quelques centaines, sans altérer la dimensionnalité des entrées ou des représentations cachées.
Avant d'aller plus loin, nous devons revoir brièvement pourquoi l'opération ci-dessus est appelée une convolution. En mathématiques, la convolution entre deux fonctions, disons $f, g : \mathbb{R}^d \to \mathbb{R}$ est définie comme étant
$$(f * g)(\mathbf{x}) = \int f(\mathbf{z}) g(\mathbf{x}-\mathbf{z}) d\mathbf{z}.$$Autrement dit, nous mesurons le chevauchement entre $f$ et $g$ lorsqu'une fonction est " retournée " et décalée de $\mathbf{x}$. Dès que nous avons des objets discrets, l'intégrale se transforme en une somme. Par exemple, pour les vecteurs de l'ensemble des vecteurs de dimension infinie sommables au carré dont l'indice s'étend sur $\mathbb{Z}$, nous obtenons la définition suivante :
$$(f * g)(i) = \sum_a f(a) g(i-a).$$Pour les tenseurs à deux dimensions, nous avons une somme correspondante avec des indices $(a, b)$ pour $f$ et $(i-a, j-b)$ pour $g$, respectivement :
$$(f * g)(i, j) = \sum_a\sum_b f(a, b) g(i-a, j-b).$$Une différence majeure. Plutôt que d'utiliser $(i+a, j+b)$, nous utilisons la différence à la place.
Jusqu'à présent, nous avons ignoré que les images se composent de 3 canaux : rouge, vert et bleu. En réalité, les images ne sont pas des objets bidimensionnels mais plutôt des tenseurs 3D, caractérisés par une hauteur, une largeur et un canal, par exemple, de forme $1024 \times 1024 \times 3$ pixels. Alors que les deux premiers de ces axes concernent les relations spatiales, le troisième peut être considéré comme l'attribution d'une représentation multidimensionnelle à chaque emplacement de pixel. Nous indexons donc $\mathsf{X}$ comme $\mathsf{X}_{i, j, k}$. Le filtre convolutif doit s'adapter en conséquence. Au lieu de $\mathbf{V}_{a,b}$, nous avons maintenant $\mathsf{V}_{a,b,c}$.
De plus, tout comme notre entrée consiste en un tenseur 3D, il s'avère être une bonne idée de formuler de la même manière nos représentations cachées comme des tenseurs 2D $\mathsf{H}$. En d'autres termes, plutôt que d'avoir une seule représentation cachée correspondant à chaque emplacement spatial, nous voulons un vecteur entier de représentations cachées correspondant à chaque emplacement spatial. Nous pouvons considérer que les représentations cachées comprennent un certain nombre de grilles bidimensionnelles empilées les unes sur les autres. Comme pour les entrées, on les appelle parfois des canaux. Elles sont aussi parfois appelées "cartes de caractéristiques", car chacune fournit un ensemble spatialisé de caractéristiques apprises à la couche suivante. Intuitivement, on peut imaginer que dans les couches inférieures, plus proches des entrées, certains canaux pourraient être spécialisés dans la reconnaissance des bords, tandis que d'autres pourraient reconnaître les textures.
Pour prendre en charge plusieurs canaux à la fois dans les entrées ($\mathsf{X}$) et les représentations cachées ($\mathsf{H}$), nous pouvons ajouter une quatrième coordonnée à $\mathsf{V}$ : $[\mathsf{V}]_{a, b, c, d}$. En mettant tout ensemble, nous avons :
$$[\mathsf{H}]_{i, j, d} = \sum_{a = -\Delta}^{\Delta} \sum_{b = -\Delta}^{\Delta} \sum_c [\mathsf{V}]_{a, b, c, d} [\mathsf{X}]_{i+a, j+b, c},$$Rappelons qu'à proprement parler, les couches convolutionnelles sont mal nommées, puisque les opérations qu'elles expriment sont plus précisément décrites comme des corrélations croisées. En se basant sur nos descriptions des couches convolutionnelles, dans une telle couche, un tenseur d'entrée et un tenseur de noyau sont combinés pour produire un tenseur de sortie par une (opération de corrélation croisée.)
Ignorons les canaux pour l'instant et voyons comment cela fonctionne avec des données bidimensionnelles et des représentations cachées. Dans la figure ci-dessous, l'entrée est un tenseur bidimensionnel avec une hauteur de $3$ et une largeur de $3$. Nous marquons la forme du tenseur comme étant $3 \times 3$ ou ($3$, $3$). La hauteur et la largeur du noyau sont toutes deux de $2$. La forme de la fenêtre du noyau (ou fenêtre de convolution) est donnée par la hauteur et la largeur du noyau. (ici, elle est de $2 \times 2$).
Ici, le tenseur de sortie a une hauteur de 2 et une largeur de 2 et les quatre éléments sont dérivés de l'opération de corrélation croisée bidimensionnelle :
$$ 0\times0+1\times1+3\times2+4\times3=19,\\ 1\times0+2\times1+4\times2+5\times3=25,\\ 3\times0+4\times1+6\times2+7\times3=37,\\ 4\times0+5\times1+7\times2+8\times3=43. $$Nous avons besoin de suffisamment d'espace
pour "décaler" le noyau de convolution dans l'image.
Nous verrons plus tard comment garder la taille inchangée
en remplissant l'image de zéros autour de sa limite
de sorte qu'il y ait suffisamment d'espace pour déplacer le noyau.
Ensuite, nous implémentons ce processus dans la fonction corr2d
,
qui accepte un tenseur d'entrée X
et un tenseur de noyau K
.
et retourne un tenseur de sortie Y
.
import torch
from torch import nn
from d2l import torch as d2l
def corr2d(X, K): #@save
"""Compute 2D cross-correlation."""
h, w = K.shape
Y = torch.zeros((X.shape[0] - h + 1, X.shape[1] - w + 1))
for i in range(Y.shape[0]):
for j in range(Y.shape[1]):
Y[i, j] = (X[i:i + h, j:j + w] * K).sum()
return Y
X = torch.tensor([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]])
K = torch.tensor([[0.0, 1.0], [2.0, 3.0]])
corr2d(X, K)
Une couche de convolution calcule la corrélation croisée de l'entrée avec le noyau et ajoute un biais pour produire la sortie. Les deux paramètres d'une couche de convolution sont :
Pour l'entraînement d'un réseau de neurone avec des couche de convolution, nous utilisons l'initialisation aléatoire comme nous l'avons fait précédemment.
Nous sommes maintenant en mesure d'implémenter une couche de convoluton en 2D à l'aide de la fonction
corr2d
déclarée ci-dessus.
Dans le constructeur __init__
, nous déclarons weight
et bias
comme les deux paramètres du modèle. La fonction de propagation forward va faire appel à la fonction corr2d
et ajoute le biais.
class Conv2D(nn.Module):
def __init__(self, kernel_size):
super().__init__()
self.weight = nn.Parameter(torch.rand(kernel_size))
self.bias = nn.Parameter(torch.zeros(1))
def forward(self, x):
return corr2d(x, self.weight) + self.bias
In $h \times w$ convolution or a $h \times w$ convolution kernel, the height and width of the convolution kernel are $h$ and $w$, respectively. Nous déclarons un noyau de convolution à l'aide des deux paramètres $h$ (height) et $w$ (width). On dit qu'on a une couche de convolution $h \times w$.
Prenons un moment pour analyser une application simple d'une couche convolutive : détecter le bord d'un objet dans une image en trouvant l'emplacement du changement de pixel. Tout d'abord, nous construisons une "image" de $6\times 8$ pixels. Les quatre colonnes du milieu sont noires ($0$) et les autres sont blanches ($1$).
X = torch.ones((6, 8))
X[:, 2:6] = 0
X
Ensuite, nous construisons un noyau K
avec une hauteur de $1$ et une largeur de $2$. Lorsque nous effectuons l'opération de corrélation croisée avec l'entrée, si les éléments horizontalement adjacents sont les mêmes, la sortie est $0$. Sinon, la sortie est non nulle.
K = torch.tensor([[1.0, -1.0]])
Nous pouvons maintenant mettre en place l'opération de corrélation croisée avec les arguments X
(notre entrée) et K
(notre noyau). Comme vous pouvez le voir, nous détectons $1$ pour le bord allant du blanc au noir et $-1$ pour le bord du noir au blanc. Toutes les autres sorties prennent la valeur $0$.
Y = corr2d(X, K)
Y
Nous pouvons appliquer le noyau à l'image transposée. Comme prévu, il disparaît. Le noyau K
ne détecte que les bords verticaux.
corr2d(X.t(), K)
Concevoir un détecteur de bords par différences finies [1, -1]
est bien si nous savons que c'est précisément ce que nous recherchons. Cependant, si l'on considère des noyaux plus grands et des couches successives de convolutions, il peut être impossible de spécifier manuellement ce que chaque filtre doit faire.
On souhaite apprendre le noyau qui a généré Y
à partir de X
en regardant uniquement les paires d'entrée-sortie. Nous construisons d'abord une couche convolutionnelle et initialisons son noyau comme un tenseur aléatoire. Ensuite, à chaque itération, nous utiliserons l'erreur quadratique pour comparer Y
avec la sortie de la couche convolutive. Nous pouvons alors calculer le gradient pour mettre à jour le noyau. Pour des raisons de simplicité, dans ce qui suit, nous utilisons la classe intégrée pour les couches convolutionnelles bidimensionnelles et ignorons le biais.
# Construct a two-dimensional convolutional layer with 1 output channel and a
# kernel of shape (1, 2). For the sake of simplicity, we ignore the bias here
conv2d = nn.Conv2d(1,1, kernel_size=(1, 2), bias=False)
# The two-dimensional convolutional layer uses four-dimensional input and
# output in the format of (example, channel, height, width), where the batch
# size (number of examples in the batch) and the number of channels are both 1
X = X.reshape((1, 1, 6, 8))
Y = Y.reshape((1, 1, 6, 7))
lr = 3e-2 # Learning rate
for i in range(10):
Y_hat = conv2d(X)
l = (Y_hat - Y) ** 2
conv2d.zero_grad()
l.sum().backward()
# Update the kernel
conv2d.weight.data[:] -= lr * conv2d.weight.grad
if (i + 1) % 2 == 0:
print(f'epoch {i + 1}, loss {l.sum():.3f}')
Notons que l'erreur est tombée à une faible valeur après 10 itérations.
conv2d.weight.data.reshape((1, 2))
Dans l'exemple illustré précédemment, on avait une entrée $3 \times 3$ et notre noyau de convolution $2 \times 2$, ce qui donnait une représentation de sortie $2\times2$. Comme nous l'avons généralisé, en supposant que la forme de l'entrée est $n_h\times n_w$ et que la forme du noyau de convolution est $k_h\times k_w$, alors la forme de la sortie sera $(n_h-k_h+1) \times (n_w-k_w+1)$. Par conséquent, la forme de la sortie de la couche convolutive est déterminée par la forme de l'entrée et de la forme du noyau de convolution.
Dans plusieurs cas, nous incorporons des techniques, y compris le padding et les convolutions dite (strided), qui affectent la taille de la sortie. En guise de motivation, notons que puisque les noyaux ont généralement une largeur et une hauteur supérieures à $1$, après avoir appliqué de nombreuses convolutions successives, nous avons tendance à nous retrouver avec des sorties considérablement plus petites que notre entrée. Si nous partons d'une image de $240 \times 240$ pixels, $10$ couches de $5 \times 5$ convolutions réduisent l'image à $200 \times 200$ pixels, coupant $30 \%$ de l'image et oblitérant ainsi toute information intéressante sur les limites de l'image originale. Le padding est l'outil le plus populaire pour traiter ce problème.
Dans d'autres cas, nous pouvons vouloir réduire radicalement la dimensionnalité. La convolution strided est une technique populaire qui peut aider dans ces cas.
Un problème délicat est que nous avons tendance à perdre des pixels sur le périmètre de notre image. Comme nous utilisons généralement de petits noyaux, pour une convolution donnée, nous pouvons ne perdre que quelques pixels, mais cela peut s'accumuler au fur et à mesure que nous appliquons de nombreuses couches convolutives successives. Une solution directe à ce problème est d'ajouter des pixels supplémentaires de remplissage autour de la limite de notre image d'entrée, augmentant ainsi la taille effective de l'image. Typiquement, nous fixons les valeurs des pixels supplémentaires à zéro. Sur l'image suivante, nous remplissons une image d'entrée de $3 \times 3$, ce qui augmente sa taille à $5 \times 5$. La sortie correspondante passe alors à une matrice de $4 \times 4$. Les parties ombrées sont le premier élément de sortie ainsi que les éléments tensoriels d'entrée et de noyau utilisés pour le calcul de la sortie : $0\times0+0\times1+0\times2+0\times3=0$.
En général, si nous ajoutons un total de $p_h$ rangées de padding (à peu près la moitié en haut et la moitié en bas) et un total de $p_w$ colonnes de padding (environ la moitié à gauche et la moitié à droite), la forme de sortie sera
$$(n_h-k_h+p_h+1)\times(n_w-k_w+p_w+1).$$Cela signifie que la hauteur et la largeur de la sortie augmentera respectivement de $p_h$ et $p_w$.
Dans de nombreux cas, nous voudrons définir $p_h=k_h-1$ et $p_w=k_w-1$. pour que l'entrée et la sortie aient la même hauteur et la même largeur. Il sera ainsi plus facile de prévoir la forme de la sortie de chaque couche lors de la construction du réseau. lors de la construction du réseau. En supposant que $k_h$ est impair ici, nous ajouterons $p_h/2$ lignes de part et d'autre de la hauteur. Si $k_h$ est pair, une possibilité est de de remplir $\lceil p_h/2\rceil$ lignes sur le haut de l'entrée et $\lfloor p_h/2\rfloor$ lignes sur le bas. Nous remplirons les deux côtés de la largeur de la même manière.
Les CNN utilisent généralement des noyaux de convolution avec des valeurs de hauteur et de largeur impaires, telles que $1$, $3$, $5$ ou $7$. Le choix de tailles de noyau impaires présente l'avantage que nous pouvons préserver la dimensionnalité spatiale tout en ayant le même nombre de lignes en haut et en bas, et le même nombre de colonnes à gauche et à droite.
De plus, cette pratique consistant à utiliser des noyaux impairs et le padding pour préserver précisément la dimensionnalité offre un avantage. Pour tout tenseur bidimensionnel X
, lorsque la taille du noyau est impaire et que le nombre de lignes et de colonnes de pdding sur tous les côtés sont les mêmes, produisant une sortie avec la même hauteur et largeur que l'entrée, on sait que la sortie Y[i, j]
est calculée par corrélation-croisée de l'entrée et du noyau de convolution avec la fenêtre centrée sur X[i, j]
.
Dans l'exemple suivant, nous créons une couche convolutionnelle bidimensionnelle avec une hauteur et une largeur de $3$ et appliquer $1$ pixel de padding sur tous les côtés. Étant donné une entrée avec une hauteur et une largeur de $8$, nous constatons que la hauteur et la largeur de la sortie sont également de $8$.
import torch
from torch import nn
# We define a convenience function to calculate the convolutional layer. This
# function initializes the convolutional layer weights and performs
# corresponding dimensionality elevations and reductions on the input and
# output
def comp_conv2d(conv2d, X):
# Here (1, 1) indicates that the batch size and the number of channels
# are both 1
X = X.reshape((1, 1) + X.shape)
Y = conv2d(X)
# Exclude the first two dimensions that do not interest us: examples and
# channels
return Y.reshape(Y.shape[2:])
# Note that here 1 row or column is padded on either side, so a total of 2
# rows or columns are added
conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1)
X = torch.rand(size=(8, 8))
comp_conv2d(conv2d, X).shape
Lorsque la hauteur et la largeur du noyau de convolution sont différentes, nous pouvons faire en sorte que la sortie et l'entrée aient la même hauteur et la même largeur en réglant des nombres de remplissage différents pour la hauteur et la largeur.
# Here, we use a convolution kernel with a height of 5 and a width of 3. The
# padding numbers on either side of the height and width are 2 and 1,
# respectively
conv2d = nn.Conv2d(1, 1, kernel_size=(5, 3), padding=(2, 1))
comp_conv2d(conv2d, X).shape
Lors du calcul d'une convolution, nous commençons par la fenêtre au coin supérieur gauche du tenseur d'entrée, puis nous la faisons glisser sur tous les emplacements vers le bas et vers la droite. Dans les exemples précédents, nous avons choisi par défaut de faire glisser un élément à la fois. Cependant, parfois, soit pour des raisons d'efficacité informatique ou parce que nous souhaitons réduire l'échantillonnage, nous déplaçons notre fenêtre de plus d'un élément à la fois, en sautant les emplacements intermédiaires.
Le nombre de lignes et de colonnes traversées par glissement est appelé "stride". Nous avons utilisé des strides de $1$, tant pour la hauteur que pour la largeur. Parfois, nous pouvons vouloir utiliser un stride plus grand. La figure montre une opération de convolution avec un stride de $3$ verticalement et $2$ horizontalement. Les parties dites shaded sont les éléments de sortie ainsi que les éléments tenseurs d'entrée et de noyau utilisés pour le calcul de la sortie : $0\times0+0\times1+1\times2+2\times3=8$, $0\times0+6\times1+0\times2+0\times3=6$. Nous pouvons voir que lorsque le deuxième élément de la première colonne est sorti, la fenêtre de convolution glisse de trois rangées vers le bas. La fenêtre de convolution glisse de deux colonnes vers la droite lorsque le deuxième élément de la première ligne est sorti. Lorsque la fenêtre de convolution continue à glisser de deux colonnes vers la droite sur l'entrée, il n'y a pas de sortie car l'élément d'entrée ne peut pas remplir la fenêtre (sauf si nous ajoutons une autre colonne par padding).
En général, lorsque le pas de la hauteur est de $s_h$ et celui de la largeur de $s_w$. et le pas pour la largeur est $s_w$, la forme de sortie est
$$\lfloor(n_h-k_h+p_h+s_h)/s_h\rfloor \times \lfloor(n_w-k_w+p_w+s_w)/s_w\rfloor.$$Si nous fixons $p_h=k_h-1$ et $p_w=k_w-1$, alors la forme de la sortie sera simplifiée en $\lfloor(n_h+s_h-1)/s_h\rfloor \times \lfloor(n_w+s_w-1)/s_w\rfloor$.
En allant un peu plus loin, si la hauteur et la largeur d'entrée sont divisibles par les strides de la hauteur et de la largeur, alors la forme de sortie sera $(n_h/s_h) \times (n_w/s_w)$.
Ci-dessous, nous établissons les pas sur la hauteur et la largeur à 2, ce qui divise par deux la hauteur et la largeur d'entrée.
conv2d = nn.Conv2d(1, 1, kernel_size=3, padding=1, stride=2)
comp_conv2d(conv2d, X).shape
Ensuite, nous allons examiner un exemple légèrement plus compliqué.
conv2d = nn.Conv2d(1, 1, kernel_size=(3, 5), padding=(0, 1), stride=(3, 4))
comp_conv2d(conv2d, X).shape
Lorsque le nombre de padding des deux côtés de la hauteur et de la largeur d'entrée est respectivement de $p_h$ et $p_w$, nous appelons le padding $(p_h, p_w)$. Plus précisément, lorsque $p_h = p_w = p$, le padding est de $p$. Lorsque les strides sur la hauteur et la largeur sont $s_h$ et $s_w$, respectivement, nous appelons le stride $(s_h, s_w)$. Plus précisément, lorsque $s_h = s_w = s$, le stride est de $s$. Par défaut, le padding est égal à $0$ et le stride à $1$. En pratique, nous utilisons rarement des strides ou des padding inhomogènes, c'est-à-dire que nous avons généralement $p_h = p_w$ et $s_h = s_w$.
Le padding peut augmenter la hauteur et la largeur de la sortie. Il est souvent utilisé pour donner à la sortie la même hauteur et la même largeur que l'entrée.
Le stride peut réduire la résolution de la sortie, par exemple en réduisant la hauteur et la largeur de la sortie à seulement $1/n$ de la hauteur et de la largeur de l'entrée ($n$ est un entier supérieur à $1$).
Le padding et le stride peuvent être utilisés pour ajuster efficacement la dimensionnalité des données.
Souvent, nous voulons progressivement réduire la résolution spatiale de nos représentations cachées, en agrégeant les informations de sorte que plus on monte dans le réseau.
Souvent, notre tâche ultime pose une question globale sur l'image, par exemple : "Est-ce qu'elle contient un chat ?". Donc typiquement, les unités de notre couche finale devraient être sensibles à l'ensemble de l'entrée. En agrégeant progressivement les informations, nous accomplissons ce but d'apprendre finalement une représentation globale, tout en conservant tous les avantages des couches convolutionnelles dans les couches intermédiaires de traitement.
De plus, lors de la détection de caractéristiques de plus bas niveau, telles que les bords, nous voulons souvent que nos représentations soient quelque peu invariantes à la translation. Par exemple, si nous prenons l'image X
avec une délimitation nette entre le noir et le blanc et qu'on déplace l'image entière d'un pixel vers la droite, c'est-à-dire, Z[i, j] = X[i, j + 1]
, alors le résultat de la nouvelle image Z
sera très différent. Le bord se sera déplacé d'un pixel. En réalité, les objets ne se trouvent presque jamais exactement au même endroit.
En fait, même avec un trépied et un objet stationnaire, les vibrations de l'appareil photo dues au mouvement du shutter peuvent tout décaler d'un pixel ou plus (les appareils photo haut de gamme sont dotés de fonctions spéciales pour résoudre ce problème).
Nous présentons ici, les couches de pooling, qui ont le double objectif de d'atténuer la sensibilité des couches convolutionnelles à l'emplacement et de réduire l'échantillonnage spatial des représentations.
import torch
from torch import nn
from d2l import torch as d2l
def pool2d(X, pool_size, mode='max'):
p_h, p_w = pool_size
Y = torch.zeros((X.shape[0] - p_h + 1, X.shape[1] - p_w + 1))
for i in range(Y.shape[0]):
for j in range(Y.shape[1]):
if mode == 'max':
Y[i, j] = X[i: i + p_h, j: j + p_w].max()
elif mode == 'avg':
Y[i, j] = X[i: i + p_h, j: j + p_w].mean()
return Y
X = torch.tensor([[0.0, 1.0, 2.0], [3.0, 4.0, 5.0], [6.0, 7.0, 8.0]])
pool2d(X, (2, 2))
pool2d(X, (2, 2), 'avg')