tf.data を使って CSV をロードする

View on TensorFlow.org Run in Google Colab View source on GitHub

このチュートリアルでは、CSV データを tf.data.Dataset にロードする手法の例を示します。

このチュートリアルで使われているデータはタイタニック号の乗客リストから取られたものです。乗客が生き残る可能性を、年齢、性別、チケットの等級、そして乗客が一人で旅行しているか否かといった特性から予測することを試みます。

設定

try:
  !pip install -q tf-nightly-2.0-preview
except Exception:
  pass

from __future__ import absolute_import, division, print_function, unicode_literals

import numpy as np
import tensorflow as tf
import tensorflow_datasets as tfds
TRAIN_DATA_URL = "https://storage.googleapis.com/tf-datasets/titanic/train.csv"
TEST_DATA_URL = "https://storage.googleapis.com/tf-datasets/titanic/eval.csv"

train_file_path = tf.keras.utils.get_file("train.csv", TRAIN_DATA_URL)
test_file_path = tf.keras.utils.get_file("eval.csv", TEST_DATA_URL)
Downloading data from https://storage.googleapis.com/tf-datasets/titanic/train.csv
32768/30874 [===============================] - 0s 0us/step
Downloading data from https://storage.googleapis.com/tf-datasets/titanic/eval.csv
16384/13049 [=====================================] - 0s 0us/step
# numpy の値を読みやすくする
np.set_printoptions(precision=3, suppress=True)

データのロード

それではいつものように、扱っている CSV ファイルの先頭を見てみましょう。

!head {train_file_path}
survived,sex,age,n_siblings_spouses,parch,fare,class,deck,embark_town,alone
0,male,22.0,1,0,7.25,Third,unknown,Southampton,n
1,female,38.0,1,0,71.2833,First,C,Cherbourg,n
1,female,26.0,0,0,7.925,Third,unknown,Southampton,y
1,female,35.0,1,0,53.1,First,C,Southampton,n
0,male,28.0,0,0,8.4583,Third,unknown,Queenstown,y
0,male,2.0,3,1,21.075,Third,unknown,Southampton,n
1,female,27.0,0,2,11.1333,Third,unknown,Southampton,n
1,female,14.0,1,0,30.0708,Second,unknown,Cherbourg,n
1,female,4.0,1,1,16.7,Third,G,Southampton,n

ご覧のように、CSV の列にはラベルが付いています。後ほど必要になるので、ファイルから読み出しておきましょう。

# 入力ファイル中の CSV 列
with open(train_file_path, 'r') as f:
    names_row = f.readline()


CSV_COLUMNS = names_row.rstrip('\n').split(',')
print(CSV_COLUMNS)
['survived', 'sex', 'age', 'n_siblings_spouses', 'parch', 'fare', 'class', 'deck', 'embark_town', 'alone']

データセットコンストラクタはこれらのラベルを自動的にピックアップします。

使用するファイルの1行目に列名がない場合、make_csv_dataset 関数の column_names 引数に文字列のリストとして渡します。


CSV_COLUMNS = ['survived', 'sex', 'age', 'n_siblings_spouses', 'parch', 'fare', 'class', 'deck', 'embark_town', 'alone']

dataset = tf.data.experimental.make_csv_dataset(
     ...,
     column_names=CSV_COLUMNS,
     ...)
  

この例では使用可能な列をすべて使うことになります。データセットから列を除く必要がある場合には、使用したい列だけを含むリストを作り、コンストラクタの(オプションである)select_columns 引数として渡します。


drop_columns = ['fare', 'embark_town']
columns_to_use = [col for col in CSV_COLUMNS if col not in drop_columns]

dataset = tf.data.experimental.make_csv_dataset(
  ...,
  select_columns = columns_to_use, 
  ...)

各サンプルのラベルとなる列を特定し、それが何であるかを示す必要があります。

LABELS = [0, 1]
LABEL_COLUMN = 'survived'

