In [0]:
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

Introduction aux données rares et aux représentations vectorielles continues

Objectifs d'apprentissage :

  • Convertir des données de chaîne de critique de film en un vecteur de caractéristique clairsemée
  • Mettre en œuvre un modèle linéaire d'analyse des sentiments à l'aide d'un vecteur de caractéristique clairsemée
  • Mettre en œuvre un modèle DNN d'analyse des sentiments à l'aide d'une représentation vectorielle continue qui projette des données dans deux dimensions
  • Visualiser des représentations vectorielles continues pour déterminer ce que le modèle a appris sur les relations entre les mots

Dans cet exercice, nous allons examiner des données rares et travailler avec les représentations vectorielles continues à l'aide de données textuelles issues de critiques de films (provenant de l'ensemble de données IMDB ACL 2011). Ces données ont déjà été traitées au format tf.Example.

Configuration

Nous allons importer nos dépendances, et télécharger les données de test et d'apprentissage. tf.keras comprend un outil de mise en cache et de téléchargement de fichiers que nous pouvons utiliser pour récupérer les ensembles de données.


In [0]:
from __future__ import print_function

import collections
import io
import math

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import tensorflow as tf
from IPython import display
from sklearn import metrics

tf.logging.set_verbosity(tf.logging.ERROR)
train_url = 'https://download.mlcc.google.com/mledu-datasets/sparse-data-embedding/train.tfrecord'
train_path = tf.keras.utils.get_file(train_url.split('/')[-1], train_url)
test_url = 'https://download.mlcc.google.com/mledu-datasets/sparse-data-embedding/test.tfrecord'
test_path = tf.keras.utils.get_file(test_url.split('/')[-1], test_url)

Construction d'un modèle d'analyse des sentiments

Vous allez effectuer, sur ces données, l'entraînement d'un modèle d'analyse des sentiments qui prédit si une critique est globalement favorable (libellé 1) ou défavorable (libellé 0).

Pour ce faire, vous allez transformer la valeur de chaîne terms en vecteurs de caractéristiques en utilisant un vocabulaire, c'est-à-dire une liste de termes que vous vous attendez à trouver dans nos données. Pour les besoins de cet exercice, nous avons créé un vocabulaire réduit qui porte sur un ensemble limité de termes. Il s'est avéré que la plupart de ces termes indiquaient clairement un fort sentiment favorable ou défavorable. D'autres cependant ont simplement été ajoutés, car ils étaient intéressants.

Chaque terme du vocabulaire est associé à une coordonnée du vecteur de caractéristiques. Pour convertir la valeur de chaîne terms d'un exemple dans ce format de vecteur, vous allez coder comme suit : chaque coordonnée reçoit la valeur 0 si le terme de vocabulaire ne figure pas dans l'exemple de chaîne, et la valeur 1 dans le cas contraire. Les termes qui ne figurent pas dans le vocabulaire sont rejetés.

REMARQUE : Vous pourriez bien sûr utiliser un vocabulaire plus vaste ; il existe de nombreux outils spécialisés pour cela. Au lieu de simplement ignorer les termes qui ne figurent pas dans le vocabulaire, vous pourriez également introduire un petit nombre de segments OOV pour hacher les termes hors vocabulaire. Vous pourriez aussi utiliser une méthode axée sur le hachage de caractéristiques qui hache chaque terme, au lieu de créer un vocabulaire explicite. Cela fonctionne très bien dans la pratique, mais au détriment de l'interprétabilité, qui s'avère utile pour cet exercice. Pour consulter les outils adaptés à ce type d'opération, reportez-vous au module tf.feature_column.

Construction du pipeline d'entrée

Commencez par créer le pipeline d'entrée pour importer les données dans un modèle TensorFlow. Vous pouvez utiliser la fonction suivante pour analyser les données de test et d'apprentissage (qui se trouvent au format TFRecord), et renvoyer un dictionnaire des caractéristiques et les libellés correspondants.


