Vegan or not Vegan? Faire la différence avec du Deep Learning

Rémi Connesson
France School of AI
9 min readFeb 10, 2019

--

Pour le kiff, j’ai décidé de récupérer 20000 images sur Instagram et d’entraîner un réseau de neurones à faire la différence entre des plats qui sont vegans et des plats qui ne le sont pas.

Le github associé à cet article se trouve ici.

J’ai fait ça dans le cadre du challenge sur la semaine 1 du cours Fast.AI v3 qui se tient sur le forum de France School of AI. Tu peux aller y jeter un oeil pour voir ce qu’ont fait les autres membres.

Pourquoi Instagram

J’avais déjà, par le passé, entraîné un réseau de neurones sur des images récoltées sur Google Images. Pour varier les plaisirs, j’ai réfléchi à une autre source d’images et j’ai pensé à… Instagram.

J’avais vraiment envie d’essayer car quand on y réfléchit, Instagram c’est des millions d’images disponnibles et qui sont déjà relativement triées.

Le premier type de tri qui nous vient à l’esprit c’est le tri par #hashtag.

Récolter par hashtag?

Ce qui m’a géné avec ce type de tri c’est que les photos proviennent de photographes amateurs et du coup on y trouve un peu n’importe quoi.

Le #Hastag vegan food… Des plats amateurs, des plats dans des tupperware, des gens (???).

Mais surtout c’est qu’il n’est pas possible de faire aisaiement une recherche par exclusion comme sur google image, exemple: “Donnne moi toutes les photos tagués #food mais pas #vegan” pour créer le dataset de contre-exemples.

La solution, les comptes qui font de la curation

Certains utilisateurs décident de reposter du contenu d’un genre spécifique, généralement les photos sont belles et tendent vers le même idéal esthétique bien que le contenu soit ultra-diversifié.

Des comptes spécialisés dans la curation de contenu #foodporn
Des comptes spécialisés dans la curation de contenu #vegan

On se rend compte que le problème n’est pas si simple à résoudre que ça même pour un humain. En effet certains chefs vegans crééent, avec beaucoup de talent, des plats dans le but de faire illusion de la présence de viande.

Voici la liste des comptes que j’ai séléctionné pour créer mon dataset

Comptes de plats vegans

Comptes de plats homnivores

Récupérer les données

Pour récupér les données j’ai utilisé un scraper open source.

Ultra-pratique j’ai été étonné par sa simplicité d’utilisations. C’est même encore plus simple que de récolter sur Google Images. (Je pensais que ça allait être l’inverse, comme quoi…)

Attention pour télécharger des images de bonnes qualité il faut fournir à l’outil un compte instagram valide (username et mot de passe). Si vous souhaitez reproduire la même chose chez vous, utilisez un compte Instagram auquel vous ne tenez pas plus que ça, n’utilisez pas votre compte principal.

Méthodologie pour entraîner son réseau de neurones

Rentrons dans le vif du sujet, l’entraînement du réseau de neurones. Je me suis basé sur le notebook créé par Jeremy Howard pour la semaine 1 de Fast.AI

TIPS: Avant de passer beaucoup de temps à entraîner les réseaux sur une grosse quantité de données, il est souvent rusé de commencer à le faire fonctionner sur un échantillon de données. Ca permet de vérifier que tout marche comme prévu.

Dans la suite de cet article, on va se concentrer sur un échantillon de 300 images. Vous pouvez accéder au code sur le github associé à cet article, lase trouve ici. Il s’y trouve le code pour entraîner sur un échantillon, et le code pour entraîner sur le dataset de 20000 images.

On commence par vérifier que les images ne sont pas corrompues

La nouvelle librairie Fast.AI qui utilise Pytorch 1.0 en backend fournit quelques utilitaires très sympathique, comme par exemple la fonction verify_images(), qui permet de supprimer d’un dossier toutes les images qui ne sont pas au format RGB ou qu’on ne peut ouvrir pour une raison quelconque.

# vegan_dir est le chemin d'accès au repertoire contenant..
# .. les photos de plats vegans.
verify_images(vegan_dir)

