Reconnaissance de caractères manuscrits (MNIST) par apprentissage épais (deep learning) avec tensorflow et Keras

Introduction

Objectif

Ce calepin reprend le même objectif que les calepins de l'Atelier MNIST et sur les mêmes données mais en utilisant cette fois les librairies Keras et TensorFlow pour aborder l'apprentissage profond. Il est une adpatation du tutoriel de Keras.

Importation des librairies


In [ ]:
%matplotlib inline 
import matplotlib.pyplot as plt
import seaborn as sb
sb.set()

import pandas as pd
import numpy as np
import time

import keras.utils as ku
import keras.models as km
import keras.layers as kl
import keras.optimizers as ko

from sklearn.metrics import confusion_matrix

# Paramètres
batch_size = 128
epochs = 10

import sys
print(sys.version)
from tensorflow.python.client import device_lib
print(device_lib.list_local_devices())

In [ ]:
import keras
keras.__version__

Lecture des données d'apprentissage et de test

Les données peuvent être préalablement téléchargées ou directement lues. Ce sont celles originales du site MNIST DataBase mais préalablement converties au format .csv, certes plus volumineux mais plus facile à lire. Attention le fichier mnist_train.zip présent dans le dépôt est compressé.


In [ ]:
# Lecture des données d'apprentissage
N_classes = 10

# path="" # Si les données sont dans le répertoire courant sinon:
path=""
Dtrain=pd.read_csv(path+"mnist_train.zip",header=None)
X_train = Dtrain.values[:,:-1]
Y_train = Dtrain.values[:,-1]

Dtest=pd.read_csv(path+"mnist_test.csv",header=None)
X_test = Dtest.values[:,:-1]
Y_test = Dtest.values[:,-1]

Attention, avec Keras, la variable réponse doit être une matrice binaire où chaque classe est représentée par une indicatrice: pour chaque individu, l'élément de la colone correspondant à la classe à laquelle il appartient est à 1, sinon il est à 0.

Keras possède une fonction to_catergorical permettant de convertir directement le vecteur variable Y_train de réponse en matrice (array numpy) indicatriceY_train_cat.

C'est l'équivalent de get_dummies de pandas ou OneHotEncoder de scikit-learn.


In [ ]:
Y_train_cat = ku.to_categorical(Y_train, N_classes)
Y_test_cat = ku.to_categorical(Y_test, N_classes)

Apprentissage et prévision du test Avec réseau dense

Première tentative d'appliquer un réseaux de neurone de type Perceptron classique avec 4 couches:

  • Dense: 52 neurones + Foncton d'activation relu
  • Dropout: 20% des neurones tiré aléatoirement sont desactivés
  • Dense: 52 neurones + Foncton d'activation relu
  • Dropout: 20% des neurones tiré aléatoirement sont desactivés

Une dernière couche softmax fournit la classification

Apprentissage


In [ ]:
X_train.shape

In [ ]:
# Définition du réseau
model = km.Sequential()
model.add(kl.Dense(128, activation='relu', input_shape=(784,)))
model.add(kl.Dropout(0.2))
model.add(kl.Dense(128, activation='relu'))
model.add(kl.Dropout(0.2))
model.add(kl.Dense(N_classes, activation='softmax'))
# Réumé
model.summary()

Q Retrouvez manuellement le nombre de paramètres.


In [ ]:
# apprentissage
model.compile(loss='categorical_crossentropy',
              optimizer=ko.RMSprop(),
              metrics=['accuracy'])
ts = time.time()
history = model.fit(X_train, Y_train_cat,
                    batch_size=batch_size,
                    epochs=epochs,
                    verbose=1,
                    validation_data=(X_test, Y_test_cat))
te = time.time()
t_train_mpl = te-ts

Résultats


In [ ]:
score_mpl = model.evaluate(X_test, Y_test_cat, verbose=0)
predict_mpl = model.predict(X_test)
print('Test loss:', score_mpl[0])
print('Test accuracy:', score_mpl[1])
print("Time Running: %.2f seconds" %t_train_mpl )
fig=plt.figure(figsize=(7,6))
ax = fig.add_subplot(1,1,1)
ax = sb.heatmap(pd.DataFrame(confusion_matrix(Y_test, predict_mpl.argmax(1))), annot=True, fmt="d")

Q Que dire de ces résultats ?

Q Faites tourner de nouveaux l'algorithme en normalisant les données afin que celles-ci soit comprises entre 0 et 1. Qu'observez vous?

Convolutional Layers

Format des données

Dans les exemples précédents. Les données était "applaties". Une imade de $28\times 28=784$ pixels est considérée comme un vecteur.