In [0]:
def _parse_function(record):
  """Extracts features and labels.
  
  Args:
    record: File path to a TFRecord file    
  Returns:
    A `tuple` `(labels, features)`:
      features: A dict of tensors representing the features
      labels: A tensor with the corresponding labels.
  """
  features = {
    "terms": tf.VarLenFeature(dtype=tf.string), # terms are strings of varying lengths
    "labels": tf.FixedLenFeature(shape=[1], dtype=tf.float32) # labels are 0 or 1
  }
  
  parsed_features = tf.parse_single_example(record, features)
  
  terms = parsed_features['terms'].values
  labels = parsed_features['labels']

  return  {'terms':terms}, labels

Pour vérifier que cela fonctionne comme prévu, construisez un ensemble TFRecordDataset pour les données d'apprentissage, et associez ces dernières aux caractéristiques et aux libellés à l'aide de la fonction ci-dessus.


In [0]:
# Create the Dataset object.
ds = tf.data.TFRecordDataset(train_path)
# Map features and labels with the parse function.
ds = ds.map(_parse_function)

ds

Exécutez la cellule suivante pour récupérer le premier exemple de l'ensemble de données d'apprentissage.


In [0]:
n = ds.make_one_shot_iterator().get_next()
sess = tf.Session()
sess.run(n)

Vous allez maintenant construire une fonction d'entrée formelle que vous pouvez transmettre à la méthode train() d'un objet TensorFlow Estimator.


In [0]:
# Create an input_fn that parses the tf.Examples from the given files,
# and split them into features and targets.
def _input_fn(input_filenames, num_epochs=None, shuffle=True):
  
  # Same code as above; create a dataset and map features and labels.
  ds = tf.data.TFRecordDataset(input_filenames)
  ds = ds.map(_parse_function)

  if shuffle:
    ds = ds.shuffle(10000)

  # Our feature data is variable-length, so we pad and batch
  # each field of the dataset structure to whatever size is necessary.     
  ds = ds.padded_batch(25, ds.output_shapes)
  
  ds = ds.repeat(num_epochs)

  
  # Return the next batch of data.
  features, labels = ds.make_one_shot_iterator().get_next()
  return features, labels

Tâche 1 : Utiliser un modèle linéaire avec des entrées clairsemées et un vocabulaire explicite

Pour le premier modèle, vous allez construire un modèle LinearClassifier à l'aide de 50 termes informatifs ; commencez toujours simplement !

Le code suivant construit la colonne de caractéristiques pour ces termes. La fonction categorical_column_with_vocabulary_list crée une colonne de caractéristiques avec la mise en correspondance chaîne/vecteur de caractéristiques.


In [0]:
# 50 informative terms that compose our model vocabulary. 
informative_terms = ("bad", "great", "best", "worst", "fun", "beautiful",
                     "excellent", "poor", "boring", "awful", "terrible",
                     "definitely", "perfect", "liked", "worse", "waste",
                     "entertaining", "loved", "unfortunately", "amazing",
                     "enjoyed", "favorite", "horrible", "brilliant", "highly",
                     "simple", "annoying", "today", "hilarious", "enjoyable",
                     "dull", "fantastic", "poorly", "fails", "disappointing",
                     "disappointment", "not", "him", "her", "good", "time",
                     "?", ".", "!", "movie", "film", "action", "comedy",
                     "drama", "family")

terms_feature_column = tf.feature_column.categorical_column_with_vocabulary_list(key="terms", vocabulary_list=informative_terms)

Vous allez ensuite construire le modèle LinearClassifier, l'entraîner sur l'ensemble d'apprentissage et l'évaluer sur l'ensemble d'évaluation. Après avoir parcouru le code, exécutez-le et voyons comment vous vous en sortez.


In [0]:
my_optimizer = tf.train.AdagradOptimizer(learning_rate=0.1)
my_optimizer = tf.contrib.estimator.clip_gradients_by_norm(my_optimizer, 5.0)

feature_columns = [ terms_feature_column ]


classifier = tf.estimator.LinearClassifier(
  feature_columns=feature_columns,
  optimizer=my_optimizer,
)

classifier.train(
  input_fn=lambda: _input_fn([train_path]),
  steps=1000)

