image semantic segmentation keras tensorflow Cours pratiques - Deep learning

Segmentation sémantique d’images

Introduction & code source Github

Je vous met ici l’ensemble du code source sur mon dépôt Github, avec un exemple concret de segmentation sémantique d’images sur la reconnaissance de carries sur des radiographies dentaire.

 

Que-ce que la segmentation sémantique ?

Ce type de segmentation consiste à classifier chaque pixel d’une image en un label. Plus d’informations ici.

 

Types de vos données

En entrée, nous allons envoyer à un réseau de neurones des images RGB. Plus d’informations sur la composition d’une image ici. Mais de façon concise, notre image RGB à la forme d’un tenseur d’ordre 3 et de dimensions ( hauteur x largeur x 3 ). Le 3 est le nombre de canal. 1 pour une image noir/blanc, 3 pour RGB. Un canal pour le rouge, un pour le vert, et un dernier pour le bleu. Chaque valeur de la matrice représente donc un pixel. Ce pixel qui à une valeur entre 0 et 255, selon l’intensité de la couleur d’un canal spécifique.

En sortie, on souhaite avoir une matrice de dimensions (hauteur x largeur  x 1), avec chaque pixel ayant n’ont pas une intensité entre 0 et 255, mais un nombre correspondant à nos classes. La valeur du pixel est donc compris entre 0 (indice du background) à n (nombre de classe dans nos images).

Une image sera plus parlante :

semantic segmentation image keras tensorflow

 

Dataset X et Y

Pour réaliser ce genre de classification on utilise de l’entrainement supervisé. Cela consiste à entrainer notre réseau de neurones sur des couples (X, Y).

  • X est votre image dans laquelle vous souhaitez analyser la présence ou non d’une classe
  • Y que l’on peut appeller son ‘étiquette’, est ce que l’on cherche à avoir. Cela correspond donc à nos masques contenant nos classes à détecter. Nous allons détailler juste après la notion de masque.

 

Labeliser les données

Nous avons précédemment parlé de masque. Un masque est une partie de votre image que l’on souhaite mettre en évidence. Dans notre cas, le masque sera un polygone représentant une carrie. Selon le type d’objet que vous souhaitez détecter, cela pourra avoir une forme de carré, cercle, etc.

Pour chacune de vos images, vous aller devoir donc générer les masques, et par la suite les labéliser, les annoter. Via des logiciels spécialisés (VoTT, SuperAnnotate, LabelMe, etc), vous allez pouvoir segmenter vos classes au sein de vos images. Voici un Example de segmentation de Carrie sur des radiographies :

image segmentation tools vott super annotate

 

A la suite de cette phase d’annotation, selon le logiciel que vous utilisez, il va vous générer un fichier (JSON dans mon cas, mais cela peut être dans le format que vous voulez). Celui-ci contiendra pour chaque photos un ensemble de coordonnées, de points (X,Y) correspondant aux formes géométriques que vous aurez dessiné sur le logiciel.

segmentation image labeling

Dans l’exemple précédent, on peut voir que pour une photo, j’ai le type de forme géométrique utilisé pour mon masque de carie, un ID de classe, une proba, et un tableau de coordonnées de points.

 

Génération des masques

Vous devez accorder la configuration de vos masques selon ce que vous souhaitez avoir en sortie du réseau. Vous allez utiliser une fonction d’activation spécifique afin de faire de la prédiction d’une valeur binaire, ou utiliser une autre fonction d’activation pour de la prédiction multi classes. Mais vous pouvez très bien utiliser l’une ou l’autre selon comment vous agencez vos données. On va voir cela dans les prochaines lignes.

Ces différences de configuration de masque concerne la fonction d’activation de la couche final de votre réseau. Et selon elle, vous devez accorder vos métriques et fonction de perte.

 

Génération du masque

Pour générer vos masques, vous allez devoir parcourir l’ensemble de vos radiographies une à une avec le fichier JSON de coordonnées qui lui est associé. Vous allez pouvoir tracer des masques  via plusieurs procédés. Le but est de remplir des matrices Numpy, avec la classe souhaités contenu dans chaque pixel.

Pour vous donnez des idées de librairies pouvant le faire facilement :

  • Skimage via Polygon2Mask
  • Pillow (PIL) via ImageDraw
  • Scipy
  • OpenCV
  • Matplotlib via points_inside_poly

 

Exemple d’un masque crée via Skimage :

image segmentation mask polygon 2d numpy

 

⚠️ Un point extrêmement important qui m’a bieeen fait galérer dans mes prédictions concernant la génération de mes masques sont le format de sauvegarde.

En effet pour les sauvegarder, privilégiez les formats PNG ou TIFF. Car JPG est un format comprimé et vous allez perdre de l’information voir pire avoir des changements de classe. TIFF est un format sans perte, un peu lourd. PNG est un format compressé mais sans perte, donc vous pouvez le choisir sans soucis.

 

Integer ou One hot encoding ?

Comment définir nos classes pour que notre réseau apprennent à les reconnaîtrais ? Vous pouvez avoir le choix selon votre type de segmentation, qu’elle soit binaire ou multi-classes. Comment donc définir nos classes dans nos matrices ?

 

Integer encoding consiste à donner un entier, un nombre unique qui sert d’identifiant unique pour chaque classe. Pour un exemple avec 3 classes différentes, on peut définir comme ci :

  • Chat, 0
  • Chien, 1
  • Lapin, 2

Dans notre exemple, chaque pixel aura donc une valeur entre 0 et n, correspondant au nombre de classe total présent au sein de notre dataset.

 

One hot encoding consiste à définir un ensemble de colonne selon le nombre de classe possible. Chaque colonne représente une seule classe. Seul la colonne représentant la classe aura comme valeur 1. Les autres colonnes auront comme valeur 0. Si on prends pour trois classe (chat, chien, lapin) on aura un vecteur de la forme suivante :

[probabilité chat, probabilité chien, probabilité lapin], soit [0, 0, 1] par exemple, si on a un lapin à prédire.

Dans notre exemple, chaque pixel aura donc comme valeur un tenseur d’ordre 1, un vecteur. Celui ci sera de taille n, correspondant au nombre de classe total présent au sein de notre dataset.

 

integer labeling vs one hot encoding

 

Pour résumer, si vos données ont des relations entre elles, privilégiez l’integer encoding. Dans le cas contraire ou vos données n’ont pas de relation, privilégiez le one hot encoding.

 

Préparation des données pour du binaire ( 1 classe en plus du background )

Votre fonction d’activation final sera sigmoid.

sigmoid activation function

 

Masque et fonction d’activation finale du réseau pour du binary class

Vous aurez donc des masques de dimensions [Hauteur, Largeur, 1]. Chaque pixel aura une valeur comprise entre 0 et 1.

De façon concrète vous aurez ceci en sortie de prédictions :

En fonction de la sortie, vous aurez un hyperparamètre de sensibilité à définir, un ‘threshold’ permettant de définir la frontière entre votre classe 0 et 1. Pour le cas de l’exemple si dessus, j’ai mis ‘0.5’. Vous êtes obligé de choisir une fonction d’activation finale sigmoid.

 

Préparation des données pour du multi-classes ( > 2 classes )

softmax activation function

Pensez à ajouter une classe supplémentaire étant le background. En effet elle servira de classe ‘poubelle’ si on ne rencontre aucun de nos classes.

Pour les sauvegarder étant donnée que l’on aura N canal, on ne peut utiliser le TIFF ou PNG car ils ont (je fais un raccourcie et une généralité car c’est plus complexe que cela) que 4 channels (rouge, vert, bleu alpha pour la transparence). Donc vous pourrez les sauvegarder directement sous leur format matricielle Numpy (.NPY).

 

Masque et fonction d’activation finale du réseau pour du multiclasses

Selon le type de fonction d’activation finale pour votre dernière couche de votre dernier neurone de votre réseau, vous aurez un agencement différent de vos données en sortie. Vous devrez donc prendre en considération pour en faire autant avec vos masques que vous générez et envoyer en entrée du réseau lors de la constitution de votre dataset.

 

Output d’un réseau basé sur sigmoid :

Tenseur de taille : [hauteurImage x largeurImage x nombreClasses]

 

Output d’un réseau basé sur softmax :

Tenseur de taille : [hauteurImage x largeurImage x 1]

semantic segmentation image keras tensorflow

Réaliser une prédiction

Masque pour classe unique

Etant donné que l’on souhaite prédire une seule classe, notre masque sera binaire et ne contiendra que des 0 (classe background ou poubelle) ou 1 (notre classe à détecter).

prediction classe binary sigmoid

En sortie de notre réseau, nous devrons appliquer une fonction à l’ensemble de nos valeurs de notre tenseur. Pour les valeurs inférieur à notre threshold, on applique la valeur 0. Pour celle au dessus de notre threshold, on applique la classe 1.

Masque pour multi-classes

Si l’on souhaite prédire plusieurs classes, nous aurons donc soit plusieurs cannal (pour du sigmoid ) ou un seul (pour du softmax). Dans un cas ou l’autre, on va avoir pour un même pixel autant de valeurs que de classes à prédire. La classe prédite par notre réseau et la classe qui aura la valeur la plus haute. On utilisera la fonction argMax par exemple de Numpy pour nous récupérer la classe prédite.

prediction multi classes numpy argmax softmax

 

Fonction de perte plus adapté

Les fonctions de perte de type CrossEntropy sont les fonctions basique pour tout problème de classification.

Alors pourquoi ne pas garder nos fonctions de pertes cités plus haut si elles font le boulot ? Car celles-ci évaluent pour chaque pixel de façon individuelle, pour en faire une moyenne sur l’ensemble des pixels. Etant donnée que l’on va travailler avec des dataset déséquilibrés, nous pouvoir avoir des fonctions qui sont d’avantages étudiés pour ces problématique ci. En effet en gardant ces fonctions de base, on pourrait alors avoirs des chances d’avoir des prédictions penchant vers les classes les plus présente au sein de nos images, car basé sur leurs distributions.

Si vous souhaitez néanmoins rester sur une fonction de type CrossEntropy, sachez qu’il en existe des variantes. En effet, vous pouvez attribuer des poids différents pour chacune de vos classes, selon leur plus ou moins grande présence aux seins de vos images.

Mais nous allons passer en revu certaines autres fonctions de pertes qui peuvent s’avérer bien plus efficaces pour de la segmentation. Préférez la Focal Tversky ou la Lovasz.

 

Métrique de suivi la plus adapté

Evitez les traditionnelles accurary ou d’un style similaire pour ce domaine ci. Préférez l’IoU ou la Dice.

 

 

dataset loader custom keras tensorflow batch Cours pratiques - Deep learning

Réaliser son propre générateur de données

Pourquoi faire son custom loader de données ?

Nous avons vu dans l’article Charger & entrainer le réseau sur des images : fit() vs fit_generator(), qu’il est préférable d’utiliser un générateur pour charger l’ensemble de ses données à son réseau de neurones via des mini-lots de données, par rapport à envoyer le dataset dans sa globalité via un fichier (numpy par exemple).

Vous pouvez gagner du temps en utilisant les générateurs intégré à Keras :

  • flow : charge des données via une variable
  • flow_from_dataframe : charge des données via un dataframe Pandas
  • flow_from_directory : charge des données via un dossier spécifique sur l’ordinateur

 

Vous répondre à une problématique simple, ils feront largement l’affaire. Mais ils vous imposent certaines choses :

  • Train/Validation dataset : ça peut devenir un peu tricky pour définir ces deux jeux de données via Keras. Par exemple construire ces deux jeux avec des données dans un dossier unique. Vous pouvez à la rigueur vous en sortir comme cela :
  • Réaliser des opérations spécifiques, comme de la data augmentation à la volée, avant qu’elles ne soient envoyé au réseau. Keras en propose quelque une via sa classe ImageDataGenerator, mais reste limiter.

 

Cette solution nous permet donc de nous adapter face à de large dataset plus facilement, et évite tout saturation de RAM ou celle du GPU. On profitera en plus du calcul parallèles via les threads de votre CPU.

Etat actuel des choses

Vous devriez avoir pour le moment quelques choses comme ceci comme cheminement pour charger vos données. Je minimalise l’exemple pour la compréhension :

  1. On charge le dataset dans sa globalité via Numpy
  2. On créer son modèle et le compile selon son optimiser et diverses métriques souhaités
  3. On entraine notre modèle sur nos données précédemment chargé

 

Réalisation du générateur

Squelette de base

Voici le squelette de base que je préconise. Votre classe doit hériter de la classe Keras.utils.Sequence :