Ensuite, charger les images en mémoire

La librairie Fast.AI fournit une classe ImageDataBunch qui nous donne accès à des méthodes permettant de charger son dataset très rapidement.

Ici on utilise la méthode .from_folder() qui nous permet en un seul coup de :
1. Charger les images depuis un dossier
2. Définir le ratio d’images à garder pour l’introduire dans le set de validation.
3. D’uniformiser la taille de nos images à 224 pixels.
4. D’appliquer des transformations légères qui nous permettent de dé-multiplier la taille de notre dataset. (c’est la data-augmentation)

On l’enchaîne tout de suite avec la méthode .normalize() qui nous permet de modifier les valeurs des pixels de nos images pour qu’elles correspondent à ce que le réseau de neurone qu’on va utiliser s’attend à voir. (c’est la normalization)

# En une ligne on a notre pipeline de preprocessing..
# ..qui est opérationnelle.
data = ImageDataBunch.from_folder(p, train='data', valid_pct=0.2, size=224, ds_tfms=get_transforms()).normalize(imagenet_stats)

Ensuite avec la méthode .show_batch() on peut visualiser le résultat de notre pré-traitement des données. (le preprocessing)

Miam! Ça me donne faim!

Il est temps de créer le réseau de neurones

Pour ce faire, on appelle la fonction create_cnn() en lui précisant:
1. La référence de nos données une fois qu’elles ont été prétraîtées
2. L’architecture pré-entraînée qu’on souhaite utiliser (ici un Resnet34)
3. Ce qu’on essaie de faire avec le réseau. (Dans notre cas on veut qu’il se trompe le moins souvent possible quand il essaie de deviner si un plat est végan, ou pas.)

learn = create_cnn(data, models.resnet34, metrics=error_rate)

Ne pas se précipiter, tâter le terrain avant d’avancer

Notre but est d’entraîner notre réseau pour obtenir de bons résultats le plus vite possible. On va donc devoir jouer avec un hyper-paramètres qui nous permet d’influencer la vitesse d’apprentissage du réseau. Cette vitesse s’apelle le learning rate en anglais.

Plus cette vitesse est élévée, plus l’entraînement est instable et risque de “dérailler”.

À l’inverse, plus cette vitesse est faible, plus l’entraînement est stable et plus on a de chances d’arriver à “bon port” mais par contre, plus on doit attendre…

Et on n’aime pas attendre.

C’est pour ça qu’on va commencer par chercher jusqu’où on peut appuyer sur l’accélérateur sans “dérailler” notre entraînement.

Pour ça on va utiliser la méthode la technique du Learning Rate Finder, pour cette technique il faut procéder en deux temps.
1. On appelle lr_find() pour faire des essais de vitesse
2. Puis on appelle .record.plot() pour afficher le résultat et idfentifier une zone de vitesse sûre

learn.lr_find()
learn.record.plot()
Le résultat de lr_find(), en jaune l’entraînement a déraillé.

Là on voit qu’on peut aller aussi vite que 2e-02 (soit 0.02) avant que l’entraînement ne risque fortement de dérailler.

Première phase de l’entraînement

On va entrainer notre réseau par cycle, on commence avec un learning_rate qui vaut 2e-2 (soit 0.02) puis qui ralenti jusqu’à atteindre 5e-3 (soit 0.005).
Dans cet exemple, on choisit de faire durer le cycle 8 epochs.

Dans le jargon une epoch est une unité de temps qui correpond au temps qu’il faut pour montrer la totalité du dataset d’entraînement au modèle.

Dire qu’on entraîne pour huit epochs signifie qu’on va montrer le dataset en entier huit fois au modèle.

TIPS: Pensez à bien sauvegarder le modèle après un cycle avec un nom différent (et identifiable) à chaque fois. Comme ça si à l’étape suivante on fait dérailler l’entraînement, on peut revenir avant l’incident grace à la sauvegarde.