FEATURE_COLUMNS = [column for column in CSV_COLUMNS if column != LABEL_COLUMN]

コンストラクタの引数の値が揃ったので、ファイルから CSV データを読み込みデータセットを作ることにしましょう。

(完全なドキュメントは、tf.data.experimental.make_csv_dataset を参照してください)

def get_dataset(file_path):
  dataset = tf.data.experimental.make_csv_dataset(
      file_path,
      batch_size=12, # 見やすく表示するために意図して小さく設定しています
      label_name=LABEL_COLUMN,
      na_value="?",
      num_epochs=1,
      ignore_errors=True)
  return dataset

raw_train_data = get_dataset(train_file_path)
raw_test_data = get_dataset(test_file_path)
WARNING:tensorflow:From /tmpfs/src/tf_docs_env/lib/python3.6/site-packages/tensorflow_core/python/data/experimental/ops/readers.py:540: parallel_interleave (from tensorflow.python.data.experimental.ops.interleave_ops) is deprecated and will be removed in a future version.
Instructions for updating:
Use `tf.data.Dataset.interleave(map_func, cycle_length, block_length, num_parallel_calls=tf.data.experimental.AUTOTUNE)` instead. If sloppy execution is desired, use `tf.data.Options.experimental_deterministic`.

データセットを構成する要素は、(複数のサンプル, 複数のラベル)の形のタプルとして表されるバッチです。サンプル中のデータは(行ベースのテンソルではなく)列ベースのテンソルとして構成され、それぞれはバッチサイズ(このケースでは12個)の要素が含まれます。

実際に見てみましょう。

examples, labels = next(iter(raw_train_data)) # 最初のバッチのみ
print("EXAMPLES: \n", examples, "\n")
print("LABELS: \n", labels)
EXAMPLES: 
 OrderedDict([('sex', <tf.Tensor: shape=(12,), dtype=string, numpy=
array([b'male', b'female', b'female', b'male', b'male', b'female',
       b'female', b'male', b'female', b'female', b'male', b'male'],
      dtype=object)>), ('age', <tf.Tensor: shape=(12,), dtype=float32, numpy=
array([28., 18., 21., 28., 28., 31., 15., 36., 28., 28., 15., 18.],
      dtype=float32)>), ('n_siblings_spouses', <tf.Tensor: shape=(12,), dtype=int32, numpy=array([0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0], dtype=int32)>), ('parch', <tf.Tensor: shape=(12,), dtype=int32, numpy=array([0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0], dtype=int32)>), ('fare', <tf.Tensor: shape=(12,), dtype=float32, numpy=
array([ 7.896,  9.35 , 77.958,  8.05 , 15.5  ,  8.683, 14.454,  7.896,
        7.787,  7.75 ,  7.229, 13.   ], dtype=float32)>), ('class', <tf.Tensor: shape=(12,), dtype=string, numpy=
array([b'Third', b'Third', b'First', b'Third', b'Third', b'Third',
       b'Third', b'Third', b'Third', b'Third', b'Third', b'Second'],
      dtype=object)>), ('deck', <tf.Tensor: shape=(12,), dtype=string, numpy=
array([b'unknown', b'unknown', b'D', b'unknown', b'unknown', b'unknown',
       b'unknown', b'unknown', b'unknown', b'unknown', b'unknown',
       b'unknown'], dtype=object)>), ('embark_town', <tf.Tensor: shape=(12,), dtype=string, numpy=
array([b'Southampton', b'Southampton', b'Southampton', b'Southampton',
       b'Queenstown', b'Southampton', b'Cherbourg', b'Southampton',
       b'Queenstown', b'Queenstown', b'Cherbourg', b'Southampton'],
      dtype=object)>), ('alone', <tf.Tensor: shape=(12,), dtype=string, numpy=
array([b'y', b'n', b'y', b'y', b'y', b'y', b'n', b'y', b'y', b'y', b'n',
       b'y'], dtype=object)>)]) 