evaluation_metrics = classifier.evaluate(
  input_fn=lambda: _input_fn([train_path]),
  steps=1000)
print("Training set metrics:")
for m in evaluation_metrics:
  print(m, evaluation_metrics[m])
print("---")

evaluation_metrics = classifier.evaluate(
  input_fn=lambda: _input_fn([test_path]),
  steps=1000)

print("Test set metrics:")
for m in evaluation_metrics:
  print(m, evaluation_metrics[m])
print("---")

Tâche 2 : Utiliser un modèle de réseau neuronal profond (DNN)

Le modèle ci-dessus est de type linéaire. Il fonctionne relativement bien. Cependant, peut-on obtenir de meilleurs résultats avec un modèle DNN ?

Remplaçons le modèle LinearClassifier par le modèle DNNClassifier. Exécutez la cellule suivante et voyons comment vous vous en sortez.


In [0]:
##################### Here's what we changed ##################################
classifier = tf.estimator.DNNClassifier(                                      #
  feature_columns=[tf.feature_column.indicator_column(terms_feature_column)], #
  hidden_units=[20,20],                                                       #
  optimizer=my_optimizer,                                                     #
)                                                                             #
###############################################################################

try:
  classifier.train(
    input_fn=lambda: _input_fn([train_path]),
    steps=1000)

  evaluation_metrics = classifier.evaluate(
    input_fn=lambda: _input_fn([train_path]),
    steps=1)
  print("Training set metrics:")
  for m in evaluation_metrics:
    print(m, evaluation_metrics[m])
  print("---")

  evaluation_metrics = classifier.evaluate(
    input_fn=lambda: _input_fn([test_path]),
    steps=1)

  print("Test set metrics:")
  for m in evaluation_metrics:
    print(m, evaluation_metrics[m])
  print("---")
except ValueError as err:
  print(err)

Tâche 3 : Utiliser une représentation vectorielle continue avec un modèle DNN

Dans le cadre de cette tâche, vous allez mettre en œuvre un modèle DNN à l'aide d'une colonne de représentations vectorielles continues. Une colonne de ce type accepte des données rares en guise d'entrée et renvoie un vecteur dense de plus faible dimension en sortie.

REMARQUE : En règle générale, une colonne de représentations vectorielles continues constitue l'option la plus efficace du point de vue de l'utilisation des ressources pour entraîner un modèle sur des données rares. Dans une section facultative à la fin de cet exercice, nous examinerons plus en détail les différences de mise en œuvre entre l'utilisation des options embedding_column et indicator_column, ainsi que les avantages et inconvénients de ces deux options.

Effectuez ce qui suit dans le code ci-dessous :

  • Définissez les colonnes de caractéristiques pour le modèle à l'aide d'une option embedding_column qui projette les données dans deux dimensions (voir les documents TF pour en savoir plus sur la signature de fonction pour embedding_column).
  • Définissez un modèle DNNClassifier avec les spécifications suivantes :
    • Deux couches cachées de 20 unités chacune
    • Une optimisation AdaGrad avec un taux d'apprentissage de 0,1
    • Une valeur gradient_clip_norm de 5,0

REMARQUE : Dans la pratique, vous pourriez projeter des données dans un nombre de dimensions supérieur à 2 ; 50 ou 100, par exemple. Cependant, pour l'instant, se limiter à deux dimensions permet une visualisation aisée.

Astuce


In [0]:
# Here's a example code snippet you might use to define the feature columns:

terms_embedding_column = tf.feature_column.embedding_column(terms_feature_column, dimension=2)
feature_columns = [ terms_embedding_column ]

Exécutez le code ci-dessous


In [0]:
########################## YOUR CODE HERE ######################################
terms_embedding_column = # Define the embedding column
feature_columns = # Define the feature columns

classifier = # Define the DNNClassifier
################################################################################

classifier.train(
  input_fn=lambda: _input_fn([train_path]),
  steps=1000)

evaluation_metrics = classifier.evaluate(
  input_fn=lambda: _input_fn([train_path]),
  steps=1000)
