TensorFlow2に改めて入門2(モデルのカスタマイズ)

TensorFlow2系の書き方を体系的にまとめたものがあまりなかったので入門記事としてまとめようと思います。
とりあえず動いて機械学習の流れを掴めるというようなゆるい内容です。
基本的にはColab上で動くように想定しています。
Done is better than perfectを目指しているので、とりあえずアップして適宜直していくスタイルでいければと思っています。

環境

Google Colaboratory(通常版)

何をするか

前回は高レベルAPIを使って細かなロスなどの調整を行わずに予測を行いました。
今回は、学習過程部分をもう少し詳しく定義するためにGradientTapeを使って実装していきます。

データセット

前回と同じくアヤメのデータセットを使います。
データの作り方も前回同様です。

import numpy as np
import pandas as pd

import tensorflow as tf
from tensorflow.keras import layers
from tensorflow.keras.utils import to_categorical

from sklearn.model_selection import train_test_split
from sklearn.datasets import load_iris

iris = load_iris() #sklearnのirisデータをロード
iris_data = np.concatenate((iris.data, iris.target[:, np.newaxis]), axis=1) #irisデータの特徴量とラベルを結合
iris.feature_names.append("y") #ラベル名にyを追加
dataframe = pd.DataFrame(iris_data, columns=iris.feature_names) #アヤメのデータフレームを作成
display(dataframe)

train, test = train_test_split(dataframe, test_size=0.2)
train, val = train_test_split(train, test_size=0.2)
print(f"訓練データ数:{len(train)}")
print(f"評価データ数:{len(val)}")
print(f"テストデータ数:{len(test)}")

def df_to_dataset(dataframe, shuffle=True, batch_size=32):
  """データフレームをtf.data.Dataに変換する関数
  """
  dataframe = dataframe.copy()
  labels = dataframe.pop("y")
  labels = to_categorical(labels)
  ds = tf.data.Dataset.from_tensor_slices((dataframe, labels))
  if shuffle:
    ds = ds.shuffle(buffer_size=len(dataframe))
  ds = ds.batch(batch_size)
  return ds

batch_size = 5 
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)

モデル作成

今回もKerasで簡単にモデルを作成します。

model = tf.keras.Sequential([
  tf.keras.layers.Dense(10, activation=tf.nn.relu, input_shape=(4,)),  # input shape required
  tf.keras.layers.Dense(10, activation=tf.nn.relu),
  tf.keras.layers.Dense(3)
])

モデルにデータを流してみます。
まずはデータを1バッチ分準備してみます。
Datasetに変換したtrain_dsは繰り返しができるオブジェクトになっています。
このオブジェクトに対してイテレータとnext関数でバッチを取り出すことができます。

feature_batch, label_batch = next(iter(train_ds))
predictions = model(feature_batch)
predictions[:5]

すると、以下のようにそれぞれのサンプルに対して出力が得られます。

<tf.Tensor: shape=(5, 3), dtype=float32, numpy=
array([[ 1.0018804 ,  0.52744055,  3.8560932 ],
       [ 1.1096393 , -0.1522671 ,  7.3296337 ],
       [ 0.91566104,  0.53718495,  3.3868132 ],
       [ 1.1310537 , -0.3076315 ,  7.4193387 ],
       [ 0.9660299 , -0.17554164,  6.4465504 ]], dtype=float32)>

ソフトマックス関数に通していないので、それぞれのサンプルは正規化されていません。
しかし、値の順序は決まっているので一番値が大きなもののインデックスを取ることでラベルを得られます。

print(f"Prediction: {tf.argmax(predictions, axis=1)}")
print(f"    Labels: {tf.argmax(label_batch, axis=1)}")
Prediction: [0 0 0 0 0]
    Labels: [2 1 1 0 2]

損失を詳しく設定する

次は損失(loss)を関数で設定します。
loss関数は(model, 特徴量, 正解ラベル)をとるようにします。
lossの内部で予測を行わせて、関数外で定義したloss_objectでロスを計算しその値を返すようにします。

