Classification d'images à l'aide d'un réseau de neurones convolutif.

Le défi ici est de créer un classificateur d’image. Les images serons des images prises au microscopes de cellules immunitaires et le but est de prédire si celles-ci sont la famille des polynucléaires ou des mononucléaires.

Voici une illustration de ce que nous allons bâtir:

Qu'est ce qu'est une cellule mononucléaire ou polynucléaires?

La genèse des cellules du sang est un processus nommé hématopoïèses. Partant d’une cellule souche hématopoïétique multipotente dans la moelle du squelette axial chez les adultes se divisent pour former la panoplie de cellules contenue dans notre sang. Celle-ci on tous des caractéristiques uniques incluant la forme à la fois de leur membrane mais aussi de leur membrane nucléaire. 

A. Rad. “Hematopoiese Simple.” Wiképedia, 21 July 2009, fr.wikipedia.org/wiki/H%C3%A9matopo%C3%AF%C3%A8se#/media/File:Hematopoiesis_simple.svg.

Pour notre algorithme commençons simplement par nous poser la question quelles cellules on un seul nucleus, ainsi appelée mononucléaire comme le monocyte:

Monocyte

Sinon, nous avons d’autres cellules qui ont plusieurs noyaux et donc appelées polynucléaires comme le neutrophile:

Neutrophile

Nous allons donc bâtir un réseau de neurones ayant comme tâche de classifier des images selon si elles contiennent une cellule polynucléaire ou mononucléaire.

Notre jeu de données

Nous allons utiliser le jeu de données publié sur Kaggle par Paul Timothy Mooney disponible ici. 

Pour le télé charger, simplement cliquer sur l’icône Download:

Une fois UnZipped, il est important de vous rappeler où dans votre ordinateurs vous l’avez téléchargé!

Bases sur les réseaux de neurones convolutifs

Commençons par regarder comment le système visuel humain fonctionne; Nous possédons deux yeux dont le fond est couvert de photorécepteurs (cellule rouges et vertes ci-bas) qui capturent l’image d’une infime partie de l’image qui entre dans l’oeil sur la structure que nous appelons rétine. L’information est ensuite transmise à une série d’image qui ensuite se transmettent au cellules ganglionnaires (cellules bleues ci-bas) dont les axones forment le nerf optique.

Pourquoi parler de la rétine? Et bien c’est parce que la première zone de transformation de l’information où les cellules ganglionnaires permettent de detecter des zones de contraste!

Cela fonctionne en combinant l’information en aval, les photorécepteurs, qui est combiné de manière particulière par les cellules ganglionnaires.

De la vue de la rétine, chaque cellule ganglionnaire perçoit les signaux de ~100 photorécepteurs d’une manière particulière, soit si les cellules à la périphérie de leur champ récepteur sont activées et leur centre ne l’est pas, ou l’inverse, aucun stimuli en périphérie et un stimuli en leur centre. 

Archibald, N. K., Clarke, M. P., Mosimann, U. P., & Burn, D. J. (2009). The retina in Parkinson’s disease. Brain, 132(5), 1128–1145. https://doi.org/10.1093/brain/awp068

Voici ma tentative d’illustrer le point:

Voici deux cellules ganglionnaires avec le même camp récepteur connectées à trois photorécepteurs. Une Off-Center, voulant dire qu’elle est activée seulement si sa périphérie et non son centre sont excité, et une cellule ganglionnaire On-center qui est activé par un centre excité et une périphérie qui ne l’est pas. Bref, grâce à cette aggregation de 100:1, l’oeil permet d’extraire non seulement l’information visuelle, mais d’envoyer au cerveau l’information de contraste.

Si nous continuons le trajet de l’information des cellules ganglionnaires arrivent au centre visuel dans l’aire occipitale du cerveau. Plus précisément, l’information arrive dans la zone V1 à la surface occipitale qui traite l’information de contraste pour détecter les lignes.

Ici l’information de contrastes des cellules ganglionnaires de la rétine y est combiné pour extirper un nouveau type d’information: des lignes.

