Pretraitements avec TensorFlow

1 Introduction

TensorFlow est une plateforme open-source cree par Google pour faire de l’apprentissage automatique, et notamment du Deep Learning. Dans ce 1er document on decrit la phase de pretraitement des Dataframes pandas et des Datasets TensorFlow.

On trouve beaucoup de resources en ligne dont des tutoriaux qui tournent sous Colab https://www.tensorflow.org/tutorials?hl=fr

1.0.1 Notions sur les reseaux de neurones

Pour ceux qui ne connaissent pas trop le sujet, un exemple classique de reseau de neurones est la regression logistique : c’est un reseau de neurones a 0 couche cachee et avec la fonction sigmoide 11+ex comme fonction d’activation qui s’applique a une combinaison lineaire des variables en entree.

Un peu de terminologie :

  • les poids sont les parametres du modele
  • un exemple est une ligne de la table d’apprentissage, encore appele individu en statistique classique : un vecteur X des valeurs des variables explicatives et une valeur Y de la variable cible
  • un mini-batch est un petit ensemble d’exemples tires aleatoirement. La taille d’un mini-batch est souvent une puissance de 2 (32, 64, 128, …) car cela permet d’optimiser des calculs realises en parallele sur les processeurs de votre machine (CPU, GPU), processeurs dont le nombre est souvent une puissance de 2

L’entrainement d’un reseau de neurones ne se pratique pas sur tout le jeu de donnees d’apprentissage en un bloc comme le font les autres algorithmes d’apprentissage automatique, cela pour plusieurs raisons :

  • les reseaux de neurones necessitent souvent des volumes de donnees trop grands pour la RAM
  • la mise a jour par etapes successives des parametres du modele par l’algorithme de retropropagration du gradient necessite un calcul assez couteux de gradients de la fonction de perte, on ne veut pas le realiser sur l’integralite des exemples du jeu d’apprentissage