Cette classe parente vous assure dans le cas ou vous souhaitez utiliser du calcul parallèle avec vos threads, de garantir de parcourir une seule et unique fois vos données au cours d’une époch, contrairement à pures custom générateurs maison que j’ai déjà vu sur des forums qui ne se synchronisaient pas entre eux. On verra plus tard dans le tutoriel comment utiliser ce multiprocess.

 

Implémentation du constructeur

Obligatoire à écrire, et défini dans l’objet de base de Keras.utils.Sequence. Permet d’instancier un nouvel objet.

Vous aurez besoin au minima besoin de vos données X et Y. Je vous montre ici un exemple :

  • data : nos données dans un dataframe Pandas
  • xLabel : nom de colonne du df contenant nos données X
  • yLabel : nom de colonne du df contenant nos données Y
  • batchSize : taille d’un mini lot de données
  • shuffle : booléen si on souhaite envoyer des données de façon aléatoire, ou dans l’ordre de l’index du dataframe
  • targetSize : afin de resize nos images

 

Implémentation __len__

Obligatoire à écrire, et défini dans l’objet de base de Keras.utils.Sequence. Définie le nombre de batch durant une époch.

 

Implémentation on_epoch_end

Défini dans l’objet de base de Keras.utils.Sequence. Appelé à chaque fin d’epoch. On va s’en servir ici afin de rendre aléatoire l’ordre de la liste des ID des items à envoyer pour constituer le batch de données courant.

 

Implémentation __getitem__

Obligatoire à écrire, et défini dans l’objet de base de Keras.utils.Sequence. Génère un batch de données.

Concrètement je récupère une liste d’ID qui correspond à des items spécifiques (X et Y) contenu dans le dataframe de données, de taille batchSize.  Ici pour l’exemple je ne fais que charger des images dans un tableau Numpy

A vous d’effectuer vos transformation souhaités sur vos données, selon votre problématique. A savoir normaliser les données (diviser par 255 les valeurs des images par exemple pour passer de 0<x<255 à 0<x<1), réaliser de la data augmentation, etc.

 

Appel à notre générateur

On a plus qu’a créer un générateur pour le jeu d’entrainement, et un second pour le jeu de validation à partir de notre classe custom. On lui fourni un dataframe avec un nombre d’élément spécifique à chaque type de jeu de données spécifié avec un ratio de 0,8/0,2.

On fourni ainsi nos deux générateurs au modèle via la méthode fit_generator(). On peut spécifier si on souhaite utiliser nos threads afin de paralléliser le chargement des données du disque dur vers le GPU.

 

Data augmentation à la volée

Vous pouvez très bien réaliser une data augmentation sur votre fichier de données en même temps que vos masques, en local, au préalable. L’utilisation du générateur prendra en compte l’ensemble des fichiers de vos deux dossiers.

Vous pouvez néanmoins réaliser de la data augmentation au sein même du générateur, à la volée. Cela permet de stocker votre dataset de base, sans avoir des dizaines et dizaines de giga-octets supplémentaire et ainsi d’économiser du stockage sur votre disque dur. Cependant vous aurez alors un points négatif dans cette affaire. Vous perdrez légèrement en temps de chargement des données vers le GPU. En effet, en plus de lire et charger les images en local, on ajoute une petit étape ici, de créer de nouveaux échantillons à la volée avant de tout envoyer au GPU. A vous de voir quel moyen au final est le plus adapté selon votre type de dataset, votre hardware et votre approche pour répondre à votre problématique.

Ici je vous montre les quelques changements à réaliser pour adapter notre précédent générateur, afin d’y ajouter de la data augmentation à la volée.

 

Modification du constructeur

On va initialiser ici un nouvel attribut, batchSizeAugmented :

  • batchSizeAugmented : nombre d’image total du batch
  • batchSize : nombre d’image lu en local de notre dataset d’origine

Ici comme exemple, je prends batchSize =2 et batchSizeAugmented =32. Dans mon raisonnement, je souhaite que pour une image lu en local, je crée 15 nouvelles images augmentées avec divers effets. Donc si je lis 2 images, j’aurais 30 images augmentés. Ce qui fait 2 + 30 = 32 images au total.

 

Modification de __len__

Rappelez vous cette méthode défini le nombre de batch par époch. On remplace alors batchSize par batchSizeAugmented :

 

Modification de __getitem__

On souhaite générer des images augmentés (30) à partir d’image local (2), aucun changement donc pour l’objet currentBatchIdsRow. 

On change cependant la première dimension de xTrain et yTrain de batchSize à batchSizeAugmented, qui correspond au nombre total d’image par batch ( donc les images locales en plus des augmentés).

On ajoute une nouvelle boucle dans la première , et c’est ici que vous allez opérez vos transformations pour générer de nouvelles images.

fit vs fit_generator keras tensorflow numpy generator Cours pratiques - Deep learning

Charger & entrainer le réseau sur des images :…

Petit comparatif sur deux méthodes permettant de chargeur un dataset pour entrainer un réseau de neurones via Tensorflow et Keras.

 

Fit()

Cela permet d’envoyer en un seul coup l’ensemble du dataset au réseau de neurones. En conséquence, on doit être sur que le dataset puisse rentrer en RAM.

C’est donc plutôt adapté pour les petits dataset.

 

Générer les fichiers numpy en local

On commence par définir nos tableau qui vont contenir nos images. Pour cela je vous donne deux méthodes différentes :

 

Maintenant on va parcourir un dossier supposé contenant nos images que l’on souhaite ajouter à nos deux précédents tableau. Je vous montre selon les deux types d’initialisation faîte précédemment. Pour chaque image que l’on aura, on va devoir les ouvrir et les transformer en tenseur de taille 3 (largeur x hauteur x canal, ou canal=3 si RGB ou canal=1 si Noir/Blanc). Une couleur peut avoir un gradient allant de 0 à 255, selon son intensité. Pour chaque pixel, on aura alors des triplets (0<Intensité rouge<255, 0<intensité vert<255, 0<intensité bleu<255). Pour des raisons d’optimisation, les réseaux apprennent plus facilement sur des valeurs normalisés. Pour cela, je vais divisé par 255 les valeurs, pour avoir des valeurs comprises entre 0 et 1.

 

Maintenant on les enregistre en local :

 

Charger les fichiers numpy

On a plus qu’a charger les fichiers précédemment sauvegardé en local, et de les fournir directement au réseau via la méthode fit():

 

Fit_Generator()

On se sert de générateurs afin d’envoyer des mini-lots (batch) de notre dataset au réseau.

C’est donc plutôt adapté pour les grands dataset et convient donc mieux aux problématiques rencontré dans la vie réel. Cela permet en plus d’ajouter de la data augmentation à la volée, donc pratique !

 

Besoin de data augmentation ?

C’est ici que l’on défini notre data augmentation. Vous pouvez laisser vide si vous n’en souhaitez pas. On laisse juste la normalisation des données, comme vu précédemment :

 

Chargement des données

On peut charger les données directement via un dossier spécifique ou via un dataframe de pandas. Gardez un seed identique permettant de synchro les deux générateurs sur les mêmes images en entrée entre sa donnée et son label.

 

Via un dossier spécifique

 

Via un datafame pandas

 

Train & Validation set depuis un même dossier commun

Simple exemple pour de la segmentation d’image. Mais selon votre problématique vous devrez changez dans la méthode flow_from_directory/dataframe le class_mode (type de vos données en Y, catégorie, binaire, etc.) et le classes (liste de vos classes [chiens, chats, etc.])

Cours pratiques - Deep learning

Radar automatique partie 1 : Constitution du dataset de…

Introduction

Projet qui consiste à reproduire le fonctionnement d’un radar automatique.

Comment fait-il, à partir d’une photo, pour reconnaître l’emplacement d’une plaque d’immatriculation, afin d’en lire les caractères ?

On s’intéressera ici dans cette première partie à la constitution du dataset nécessaire pour entraîner des modèles de réseau de neurones pour réaliser un apprentissage profond.

Je vous met ici le lien vers mon dépôt Github avec tout le code source concernant cette première partie 🙂

Scénario global de notre projet

 

Données nécessaires à notre projet

Pourquoi ?

Vous avez une multitude de technique en traitement de l’image pour réaliser une lecture de caractère sur une image. Mais j’ai souhaité rester dans le domaine de la data-science, en pensant au réseau de neurones à convolution français le plus connu au monde, à savoir leNet-5 de Y.Le Cun, qui permet de transcrire en texte des chiffres écrit à la main sur une image.

Qui dit deep learning, dit nécessité de beaucoup de données.

 

Quels types de données

Je suis partie sur l’architecture suivante :

Entraîner un premier réseau de neurones, pour qu’il puisse repérer une plaque d’immatriculation au sein d’une photo. On va utiliser un CNN des plus standard. Il va prendre en entrée des images de voiture, et nous extraire les coordonnées d’un rectangle contenant la plaque. Nous pourrons l’extraire par la suite de l’image pour la fournir à notre second réseau.

Notre premier dataset sera constitué de 2 parties :

  • partie 1 : regroupe des images de voitures.
  • partie 2 : un fichier CSV ou n’importe quel format permettant de stocker le nom d’une image de voiture (exemple: image1.png) associés aux coordonnées du rectangle contenant la plaque.
Dataset 1 pour notre premier réseau

 

Entraîner un second réseau de neurones qui va prendre en entrée la sortie du premier réseau, à savoir une image sous forme de rectangle, contenant la plaque d’immatriculation de la voiture. Celui-ci encore sera un CNN standard. Il donnera en sortie, du texte, représentant les caractères présents sur la plaque d’immatriculation de la voiture.

Ce second dataset lui aussi sera en deux parties :

  • partie 1 :  regroupe des images de plaques d’immatriculation.
  • partie 2 : un fichier CSV ou n’importe quel format permettant de stocker le nom d’une image d’une plaque (exemple: image1.png) associés au caractères présent sur la plaque.
    Dataset 2 pour notre second réseau

     

Constituer son jeu de données

Je vais vous présenter quelques techniques afin de récolter et réunir des données.

Dataset pré-existant

Comme tout bon informaticien, on sait que c’est inutile de réinventer la roue. En effectuant quelques recherches, vous avez déjà de fortes chances que des chercheurs ou autres développeurs aient déjà fait le travail pour vous. En recherchant des articles scientifiques sur des moteurs de recherches spécifique ( Google Scholar, Arxiv, etc.), on peut trouver des travaux sur des reconnaissances comme celle que je souhaite réaliser. Vous pouvez des fois tomber pile sur ce que vous cherchez et gagner grandement en temps (phase de récolte, nettoyage & régulation des données, etc.), c’est tout bénéf pour vous. Ou des fois tomber comme pour moi, sur des données, mais qui ne s’adapte pas pleinement à mon cas précis pour plusieurs raisons :

  • Pas de dataset sur des plaques exclusivement françaises
  • Certains dataset ont des images de qualité médiocre
  • Certains dataset ont très peu de données ( une centaine… )

Comme expliqué dans le point précédent, j’ai un type de données bien précis. C’est d’ailleurs pour cette raison que j’ai repoussé x fois de faire ce tutoriel, car la constitution du dataset aller être un casse-tête terrible, me demandant des fois si je ne devrais pas passer des journées entières sur la rocade Bordelaise à photographier le passage du moindre véhicule 🤣

 

Crawling & Scraping

En effectuant des recherches sur le net, vous pouvez trouver des sites comme Google image, Twitter, Instagram, etc, contenant des images de voiture ou de plaque. Une première solution serait de réaliser des requêtes BigQuery afin de pouvoir tout télécharger (pour google), ou faire du web crawling et scraping (pour les autres réseaux). On va s’attarder sur cette seconde solution 😀

 

Cela consiste en quoi ? ( Théorie )

Faisons simple.

Le web crawling est le fait d’utiliser des robots afin de circuler et parcourir un site web de façon scripter, automatique, dans l’ensemble de son domaine.

Le web scraping est le fait d’utiliser des robots afin de récupérer des informations d’un site, de télécharger ses images, en bref récupérer l’ensemble de son DOM.

 

On va bosser avec des Dataframes Pandas, donc on va rester sur du Python. Et ça tombe bien, car il se prête bien à ce type d’utilisation. Les principales librairies de crawl/scrap sont les suivantes :

  • BeautifulSoup : libraire des plus simples, donc très pratique pour ce genre de petit projet. Je l’ai utilisé pour récolter des datas pour de la détection de harcèlement.
  • Scrapy : bien plus complet, il s’apparente à un framework à part entière, donc plus compliqué à prendre en main mais permet beaucoup plus de chose.