Ceci est fait par l’alignement de champ récepteur de cellules ganglionnaires de même type. Ainsi, les zones centrales et périphériques s’alignes et permettent d’extraire les lignes du champs de vision.

Anastasia Lavdaniti http://steevelaquitaine.blogspot.com/2019/03/the-emergence-of-orientation.html

Bref, le cerveau combine de l’information pour en extraire, à chaque étape de traitement d’information, de l’information plus complex pour arriver finalement à detecter les objets dans notre environment.

Mais comment peut-on demander à l’ordinateur d’accomplir une tâche similaire?  

Commençons par décrire ce qu’est une image; Une image est une série de pixels avec une valeur particulière décrivant leur propriétés. Prenons une image en noir et blanc pour faciliter l’imagerie:

Comme nous pouvons voir, l’image n’est qu’une grille de chiffre entre le blanc à 255 et le noir à 0. Pour les images en couleur, nous avons 3 valeurs pour chacun de nos pixels, donc nous obtenons l’équivalent de 3 images, une en rouge, une un vert et une en bleu pour le fameux RGB.

Maintenant le cour du sujet, comment extraire l’information visuelle de l’image pour pouvoir créer notre classificateur: la convolution. La convolution est la combinaison de deux fonctions pour en créer une troisième. Dans notre cas cela va consister de trois partie: une entrée de données ou input, un filtre ou kernel et un feature map. 

Prenons un example:

Voici une image ou j’ai simplifier le tout avec des 0 pour un pixel blanc et un 1 pour un pixel noir.

Nous allons ensuite traiter les pixels avec un kernel ou un filtre. C’est un autre tableau qu’on va utiliser pour modifier les valeurs de notre grille qui représente notre image, similairement à la transformation que les cellules exercent sur l’information entrante. Prenons un kernel simple 3×3.

Ici, les “x” représente une multiplication qui sera exécutée de manière sériée sur l’image. Ceci sera fait en itérant le kernel sur l’image ou chaque valeur de pixel dans l’image sera transformé par la valeur de la cellule du kernel associée similairement à ceci: 

Ont peut ensuite voir comment le passage complet du kernel sur l’image produit ensuite ce qu’on appel un feature map qui est l’information extraite de l’image. Cette étape est nommée convolution et est au coeur de l’architecture de notre algorithme.

Le kernel peut être changé de taille, mais celui-ci est presque toujours exclusivement de taille impaire (3×3 ou 5×5). Ceci s’explique par le fait qu’ils permettent la comparaison d’une valeur centrale avec son entourage:

Nous pouvons aussi modifier le déplacement du kernel sur l’image original avec deux valeurs. Lap première étant le padding. Celle-ci contrôle d’où et jusqu’où le kernel peut se déplacer sur l’image. 

Dumoulin, Vincent, and Francesco Visin. “A Guide to Convolution Arithmetic for Deep Learning.” ArXiv:1603.07285 [Cs, Stat], Jan. 2018. arXiv.org, http://arxiv.org/abs/1603.07285.

De plus, nous pouvons aussi contrôler les foulée ou strides qui sont prise lors de la convolution comme cet exemple ou le kernel fait des enjambées de deux au lieu de un:

Dumoulin, Vincent, and Francesco Visin. “A Guide to Convolution Arithmetic for Deep Learning.” ArXiv:1603.07285 [Cs, Stat], Jan. 2018. arXiv.org, http://arxiv.org/abs/1603.07285.

Maintenant que nous avons la résultante de notre convolution: notre feature map, il faut parler d’un problème lié à celle-ci: sa spécificité géographique. Ce qui est entendu par ici peut être illustré par comment ces deux images similaires en tout points sauf en ce qui attrait à la position du chat ce qui donne deux feature map différentes!

Pour permettre à l’algorithme de ne pas être sensible à la localisation des points d’intérêts dans l’image nous utilisons une technique appelée pooling. Il existe deux approches: max pooling et average pooling. Max pooling consiste à créer, similairement à la convolution, une sous-zone de notre grille et prendre la valeur la plus élevée dans cette sous-zone. le average pooling consiste à prendre la moyenne de la sous-zone. 

