Mode de soumission : Turnin TP2
Total de points : 30
Ce projet sert d’introduction aux réseaux de neurones profonds.
Le code pour ce projet contient les fichiers ci-dessous, disponibles dans un fichier compressé.
Fichiers à modifier :
models.py
Contient les réseaux de neurones pour les différentes applications
Fichiers à lire seulement (NE PAS modifier) :
nn.py
Mini-librairie de réseau de neurones
Autres fichiers :
autograder.py
Auto-correcteur du projet
backend.py
Backend pour différentes tâches
data
Contient les différentes données d’entraînement
Fichiers à modifier et soumettre : vous devez
remplir les sections manquantes du fichier models.py
. Il
faut ne faut pas modifier les autres fichiers.
Évaluation : l’auto-correcteur s’assure du bon fonctionnement de votre code. Ne changez aucun nom de fonction ou nom de classe dans le code, sans quoi l’auto-correcteur ne fonctionnera pas. L’auto-correcteur ne détermine pas entièrement votre résultat final. La qualité de votre implémentation - et non les résultats obtenus par l’auto-correcteur - déterminent votre résultat final.
Utilisation des données : une partie des notes obtenues dépend de la performance de votre modèle sur l’ensemble de test. La base de code n’offre aucun API permettant d’accéder à cet ensemble directement. Par conséquent, toute tentative de modification des données de test sera considérée comme de la tricherie et sera sévèrement pénalisée en conséquence.
Pour ce projet, vous devez installer les bibliothèques suivantes :
numpy, une librairie de calcul matriciel - installation
matplotlib, une librairie de visualisation - installation
PyTorch, un framework de machine learning - installation
pip install numpy
pip install matplotlib
pip install torch
Contrairement au TP1, vous ne devriez pas utiliser ces librairies directement (a l’exception de PyTorch pour la question 4). Elles sont toutefois nécessaires au fonctionnement de l’auto-correcteur et du backend.
Pour vérifier l’installation des dépendances, exécutez la commande suivante :
python autograder.py --check-dependencies
Vous devriez alors observer une fenêtre s’ouvrir avec un ligne qui tourne dans un cercle
Pour ce projet, vous avez accès à une mini-librairie de réseau de
neurones (nn.py
) ainsi qu’une collection de base de données
(backend.py
).
La librairie nn.py
définie une collection d’objets node.
Chaque node représente un nombre réel ou une matrice de nombres réels.
Les opérations sur les objets node sont optimisées et donc plus rapides
que les types primitifs de Python (comme les listes par exemple).
Lorsqu’on entraîne un réseau de neurones, on met à jour les paramètres après avoir effectué des prédictions sur plusieurs données contrairement au perceptron du TP1 où les poids étaient affectés après chaque prédiction. Cette procédure d’entraînement par lot permet ainsi au réseau de mieux se généraliser et de ne pas se sur-spécialiser sur chacune des observations singulières. L’argument \(\text{batch_size}\) réfère au nombre d’observations utilisées pour mettre à jour le réseau. Aussi, veuillez noter que le paramètre \(\text{num_features}\) dénote le nombre d’attribut ou de variables associées à chaque donnée.
Voici une brève description de l’API :
nn.Constant
représente une matrice de nombres réels
utilisée pour représenter les données en entrée, la sortie des modèles
d’apprentissage, ou les étiquettes.nn.Parameter
représente les paramètres qu’on optimise
(par exemple, la matrice W dans le perceptron multi-classe du TP1).nn.DotProduct
calcule le produit scalaire.nn.as_scalar
peut extraire un nombre à virgule flotante
d’un node.nn.Add
effectue une addition de matrice.
nn.Add(x, y)
accepte deux node de
dimension \(\text{batch_size} \times
\text{num_features}\) et retourne un node de dimension \(\text{batch_size} \times
\text{num_features}\).nn.AddBias
ajoute un vecteur de biais.
nn.AddBias(features, bias)
accepte une
matrice features
de dimension \( \) et un biais
bias
de dimension \(1 \). Retourne un node de dimension \(
\).nn.Linear
applique une transformation linéaire
(multiplication de matrice) sur les arguments en entrée.
nn.Linear(features, weights)
accepte
features
de dimension \( \times \) et
weights
de dimension \( \times \), et retourne un node de
dimension ( \).nn.ReLU
applique la fonction Rectified Linear Unit
\(relu(x) = \max(x, 0)\) sur chaque élément de l’argument en entrée.
nn.ReLU(features)
, retourne un node avec
la même dimension que l’argument en entrée.nn.SquareLoss
calcule une perte quadratique sur un
batch (utilisée pour la régression).
nn.SquareLoss(a, b)
, où a
et
b
ont comme dimension \( \).nn.SoftmaxLoss
calcule la fonction softmax sur un batch
(utilisée pour les problèmes de classification).
nn.SoftmaxLoss(logits, labels)
, où
logits
et labels
ont comme dimension \( \). Le
terme “logits” refère aux scores produits par un modèle. Les étiquettes
“labels” doivent être non-négatives.nn.gradients
calcule le gradient d’une perte par
rapport aux paramètres passés comme arguments.
nn.gradients(loss, [parameter_1, parameter_2, ..., parameter_n])
retourne une liste
[gradient_1, gradient_2, ..., gradient_n]
, où chaque
élément est un nn.Constant
contenant le gradient de la
perte par rapport à un paramètre.nn.as_scalar
converti un node en type primitif Python
pouvant s’avérer utile comme critère d’arrêt lors de l’apprentissage.
nn.as_scalar(node)
, où node
est soit un node de perte ou possède la forme (1,1)
.Additionnellement, les instances de datasets
possèdent
les deux méthodes suivantes :
dataset.iterate_forever(batch_size)
génère une suite
infinie de batch pour un exemple.dataset.get_validation_accuracy()
retourne la métrique
de justesse (“accuracy”) de votre modèle sur l’ensemble de validation.
Cette métrique peut aussi être utile comme critère d’arrêt lors de
l’apprentissage.À titre d’exemple, essayons de trouver les paramètres d’une équation linéaire permettant de prédire les valeurs de \(\mathbf{Y}\). On commence par \(|\mathbf{Y}| = 4\). L’équation optimale obtenue devrait être \( y = 7x_0 + 8x_1 + 3 \) (pour vous en convaincre, vous pouvez effectuer les calculs et remarquer la différence entre la prédiction et la valeur réelle). Autrement dit, à partir d’une matrice de données \(\mathbf{X}\), on essaie de trouver les paramètres \(m_0\), \(m_1\) et \(b\) qui permettent de prédire fidèlement chaque \(y \in \mathbf{Y}\).
\[\mathbf{X} = \begin{bmatrix} 0 & 0 \\ 0 & 1 \\ 1 & 0 \\ 1 & 1 \end{bmatrix} \quad \text{et} \quad \mathbf{Y} = \begin{bmatrix} 3 \\ 11 \\ 10 \\ 18 \end{bmatrix}\]
Supposons que les données sont fournies en node
nn.Constant
:
>>> x
<Constant shape=4x2 at 0x10a30fe80>
>>> y
<Constant shape=4x1 at 0x10a30fef0>
Nous entraînons un modèle de la forme \( f(x) = x_0 m_0 + x_1 m_1 +b \).
D’abord, on crée nos paramètres optimisables. Sous forme de matrice, ils sont de la forme suivante:
\[\mathbf{M} = \begin{bmatrix}m_0 \\ m_1 \end{bmatrix} \quad \text{et} \quad\mathbf{B} =\begin{bmatrix} b \end{bmatrix}\]
Ces formules se traduisent en code comme suit :
m = nn.Parameter(2, 1)
b = nn.Parameter(1, 1)
Leur interprétation en chaîne de caractères devrait donner :
>>> m
<Parameter shape=2x1 at 0x112b8b208>
>>> b
<Parameter shape=1x1 at 0x112b8beb8>
Ensuite, on calcule une prédiction de notre modèle pour un y :
xm = nn.Linear(x, m)
y_hat = nn.AddBias(xm, b)
Le but est que notre prédiction soit exacte, c’est-à-dire \(\hat{y} = y\). En régression linéaire, une telle tâche est réalisée en minimisant la somme des différences au carré : \( \mathcal{L} = \frac{1}{2N} \sum_{(x, y)} (y - f(x))^2 \).
On construit donc un node de type loss :
loss = nn.SquareLoss(y_hat, y)
La base de code permet le gradient de la perte en fonction des paramètres :
grad_wrt_m, grad_wrt_b = nn.gradients(loss, [m, b])
L’impression des gradients devrait donner :
>>> xm
<Linear shape=4x1 at 0x11a869588>
>>> y_hat
<AddBias shape=4x1 at 0x11c23aa90>
>>> loss
<SquareLoss shape=() at 0x11c23a240>
>>> grad_wrt_m
<Constant shape=2x1 at 0x11a8cb160>
>>> grad_wrt_b
<Constant shape=1x1 at 0x11a8cb588>
On utilise la méthode update
afin de mettre en jour les
paramètres. Voici un exemple de mise à jour de m
(on assume
qu’une variable multiplier
a été initialisée avec un taux
d’apprentissage adéquat) :
m.update(grad_wrt_m, multiplier)
Si on inclut la mise à jour de b
et qu’on répète les
opérations précédentes dans une structure itérative, on retrouve une
procédure complète de régression linéaire.
Lors de l’entraînement du réseau, vous recevez une instance de
dataset
pour laquelle vous retrouvez les batch
d’entraînement en appelant dataset.iterate_once(batch_size)
:
for x, y in dataset.iterate_once(batch_size):
...
L’exemple suivant extrait un batch size de 1 (c-à-d, un seul exemple d’entraînement) :
>>> batch_size = 1
>>> for x, y in dataset.iterate_once(batch_size):
... print(x)
... print(y)
... break
...
<Constant shape=1x3 at 0x11a8856a0>
<Constant shape=1x1 at 0x11a89efd0>
Les données d’entrées x
et les étiquettes associées
y
sont données sur la forme de node de type
nn.Constant
. Le format de x
est
(batch_size, num_features)
, et le format de y
est (batch_size, num_outputs)
. Voici un exemple de produit
scalaire de x
avec lui-même représenté d’abord en tant que
node et ensuite en tant que type primitif Python.
>>> nn.DotProduct(x, x)
<DotProduct shape=1x1 at 0x11a89edd8>
>>> nn.as_scalar(nn.DotProduct(x, x))
1.9756581717465536
Dans le projet, vous devrez implémenter les modèles suivants :
Votre implémentation du réseau de neurones passe par
nn.py
. Comme nous avons vu dans le cours, un réseau de
neurones de base possède une ou plusieurs couches qui effectuent toutes
une opération linéaire (comme le perceptron). Les couches sont séparées
par une fonction d’activation non-linéaire qui permet au réseau
de modéliser des fonctions complexes. Nous utiliserons la fonction ReLU
comme fonction d’activation. Celle-ci est définie comme \(relu(x) = \max(x, 0)\). Par exemple, la
sortie \(\mathbf{f}(\mathbf{x})\) d’un
réseau simple à deux couches pour une donnée \(\mathbf{x}\) est donnée par:
\[\mathbf{f}(\mathbf{x}) = relu(\mathbf{x} \cdot \mathbf{W_1} + \mathbf{b_1}) \cdot \mathbf{W_2} + \mathbf{b}_2\]
Ce réseau optimise les paramètres \(\mathbf{W_1}\), \(\mathbf{W_2}\), \(\mathbf{b}_1\) et \(\mathbf{b}_2\) par la descente de gradient stochastique. \(\mathbf{W_1}\) est une matrice de format \(i \times h\) où \(i\) et \(h\) correspondent respectivement au nombre de lignes du vecteur \(\mathbf{x}\) et la taille de la couche cachée. \(\mathbf{b_1}\) est un vecteur de dimension \(h\). Le choix de \(h\) est laissé à la discrétion du programmeur ou de la programmeuse. Vous devez toutefois vous assurer que les dimensions entre les couches soient adéquates sans quoi le produit scalaire ne fonctionnera pas ou retournera une matrice de dimension absurde. L’utilisation d’un grand \(h\) permet généralement un plus grand pouvoir représentational du modèle, mais ajoute plus de paramètres à optimiser en plus de dégénérer vers un modèle qui se généralise mal sur des données nouvelles (sur-apprentissage).
On peut créer des réseaux plus profonds en ajoutant des couches. Par exemple, pour un réseau à trois couches, on a : \[\mathbf{f}(\mathbf{x}) = relu(relu(\mathbf{x} \cdot \mathbf{W_1} + \mathbf{b_1}) \cdot \mathbf{W_2} + \mathbf{b}_2) \cdot \mathbf{W_3} + \mathbf{b_3}\]
Contrairement au perceptron, où vous optimisiez les paramètres après chaque exemple d’entraînement, vous devez maintenant optimiser le réseau en batch, c’est-à-dire en prenant plusieurs exemples d’entraînement au-lieu d’un seul. Formellement, au-lieu de mettre à jour les poids après chaque entrée \(x\) de taille D, vous devez considérer un ensemble de N d’entrées représenté comme une matrice \(X\) de dimension \(N \times D\).
Au premier TP, nous initialisions les paramètres par des valeurs nulles, ce qui est généralement déconseillé. En pratique, les paramètres du réseau de neurones sont initialisés pseudo-aléatoirement. En conséquent, vous pouvez être malchanceux et échouer certaines tâches de classification avec une très bonne architecture (problème des minima locaux). Cependant, ce résultat est plutôt rare dans notre contexte. Donc, une série de résultats négatifs avec l’auto-correcteur devrait être un indicateur que votre modèle n’est pas bien implémenté ou que vous devriez explorer d’autres architectures.
Voici quelques astuces pour vous aider dans l’élaboration des réseaux de neurones :
Soyez systématique et conservez une trace de chaque architecture essayée avec leurs hyper-paramètres (taille des couches, taux d’apprentissage, etc.) respectifs, ainsi que les résultats obtenus.
Les réseaux profonds dépendent d’une multitude de paramètres et une mauvaise combinaison de ceux-ci peut nuire à leur performance. Commencez donc par un réseau simple (seulement deux couches et une seule activation) afin de déterminer un bon taux d’apprentissage et un bon nombre de neurones pour la couche cachée. Vous pouvez ensuite complexifier avec plus de couches ayant des dimensions similaires.
Votre taux d’apprentissage est l’hyper-paramètre le plus déterminant. S’il est mauvais, le choix des autres hyper-paramètres importera peu. Vous pouvez utiliser la meilleure architecture connue et modifier le taux d’apprentissage de telle sorte que ses performances deviennent gênantes. Un taux trop lent produit un modèle qui converge très lentement; par contraste, un taux trop rapide ne convergera probablement jamais. Commencez en essayant différents taux et observez la courbe d’apprentissage, c’est-à-dire comment la perte évolue après chaque itération. Si votre perte augmente, c’est signe que votre taux d’apprentissage est trop élevé.
Des tailles de batch plus petites demandent des taux d’apprentissage petits.
Vos couches cachées ne devraient pas avoir trop de neurones sans quoi la précision du réseau diminuera, mais le temps d’entraînement augmentera. L’auto-correcteur devrait demander au plus entre 2 et 12 minutes.
Si votre modèle retourne des valeurs abbérantes comme Infinity ou NaN, votre taux d’apprentissage est probablement trop élevé.
Valeurs recommandées pour les hyper-paramètres :
Pour cette question, vous entrainerez un réseau de neurones afin d’approximer la fonction \(\frac{x\text{cos}(x)}{4}\) sur l’intervalle \([-2\pi,2\pi]\).
Vous devrez compléter l’implémentation de la classe
RegressionModel
dans models.py
. Pour ce
problème, une architecture relativement simple devrait suffir. Utilisez
nn.SquareLoss
comme perte.
Tâches :
RegressionModel.__init__
avec toute
initialization nécessaire;RegressionModel.run
pour renvoyer un noeud
\(\text{batch_size} \times 1\) qui
représente les predictions de votre modèle;RegressionModel.get_loss
qui retourne la
perte pour les entrées et les cibles données;RegressionModel.train
, qui doit entrainer
votre modèle en utilisant des mise-à-jours basées sur les
gradients.Il n’y a qu’un seul jeu de données pour cette tâche (pas de
validation ni de test car les données peuvent etre générées à la volée).
Votre implémentation doit obtenir une perte inférieure ou égale à 0.02
pour avoir tous les points. Vous pouvez utiliser la perte sur l’ensemble
d’entrainement pour déterminer quand vous arrêter (utilisez
nn.as_scalar
pour convertir un loss node en un nombre
Python). Notez que le modèle peut prendre plusieurs minutes à être
entrainé.
Pour tester votre implémentation, utilisez l’auto-correcteur comme suit :
python autograder.py -q q1
Code fonctionnel (3pts)
Perte sur le jeu de test <= 0.02 (5pts)
Qualité / lisibilité du code (1pt)
Pas de boucles for
inutiles (code vectorisé) (1pt)
Pour cette question, vous allez entrainer un réseau pour classifier des articles de mode du jeu de données Fashion-MNIST qui contient 60,000 exemples d’entraînement et 10,000 exemples de test.
Chaque image est de taille \(28 \times 28\) pixels, representée par un vecteur réel de dimension \(784\). Chaque cible fournie est un vecteur de dimension \(10\) avec des zéros pour toutes les valeurs à l’exception d’un \(1\) à la position de la classe correcte (One-Hot Encoding).
Complétez l’implémentation de la classe
FashionClassificationModel
dans models.py
.
FashionClassificationModel.run()
devrait retourner un node
\(\\text{batch\_size} \\times 10\)
contenant des scores, où un score plus élevé indique une plus grande
probabilité qu’un article appartienne à une classe particulière. Vous
devez utiliser nn.SoftmaxLoss
pour la perte. N’utilisez pas
de fonction d’activation (ReLU) sur la dernière couche de votre
réseau.
Pour cette question et la question 3, en plus des données
d’entrainement, il y a aussi des jeux de données de validation et de
test. Vous pouvez utiliser
dataset.get_validation_accuracy()
pour calculer la
précision de votre modèle sur le jeu de données de validation, ce qui
peut être utile pour décider quand arrêter l’entrainement. Le jeu de
données de test sera utilisé par l’auto-correcteur.
Pour recevoir les points sur cette question, votre modèle doit atteindre une précision de 80% (ou plus) sur le jeu de test. Notez que l’auto-correcteur vous note sur la précision sur le jeu de test. Ainsi, même si votre modèle atteint 80% de précision sur le jeu de validation, il peut obtenir moins sur le jeu de test. Il peut donc être utile d’arrêter l’entrainement au dela de 80% (81, 82%).
Tâches :
FashionClassificationModel.__init__
avec
toute initialization nécessaire;FashionClassificationModel.run
pour
renvoyer un noeud \(\text{batch_size} \times
10\) qui représente les predictions de votre modèle;FashionClassificationModel.get_loss
qui
retourne la perte pour les entrées et les cibles données;FashionClassificationModel.train
, qui doit
entrainer votre modèle en utilisant des mise-à-jours basées sur les
gradients.Pour tester votre implémentation, utilisez l’auto-correcteur comme suit :
python autograder.py -q q2
À titre de référence voici à quoi correspondent les étiquettes :
Étiquette | Article |
---|---|
0 | T-shirt/top |
1 | Trouser |
2 | Pullover |
3 | Dress |
4 | Coat |
5 | Sandal |
6 | Shirt |
7 | Sneaker |
8 | Bag |
9 | Ankle boot |
Code fonctionnel (3pts)
Précision sur le jeu de test >= 80% (5pts)
Qualité / lisibilité du code (1pt)
Pas de boucles for
inutiles (code vectorisé) (1pt)
Pour cette dernière question, vous allez concevoir l’architecture d’un CNN. Pour ce faire, contrairement aux questions précédentes, vous allez utiliser la bibliothèque PyTorch.
PyTorch vous permet de définir un modèle comme une suite de couches à
l’aide de la classe Sequential
(documentation).
Cette classe prend dans son constructeur les différentes couches du
modèle, puis les connecte automatiquement. Ce tutoriel
pourrait s’avérer utile.
Parmi les couches dont vous pourriez avoir besoin (vous pouvez aussi en utiliser d’autres si vous le souhaitez) :
Conv2d
(documentation)
: réalise une convolution sur les entrées 2D;MaxPool2d
(documentation)
: réalise un max-pooling sur les entrées 2D;AvgPool2d
(documentation)
: réalise un average-pooling sur les entrées 2D;ReLU
(documentation)
: applique la fonction ReLU sur toutes les entrées;Tanh
(documentation)
: applique la fonction Tanh sur toutes les entrées;Dropout
(documentation)
: désactive aléatoirement certains poids pour éviter le
sur-apprentissage;Flatten
(documentation)
: cette couche particulière change les dimensions de son entrée pour
transformer une matrice (ou un tenseur) en un vecteur;Linear
(documentation)
: réalise une convolution 2D sur les entrées;Vous allez devoir commencer votre modèle par une ou plusieurs couches
à convolution, puis finir votre modèle par une ou plusieurs couches
linéaires. Entre vos couches à convolution et vos couches linéaires,
vous devrez mettre la couche Flatten
afin de redimensionner
les entrées pour les couches linéaires.
Vous ne devez compléter QUE la création du modèle. La boucle d’entrainement a déjà été concue pour vous.
Tâches :
self.model = torch_nn.Sequential(...)
dans la classe
PyTorchCNNFashionClassificationModel
.Pour recevoir tous les points, votre architecture doit être capable d’atteindre une précision d’au moins 84% sur le jeu de test.
Pour tester votre implémentation, utilisez l’auto-correcteur comme suit :
python autograder.py -q q4
Code fonctionnel (3pts)
Précision sur le jeu de test >= 84% (5pts)
Qualité / lisibilité du code (2pts)