Pour un projet aussi simple j’aurais dû partir sur BS, mais pour le goût du défi je suis parti sur Scrapy pour découvrir de nouvelles choses. C’est vrai que BS nécessite que quelques lignes de code pour récupérer des données, chose un poil plus complexe avec Scrapy.

 

Initialiser un projet Scrapy

On commence par installer notre paquet :

  • pip install scrapy

Vous pouvez l’installer aussi via Anaconda.

 

Dans une console initialiser un nouveau projet :

  • scrapy startproject Nom_De_Ton_Projet

 

Vous allez avoir l’architecture suivante :

  • Projet
    • __init__.py
    • middlewares.py : permet de définir des customisations sur le mécanisme de comment procède le spider
    • pipelines.py : pour créer des pipelines. Les items scrapé sont envoyé là-dedans pour y affecter de nouvelles choses, comme vérifier la présence de doublon, de nettoyer des datas, qu’ils contiennent bien les champs souhaités, etc. ( on s’en fou )
    • settings.py : fichier de configuration du projet. Définissez vos vitesses de crawl, de parallélisation, nom de l’agent, etc.
    • scrapy.cfg : fichier de configuration de déploiement ( on s’en fou )
    • SPIDERS/
      • __init__.py

On a vraiment besoin que du strict minimal concernant les outils que nous propose ce framework.

 

On va juste s’attarder un poil sur le settings.py. Ce genre de technique est très peu apprécié car vous pouvez littéralement surcharger les accès à un site. Vérifier que le site ne possède pas une API publique permettant de récupérer les données souhaitées.

Pour rester courtois, renseignez les champs BOT_NAME et USER_AGENT. Ce qui donne pour ma part :

BOT_NAME = 'Crawl&Scrap'
USER_AGENT = 'Bastien Maurice (+https://deeplylearning.fr/)'

Utiliser des vitesses de crawl/scrap et un nombre de requête concurrente de façon raisonné :

CONCURRENT_REQUESTS = 16

Scrapy propose même des outils permettant d’ajuster sa vitesse de requêtes de façon intelligente :

AUTOTHROTTLE_ENABLED = True
AUTOTHROTTLE_START_DELAY = 5
AUTOTHROTTLE_MAX_DELAY = 60
AUTOTHROTTLE_TARGET_CONCURRENCY = 1.0 (le plus important)
Squelette minimal d’un spider

Un spider est un composant qui va scrap un type de donnée. Nous souhaitons scrap deux types de données, nous aurons alors deux spiders. Voici le code minimal lors de la création d’un spider :

name est le nom du spider

allowed_domains est un tableau contenant les noms de domaines autorisés pour notre spider

start_urls est un tableau contenant les URL de points de départ par où commencer à scrap

La méthode __init__ est le constructeur de la classe. On va définir deux méthode (start et end) qui seront exécuté lors de l’appel de signaux (signals.spider_closed & signals.spider_opened) correspondant au lancement et à la clôture du scraper.

La méthode parse est la méthode appelé par Scrapy, c’est ici que toute la magie du scraping va se dérouler et ou on devra définir ce que l’on souhaite récupérer du site distant 😎

 

Coder notre spider pour le dataset 1 & 2 😏 ( Simple parsage )

La première idée était d’utiliser ces deux méthodes sur un maximum de site afin de récupérer un maximum de donnée. Seul bémol, j’aurais dû écrire à la main mon fichier CSV pour y ajouter le numéro des plaques. Mais j’ai réussi en creusant dans mes recherches, à trouver un site qui expose des images de plaques, avec diverses infos :

  • Choix de la nationalité de la plaque ( voiture française dans notre cas exclusivement )
  • Photo de la voiture en entier
  • Photo de la plaque d’immatriculation (généré automatiquement, ce n’est pas un crop de la plaque de la photo)
  • Date d’ajout
  • Pseudo de l’utilisateur qui a ajouté
Exemple d’une photo disponible sur le site

 

On va pouvoir récupérer à la fois des données pour le dataset 1 et pour le dataset 2.

Créons un nouveau fichier, scraper.py dans le dossier ‘spider‘ de notre projet Scrapy. Ajoutons-lui les attributs suivants :

csvFilepath : chemin de destination vers mon fichier CSV, qui va contenir les données de ma plaque d’immatriculation. Je souhaite que à chaque donnée on récupère les données suivantes :

  • date : date à laquelle la photo a été upload sur le site distant
  • heure : heure à laquelle la photo a été upload sur le site distant
  • voitureMarque : marque de la voiture
  • voitureModele : modèle de la voiture
  • imgGlobalName : nom de l’image de la voiture global que je vais enregistrer en local
  • imgPlaqueName : nom de l’image de la plaque que je vais enregistrer en local
  • plateNumber : numéro de la plaque d’immatriculation
page : numéro de la page en cours de scraping
maxPage : nombre de page max à parser
DIRECTORY_IMG_PLATE : dossier de destination pour enregistrer les images PNG de la plaque seule
DIRECTORY_IMG_GLOBAL : dossier de destination pour enregistrer les images PNG de la voiture globale
GLOBAL_DATA : fichier CSV qui sera lu par la librairie Pandas
L’attribut urllib.urlopener va être extrêmement important. Il peut vous arriver que le site distant vous remonte une erreur, signifiant qu’il bloque les spiders qui scrap des datas. On va camoufler notre spider en lui associant une version pour le faire ressembler à une connexion venant d’un humain. Vous pouvez lui donner d’autres versions, peu importe qu’il vienne de Mozilla, Chrome, Firefox, de même pour les versions…
On va pouvoir ainsi scrap nos data comme souhaité 😁

 

Je connecte mes signaux :
Début du scrap : je demande à Pandas de charger mon fichier CSV contenant l’ensemble de mes datas
Fin du scrap : je demande à Pandas de sauvegarder mes nouvelles données en plus des anciennes en local

 

Et enfin la méthode parse qui permet de récupérer nos données, de les nettoyer :
Quelques infos utiles :
La méthode xpath me renvoi de base un object xpath
La méthode .get() me converti mon object xpath en string
L’attribut xpath du sélecteur text() me renvoi le champ text, d’une balise <division>Coucou</division par exemple
L’attribut xpath du sélecteur @src me renvoi le champ src, d’une balise <img src= »lien_vers_image« > par exemple
L’attribut xpath du sélecteur @altme renvoi le champ alt, d’une balise <img alt= »infos_utiles« > par exemple
Résultat des données récupérés par notre spider

 

Coder notre spider pour le dataset 2 😏 ( Utilisation de leur API )

Je me suis dit pour augmenter notre dataset de façon bien plus conséquente, qu’il serait intéressant d’avoir un générateur de plaque avec des caractères aléatoire. Une image de plaque d’immat est simple à réaliser en effet. Et il se trouve que sur ce même site il y a un système permettant de réaliser nos plaques sur mesure via leur API :

Capture d’écran de leur système de génération de plaque

Comme vous pouvez le voir, on peut choisir nos plaques selon :

  • La nationalité
  • Le type de standard
  • Le nombre de rangé
  • Le département
  • L’ensemble des caractères de la plaque

Après avoir remplis l’ensemble de ces informations, le site vous génère la plaque selon vos numéros choisi. J’ai souhaité rendre ce processus de façon automatique. Etant donnée que c’est nous qui rentrons les lettres voulues, on pourra les récupérer pour les inscrire dans notre fichier de données CSV.

 

Avant de se lancer dans le code, il va falloir faire une petite analyse du site, pour savoir comment générer une plaque en utilisant l’API, sans que celle-ci nous soit indiqué explicitement. En effet, contrairement à des outils tel que Sélénium, Scrapy ne peut faire des interactions directement avec le javascript, comme par exemple simuler des clics de souris sur des boutons. On a donc besoin de l’accès de l’API.

En utilisant les outils de Chrome qui sont intégrés, on va aller chercher que fait ce bouton lors d’un événement (click), et lire le code source de la page. En remontant dans les parents du bouton, on aperçoit le formulaire qu’envoi le bouton lors d’un clique. Il envoi l’action « /fr/informer » avec comme méthode « post » :

 

 

On va maintenant aller voir du côté réseaux et des paquets qui y sont échangés. Vous pouvez enregistrer sur des périodes, l’ensemble des requêtes qui transite de vous au serveur, permettant de voir les informations qui y sont échangés. J’ai lancé mon enregistrement, puis j’ai cliqué sur le bouton pour générer une plaque. Dans l’onglet Network, vous aurez pleins de requêtes. A vous de les analyser pour récupérer les informations dont vous souhaitez.

J’ai réussi à retrouver l’action appelé par mon bouton :

On peut observer dans la partie Form Data, que plusieurs paramètres ont transités lors de l’appel à l’API. Celles-ci sont les données permettant de générer la plaque, donnée par les champs de texte vu précédemment et rempli par l’utilisateur. Nous devrons donc induire de nouvelles notions pour coder ce nouveau spider, afin d’y injecter des paramètres lors de notre requête au site distant.

 

 

Nous venons d’analyser brièvement le site. Passons à la pratique.

Créons un nouveau fichier, plateGenerator.py dans le dossier ‘spider‘ de notre projet Scrapy que voici :

Pas de nouvelles choses comparées à l’exemple précédant. On garde une version custom pour les en-têtes URL pour by-pass le 403 forbidden.

 

Je définis deux nouvelles fonctions. Celles-ci vont me permettre de me générer de façon aléatoire soit seulement des lettres, soit des chiffres, de longueur que je souhaite. Elles vont me permettre de générer les valeurs de mes plaques générées :

 

On va utiliser la méthode « start_requests ». Celle-ci est appelé de base, par rapport à notre url défini dans « start_urls » :

L’ensemble du code est documenté ligne à ligne.

La dernière ligne, avec le mot clé ‘yield’, va nous permettre de générer une requête basée sur des données de formulaires. On lui donne les données de la plaque que l’on souhaite générer à l’API. Le paramètre « meta » n’est utilisé que pour passer des informations de ma fonction « start_requests » à ma méthode « parse ».

 

Cette requête appelé va être interprété par notre méthode « parse ». Comme pour le premier cas, c’est ici que tout la magie du scrapping s’opère :

Résultat des données récupérés par notre spider

Ajuster ses données

Ajout des coordonnées de la plaque dans le dataset 1

Après avoir craw/scrap le site en question, on se retrouve avec la photo de la voiture, le numéro de sa plaque, mais pas la position (X, Y) où se trouve la plaque sur la photo de la voiture. Cela aurait été fait dans des datasets pré-existant. Etant donné que je le constitue moi-même, je vais devoir le réaliser moi-même.

Vous avez plusieurs logiciels sur Github vous permettant d’ouvrir des images, de placer des rectangles, et celui-ci vous renvoi les coordonnées et vous permet de constituer un fichier CSV avec l’ensemble des coordonnées des images de votre dataset. ça risque d’être long et répétitif. C’est pour cela que je suis parti pour créer mon propre logiciel me permettant de tout automatiser. Cela risque de me prendre du temps, mais à terme, d’en gagner pas mal de façon proportionnelle à la taille de mon dataset.

 

Coder son propre logiciel

Nous avons vu les limites pour constituer un dataset.

Que cela soit en reprenant le travail d’autres personne malgré qu’il ne corresponde pas à 100% de notre objectif. Ou le fait qu’il soit incomplet (manque de la positions X,Y de la plaque d’immatriculation au sein de la photo) lorsque on le réalise soit même par crawl/scrap. Ou encore que l’on manque cruellement de données.

 

Une possible solution pourrait être de créer son propre logiciel pour générer des images ?

On pourrait réaliser un système de génération de plaque d’immatriculation pour remplir le dataset 2 (comme le fait l’API du site précédent), ou encore coller ces plaques générées sur des photos aléatoires de Google. En effet, notre réseau de neurones 1 n’a pour but que de détecter, de localiser, et de fournir la position de la plaque au sein de la photo, rien de plus. Donc que l’on prenne de vraies photos de voiture avec de vraies plaques d’immatriculations placé au vrai endroit revient au même pour notre réseau que de générer une plaque aléatoire, et de l’insérer dans une photo aléatoire.

Le fond n’est pas important, car on lui apprend à détecter seulement les plaques. Les sorties resteront les mêmes dans les deux cas

 

 