Mais comment la décision finale de l’algorithme est prise? Il faut 3 choses: 

  1. Aplatir notre feature map
  2. Créer une ou des hidden layers
  3. Créer un output layer pour prendre la décision finale. 

l’aplatissement du feature map est appelé flattening ou flatten layer. C’est simplement le passae d’une grille NxN à une grille 1x(N*N). Par exemple, une grille 3×3 devient une grille de 1×9.

Par la suite cette couche d’aplatissement est liée à des couches de neurones dense qui agissent comme un réseau de neurones usuel. Sans trop de détails, ces couches vont traiter l’information extraite de l’image et permettre de prendre une décision. LA dernière couche est une couche dense aussi mais avec un nombre de neurones égale au classes que l’ont tente de prédire. Ici, 2 puisque nous avons 2 classes (mononucléaire et polynucléaire) cette dernière couche aurait 2 neurones. 

Maintenant que l’architecture de l’algorithme est couverte nous allons sauter à la construction de l’algorithme!

Installation

Voici une version détaillée de l’installation de python, anaconda et des packages nécessaire pour créer l’algorithme.

Installation de python:

Aller sur python/downloads et télécharger les fichiers nécessaires. L’installation devrait être conventionnelle sans surprise. Simplement suivre les instructions affichées par l’installateur. 

Par la suite il faut télécharger Anaconda. C’est un package manager, qui va nous permettre d’ajouter des package contenant des fonctionnalités nécessaires pour la création de notre algorithme. Pour l’installation, simplement aller sur anaconda/install/ et choisir l’installateur pour votre system d’opération. Les instructions sont bien détaillées pour chacun des installateurs.

Par la suite, malgré qu’Anaconda viens déjà avec plusieurs packages pré-installé, il faut tout de même ajouter keras, cv2, seaborn et sklearn.

Pour ce faire il faut aller ouvrir son terminal.

Sur Mac: (command) + espace. Puis tapper terminal dans la barre de recherche. Peser sur Enter pour faire apparaître le terminal.

Sur Windows: (touche windows). Puis, tapper dans la barre de recherche cmd ou command prompt. Puis, ouvrir l’application command prompt. 

Ensuite, s’assurer que nos package sont installé en tapant/copy paste ces lignes dans votre terminal:

pip3 install -U scikit-learn keras opencv-python seaborn scipy==1.2.2

L'algorithme

Ouvrir votre session python simplement en tapant python dans le terminal ou voici le jupyter notebook. 

python

Commençons par installer nos packages et fonctions. (simplement copier coller le code dans votre session python)

import os
import numpy as np
import numpy as np
from keras.models import Sequential
from keras.layers import Dense, Dropout, Activation, Flatten
from keras.layers import Conv2D, MaxPooling2D, Lambda
from keras.layers import Dense
from keras.utils import np_utils
from keras.preprocessing.image import ImageDataGenerator
import sklearn
from sklearn.preprocessing import LabelEncoder
from sklearn.model_selection import train_test_split
import cv2
import scipy
from scipy.misc import imresize
import matplotlib.pyplot as plt
from sklearn.metrics import accuracy_score import seaborn as sns

Ceci s’assure que toutes les fonctionnalités nécessaires sont présentes pour être utilisées par notre code.

Par la suite, il faut s’assurer d’avoir le directory ou le lieu où les fichiers sont situés. En d’autres termes il faut trouver l’emplacement du fichier qui contient les fichiers TRAIN et TEST Pour moi c’est dans ‘/Users/alexis/bloods_cnn/WBC-Classification/CNN/datasets/‘.

train_ = 'TRAIN'
test_ = 'TEST'
_dir = '/Users/User_name/bloods_cnn/WBC-Classification/CNN/datasets/'

Sur Mac, aller dans votre Finder et chercher datasets par example. Ensuite cliquer à deux doigts pour fair apparaître les options. Puis peser sur la touche option (⌥) pour faire apparaître les options secondaires. Cliquer sur Copy “datasets” as pathname.