Pour pouvoir utiliser le principe de la convolution la structure des images est conservée. Une image n'est pas un vecteur de tailles $784\times 1$ mais une matrice de taille $28\times 28$. Une troisième dimension est également nécessaire pour décrire afin de prendre en compte les différents channels de l'image. Dans le cas de MNIST cette dernière dimension est de taille 1 car les pixels ne sont décrits qu'avec un seul niveau de gris. Cependant, des images couleurs en RGB sont généralement codées avec trois niveaux d'intensité (Rouge, Vert et Bleus).

Ainsi X_train est réorganisée en cube ou multitableau de dimensions $60000\times 28\times 28\times 1$ pour être utilisé dans un réseau de convolution avec Keras.


In [ ]:
X_train_conv = X_train.reshape(60000, 28, 28, 1)
X_test_conv = X_test.reshape(10000, 28, 28, 1)

Visualisation des données


In [ ]:
import keras.preprocessing.image as kpi
fig  = plt.figure(figsize=(5,5))
ax = fig.add_subplot(1,1,1)
x = kpi.img_to_array(X_train_conv[0])
ax.imshow(x[:,:,0]/255, interpolation='nearest', cmap="binary")
ax.grid(False)
plt.show()

Edge detection

Dans cette partie vous pouvez explorer l'effet de filtre de convolution simple sur une image.

Un réseau de neuronne constitué d'une couche de convolution constitué d'un seul filtre définie manuellement (non appris par optimisation) est définie dans le code ci-dessous.


In [ ]:
from keras.models import Sequential
from keras.layers import Conv2D

conv_filter = np.array([
        [0.2, -0.2, 0],
        [0.2, -0.2, 0],
        [0.2, -0.2, 0],
    ])

def my_init_filter(shape, conv_filter = conv_filter, dtype=None):
    xf,yf = conv_filter.shape
    array = conv_filter.reshape(xf, yf, 1, 1)
    return array
my_init_filter(0).shape

conv_edge = Sequential([
    Conv2D(kernel_size=(3,3), filters=1, kernel_initializer=my_init_filter, input_shape=(28, 28, 1))   
])

Q Notez que dans la fonction my_init_filter les dimensions de l'image sont modifiés. A quoi correspondent les deux dimensions ajoutées?


In [ ]:
img_in = np.expand_dims(x, 0)
img_out = conv_edge.predict(img_in)

fig, (ax0, ax1, ax2) = plt.subplots(ncols=3, figsize=(15, 5))
ax0.imshow(img_in[0,:,:,0], cmap="binary")
ax0.set_title("Image originale")
ax0.grid(False)

norm_conv_filter = (conv_filter-conv_filter.min())/conv_filter.max()
ax1.imshow(norm_conv_filter.astype(np.uint8), cmap="binary")
ax1.set_title("Filtre")
ax1.grid(False)

ax2.imshow(img_out[0,:,:,0].astype(np.uint8), cmap="binary")
ax2.set_title("Image Filtre")
ax2.grid(False)

Q Que constatez vous? Verifiez que les dimensions de l'image en sortie sont cohérentes.

Q Testez ce même code avec un filtre différent.

Strides and Padding

Dans cette partie vous pouvez explorer l'effet des arguments strideset padding sur une image.


In [ ]:
from keras.models import Sequential
from keras.layers import Conv2D

conv_filter = np.array([
        [0, 0, 0],
        [0, 1, 0],
        [0, 0, 0],
    ])

def my_init_filter(shape, conv_filter = conv_filter, dtype=None):
    xf,yf = conv_filter.shape
    array = conv_filter.reshape(xf, yf, 1, 1)
    return array
my_init_filter(0).shape

conv_sp = Sequential([
    Conv2D(kernel_size=(3,3), filters=1, kernel_initializer=my_init_filter, input_shape=(28, 28, 1),
           strides=2,
           padding="SAME") ])

Q Quel est l'effet du filtre défini ici?


In [ ]:
img_in = np.expand_dims(x, 0)
img_out = conv_sp.predict(img_in)

fig, (ax0, ax1, ax2) = plt.subplots(ncols=3, figsize=(15, 5))
ax0.imshow(img_in[0,:,:,0].astype(np.uint8),
           cmap="binary");
ax0.grid(False)

norm_conv_filter = (conv_filter-conv_filter.min())/conv_filter.max()
ax1.imshow(norm_conv_filter.astype(np.uint8),
           cmap="binary");
ax1.grid(False)

ax2.imshow(img_out[0,:,:,0].astype(np.uint8),
           cmap="binary");
ax2.grid(False)

Q Modifiez les paramètres strideet padding, et observez l'effet sur la dimension des images.

Max Pooling

Exercice Ecrivez un code similaire pour observez l'effet du MaxPooling.