Je dit presque, car il y aura toujours une différences entre de vraies photos et des photos/plaques générés. Il ne faudrait pas entraîner notre réseau sur des données qu’il ne rencontrera pas à nouveau, du moins des données semblables. Les fonds générés ne devraient pas gêner notre système, contrairement à nos plaques que l’on va générer :

  • Plaque plus ou moins inclinés et non droite selon la prise de photo
  • Plaque plus ou moins éclairés selon le soleil
  • Ombre plus ou moins importante selon le soleil
  • Plaque plus ou moins propre (boue, moustiques, etc.)
Différences plaque généré et plaque réelle…

On peut prévoir d’avance des risques d’avoir quelques soucis en utilisant notre système tel quel. Il ne pourrait reconnaître que des plaques parfaitement blanches, et droite, ce qui risque de peu arriver dans la réalité sur le terrain. Il faut donc permettre à notre réseau de mieux généraliser, et donc lui fournir des cas d’exemples pour qu’il puisse apprendre plus ‘intelligemment’. Il faudra donc ajouter à notre logiciel de génération de plaque le moyen de pouvoir répondre au problématiques précédents :

  • Permettre d’ajouter une rotation et
  • Permettre d’ajouter un effet de perspective
  • Permettre d’ajouter du bruit,
  • Permettre d’ajouter des assombrissement et/ou tâche

Ces effets permettront de se rapprocher un peu plus à la réalité des plaques d’immatriculation que l’on retrouve en service. Ces effets devront être généré de façon aléatoire, et ceux dans des proportions minimal et maximal définit par des intervalles.

 

L’article se faisant déjà assez long, je ferais un article dédié à mon logiciel de générateur de plaque, DeeplyPlate.

 

Conclusion

Au bout de cette première partie je vous ait données quelques pistes pour  :

  • Circuler sur un site via Scrapy
  • Coder son premier spider pour récupérer des données sur le net

Les données que vous souhaitez ne seront pas forcement existante sur le net, ou sous un format spécifique à votre utilisation. C’est pour cela que je vous montre que vous pouvez réaliser votre propre logiciel assez rapidement, en 10-15 jours de travail régulier pour soit réaliser de façon artificielle vos données, ou encore créer votre propre logiciel de data augmentation afin d’augmenter la taille de vos données aussi petite soient-elles.

 

Je vous propose de passer à la Partie 2. On s’attaquera cette fois-ci à comment faire de la détection d’objets sur une image. Nous allons montrer comment entraîner un modèle pour qu’il puisse nous fournir les coordonnées sur une image donnée, de l’emplacement d’une plaque d’immatriculation. Nous pourrons ensuite découper cette partie de l’image afin de le donner à un second modèle, afin qu’il puisse nous lire et extraire sous forme de texte les valeurs de la plaque. Mais celle-ci se fera en Partie 3 😎

 

Remerciement

Le tutoriel n’aurait pu être possible sans le site Platesmania. Pour préserver la stabilité du site, ne crawler/scraper qu’avec un nombre de requête et parallèles raisonnable.

 

 

recommandeur hashtag image Cours pratiques - Deep learning

Classification d’images multi labels/classes

Pour ce nouveau cours, je vous propose de revenir sur de la classification d’image. Contrairement à mon cours sur la classification d’image simple, celui-ci sera légèrement différent en utilisant de la classification d’une multitude de label pour une même image donnée.

Le but est de créer un système permettant de proposer des hashtags en fonction d’une image donnée en entrée. Nous resterons sur nos outils habituels, à savoir Tensforflow en backend et Keras pour l’API de haut niveau, nous facilitant la mise en place d’un réseau de neurones, qui utilisera de la convolution pour cette fois-ci.

Comme d’habitude, le code source entièrement documenté est sur mon Github, libre à vous de venir pour me faire part d’éventuels correctifs et optimisation.

C’est parti ! 😉

ATTENTION : On va commencer à travailler sur des dataset assez conséquent en termes de taille, comparé aux autres tutoriels. Ce cours a pour principal but d’expliquer des méthodes, un cheminement, ainsi que des astuces pour constituer un projet en data science, et non pas d’avoir des modèles ultra performants, sinon je serais sur kaggle et non pas sur mon site perso. Travaillant sur un ordinateur portable dépourvue de carte graphique, je ne peux malheureusement pas entraîner de modèle performant.

resultat prediction
Résultat d’une prédiction de hashtag, pour une image donnée

Pré-requis

Nullement besoin d’être top 1 europe Kaggle pour comprendre mon code. Mais si vous souhaitez aller plus loin dans ce cours et pousser les performances de votre modèle, je peux vous conseiller deux autres articles qui vous seront utile :

 

Concernant le code, on va avoir besoin de quelques librairies externe pour ce projet.  Je vous laisse gérer leur installation via Conda ou Pip selon vos préférences et environnement à chacun :

  • Tensorflow
  • Keras
  • Pandas ( gestion de tableaux performants pour la lecture et écriture de certaines de nos données )
  • TQDM ( outils permettant de créer des barres de chargement au sein d’un shell, utile pour savoir où on en est du traitement de données en temps réel )
  • PIL ( gestion d’image pour leur lecture et écriture )
  • HARRISON dataset (torrent)
  • Fix image corrompue + liste méta data

 

Constitution du dataset

Pour créer un dataset, vous devrez crawler des réseaux sociaux proposant des images ainsi que des hastag, facilement récupérable via leur API. Je pense sans trop me tromper que les deux plus grands sont Instagram ainsi que Flickr.

 

Crawler FlickR

Directement via leur API public : https://www.flickr.com/services/api/

 

Malheureusement pour nous, Instagram a depuis quelques années, restreint l’accès à leur API, nous empêchant de récupérer nos données. Il existe néanmoins certaines astuces pour contourner tout ça.

 

Crawler instagram, Astuce 1

Vous pouvez récupérer des informations sur des profiles avec pas mal d’informations sous format JSON. Avec un simple parseur, vous pourrez faire votre propre outil de crawl via de simple liens :

Avoir des informations au format JSON pour un profil spécifique :

  • https://www.instagram.com/{pseudo_du_profil}/?__a=1

Vous pouvez donc avoir accès aux profils que vous souhaitez, si et seulement si celui-ci est en public.

Avoir des informations au format JSON concernant un hashtag spécifique :

  • https://www.instagram.com/explore/tags/{hashtag_a_tester}/?__a=1

Ne vous en faîte pas, vous pouvez récupérer les images via les liens disposés dans le fichier JSON.

 

Crawler instagram, Astuce 2

Certains malins ont réussi à faire des outils bien plus pratique comme celui-ci (instagram-scrapper). Pour l’avoir utilisé, vous pouvez effectuer des recherches selon des hashtag, et donc créer son propre dataset avec ses propres classes souhaitées.

 

Utiliser des dataset pré-existant

Comparatif de 3 dataset comportant des images associés à des hashtag

Je n’ai pas voulu perdre trop de temps à constituer mon propre dataset, et me concentrer sur mon algorithme. J’ai trouvé deux dataset pouvant être intéressant pour notre projet :

  • HARRISON : Celui qui me semble le plus adapté pour notre projet. En effet, il est plus récent, et crawler depuis instagram même. Bon compromis pour la taille. Chaque image comporte de 1 à 10 hashtag
  • MIRFLICKR : Peut donner éventuellement de meilleurs résultats car composé de bien plus d’images. Cependant, il est plus orienté photo que réseau social ; je m’explique. Ses tags sont principalement orientés pour la photographie, à en suivre le schéma suivant, qui montre le top des 10 hashtag les plus cités sur nos deux dataset. Je ne pense pas dans le cadre de notre projet, le fait de mentionner un constructeur (nikon,canon…)  apportent une plus valus pour notre recommandeur. En effet, lors de la constitution de ce dataset, les chercheurs ont utilisé le fichier EXIF ( Exchangable image file format ) qui sont associés à chaque photo , lorsque on est sur la plateforme Flickr.  Ce fichier contient des méta données concernant une multitude de paramètres sur les appareils photos ( constructeur, exposition, ouverture, taille des focales et objectifs, iso, résolution, compression, etc…).
comparatif top 10 word dataset
Top 10 des hashtag les plus présents

 

Pré traitement de nos données

Le dataset HARRISON est constitué de la façon suivante :

structure harrison dataset
Structure du dataset HARRISON

La façon dont est organisé le rangement des classes par dossier est parfait pour notre réseau, on ne touchera pas leur agencement. Cependant, on ne peut en dire autant pour la partie des fichiers texte qui contient l’ensemble de nos métas données qui caractérise nos classes d’images. On va devoir faire quelques arrangements.

La première étape va être de rassembler les liens des images avec leur hashtag respectif (data_list.txt & tag_list.txt) dans un seul et même fichier :


 

La seconde étape va être de changer le format du fichier vocab_index ; on supprime les espaces inutiles, et on forme des couples => « nomTag, idTag » :

 

Constitution du modèle & entrainement

On va pouvoir passer à la partie la plus fun des réseaux de neurones, entraîner notre réseaux (forcement, on a rien à faire 😎 ).

Petite astuce en cas de présence d’images qui seraient corrompues. Sois-vous téléchargez les deux images qui nous remontent des erreurs qui sont disponible sur le Github du dataset et vous aller les remplacer dans leurs dossier respectifs, soit vous pouvez forcer PIL à traiter ces images ci :

 

On va charger nos fichiers contenant nos métas donnés :

La 3ème ligne va être extrêmement importante, puisque on souhaite faire comprendre à notre réseau que l’image 2 contient à la fois le label « SNOW » et « MOUNTAINS » séparé par notre virgule, et non un seul label « SNOW,MOUNTAINS ». Pour cela on va convertir chacune de nos entrées en liste.

 

Nous pouvons définir l’ensemble de nos variables globales :

  • NB_CLASSES : va permettre de définir le nombre de neurone dans notre dernière couche du réseau. Nous souhaitons un neurone par classe, qui va permettre de déterminer la probabilité de la présence ou non de cette classe, qui sera entre 0 et 1.
  • NB_EPOCH : nombre d’époque durant l’entrainement. L’entrainement étant extrêmement long, je le laisse à 1 par obligation.
  • BATCH_SIZE : nombre de donnée envoyé dans le réseau par itération.
  • SHUFFLE : permet de mélanger les données. Important puisque dans nos fichiers, ils sont listés classe par classe et donc à la suite. On souhaite que le réseau apprenne de façon équilibré et arbitraire.
  • IMG_SIZE : permet de resize nos images. Correspond à la taille de tenseur en input du réseau.
  • TRAINSIZE_RATIO : définit le ratio entre jeu de donnée d’entrainement et de validation
  • TRAINSIZE : nombre d’image pour le jeu d’entrainement
  • LIST_CLASS : liste de nos labels
  • DIRECTORY_DATA : répertoire parent contenant le dataset harrison
  • DIRECTORY_TRAINED_MODEL : répertoire ou on va aller sauvegarder notre modèle, une fois qu’il sera entraîné.
  • COLOR_MODE : permet de choisir entre des images en couleurs, ou en grayscale.

 

On va ensuite définir nos appels de retours ( callback ) appelé à la fin de chaque itération :

  • modelCheckPoint : va permettre de définir comment on souhaite enregistrer notre modèle : répertoire, avec ou sans poids, etc.
  • earlyStopping : va permettre de juger l’évolution d’une métrique (validation_accuracy) sur le jeu de donnée. Si celle-ci n’évolue plus selon un certain paramètre défini, un certain gap (patiente), on stop l’entrainement

 

Nous allons ensuite charger nos images en mémoire. Contrairement au cours sur la classification d’image, on va utiliser un outils concu dans Keras qui est le ImageDataGenerator. Il va nous permettre des choses bien plus poussé contrairement à la méthode que j’avais pu utiliser il y a de cela quelques mois pour le chargement des images. J’avais à l’époque utilisé Numpy pour lire mes images, les transformers en tenseur, et les enregistrer sur le disque. Pour enfin dans un second temps les relire pour les charger en mémoire. L’avantage des imagesDataGenerator sont multiples

  1. Une seule lecture, pas d’écriture.
  2. C’est un générateur ; il envoie les données au fur et à mesure et permet donc de traiter de datasets bien plus volumineux car ne charge pas TOUT le dataset en RAM.

On a donc un gain de mémoire et de temps.

On définit une normalisation des données. Cela permet de traiter des données des images compris entre 0 et 1, et non plus sur l’ensemble de leurs échelles de couleurs RGB, qui s’étend de 0 à 255, ce qui permet une meilleure compréhension de la part de notre réseau ; il n’aime pas vraiment les valeurs extrêmes.

 