Sur Windows c’est le même processus simplement trouver le fichier dans votre File Explorer et peser sur Shift et faire un clique de droite sur le fichier pour faire apparaître les options secondaires. Ensuite cliquer sur Copy as path.

How-to-Geek. https://www.howtogeek.com/670447/how-to-copy-the-full-path-of-a-file-on-windows-10/

Par la suite, il va falloir créer nos datasets et combiner nos images pour créer nos datasets POLYNUCLÉAIRE et MONONUCLÉAIRE.

def annoter_photo(dir_, subdir):
    X = []
    y = []
    for type_cellulaire in os.listdir(os.path.join(_dir,subdir)):
        if type_cellulaire in ['EOSINOPHIL','NEUTROPHIL']:
            annotation = 'POLYNUCLÉAIRE'
        elif type_cellulaire in ['LYMPHOCYTE','MONOCYTE']:
            annotation = 'MONONUCLÉAIRE'
        for image_filename in os.listdir(os.path.join(_dir,subdir,type_cellulaire)):
            img_file = cv2.imread(os.path.join(_dir,subdir,type_cellulaire,image_filename))
            if img_file is not None:
                img_file = scipy.misc.imresize(arr=img_file, size=(120, 160, 3))
                img_arr = np.asarray(img_file)
                X.append(img_arr)
                y.append(annotation)
    X = np.asarray(X)
    y = np.asarray(y)
    return X,y

La fonction ici s’assure de créer un groupe nommé polynucléaire et de rendre toute les images dans un format de 120×160 pixels. Ceci est important puisque l’entrée de données dans notre algorithme doit être identique pour chaque image. Le 3 dans (120,160,3) est nécessaire puisque nous avons des images en RGB (une sous-image par couler de décomposition).

Pour appliquer la fonction sur toutes nos données, nous avons simplement à les rouler sur nos fichiers TEST et TRAIN.

X_train, y_train = annoter_photo(_dir,train_)
X_test, y_test = annoter_photo(_dir,test_)

Maintenant, puisque l’ordinateur préfère fonctionner avec des chiffres, nous allons renommer les images de polynucléaires en 0 et les mononucléaires en 1. Ceci peut être fait par le script suivant:

encoder = LabelEncoder()
encoder.fit(y_train)
y_train = encoder.transform(y_train)
y_test = encoder.transform(y_test)

Maintenant, la construction de notre réseau de neurones convolutifs!

model = Sequential()
model.add(Lambda(lambda x: x * 1./255., input_shape=(120, 160, 3), output_shape=(120, 160, 3)))
model.add(Conv2D(32, (3, 3), input_shape=(120, 160, 3)))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))

model.add(Conv2D(32, (3, 3)))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))

model.add(Conv2D(64, (3, 3)))
model.add(Activation('relu'))
model.add(MaxPooling2D(pool_size=(2, 2)))

model.add(Flatten())
model.add(Dense(64))
model.add(Activation('relu'))
model.add(Dropout(0.7))
model.add(Dense(1))
model.add(Activation('sigmoid'))
Pour avoir une représentation du model créé, simplement rouler ce code:
print(model.summary())

Maintenant le modèle créé, il ne reste plus qu’à le laisser s’entrainer sur les données TRAIN!

history = model.fit(
    X_train,
    y_train,
    validation_split=0.2,
    epochs=20,
    shuffle=True,
    batch_size=32)

Ici, notre algorithme s’entraine et tente d’améliorer sa capacité à mieux prédire si une image est bel et bien mononucléaire ou polynucléaire. Nous pouvons voir que la valeur de précision ou accuracy (acc) passe de 0.71 à 0.96 sur les 20 époques (epochs).

Nous pouvons valider la précision finale de notre algorithme avec le code suivant:
y_pred = np.rint(model.predict(X_test))
print(accuracy_score(y_test, y_pred))
Nous pouvons créer notre confusion matrix pour voir la quantité de vrai et faux positifs et négatifs.