Classer les données structurées avec des colonnes de caractéristiques

Voir sur TensorFlow.org Exécuter dans Google Colab Voir la source sur GitHub Télécharger le cahier

Ce didacticiel montre comment classer des données structurées (par exemple, des données tabulaires dans un CSV). Nous utiliserons Keras pour définir le modèle et tf.feature_column comme pont pour mapper des colonnes dans un CSV aux fonctionnalités utilisées pour former le modèle. Ce tutoriel contient le code complet pour :

  • Chargez un fichier CSV à l'aide de Pandas .
  • Créez un pipeline d'entrée pour regrouper et mélanger les lignes à l'aide de tf.data .
  • Mappez des colonnes du CSV aux fonctionnalités utilisées pour entraîner le modèle à l'aide de colonnes de fonctionnalités.
  • Créez, entraînez et évaluez un modèle à l'aide de Keras.

L'ensemble de données

Nous utiliserons une version simplifiée du jeu de données PetFinder. Il y a plusieurs milliers de lignes dans le CSV. Chaque ligne décrit un animal de compagnie et chaque colonne décrit un attribut. Nous utiliserons ces informations pour prédire la vitesse à laquelle l'animal sera adopté.

Voici une description de cet ensemble de données. Notez qu'il existe des colonnes numériques et catégorielles. Il y a une colonne de texte libre que nous n'utiliserons pas dans ce tutoriel.

Colonne La description Type de fonctionnalité Type de données
Taper Type d'animal (Chien, Chat) Catégorique chaîne de caractères
Âge Âge de l'animal Numérique entier
Race1 Race principale de l'animal Catégorique chaîne de caractères
Couleur1 Couleur 1 de l'animal Catégorique chaîne de caractères
Couleur2 Couleur 2 de l'animal Catégorique chaîne de caractères
MaturitéTaille Taille à maturité Catégorique chaîne de caractères
FourrureLongueur Longueur de la fourrure Catégorique chaîne de caractères
Vacciné L'animal a été vacciné Catégorique chaîne de caractères
Stérilisé L'animal a été stérilisé Catégorique chaîne de caractères
Santé État de santé Catégorique chaîne de caractères
Frais Frais d'adoption Numérique entier
La description Rédaction du profil de cet animal de compagnie Texte chaîne de caractères
PhotoAmt Total de photos téléchargées pour cet animal de compagnie Numérique entier
Vitesse d'adoption Rapidité d'adoption Classification entier

Importer TensorFlow et d'autres bibliothèques

pip install sklearn
import numpy as np
import pandas as pd

import tensorflow as tf

from tensorflow import feature_column
from tensorflow.keras import layers
from sklearn.model_selection import train_test_split

Utiliser Pandas pour créer une dataframe

Pandas est une bibliothèque Python avec de nombreux utilitaires utiles pour charger et travailler avec des données structurées. Nous utiliserons Pandas pour télécharger l'ensemble de données à partir d'une URL et le charger dans une base de données.

import pathlib

dataset_url = 'http://storage.googleapis.com/download.tensorflow.org/data/petfinder-mini.zip'
csv_file = 'datasets/petfinder-mini/petfinder-mini.csv'

tf.keras.utils.get_file('petfinder_mini.zip', dataset_url,
                        extract=True, cache_dir='.')
dataframe = pd.read_csv(csv_file)
Downloading data from http://storage.googleapis.com/download.tensorflow.org/data/petfinder-mini.zip
1671168/1668792 [==============================] - 0s 0us/step
1679360/1668792 [==============================] - 0s 0us/step
dataframe.head()

Créer une variable cible

La tâche dans l'ensemble de données d'origine est de prédire la vitesse à laquelle un animal de compagnie sera adopté (par exemple, la première semaine, le premier mois, les trois premiers mois, etc.). Simplifions cela pour notre tutoriel. Ici, nous allons transformer cela en un problème de classification binaire et prédire simplement si l'animal a été adopté ou non.

Après avoir modifié la colonne d'étiquette, 0 indiquera que l'animal n'a pas été adopté et 1 indiquera qu'il l'a été.