La classe ImageDataGenerator nous fournit 3 principales méthodes :

  1. flow()
  2. flow_from_directory()
  3. flow_from_datadrame()

C’est cette dernière que nous allons utiliser pour traiter nos images.

Nous allons créer un générateur pour nos images du jeu de donnée d’entrainement, ainsi qu’un second pour celle de validation. On va pouvoir lui fournir en entrée notre dataframe chargé précédemment avec l’ensemble des chemins vers nos images, avec leurs labels associés. Les principaux attributs que l’on va utiliser sont :

  • dataframe : dataframe contenant les méta données. Bien faire attention a dissocier les images du jeu d’entrainement et de validation à donner à nos deux générateur.
  • directory : répertoire contenant nos images.
  • x_col : nom de colonne contenant les chemins.
  • y_col : nom de colonne contenant les labels.
  • shuffle : permet de mélanger les images.
  • class_mode : choix entre le mode binaire ( deux classe à prédire ) ou categorical ( plusieurs ).
  • target_size : choix de la taille des images en entrée.
  • color_mode : choix entre images en couleurs, ou grayscale.
  • classes : listes des labels

 

Petit point à ne pas oublier pour la suite du projet, lorsque vous voudrez tester votre modèle pour effectuer de nouvelles prédictions sur de nouvelles images. Chose que j’ai bien entendu oublier lors du lancement du fichier pour la première fois 😅.

A savoir le fait d’enregistrer les labels avec leurs indices, sous format JSON pour faciliter le parsage par la suite du fichier. C’est grâce à lui que on pourra retrouver les labels en sortie dans la dernière couche de neurone du réseau.

 

Pour gagner du temps, on va effectuer du Transfer learning. On va récupérer un réseau qui sera pré entraîné sur un jeu de donne ( IMAGENET ). Dans ce cas-là, on va utiliser MobileNetV2, en précisant la taille d’entrée de nos images avec le nombre de canaux souhaité (3 pour couleurs, 1 pour grayscale). Ce qui nous donne la dimension d’entrée de (96,96,3). On lui indique que on ne souhaite pas avoir ses couches de décisions finales, et que on souhaite du max_pooling sur les couches de pooling.

Etant donnée que les images de IMAGENET et de notre dataset sont différentes, on aura donc des sorties différentes. C’est pour cela que précédemment je ne souhaitais pas d’inclure leurs couches supérieures. On va ajouter nos propres couches de décisions pour que le réseaux MobileNet soit utilisable sur nos images à nous. Cependant, on va geler les couches profondes pour qu’elle ne puisse pas être modifié lors de notre entrainement. C’est ça le but du transfer learning, utilisé des réseaux pré entraîné, mais en modifier la surcouche pour qu’il soit adapté à nos problèmes, tout en gardant les couches profondes intactes pour gagner du temps lors de l’entrainement.

La dernière ligne va nous permettre d’indiquer les entrées et sorties de nos deux modèles pour permettre de les fusionner afin d’en avoir un seul et unique. On n’oublie pas de compiler notre modèle en choisissant un optimizer de son choix. Vous pouvez trouver d’avantages d’optimizer ici.

Concernant la fonction d’objectif, il y a un article scientifique qui aurait démontré qu’avec un environnement ou les poids sont choisi de façon aléatoire à l’initialisation du réseau, que la cross-entropy serait plus performante que la mean-squared-error pour trouver un minimum local. L’article est disponible ici si vous souhaitez d’avantages d’informations, et vous pouvez trouver d’avantages de fonction de perte ici.

 

Dernière étape de ce chapitre, l’entrainement du réseau.

  • generator : définit le générateur du jeu de donnée d’entrainement.
  • validation_data : définit le générateur du jeu de donnée de validation.
  • callback : définit les appels de retours effectué à chaque fin d’époque.

 

Prédiction sur de nouvelles images

Le but est de créer un nouveau fichier ou on va devoir redéfinir le même environnement de pré-traitement des images que lors de l’entrainement, pour lui donner à notre modèle, des formats d’images identiques à celui durant lequel il a appris. La première étape va donc être de créer un fichier texte pour indiquer les chemins des images que l’on souhaite prédire leurs hashtags, en remplissant notre fichier imgTest.Txt.

 

On va donc pouvoir ensuite charger le modèle, charger notre dataframe contenant nos chemins vers nos images de test, accompagné de leurs labels respectifs.

Ensuite, on va recréer notre ImageDataGenerator avec les mêmes paramètres de normalization d’image que on avait lors de l’entrainement.

 

On va pouvoir définir notre générateur de prédiction basé sur notre modèle entraîné :

 

Pour une image en entrée, nous auront une sortie composée de 1000 prédiction entre 0 et 1, correspondant à nos 1000 classe.

On va devoir définir un interrupteur booleen, pour lequel on lèvera si un label est présent ou non dans une image si la prédiction de celui-ci dépasse la valeur de l’interrupteur. Pour notre modèle, on lève un flag de présence si une prédiction pour une classe donnée dépasse 10% de probabilité. On se chargera par la suite à charger notre fichier JSON crée précédemment.

 

On va ensuite itérer sur l’ensemble de nos images séparément :

On va pouvoir comparer le tableau de probabilité à la suite d’une prédiction d’une image, avec notre fichier JSON. Cela va nous permettre de savoir à quel label appartient tel ou tel flag qui aura été levé, vu que on a seulement sa position dans le tableau à la suite d’une prédiction.

 

Dernière étape du projet :

On va récupérer l’ensemble des chemins des images que l’on a insérer dans notre générateur de prédiction, et les fusionner à notre tableau des sorties de labels effectué précédemment. A votre souhait de vouloir les afficher dans la console, ou les enregistrer dans un fichier csv.

 

 

Axe d’amélioration

  • Vers une reconnaissance de lieux ou de monuments ?

En effet, on a peu parlé du fichier EXIF que compose les images venant de Flickr. Mais on pourrait penser à l’exploiter davantage pour éventuellement avoir de nouveaux types d’informations pertinentes qui pourrait nous renseigner, afin d’avoir un système permettant une reconnaissance de monuments connus, ou encore de lieux grâce aux données géographique présent, via la latitude et longitude.

 

Conclusion

Nous venons de voir à travers de ce projet comment réaliser un recommandeur de hashtag selon une image en entrée. Mais celle-ci est en réalité une simple classification d’image à multi label. On peut le comparer à une classification simple d’image comme sur mon cours sur comment classifier des fleurs. Les principaux changements entre ces deux types de classifieurs seront les suivant :

comparatif classification sortie multiple et sortie unique
Comparatif des différences entre un classifieur à sortie unique et un classifieur à sortie multiple
  • Fonction d’activation : Pour la dernière couche d’un classifieur à sortie unique, on souhaite une seule classe qui corresponde à notre donnée d’entrée. Par exemple un classifieur chien/chat ne nous donnera qu’une de ces deux classes en sortie. On a donc un impact entre les deux classes, elles sont dépendantes. En effet quand le modèle pense détecter une forte proba pour une classe ( chien 95% ), l’autre classe sera donc faible ( chat 5% ), car c’est les probas de l’ensemble des classes qui sont égale à 1 ( soit :  proba(chat) + proba(chien) = 1). On choisira alors la fonction d’activation softmax.  Alors que pour une classification à sortie multiple, on souhaite que le calcul de la proba des différentes classes soit indépendante, puisque plusieurs classes peuvent être présent dans notre donnée d’entrée. On aurait donc : 0 < proba(chat) < 1 et 0 < proba(chien) < 1. Pour cela on doit donc utiliser la fonction d’activation sigmoid. Pour comprendre les fonctions d’activations en détails, j’ai écrit auparavant un article disponible ici.
  • Fonction de perte : Celle-ci est choisi selon le problème à résoudre. Comme expliqué au point précédent, on n’a pas le même problème à résoudre. On aura donc pas la même fonction d’objectif.

 

Je vous joins ici l’ensemble de mon code source documenté et commenté sur mon profil Github, avec les informations nécessaire pour sa compilation et lancement. Vous aurez l’ensemble des informations nécessaires pour pouvoir en recréer un vous-même. Je compte d’ailleurs sur vous pour me proposer d’éventuelles corrections et optimisations pour le mien. 🙂

 

Remerciements

HARRISON16, Minseok Park and Hanxiang Li and Junmo Kim, HARRISON: A Benchmark on HAshtag Recommendation for Real-world Images in Social Networks, 2016

detecteur de harcelement Cours pratiques - Deep learning

Détecteur de harcèlement

Après avoir fait un article sur de l’image et un autre sur du son, cette semaine je m’attaque à du texte. Je vous propose de réaliser un classifieur de sentiments. A me perdre sur les réseaux sociaux, je croise énormément (et je pèse mes mots) de commentaires haineux, ou l’insulte gratuite est bien connue des espaces publics dans les commentaires. Le but de ce classifieur va être d’analyser un texte, et de nous retourner si celui-ci porte une atteinte morale à une personne, quel que soit son type.

Alors oui vous allez me dire il existe des datasets déjà existants sur des millions de tweets et qui fera bien mieux que moi en termes d’efficacité. Je vous l’accorde, mais ça peut être intéressant de connaître les outils pour ajuster son dataset en fonction de ses besoins. Par exemple, un dataset pour classifier des sentiments, qui soit en français, qui prennent en compte les emojis (qui peuvent apporter des informations en plus sur les intentions du message) et qui soit classifié via plusieurs types et non juste positif/négatif. Et bien cela reste inexistant. D’où le fait de savoir récolter soit même ses données est extrêmement important.

Comme d’habitude, le code sera entièrement disponible et commenté en détails sur Github :

 

Constituer son propre dataset

Dans les précédents articles, je ne m’attardais pas sur le dataset. Il en existe beaucoup pour réaliser des tâches simples, et surtout de bien plus gros et déjà annoté que le mien que je vais vous présenter, ce qui nous fait un gain de temps notable. Mais comment faire si, dans une entreprise, on se retrouve avec un besoin précis sur une tâche précise dont-il n’existe pas encore de dataset ? Et bien il n’existe pas 36 solutions, il faudra le créer soit même. Pour ce tutoriel, j’aurais très bien pu le faire à la main en utilisant le maximum de synonyme possible. Seulement, il y a de grande chance que notre réseau comprenne seulement nos phrases, puisque chaque personne à un style d’écriture bien différents. Pour avoir un maximum de variétés et surtout dans un minimum de temps, je vous propose de crawl le web !

 

Crawler le web ? 
C’est le fait de parser, d’analyser un site web et de récupérer des données. Je vous l’accorde, cette technique peut nous aider, mais ne sera pas approprié pour 100% des tâches, dans des cas précis, comme durant mon stage de master, j’ai eu besoin d’un type spécifique de donnée qui n’existait nulle part, auquel j’ai dû moi-même le constituer de A à Z. Mais pour le cas de notre classifieur, je vous propose de récupérer des données soit sur des sites telle que Jeux vidéo, Twitter ou encore l’espace commentaire de Youtube. Le but est d’avoir autant de commentaires positifs que d’injurieux.

Pour la récupération des données, il en existe pas mal, mais je vais vous parler des 3 principaux :

  • Scrappy : C’est le plus connu, le plus complet, mais aussi le plus complexe à l’utilisation. Il permet de récupérer depuis des api, les informations sous format JSON.
  • BeautifulSoup : Très connu aussi, je l’ai favorisé à Scrappy, quant au fait que la mise en place d’un spyder est beaucoup plus rapide et simple. En effet, il ne m’a fallu que quelques lignes pour faire mon script. Autant dire que pour débuter, c’est celui que je vous conseille !
  • Selenium : Celui-ci n’est pas son but premier de crawler pour récupérer des données. Il peut le faire mais ce n’est clairement pas le plus optimisé pour ce genre d’utilisation, puisqu’il est plus orienté pour réaliser des tests sur votre application web. Vous pouvez simuler les scénarios et naviguer de bout en bout sur votre site. Cependant, il permet de faire des actions telles que la visualisation dans votre navigateur, des différents scénarios et de voir en temps réel les actions et répercutions. Il permet aussi de réaliser des clicks sur une application, ce qui peut se révéler extrêmement utile si on doit rafraîchir des données via un bouton sur le site.
exemple de crawling via BeautifulSoup4
Exemple de récupération de données via BeautifulSoup

