Cet article commente le tutoriel Tensorflow #1 de Magnus Erik Hvass Pedersen  : Simple Linear Model.

La vidéo du tutoriel est ci-dessous (en anglais).

A moins d’être déjà bien familier avec Tensorflow (Tf) et le Machine Learning (ML), il est peu probable que tout soit très clair pour vous – non pas que ce tutoriel est mauvais (au contraire) mais parce qu’il suppose quelques prérequis apportés ici.

You should be familiar with basic linear algebra, Python and the Jupyter Notebook editor. It also helps if you have a basic understanding of Machine Learning and classification.

Le code du tutoriel est ici (pour Jupyter) . Ce code est commenté dans notre article.

Pour voir la vidéo Youtube, c’est ici

MNIST

MNIST ( Modified ou Mixed National Institute of Standards and Technolog,) est le Hello World du Machine Learning. Il s’agit d’une base de données de chiffres manuscrits qu’il s’agit de reconnaitre.

En 2018, la meilleure performance de reconnaissance  annonce un taux d’erreur de 0,18% (en 2013, c’était 0,21%).

Imports

%matplotlib inline
import matplotlib.pyplot as plt
import tensorflow as tf
import numpy as np
from sklearn.metrics import confusion_matrix

Si vous lisez régulièrement nos articles, vous savez que l’environnement de développement suppose Anaconda auquel est ajouté Tensorflow.

%matplotlib

%matplotlib is a magic function in IPython. I’ll quote the relevant documentation here for you to read for convenience:

IPython has a set of predefined ‘magic functions’ that you can call with a command line style syntax. There are two kinds of magics, line-oriented and cell-oriented. Line magics are prefixed with the % character and work much like OS command-line calls: they get as an argument the rest of the line, where arguments are passed without parentheses or quotes. Lines magics can return results and can be used in the right hand side of an assignment. Cell magics are prefixed with a double %%, and they are functions that get as an argument not only the rest of the line, but also the lines below it in a separate argument.

%matplotlib inline sets the backend of matplotlib to the ‘inline’ backend:

With this backend, the output of plotting commands is displayed inline within frontends like the Jupyter notebook, directly below the code cell that produced it. The resulting plots will then also be stored in the notebook document.

En clair, avec la magic function %matplotlib, les graphique sont affichés dans Jupyter après l’appel des fonctions.

tf.__version__

La version de Tf utilisée au moment de la rédaction de cet article est la 1.10.0. Au moment de la rédaction de l’article de Magnus, la version était : 1.9.0

Lecture des données

from mnist import MNIST
data = MNIST(data_dir="data/MNIST/")

Dans d’autres tutoriels on lit les données MNIST de cette façon :

from tensorflow.examples.tutorials.mnist import input_data
mnist = input_data.read_data_sets('MNIST_data', one_hot=True)

our avec keras :

from keras import backend as K
from keras import layers
from keras.datasets import mnist

(x_train, y_train), (x_test, y_test) = mnist.load_data()

Les données lues ont les caractéristiques suivantes :

data.num_train, data.num_val, data.num_test, data.img_size_flat, data.img_shape, data.num_classes
(55000, 5000, 10000, 784, (28, 28), 10)

Les classes sont codées sous 2 formes, en hot encoding et avec un chiffre.

data.y_test[0:5, :]
array([[0., 0., 0., 0., 0., 0., 0., 1., 0., 0.],
       [0., 0., 1., 0., 0., 0., 0., 0., 0., 0.],
       [0., 1., 0., 0., 0., 0., 0., 0., 0., 0.],
       [1., 0., 0., 0., 0., 0., 0., 0., 0., 0.],
       [0., 0., 0., 0., 1., 0., 0., 0., 0., 0.]])
data.y_test_cls[0:5]
array([7, 2, 1, 0, 4])

Affichage des images

La fonction plot_images qui permet d’afficher 9 images en même temps n’est pas commentée

