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.
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.
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 :
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.
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.
Pour comprendre mon cours pratique quant à la réalisation d’une reconnaissance vocale de mots clés via un réseau de neurones utilisant de la convolution, vous devez comprendre les différentes étapes de transformations, entre notre fichier audio de base, vers un tenseur d’ordre 3 compréhensible et utilisable par notre modèle.
Mais, c’est quoi le son ?
Les sujets autour du son et des ondes étant plus proche du domaine de la physique que de l’informatique, je vais simplifier au mieux. Un son est une variation de pression qui va entrainer une vibration dans nos molécules d’air, se propageant ainsi sous forme d’ondes. On peut définir et analyser ce signal qui est en faîte une amplitude en fonction du temps. Cette amplitude sonore nous donne une indication concernant la puissance de celle-ci, puisqu’elle représente la valeur de la pression la plus haute attend par l’onde. Concrètement, plus on produit un son fort, plus son amplitude est forte, et inversement.
Ces enchaînements de montées et de descentes que l’on peut observer sur la courbe, représente les surpression et dépression du milieu, engendré par le déplacement de l’onde.
Nous venons de voir comment les signaux sont défini dans l’espace temporel, on va maintenant s’attarder sur ces signaux dans un espace fréquentiel.
Fréquence et spectre
La fréquence va nous permettre de définir et de classer si un son est aigu ou grave, nous donnant des informations supplémentaires qui vont être importante pour la suite. Elle se définit en hertz, et représente le nombre d’oscillations (période) par seconde.
Types d’images utilisable
Je vais parler des 3 plus connues pour vous donner quelques pistes d’utilisations pour alimenter vos réseaux de neurones. On va utiliser des spectrogramme, qui sont calculés selon le type, via une transformation discrète de Fourrier, une transformation rapide de Fourrier, ou encore une transformation discrète de cosinus. Ce sont des photographies représentent le spectre d’un son. Ces type de spectrogramme vont nous permettre d’analyser des sons et de nous renvoyer :
Un temps
Une fréquence
Une intensité
Vous devriez vous demander d’où provient ce besoin de passer sur des spectres… Et bien pour pouvoir reconnaître des phonèmes ! Un phonème est la plus petite entité phoniques, un élément sonore du langage parlé, pour faire simple : un son, qui peut correspondre à plusieurs sons. C’est cette association de phonème entre eux qui permettent de constituer les mots. Il en existe dans la langue Française 16 de voyelles, 17 de consommes, et 3 de semi consonnes/voyelles.
En prenant l’exemple ci-dessus, je vous montre une comparaison entre notre 3 types de spectrogrammes différents, sur le mot ‘bonbon’. Il est constitué de deux phonèmes de voyelle et de deux phonèmes de consonne.
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.
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.
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. 😉
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.
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.
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.
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 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.
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.
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 :
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 :
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 :
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.
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. 🙂
Il n’est pas toujours évident de commencer à développer nos premières applications d’intelligence artificielle. En effet, les codes natifs des framework de deep learning peuvent rapidement devenir complexe à l’utilisation. J’ai personnellement commencé avec la combinaison Tensorflow/Keras.
Keras
C’est une API de haut niveau qui va se superposer sur un framework de bas niveau (Tensorflow, CNTK, Theano). Il va servir de liaison, comme une sorte de wrapper. Il va pouvoir simplifier l’implémentation d’un réseau de neurone. C’est un peu à l’image entre le développement en C d’un côté, et de java de l’autre ; le niveau d’abstraction n’est pas le même. Il est d’ailleurs développé par un français, François Chollet, actuellement chez Google. Je vous recommande d’ailleurs son livre ‘Deep learning with python’ qui vous donnera de bonnes bases quant à l’utilisation de Keras.
Installation
Dans un précèdent chapitre, je vous ai d’ailleurs montré comment installer Anaconda. On va donc se servir de celui-ci pour installer Keras :
Version CPU ONLY
conda install -c anaconda keras
VERSION GPU ONLY
conda install -c anaconda keras-gpu
Vous n’aurez plus qu’à valider, vous devriez avoir plusieurs librairies annexes qui s’installeront en même temps et qui sont nécessaire au bon fonctionnement de Keras.
Anaconda est un gestionnaire de librairie, de la même façon qu’est NPM pour le web, Nugget pour C# ou encore apt-get pour linux. Il va nous permettre de créer facilement des environnements virtuels séparé selon nos utilisation. Ainsi, vous allez pouvoir installer, mettre à jour et supprimer via de simples lignes de commandes les bibliothèque nécessaire pour entraîner nos réseaux de neurones.
Installation d’Anaconda
Récupérer la dernière version sur le site Anaconda , et suivez les instructions d’installation.
Créer votre premier environnement virtuel
Lancer Anaconda Prompt via la recherche windows. C’est un cmd réservé pour Anaconda.
Conda create -n nonDeMonEnvironnement python=3.6
La spécialisation de version de python est optionnelle, mais ayant eu quelques soucis avec la 3.7, je vous conseille de rester sur la 3.6.
Activer votre environnement
Pour le moment vous êtes sur l’environnement de base. Pour switch, utilisez la commande :
Conda activate nonDeMonEnvironnement
Bibliothèque indispensable
Comme tout bon data scientist, vous allez avoir besoin d’un bagage minimal suivant à installer :
Numpy, Scikit-learn, Matplotlib, Pandas, Pilllow, h5py et bien d’autres.
Une fonction d’activation est une fonction mathématique utilisé sur un signal. Elle va reproduire le potentiel d’activation que l’on retrouve dans le domaine de la biologie du cerveau humain. Elle va permettre le passage d’information ou non de l’information si le seuil de stimulation est atteint. Concrètement, elle va avoir pour rôle de décider si on active ou non une réponse du neurone. Un neurone ne va faire qu’appliquer la fonction suivante :
X = ∑ ( entrée * poids ) + biais
C’est sur cette sortie que la fonction d’activation va s’appliquer.
Exemple de fonctions
Voici les principales fonctions d’activations que l’on peut trouver dans le domaine des réseaux de neurones :
Linear : Utilisé en couche de sortie pour une utilisation pour une régression. On peut la caractériser de nulle, puisque les unités de sortie seront identiques à leur niveau d’entré. Intervalle de sortie (-∞;+∞).
Step : Elle renvoi tout le temps 1 pour un signal positif, et 0 pour un signal négatif.
Sigmoid (logistic) : Fonction la plus populaire depuis des décennies. Mais aujourd’hui, elle devient beaucoup moins efficace par rapport à d’autre pour une utilisation pour les couches cachées. Elle perd de l’information due à une saturation que cela soit pour la phase de feed forward ou de backpropagation, en donnant des effets non linéaires au réseau due à un paramètre unique. Elle a aussi des soucis de gradient 0 avec des entrées étant très large, même si le soucis est minimalisé avec les système utilisant des batch par lots (mini batch). Utilisé en couche de sortie pour de la classification binaire. Intervalle de sortie : {0,1}
TanH : Utilisé pour des LSTM pour des données en continue. Intervalle de sortie : (-1,1)
Softmax : Utilisé pour de la multi classification en couche de sortie. Intervalle de sortie (-∞;+∞).
ReLU ( Rectified Linear Unit ) : Ce sont les fonctions les plus populaires de nos jours. Elles permettent un entrainement plus rapide comparé aux fonctions sigmoid et tanh, étant plus légères. Attention au phénomène de ‘Dying ReLU’, auquel on préférera les variations de ReLU. Plus d’informations en fin d’article. Très utilisé pour les CNN, RBM, et les réseaux de multi perceptron. Intervalle de sortie (0;+∞).
Leaky ReLU : La Leakey Relu permet d’ajouter une variante pour les nombres négatifs, ainsi les neurones ne meurent jamais. Ils entrent dans un long coma mais on toujours la chance de se réveiller à un moment donné. Intervalle de sortie (-∞;+∞).
PReLU (Parametric ReLU) : La paramétrique Leaky Relu permet quant à elle de définir alpha comme paramètre du modelé et non plus comme hyper paramètre. Il sera alors apprentis sable. Il sera ainsi modifié durant la rétro propagation du gradient. Le top pour de large datasheet, moins bon sur de petit, causant d’éventuelle sur ajustement. Intervalle de sortie (-∞;+∞).
TReLU (Thresholded ReLU) : Elle est identique à la simple ReLU. Mais la localisation de son seuil d’activation va être décalé, il n’est plus à 0, mais selon un paramètre theta.
RRELU (Randomized Leaky ReLU) : La Randomise Leakey Relu permet de choisir le hyper paramètre ALPHA. Durant l’entrainement alpha est choisi aléatoirement. Puis durant les tests, il est calculé via une moyenne. Intervalle de sortie (-∞;+∞).
ELU ( Exponential Linear Unit ) : Autre dérivé de la ReLU. Celle-ci va approcher les valeurs moyenne proche de 0, ce qui va avoir comme impact d’améliorer les performances d’entrainements. Elle utilise exponentiel pour la partie négative et non plus une fonction linéaire. Elle parait plus performante en expérimentation que les autres Relu. Pas de soucis de neurone mort (dying ReLU). Intervalle de sortie (-∞;+∞).
SeLU (Scaled ELU) : C’est comme ELU en redimensionné mais avec en plus un paramètre ALPHA pré définit. Bon résultat, bonne vitesse, et évite les problèmes d’explosion et disparition de gradients en s’auto normalisant et gardant les mêmes variances pour les sorties de chaque couche, et ce tout au long de l’entrainement.
Problèmes récurrents
Les fonctions standards amènent au réseau la disparition ou l’explosion de gradient, et donc une saturation et entraîne un ralentissement de la back propagation dans les couches basses du réseau. Voici une liste d’éventuels problèmes que vous pouvez rencontrer concernant ce chapitre :
Problème de disparition de gradient : L’algorithme progresse vers les couches inférieures du réseau, rendant les gradients de plus en plus petits. La mise à jour donc par descente de gradient ne modifie que très peu les poids des connexions de la couche inférieur, empêchant une bonne convergence de l’entrainement vers la solution.
Problème d’explosion du gradient : Dans ce cas-ci, les gradients deviennent de plus en plus grands. Les couches reçoivent alors de trop gros poids, faisant diverger l’algorithme.
Dying ReLU : La Relu souffre d’un souci : saturation pour les nombres négatifs, ce qui entraîne la mort de certains neurones, ils arrêtent de produire autre chose que des 0. Dans certains cas, la moitié des neurones peuvent mourir durant un entrainement. Il est peu probable qu’il reprenne vie en cours d’entrainement, rendant le réseau passif. C’est là que les variantes sont utiles puisque leur principal idée est d’empêcher pour la partie négative d’avoir des gradient égale à zéro.
Recommandation personnel
Sur le papier, les ReLU fonctionnent bien mieux en pratiques que les fonctions standard. Mais ce n’est pas pour autant la peine d’en mettre à toute les sauces. Il faudra choisir la bonne fonction selon votre type de problème à résoudre. Mais si vous débuter et que votre choix n’est pas sûr, commencer par expérimenter alors avec la ReLU pour avoir un premier retour. Celle-ci fonctionnera très bien dans la plupart des cas.
Me concernant, les résultats de ELU sont meilleurs que les autres Relu pour avoir comparé personnellement l’ensemble des fonctions lors de mon stage sur un cas précis de NLP/CNN. Mais les calculs seront plus lents car on utilise exponentiel pour la partie négative, en ajoutant de la non-linéarité. Donc si vous avez le temps et la puissance de calcul nécessaire, je vous conseille l’ordre suivant d’utilisation :