LABELS: 
 tf.Tensor([0 1 1 0 0 1 1 0 1 1 0 0], shape=(12,), dtype=int32)

データの前処理

カテゴリデータ

この CSV データ中のいくつかの列はカテゴリ列です。つまり、その中身は、限られた選択肢の中のひとつである必要があります。

この CSV では、これらの選択肢はテキストとして表現されています。このテキストは、モデルの訓練を行えるように、数字に変換する必要があります。これをやりやすくするため、カテゴリ列のリストとその選択肢のリストを作成する必要があります。

CATEGORIES = {
    'sex': ['male', 'female'],
    'class' : ['First', 'Second', 'Third'],
    'deck' : ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J'],
    'embark_town' : ['Cherbourg', 'Southhampton', 'Queenstown'],
    'alone' : ['y', 'n']
}

カテゴリ値のテンソルを受け取り、それを値の名前のリストとマッチングして、さらにワンホット・エンコーディングを行う関数を書きます。

def process_categorical_data(data, categories):
  """カテゴリ値を表すワンホット・エンコーディングされたテンソルを返す"""
  
  # 最初の ' ' を取り除く
  data = tf.strings.regex_replace(data, '^ ', '')
  # 最後の '.' を取り除く
  data = tf.strings.regex_replace(data, r'\.$', '')
  
  # ワンホット・エンコーディング
  # data を1次元(リスト)から2次元(要素が1個のリストのリスト)にリシェープ
  data = tf.reshape(data, [-1, 1])
  # それぞれの要素について、カテゴリ数の長さの真偽値のリストで、
  # 要素とカテゴリのラベルが一致したところが True となるものを作成
  data = categories == data
  # 真偽値を浮動小数点数にキャスト
  data = tf.cast(data, tf.float32)
  
  # エンコーディング全体を次の1行に収めることもできる:
  # data = tf.cast(categories == tf.reshape(data, [-1, 1]), tf.float32)
  return data

この処理を可視化するため、最初のバッチからカテゴリ列のテンソル1つを取り出し、処理を行い、前後の状態を示します。

class_tensor = examples['class']
class_tensor
<tf.Tensor: shape=(12,), dtype=string, numpy=
array([b'Third', b'Third', b'First', b'Third', b'Third', b'Third',
       b'Third', b'Third', b'Third', b'Third', b'Third', b'Second'],
      dtype=object)>
class_categories = CATEGORIES['class']
class_categories
['First', 'Second', 'Third']
processed_class = process_categorical_data(class_tensor, class_categories)
processed_class
<tf.Tensor: shape=(12, 3), dtype=float32, numpy=
array([[0., 0., 1.],
       [0., 0., 1.],
       [1., 0., 0.],
       [0., 0., 1.],
       [0., 0., 1.],
       [0., 0., 1.],
       [0., 0., 1.],
       [0., 0., 1.],
       [0., 0., 1.],
       [0., 0., 1.],
       [0., 0., 1.],
       [0., 1., 0.]], dtype=float32)>

2つの入力の長さと、出力の形状の関係に注目してください。

print("Size of batch: ", len(class_tensor.numpy()))
print("Number of category labels: ", len(class_categories))
print("Shape of one-hot encoded tensor: ", processed_class.shape)
Size of batch:  12
Number of category labels:  3
Shape of one-hot encoded tensor:  (12, 3)

連続データ

連続データは値が0と1の間にになるように標準化する必要があります。これを行うために、それぞれの値を、1を列値の平均の2倍で割ったものを掛ける関数を書きます。

この関数は、データの2次元のテンソルへのリシェープも行います。

def process_continuous_data(data, mean):
  # data の標準化
  data = tf.cast(data, tf.float32) * 1/(2*mean)
  return tf.reshape(data, [-1, 1])

この計算を行うためには、列値の平均が必要です。現実には、この値を計算する必要があるのは明らかですが、この例のために値を示します。