def plot_images(images, cls_true, cls_pred=None):
    assert len(images) == len(cls_true) == 9
    
    # Create figure with 3x3 sub-plots.
    fig, axes = plt.subplots(3, 3)
    fig.subplots_adjust(hspace=0.3, wspace=0.3)

    for i, ax in enumerate(axes.flat):
        # Plot image.
        ax.imshow(images[i].reshape(img_shape), cmap='binary')

        # Show true and predicted classes.
        if cls_pred is None:
            xlabel = "True: {0}".format(cls_true[i])
        else:
            xlabel = "True: {0}, Pred: {1}".format(cls_true[i], cls_pred[i])

        ax.set_xlabel(xlabel)
        
        # Remove ticks from the plot.
        ax.set_xticks([])
        ax.set_yticks([])
        
    # Ensure the plot is shown correctly with multiple plots
    # in a single Notebook cell.
    plt.show()

Pour afficher une seule image du jeu de données, sans légende, utiliser :

my_image=data.x_test[0].reshape(img_shape)
my_image.shape
plt.matshow(my_image)

ou

plt.imshow(my_image, cmap='binary')

ce qui vous donnera :

Tensorflow

Les avantages de Tf, d’après l’auteur, sont l’existence du graphe (computational graph), le calcul automatique du gradient, la distribution sur CPU/GPU.

Placeholders

Placeholder variables serve as the input to the graph that we may change each time we execute the graph. We call this feeding the placeholder variables and it is demonstrated further below.

La définition du placeholder n’est pas évidente.

So far we have used Variables to manage our data, but there is a more basic structure, the placeholder. A placeholder is simply a variable that we will assign data to at a later date. It allows us to create our operations and build our computation graph, without needing the data. In TensorFlow terminology, we then feed data into the graph through these placeholders.

La différence entre un placeholder et une variable :

Think of Variable in tensorflow as a normal variables which we use in programming languages. We initialize variables, we can modify it later as well. Whereas placeholder doesn’t require initial value. Placeholder simply allocates block of memory for future use. Later, we can use feed_dict to feed the data into placeholder. By default, placeholder has an unconstrained shape, which allows you to feed tensors of different shapes in a session. You can make constrained shape by passing optional argument -shape, as I have done below.

Si cette explication n’est pas satisfaisante pour vous, dites-vous qu’on utilise :

The difference is that with tf.Variable you have to provide an initial value when you declare it. With tf.placeholder you don’t have to provide an initial value and you can specify it at run time with the feed_dict argument inside Session.run

In short, you use tf.Variable for trainable variables such as weights (W) and biases (B) for your model.

Cette explication est plus complète :

Since Tensor computations compose of graphs then it’s better to interpret the two in terms of graphs.

Take for example the simple linear regression WX+B=Y(where W and B stand for the weights and bias and X for the observations’ inputs and Y for the observations’ outputs). Obviously X and Y are of the same nature which differs from that of W and B. X and Y are values of the samples(observations) and hence need a place to be filled, while W and B are the weights and bias, Variables(the previous value affects the later) in the graph which should be trained using different X and Y pairs. We place different samples to the Placeholders to train the Variables.

We can and only need to save or restore the Variables to save or rebuild the graph. Placeholders are mostly holders for the different datasets (for example training data or test data) but Variables are trained in the training process and remain the same(to predict the outcome of the input or map the inputs and outputs[labels] of the samples) later until you retrain the model(using different or the same samples to fill into the Placeholders often through the dict, for instance session.run(a_graph, dict={a_placeholder_name: sample_values}), Placeholders are also passed as parameters to set models).

If you change placeholders(add or delete or change the shape and etc) of a model in the middle of training, you still can reload the checkpoint without any other modifications. But if the variables of a saved model are changed you should adjust the checkpoint accordingly to reload it and continue the training.

