特徴量列を使用して構造化データを分類する

TensorFlow.org で表示 Google Colab で実行 GitHub でソースを表示 ノートブックをダウンロード

警告: このチュートリアルで説明されている tf.feature_columns モジュールは、新しいコードにはお勧めしません。 Keras 前処理レイヤーがこの機能をカバーしています。移行手順については、特徴量列の移行ガイドをご覧ください。tf.feature_columns モジュールは、TF1 Estimators で使用するために設計されました。互換性保証の対象となりますが、セキュリティの脆弱性以外の修正は行われません。

This tutorial demonstrates how to classify structured data (e.g. tabular data in a CSV). We will use Keras to define the model, and tf.feature_column as a bridge to map from columns in a CSV to features used to train the model. This tutorial contains complete code to:

  • Pandas を使用して CSV ファイルを読み込みます。
  • tf.data を使用して、行をバッチ化してシャッフルする入力パイプラインを構築します。
  • 特徴量の列を使ってモデルをトレーニングするために使用する特徴量に、CSV の列をマッピングします。
  • Kerasを使ったモデルの構築と、訓練及び評価

データセット

下記はこのデータセットの說明です。数値列とカテゴリー列があることに注目してください。

Following is a description of this dataset. Notice there are both numeric and categorical columns. There is a free text column which we will not use in this tutorial.

説明 特徴量の型 データ型
Type 動物の種類(犬、猫) カテゴリカル 文字列
Age ペットの年齢 数値 整数
Breed1 ペットの主な品種 カテゴリカル 文字列
Color1 ペットの毛色 1 カテゴリカル 文字列
Color2 ペットの毛色 2 カテゴリカル 文字列
MaturitySize 成獣時のサイズ カテゴリカル 文字列
FurLength 毛の長さ カテゴリカル 文字列
Vaccinated 予防接種済み カテゴリカル 文字列
Sterilized 不妊手術済み カテゴリカル 文字列
Health 健康状態 カテゴリカル 文字列
Fee 引き取り料 数値 整数
Description ペットのプロフィール テキスト 文字列
PhotoAmt アップロードされたペットの写真数 数値 整数
AdoptionSpeed 引き取りまでの期間 分類 整数

TensorFlow他ライブラリのインポート

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
2022-12-14 23:03:28.983998: W tensorflow/compiler/xla/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libnvinfer.so.7'; dlerror: libnvinfer.so.7: cannot open shared object file: No such file or directory
2022-12-14 23:03:28.984090: W tensorflow/compiler/xla/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libnvinfer_plugin.so.7'; dlerror: libnvinfer_plugin.so.7: cannot open shared object file: No such file or directory
2022-12-14 23:03:28.984099: W tensorflow/compiler/tf2tensorrt/utils/py_utils.cc:38] TF-TRT Warning: Cannot dlopen some TensorRT libraries. If you would like to use Nvidia GPU with TensorRT, please make sure the missing libraries mentioned above are installed properly.

Pandasを使ったデータフレーム作成

Pandasは、構造化データの読み込みや操作のための便利なユーティリティを持つPythonのライブラリです。ここでは、Pandasを使ってURLからデータをダウンロードし、データフレームに読み込みます。

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
1668792/1668792 [==============================] - 0s 0us/step
dataframe.head()

ターゲット変数を作成する

元のデータセットでは、ペットが引き取られるまでの期間 (1 週目、1 か月目、3 か月目など) を予測することがタスクとなっていますが、このチュートリアルでは、このタスクを単純化します。ここでは、このタスクを二項分類問題にし、単にペットが引き取られるかどうかのみを予測します。

ラベルの列を変更すると、0 は引き取られなかった、1 は引き取られたことを示すようになります。

# 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'])

データフレームを、訓練用、検証用、テスト用に分割

ダウンロードしたデータセットは1つのCSVファイルです。これを、訓練用、検証用、テスト用のデータセットに分割します。

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

tf.dataを使った入力パイプラインの構築

次に、tf.data を使ってデータフレームをラップします。こうすることで、特徴量の列を Pandas データフレームの列からモデルトレーニング用の特徴量へのマッピングするための橋渡し役として使うことができます。(メモリに収まらないぐらいの) 非常に大きな CSV ファイルを扱う場合には、tf.data を使ってディスクから直接 CSV ファイルを読み込むことになります。この方法は、このチュートリアルでは取り上げません。

# 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)

入力パイプラインを理解する

入力パイプラインを構築したので、それが返すデータのフォーマットを見るために呼び出してみましょう。出力を読みやすくするためにバッチサイズを小さくしてあります。

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([ 2  2 24  1  3], shape=(5,), dtype=int64)
A batch of targets: tf.Tensor([1 0 0 1 1], shape=(5,), dtype=int64)

