MNSIT : Reconnaître les chiffres (Partie 2)

Dans le précédent article sur la reconnaissance de digits (MNSIT), nous avons juste récupéré les données de la compétition Kaggle (42000 images) et nous avons lancé un premier entrainement suivi d’une prédiction sur le jeu de test qui a produit un très modeste 84%. L’objectif de cet article est de vous montrer comment au travers de nouvelles astuces nous allons pouvoir booster ce score à +97% !

Bilan et difficultés

Ce fameux 84% nous l’avons donc obtenu sans rien changer aux données et via l’utilisation d’un simple algorithme de Machine Learning : la descente de gradient Stochastique (SGD). Nous avons donc comme souvent au moins trois pistes d’améliorations et de travail :

  1. Ajuster les caractéristiques (feature-engineering)
  2. Changer d’algorithme (Random Forest par exemple)
  3. Changer et ajuster les hyper-paramètres

Ajuster les caractéristiques

Si l’ajustement et le re-travail parait assez évident quand on travaille sur des « données métier » comme celles du Titanic, c’est tout de suite plus abstrait lorsque l’on aborde des images ou plus généralement des médias. Impossible en effet de combiner en effet des variables entres elles de la même manière. Comment en effet extraire de nouvelles informations à partir de pixels ? La tâche s’annonce donc ardue, et surtout très différente de ce que l’on avait fait jusqu’à présent.

Il va falloir trouver de nouvelles pistes d’amélioration avec des données que l’on a en main.

Voici quelques pistes que je propose :

  • Mettre à l’échelle les données (indispensable en fait 😉 )
  • Nettoyer les images en supprimant par exemple les pixels trop clairs
  • ‘Binariser’ les images
  • Faire de la retouche d’images plus ciblée encore

A part le premier point qui j’en suis certain améliorera le résultat, je n’ai aucune certitudes pour les autres approches … On va donc devoir tout tester ! Il sera même interressant de combiner les astuces pour voir ce que cela donne 😉

Mise à l’echelle

Certains algorithmes de Machine Learning sont très sensibles au échelles de valeurs des caractéristiques. On savait que les algorithmes ne savaient pas gérer de données autres que des données numériques, mais même ces données doivent être circonscrites dans les mêmes espaces de valeurs.

Ce type de retraitement, bien souvent indispensable est complémentaire au Feature Engineering que nous avons déjà vu précédemment et s’appelle le Feature Scaling. Nous verrons dans un prochain article les diverses options qui s’offrent à nous. Dans l’attente je vous suggère de vous référer à la documentation de Scikit-Learn.

Dans le cadre de ce projet MNSIT nous allons utiliser la mise à l’échelle Min-Max de scikit-learn :

scale = MinMaxScaler()
X_TRAIN = scale.fit_transform(X_TRAIN)

Pour résumer simplement cette opération MinMaxScaler() va convertir les valeurs de la distribution pour qu’elles se situent entre 0 et 1 (-1 et 1 si on avait des valeurs négatives).

Nettoyer les images

Nous pouvons tout aussi bien nettoyer les images. La première opération pourrait consister à supprimer les bruits. Dans notre cas les bruits sont bien souvent les pixels résiduels qui ne devraient pas être là. Par exemple ceux qui auraient pu aparraitre lors d’un scan de l’image qui était initialement écrite à la main.

Une première approche simpliste nous inciterait à supprimer les pixels inférieurs à une certaine valeur. En effet nous sommes en noir et blanc et si on supprime les pixels de valeurs faible il y a peu de chance que cela influe sur le digit lui-même. Essayons.

def removeNoise(val):
    if (val < 20):
        return 0
    else:
        return val

# Remove noise to the global dataset
def removeNoiseDataset(dataset):
    for i in range(dataset.shape[1]):
        dataset['pixel' + str(i)] = [removeNoise(x) for x in dataset['pixel' + str(i)]]

removeNoiseDataset(X_TRAIN)

Voyez le résultat :

NB: Il est bien sur possible de retoucher encore en changeant la valeur de seuil qui est ici à 20.

'Binariser' les images