loss_object = tf.keras.losses.CategoricalCrossentropy(from_logits=True)
def loss(model, x, y):
  y_ = model(x)

  return loss_object(y_true=y, y_pred=y_)

sample_y=np.array([1,2,1,1,0])
l = loss(model, feature_batch, label_batch)
print("Loss test: {}".format(l))

今回は損失としてCategoricalCrossentropyを選びます。
ラベルの形式がカテゴリデータでOne-hotの場合はこれを使います。

Prediction:
 [[ 0.887949   -0.63344324 -1.5089586 ]
 [ 0.9441681  -0.755911   -1.2907145 ]]
    Labels:
  [[0. 0. 1.]
 [0. 1. 0.]]

ちなみにカテゴリデータで出力が下のようなラベルになっている場合はSparseCategoricalCrossentropyを使います。
多くのデータではOne-HotされていないのでSparseCategoricalCrossentropyを使うことが多い気がします。

Prediction: [0 0 0 0 0]
    Labels: [2 1 1 0 2]

実際にロスを計算してみます。

l = loss(model, feature_batch, label_batch)
print(f"Loss test: {l}")
Loss test: 1.9186179637908936

ロスを出力することができました。

勾配、最適化部分の設定

GradientTapeを使うことで、学習時の毎回のステップでの詳しい処理を追記できます。
対して高レベルAPIでは引数としてepoch、optimizer、lossを設定するだけでした。
下のようにwith文の中に損失を入れることで、この関数が呼び出されるごとに勾配計算を行ってくれます。
要はwith文内部に微分したい数式を入れれば微分してくれるイメージです。
リターンとしては、計算したロスと計算した勾配の結果を出力します。

def grad(model, inputs, targets):
  with tf.GradientTape() as tape:
    loss_value = loss(model, inputs, targets)
  return loss_value, tape.gradient(loss_value, model.trainable_variables)

次は、実際に上の関数を呼び出すところを書きます。
optimizerを設定し、上で定義したgrad関数を呼び出します。
これにより、lossと勾配が計算されるようになります。
そして、apply_gradientsによって実際に勾配が計算されモデルに適用されます。

optimizer = tf.keras.optimizers.Adam(learning_rate=0.01)
loss_value, grads = grad(model, feature_batch, label_batch)

print(f"Step: {optimizer.iterations.numpy()}, 初期値のLoss: {loss_value.numpy()}")

optimizer.apply_gradients(zip(grads, model.trainable_variables))

print(f"Step: {optimizer.iterations.numpy()},  計算後のLoss: {loss(model, feature_batch, label_batch).numpy()}")
Step: 0, 初期値のLoss: 1.9186179637908936
Step: 1,  計算後のLoss: 1.4329802989959717

実際に学習部分に適用してみる。

データセットをバッチごとに取り出し計算を行います。
また、エポック内で計算したlossとaccuracyを細かく保存していきます。

# グラフ化の際に利用
train_loss_results = []
train_accuracy_results = []
val_loss_results = []
val_accuracy_results = []

num_epochs = 101

# validationデータ
x_val, y_val = next(iter(val_ds))
for epoch in range(num_epochs):
  epoch_loss_avg = tf.keras.metrics.Mean()
  epoch_accuracy = tf.keras.metrics.CategoricalAccuracy()
  epoch_val_loss_avg = tf.keras.metrics.Mean()
  epoch_val_accuracy = tf.keras.metrics.CategoricalAccuracy()

  # 訓練ループ - 8個ずつのバッチを使用
  for x, y in train_ds:
    # モデルの最適化
    loss_value, grads = grad(model, x, y)
    optimizer.apply_gradients(zip(grads, model.trainable_variables))

    # 進捗の記録
    epoch_loss_avg(loss_value)  # 現在のバッチの損失を加算
    epoch_val_loss_avg(loss(model, x_val, y_val))
    # 予測ラベルと実際のラベルを比較
    epoch_accuracy(y, model(x))
    epoch_val_accuracy(y_val, model(x_val))

  # エポックの終わり
  train_loss_results.append(epoch_loss_avg.result())
  train_accuracy_results.append(epoch_accuracy.result())
  val_loss_results.append(epoch_val_loss_avg.result())
  val_accuracy_results.append(epoch_val_accuracy.result())