# On entraine le modèle pour huit epochs
learn.fit_one_cycle(8, max_lr=slice(5e-3, 2e-2))
# On sauvegarde le résultat du cycle
learn.save('frozen-1')
Le rapport d’entraînement sur un cycle

Interlude : Comment lire le rapport d’entraînement

L’entraînement nous fournit un rapport de ce qu’il s’est passé, il contient généralement 3 rubriques:
Train Loss: C’est le score obtenu par le modèle lors d’une epoch sur les données d’entraînement, le but est de l’abaisser le plus possible.
Valid Loss: C’est le score obtenu par le modèle lors d’une epoch sur les données d’entraînement, le but est de l’abaisser le plus possible.
Error Rate: C’est le taux d’images pour lesquelles le modèle se trompe, le but est de l’abaisser le plus possible.

En résumé le but est de faire descendre ces trois valeurs, tant que les trois descendent tout va bien. On peut continuer.

Par contre, dès que l’une de ces valeurs arrête de diminuer, c’est généralement un signal qu’il est temps de réviser sa stratégie ou bien de s’arrêter là, si on a plus aucun tour dans notre sac.

Et si l’une des valeurs se remet à augmenter brusquement c’est que l’entraînement a déraillé. Dans ce cas, il faut revenir en arrière voir recommencer à zéro si on a pas fait de sauvegardes.

Seconde phase facultative de l’entraînement

Après avoir enchainé un ou plusieurs cycles et constaté qu’on ne fait plus vraiment de progrès, on peut soit:
• S’arrêter là si les résultats nous conviennent
• Tenter d’améliorer encore un petit peu les scores

Pour tenter d’améliorer les scores encore plus on va entraîner les couches profondes de notre réseau, qui en constitue la plus grosse partie.

Jusqu’à présent nous n’avions entraîné que les couches superficielles.

Pour ce faire on va utiliser la méthode .unfreeze() puis on va, comme dans la première partie, identifier la zone sûre pour notre learning rate.

learn.unfreeze()

learn.lr_find()
learn.recorder.plot()
Cette fois il va falloir être plus prudent

D’après le learning rate finder, on peut aller sans trop de problème jusqu’à 2e-4 (soit 0.0004). Pour donner un ordre de comparaison c’est environ cent fois plus lent que tout à l’heure.

# Et c'est reparti pour huit epochs.
learn.fit_one_cycle(8, max_lr=slice(1e-6,2e-4))
# Et on n'oublie pas de sauvegarder
learn.save('unfrozen-1')

Bon c’est pas mal, ça nous a permis d’améliorer notre score d’erreur de trois points. En guise d’observations: on remarque que la valid loss commence à remonter, on aurait sûrement pu s’arrêter avant. On remarque aussi que l’error rate a atteint un plateau. Il sera probablement difficile de faire mieux.

TIPS: Il est important de toujours remettre en perspective les nombres qu’on observe.

Par exemple l’error rate.

Il ne veut pas dire la même chose si l’on considère un validation set de 50 images ou l’un de 5000 images.

• Sur 50 images, 0.01 de variation représente 0.5 image. Ca ne veut rien dire.
• Sur 5000 images, 0.01 de variation représente 50 images. Ca a déjà plus de sens.

Interpréter les résultats

Maitenant il est temps de jetter un oeil aux données pour essayer de comprendre ce qu’il se passe dans le réseau de neurones.

Pour ça on va utiliser .interpret() pour obtenir les résultats sur le valdiation set et .plot_top_losses() pour afficher les images sur lesquelles on s’est le plus trompé.

# On obtient les résultats sur le validation set.
interp = learn.interpret()
# Affiche les images sur lequel on a le plus de perte
interp.plot_top_losses(k=2)
Ces deux plats sont vegan, si on ne me l’avait pas dit je ne l’aurai sûrement pas trouver non plus.

Ca t’a plu? Toi aussi, essaye de faire pareil !

Rejoins nous, et redresse le défi :)

--

--

Rémi Connesson
France School of AI

I use Roam + Notion along my way of building expertise in marketing and ML ops. You can tag along the journey here https://tinyletter.com/remiconnesson :)