Le premier moyen que j’ai trouvé de récupérer des données est de crawl des forums de jeux vidéo, tel que JVC qui est notamment réputé pour avoir une communauté assez agressive entre leurs membres. Pour faire au plus simple j’ai utilisé BeautifulSoup. J’ai procédé de la façon suivante :

  • Récupération des données brutes : pour cela rien de plus simple, on parse les pages et on récupère tout cela dans un fichier texte en local
  • Nettoyage des données : et c’est ici l’étape la plus importante. En effet, le dataset doit être le plus complet, diversifié et correcte. Il ne faut pas oublier que c’est la data qui est à l’origine d’un bon ou d’un mauvais réseau en sortie, du moins en grande partie. J’ai pu rencontrer plusieurs soucis telle que l’encodage des phrases récupéré, avec les accents français écrit en code et donc qui m’a fait des phrases extrêmement bruités, ou encore des retours avec des phrases vide. Tout cela est donc à vérifier avant tout entrainement du réseau.
  • Format des données : il faut ensuite donner une structure identique entre chacune de nos données pour former une dataset homogène. J’ai réalisé de la sorte suivante :
    • Phrase 1, 0
    • Phrase 2, 1
    • Etc.

Je vais associer pour chaque extrait une classe, pour indiquer si ce message est soit normal (classe 0), ou soit injurieux et présentant un risque de harcèlement (classe 1).

Autant pour trouver des textes injurieux, sexistes, homophobe ou encore raciste, cela reste une chose assez simple via une recherche par mots clés. Autant pour trouver des datas qui soient positive, cela peut être plus complexe.
On se retrouve avec un premier ‘dataset’ (si on peut l’appeler ça un dataset.. :o) constitué de 1500 phrases. C’est peu, beaucoup trop peu de parser des sites indépendant. On va alors privilégier la quantité à la qualité pour ce cours ci, en allant crawler des réseaux sociaux, tel que Twitter. J’ai utilisé TweetScrapper, qui est un module libre dispo sur Github et qui est basé sur Scrapy. L’utilité de passer par ce module, est que l’on va pouvoir parcourir Twitter sans avoir besoin de l’API développeur, qui est accessible via des clés distribuées au compte goutte par Twitter. J’ai notamment essayé d’en récupérer en faisant une demande, auquel je me suis vu refuser 3 fois de suite ma demande. Je pense d’ailleurs qu’a notre ère ou la data est une ressource très convoitée, que Twitter tente de filtrer et de limiter leur accès.

C’est à ce moment-là que l’on voit que Scrapy est un outil extrêmement performant puisque qu’avec mon petit laptop, j’arrive à atteindre jusqu’à 1416 tweet récupéré par minute. Autant dire que c’est une autre paire de manche comparé à mon premier essai à crawl JVC ou encore Bodyguard pour récolter péniblement 1500 extraits.

 

Comment je m’y suis prit pour récolter des données qui soit offensante et non offensante ?
J’ai fait quelque chose d’extrêmement simple, en faisant des re recherche de tweet selon des mots clés. Je vous mets mes 2 fonctions utilisé via Scrappy :

 

  • Data négative :
    scrapy crawl TweetScraper -a query= »pute OR salope OR pd OR connasse OR encule OR bite OR cul OR connard OR batard OR enfoire OR abruti OR gueule » -a lang=fr

 

  • Data positive :
    scrapy crawl TweetScraper -a query= »content OR excellent OR bravo OR felicitations OR heureux OR super » -a lang=fr

 

Nettoyer le dataset

J’ai stoppé le crawling à grosso modo 50 000 extraits pour chacune de nos classes. Scrappy nous enregistre un fichier texte par tweet avec beaucoup d’informations qui nous ne serons pas utile. Or ce que l’on recherche, et d’avoir un fichier unique contenant l’ensemble des tweets avec un format le plus simple possible pour minimiser le pré traitement des données juste avant notre entrainement. Tout ce qui est fait en amont ne sera pas à faire en aval 😉

 

Exemple de tweet remonté directement depuis Scrapy :

{« usernameTweet »: « HIRASUS298 », « ID »: « 1066516448661725184 », « text »: « un jour jvai le croiser \u00e0 chatelet jvai lui niquer sa grand m\u00e8re surtout pcq un moment il sfoutait dla  gueule  des marocains », « url »: « /HIRASUS298/status/1066516448661725184 », « nbr_retweet »: 0, « nbr_favorite »: 1, « nbr_reply »: 0, « datetime »: « 2018-11-25 03:18:30 », « is_reply »: true, « is_retweet »: false, « user_id »: « 4537565739 »}

 

On va devoir réaliser plusieurs traitements de ce texte-là, afin de minimiser les temps de calculs et de faciliter le pré traitement avant d’entrainer notre réseau.

Premièrement j’ai commencé par ne récupérer que le texte du tweet et lui associer une classe, qui soit 0 ou 1 pour savoir si celui-ci est considéré comme offensant ou non. Le code est très simple je ne vais donc pas perdre de temps à l’expliquer ici, mais j’ai parcouru l’ensemble de mes fichier texte, je les ouvre et je récupérer seulement les données que j’ai besoin, car le tweet s’apparente comme état un dictionnaire. Je récupérer donc juste le contenu du champ TEXT, de la même façon que celui-ci serait un fichier JSON.

Nous obtenons à la suite de cette phase là le texte suivant :

un jour jvai le croiser \u00e0 chatelet jvai lui niquer sa grand m\u00e8re surtout pcq un moment il sfoutait dla  gueule  des marocains, 1

La seconde étape va être de nettoyer tout ces problèmes d’encodage. Merci python, je n’ai pas pu me débrouiller pour récupérer dès la source un texte correctement écrit… En effet j’ai eu un sacré souci concernant les accents et emojis, qui me renvoyer leur code. Tant pis, j’ai fait un nettoyage d’une façon qui s’apparente un peu à du bricolage, mais cela fonctionne et c’est le principal. On optimisera plus tard 😉 . J’obtiens le texte suivant.

un jour jvai le croiser a chatelet jvai lui niquer sa grand mere surtout pcq un moment il sfoutait dla  gueule  des marocains, 1

Malgré les fautes du monsieur, on arrive à un texte qui à du sens. Du moins un minimum de sens héhé. Une dernière étape va être de fusionner l’ensemble des tweets. En effet, rappelez vous que j’ai un fichier par tweet, ce qu’il me fait plus de 100 000 fichiers sous la main. Je vais donc pouvoir rassembler l’ensemble des tweets dans un même fichier grâce à des dataframes de Pandas.

 

Certaines de mes requêtes m’ont aussi renvoyé des textes vides. Il a fallu donc penser à plusieurs types de filtrage en plus en annexe, notamment à des textes qui étaient associés à des classes autres que 0 ou 1 suite à des soucis de crawling.

 

Vers une reconnaissance plus pointue ?

Pour l’article je n’ai souhaité que classifier un texte en fonction de deux états, qu’il soit positif ou négatif pour me faciliter la chose avec le dataset. Mais nous pourrions aller plus loin en proposant bien d’autres états de sortie quant à une entrée. En effet une chouette utilisation serait d’avoir plusieurs niveaux pour avoir une catégorie des plus précises concernant le type d’injure, par exemple :

  • Aucun risque
  • Haine
  • Homophobe
  • Raciste
  • Harcèlement morale
  • Insulte
  • Troll

 

Mon projet pourrait être bien plus robuste. On le lit souvent que la qualité du dataset va jouer pour beaucoup concernant les nouvelles prédictions du réseau. Il faut alors avoir un équilibre entre :

  • Le nombre de données : plus vous en avez mieux c’est, car vous apportez de la diversité. En effet, selon vos données, elles doivent venir de plusieurs origines afin de diversifier les réseaux sociaux, auquel nous avons des tranches d’âges différentes, et donc des façons de parler différentes, mais aussi des communautés et donc opinions différentes.
  • La qualité : mais dans l’autre cas avoir une infinité de donnée qui ne sont que peu pertinentes n’est pas bon non plus. Il faudra donc s’assurer que les données soient à la fois utiles et juste. Autant quand vous créer votre propre dataset avec vos propres données comme j’ai pu le faire dans le cours sur la reconnaissance vocale, vous connaissez l’ensemble des variables et donc vous savez ce que vous mettez dans votre jeu de donnée est juste et pertinent. Autant comme pour le cours d’aujourd’hui on ne vérifie pas les 100 000 données une par une pour s’assurer de la pertinence de chacune d’entre elle. En effet en se basant sur des mots clés je sais déjà par avance que certains de mes données seront mal classé. Exemple simple, il nous ait déjà à tous arriver d’inclure une insulte envers un amis proche, mais qui est un contexte amical.

 

Conclusion

Nous venons de réaliser un outils permettant de classifier si un texte est injurieux ou non. Cela peut pourrait être utile sur les plateformes d’échanges de commentaires des grands réseaux sociaux pour luter contre le harcèlement moral qui peut se présenter sous diverses formes.

installation cudnn cuda Cours pratiques - Deep learning

Configuration des dépendances pour utiliser le GPU (CUDA, cuDNN)

Le CPU, c’est bien. Le GPU, c’est mieux !

Nous avons installé tout ce qu’il nous faut pour lancer des entraînements de réseaux de neurones. Cependant, si on lance un entrainement à ce stade-là, nous allons taper exclusivement sur le CPU. Il serait dommage de ne profiter de l’accélération de calculs offert par notre carte graphique (et surtout au prix ou elles sont actuellement, on remercie NVIDIA hein 😉)

Nous allons devoir installer 2 outils nécessaires :

  • CUDA : C’est une technologie propriétaire de NVIDIA permettant d’effectuer du traitement parallèle via leur carte graphique, permettant un bien meilleur rendement.
  • cuDNN : C’est la librairie des primitives de NVIDIA concernant les réseaux de neurones. Elle va permettre d’accélérer nos traitements , qu’ils concernant les routines standard comme la backpropagation , les fonctions d’activations etc. Il fait partie du SDK de deep learning, et peut s’utiliser avec différents backend (Caffe, Caffe2, Chainer, Keras, Matlab, MxNet, Tensorflow et Pytorch).

Sur beaucoup de site, les gens renseigne que pour installer ces deux dépendances nécessaires, il suffit de les installer à la main depuis le site NVIDIA. Cuda s’installe facilement, mais pour cuDNN vous aller devoir vous créer un compte développeur et drag&drop leur fichier zip au bon endroit. Cette méthode à fonctionné pour mon premier laptop, mais pas le second, surement un soucis de version entre les deux versions. Bref, une installation pas des plus rapide et fonctionnel.

 

Installation

Du coup je vous propose de simplifier tout cela, en installant ces deux librairies directement via Anaconda. Ouvrez votre environnement souhaité, et taper les lignes suivantes :

  • conda install -c anaconda cudatoolkit
  • conda install -c anaconda cudnn

 

Et voilà. Simple, basique.😎

constitution image rgb Cours pratiques - Deep learning

Composition et conversion d’une image

Lorsque l’on souhaite analyser des images, ce sont les réseaux de neurones à convolution qui s’en sortent le mieux. Ils sont basés grâce à des expériences effectué sur le cortex et le système de vision des animaux, ces réseaux sont beaucoup plus légers que leurs confrères composé de couche de neurones entièrement connectés les uns aux autres. Attention cependant à ne pas faire trop le rapprochement entre CNN (Convolutional Neural Networks) et images, car ceux-ci peuvent tout aussi bien être utilisé sur du texte, pour réaliser des analyseur de sentiments pour ne citer qu’un exemple. Mais ce n’est pas l’objectif de cet article-ci, j’en reparlerais dans des articles dédiés à ce sujet. 😉

Comme je disais, ces réseaux qui analysent des images, vont pouvoir extraire des caractéristiques propre à celle-ci et à l’ensemble des objets, personnes etc. la constituant. Cependant, comme je l’ai expliqué dans ce post-ci, les réseaux de neurones effectuent des calculs matricielles sur des tenseurs. Ce serait donc outrageux d’envoyer directement nos images tel quel, dans leurs formats natif comme jpg ou encore png (et de toute façon, ce n’est pas possible).

Je vais donc vous montrer comment est constitué une image, afin que vous compreniez les processus que j’effectue sur mes tutoriels pratiques, lorsque je convertis les images de mon jeu de données en tableaux de valeurs (fichier Numpy).

 

Comment est constitué une image ?

Premièrement, notre image est composée de pixel. Je ne vais vous faire la description globale de ce qu’est un Pixel (wikipedia le fera bien mieux que moi), mais c’est l’unité de base, qui définit une image. Une image ayant une taille de 50 par 50 veut dire qu’elle sera composée de 50 pixel par 50 pixel.

cannaux image rgb
Les 3 canaux RGB constituant une image

 

