Fonction la plus connue, définissant comme mesure de la différence entre deux probabilité de distribution pour une variable prise aléatoirement.
Quand l’utiliser ?
Utilisé pour de la classification binaire, ou classification multi-classes ou l’on souhaite plusieurs label en sortie. Utilisable aussi pour de la segmentation sémantique. Fonction basé sur la distribution de Bernoulli.
Référence de base à utiliser comme première fonction pour tout nouveau réseau. Non adapté à des datasets biaisé/déséquilibré.
Categorical Cross-Entropy
Utilisé pour de la classification multi-classes, mais en souhaitant un seul label en sortie.
Quand l’utiliser ?
Pour de la classification multi-class à simple label de sortie.
Weighted Cross-Entropy
C’est une variante de la Binary Cross-Entropy. Elle permet de rectifier la faiblesse de la BCE pour les datasets déséquilibrés, en ajoutant des poids selon des coefficients, pour les exemples positifs.
Quand l’utiliser ?
Pour des datasets biaisé/déséquilibré.
Balenced Cross-Entropy
Elle est similaire à la Weighted Cross-Entropy. A la différence prêt, qu’elle pénalise en plus des exemples positives, les exemples négatifs
Quand l’utiliser ?
Pour des datasets biaisé/déséquilibré.
Focal
Elle est similaire à la Binary Cross-Entropy. Elle excelle dans les datasets déséquilibrés pour la raison qu’elle diminue le poids lors de la prédiction d’exemples facile, pour permettre au réseau de se focaliser pour l’apprentissage des exemples complexes.
Quand l’utiliser ?
Pour des datasets TRES biaisé/déséquilibré.
Region-based Loss
Pixel-Wise Cross-Entropy
Fonction la plus connue pour des tâches d’image segmentation. Celle-ci examine chaque pixel de façon individuelle. On compare la classe prédite du pixel par notre réseau à notre One Hot encoding de notre masque original.
Etant donnée que le calcul se fait selon la moyenne des pixels, on suppose d’avoir des classes en répartition égales sur nos images. Peut donc poser des problèmes si on a des classes non équilibré. Par exemple pour un algo ou l’on souhaite détecter piétons, ciel et route dans des photos, on sait déjà d’avance que les classes ciel et route seront bien plus présente que les piétons. Résultant ainsi dans un réseau avec des prédisposition à détecter d’avantage les classes les plus présentes.
Quand l’utiliser ?
Référence de base à utiliser comme première fonction pour tout nouveau réseau de segmentation d’image. Non adapté à des datasets biaisé/déséquilibré.
Dice
Inspiré des coefficients Dice, métriques utilisé pour évaluer la performance d’un réseau de segmentation sémantique d’image en mesurant le chevauchement entre deux objets.
Quand l’utiliser ?
Très apprécié pour des problématiques de segmentation d’image.
Soft Dice
Calcul la probabilité de chaque classe de façon indépendante, pour ensuite donner un score final résultant d’une moyenne.
Tversky
Similaire à la Dice. Elle ajoute un poids aux éléments FP (Faux Positif) et FN (Faux Négatif).
Quand l’utiliser ?
Très apprécié pour des problématiques de segmentation d’image.
Focal Tversky
Mélange de la Focal, qui focus l’entrainement sur les exemples complexe en diminuant le poids des exemples simples, comme par exemple pour les petits ROI (Region of Interest) avec la Tversky.
Quand l’utiliser ?
Très apprécié pour des problématiques de segmentation d’image.
BCE-Dice
Mélange entre la Dice et la Binary Cross-Entropy.
Quand l’utiliser ?
Très apprécié pour des problématiques de segmentation d’image.
Jaccard / Intersection Over Union (IoU)
Inspiré des coefficients IoU ou communement appelé index de Jaccard, métriques utilisé pour évaluer la performance d’un réseau de segmentation sémantique d’image.
Quand l’utiliser ?
Très apprécié pour des problématiques de segmentation d’image.
Je vous met ici l’ensemble du code source sur mon dépôt Github, avec un exemple concret de segmentation sémantique d’images sur la reconnaissance de carries sur des radiographies dentaire.
Que-ce que la segmentation sémantique ?
Ce type de segmentation consiste à classifier chaque pixel d’une image en un label. Plus d’informations ici.
Types de vos données
En entrée, nous allons envoyer à un réseau de neurones en entrée des images RGB. Plus d’informations sur la composition d’une image ici. Mais de façon concise, notre image RGB à la forme d’un tenseur d’ordre 3 et de dimensions ( hauteur x largeur x 3 ). Le 3 est le nombre de canal. 1 pour une image noir/blanc, 3 pour RGB. Un canal pour le rouge, un pour le vert, et un dernier pour le bleu. Chaque valeur de la matrice représente donc un pixel. Ce pixel qui à une valeur entre 0 et 255, selon l’intensité de la couleur d’un canal spécifique.
En sortie, on souhaite avoir une matrice de dimensions (hauteur x largeur x 1), avec chaque pixel ayant n’ont pas une intensité entre 0 et 255, mais un nombre correspondant à nos classes. La valeur du pixel est donc compris entre 0 (indice du background) à n (nombre de classe dans nos images).
Une image sera plus parlante :
Dataset X et Y
Pour réaliser ce genre de classification on utilise de l’entrainement supervisé. Cela consiste à entrainer notre réseau de neurones sur des couples (X, Y). X est l’image à inspecter, Y son étiquette et ce que l’on cherche à avoir. Cela correspond donc à nos masques contenant nos classes à détecter.
Labeliser les données
Pour chacune de vos images, vous aller devoir, afin de générer les masques par la suite, les labéliser, les annoter. Via des logiciels spécialisés (VoTT, SuperAnnotate, LabelMe, etc), vous allez pouvoir segmenter vos classes au sein de vos images. Voici un Example de segmentation de Carrie sur des radiographies :
Vous aurez la possibilités de faire des carrés, des cubes, des polygones en tout genre. Cela a pour but de générer un fichier JSON. Celui-ci contiendra pour chaque photos un ensemble de coordonnées, de points (X,Y) correspondant aux formes géométriques que vous aurez dessiné sur le logiciel.
Génération des masques
Vous devez accordez la configuration de vos masques selon ce que vous souhaitez avoir en sortie du réseau. Vous allez utiliser une fonction d’activation spécifique afin de faire de la prédiction d’une valeur, ou utiliser une autre fonction d’activation pour de la prédiction multi classes. Mais vous pouvez très bien utiliser l’une ou l’autre selon comment vous agencez vos données. On va voir cela dans les prochaines lignes.
Ces différences de configuration de masque concerne la fonction d’activation de la couche final de votre réseau. Et selon elle, vous devez accorder vos métriques et fonction de perte.
Génération du masque
Pour générer vos masques, vous allez devoir parcourir l’ensemble de vos radiographies une à une avec le fichier JSON de coordonnées qui lui est associé. Vous allez pouvoir tracer des masques (polygones) via plusieurs procédés. Le but est de remplir des matrices Numpy, avec la classe souhaités contenu dans chaque pixel.
Pour vous donnez des idées de librairies pouvant le faire facilement :
Skimage via Polygon2Mask
Pillow (PIL) via ImageDraw
Scipy
OpenCV
Matplotlib via points_inside_poly
A FAIRE /// Image toshop photo original input -> masque généré binaire
Exemple d’un masque crée via Skimage :
Integer ou One hot encoding ?
Comment définir nos classes pour que notre réseau apprennent à les reconnaîtrais ? En effet, on ne peut pas envoyer des mots sous forme de chaine de caractères. Comment donc définir nos classes dans nos matrices ?
Integer encoding consiste à donner un entier, un nombre unique qui sert d’identifiant unique pour chaque classe. Pour un exemple avec 3 classes différentes, on peut définir comme ci :
Chat, 0
Chien, 1
Lapin, 2
Dans notre exemple, chaque pixel aura donc une valeur entre 0 et n, correspondant au nombre de classe total présent au sein de notre dataset.
One hot encoding consiste à définir un ensemble de colonne selon le nombre de classe possible. Chaque colonne représente une seule classe. Seul la colonne représentant la classe aura comme valeur 1. Les autres colonnes auront comme valeur 0. Si on prends pour trois classe (chat, chien, lapin) on aura un vecteur de la forme suivante :
[probabilité chat, probabilité chien, probabilité lapin], soit [0, 0, 1] par exemple, si on a un lapin à prédire.
Dans notre exemple, chaque pixel aura donc comme valeur un tenseur d’ordre 1, un vecteur. Celui ci sera de taille n, correspondant au nombre de classe total présent au sein de notre dataset.
Pour résumer, si vos données ont des relations entre elles, privilégiez l’integer encoding. Dans le cas contraire ou vos données n’ont pas de relation, privilégiez le one hot encoding.
A FAIRE
Préparation des données pour Softmax
Softmax permet de définir une probabilité dans un ensemble de valeurs.
La métrique la plus commune de suivi sera accuracy.
La fonction la plus commune de perte sera categorical_cross_entropy.
Vous aurez donc des masques de dimensions (Hauteur, Largeur, 1).
Pour les sauvegarder, privilégiez les formats PNG ou TIFF. Car JPG est un format comprimé et vous allez perdre de l’information voir pire avoir des changements de classe. TIFF est un format sans perte, un peu lourd. PNG est un format compressé mais sans perte, donc vous pouvez le choisir sans soucis.
De façon concrète vous aurez ceci en sortie de prédictions :
Préparation des données pour Sigmoid
Sigmoid donne une probabilité entre 0 et 1 pour une seule et unique valeur.
La métrique la plus commune de suivi sera binary_accuracy.
La fonction la plus commune de perte sera binary_cross_entropy.
Vous aurez donc des masques de dimensions ( Hauteur, Largeur, Canal), avec Canal= nombre de classe à identifier. Pensez à ajouter une classe supplémentaire étant le background. En effet elle servira de classe ‘poubelle’ si on ne rencontre aucun de nos classes.
Pour les sauvegarder étant donnée que l’on aura N canal, on ne peut utiliser le TIFF ou PNG car ils ont (je fais un raccourcie et une généralité car c’est plus complexe que cela) que 4 channels (rouge, vert, bleu alpha pour la transparence). Donc vous pourrez les sauvegarder directement sous leur format matricielle Numpy (.NPY).
De façon concrète vous aurez ceci en sortie de prédictions :
Réaliser une prédiction
Masque pour classe unique
Etant donnée que l’on souhaite prédire une seule classe, notre masque sera binaire et ne contiendra que des 0 (classe background ou poubelle) ou 1 (notre classe à détecter).
Masque pour multi-classes
Si l’on souhaite prédire plusieurs classes, nous
Fonction de perte plus adaptés
Les fonctions de perte de type CrossEntropy sont les fonctions basique pour tout problème de classification.
Alors pourquoi ne pas garder nos fonctions de pertes cités plus haut si elles font le boulot ? Car celles-ci évaluent pour chaque pixel de façon individuelle, pour en faire une moyenne sur l’ensemble des pixels. Etant donnée que l’on va travailler avec des dataset déséquilibrés, nous pouvoir avoir des fonctions qui sont d’avantages étudiés pour ces problématique ci. En effet en gardant ces fonctions de base, on pourrait alors avoirs des chances d’avoir des prédictions penchant vers les classes les plus présente au sein de nos images, car basé sur leurs distributions.
Si vous souhaitez néanmoins rester sur une fonction de type CrossEntropy, sachez qu’il en existe des variantes. En effet, vous pouvez attribuer des poids différents pour chacune de vos classes, selon leur plus ou moins grande présence aux seins de vos images.
Mais nous allons passer en revu certaines autres fonctions de pertes qui peuvent s’avérer bien plus efficaces pour de la segmentation.
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.
Dans la tâche la plus courante lorsque l’on parle de traitement d’image, on parle de classification d’image. Cela résulte à identifier une classe au sein de l’image.
Détection d’objet
La détection d’objet se superpose à la simple classification d’image, en ajoutant de la localisation d’objet. Cela fonctionne encore avec une classe au sein de l’image, en plus d’avoir sa position. Celle-ci est souvent définie par des rectangles, appelés bounding box.
Segmentation d’objets
Segmentation sémantique
C’est le processus permettant de classifier chaque pixel d’une image en un label particulier. On ne peut faire de différence entre deux instances d’un même objet. Par exemple, si on a deux voitures sur une image, ce type de segmentation donnera le même label sur l’ensemble des pixels des deux voitures.
Segmentation d’instance
La segmentation d’instance va attribuer un unique label sur chaque instance d’un même objet au sein de l’image. En reprenant le précédent exemple, si nous avons à nouveau deux voitures sur une image, chacune d’entre elles se verront attribuer une couleur différente. À l’inverse de la segmentation sémantique, qui aurait défini la même couleur pour les deux voitures.
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.])
Vous commencez le développement mobile et vous hésitez à choisir ReactNative ou la surcouche Expo ? Je vous donne quelques indices afin d’y voir plus clair sur quel framework vous devriez choisir en fonction de vos compétences, du temps, et de vos besoins.
Présentation de React
Framework pour le développement web frontend, développé par Facebook et concurrent direct de Angular et de Vue.js.
Dom Réel et virtuel
React est à la mode. Il est performant, et ce grave à un point important, est qu’il utilise un DOM (Document Object Model) virtuel.
Le DOM (réel) est l’arbre de nœuds, la représentation textuelle de toute votre application web. Quand vous effectuez un changement d’état au sein de votre application, l’intégralité du DOM est recréer. Cela comprend alors l’ensembles des éléments fils de l’application. Et ceci peut être couteux en terme de performance pour des applications grandissantes, surtout si vous y effectuez des modifications fréquentes de votre UI.
Le DOM virtuel quand à lui est un second arbre, correspondant à l’état réel de la globalité de votre application. Vous pouvez alors vous demandez, n’est-ce pas doublon d’avoir deux arbres à l’identique ? Et bien non.
On imagine que vous effectuiez une modification de couleur, sur un texte par exemple. Dans le cas d’une application avec Angular, celui-ci va redessiner la globalité de l’application. Avec React, seul le DOM virtuel va être modifié. React va alors réaliser une comparaison avec des algorithmes spécifiques entre le DOM virtuel fraichement actualisé, et le DOM réel. En fonction des différences entre les deux arbres, seul les nœuds qui ont un nouvel état, vont être redessiner dans le DOM réel. Cela permet de redessiner seulement un seul nœud, dans notre exemple.
Voici une image plus parlante :
Présentation de React Native (Vanilla)
C’est un framework développé par Facebook pour le développement d’applications cross-platform (iOS et Android).
Développement natif vs développement hybride
Le développement natif est l’utilisation de SDK spécifique. Xcode pour iOS, et Android studio pour Android.
Pourquoi favoriser le dév. natif
Le développement natif utilise ses modules et bibliothèques natives, avec son langage natif. Il sera toujours plus performant que les solutions hybrides, même si au cours du temps nous observons de moins en moins de différences
L’accès aux modules spécifique facile (notification push par exemple)
Poids des apk/ipa très léger
Pourquoi favoriser le dév. hybride
Un seul langage de développement pour deux plateformes
Un code unique. Donc 2x moins de bug, en 2x moins de temps
Une interface UI/UX identique, évitant des différences
Présentation de Expo
Expo embarque ReactNative, c’est une sorte de wrapper, de surcouche. Vous retrouvez donc l’ensemble des items graphiques de ReactNative.
En voici les principales caractéristiques :
Projet déjà configuré ! En une minute vous avez votre Helloworld de fonctionnel ! Cela évite des paramétrages en tout genre et fastidieux sous ReactNative Vanilla pour paramétrer Xcode et AndroidStudio.
Pas besoin de mac pour tester votre app sur les deux OS mobile ! Et ça, ça fait plaisir. Ayant un vielle Iphone et mon Android de tout les jours, je peux tester via l’application Expo (disponible sur les stores iOS et Play), avec hot-reload, et sans avoir de Mac, l’application que je développe sur mes deux smartphones. Même si le dév. hybride promet d’avoir un UI/UX 100% identique entre les deux plateformes, il m’est arrivé que mes checkbox ne s’affichait pas correctement sur iOS. J’ai donc forcé pour mes deux plateforme, l’utilisation de la checkbox Android. Soucis auquel je me serais rendu compte une fois seulement uploadé sur les stores. Sous ReactNative Vanilla, vous pouvez seulement tester sur votre iOS si vous tournez sous macOS, et seulement sur votre Android si vous êtes sur Windows pour développer.
Pas besoin de mac pour builder votre app ! Vous pouvez utiliser les serveurs expo pour cela. Attention, il vous faudra obligatoirement un compte développeur chez les deux sociétés, et acheter la licence qui va avec. Autre piège, votre build se fera à distance et non en local, attention donc si vous développer une app qui doit rester à l’abris des regards.. Mais dans tout les cas si vous souhaitez build en local et sans mac, vous pouvez utiliser une VM sous macOS Catalina avec QEMU/KVM sans soucis. Sous ReactNative Vanilla il vous faudra forcement les deux OS pour builder sur les deux plateformes, mais qui se résous aussi bien par la VM.
Accès à l’ensemble des modules natifs compliqué voir impossible… A un moment donnée, votre application grandissante va prendre en fonctionnalités. Etant donnée que Expo est une surcouche, l’accès aux modules natifs n’est pas possible pour réaliser certaines fonctionnalités. Je prends comme exemple connu comme les notifications push, ou achats-in app. Vous n’y aurez simplement pas accès.
… mais vous pouvez Ejecter Expo ! Vous pouvez cependant ‘éjecter’ Expo pour palier au problème précédent. Mais à en lire certaines expériences sur le net, selon la taille et le versionning de votre projet, que ça puisse très bien se passer comme mal se passer.
Poids supérieur des apk/ipa ! J’ai publié sur les stores ma première application avec des fonctionnalités très simple. Elle fait 50mo à télécharger sur les stores.
Quel framework choisir au final ?
Vous débutez sur le développement mobile ?
Oui ? Foncez sur ReactNative + Expo
Votre application requiert des accès natifs à des modules particuliers (achats in-apps ou push-notifications) ?
Oui ? : Foncez sur ReactNative vanilla
Non ? : Foncez ReactNative + Expo
Si votre choix se confirme à développer sous Expo, je vous propose de commencer par le premier chapitre, afin d’installer l’environnement de développement. Hello World sur mon smartphone en moins d’une minute garantie ! 😉
J’ai eu récemment besoin de Catalina afin de builder une application ReactNative pour iOS. Les solutions traditionnels Virtualbox ou encore Vmware m’ont donnée des OS inutilisable, résultant en d’important lag même sur ma config à 4000€. Au bout de deux jours de recherche et d’installations en tout genre pour activer l’accélération graphique via GPU, je suis tombé ENFIN sur une option viable, gratuite, et donnant d’excellent résultat sur mon laptop à 500€ qui datent des années 2015 avec un vieux dual-core !
Vous aurez accès à l’ensemble des outils de Apple, XCode, les app stores, etc.
Prérequis
Désolé les amis, il vous faudra lâché Windows au profit d’une distrib Linux. Donc ayez une machine en dual-boot sous la main, c’est obligatoire.
Le premier de Foxlet est le plus simple. Mais celui de Kholia est un poil plus performant, et nous partirons sur celui-ci dans la suite du tuto.
Pourquoi ?
Foxlet utilise le bootmanager Clover, alors que Kholia Open-core. Open-core est plus jeune, moins robuste, mais semble être plus prometteur et performant. Clover semble être délaissé au profit du jeune OpenCore, la plupart des gros développeur sont partie pour ce dernier.
Vous pouvez y ajouter votre GPU, votre carte son, périphériques USB pour développer sous XCode, etc.
Installation
On va suivre les infos du dépôts :
On paramètre KVM
$ echo 1 | sudo tee /sys/module/kvm/parameters/ignore_msrs
cd ~
git clone --depth 1 https://github.com/kholia/OSX-KVM.git
cd OSX-KVM
Lancez le scripts suivant :
./fetch-macOS.py
Selectionnez la version de macOS que vous souhaitez installer.
$ ./fetch-macOS.py
# ProductID Version Post Date Title
1 061-26578 10.14.5 2019-10-14 macOS Mojave
2 061-26589 10.14.6 2019-10-14 macOS Mojave
3 041-91758 10.13.6 2019-10-19 macOS High Sierra
4 041-88800 10.14.4 2019-10-23 macOS Mojave
5 041-90855 10.13.5 2019-10-23 Install macOS High Sierra Beta
6 061-86291 10.15.3 2020-03-23 macOS Catalina
7 001-04366 10.15.4 2020-05-04 macOS Catalina
8 001-15219 10.15.5 2020-06-15 macOS Catalina
9 001-36735 10.15.6 2020-08-06 macOS Catalina
10 001-36801 10.15.6 2020-08-12 macOS Catalina
11 001-51042 10.15.7 2020-09-24 macOS Catalina
12 001-57224 10.15.7 2020-10-27 macOS Catalina
13 001-68446 10.15.7 2020-11-11 macOS Catalina
14 001-79699 11.0.1 2020-11-12 macOS Big Sur
Choose a product to download (1-14): 14
On a récupérer le BaseSystem.dmg, on va le convertir
qemu-img convert BaseSystem.dmg -O raw BaseSystem.img
On créer un disque dur virtuel, ou notre distri de macOS sera installé. Attribuez la valeur de stockage comme vous le souhaitez. Sachez que Xcode prends pas mal de place, donc au minimum une partition de 64go me semble correct…
qemu-img create -f qcow2 mac_hdd_ng.img 128G
Démarrer Catalina
Pour démarrer notre futur macbook pour la première ainsi que les autres sessions, lancez le script suivant :
./OpenCore-Boot.sh
On commence par booter sur le disque de base :
Sélectionnez Disk Utiliy :
Sélectionnez le disque que vous venez de créer dans les étapes précédentes :
On va cliquer sur Erase. Donnez lui un nom à notre disque, macOS Catalina par exemple. Validez en cliquant une nouvelle fois sur Erase :
Tout est fini. Fermez l’utilitaire de disque afin de revenir sur le menu de départ.
Sélectionnez Reinstall macOS :
Un tas de chose vont vous être demander, laissez vous guidez à travers l’installation. Acceptez les divers conditions, remplissez votre profil, enregistrer votre compte Apple afin d’avoir accès au store, etc. Je vous explique pas chaque détails, c’est plutôt intuitif :
Après une vingtaine de minute, votre installation est fini
Pour utiliser le plein écran : Shift + Alt + F
Pour quitter le focus et revenir sur votre linux : Shit + Alt + G
Nous avons vu dans le chapitre précédent comment stocker en local des données simples.
On va s’attarder cette fois-ci à comment stocker des informations plus nombreuses, plus complexes et ce de façon plus ordonnée, en utilisant des bases de données SQL.
On va réaliser une simple to-do app, avec possibilité d’ajouter une note, ou d’en supprimer, avec une liste permettant de toute les afficher.
On ne peut utiliser n’importe quel outils pour persister des données car nous sommes sur un projet Expo, nous empêchant d’accéder aux modules natifs de iOS et Android. SQLite est un moyen fonctionnant avec des projets Expo.
Objectifs
Stocker localement des informations via une base de donnée SQL
Prérequis
Installez le package pour utiliser SQLite :
expo install expo-sqlite
Nous l’importerons dans nos fichiers de la manière suivante :
import*asSQLitefrom'expo-sqlite';
Base de données & CRUD
CRUD : diminutif correspondant aux requêtes basique, à savoir Create, Read, Update et Delete.
Création de la base de données
Rien de plus simple, une seule ligne suffit :
On donne un nom à notre base, en argument. Celle-ci n’est crée qu’une seule et unique fois. Si on rappelle cette même méthode, on récupère la base de donnée crée auparavant, aucun risque de doublon.
Création de la table NOTE
On va maintenant créer notre table. Je vous montre la façon la plus simple de réaliser des transaction SQL vers notre base :
Requête SQL des plus basique avec le mot clé CREATE TABLE. On lui donne un nom de table, ainsi que la définitions de nos colonnes. Un ID auto-incrémenté pour garantir l’unicité de nos données, ainsi qu’un attribut TEXT qui contiendra le contenu de nos notes.
Nos deux attributs ne peuvent être null, et on leur défini un type, soit Text ou Integer.
Optimiser les requêtes
Je préfère une autre annotation que la précédente pour questionner la base de données, récupérer nos objets et leurs affecter des transformations. Je trouve plus clair et simple à l’utilisation. On définit notre modèle de requête avec une promesse :
On pourra utiliser ce modèle dans des fonctions async, via un appel par un await. On pourra utiliser des then() et catch() à l’appel du service dans nos vues, permettant par exemple d’afficher à l’utilisateur dans une popup si une note à bien été ajouté ou si dans le cas inverse afficher un message d’erreur avec son origine.
Récupérer toute les notes
On définit une interface pour les objets que l’on va récupérer en base. Cela nous facilitera leurs manipulations au sein de notre application :
On fait appel à notre modèle pour questionner la base :
On récupère nos objets via la requête. On va itérer sur notre résultat de requête pour re-typer correctement nos objets.
Ajouter une note
Seule différence, on va ici passer en argument le contenu de ma note que l’on souhaite persister en base. Pas besoin de lui passer un ID pour la note, car celle-ci est généré automatiquement en base. Nous avons défini cette option tout à l’heure, lors de la création de la table.
Supprimer une note
Quasiment identique au point précédent, sauf qu’ici on lui passe un ID de note à notre fonction, étant donné que c’est l’attribut qui défini l’unicité de mes items dans ma table NOTE :
Récupération des items dans la vue
Nous venons de créer notre base ainsi que des opérations CRUD permettant d’interagir avec elle. On va désormais créer une nouvelle vue, avec un champ de texte permettant de donner du contenu à une note, un bouton pour ajouter cette note, une liste scrollable permettant de naviguer sur l’ensemble de nos notes en base, et leur associé à chacun un bouton pour les supprimer.
Définition du state de notre vue
On commence par créer le squelette de base de notre vue :
On utilise ici un composant de classe et non un composant fonctionnel. On a besoin d’utiliser un state pour l’affichage dynamique du contenu de notre liste scrollable. On défini un attribut myNoteList correspondant à une liste de l’ensemble de nos notes en base, ainsi qu’un second attribut note correspondant au champ de texte remplissable par l’utilisateur pour créer une nouvelle note. On initialise les deux attributs dans le constructeur.
Définition des méthodes de notre vue
On défini les méthodes pour mettre à jour nos éléments, pour ajouter une note, ainsi que la supprimer :
La méthode componentDidMount() est une méthode standardisé de React, permettant d’être appelé une seule et unique fois et ce à la fin de la création du composant. On lui demande à l’ouverture de notre page, d’initialiser notre liste avec le contenu de notre base de données.
Définition des éléments visuels de notre vue
Ici rien de bien complexe. J’ai créer un titre de page, un champ de texte avec un bouton pour ajouter une note. Une liste scrollable présentant l’ensemble des notes en base, ainsi qu’une option permettant de les supprimer :
J’ai utilisé des icones Ionicons inclus dans Expo, qui s’adapte en fonction de si vous êtes sur Android ou iOS via la méthode Platform.OS, encapsulé dans des TouchableOpacity, pour rendre un peu plus esthétique mes boutons d’interactions, ainsi que des flexbox pour avoir une touche de responsive design.
Conclusion
Vous venez de voir comment créer très simplement une base de donnée pour votre application React Native tournant sous Expo, comment l’interroger et surtout comment récupérer et afficher le résultats dans une belle vue responsive.
Nous venons de voir au chapitre précédent comment organiser une navigation complexe et imbriquée pour une application avec une architecture grandissante.
Nous allons voir aujourd’hui comment stocker des données plutôt simple, par exemple des paramètres de l’application comme l’activation ou non d’un thème clair ou sombre.
On ne peut utiliser n’importe quel outils pour persister des données car nous sommes sur un projet Expo, nous empêchant d’accéder aux modules natifs de iOS et Android. AsyncStorage est un moyen fonctionnant avec des projets Expo.
AsyncStorage nous permet de persister des données en local sur le smartphone, de façon asynchrone et non crypté. Les informations sont sauvegardé en clair, donc éviter de stocker des données sensibles tel que des mots de passes.
Le stockage s’effectue sous forme de couple tel quel : <Clé, Valeur>.
On ne peut stocker que des string, donc pour des éléments plus complexe tel que des objets, on devra utiliser JSON.stringify() pour la conversion en JSON lors de la sauvegarde d’une donnée, et utiliser JSON.parse() pour lire un objet.
Sauvegarder des données
Pour un string
Pour un objet
Lire des données
Pour un string
Pour un objet
En pratique
On va faire un exemple tout bête, à savoir charger le nom d’un utilisateur à l’appui d’un bouton.
Méthode d’écriture & lecture de donnée
On commence par définir nos deux méthodes pour charger et sauvegarder une donnée :
On appellera la méthode initProfileName() dans l’entrée de notre l’application (App.tsx) pour initialiser notre donnée.
Mise à jour de notre vue
On crée un état initialisé à ‘Invité’. On aura une méthode appelant notre fonction pour lire notre donnée via AsyncStorage, définit précédemment. Et enfin une méthode render() pour afficher un champ de texte, et un bouton bindé avec la fonction de chargement de notre donnée locale :
Conclusion
On vient de voir comment stocker des informations simple en local. Nous allons voir au prochain chapitre comment stocker des informations plus complexe et surtout en quantités plus importantes.
Nous venons de voir dans le chapitre précèdent comment faire une barre de navigation principal. Mais comment faire si nous souhaitons naviguer dans de nouveaux composant à travers nos vues actuelle ?
Pour réaliser une navigation imbriqué de vue dans d’autres vues, nous allons ajouter un nouvel élément, une StackNavigation.
Objectifs
Créer de nouvelles vues
Réaliser une navigation imbriqué
Prérequis
Installez la lib qui gère la stack :
npminstall @react-navigation/stack
Créer une nouvelle vue
Pour faire de la navigation imbriquée, il va nous falloir dans une première partie créer un nouveau composant, une nouvelle page. Faisons une nouvelle page Profil, qui affiche mon prénom, par exemple :
Mise a jour des ROUTES
On va ajouter une nouvelle route dans notre constante globale des routes, amenant à notre nouvelle vue précédemment crée :
Création de la stack navigator
On va créer notre composant qui va gérer la navigation par pile. On doit lui définir nos deux composants (nos deux vues), leurs routes respectifs pour y accéder, et la route initiale lorsque le composant est crée :
Mise a jour de notre tab navigator
On va désormais fournir à notre navigation par tab, non plus mon composant WelcomePage comme initial, mais directement notre stackNavigator, crée précédemment :
Appel du changement de route par un bouton
On met à jour notre composant WelcomePage, via la méthode onPress, qui est appelé lors d’un clique sur notre item, permettant d’appeler le changement de vue en lui donnant la route que l’on souhaite. Je vous montre comment faire cet appel selon le type de composant que vous utilisez au sein de votre application :
Navigation via composant fonctionnel
Navigation via composant de classe
Résultat du cours
Conclusion
Nous venons de voir comment naviguer à l’infinie entre nos vues. Dans le prochain chapitre, nous nous attarderons à comment sauvegarder, à la fermeture de notre application, des données simples, comme des paramètres de l’application.