In [ ]:
# %load max_pooling.py

Convolutional Network (ConvNet)

Les propriété d'invariance par translation introduites par les couches opérant une convolution des images ont un impact important sur la qualité des résultats.

LeNet5

On teste dans un premier temps le modèle LeNet5 proposer par LeCun et al.


In [ ]:
LeNet5model = km.Sequential()
LeNet5model.add(kl.Conv2D(filters = 6, kernel_size = 5, strides = 1, activation = 'tanh',
input_shape = (28,28,1)))
LeNet5model.add(kl.MaxPooling2D(pool_size = 2, strides = 2))
LeNet5model.add(kl.Conv2D(filters = 16, kernel_size = 5,strides = 1, activation = 'tanh'))
LeNet5model.add(kl.MaxPooling2D(pool_size = 2, strides = 2))
LeNet5model.add(kl.Flatten())
LeNet5model.add(kl.Dense(units = 120, activation = 'tanh'))
LeNet5model.add(kl.Dense(units = 84, activation = 'tanh'))
LeNet5model.add(kl.Dense(units = 10, activation = 'softmax'))

LeNet5model.summary()

Q Retrouvez manuellement le nombre de paramètres.

Q Que dire du nombre de paramètres de ce réseau par rapport au réseau dense précédement défini?


In [ ]:
# Apprentissage
LeNet5model.compile(loss="categorical_crossentropy",
              optimizer=ko.Adadelta(),
              metrics=['accuracy'])
ts=time.time()
LeNet5model.fit(X_train_conv, Y_train_cat,
          batch_size=batch_size,
          epochs=epochs,
          verbose=1,
          validation_data=(X_test_conv, Y_test_cat))
te=time.time()
t_train_conv = te-ts

Q Que dire du temps de calcul? Pourquoi est-il plus long que le réseau Dense?

Résultats


In [ ]:
score_conv = LeNet5model.evaluate(X_test_conv, Y_test_cat, verbose=0)
predict_conv = LeNet5model.predict(X_test_conv)
print('Test loss:', score_conv[0])
print('Test accuracy:', score_conv[1])
print("Time Running: %.2f seconds" %t_train_conv )

fig=plt.figure(figsize=(7,6))
ax = fig.add_subplot(1,1,1)
ax = sb.heatmap(pd.DataFrame(confusion_matrix(Y_test, predict_conv.argmax(1))), annot=True, fmt="d")

Autre architecture

Réseau

Test d'un réseau de convolution constitué de 7 couches:

  • Une couche de convolution 2D, avec fenêtre de taille 3x3 et une fonction d'activation relu
  • Une couche de convolution 2D, avec fenêtre de taille 3x3 et une fonction d'activation relu
  • Une couche max pooling de fenêtre 2x2
  • Une couche dropout où 25% des neurones sont desactivés
  • Une couche Flatten transforme les images $N \times N$ en vecteurs $N^2$.
  • Une couche classique de 128 neurones
  • Une couche dropout ou 50% des neurones sont desactivés

Une couche softmax fournit la classification


In [ ]:
# descrition du réseau
model = km.Sequential()
model.add(kl.Conv2D(32, kernel_size=(3, 3), activation='relu', input_shape=(28,28, 1), data_format="channels_last"))
model.add(kl.Conv2D(64, (3, 3), activation='relu'))
model.add(kl.MaxPooling2D(pool_size=(2, 2)))
model.add(kl.Dropout(0.25))
model.add(kl.Flatten())
model.add(kl.Dense(128, activation='relu'))
model.add(kl.Dropout(0.5))
model.add(kl.Dense(N_classes, activation='softmax'))
# Résumé
model.summary()
# Apprentissage
model.compile(loss="categorical_crossentropy",
              optimizer=ko.Adadelta(),
              metrics=['accuracy'])
ts=time.time()
model.fit(X_train_conv, Y_train_cat,
          batch_size=batch_size,
          epochs=epochs,
          verbose=1,
          validation_data=(X_test_conv, Y_test_cat))
te=time.time()
t_train_conv = te-ts

In [ ]:
score_conv = model.evaluate(X_test_conv, Y_test_cat, verbose=0)
predict_conv = model.predict(X_test_conv)
print('Test loss:', score_conv[0])
print('Test accuracy:', score_conv[1])
print("Time Running: %.2f seconds" %t_train_conv )

fig=plt.figure(figsize=(7,6))
ax = fig.add_subplot(1,1,1)
ax = sb.heatmap(pd.DataFrame(confusion_matrix(Y_test, predict_conv.argmax(1))), annot=True, fmt="d")

Q Commenter les résultats. Comparer avec les autres techniques d'apprentissage.

Q Comment améliorer encore ces résultats?