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 :
On charge le dataset dans sa globalité via Numpy
On créer son modèle et le compile selon son optimiser et diverses métriques souhaités
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.
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.])
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.
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.
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.
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é
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
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 :
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 :
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.
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.)
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.
Un jeu de donnée (dataset) va contenir d’énorme quantité de donnée. En effet, plus notre dataset sera grand et diversifié, plus notre modèle sera apte par la suite à prédire des résultats les plus justes possible. Le dataset doit être formaté de la façon suivante :
Train set : Celui-ci va être le plus volumineux en termes de donnée. En effet, c’est sur ce jeu ci que le réseau va itérer durant la phase d’entrainement pour pouvoir s’approprier des paramètres, et les ajuster au mieux. Certaines règles préconisent qu’il soit composé de 80% des données disponibles. C’est la phase d’apprentissage.
Validation set : Quant à lui, on préconise d’avoir environ 10% des données disponible. Ce jeu sera appelé une seule fois, à la fin de chaque itération d’entrainement. Il va permettre d’équilibrer le système. C’est la phase d’ajustage.
Test set : Ce dernier va avoir un rôle bien différent des autres, puisqu’il ne servira pas à ajuster notre réseau. En effet, il va avoir pour rôle d’évaluer le réseau sous sa forme finale, et de voir comment il arrive à prédire comme si le réseau était intégré à notre application. C’est pour cela qu’il doit être composé exclusivement de nouveaux échantillons, encore jamais utilisé pour éviter de biaiser les résultats en lui envoyant des donnés, qu’il connaîtrait déjà et qu’il aurait déjà appris lors de la phase d’entrainement ou de validation. Celui-ci encore peut être estimé de l’ordre de 10% des données disponible.
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.
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.
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.
Suivit 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.
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.
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. 🙂