print("Training set metrics:")
for m in evaluation_metrics:
  print(m, evaluation_metrics[m])
print("---")

evaluation_metrics = classifier.evaluate(
  input_fn=lambda: _input_fn([test_path]),
  steps=1000)

print("Test set metrics:")
for m in evaluation_metrics:
  print(m, evaluation_metrics[m])
print("---")

Solution

Cliquez ci-dessous pour afficher la solution.


In [0]:
########################## SOLUTION CODE ########################################
terms_embedding_column = tf.feature_column.embedding_column(terms_feature_column, dimension=2)
feature_columns = [ terms_embedding_column ]

my_optimizer = tf.train.AdagradOptimizer(learning_rate=0.1)
my_optimizer = tf.contrib.estimator.clip_gradients_by_norm(my_optimizer, 5.0)

classifier = tf.estimator.DNNClassifier(
  feature_columns=feature_columns,
  hidden_units=[20,20],
  optimizer=my_optimizer
)
#################################################################################

classifier.train(
  input_fn=lambda: _input_fn([train_path]),
  steps=1000)

evaluation_metrics = classifier.evaluate(
  input_fn=lambda: _input_fn([train_path]),
  steps=1000)
print("Training set metrics:")
for m in evaluation_metrics:
  print(m, evaluation_metrics[m])
print("---")

evaluation_metrics = classifier.evaluate(
  input_fn=lambda: _input_fn([test_path]),
  steps=1000)

print("Test set metrics:")
for m in evaluation_metrics:
  print(m, evaluation_metrics[m])
print("---")

Tâche 4 : Se convaincre qu'il existe réellement une représentation vectorielle continue

L'option embedding_column est utilisée dans le modèle ci-dessus et cela semble fonctionner. Cependant, cela ne nous donne pas beaucoup d'informations sur ce qui se passe en interne. Dans ce cas, comment vérifier que le modèle utilise effectivement une représentation vectorielle continue en interne ?

Pour commencer, examinez les Tensors qui se trouvent dans le modèle :


In [0]:
classifier.get_variable_names()