Ensuite, il faut savoir qu’une image en couleurs est composé de 3 canaux, le célèbre RGB (pour Rouge Vert Bleu). Vous aurez donc deviné, que pour une image en noir et blanc, celle-ci est composé exclusivement d’un seul canal.

Ainsi, on peut représenter chaque canaux par une matrice de dimension correspondant à la largeur et la hauteur de l’image. Chaque pixel de l’image va donc représenter une variable de la matrice, qui correspond à l’intensité de la couleurs à ce pixel précis. Petit rappel concernant ce sujet, un pixel peut être définit via une variable comprise entre 0 et 255, correspondant à l’intensité de sa couleur. Cependant, pour homogénéiser nos matrices, nous allons diviser par 255 chacune de nos valeurs, pour avoir à l’entrée de notre réseau de neurones, des matrices ayant l’ensemble de ses valeurs entre 0 et 1.

 

image tenseur cannaux rgb matrice
Empilement de chaque matrice de nos canaux qui forment un tenseur d’ordre 3

Nous nous retrouvons au final avec 3 matrices correspondant à nos 3 canaux de couleurs, qui seront empilés pour former ce qu’on appelle un Tenseur d’ordre 3. Et c’est sur ces structures algébriques que la magie de notre réseau va opérer. 😉

reconnaissance vocale tutoriel Cours pratiques - Deep learning

Reconnaissance vocale de mots clés

Pour ce second tutoriel, nous allons rester sur ces réseaux de neurones à convolution. On utilise le framework Tensorflow en backend, et Keras en API de haut niveau pour nous faciliter la création de l’ensemble de notre modèle. On associe généralement le traitement de la parole (NLP, Natural Language Processing) à des réseaux de neurones récurrents, mais je vais vous montrer tout un processus différent qui va nous permettre d’utiliser de la convolution.

Sur l’article suivant, on va se concentrer sur les différentes notions et étapes nécessaire pour pouvoir réaliser une telle reconnaissance de mots clés. Pour la partie technique et les plus impatients d’entre vous, je vous joint ici l’ensemble du code source du projet disponible sur mon Github. Le principe de construction de ce projet est semblable à celui du cours pratique sur la reconnaissance d’image, puisque on utilise le même type de réseau sur des spectres. On va alors s’attarder plutôt sur les différents principes de traitement des données, afin de passer nos audios d’entrée à notre réseau.

C’est parti ! 😉

 

Pré-requis

Pour la partie audio/spectre :

Pour la partie spectre/tableau numpy

 

Transformation de l’audio en spectre

La première étape va être la plus longue et la plus importante de ce chapitre. C’est cette phase de pré-traitement de nos données d’entrées qui va demander le plus de code. En lisant l’article pré-requis, vous aller devoir transformer avant tout nos audio en image, mais pas de n’importe quel façon. En effet, on souhaite utiliser de la convolution sur des spectres qui vont nous donner des informations sur l’audio. Vous avez l’excellente bibliothèque Librosa qui va nous permettre très simplement de créer nos différents spectres. Celle-ci vous propose plusieurs types de spectres, je vous laisse aller visiter la documentation mais je peux vous en conseiller les 3 principaux qui sont :

  • Le spectrogramme
  • Le mel spectrogramme
  • Le MFCC

Je vous conseille de lire l’excellent article scientifique que je vous ai mit en bas de l’article dans les références, il vous donnera un bench de leurs résultats pour comparer les performances qu’amène l’utilisation de tel ou tel type de spectrogramme. Pour avoir tester ces 3 là, j’ai eu des résultats différents selon le type d’utilisation que j’ai eu. J’ai eu l’occasion d’utiliser plutôt le MFCC sur un CNN qui prédisait 3 classes différentes, et de devoir changer pour utiliser le mel-spectro pour un CNN qui quant à lui, prédisait 10 classes. Vous aurez aussi de votre côté des résultats différents selon vos implémentations en fonction d’une problématique, le mieux reste de les essayer et de juger en fonction de vos résultats. Soyez rassuré, via Librosa le changement est très simple pour passer d’un type de spectrogramme à un autre.

En utilisant de l’entrainement supervisé, on va apprendre au réseau à reconnaître ces différents phonèmes. Il va ainsi être capable de pouvoir différencier tel ou tel mot.

mel spectrogramme chat chier

Je vous montre ci-dessus 2 mel-spectro sur le mot ‘chat’ et ‘chien’. On peut observer le même premier phonème de consonne /ʃ/ pour ‘ch’, qui est partagé et identique entre les 2 audio. Il s’en suivra un phonème de voyelle /a/ pour ‘chat’, et de /i/ et /ɛ̃/ pour ‘chien’. Ainsi en assemblant les différents phonèmes vous pouvez retrouver votre mot prononcé.

 

Conversion de notre dataset en tableau numpy

Nous allons dans un premier temps, devoir transformer nos images d’entrées. En effet, on ne peut charger nos images en format png directement dans notre réseau de neurones. Celui-ci ne fonctionne qu’avec des tenseurs. On va donc convertir nos images vers des matrices de valeurs qui vont être empilés. Je vous ait écrit un article à propos de la constitution d’une image et quant à sa conversion,  vers un tenseur de valeurs, qui correspondent aux intensités de couleurs des 3 différents canaux ( Rouge, Vert, Bleu ) correspondant pour chaque pixel composant l’image. Nous avons ainsi un fichier numpy par classe. D’habitude, la plupart des gens inclus ce processus directement dans le même fichier d’entrainement du modèle. Ce qui n’est pas optimisé puisque l’on est obligé de re-créer ces tableaux à chaque entrainement, ce qui est purement une perte de temps. Ainsi en faisant de cette manière, nous allons les créer une seule et unique fois.

 

Création du modèle

Je souhaitais reprendre le model d’alexNET. Mais étant donnée mon peu de donnée de 250Mo ( ce qui est ridicule en terme de donnée ), je suis parti sur un modèle extrêmement simple que j’ai pris au hasard. Du moins pas complètement au hasard, puisque on utilise un réseau à convolution, on doit respecter des templates concernant les empilement des différentes couches :

[ [Conv -> ReLU]*n -> Pool ] *q -> [FC -> ReLU]*k -> FC -> Softmax

  • Conv : couche de convolution
  • ReLU : fonction d’activation, Rectified Linear Unit
  • Pool : couche de convolution
  • FC : couche de neurones entièrement connecté
  • Softmax : fonction d’activation à sorties multiples

 

Entrainement du modèle

La partie rapide du projet. C’est simple, vous n’avez rien à faire, juste à attendre que votre réseau apprenne. Celui ci va se renforcer au fur et a mesure des itérations que va parcourir votre modèle sur votre jeu de donnée, devenant ainsi meilleur.

fin entrainement
Dernière itération de l’entrainement de mon réseau de neurones

 

Suivit de l’entrainement

Évolution de la perte au cours de l’entrainement
Évolution de la précision au cours de l’entrainement

Une fois le modèle entraîné, on va vouloir voir comment il s’est comporté durant l’entrainement. Que cela soit la fonction de perte ou de précision, on va pouvoir avoir de réels informations et indices sur le comportement de notre réseau, et ce sur le jeu de donnée d’entrainement et de validation.

On peut apercevoir que le modèle n’a pas finit d’apprendre, en effet la courbe concernant le jeu de donnée de validation connait une stagnation. Nous verrons plus loin dans l’article comment améliorer notre modèle.

 

Réaliser une prédiction

Enfin la partie intéressante ! Maintenant que notre modèle est entraîné, on va enfin pouvoir réaliser des prédictions sur de nouveaux audio. Pour cela, on va lancer notre fichier autoPredict.py qui va enregistrer le microphone sur une période de deux secondes. Celle-ci est importante, et doit correspondre à la même longueur que les extraits audio de notre jeu de donnée sur lequel notre réseau s’est entraîné. En effet, pour obtenir des résultats probants, il faut obligatoirement comparer des choses comparables, et donc avec des caractéristiques semblables (la durée dans notre cas). Nous aurons ensuite une conversion de ces audios en spectre, et enfin une dernière transformation en tenseur via Numpy. Nous aurons en sortie de notre réseau une probabilités selon nos 2 classes de sortie, qui sont Chat et Chien.

Résultat d’une prédiction d’une nouvelle donnée depuis mon réseau de neurones, pour le mot ‘Chat’

 

Validation de notre modèle sur un nouveau jeu de donnée

Maintenant que nous avons un modèle, on souhaite savoir comment il va se comporter sur de grandes quantités de nouvelles données. En effet, il serait dommage de perdre du temps de l’intégrer dans notre application pour se rendre compte bien plus tard que notre réseau n’est absolument pas fonctionnel. Perte de temps et d’argent garantie. 😉

On va donc recréer un dataset de nouveaux extraits audio, auxquelles notre réseau n’aura jamais vu auparavant, pour permettre de prédire au mieux comment notre réseau va se comporter en application réelle. Pour notre exemple, on va reprendre nos 2 types de classes, avec des audios que j’ai enregistrer via un camarade auquel le réseau n’a jamais entendu sa voix. Plus votre dataset sera important, et plus vous aurez une idée précise du comportement de votre réseau. Pour le cas du tutoriel ( et que je suis fenéant ), j’ai pris seulement 5 audios différents pour chacune des classes.

Le but de notre matrice ne va pas s’arrêter là. En effet, son application va aller bien plus loin. Il va nous permettre de mettre en évidence d’éventuel erreurs qui pourrait être critique ou acceptable, ce qui sont 2 choses réellement différentes, j’en écrirais un article d’ici peu pour de plus amples informations.

matrice de confusion

On obtient un score global de 70% de bonnes prédictions sur de nouvelles données. Nous pouvons nous rendre compte qu’il a donné de parfaite prédiction concernant la classe chat. Cependant, notre modèle est peu fiable, concernant la classe chien. Le but de ce procédé va donc être de viser une diagonale pour avoir des prédictions proche de 1.

 

Axe d’amélioration

  • OVERFITTING EN VUE MON CAPITAINE ! 😐 La data augmentation peut aider dans beaucoup de cas. Mais en abuser est dangereux pour notre réseau. On voit clairement sur nos 2 graphiques que le jeu de validation réussit mieux que le jeu d’entrainement, notre modèle apprends donc par coeur les données. Et pour cause, chaque classe de mon jeu de donnée ne contient que 50 extraits audio unique, pour 1000 extraits augmenté. Par manque de temps, j’ai utilisé des techniques de data augmentation pour me faciliter la vie. Pour cela, j’ai cloné chaque extrait de base en 20 nouveaux extraits, en y ajoutant des transformations audio pour créer artificiellement de la diversité au sein de mon dataset. Cependant, mon dataset en reste néanmoins pas assez diversifié et manque clairement de vrais extraits audio.
  • Augmenter la taille du réseau : n’ayant que très peu de données, mon choix d’un réseau aussi simple est justifié. Cependant si on augmente notre jeu de données, nous allons pouvoir augmenter la profondeur de notre réseau de neurones. Ajouter des couches va permettre au réseau d’extraire des caractéristiques plus complexes.
  • Augmenter la résolution de nos images d’entrées : n’ayant pas un GPU à disposition pour mes entraînements, je suis dans l’obligation d’utiliser seulement mon CPU, me limitant ainsi dans mes calculs de tenseurs. Cependant, augmenter la résolution des images va permettre au réseau de mieux s’en sortir. En effet, plus la qualité des images du dataset est haute, et plus les prédictions en seront bonne.

 

Vers une reconnaissance vocale continue ?

Je ne suis pas aller plus loin personnellement sur ce projet, mais je peux vous partager quelques idées pour vous permettre de construire une vraie reconnaissance vocale pour créer un Speech to Text en continu. Le but serait de reprendre le même principe de mon tutoriel, et d’entraîner cependant notre réseau non pas sur 2 classes mais sur l’ensemble des phonèmes que compose la langue française, soit 36 classes. Mais pourquoi entraîner là dessus ? Le fait d’entraîner notre réseau à les reconnaître, va nous permettre de reconstituer les mots, et donc les phrases, via un système de dictionnaire que l’ont mettrait en place pour faire la conversion.

Exemple sur un spectre du mot ‘bonjour’. On découpe notre spectro d’entrée en taille identique que l’on va envoyé à notre réseau :

decoupage extrait audio

 

On aurait par la suite une analyse des spectres découpé un à un par un réseau de neurone à convolution pour permettre d’extraire les phonèmes découverts :

decoupage phoneme