To sum up, if the values are from the samples(observations you already have) you safely make a placeholder to hold them, while if you need a parameter to be trained harness a Variable(simply put, set the Variables for the values you want to get using TF automatically).

Ce qui veut dire :

Placeholder :

  1. A placeholder is simply a variable that we will assign data to at a later date. It allows us to create our operations and build our computation graph, without needing the data. In TensorFlow terminology, we then feed data into the graph through these placeholders.
  2. Initial values are not required but can have default values with tf.placeholder_with_default)
  3. We have to provide value at runtime like :
a = tf.placeholder(tf.int16) // initialize placeholder value
b = tf.placeholder(tf.int16) // initialize placeholder value

use it using session like :

sess.run(add, feed_dict={a: 2, b: 3}) // this value we have to assign at runtime


Variable :

  1. A TensorFlow variable is the best way to represent shared, persistent state manipulated by your program.
  2. Variables are manipulated via the tf.Variable class. A tf.Variable represents a tensor whose value can be changed by running ops on it.

Example :

tf.Variable("Welcome to tensorflow!!!")

Dans notre code, on définit un palceholder pour les images et d’autres pour les labels (les étiquettes des images), codés en one-hot encoding et en classe numérique.

x = tf.placeholder(tf.float32, [None, img_size_flat])
y_true = tf.placeholder(tf.float32, [None, num_classes])
y_true_cls = tf.placeholder(tf.int64, [None])

Variables

Nous sommes dans le cas  WX+B=Y. Nous avons nos placeholders pour X et Y (images et labels). Il nous faut désormais nous intéresser aux poids et aux biais.

weights = tf.Variable(tf.zeros([img_size_flat, num_classes]))
biases = tf.Variable(tf.zeros([num_classes]))

Les poids et les biais sont initialisés à 0. Ce sont des variables.

Il y a img_size_flat * num_classes poids, soit 784*10 poids et il y a 10 biais.

Model

logits = tf.matmul(x, weights) + biases
y_pred = tf.nn.softmax(logits)
y_pred_cls = tf.argmax(y_pred, axis=1)

Le modèle est classique (et déjà discuté) :  WX+B=Y.

On applique un softmax (voir ici) et on recherche l’index ayant la plus grande valeur (c’est à dire la classe).

Fonction de coût

La fonction de coût retenue est cross entropy tf.nn.softmax_cross_entropy_with_logits_v2

Il n’est pas simple de savoir quelle est la meilleure fonction de coût pour un problème donné. (voir ici).

Pour rappel, une fonction de coût mesure la distance entre la prédiction du modèle et les échantillons d’entraînement.

L’objectif est de minimiser cette fonction de coût.

cross_entropy = tf.nn.softmax_cross_entropy_with_logits_v2(logits=logits,
                                                           labels=y_true)
cost = tf.reduce_mean(cross_entropy)

Optimisation

optimizer = tf.train.GradientDescentOptimizer(learning_rate=0.5).minimize(cost)

Une descente de gradient classique. Il faudra essayer avec un autre learning_rate. Jeremy Howard présente dans son cours une façon de trouver le meilleur learning_rate sans avoir à faire manuellement des essais.

Mesure des performances

Les fonctions suivantes ne servent pas directement à calculer les poids/biais du système. Il s’agit seulement de nous aider à analyser plus correctement l’amélioration des performances.

correct_prediction = tf.equal(y_pred_cls, y_true_cls)
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

TensorFlow Run

Avec Tf, après que le graphe soit construit, avant l’exécution, on commence par créer une session et initialiser les variables.

session = tf.Session()
session.run(tf.global_variables_initializer())

On a souvent des hyper-paramètres à définir, ici le batch size.

batch_size = 100

Optimize