# In the original dataset "4" indicates the pet was not adopted.
dataframe['target'] = np.where(dataframe['AdoptionSpeed']==4, 0, 1)

# Drop un-used columns.
dataframe = dataframe.drop(columns=['AdoptionSpeed', 'Description'])

Diviser la trame de données en train, validation et test

L'ensemble de données que nous avons téléchargé était un seul fichier CSV. Nous diviserons cela en ensembles d'entraînement, de validation et de test.

train, test = train_test_split(dataframe, test_size=0.2)
train, val = train_test_split(train, test_size=0.2)
print(len(train), 'train examples')
print(len(val), 'validation examples')
print(len(test), 'test examples')
7383 train examples
1846 validation examples
2308 test examples

Créer un pipeline d'entrée à l'aide de tf.data

Ensuite, nous allons envelopper les dataframes avec tf.data . Cela nous permettra d'utiliser les colonnes de caractéristiques comme un pont pour mapper les colonnes de la base de données Pandas aux caractéristiques utilisées pour former le modèle. Si nous travaillions avec un fichier CSV très volumineux (si volumineux qu'il ne rentre pas dans la mémoire), nous utiliserions tf.data pour le lire directement à partir du disque. Ce n'est pas couvert dans ce tutoriel.

# A utility method to create a tf.data dataset from a Pandas Dataframe
def df_to_dataset(dataframe, shuffle=True, batch_size=32):
  dataframe = dataframe.copy()
  labels = dataframe.pop('target')
  ds = tf.data.Dataset.from_tensor_slices((dict(dataframe), labels))
  if shuffle:
    ds = ds.shuffle(buffer_size=len(dataframe))
  ds = ds.batch(batch_size)
  return ds
batch_size = 5 # A small batch sized is used for demonstration purposes
train_ds = df_to_dataset(train, batch_size=batch_size)
val_ds = df_to_dataset(val, shuffle=False, batch_size=batch_size)
test_ds = df_to_dataset(test, shuffle=False, batch_size=batch_size)

Comprendre le pipeline d'entrée

Maintenant que nous avons créé le pipeline d'entrée, appelons-le pour voir le format des données qu'il renvoie. Nous avons utilisé une petite taille de lot pour garder la sortie lisible.

for feature_batch, label_batch in train_ds.take(1):
  print('Every feature:', list(feature_batch.keys()))
  print('A batch of ages:', feature_batch['Age'])
  print('A batch of targets:', label_batch )
Every feature: ['Type', 'Age', 'Breed1', 'Gender', 'Color1', 'Color2', 'MaturitySize', 'FurLength', 'Vaccinated', 'Sterilized', 'Health', 'Fee', 'PhotoAmt']
A batch of ages: tf.Tensor([ 6  2 36  2  2], shape=(5,), dtype=int64)
A batch of targets: tf.Tensor([1 1 1 1 1], shape=(5,), dtype=int64)

Nous pouvons voir que l'ensemble de données renvoie un dictionnaire de noms de colonnes (de la trame de données) qui correspondent aux valeurs de colonne des lignes de la trame de données.

Démontrer plusieurs types de colonnes de caractéristiques

TensorFlow fournit de nombreux types de colonnes de fonctionnalités. Dans cette section, nous allons créer plusieurs types de colonnes de caractéristiques et montrer comment elles transforment une colonne à partir du dataframe.

# We will use this batch to demonstrate several types of feature columns
example_batch = next(iter(train_ds))[0]
# A utility method to create a feature column
# and to transform a batch of data
def demo(feature_column):
  feature_layer = layers.DenseFeatures(feature_column)
  print(feature_layer(example_batch).numpy())

Colonnes numériques

La sortie d'une colonne de caractéristiques devient l'entrée du modèle (en utilisant la fonction de démonstration définie ci-dessus, nous pourrons voir exactement comment chaque colonne de la trame de données est transformée). Une colonne numérique est le type de colonne le plus simple. Il est utilisé pour représenter des caractéristiques à valeur réelle. Lorsque vous utilisez cette colonne, votre modèle recevra la valeur de la colonne du dataframe inchangée.