ご覧のとおり、データセットは、データフレームの行から列の値にマップしている列名の (データフレームの列名) のディクショナリを返しています。

特徴量列の様々な型のデモ

TensorFlow には様々な型の特徴量列があります。このセクションでは、いくつかの型の特徴量列を作り、データフレームの列をどのように変換するかを示します。

# We will use this batch to demonstrate several types of feature columns
example_batch = next(iter(train_ds))[0]
# feature columnsを作りデータのバッチを変換する
# ユーティリティメソッド
def demo(feature_column):
  feature_layer = layers.DenseFeatures(feature_column)
  print(feature_layer(example_batch).numpy())

数値コラム

特徴量列の出力はモデルへの入力になります (上記で定義したデモ関数を使うと、データフレームの列がどのように変換されるかを見ることができます)。数値列は、最も単純な型の列です。数値列は実数特徴量を表現するのに使われます。この列を使う場合、モデルにはデータフレームの列の値がそのまま渡されます。

photo_count = feature_column.numeric_column('PhotoAmt')
demo(photo_count)
[[1.]
 [3.]
 [5.]
 [3.]
 [5.]]

PetFinder データセットでは、データフレームのほとんどの列がカテゴリカル型です。

バケット化コラム

数値をそのままモデルに入力するのではなく、値の範囲に基づいた異なるカテゴリに分割したいことがあります。例えば、人の年齢を表す生データを考えてみましょう。バケット化列を使うと年齢を数値列として表現するのではなく、年齢をいくつかのバケットに分割できます。以下のワンホット値が、各行がどの年齢範囲にあるかを表していることに注目してください。

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

カテゴリー型コラム

このデータセットでは、型は (「犬」や「猫」などの) 文字列として表現されています。文字列を直接モデルに入力することはできません。まず、文字列を数値にマッピングする必要があります。カテゴリカル語彙列を使うと、(上記で示した年齢バケットのように) 文字列をワンホットベクトルとして表現することができます。語彙はcategorical_column_with_vocabulary_list を使ってリストで渡すか、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)
[[0. 1.]
 [0. 1.]
 [0. 1.]
 [0. 1.]
 [0. 1.]]

埋め込み型コラム

数種類の文字列ではなく、カテゴリごとに数千 (あるいはそれ以上) の値があるとしましょう。カテゴリの数が多くなってくると、様々な理由から、ワンホットエンコーディングを使ってニューラルネットワークをトレーニングすることが難しくなります。埋め込み列を使うと、こうした制約を克服することが可能です。埋め込み列は、データを多次元のワンホットベクトルとして表すのではなく、セルの値が 0 か 1 かだけではなく、どんな数値でもとれるような密な低次元ベクトルとして表現します。埋め込みのサイズ (下記の例では 8) は、チューニングが必要なパラメータです。

重要ポイント: カテゴリカル列が多くの選択肢を持つ場合、埋め込み列を使用することが最善の方法です。ここでは例を一つ示しますので、今後様々なデータセットを扱う際には、この例を参考にしてください。

# 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.65292776 -0.48165944 -0.34872264 -0.40858582 -0.04265913  0.26578084
   0.4366054  -0.2958504 ]
 [-0.65292776 -0.48165944 -0.34872264 -0.40858582 -0.04265913  0.26578084
   0.4366054  -0.2958504 ]
 [-0.65292776 -0.48165944 -0.34872264 -0.40858582 -0.04265913  0.26578084
   0.4366054  -0.2958504 ]
 [-0.65292776 -0.48165944 -0.34872264 -0.40858582 -0.04265913  0.26578084
   0.4366054  -0.2958504 ]
 [ 0.10934287  0.43237892 -0.6214551  -0.26910862 -0.06913823 -0.09724928
  -0.00828571 -0.2563711 ]]

ハッシュ化特徴量列

値の種類が多いカテゴリカル列を表現するもう一つの方法として、categorical_column_with_hash_bucket を使うことができます。この特徴量列は入力のハッシュ値を計算し、文字列をエンコードするために hash_bucket_size バケットの 1 つを選択します。この列を使用する場合には、語彙を用意する必要はありません。また、スペースの節約のために、実際のカテゴリ数に比べて極めて少ない hash_buckets 数を選択することも可能です。

重要ポイント: この手法の重要な欠点の一つは、異なる文字列が同じバケットにマッピングされ、衝突が発生する可能性があるということです。しかしながら、データセットによっては問題が発生しない場合もあります。

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. 1. 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. 0. 1. 0. 0.]
 [0. 0. 0. 0. 0. 0. 0. 1. 0. 0.]]