# 50エポックごとに損失を出力
  if epoch % 50 == 0:
    print(f"Epoch {epoch:03d}: Loss: {epoch_loss_avg.result():.3f}, Accuracy: {epoch_accuracy.result():.3%}, Val Loss: {epoch_val_loss_avg.result():.3f}, Val Accuracy: {epoch_val_accuracy.result():.3%}")
Epoch 000: Loss: 1.100, Accuracy: 43.750%, Val Loss: 0.995, Val Accuracy: 45.208%
Epoch 050: Loss: 0.057, Accuracy: 98.958%, Val Loss: 0.039, Val Accuracy: 98.333%
Epoch 100: Loss: 0.045, Accuracy: 97.917%, Val Loss: 0.047, Val Accuracy: 97.917%

学習状態の可視化

学習でのLossとaccuracyを見ると次のようになっている。

import matplotlib.pyplot as plt
fig, axes = plt.subplots(2, sharex=True, figsize=(12, 8))
fig.suptitle('Training Metrics')

axes[0].set_ylabel("Loss", fontsize=14)
axes[0].plot(train_loss_results)
axes[0].plot(val_loss_results)

axes[1].set_ylabel("Accuracy", fontsize=14)
axes[1].set_xlabel("Epoch", fontsize=14)
axes[1].plot(train_accuracy_results)
axes[1].plot(val_accuracy_results)
plt.show()

f:id:nTeTn:20210722134942p:plain

テストデータでも同様のaccuracyを確認できる。

test_accuracy = tf.keras.metrics.Accuracy()

for (x, y) in test_ds:
  logits = model(x)
  prediction = tf.argmax(logits, axis=1, output_type=tf.int32)
  label = tf.argmax(y, axis=1, output_type=tf.int32)
  test_accuracy(prediction, label)

print("Test set accuracy: {:.3%}".format(test_accuracy.result()))

参考文献

カスタム訓練:ウォークスルー  |  TensorFlow Core

Tensorflow2に改めて入門1(アヤメデータセットでシンプルなモデル作成)

TensorFlow2系の書き方を体系的にまとめたものがあまりなかったので入門記事としてまとめようと思います。 とりあえず動いてかつ機械学習の流れを掴めるというようなゆるい内容です。 基本的にはColab上で動くように想定しています。 Done is better than perfectを目指しているので、とりあえずアップして適宜直していくスタイルでいければと思っています。

環境

Google Colaboratory(通常版)

何をするか

アヤメの花のデータセットを使って3値分類をやります.

データセットのロード

まずはデータセットを作ります.

import numpy as np
import pandas as pd

import tensorflow as tf
from tensorflow.keras import layers
from tensorflow.keras.utils import to_categorical

from sklearn.model_selection import train_test_split
from sklearn.datasets import load_iris

#sklearnのirisデータをロード
iris = load_iris() 
#irisデータの特徴量とラベルを結合
iris_data = np.concatenate((iris.data, iris.target[:, np.newaxis]), axis=1) 

#ラベル名にyを追加
iris.feature_names.append("y") 

#アヤメのデータフレームを作成
dataframe = pd.DataFrame(iris_data, columns=iris.feature_names) 
dataframe.head()

f:id:nTeTn:20210721122418p:plain

訓練データ、評価データ、テストデータ、に分けます。

train, test = train_test_split(dataframe, test_size=0.2)
train, val = train_test_split(train, test_size=0.2)
print(f"訓練データ数:{len(train)}")
print(f"テストデータ数:{len(test)}")
print(f"評価データ数:{len(val)}")

出力:

訓練データ数:96
テストデータ数:30
評価データ数:24

tf.data.Datasetに変換する

tf.dataはデータをモデルの読み込みやすい形に変換できます。 これによって、学習を効率的に進めることができます。