MEANS = {
    'age' : 29.631308,
    'n_siblings_spouses' : 0.545455,
    'parch' : 0.379585,
    'fare' : 34.385399
}

前と同様に、この関数が実際に何をしているかを見るため、連続値のテンソルを1つ取り、処理前と処理後を見てみます。

age_tensor = examples['age']
age_tensor
<tf.Tensor: shape=(12,), dtype=float32, numpy=
array([28., 18., 21., 28., 28., 31., 15., 36., 28., 28., 15., 18.],
      dtype=float32)>
process_continuous_data(age_tensor, MEANS['age'])
<tf.Tensor: shape=(12, 1), dtype=float32, numpy=
array([[0.472],
       [0.304],
       [0.354],
       [0.472],
       [0.472],
       [0.523],
       [0.253],
       [0.607],
       [0.472],
       [0.472],
       [0.253],
       [0.304]], dtype=float32)>

データの前処理

これらの前処理のタスクを1つの関数にまとめ、データセット内のバッチにマッピングできるようにします。

def preprocess(features, labels):
  
  # カテゴリ特徴量の処理
  for feature in CATEGORIES.keys():
    features[feature] = process_categorical_data(features[feature],
                                                 CATEGORIES[feature])

  # 連続特徴量の処理
  for feature in MEANS.keys():
    features[feature] = process_continuous_data(features[feature],
                                                MEANS[feature])
  
  # 特徴量を1つのテンソルに組み立てる
  features = tf.concat([features[column] for column in FEATURE_COLUMNS], 1)
  
  return features, labels


次に、 tf.Dataset.map 関数を使って適用し、過学習を防ぐためにデータセットをシャッフルします。

train_data = raw_train_data.map(preprocess).shuffle(500)
test_data = raw_test_data.map(preprocess)

サンプル1個がどうなっているか見てみましょう。

examples, labels = next(iter(train_data))