On utilise donc des mini-batchs couples a la technique de la descente de gradient stochastique (https://fr.wikipedia.org/wiki/Algorithme_du_gradient_stochastique). Pour realiser une etape de mise a jour des poids :

  • on tire un mini-batch du jeu de donnees d’apprentissage
  • pour chaque exemple X,Y du mini-batch on calcule le gradient L de la fonction de perte L. C’est le vecteur des derivees partielles de la fonction L vue comme une fonction mathematique des poids (cette fonction depend aussi bien sur des valeurs fixees de X et Y)
  • on obtient une estimation empirique du gradient en calculant la moyenne des gradients obtenus sur tous les exemples du mini-batch
  • on applique alors la formule de mise a jour des poids (w=wαL) ou α est le taux d’apprentissage, a prendre entre 0 et 1 (par exemple 0.01 ou 0.1)

Encore un peu de vocabulaire pour finir : une epoque correspond a la lecture de tout le jeu d’apprentissage. Si on a choisi des batchs de taille 64 et que le jeu d’apprentissage a 1000 lignes, il faut 16 etapes de mise a jour des poids pour constituer une epoque.

On a donc au moins 3 hyperparametres du modele a ajuster au mieux en fonction du jeu de donnees :

  • la taille des mini-batchs
  • la valeur du taux d’apprentissage (trop petit il va ralentir la convergence, trop grand il ne donnera pas le meilleur minima de la fonction de perte)
  • le nombre d’epoques car il n’y a aucune raison particuliere pour que l’algorithme converge apres une epoque seulement

On va traiter la modelisation dans des documents futurs, on n’a ici besoin que de la comprehension des batchs et des epoques.

1.0.2 Donnees et modules

On choisit un jeu de donnees avec des variables quantitatives et qualitatives et une cible multi-classes. Les variables explicatives devront etre mises au format TensorFlow ainsi que la variable cible qui doit etre recodee en entiers consecutifs commencant a 0.

library("reticulate")

# on a installe tensorflow dans l'environnement vituel conda "tf_env"
use_condaenv(condaenv = "tf_env")

# le jeu de donnees IncomeESL du package R arules
data(package = "arules", IncomeESL)

dtf_class = tidyr::drop_na(IncomeESL)
colnames(dtf_class) = gsub(" ", "_", colnames(dtf_class))

dtf_class = dtf_class[c("number_in_household", "marital_status", "householder_status", "income")]

# conversion de colonnes quali en quanti
dtf_class$number_in_household = as.character(dtf_class$number_in_household)
dtf_class$number_in_household[dtf_class$number_in_household == "9+"]= "9"
dtf_class$number_in_household = as.integer(dtf_class$number_in_household)

# conversion des facteurs en character
for (col_quali in colnames(dtf_class)[sapply(dtf_class, is.factor)]) {
  dtf_class[[col_quali]] = as.character(dtf_class[[col_quali]] )
}

# on remplace les , par des - dans la variable cible
dtf_class$income = gsub(",", "-", dtf_class$income)
import numpy as np
import pandas as pd
pd.set_option('display.max_columns', None)

import sys
import pprint
pp = pprint.PrettyPrinter(indent=4)

import tensorflow as tf
from tensorflow import feature_column
from tensorflow.keras import layers

tf.random.set_seed(2021)

Les versions Python et TensorFlow utilisees.

sys.version
'3.9.6 (default, Jul 30 2021, 11:42:22) [MSC v.1916 64 bit (AMD64)]'
tf.__version__
'2.6.0'

Les donnees sous Python.

dtf_class = r.dtf_class

dtf_class.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 6876 entries, 0 to 6875
Data columns (total 4 columns):
 #   Column               Non-Null Count  Dtype 
---  ------               --------------  ----- 
 0   number_in_household  6876 non-null   int32 
 1   marital_status       6876 non-null   object
 2   householder_status   6876 non-null   object
 3   income               6876 non-null   object
dtypes: int32(1), object(3)
memory usage: 188.1+ KB
dtf_class.head()
   number_in_household marital_status        householder_status   income
0                    5        married                       own      75+
1                    3        married                      rent      75+
2                    4         single  live with parents/family   [0-10)
3                    4         single  live with parents/family   [0-10)
4                    2        married                       own  [50-75)

Frequences des modalites de la cible.

dtf_class.income.value_counts(normalize=True).sort_index()
75+        0.108057
[0-10)     0.182519
[10-15)    0.076934
[15-20)    0.073444
[20-25)    0.089878
[25-30)    0.076643
[30-40)    0.123037
[40-50)    0.114020
[50-75)    0.155468
Name: income, dtype: float64

Dictionnaire pour recoder la variable cible.

# mapping pour recoder les modalites cibles en entiers
dico_income = {'[0-10)':0,'[10-15)':1,'[15-20)':2, '[20-25)':3,'[25-30)':4, '[30-40)':5, '[40-50)':6, '[50-75)':7,'75+':8}
pp.pprint(dico_income)
{   '75+': 8,
    '[0-10)': 0,
    '[10-15)': 1,
    '[15-20)': 2,
    '[20-25)': 3,
    '[25-30)': 4,
    '[30-40)': 5,
    '[40-50)': 6,
    '[50-75)': 7}

2 Les datasets

TensorFlow n’utilise pas les DataFrames pandas mais son propre format Dataset. La fonction tf.data.Dataset.from_tensor_slices permet de convertir un DataFrame pandas en Dataset Tensorflow dont on pourra lire les lignes par batch (voir plus bas).

dtf = dtf_class.copy()
labels = dtf.pop('income').map(dico_income)

dtf.head()
   number_in_household marital_status        householder_status
0                    5        married                       own
1                    3        married                      rent
2                    4         single  live with parents/family
3                    4         single  live with parents/family
4                    2        married                       own
labels[:5]
0    8
1    8
2    0
3    0
4    7
Name: income, dtype: int64
ds = tf.data.Dataset.from_tensor_slices(dict(dtf))
  
ds2 = tf.data.Dataset.from_tensor_slices((dict(dtf), labels))

Pour explorer le contenu d’un dataset, le plus simple est de le transformer en iterateur.

type(ds)
<class 'tensorflow.python.data.ops.dataset_ops.TensorSliceDataset'>
iter_batch = iter(ds.batch(5))
next(iter_batch)
{'number_in_household': <tf.Tensor: shape=(5,), dtype=int32, numpy=array([5, 3, 4, 4, 2])>, 'marital_status': <tf.Tensor: shape=(5,), dtype=string, numpy=
array([b'married', b'married', b'single', b'single', b'married'],
      dtype=object)>, 'householder_status': <tf.Tensor: shape=(5,), dtype=string, numpy=
array([b'own', b'rent', b'live with parents/family',
       b'live with parents/family', b'own'], dtype=object)>}
pd.DataFrame(next(iter_batch))
   number_in_household marital_status householder_status
0                    3      b'single'            b'rent'
1                    1    b'divorced'            b'rent'
2                    3     b'married'            b'rent'
3                    2     b'married'            b'rent'
4                    1      b'single'            b'rent'

Le dataset ds2 est essentiellement un couple (dictionnaire des predicteurs, tenseur de la variable cible).

type(ds)
<class 'tensorflow.python.data.ops.dataset_ops.TensorSliceDataset'>
iter_batch = iter(ds2.batch(5))
couple = next(iter_batch)
pd.DataFrame(couple[0])
   number_in_household marital_status           householder_status
0                    5     b'married'                       b'own'
1                    3     b'married'                      b'rent'
2                    4      b'single'  b'live with parents/family'
3                    4      b'single'  b'live with parents/family'
4                    2     b'married'                       b'own'
couple[1].numpy()
array([8, 8, 0, 0, 7], dtype=int64)

3 shuffle, repeat, batch

La lecture par batchs n’est pas suffisante : on doit par exemple aussi penser a permuter les donnees avec shuffle lors de l’apprentissage, pour que le modele ne voit pas les memes batchs a chaque nouvelle epoque. On applique ces trois methodes a un array numpy pour simplifier leur comprehension.

La documentation officielle :

Si on n’utilise que batch on va epuiser le dataset, ca revient a traiter une seule epoque. On voit au passage que la lecture des donnees est sequentielles, sans alea.

dataset = tf.data.Dataset.range(10)
dataset = dataset.batch(3)
pp.pprint(list(dataset.as_numpy_iterator()))
[   array([0, 1, 2], dtype=int64),
    array([3, 4, 5], dtype=int64),
    array([6, 7, 8], dtype=int64),
    array([9], dtype=int64)]

En ajoutant un repeat on augmente le nombe d’epoques.

dataset = tf.data.Dataset.range(10)
dataset = dataset.batch(3)
dataset = dataset.repeat(2)
pp.pprint(list(dataset.as_numpy_iterator()))
[   array([0, 1, 2], dtype=int64),
    array([3, 4, 5], dtype=int64),
    array([6, 7, 8], dtype=int64),
    array([9], dtype=int64),
    array([0, 1, 2], dtype=int64),
    array([3, 4, 5], dtype=int64),
    array([6, 7, 8], dtype=int64),
    array([9], dtype=int64)]

Enfin avec shuffle on ajoute de l’alea, mais ce n’est pas une permutation sur la totalite des lignes :

  • on definit une taille de tampon n
  • a la 1ere selection d’un element, on place les n 1eres lignes du dataset dans un tampon, on les permute et on en choisit une
  • a la seconde selection d’un element, on ajoute la ligne n+1 au tampon qui a ainsi a nouveau n elements, on les permute, …

Utiliser un tampon permet de gagner du temps et de la memoire plutot que d’appliquer une permutation a l’ensemble des elements a chaque fois, et si le tampon est assez grand la permutation obtenue est suffisante.

dataset = tf.data.Dataset.range(20)
dataset = dataset
dataset = dataset.shuffle(buffer_size = 2)
list(dataset.as_numpy_iterator())
[1, 0, 3, 2, 4, 6, 5, 7, 9, 8, 10, 12, 11, 13, 14, 16, 15, 18, 19, 17]

Si on ajoute une methode batch avant le shuffle, ce sont les batchs qui sont permutes.

dataset = tf.data.Dataset.range(20)
dataset = dataset.batch(3)
dataset = dataset.shuffle(buffer_size = 2)
pp.pprint(list(dataset.as_numpy_iterator()))
[   array([0, 1, 2], dtype=int64),
    array([6, 7, 8], dtype=int64),
    array([ 9, 10, 11], dtype=int64),
    array([12, 13, 14], dtype=int64),
    array([3, 4, 5], dtype=int64),
    array([15, 16, 17], dtype=int64),
    array([18, 19], dtype=int64)]

Et la chaine complete : on permute partiellement les donnees, et on parcourt deux epoques par batchs de 3 elements.

dataset = tf.data.Dataset.range(10)
dataset = dataset.shuffle(buffer_size = 2).repeat(2).batch(3)
pp.pprint(list(dataset.as_numpy_iterator()))
[   array([0, 1, 2], dtype=int64),
    array([4, 3, 5], dtype=int64),
    array([6, 7, 9], dtype=int64),
    array([8, 1, 0], dtype=int64),
    array([3, 4, 2], dtype=int64),
    array([6, 5, 7], dtype=int64),
    array([9, 8], dtype=int64)]

Si on place le batch avant le repeat, on peut epuiser le dataset sans remplir totalement un lot avant la relecture du dataset :

dataset = tf.data.Dataset.range(10)
dataset = dataset.shuffle(buffer_size = 2).batch(3).repeat(2)
pp.pprint(list(dataset.as_numpy_iterator()))
[   array([0, 1, 3], dtype=int64),
    array([4, 2, 6], dtype=int64),
    array([5, 7, 8], dtype=int64),
    array([9], dtype=int64),
    array([0, 2, 3], dtype=int64),
    array([1, 4, 6], dtype=int64),
    array([5, 7, 9], dtype=int64),
    array([8], dtype=int64)]

4 Lecture d’un dataframe pandas

On cree une fonction df_to_dataset qui :

  • transforme un DataFrame pandas en Dataset en separant predicteurs et cible recodee en entiers grace a un mapping par dictionnaire
  • applique une permutation ou pas
  • parametre la taille des mini-batchs
  • repete x fois la lecture du Dataset, avec None la repetition est sans fin

Sur le jeu d’apprentissage on applique la permutation et la repetition, pas sur les jeux de test et validation.

def df_to_dataset(dataframe, dico, shuffle = True, buffer_size = 1000, batch_size = 32, 
nb_repet = None):
  dataframe = dataframe.copy()
  labels = dataframe.pop('income').map(dico)
  ds = tf.data.Dataset.from_tensor_slices((dict(dataframe), labels))
  if shuffle:
    ds = ds.shuffle(buffer_size = buffer_size)
  return ds.repeat(count = nb_repet).batch(batch_size)

ds = df_to_dataset(dtf_class.iloc[:5], dico = dico_income, buffer_size = 5, batch_size = 4, 
nb_repet = None)

iter_batch = iter(ds)
pd.DataFrame(next(iter_batch)[0])
   number_in_household marital_status           householder_status
0                    2     b'married'                       b'own'
1                    3     b'married'                      b'rent'
2                    4      b'single'  b'live with parents/family'
3                    5     b'married'                       b'own'
pd.DataFrame(next(iter_batch)[0])
   number_in_household marital_status           householder_status
0                    4      b'single'  b'live with parents/family'
1                    5     b'married'                       b'own'
2                    4      b'single'  b'live with parents/family'
3                    4      b'single'  b'live with parents/family'
pd.DataFrame(next(iter_batch)[0])
   number_in_household marital_status           householder_status
0                    3     b'married'                      b'rent'
1                    2     b'married'                       b'own'
2                    4      b'single'  b'live with parents/family'
3                    4      b'single'  b'live with parents/family'
pd.DataFrame(next(iter_batch)[0])
   number_in_household marital_status householder_status
0                    2     b'married'             b'own'
1                    3     b'married'            b'rent'
2                    5     b'married'             b'own'
3                    3     b'married'            b'rent'
pd.DataFrame(next(iter_batch)[0])
   number_in_household marital_status           householder_status
0                    2     b'married'                       b'own'
1                    5     b'married'                       b'own'
2                    4      b'single'  b'live with parents/family'
3                    4      b'single'  b'live with parents/family'

5 Pretraitements

Les variables d’entree d’un reseau de neurones doivent etre mises a un format numerique. On donne ci-dessous quelques exemples de transformations possibles, ces transformations sont des fonctions qu’on applique a des jeux de donnees. Pour les visualiser on incorpore ces transformations dans la couche d’entree DenseFeatures d’un reseau Keras, ce qui produit un tenseur TensorFlow.

5.0.1 Donnees numeriques

Ici on doit juste signaler cette colonne comme etant deja numerique.

ds = df_to_dataset(dtf_class, dico = dico_income, buffer_size = 1000, batch_size = 10, nb_repet = 1)

number_in_household = feature_column.numeric_column("number_in_household")

feature_layer = layers.DenseFeatures(number_in_household)
batch_exemple = next(iter(ds))[0]
pd.DataFrame(batch_exemple)[["number_in_household"]]
   number_in_household
0                    4
1                    4
2                    3
3                    1
4                    3
5                    2
6                    1
7                    2
8                    4
9                    2
print(feature_layer(batch_exemple).numpy())
[[4.]
 [4.]
 [3.]
 [1.]
 [3.]
 [2.]
 [1.]
 [2.]
 [4.]
 [2.]]

5.0.2 Donnees numeriques discretisees

La variable est discretisee avec les intervalles python [-np.inf,] + boundaries + python [np.inf] de la forme [a,b) et on produit une indicatrice par intervalle.

number_in_household = feature_column.numeric_column("number_in_household")
# intervalles {1,2}, {3,4}, {5,6}, {7,8,9}
number_in_household = tf.feature_column.bucketized_column(number_in_household, boundaries=[3, 5, 7])

feature_layer = layers.DenseFeatures(number_in_household)
batch_exemple = next(iter(ds))[0]
pd.DataFrame(batch_exemple)[["number_in_household"]]
   number_in_household
0                    2
1                    2
2                    3
3                    2
4                    2
5                    2
6                    1
7                    1
8                    4
9                    1
print(feature_layer(batch_exemple).numpy())
[[1. 0. 0. 0.]
 [1. 0. 0. 0.]
 [0. 1. 0. 0.]
 [1. 0. 0. 0.]
 [1. 0. 0. 0.]
 [1. 0. 0. 0.]
 [1. 0. 0. 0.]
 [1. 0. 0. 0.]
 [0. 1. 0. 0.]
 [1. 0. 0. 0.]]

5.0.3 Donnees categorielles

5.0.3.1 Avec modalites explicites

On fournit la liste des modalites et on produit une indicatrice par modalite avec indicator_column.

# intervalles {1,2}, {3,4}, {5,6}, {7,8,9}
marital_status = tf.feature_column.categorical_column_with_vocabulary_list(
  'marital_status', 
  ['single', 'married', 'divorced', 'cohabitation'', widowed'])

marital_one_hot = tf.feature_column.indicator_column(marital_status)

feature_layer = layers.DenseFeatures(marital_one_hot)
batch_exemple = next(iter(ds))[0]
pd.DataFrame(batch_exemple)[['marital_status']]
    marital_status
0  b'cohabitation'
1       b'married'
2        b'single'
3       b'married'
4       b'married'
5  b'cohabitation'
6       b'married'
7        b'single'
8       b'married'
9       b'married'
print(feature_layer(batch_exemple).numpy())
[[0. 0. 0. 0.]
 [0. 1. 0. 0.]
 [1. 0. 0. 0.]
 [0. 1. 0. 0.]
 [0. 1. 0. 0.]
 [0. 0. 0. 0.]
 [0. 1. 0. 0.]
 [1. 0. 0. 0.]
 [0. 1. 0. 0.]
 [0. 1. 0. 0.]]

5.0.3.2 Hashed buckets

A utiliser pour une variable categorielle avec des milliers de modalites. Les modalites sont mises dans un plus petit nombre de groupes (“buckets”) sans avoir besoin de fournir un vocabulaire, et on produit une indicatrice par bucket. On pourrait trouver genant que des modalites differentes se retrouvent dans le meme bucket, mais les valeurs et interactions des autres variables peuvent permettre au modele de les distinguer.

A noter : le nombre de buckets est un hyperparametre supplementaire a optimiser.

marital_status = tf.feature_column.categorical_column_with_hash_bucket('marital_status', 
hash_bucket_size = 3)

marital_hashed = tf.feature_column.indicator_column(marital_status)

feature_layer = layers.DenseFeatures(marital_hashed)
batch_exemple = next(iter(ds))[0]
pd.DataFrame(batch_exemple)[['marital_status']]
    marital_status
0       b'widowed'
1        b'single'
2        b'single'
3       b'married'
4  b'cohabitation'
5       b'married'
6       b'married'
7       b'married'
8       b'widowed'
9        b'single'
print(feature_layer(batch_exemple).numpy())
[[1. 0. 0.]
 [0. 1. 0.]
 [0. 1. 0.]
 [0. 1. 0.]
 [0. 0. 1.]
 [0. 1. 0.]
 [0. 1. 0.]
 [0. 1. 0.]
 [1. 0. 0.]
 [0. 1. 0.]]

5.0.3.3 Embeddings

Un embedding est une maniere de reduire la dimension, l’analogue en statistiques classiques serait de retenir quelques composantes principales d’une Analyse en Composantes Multiples.

A utiliser la aussi pour une variable categorielle avec beaucoup de de modalites, qui cette fois est transformee en une representation numerique dense a quelques colonnes. L’entree de l’embedding doit etre une variable categorielle deja mis au format TensorFlow.

La dimension de l’embedding est un autre hyperparametre a optimiser.

marital_status = tf.feature_column.categorical_column_with_vocabulary_list(
  'marital_status', 
  ['single', 'married', 'divorced', 'cohabitation'', widowed'])
  
marital_embedding = tf.feature_column.embedding_column(marital_status, dimension = 2)

feature_layer = layers.DenseFeatures(marital_embedding)
batch_exemple = next(iter(ds))[0]
pd.DataFrame(batch_exemple)[['marital_status']]
    marital_status
0        b'single'
1        b'single'
2        b'single'
3       b'married'
4        b'single'
5       b'married'
6        b'single'
7  b'cohabitation'
8       b'married'
9        b'single'
print(feature_layer(batch_exemple).numpy())
[[-0.2315056   0.7147339 ]
 [-0.2315056   0.7147339 ]
 [-0.2315056   0.7147339 ]
 [ 0.40787768  0.10088602]
 [-0.2315056   0.7147339 ]
 [ 0.40787768  0.10088602]
 [-0.2315056   0.7147339 ]
 [ 0.          0.        ]
 [ 0.40787768  0.10088602]
 [-0.2315056   0.7147339 ]]

5.0.3.4 Interactions

Il s’agit de l’interaction usuelle entre variables qualitatives (produit cartesien des champs) qui est ensuite repartie entre diffrents buckets.

Point technique : le parametre hash_key a ete renseigne ci-dessous uniquement pour corriger un bug …

marital_status = tf.feature_column.categorical_column_with_vocabulary_list(
  'marital_status', 
  ['single', 'married', 'divorced', 'cohabitation'', widowed'])

householder_status = tf.feature_column.categorical_column_with_vocabulary_list(
  'householder_status', 
  ['own', 'rent', 'live with parents/family'])

number_in_household = feature_column.numeric_column("number_in_household")
number_in_household = tf.feature_column.bucketized_column(number_in_household, 
boundaries = [3., 5., 7.])

interactions = tf.feature_column.crossed_column([marital_status, householder_status, 
number_in_household], 
hash_bucket_size = 7, hash_key = 8)
interactions = tf.feature_column.indicator_column(interactions)

feature_layer = layers.DenseFeatures(interactions, dtype='int64')
batch_exemple = next(iter(ds))[0]
pd.DataFrame(batch_exemple)[['marital_status', 'householder_status', 'number_in_household']]
    marital_status           householder_status  number_in_household
0      b'divorced'                       b'own'                    2
1       b'married'                       b'own'                    5
2  b'cohabitation'                      b'rent'                    3
3       b'married'                      b'rent'                    2
4        b'single'  b'live with parents/family'                    3
5        b'single'                      b'rent'                    1
6       b'married'                      b'rent'                    2
7        b'single'                      b'rent'                    1
8        b'single'                      b'rent'                    5
9       b'married'                       b'own'                    2
print(feature_layer(batch_exemple).numpy())
[[0. 0. 0. 0. 0. 1. 0.]
 [0. 0. 0. 0. 1. 0. 0.]
 [0. 0. 0. 0. 0. 1. 0.]
 [1. 0. 0. 0. 0. 0. 0.]
 [1. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 1.]
 [1. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 1.]
 [0. 0. 0. 0. 1. 0. 0.]
 [1. 0. 0. 0. 0. 0. 0.]]

retour au debut du document