df_to_dataset関数を定義しdfからdatasetに変換します。 内部でデータを分割し、特徴量とラベルに変えています。

def df_to_dataset(dataframe, shuffle=True, batch_size=32):
  dataframe = dataframe.copy()
  labels = dataframe.pop("y")
  labels = to_categorical(labels)
  ds = tf.data.Dataset.from_tensor_slices((dataframe, labels))
  if shuffle:
    ds = ds.shuffle(buffer_size=len(dataframe))
  ds = ds.batch(batch_size)
  return ds

これらをそれぞれ、訓練データ、評価データ、テストデータ、に適用します。

batch_size = 5 
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)

takeメソッドを使ってbatchを確認してみます。 サンプル数5のミニバッチになっており、tf.Tensorに変換されています。

for feature_batch, label_batch in train_ds.take(1):
  print('A batch of ages:\n', feature_batch)
  print('A batch of targets\n:', label_batch )
A batch of ages:
 tf.Tensor(
[[7.4 2.8 6.1 1.9]
 [5.7 3.  4.2 1.2]
 [6.1 3.  4.9 1.8]
 [5.1 2.5 3.  1.1]
 [6.7 3.1 4.4 1.4]], shape=(5, 4), dtype=float64)
A batch of targets:
: tf.Tensor(
[[0. 0. 1.]
 [0. 1. 0.]
 [0. 0. 1.]
 [0. 1. 0.]
 [0. 1. 0.]], shape=(5, 3), dtype=float32)

モデル作成

モデルとして全結合3層ニューラルネットを作成します。 また、モデルに対してcompileを使って最適化手法と損失、評価指標を設定します。 そして、最後にfitでモデルを学習させます。

model = tf.keras.Sequential([
  layers.Dense(128, activation='relu', input_shape=(4,)),
  layers.Dense(128, activation='relu'),
  layers.Dense(3, activation="softmax")
])

model.compile(optimizer='adam',
              loss='categorical_crossentropy',
              metrics=['accuracy'])

model.fit(train_ds, 
          validation_data=val_ds, 
          epochs=5)
Epoch 1/7
20/20 [==============================] - 1s 7ms/step - loss: 1.0765 - accuracy: 0.4688 - val_loss: 0.8818 - val_accuracy: 0.7500
Epoch 2/7
20/20 [==============================] - 0s 2ms/step - loss: 0.8072 - accuracy: 0.7708 - val_loss: 0.7610 - val_accuracy: 0.5833
Epoch 3/7
20/20 [==============================] - 0s 2ms/step - loss: 0.6388 - accuracy: 0.7917 - val_loss: 0.5299 - val_accuracy: 0.9583
Epoch 4/7
20/20 [==============================] - 0s 2ms/step - loss: 0.4845 - accuracy: 0.8750 - val_loss: 0.4420 - val_accuracy: 0.8750
Epoch 5/7
20/20 [==============================] - 0s 2ms/step - loss: 0.4037 - accuracy: 0.8958 - val_loss: 0.3936 - val_accuracy: 0.9167
Epoch 6/7
20/20 [==============================] - 0s 3ms/step - loss: 0.3526 - accuracy: 0.9375 - val_loss: 0.3335 - val_accuracy: 0.8750
Epoch 7/7
20/20 [==============================] - 0s 2ms/step - loss: 0.3129 - accuracy: 0.9479 - val_loss: 0.3135 - val_accuracy: 0.9583

評価

テストデータで評価指標を確認してみます。 テストデータでもしっかりと予測できています。

loss, accuracy = model.evaluate(test_ds)
print("Accuracy", accuracy)
6/6 [==============================] - 0s 2ms/step - loss: 0.0979 - accuracy: 1.0000
Accuracy 1.0

参考文献

カスタム訓練:ウォークスルー  |  TensorFlow Core

TensorFlowで使えるデータセット機能が強かった話 - Qiita

第2回 ニューラルネットワーク最速入門 ― 仕組み理解×初実装(中編):TensorFlow 2+Keras(tf.keras)入門 - @IT