def optimize(num_iterations):
    for i in range(num_iterations):
        # Get a batch of training examples.
        # x_batch now holds a batch of images and
        # y_true_batch are the true labels for those images.
        x_batch, y_true_batch, _ = data.random_batch(batch_size=batch_size)
        
        # Put the batch into a dict with the proper names
        # for placeholder variables in the TensorFlow graph.
        # Note that the placeholder for y_true_cls is not set
        # because it is not used during training.
        feed_dict_train = {x: x_batch,
                           y_true: y_true_batch}

        # Run the optimizer using this batch of training data.
        # TensorFlow assigns the variables in feed_dict_train
        # to the placeholder variables and then runs the optimizer.
        session.run(optimizer, feed_dict=feed_dict_train)

Les données sont transmises au moteur par batch de données prises au hasard.

Accuracy

Pour la mesure de l’exactitude, on utilise les données de test.

feed_dict_test = {x: data.x_test,
                  y_true: data.y_test,
                  y_true_cls: data.y_test_cls}
def print_accuracy():
    # Use TensorFlow to compute the accuracy.
    acc = session.run(accuracy, feed_dict=feed_dict_test)
    
    # Print the accuracy.
    print("Accuracy on test-set: {0:.1%}".format(acc))

Confusion matrix

En Machine Learning, on aime bien les matrices de confusion car elles permettent d’analyser facilement les résultats obtenus.

def print_confusion_matrix():
    # Get the true classifications for the test-set.
    cls_true = data.y_test_cls
    
    # Get the predicted classifications for the test-set.
    cls_pred = session.run(y_pred_cls, feed_dict=feed_dict_test)

    # Get the confusion matrix using sklearn.
    cm = confusion_matrix(y_true=cls_true,
                          y_pred=cls_pred)

    # Print the confusion matrix as text.
    print(cm)

    # Plot the confusion matrix as an image.
    plt.imshow(cm, interpolation='nearest', cmap=plt.cm.Blues)

    # Make various adjustments to the plot.
    plt.tight_layout()
    plt.colorbar()
    tick_marks = np.arange(num_classes)
    plt.xticks(tick_marks, range(num_classes))
    plt.yticks(tick_marks, range(num_classes))
    plt.xlabel('Predicted')
    plt.ylabel('True')
    
    # Ensure the plot is shown correctly with multiple plots
    # in a single Notebook cell.
    plt.show()

Images mal classifiées

Une fonction toute simple pour afficher des images mal classifiées

def plot_example_errors():
    # Use TensorFlow to get a list of boolean values
    # whether each test-image has been correctly classified,
    # and a list for the predicted class of each image.
    correct, cls_pred = session.run([correct_prediction, y_pred_cls],
                                    feed_dict=feed_dict_test)

    # Negate the boolean array.
    incorrect = (correct == False)
    
    # Get the images from the test-set that have been
    # incorrectly classified.
    images = data.x_test[incorrect]
    
    # Get the predicted classes for those images.
    cls_pred = cls_pred[incorrect]

    # Get the true classes for those images.
    cls_true = data.y_test_cls[incorrect]
    
    # Plot the first 9 images.
    plot_images(images=images[0:9],
                cls_true=cls_true[0:9],
                cls_pred=cls_pred[0:9])

Affichage des poids

def plot_weights():
    # Get the values for the weights from the TensorFlow variable.
    w = session.run(weights)
    
    # Get the lowest and highest values for the weights.
    # This is used to correct the colour intensity across
    # the images so they can be compared with each other.
    w_min = np.min(w)
    w_max = np.max(w)

    # Create figure with 3x4 sub-plots,
    # where the last 2 sub-plots are unused.
    fig, axes = plt.subplots(3, 4)
    fig.subplots_adjust(hspace=0.3, wspace=0.3)

    for i, ax in enumerate(axes.flat):
        # Only use the weights for the first 10 sub-plots.
        if i<10:
            # Get the weights for the i'th digit and reshape it.
            # Note that w.shape == (img_size_flat, 10)
            image = w[:, i].reshape(img_shape)

            # Set the label for the sub-plot.
            ax.set_xlabel("Weights: {0}".format(i))

            # Plot the image.
            ax.imshow(image, vmin=w_min, vmax=w_max, cmap='seismic')

        # Remove ticks from each sub-plot.
        ax.set_xticks([])
        ax.set_yticks([])
        
    # Ensure the plot is shown correctly with multiple plots
    # in a single Notebook cell.
    plt.show()