examples, labels
(<tf.Tensor: shape=(12, 24), dtype=float32, numpy=
 array([[1.   , 0.   , 0.607, 0.   , 0.   , 0.187, 0.   , 1.   , 0.   ,
         0.   , 0.   , 0.   , 1.   , 0.   , 0.   , 0.   , 0.   , 0.   ,
         0.   , 1.   , 0.   , 0.   , 1.   , 0.   ],
        [1.   , 0.   , 0.574, 0.   , 0.   , 0.117, 0.   , 0.   , 1.   ,
         0.   , 0.   , 0.   , 0.   , 0.   , 0.   , 0.   , 0.   , 0.   ,
         0.   , 0.   , 0.   , 0.   , 1.   , 0.   ],
        [0.   , 1.   , 0.894, 1.833, 0.   , 0.749, 1.   , 0.   , 0.   ,
         0.   , 0.   , 1.   , 0.   , 0.   , 0.   , 0.   , 0.   , 0.   ,
         0.   , 0.   , 0.   , 0.   , 0.   , 1.   ],
        [0.   , 1.   , 0.245, 0.917, 0.   , 0.21 , 0.   , 0.   , 1.   ,
         0.   , 0.   , 0.   , 0.   , 0.   , 0.   , 0.   , 0.   , 0.   ,
         0.   , 1.   , 0.   , 0.   , 0.   , 1.   ],
        [0.   , 1.   , 0.472, 0.917, 0.   , 1.296, 1.   , 0.   , 0.   ,
         0.   , 0.   , 1.   , 0.   , 0.   , 0.   , 0.   , 0.   , 0.   ,
         0.   , 1.   , 0.   , 0.   , 0.   , 1.   ],
        [1.   , 0.   , 0.726, 0.917, 1.317, 0.382, 0.   , 1.   , 0.   ,
         0.   , 0.   , 0.   , 0.   , 0.   , 0.   , 0.   , 0.   , 0.   ,
         0.   , 0.   , 0.   , 0.   , 0.   , 1.   ],
        [1.   , 0.   , 0.827, 0.917, 0.   , 0.828, 1.   , 0.   , 0.   ,
         1.   , 0.   , 0.   , 0.   , 0.   , 0.   , 0.   , 0.   , 0.   ,
         0.   , 1.   , 0.   , 0.   , 0.   , 1.   ],
        [0.   , 1.   , 0.371, 0.917, 1.317, 0.422, 0.   , 1.   , 0.   ,
         0.   , 0.   , 0.   , 0.   , 0.   , 0.   , 0.   , 0.   , 0.   ,
         0.   , 0.   , 0.   , 0.   , 0.   , 1.   ],
        [1.   , 0.   , 0.017, 4.583, 2.634, 0.682, 0.   , 0.   , 1.   ,
         0.   , 0.   , 0.   , 0.   , 0.   , 0.   , 0.   , 0.   , 0.   ,
         0.   , 0.   , 0.   , 0.   , 0.   , 1.   ],
        [0.   , 1.   , 0.405, 0.   , 2.634, 0.243, 0.   , 0.   , 1.   ,
         0.   , 0.   , 0.   , 0.   , 0.   , 0.   , 1.   , 0.   , 0.   ,
         0.   , 0.   , 0.   , 0.   , 0.   , 1.   ],
        [1.   , 0.   , 0.574, 0.917, 0.   , 0.305, 0.   , 1.   , 0.   ,
         0.   , 0.   , 0.   , 0.   , 0.   , 0.   , 0.   , 0.   , 0.   ,
         0.   , 0.   , 0.   , 0.   , 0.   , 1.   ],
        [0.   , 1.   , 0.742, 0.   , 1.317, 0.843, 1.   , 0.   , 0.   ,
         0.   , 1.   , 0.   , 0.   , 0.   , 0.   , 0.   , 0.   , 0.   ,
         0.   , 1.   , 0.   , 0.   , 0.   , 1.   ]], dtype=float32)>,
 <tf.Tensor: shape=(12,), dtype=int32, numpy=array([0, 0, 1, 0, 1, 0, 1, 1, 0, 1, 0, 1], dtype=int32)>)

このサンプルは、(バッチサイズである)12個のアイテムをもつ2次元の配列からできています。アイテムそれぞれは、元の CSV ファイルの1行を表しています。ラベルは12個の値をもつ1次元のテンソルです。

モデルの構築

この例では、Keras Functional API を使用し、単純なモデルを構築するために get_model コンストラクタでラッピングしています。

def get_model(input_dim, hidden_units=[100]):
  """複数の層を持つ Keras モデルを作成

  引数:
    input_dim: (int) バッチ中のアイテムの形状
    labels_dim: (int) ラベルの形状
    hidden_units: [int] DNN の層のサイズ(入力層が先)
    learning_rate: (float) オプティマイザの学習率
    
  戻り値:
    Keras モデル
  """

  inputs = tf.keras.Input(shape=(input_dim,))
  x = inputs

  for units in hidden_units:
    x = tf.keras.layers.Dense(units, activation='relu')(x)
  outputs = tf.keras.layers.Dense(1, activation='sigmoid')(x)

  model = tf.keras.Model(inputs, outputs)
 
  return model

get_model コンストラクタは入力データの形状(バッチサイズを除く)を知っている必要があります。

input_shape, output_shape = train_data.output_shapes

input_dimension = input_shape.dims[1] # [0] はバッチサイズ

訓練、評価、そして予測

これでモデルをインスタンス化し、訓練することができます。

model = get_model(input_dimension)
model.compile(
    loss='binary_crossentropy',
    optimizer='adam',
    metrics=['accuracy'])