photo_count = feature_column.numeric_column('PhotoAmt')
demo(photo_count)
[[2.]
 [4.]
 [4.]
 [1.]
 [2.]]

Dans le jeu de données PetFinder, la plupart des colonnes du dataframe sont catégorielles.

Colonnes compartimentées

Souvent, vous ne souhaitez pas introduire un nombre directement dans le modèle, mais plutôt diviser sa valeur en différentes catégories en fonction de plages numériques. Considérez les données brutes qui représentent l'âge d'une personne. Au lieu de représenter l'âge sous la forme d'une colonne numérique, nous pourrions diviser l'âge en plusieurs compartiments à l'aide d'une colonne compartimentée . Notez que les valeurs ponctuelles ci-dessous décrivent la tranche d'âge à laquelle chaque ligne correspond.

age = feature_column.numeric_column('Age')
age_buckets = feature_column.bucketized_column(age, boundaries=[1, 3, 5])
demo(age_buckets)
[[0. 0. 0. 1.]
 [0. 1. 0. 0.]
 [0. 0. 0. 1.]
 [0. 0. 1. 0.]
 [0. 1. 0. 0.]]

Colonnes catégorielles

Dans cet ensemble de données, Type est représenté sous la forme d'une chaîne (par exemple, "Chien" ou "Chat"). Nous ne pouvons pas envoyer de chaînes directement à un modèle. Au lieu de cela, nous devons d'abord les mapper à des valeurs numériques. Les colonnes de vocabulaire catégoriel permettent de représenter les chaînes sous la forme d'un vecteur unique (un peu comme vous l'avez vu ci-dessus avec les tranches d'âge). Le vocabulaire peut être passé sous forme de liste en utilisant categorical_column_with_vocabulary_list , ou chargé à partir d'un fichier en utilisant categorical_column_with_vocabulary_file .

animal_type = feature_column.categorical_column_with_vocabulary_list(
      'Type', ['Cat', 'Dog'])

animal_type_one_hot = feature_column.indicator_column(animal_type)
demo(animal_type_one_hot)
[[1. 0.]
 [1. 0.]
 [1. 0.]
 [1. 0.]
 [0. 1.]]

Incorporer des colonnes