フィーチャークロス列

複数の特徴量をまとめて1つの特徴量にする、フィーチャークロスとして知られている手法は、モデルが特徴量の組み合わせの一つ一つに別々の重みを学習することを可能にします。ここでは年齢と型を交差させて新しい特徴量を作ってみます。(crossed_column) は、起こりうるすべての組み合わせ全体の表 (これは非常に大きくなる可能性があります) を作るものではないことに注意してください。フィーチャークロス列は、代わりにバックエンドとして hashed_column を使用しているため、表の大きさを選択することができます。

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

使用するコラムを選択する

これまで、いくつかの特徴量列の使い方を見てきました。これからモデルのトレーニングにそれらを使用します。このチュートリアルの目的は、特徴量列を使うのに必要な完全なコード (いわば仕組み) を示すことです。以下ではモデルをトレーニングするための列を適当に選びました。

キーポイント:正確なモデルを構築するのが目的である場合には、できるだけ大きなデータセットを使用して、どの特徴量を含めるのがもっとも意味があるのかや、それらをどう表現したらよいかを、慎重に検討してください。

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))

特徴量層の構築

特徴量列を定義したので、次に DenseFeatures レイヤーを使って Keras モデルに入力します。

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

これまでは、feature columnsの働きを見るため、小さなバッチサイズを使ってきました。ここではもう少し大きなバッチサイズの新しい入力パイプラインを作ります。

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)

モデルの構築、コンパイルと訓練

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.
223/231 [===========================>..] - ETA: 0s - loss: 0.6654 - accuracy: 0.6879WARNING: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 [==============================] - 7s 18ms/step - loss: 0.6666 - accuracy: 0.6866 - val_loss: 0.5611 - val_accuracy: 0.6517
Epoch 2/10
231/231 [==============================] - 2s 7ms/step - loss: 0.5820 - accuracy: 0.7085 - val_loss: 0.5251 - val_accuracy: 0.7411
Epoch 3/10
231/231 [==============================] - 2s 7ms/step - loss: 0.5307 - accuracy: 0.7229 - val_loss: 0.5172 - val_accuracy: 0.7010
Epoch 4/10
231/231 [==============================] - 2s 7ms/step - loss: 0.5016 - accuracy: 0.7309 - val_loss: 0.4984 - val_accuracy: 0.7416
Epoch 5/10
231/231 [==============================] - 2s 7ms/step - loss: 0.4971 - accuracy: 0.7362 - val_loss: 0.4997 - val_accuracy: 0.7308
Epoch 6/10
231/231 [==============================] - 2s 7ms/step - loss: 0.4894 - accuracy: 0.7421 - val_loss: 0.5084 - val_accuracy: 0.7514
Epoch 7/10
231/231 [==============================] - 2s 7ms/step - loss: 0.4834 - accuracy: 0.7463 - val_loss: 0.5037 - val_accuracy: 0.7432
Epoch 8/10
231/231 [==============================] - 2s 8ms/step - loss: 0.4783 - accuracy: 0.7464 - val_loss: 0.5044 - val_accuracy: 0.7384
Epoch 9/10
231/231 [==============================] - 2s 8ms/step - loss: 0.4697 - accuracy: 0.7578 - val_loss: 0.5104 - val_accuracy: 0.7086
Epoch 10/10
231/231 [==============================] - 2s 7ms/step - loss: 0.4679 - accuracy: 0.7535 - val_loss: 0.5074 - val_accuracy: 0.7389
<keras.callbacks.History at 0x7f8ac8cb1730>
loss, accuracy = model.evaluate(test_ds)
print("Accuracy", accuracy)
73/73 [==============================] - 0s 5ms/step - loss: 0.5103 - accuracy: 0.7288
Accuracy 0.7287694811820984

重要ポイント: 通常、データベースの規模が大きく複雑であるほど、ディープラーニングの結果がよくなります。このチュートリアルのデータセットのように、小さなデータセットを使用する場合は、決定木またはランダムフォレストを強力なベースラインとして使用することをお勧めします。このチュートリアルでは、構造化データとの連携の仕組みを実演することが目的であり、コードは将来的に独自のデータセットを使用する際の出発点として使用することができます。

次のステップ

構造化データの分類をさらに学習するには、ご自分で別のデータセットを使用し、上記のようなコードを使用し、モデルのトレーニングと分類を試してみてください。正解度を改善するには、モデルに含める特徴量とその表現方法を吟味してください。