model.fit(train_data, epochs=20)
Epoch 1/20
53/53 [==============================] - 1s 11ms/step - loss: 0.5695 - accuracy: 0.7464
Epoch 2/20
53/53 [==============================] - 0s 2ms/step - loss: 0.4695 - accuracy: 0.8038
Epoch 3/20
53/53 [==============================] - 0s 2ms/step - loss: 0.4401 - accuracy: 0.8118
Epoch 4/20
53/53 [==============================] - 0s 2ms/step - loss: 0.4322 - accuracy: 0.8134
Epoch 5/20
53/53 [==============================] - 0s 2ms/step - loss: 0.4167 - accuracy: 0.8166
Epoch 6/20
53/53 [==============================] - 0s 2ms/step - loss: 0.4339 - accuracy: 0.8150
Epoch 7/20
53/53 [==============================] - 0s 2ms/step - loss: 0.4136 - accuracy: 0.8246
Epoch 8/20
53/53 [==============================] - 0s 2ms/step - loss: 0.4119 - accuracy: 0.8262
Epoch 9/20
53/53 [==============================] - 0s 2ms/step - loss: 0.4143 - accuracy: 0.8246
Epoch 10/20
53/53 [==============================] - 0s 2ms/step - loss: 0.4017 - accuracy: 0.8278
Epoch 11/20
53/53 [==============================] - 0s 2ms/step - loss: 0.4027 - accuracy: 0.8246
Epoch 12/20
53/53 [==============================] - 0s 2ms/step - loss: 0.4146 - accuracy: 0.8246
Epoch 13/20
53/53 [==============================] - 0s 2ms/step - loss: 0.3909 - accuracy: 0.8198
Epoch 14/20
53/53 [==============================] - 0s 2ms/step - loss: 0.3924 - accuracy: 0.8246
Epoch 15/20
53/53 [==============================] - 0s 2ms/step - loss: 0.3872 - accuracy: 0.8357
Epoch 16/20
53/53 [==============================] - 0s 2ms/step - loss: 0.3856 - accuracy: 0.8309
Epoch 17/20
53/53 [==============================] - 0s 2ms/step - loss: 0.3890 - accuracy: 0.8262
Epoch 18/20
53/53 [==============================] - 0s 2ms/step - loss: 0.3868 - accuracy: 0.8373
Epoch 19/20
53/53 [==============================] - 0s 2ms/step - loss: 0.3938 - accuracy: 0.8309
Epoch 20/20
53/53 [==============================] - 0s 2ms/step - loss: 0.3806 - accuracy: 0.8389

<tensorflow.python.keras.callbacks.History at 0x7f56146832e8>

モデルの訓練が終わったら、test_data データセットでの正解率をチェックできます。

test_loss, test_accuracy = model.evaluate(test_data)

print('\n\nTest Loss {}, Test Accuracy {}'.format(test_loss, test_accuracy))
     22/Unknown - 0s 5ms/step - loss: 0.4417 - accuracy: 0.8068

Test Loss 0.441657653586431, Test Accuracy 0.8068181872367859

単一のバッチ、または、バッチからなるデータセットのラベルを推論する場合には、tf.keras.Model.predict を使います。

predictions = model.predict(test_data)

# 結果のいくつかを表示
for prediction, survived in zip(predictions[:10], list(test_data)[0][1][:10]):
  print("Predicted survival: {:.2%}".format(prediction[0]),
        " | Actual outcome: ",
        ("SURVIVED" if bool(survived) else "DIED"))

Predicted survival: 10.30%  | Actual outcome:  SURVIVED
Predicted survival: 32.64%  | Actual outcome:  DIED
Predicted survival: 95.11%  | Actual outcome:  DIED
Predicted survival: 86.40%  | Actual outcome:  DIED
Predicted survival: 77.14%  | Actual outcome:  SURVIVED
Predicted survival: 39.89%  | Actual outcome:  DIED
Predicted survival: 94.80%  | Actual outcome:  DIED
Predicted survival: 10.34%  | Actual outcome:  DIED
Predicted survival: 28.11%  | Actual outcome:  DIED
Predicted survival: 93.66%  | Actual outcome:  SURVIVED