Supposons qu'au lieu d'avoir seulement quelques chaînes possibles, nous ayons des milliers (ou plus) de valeurs par catégorie. Pour un certain nombre de raisons, à mesure que le nombre de catégories augmente, il devient impossible de former un réseau de neurones à l'aide d'encodages à chaud. Nous pouvons utiliser une colonne d'intégration pour surmonter cette limitation. Au lieu de représenter les données sous la forme d'un vecteur unique de plusieurs dimensions, une colonne d'intégration représente ces données sous la forme d'un vecteur dense de dimension inférieure dans lequel chaque cellule peut contenir n'importe quel nombre, pas seulement 0 ou 1. La taille de l'intégration ( 8, dans l'exemple ci-dessous) est un paramètre qui doit être réglé.

# Notice the input to the embedding column is the categorical column
# we previously created
breed1 = feature_column.categorical_column_with_vocabulary_list(
      'Breed1', dataframe.Breed1.unique())
breed1_embedding = feature_column.embedding_column(breed1, dimension=8)
demo(breed1_embedding)
[[-0.22380038 -0.09379731  0.21349265  0.33451992 -0.49730566  0.05174963
   0.2668497   0.27391028]
 [-0.5484653  -0.03492585  0.05648395 -0.09792244  0.02530896 -0.15477926
  -0.10695003 -0.45474145]
 [-0.22380038 -0.09379731  0.21349265  0.33451992 -0.49730566  0.05174963
   0.2668497   0.27391028]
 [ 0.10050306  0.43513173  0.375823    0.5652766   0.40925583 -0.03928828
   0.4901914   0.20637617]
 [-0.2319875  -0.21874283  0.12272807  0.33345345 -0.4563055   0.21609035
  -0.2410521   0.4736915 ]]

Colonnes de caractéristiques hachées

Une autre façon de représenter une colonne catégorielle avec un grand nombre de valeurs consiste à utiliser un categorical_column_with_hash_bucket . Cette colonne de fonctionnalités calcule une valeur de hachage de l'entrée, puis sélectionne l'un des hash_bucket_size pour encoder une chaîne. Lorsque vous utilisez cette colonne, vous n'avez pas besoin de fournir le vocabulaire et vous pouvez choisir de réduire considérablement le nombre de hash_buckets au nombre de catégories réelles pour économiser de l'espace.

breed1_hashed = feature_column.categorical_column_with_hash_bucket(
      'Breed1', hash_bucket_size=10)
demo(feature_column.indicator_column(breed1_hashed))
[[0. 0. 0. 0. 0. 0. 0. 0. 0. 1.]
 [0. 0. 0. 0. 0. 0. 0. 1. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 0. 0. 1.]
 [0. 0. 0. 0. 0. 0. 1. 0. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 1. 0. 0.]]

Colonnes de caractéristiques croisées

La combinaison de caractéristiques en une seule caractéristique, mieux connue sous le nom de croix de caractéristiques , permet à un modèle d'apprendre des pondérations distinctes pour chaque combinaison de caractéristiques. Ici, nous allons créer une nouvelle fonctionnalité qui est le croisement de l'âge et du type. Notez que crossed_column ne construit pas la table complète de toutes les combinaisons possibles (qui peuvent être très grandes). Au lieu de cela, il est soutenu par un hashed_column , vous pouvez donc choisir la taille de la table.

crossed_feature = feature_column.crossed_column([age_buckets, animal_type], hash_bucket_size=10)
demo(feature_column.indicator_column(crossed_feature))
[[1. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 0. 0. 1. 0. 0. 0. 0. 0. 0.]
 [1. 0. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 1. 0. 0. 0. 0. 0. 0. 0. 0.]
 [0. 1. 0. 0. 0. 0. 0. 0. 0. 0.]]

Choisissez les colonnes à utiliser

Nous avons vu comment utiliser plusieurs types de colonnes de caractéristiques. Nous allons maintenant les utiliser pour former un modèle. Le but de ce didacticiel est de vous montrer le code complet (par exemple, la mécanique) nécessaire pour travailler avec des colonnes de fonctionnalités. Nous avons sélectionné quelques colonnes pour former notre modèle ci-dessous arbitrairement.

feature_columns = []

# numeric cols
for header in ['PhotoAmt', 'Fee', 'Age']:
  feature_columns.append(feature_column.numeric_column(header))
# bucketized cols
age = feature_column.numeric_column('Age')
age_buckets = feature_column.bucketized_column(age, boundaries=[1, 2, 3, 4, 5])
feature_columns.append(age_buckets)
# indicator_columns
indicator_column_names = ['Type', 'Color1', 'Color2', 'Gender', 'MaturitySize',
                          'FurLength', 'Vaccinated', 'Sterilized', 'Health']
for col_name in indicator_column_names:
  categorical_column = feature_column.categorical_column_with_vocabulary_list(
      col_name, dataframe[col_name].unique())
  indicator_column = feature_column.indicator_column(categorical_column)
  feature_columns.append(indicator_column)
# embedding columns
breed1 = feature_column.categorical_column_with_vocabulary_list(
      'Breed1', dataframe.Breed1.unique())
breed1_embedding = feature_column.embedding_column(breed1, dimension=8)
feature_columns.append(breed1_embedding)
# crossed columns
age_type_feature = feature_column.crossed_column([age_buckets, animal_type], hash_bucket_size=100)
feature_columns.append(feature_column.indicator_column(age_type_feature))

Créer une couche d'entités

Maintenant que nous avons défini nos colonnes de caractéristiques, nous allons utiliser une couche DenseFeatures pour les entrer dans notre modèle Keras.

feature_layer = tf.keras.layers.DenseFeatures(feature_columns)

Auparavant, nous avons utilisé une petite taille de lot pour démontrer le fonctionnement des colonnes de caractéristiques. Nous créons un nouveau pipeline d'entrée avec une taille de lot plus grande.

batch_size = 32
train_ds = df_to_dataset(train, batch_size=batch_size)
val_ds = df_to_dataset(val, shuffle=False, batch_size=batch_size)
test_ds = df_to_dataset(test, shuffle=False, batch_size=batch_size)

Créer, compiler et former le modèle

model = tf.keras.Sequential([
  feature_layer,
  layers.Dense(128, activation='relu'),
  layers.Dense(128, activation='relu'),
  layers.Dropout(.1),
  layers.Dense(1)
])

model.compile(optimizer='adam',
              loss=tf.keras.losses.BinaryCrossentropy(from_logits=True),
              metrics=['accuracy'])

model.fit(train_ds,
          validation_data=val_ds,
          epochs=10)
Epoch 1/10
WARNING:tensorflow:Layers in a Sequential model should only have a single input tensor. Received: inputs={'Type': <tf.Tensor 'IteratorGetNext:11' shape=(None,) dtype=string>, 'Age': <tf.Tensor 'IteratorGetNext:0' shape=(None,) dtype=int64>, 'Breed1': <tf.Tensor 'IteratorGetNext:1' shape=(None,) dtype=string>, 'Gender': <tf.Tensor 'IteratorGetNext:6' shape=(None,) dtype=string>, 'Color1': <tf.Tensor 'IteratorGetNext:2' shape=(None,) dtype=string>, 'Color2': <tf.Tensor 'IteratorGetNext:3' shape=(None,) dtype=string>, 'MaturitySize': <tf.Tensor 'IteratorGetNext:8' shape=(None,) dtype=string>, 'FurLength': <tf.Tensor 'IteratorGetNext:5' shape=(None,) dtype=string>, 'Vaccinated': <tf.Tensor 'IteratorGetNext:12' shape=(None,) dtype=string>, 'Sterilized': <tf.Tensor 'IteratorGetNext:10' shape=(None,) dtype=string>, 'Health': <tf.Tensor 'IteratorGetNext:7' shape=(None,) dtype=string>, 'Fee': <tf.Tensor 'IteratorGetNext:4' shape=(None,) dtype=int64>, 'PhotoAmt': <tf.Tensor 'IteratorGetNext:9' shape=(None,) dtype=int64>}. Consider rewriting this model with the Functional API.
WARNING:tensorflow:Layers in a Sequential model should only have a single input tensor. Received: inputs={'Type': <tf.Tensor 'IteratorGetNext:11' shape=(None,) dtype=string>, 'Age': <tf.Tensor 'IteratorGetNext:0' shape=(None,) dtype=int64>, 'Breed1': <tf.Tensor 'IteratorGetNext:1' shape=(None,) dtype=string>, 'Gender': <tf.Tensor 'IteratorGetNext:6' shape=(None,) dtype=string>, 'Color1': <tf.Tensor 'IteratorGetNext:2' shape=(None,) dtype=string>, 'Color2': <tf.Tensor 'IteratorGetNext:3' shape=(None,) dtype=string>, 'MaturitySize': <tf.Tensor 'IteratorGetNext:8' shape=(None,) dtype=string>, 'FurLength': <tf.Tensor 'IteratorGetNext:5' shape=(None,) dtype=string>, 'Vaccinated': <tf.Tensor 'IteratorGetNext:12' shape=(None,) dtype=string>, 'Sterilized': <tf.Tensor 'IteratorGetNext:10' shape=(None,) dtype=string>, 'Health': <tf.Tensor 'IteratorGetNext:7' shape=(None,) dtype=string>, 'Fee': <tf.Tensor 'IteratorGetNext:4' shape=(None,) dtype=int64>, 'PhotoAmt': <tf.Tensor 'IteratorGetNext:9' shape=(None,) dtype=int64>}. Consider rewriting this model with the Functional API.
231/231 [==============================] - ETA: 0s - loss: 0.6759 - accuracy: 0.6802WARNING:tensorflow:Layers in a Sequential model should only have a single input tensor. Received: inputs={'Type': <tf.Tensor 'IteratorGetNext:11' shape=(None,) dtype=string>, 'Age': <tf.Tensor 'IteratorGetNext:0' shape=(None,) dtype=int64>, 'Breed1': <tf.Tensor 'IteratorGetNext:1' shape=(None,) dtype=string>, 'Gender': <tf.Tensor 'IteratorGetNext:6' shape=(None,) dtype=string>, 'Color1': <tf.Tensor 'IteratorGetNext:2' shape=(None,) dtype=string>, 'Color2': <tf.Tensor 'IteratorGetNext:3' shape=(None,) dtype=string>, 'MaturitySize': <tf.Tensor 'IteratorGetNext:8' shape=(None,) dtype=string>, 'FurLength': <tf.Tensor 'IteratorGetNext:5' shape=(None,) dtype=string>, 'Vaccinated': <tf.Tensor 'IteratorGetNext:12' shape=(None,) dtype=string>, 'Sterilized': <tf.Tensor 'IteratorGetNext:10' shape=(None,) dtype=string>, 'Health': <tf.Tensor 'IteratorGetNext:7' shape=(None,) dtype=string>, 'Fee': <tf.Tensor 'IteratorGetNext:4' shape=(None,) dtype=int64>, 'PhotoAmt': <tf.Tensor 'IteratorGetNext:9' shape=(None,) dtype=int64>}. Consider rewriting this model with the Functional API.
231/231 [==============================] - 4s 10ms/step - loss: 0.6759 - accuracy: 0.6802 - val_loss: 0.5361 - val_accuracy: 0.7351
Epoch 2/10
231/231 [==============================] - 2s 9ms/step - loss: 0.5742 - accuracy: 0.7054 - val_loss: 0.5178 - val_accuracy: 0.7411
Epoch 3/10
231/231 [==============================] - 2s 9ms/step - loss: 0.5369 - accuracy: 0.7231 - val_loss: 0.5031 - val_accuracy: 0.7438
Epoch 4/10
231/231 [==============================] - 2s 9ms/step - loss: 0.5161 - accuracy: 0.7214 - val_loss: 0.5115 - val_accuracy: 0.7259
Epoch 5/10
231/231 [==============================] - 2s 9ms/step - loss: 0.5034 - accuracy: 0.7296 - val_loss: 0.5173 - val_accuracy: 0.7237
Epoch 6/10
231/231 [==============================] - 2s 8ms/step - loss: 0.4983 - accuracy: 0.7301 - val_loss: 0.5153 - val_accuracy: 0.7254
Epoch 7/10
231/231 [==============================] - 2s 9ms/step - loss: 0.4912 - accuracy: 0.7412 - val_loss: 0.5258 - val_accuracy: 0.7010
Epoch 8/10
231/231 [==============================] - 2s 9ms/step - loss: 0.4890 - accuracy: 0.7360 - val_loss: 0.5066 - val_accuracy: 0.7221
Epoch 9/10
231/231 [==============================] - 2s 9ms/step - loss: 0.4824 - accuracy: 0.7443 - val_loss: 0.5091 - val_accuracy: 0.7481
Epoch 10/10
231/231 [==============================] - 2s 9ms/step - loss: 0.4758 - accuracy: 0.7466 - val_loss: 0.5159 - val_accuracy: 0.7492
<keras.callbacks.History at 0x7f06b52a1810>
loss, accuracy = model.evaluate(test_ds)
print("Accuracy", accuracy)
73/73 [==============================] - 0s 6ms/step - loss: 0.4812 - accuracy: 0.7543
Accuracy 0.7543327808380127

Prochaines étapes

La meilleure façon d'en savoir plus sur la classification des données structurées est de l'essayer vous-même. Nous vous suggérons de trouver un autre ensemble de données avec lequel travailler et de former un modèle pour le classer à l'aide d'un code similaire à celui ci-dessus. Pour améliorer la précision, réfléchissez bien aux fonctionnalités à inclure dans votre modèle et à la manière dont elles doivent être représentées.