A la suite, notre dictionnaire de conversion des phonèmes nous permettrait de récupérer depuis les phonèmes les mots prononcés. On aurait forcement des effets de bords du fait que l’ont ait le même phonème sur plusieurs spectre, cela est en fonction de la taille de découpage de nos spectres ou encore caractérisé à la vitesse ou on parle si on accentue plus ou moins certains phonèmes :

dictionnaire phoneme conversion

Pour une meilleure visibilité, je ne vous ait mit seulement les phonèmes qui nous intéresse. Les cases blancs correspondent à des silences, et les cases rouges correspondent au phonème détecté par notre CNN sur un spectre.

Si on suit l’exemple que j’ai élaboré, cela nous donne la phrase :

_ B ON ON J OU OU OU R R _ _

Il faudrait alors s’en suivre une première étape de nettoyage de notre phrase, en supprimant les blancs :

B ON ON J OU OU OU R R

Et enfin une seconde étape de nettoyage pour supprimer les doublons :

B ON  J OU R 

Auquel cas on retrouve notre mot prononcé, ‘Bonjour’.

 

Je ne pense pas que l’ajout de cette étape, nous permettant de passer d’une simple reconnaissance vocale de mot à un réel speech to text soit si complexe que ça. Le seul point qui prendrait un peu de temps, serait de récolter assez de data pour chacun des phonèmes, afin d’entraîner notre modèle pour les reconnaître.

 

Conclusion

Je vous montre comment réaliser une simple reconnaissance vocale. Très simple à réaliser, si vous comprenez comment fonctionne ce projet, vous pouvez l’appliquer ailleurs. Vous pouvez très bien pousser le projet plus loin, et permettre de réaliser une vraie reconnaissance vocale appliqué sur des phrases entière, et non sur de simple mot, pré-défini en avance. Une chouette utilisation  de tel réseau peut être d’intégrer ces modèles pour réaliser des applications mobiles, ou encore faire un système de domotique avec un microphone associé à un RaspBerry pie pour fermer vos volets. 😎

Je vous joint ici l’ensemble de mon code source documenté et commenté sur mon profil Github, avec les informations nécessaire pour sa compilation et lancement. Vous aurez l’ensemble des informations nécessaire pour pouvoir en recréer un vous même. Je compte d’ailleurs sur vous pour me proposer d’éventuelles corrections et optimisations pour le miens. 🙂

reconnaissance d'image Cours pratiques - Deep learning

Classification d’images

Pour ce premier tutoriel , je vous proposer de réaliser très facilement avec Tensorflow en backend et Keras en API de haut niveau, un classificateur d’images, permettant de réaliser une reconnaissance d’images. Nous allons décortiquer comment réaliser l’ensemble du processus, allant du traitement des données, à l’entrainement de notre réseau de neurones, jusqu’au test de notre modèle dans de futures condition réelles pour pouvoir avoir une idée de comment se comporte notre algorithme avant même qu’il soit intégré dans une application.

N’ayant pas tellement la main verte ( en plus d’être daltonien ), on va créer un modèle permettant de reconnaître entre 5 fleurs différentes.

classificateur d image
Voici les différentes fleurs que l’on va apprendre à notre réseau de neurones à reconnaître

On va sur cet article se concentrer sur les différentes notions et étapes nécessaire pour pouvoir réaliser un tel classificateur d’image. Pour la partie technique et les plus impatients d’entre vous, je vous joint ici l’ensemble du code source du projet disponible sur mon Github.

C’est parti ! 😉

 

Pré-requis

Cours théorique sur la constitution et la conversion d’une image

 

Conversion de notre dataset en tableau numpy

Nous allons dans un premier temps, devoir transformer nos images d’entrées. En effet, on ne peut charger nos images en format png directement dans notre réseau de neurones. Celui-ci ne fonctionne qu’avec des tenseurs. On va donc convertir nos images vers des matrices de valeurs qui vont être empilés. Je vous ait écrit un article à propos de la constitution d’une image et quant à sa conversion,  vers un tenseur de valeurs, qui correspondent aux intensités de couleurs des 3 différents canaux ( Rouge, Vert, Bleu ) correspondant pour chaque pixel composant l’image. Nous avons ainsi un fichier numpy par classe. D’habitude, la plupart des gens inclus ce processus directement dans le même fichier d’entrainement du modèle. Ce qui n’est pas optimisé puisque l’on est obligé de re-créer ces tableaux à chaque entrainement, ce qui est purement une perte de temps. Ainsi en faisant de cette manière, nous allons les créer une seule et unique fois.

 

Pré traitement des données

On va devoir générer deux types différents de dataset à partir de nos fichiers Numpy :

  • Dataset d’entrainement
  • Dataset de validation

Le premier va permettre à notre réseau d’apprendre et d’extraire des caractéristiques distinctes de chacune de nos fleurs.

Le second quand à lui va servir à valider le modèle en fin de chaque itération au cours de l’entrainement. En effet, en montrant de nouvelles images à notre réseau, il va lui permettre de se recalibrer pour éviter de sur-apprendre les fleurs du jeu de données d’entrainement. Cette calibration va lui permettre de bien meilleurs généralisation de données.

Il faudra respecter un certain ratio entre ces deux jeux de données. A partir de notre dataset original, nous allons récupérer 80 à 90% des données pour le dataset d’entrainement, et donc de 10 à 20% pour le dataset de validation.
Notre réseau à convolution va avoir comme entrée un tenseur de la dimension suivante :

( n, w, h, c )

  • n : nombre total d’image de notre dataset
  • w : largeur en pixel de nos images
  • h : hauteur en pixel de nos images
  • c : nombre de canaux de nos images. Correspond donc à 1 pour du noir & blanc, et 3 pour des entrées en couleurs

Il faudra donc bien faire attention de reshape nos données en les récupérant depuis nos fichiers numpy.

 

Création du modèle

Je souhaitais reprendre le model d’alexNET. Mais étant donnée mon peu de donnée de 250Mo ( ce qui est ridicule en terme de donnée ), je suis parti sur un modèle extrêmement simple que j’ai pris au hasard. Du moins pas complètement au hasard, puisque on utilise un réseau à convolution, on doit respecter des templates concernant les empilement des différentes couches :

[ [Conv -> ReLU]*n -> Pool ] *q -> [FC -> ReLU]*k -> FC -> Softmax

  • Conv : couche de convolution
  • ReLU : fonction d’activation, Rectified Linear Unit
  • Pool : couche de convolution
  • FC : couche de neurones entièrement connecté
  • Softmax : fonction d’activation à sorties multiples

 

Entrainement du modèle

La partie rapide du projet. C’est simple, vous n’avez rien à faire, juste à attendre que votre réseau apprenne. Celui ci va se renforcer au fur et a mesure des itérations que va parcourir votre modèle sur votre jeu de donnée, devenant ainsi meilleur.

entrainement d'un réseau de neurone
Dernière itération de l’entrainement de mon réseau de neurones

 

Suivit de l’entrainement

graphique de suivi de metriques loss
Évolution de la perte au cours de l’entrainement
graphique de suivi de metriques precision
Évolution de la précision au cours de l’entrainement

Une fois le modèle entraîné, on va vouloir voir comment il s’est comporté durant l’entrainement. Que cela soit la fonction de perte ou de précision, on va pouvoir avoir de réels informations et indices sur le comportement de notre réseau, et ce sur le jeu de donnée d’entrainement et de validation.

On peut apercevoir que le modèle n’a pas finit d’apprendre, en effet la courbe concernant le jeu de donnée de validation connait une stagnation. Nous verrons plus loin dans l’article comment améliorer notre modèle.

 

Réaliser une prédiction

Enfin la partie intéressante ! Maintenant que notre modèle est entraîné, on va enfin pouvoir réaliser des prédictions sur de nouvelles images. Nous avons juste à le charger en mémoire, à transformer notre image au format jpg, vers un tableau numpy, puis de reshape sa dimension vu précédemment. Nous aurons en sortie un tableau de 5 valeurs, correspondant aux 5 neurones de la couche de sortie de notre modèle, et donc à nos 5 classes de fleurs. On aura pour chaque classe un pourcentage concernant sa prédiction. On prendra alors la valeur la plus élevée des 5, qui correspond donc à la prédiction effectué par notre modèle.

prediction d'un réseau de neurones
Résultat d’une prédiction d’une nouvelle donnée depuis mon réseau de neurones

 

Test de notre modèle sur un jeu de donnée entier

Maintenant que nous avons un modèle, on souhaite savoir comment il va se comporter sur de grandes quantités de nouvelles données. En effet, il serait dommage de perdre du temps de l’intégrer dans notre application pour se rendre compte bien plus tard que notre réseau n’est absolument pas fonctionnel. Perte de temps et d’argent garantie. 😉

On va donc recréer un dataset de nouvelles images, auxquelles notre réseau n’aura jamais vu auparavant, pour permettre de prédire au mieux comment notre réseau va se comporter en application réelle. Pour notre exemple, on va reprendre nos 5 types de fleurs différentes, avec des images que j’ai pu récupérer sur un moteur de recherche. Plus votre dataset sera important, et plus vous aurez une idée précise du comportement de votre réseau. Pour le cas du tutoriel ( et que je suis fenéant ), j’ai pris seulement 3 images différentes pour chacune des fleurs.

Le but de notre matrice ne va pas s’arrêter là. En effet, son application va aller bien plus loin. Il va nous permettre de mettre en évidence d’éventuel erreurs qui pourrait être critique ou acceptable, ce qui sont 2 choses réellement différentes, j’en écrirais un article d’ici peu pour de plus amples informations.

matrice de confusion

On obtient un score global de 93% de bonnes prédictions sur de nouvelles données. Nous pouvons nous rendre compte qu’il a donné de parfaite prédiction concernant 4 de nos classes. Cependant, notre modèle s’est trompé sur 1 fleur sur 3, concernant les tulipes. Le but de ce procédé va donc être de viser une diagonale pour avoir des prédictions proche de 1.

 

Axe d’amélioration

On voit sur les graphiques de suivi de métriques que notre courbe d’apprentissage laisse à désirer sur le jeu de données de validation, mais s’en sort plutôt bien sur notre jeu de données de test, de notre matrice de confusion. Pour le tutoriel, j’ai pris des photos relativement simple, ce qui peut justifier notre haut taux de reconnaissance. Il s’en sort beaucoup moins bien sur celui de validation. Je vais vous proposer plusieurs pistes pour corriger cela et vous permettre de développer un modèle bien plus robuste que le mien.

  • Augmenter notre jeu de données : en effet, on a entre 700 et 1000 fichiers pour chacune de nos classe, ce qui est extrêmement ridicule. Plus on va fournir un jeu de données important et diversifié, plus il pourra apprendre et donc réaliser de meilleurs prédictions. Vous pouvez soit en récupérer d’avantage vous même à la main. Ou si votre jeu de données est cependant limité ou impossible à étendre, vous pouvez toujours utiliser des techniques de data augmentation.
  • Augmenter la taille du réseau : n’ayant que très peu de données, mon choix d’un réseau aussi simple est justifié. Cependant si on augmente notre jeu de données, nous allons pouvoir augmenter la profondeur de notre réseau de neurones. Ajouter des couches va permettre au réseau d’extraire des caractéristiques plus complexes.
  • Augmenter la résolution de nos images d’entrées : n’ayant pas un GPU à disposition pour mes entraînements, je suis dans l’obligation d’utiliser seulement mon CPU, me limitant ainsi dans mes calculs de tenseurs. Cependant, augmenter la résolution des images va permettre au réseau de mieux s’en sortir. En effet, plus la qualité des images du dataset est haute, et plus les prédictions en seront bonne.

 

Conclusion

Je vous montre comment classifier des fleurs ( je vous l’accorde c’est absolument inutile ). Mais la principal chose est de comprendre la démarche du projet. Si vous comprenez comment fonctionne ce projet, vous pouvez l’appliquer ailleurs. Vous pouvez très bien faire votre propre réseau de neurones capable d’analyser des images médicales, telles que les radiographies et échographie, pour mettre en évidence d’éventuelles tumeurs qui aboutissent à des cancers pour ne donner qu’un simple exemple d’utilisation. Vous pouvez éventuellement installer des dizaines de caméras sur la voiture de votre mère, et créer votre propre voiture autonome si vous vous en sentez le courage. 😉

Je vous joint ici l’ensemble de mon code source documenté et commenté sur mon profil Github, avec les informations nécessaire pour sa compilation et lancement. Vous aurez l’ensemble des informations nécessaire pour pouvoir en recréer un vous même. Je compte d’ailleurs sur vous pour me proposer d’éventuelles corrections et optimisations pour le miens. 🙂