Performance avant optimisation

Lorsqu’on mesure l’exactitude des résultats obtenus avant le lancement de l’optimisation, on obtient 9,8% – parce que le logiciel prévoir toujours un 0 (les poids) et qu’il y a 9,8% de 0 dans le test-set !

print_accuracy()
Accuracy on test-set: 9.8%

Performance après 1 optimisation

optimize(num_iterations=1)
print_accuracy()
Accuracy on test-set: 40.7%

Après une itération on obtient déjà 40,7% de résultats corrects.

Affichage des poids

plot_weights()

Il faut voir l’affichage des poids comme un filtre Les poids positifs sont rouges et les négatifs sont bleus.

Performance après 10 optimisations

10 optimisations c’est 1 déjà faite + 9 autres.

# We have already performed 1 iteration.
optimize(num_iterations=9)

Le résultat progresse :

print_accuracy()
Accuracy on test-set: 76.8%

On a désormais 76,8% de bons résultats.

L’affichage des poids est encore lisible.

Performance après 1000 optimisations

# We have already performed 10 iterations.
optimize(num_iterations=990)
print_accuracy()
Accuracy on test-set: 91.4%

On peut se dire que 91,4% de résultats c’est pas mal.

L’affichage des poids fournit des images de moins en moins compréhensibles.

Matrice de confusion

print_confusion_matrix()
[[ 955    0    2    4    0    3   11    2    3    0]
 [   0 1107    2    2    0    1    4    2   17    0]
 [   8   10  905   24    9    1   12   11   43    9]
 [   1    1   13  948    0    6    1    9   20   11]
 [   1    2    4    2  904    0   10    2    6   51]
 [  10    5    2   77   10  701   18   10   46   13]
 [  13    3    5    2   11    9  909    2    4    0]
 [   1   11   25    9    6    0    0  928    1   47]
 [   5   10    5   35    9   16   10    6  863   15]
 [   8    7    2   14   34    4    0   17    7  916]]

On constate que le 5 est souvent confondu avec le 3, le 5 et le 8.

Exercices

learning_rate

On nous propose de tester ce qu’apporte un changement de learning rate.

optimizer = tf.train.GradientDescentOptimizer(learning_rate=0.5).minimize(cost)

En effet, pourquoi learning_rate=0.5 est-il égal à 0.5 et pas à 0.6 ou 0.1 ?

On constate qu’en mettant learning_rate=0.1, on a le même résultat après 0 optimisation (normal), accuracy = 52,7% après 1 optimisation (au lieu de 40,7%)

Accuracy on test-set: 52.7%

accuracy = 75% après 10 optimisations (au lieu de 76,8%)

Accuracy on test-set: 75.0%

et accuracy = 91,4% après 1000 optimisations (comme avec lr = 0.5)

Accuracy on test-set: 91.4%

Faites l’exercice avec d’autres valeurs de learning_rate. On voit peu de différences.

optimizer

Si on change l’optimizer (au lieu de GradientDescentOptimizer on utilise AdagradOptimizer ou AdamOptimizer

optimizer = tf.train.GradientDescentOptimizer(learning_rate=0.5).minimize(cost)

est remplacé par :

optimizer = tf.train.AdagradOptimizer(learning_rate=0.5).minimize(cost)

après 1 optimisation accuracy = 19,1% (au lieu de 40,7%), après 10 optimisations 77,5% (au lieu de 76,8%) et 91,9% au lieu de 91,4%.

Cet optimizer donne de meilleurs résultats après un nombre élevé d’optimisations (91,9% au lieu de 91,4%).

 

Laisser un commentaire

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