Comme vous pouvez le voir, le modèle contient une couche de représentations vectorielles continues : 'dnn/input_from_feature_columns/input_layer/terms_embedding/...'. (Ce qui est intéressant ici, c'est que cette couche peut être apprise avec le reste du modèle, comme c'est le cas pour n'importe quelle couche cachée.)

La forme de la couche de représentations vectorielles continues est-elle correcte ? Pour le savoir, exécutez le code suivant.

REMARQUE : Dans le cas présent, souvenez-vous que la représentation vectorielle continue est une matrice qui nous permet de projeter un vecteur allant de 50 dimensions jusqu'à 2 dimensions.


In [0]:
classifier.get_variable_value('dnn/input_from_feature_columns/input_layer/terms_embedding/embedding_weights').shape

Consacrez du temps à la vérification manuelle des différentes couches et formes pour vous assurer que tout est correctement connecté.

Tâche 5 : Examiner la représentation vectorielle continue

Nous allons à présent nous pencher sur l'espace de représentation vectorielle proprement dit et voir où se classent les termes. Pour cela, procédez comme suit :

  1. Exécutez le code suivant pour voir la représentation vectorielle continue dont vous avez effectué l'apprentissage à la Tâche 3. Est-ce que tout est conforme à vos attentes ?

  2. Entraînez à nouveau le modèle en exécutant une nouvelle fois le code de la Tâche 3, puis exécutez à nouveau la visualisation des représentations vectorielles continues ci-dessous. Quels sont les changements ? Quels sont les éléments qui restent identiques ?

  3. Pour terminer, entraînez à nouveau le modèle en utilisant seulement 10 mesures (ce qui donnera lieu à un très mauvais modèle). Exécutez à nouveau la représentation vectorielle continue ci-dessous. Que voyez-vous maintenant, et pourquoi ?


In [0]:
import numpy as np
import matplotlib.pyplot as plt

embedding_matrix = classifier.get_variable_value('dnn/input_from_feature_columns/input_layer/terms_embedding/embedding_weights')

for term_index in range(len(informative_terms)):
  # Create a one-hot encoding for our term.  It has 0s everywhere, except for
  # a single 1 in the coordinate that corresponds to that term.
  term_vector = np.zeros(len(informative_terms))
  term_vector[term_index] = 1
  # We'll now project that one-hot vector into the embedding space.
  embedding_xy = np.matmul(term_vector, embedding_matrix)
  plt.text(embedding_xy[0],
           embedding_xy[1],
           informative_terms[term_index])

# Do a little setup to make sure the plot displays nicely.
plt.rcParams["figure.figsize"] = (15, 15)
plt.xlim(1.2 * embedding_matrix.min(), 1.2 * embedding_matrix.max())
plt.ylim(1.2 * embedding_matrix.min(), 1.2 * embedding_matrix.max())
plt.show()

Tâche 6 : Essayer d'améliorer les performances du modèle

Voyez s'il est possible d'affiner le modèle afin d'en améliorer les performances. Voici quelques pistes :

  • Modifier les hyperparamètres ou utiliser un autre optimiseur comme Adam (ces stratégies vous permettront de gagner, tout au plus, un ou deux points de pourcentage de précision).
  • Ajouter des termes aux informative_terms. Vous trouverez, à l'adresse suivante, un fichier de vocabulaire complet contenant 30 716 termes pour cet ensemble de données : https://download.mlcc.google.com/mledu-datasets/sparse-data-embedding/terms.txt. Vous pouvez choisir des termes supplémentaires dans ce fichier ou l'utiliser intégralement via la colonne de caractéristiques categorical_column_with_vocabulary_file.

In [0]:
# Download the vocabulary file.
terms_url = 'https://download.mlcc.google.com/mledu-datasets/sparse-data-embedding/terms.txt'
terms_path = tf.keras.utils.get_file(terms_url.split('/')[-1], terms_url)

In [0]:
# Create a feature column from "terms", using a full vocabulary file.
informative_terms = None
with io.open(terms_path, 'r', encoding='utf8') as f:
  # Convert it to a set first to remove duplicates.
  informative_terms = list(set(f.read().split()))
  
terms_feature_column = tf.feature_column.categorical_column_with_vocabulary_list(key="terms", 
                                                                                 vocabulary_list=informative_terms)

terms_embedding_column = tf.feature_column.embedding_column(terms_feature_column, dimension=2)
feature_columns = [ terms_embedding_column ]

my_optimizer = tf.train.AdagradOptimizer(learning_rate=0.1)
my_optimizer = tf.contrib.estimator.clip_gradients_by_norm(my_optimizer, 5.0)

classifier = tf.estimator.DNNClassifier(
  feature_columns=feature_columns,
  hidden_units=[10,10],
  optimizer=my_optimizer
)

classifier.train(
  input_fn=lambda: _input_fn([train_path]),
  steps=1000)

evaluation_metrics = classifier.evaluate(
  input_fn=lambda: _input_fn([train_path]),
  steps=1000)
print("Training set metrics:")
for m in evaluation_metrics:
  print(m, evaluation_metrics[m])
print("---")

evaluation_metrics = classifier.evaluate(
  input_fn=lambda: _input_fn([test_path]),
  steps=1000)

print("Test set metrics:")
for m in evaluation_metrics:
  print(m, evaluation_metrics[m])
print("---")

Le mot de la fin

La solution DNN avec représentation vectorielle continue que nous avons développée est peut-être meilleure que notre modèle linéaire d'origine. Cependant, ce dernier offrait également de bons résultats et son apprentissage s'avérait bien plus rapide. Cette vitesse d'apprentissage est due au fait que les modèles linéaires ne comptent pas autant de paramètres à mettre à jour, ni autant de couches à traverser pour la rétropropagation.

Pour certaines applications, la vitesse des modèles linéaires peut changer la donne ou, d'un point de vue qualitatif, ces modèles peuvent s'avérer bien suffisants. Dans d'autres cas, la complexité accrue du modèle et la capacité fournie par les solutions DNN peuvent être plus importantes. Lorsque vous définissez l'architecture de votre modèle, étudiez le problème de manière suffisamment approfondie pour déterminer l'espace dans lequel vous vous trouvez.

Discussion facultative : Avantages et inconvénients de l'option embedding_column par rapport à indicator_column

D'un point de vue conceptuel, un adaptateur est nécessaire pour utiliser une colonne de données rares lors de l'apprentissage d'un modèle LinearClassifier ou DNNClassifier. TF propose deux options : embedding_column et indicator_column.

Lors de l'apprentissage d'un modèle LinearClassifier (comme dans la Tâche 1), l'option embedding_column est utilisée. Comme nous l'avons vu dans la Tâche 2, lors de l'apprentissage d'un modèle DNNClassifier, vous devez choisir explicitement embedding_column ou indicator_column. Cette section décrit la distinction entre ces deux options, ainsi que les avantages et inconvénients de l'une par rapport à l'autre, à l'aide d'un exemple simple.

Prenons comme exemple des données rares contenant les valeurs "great", "beautiful" et "excellent". Étant donné que la taille du vocabulaire utilisé ici est de $V = 50$, chaque unité (neurone) de la première couche aura 50 pondérations. Pour représenter le nombre de termes d'une entrée clairsemée, nous allons utiliser $s$. Pour cet exemple de données rares, nous avons donc $s = 3$. Pour une couche d'entrée avec $V$ valeurs possibles, une couche cachée avec $d$ unités doit effectuer la multiplication d'un vecteur par une matrice : $(1 \times V) * (V \times d)$. Le coût de calcul de cette opération est de $O(V * d)$. Notez que ce coût est proportionnel au nombre de pondérations dans cette couche cachée et indépendant de $s$.

Si les entrées sont encodées au format one-hot (un vecteur booléen de longueur $V$ avec la valeur 1 pour les termes présents et la valeur 0 pour le reste) à l'aide de l'option indicator_column, cela signifie qu'une multiplication est nécessaire et qu'il faut ajouter beaucoup de zéros.

Lorsque l'on obtient exactement les mêmes résultats en utilisant une option embedding_column de taille $d$, on recherche et on ajoute simplement les représentations vectorielles continues correspondant aux trois caractéristiques présentes dans notre exemple d'entrée de "great", "beautiful", "excellent": $(1 \times d) + (1 \times d) + (1 \times d)$. Étant donné que les pondérations relatives aux caractéristiques qui sont absentes sont multipliées par 0 dans la multiplication d'un vecteur par une matrice, elles n'entrent pas en ligne de compte dans le résultat. Les pondérations relatives aux caractéristiques présentes sont, elles, multipliées par 1. Dès lors, l'ajout des pondérations obtenues par le biais de la recherche des représentations vectorielles continues donnera le même résultat que la multiplication d'un vecteur par une matrice.

Lorsque vous utilisez des représentations vectorielles continues, calculer la recherche de ces représentations correspond à $O(s * d)$, ce qui, sur le plan de l'utilisation des ressources, s'avère beaucoup plus efficace que le coût $O(V * d)$ de l'option indicator_column dans les données rares, pour laquelle $s$ est sensiblement plus petit que $V$. (Pour rappel, ces représentations vectorielles continues sont en cours d'apprentissage. Dans n'importe quelle itération d'apprentissage, ce sont les pondérations en cours qui sont recherchées.)

Comme nous l'avons vu dans la Tâche 3, lorsque l'on utilise une option embedding_column pour effectuer l'apprentissage du modèle DNNClassifier, notre modèle apprend une représentation bidimensionnelle des caractéristiques, dans laquelle le produit scalaire définit une statistique de similitude adaptée à la tâche souhaitée. Dans cet exemple, les termes utilisés de la même façon dans le cadre des critiques de films ("great" et "excellent", par exemple) seront plus proches les uns des autres dans l'espace de représentation vectorielle (grand produit scalaire), tandis que les termes dissemblables ("great" et "bad", par exemple) seront plus éloignés (petit produit scalaire).