Pourquoi ne pas avoir une approche encore plus extrême. Nos images sont en effet en niveau de gris (les pixel de 0 à 255 représentent l'intensité du noir). Et si à partir d'un seuil nous décidions qu'un pixel est soit blanc (0) soit noir (255) ? pour faire plus simple nous allons transformer les valeurs en 0 (blanc) ou 1(noir) selon une valeur de seuil atteinte.

# dark or white / wash the pixel
def darkOrWhite(val):
    if (val > 150):
        return 1
    else:
        return 0

# Clean a global dataset
def darkOrWhiteDataset(dataset):
    for i in range(dataset.shape[1]):
        dataset['pixel' + str(i)] = [darkOrWhite(x) for x in dataset['pixel' + str(i)]]

darkOrWhiteDataset(X_TRAIN)

Faire de la retouche d'images plus ciblée encore

La solution encore plus extrême pourrait consister à utiliser un outil de retouche d'image beaucoup plus sérieux que les opérations que j'ai effectuées précédemment afin de les retoucher intelligemment. SI vous voulez explorer cette voie je vous suggère d'utiliser Imagemagick.

Faire gonfler artificiellement le jeu d'entrainement

Nous avons 42000 images dans notre jeu d'entrainement. Et bien ce n'est pas assez, d'ailleurs en utilisant certains algorithmes j'ai très vite atteint un glorieux mais finalement très décevant 100% lors de l'entrainement. Pourquoi décevant ?

Tout simplement parce qu’un 100% veut clairement dire overfitting (sur-entrainement). Celà signifie que mon modèle s'ajuste trop bien aux données d'entrainement et qu'il risque de ne pas être capable de se généraliser lors de la phase de test.

J'ai donc besoin de plus de données ! je pourrais aller chercher d'autres images dans la base de données MNSIT (qui en compte 70000), mais soyons réglo et battons nous avec ce que nous a donné Kaggle. Nous allons devoir réutiliser les données (42000 images) en notre possession en les dupliquant de manière intelligente.

Pourquoi ne pas créer de nouvelles images à partir de celles existantes, mais décalées (dans toutes les directions), on pourrait aussi changer des intensité de pixels, etc.

Voici les fonctions de décalage que j'ai mises en oeuvre :

# returns the image in digit (28x28)
# fromIndex = 0 if no labels 1 else
def getImageMatriceDigit(dataset, rowIndex, fromIndex):
    return dataset.iloc[rowIndex, fromIndex:].values.reshape(28,28)

# returns the image matrix in one row
# fromIndex = 0 if no labels 1 else
def getImageLineDigit(dataset, rowIndex, fromIndex):
    return dataset.iloc[rowIndex, fromIndex:]

# shift the image
def shiftImage(imageMatrix, shiftConfig):
    return sc.shift(imageMatrix, shiftConfig, cval=0)

# convert an image 28:28 in one matrix row
def convertImageInRow(img):
    return pd.DataFrame(img.reshape(1,784),
                        columns=["pixel" + str(x) for x in range(784)])

# returns 4 images shifted from the original one
def shift4LineImages(_imageMatrix, label):
    shft = 1
    row1 = convertImageInRow(shiftImage(_imageMatrix, [0, shft]))
    row2 = convertImageInRow(shiftImage(_imageMatrix, [shft, 0]))
    row3 = convertImageInRow(shiftImage(_imageMatrix, [shft * -1, 0]))
    row4 = convertImageInRow(shiftImage(_imageMatrix, [0, shft * -1]))
    row1.insert(0, 'label', label)
    row2.insert(0, 'label', label)
    row3.insert(0, 'label', label)
    row4.insert(0, 'label', label)
    #row1['label'] = row2['label'] =row3['label'] =row4['label'] = label
    return pd.concat([row1, row2, row3, row4], ignore_index=True)

X_NEWTRAIN = pd.DataFrame()
Rangeloop = range(X_TRAIN.shape[0])
for rowIdx in Rangeloop:
    if (rowIdx % 1 == 100):
        print ("Index: " + str(rowIdx) + " | Shape: " + str(X_NEWTRAIN.shape))
    X_NEWTRAIN = pd.concat([X_NEWTRAIN, shift4LineImages(getImageMatriceDigit(TRAIN, rowIdx, 1), y[rowIdx])], ignore_index=True)

X_NEWTRAIN = pd.concat([X_NEWTRAIN, TRAIN], ignore_index=True)

En appliquant cette fonction on multiplie par 5 le nombre de données (images) en rajoutant au jeu initial 4 nouvelles images par image d'origines décalées dans toutes les directions. Nous avons donc après un assez long traitement un jeu de données comprenant 210000 images.

Le gain est assez flagrant est vous allez vite atteindre les 97% sur Kaggle.

Conclusion

Maintenant vous avez les données retravaillées. Il reste juste à trouver le bon algorithme et l'ajuster comme il convient. Il va falloir être patient car ayant rajouté pas mal de données les traitements seront bien plus long. Je vous suggère de rester sur de la bonne artillerie lourde tel que RandomForest ou SVC et de trouver les bon hyper-paramètres (une recherche par quadrillage sera certainement pratique). Maintenant obtenir plus que 97% (éventuellement 98%) reste illusoire avec ce type d'approche ... il va falloir vraiment sortir les gros calibres et songer à du Deep learning si vous voulez réellement améliorer ce score ! Mais ça c'est encore une autre histoire 😉

Partager cet article

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.