logo

TensorFlow-Kerasでシーケンスモデル、関数型APIでMLPを定義してトレーニングする方法 📂機械学習

TensorFlow-Kerasでシーケンスモデル、関数型APIでMLPを定義してトレーニングする方法

概要

TensorFlowでは、Kerasを使用して簡単にニューラルネットワークを定義することができます。以下では、Sequential()と関数型APIを使用してシンプルなMLPを定義し、訓練する方法を紹介します。ただし、Sequential()はモデルの定義自体は簡単ですが、それを使用して複雑な構造を設計するには適していません。同様に、関数型APIを使用して複雑な構造を設計する場合は、keras.Modelクラスの使用が適しており、より複雑で自由なカスタマイズを求める場合は、Kerasを使用せずに低レベルで実装する方が良いでしょう。どのような作業にディープラーニングを使用するかによって異なりますが、もし自分が理工学の研究者であり、専門分野にディープラーニングを応用したい場合は、以下の方法を主に使用する可能性は低いでしょう。ディープラーニングを初めて学び、実践する際は、「これが使用法だ」と感じ取る程度だと考えられます。

シーケンシャルモデル

モデル定義

サイン関数 $\sin : \mathbb{R} \to \mathbb{R}$ の近似のために、入力と出力の次元が1のMLPを次のように定義しましょう。

import tensorflow as tf
from tensorflow.keras import Sequential
from tensorflow.keras.layers import Dense

# モデル定義
model = Sequential([Dense(10, input_dim = 1, activation = "relu"),
                    Dense(10, input_dim = 10, activation = "relu"),
                    Dense(1, input_dim = 10)])
model.summary() # output↓
# Model: "sequential_3"
# _________________________________________________________________
# Layer (type)                Output Shape              Param #   
# =================================================================
# dense_9 (Dense)             (None, 10)                20        
#                                                                 
# dense_10 (Dense)            (None, 10)                110       
#                                                                 
# dense_11 (Dense)            (None, 1)                 11        
#                                                                 
# =================================================================
# Total params: 141
# Trainable params: 141
# Non-trainable params: 0
# _________________________________________________________________

keras.layers.Dense()の特徴の一つに、入力の次元を記述する必要がないという点があります。なぜこのような許容がされているのかは分かりませんが、コードの可読性のためには(特に他の人が見る可能性があるコードであれば)入力の次元を明示的に記述することが良いでしょう。このために、出力の次元が左、入力の次元が右に記述されるという特徴があります。したがって、モデルの構造を読むためには、アラビア語ではなく、右から左に読む必要があります。もし線形層を線形変換としての行列と考えた場合、$\mathbf{y} = A\mathbf{x}$ なので、入力が右、出力が左に来るのが自然です。しかし、TensorFlowはこのような数学的な厳密さを考慮して設計された言語ではないので、この理由だけでそう設計されたとは考えにくいです。数学的な厳密さを非常に重視するJuliaでも、線形層は Dense(in, out) のように実装されています。これは、左から右へ読む方が便利で分かりやすいためです。元々、$X$ から $Y$ への関数 $f$ の記述自体が $f : X \to Y$ であり、(Kerasを除いて)世界のどこにも右から左へのマッピングで記述される関数はありません。

データ生成

サイン関数を訓練するため、データをサイン関数の関数値とし、モデルの出力とサイン関数のグラフを比較すると以下のようになります。

# データ生成
from math import pi

x = tf.linspace(0., 2*pi, num=1000)    # 入力データ
y = tf.sin(x)                          # 出力データ(label)

# モデルの出力確認
import matplotlib.pyplot as plt

plt.plot(x, model(x), label="model")
plt.plot(x, y, label="sin")
plt.legend()
plt.show()

訓練及び結果

from tensorflow.keras.optimizers import Adam

model.compile(optimizer=Adam(learning_rate=0.001), loss='mse')
  • model.compile(optimizer, loss, metric)

.compile() メソッドでオプティマイザと損失関数を指定します。他の主要なオプションには metric があり、これはモデルを評価する関数を意味します。これは loss と同じになることもありますし、異なることもあります。例えば、MLPで MNISTデータセット を学習する場合、lossは出力とラベルのMSEであり、metricは全データの中で予測に成功した割合になるでしょう。

> model.fit(x, y, epochs=10000, batch_size=1000, verbose='auto')
.
.
.
Epoch 9998/10000
1/1 [==============================] - 0s 8ms/step - loss: 6.2260e-06
Epoch 9999/10000
1/1 [==============================] - 0s 4ms/step - loss: 6.2394e-06
Epoch 10000/10000
1/1 [==============================] - 0s 3ms/step - loss: 6.2385e-06
  • .fit() メソッドに入力とラベル、エポック数、バッチサイズなどを入力すると訓練が実行されます。verboseは訓練の進行状況をどのように表示するかを決めるオプションで、0、1、2の中から選択でき、0は何も表示しません。他のオプションは以下のフォーマットで表示されます。
# verbose=1
Epoch (現在のエポック)/(全エポック)
(現在のバッチ)/(全体のバッチ) [==============================] - 0s 8ms/step - loss: 0.7884

# verbose=2
Epoch (現在のエポック)/(全エポック)
(現在のバッチ)/(全体のバッチ) - 0s - loss: 0.7335 - 16ms/epoch - 8ms/step

訓練が終わり、サイン関数とモデルの関数値を比較すると、学習がうまく行われたことがわかります。

関数型API

Input() 関数と Model() 関数でレイヤーを直接連結する方法です。MLPのようなシンプルなモデルであれば、上記のシーケンシャルモデルで定義する方がはるかに簡単です。上のシーケンシャルモデルで定義したニューラルネットワークと同じ構造のモデルを定義する方法は次のようになります。

from tensorflow.keras import Model
from tensorflow.keras.layers import Input, Dense

input = Input(shape=(10)) # 変数は "出力の次元 = 最初の層の入力の次元"
dense1 = Dense(10, activation = "relu")(input)
dense2 = Dense(10, activation = "relu")(dense1)
output = Dense(1)(dense2)

model = Model(inputs=input, outputs=output)
model.summary() # output↓
# Model: "model_10"
# _________________________________________________________________
#  Layer (type)                Output Shape              Param #
# =================================================================
#  input_13 (InputLayer)       [(None, 1)]               0
# 
#  dense_19 (Dense)            (None, 10)                20
# 
#  dense_20 (Dense)            (None, 10)                110
# 
#  dense_21 (Dense)            (None, 1)                 11
# 
# =================================================================
# Total params: 141
# Trainable params: 141
# Non-trainable params: 0
# _________________________________________________________________

Inputはインプットレイヤーを定義する関数です。正確にはレイヤーではなくテンソルですが、重要な点ではないので、ただの入力層として受け入れても良いでしょう。混乱する点は、出力の次元を入力する必要があるという点です。つまり、最初の層の入力の次元を入力する必要があります。これを定義した後、Dense関数の入力として入力し、明示的に直接各層を連結します。最後に、Model関数で入力と出力を引数に入れると、モデルを定義することができます。

その後、モデルを .compile() メソッドでコンパイルし、.fit() メソッドで訓練するプロセスは、上で紹介した通りです。

環境

  • OS: Windows11
  • Version: Python 3.9.13, tensorflow==2.12.0, keras==2.12.0