TensorFlow・Keras を使って分類問題を学習する

今回は前回に引き続き、TensorFlow・Keras を用いて分類問題を学習していきたいと思います。設定や学習する問題は、前回の分類問題とできるだけ同じになるようにします。

学習する問題

前回の分類問題と同じ問題です。xy平面上に等間隔に400個の点を配置し、xy平面をサインカーブ2つの領域に区切ります。400個の点がどちらの領域に属しているのかを学習します。前回の分類問題で詳しく説明していますので、参照してください。

設定

入力層・中間層・出力層の3層からなるネットワークにします。各種設定は下記の通りです。

環境

前回の記事で回帰問題をkerasで学習したときと同じく TensorFlow の Docker イメージ tensorflow/tensorflow:latest-jupyter でコンテナを立ち上げ、jupyter notebook 上で動作させています。

プログラムソース

今回もまずは全ソースを貼ります。

import os
import shutil
import random

import matplotlib.pyplot as plt
import numpy as np
from keras.models import Sequential, load_model
from keras.layers import Dense
from keras.callbacks import ModelCheckpoint
%matplotlib inline

def create_result_distribution_chart(inputs_list, outputs_list):
    """結果の散布図を作成する

    :param inputs_list: 入力値のリスト
    :param outputs_list: 出力値のリスト
    :param path: 散布図を出力するファイルパス
    """
    x1, y1, x2, y2 = [], [], [], []

    for inputs, outputs in zip(inputs_list, outputs_list):
        _x, _y = inputs
        # どちらに判定されているかで分類する
        if outputs[0] >= outputs[1]:
            x1.append(_x)
            y1.append(_y)
        else:
            x2.append(_x)
            y2.append(_y)

    plt.scatter(x1, y1, s=20, c="blue", marker="*")
    plt.scatter(x2, y2, s=20, c="green")

    # 境界となる線を書く
    pi = 3.141592  # 円周率
    x = np.linspace(-pi, pi, 100)
    y = [target_func(_x) for _x in x]
    plt.plot(x, y, color="orange")

# 設定
split_num = 20      # np.linspace で何分割にするか
neuron_num = 20     # 中間層のニューロン数
batch_size = 1      # バッチサイズ
epoch = 250         # エポック数

target_func = lambda x: np.sin(x)       # 境界線となる関数(サインカーブ)
model_dir = "/root/practice/model/"     # 保存したモデルファイルを格納するディレクトリ

# ディレクトリを空にする
shutil.rmtree(model_dir)
os.makedirs(model_dir, exist_ok=True)

# 訓練・教師データ作成
trainings, teachings = [], []

x = np.linspace(-3, 3, split_num)
y = np.linspace(-1, 1, split_num)
random.shuffle(x)
random.shuffle(y)

for _x in x:
    value = target_func(_x)

    for _y in y:
        teaching = [1, 0] if _y >= value else [0, 1]
        trainings.append([_x, _y])
        teachings.append(teaching)

# モデル作成
model = Sequential()
model.add(Dense(neuron_num, activation='sigmoid', input_shape=(2,)))
model.add(Dense(2, activation='softmax'))
model.compile(optimizer='SGD', loss='categorical_crossentropy')

checkpoint = ModelCheckpoint(os.path.join(model_dir, "{epoch:03d}.hdf5"), verbose=0)

# 学習
history = model.fit(
    trainings,
    teachings,
    batch_size=batch_size,
    epochs=epoch,
    verbose=1,
    callbacks=[checkpoint]
)

# 結果表示
plt.plot(history.history["loss"])

# 予測
predicts = model.predict(trainings, 1, verbose=1)
create_result_distribution_chart(trainings, predicts)

一つ一つ説明します。

import os
import shutil
import random

import matplotlib.pyplot as plt
import numpy as np
from keras.models import Sequential, load_model
from keras.layers import Dense
from keras.callbacks import ModelCheckpoint
%matplotlib inline

必要なライブラリのインポートと、matprotlib のグラフをインラインで表示させる記述です。

def create_result_distribution_chart(inputs_list, outputs_list):
    """結果の散布図を作成する

    :param inputs_list: 入力値のリスト
    :param outputs_list: 出力値のリスト
    :param path: 散布図を出力するファイルパス
    """
    x1, y1, x2, y2 = [], [], [], []

    for inputs, outputs in zip(inputs_list, outputs_list):
        _x, _y = inputs
        # どちらに判定されているかで分類する
        if outputs[0] >= outputs[1]:
            x1.append(_x)
            y1.append(_y)
        else:
            x2.append(_x)
            y2.append(_y)

    plt.scatter(x1, y1, s=20, c="blue", marker="*")
    plt.scatter(x2, y2, s=20, c="green")

    # 境界となる線を書く
    pi = 3.141592  # 円周率
    x = np.linspace(-pi, pi, 100)
    y = [target_func(_x) for _x in x]
    plt.plot(x, y, color="orange")

まずは、予測結果を散布図で描画する関数を定義します。前回、分類問題を学習したときに定義した関数と同じ関数なので、詳細は前回、分類問題を学習したときの記事を参照してください。引数 inputs_list に訓練データを、outputs_listニューラルネットワークの出力を渡せば、学習結果を表示させることができます。

# 設定
split_num = 20      # numpy.linspace で何分割にするか
neuron_num = 20     # 中間層のニューロン数
batch_size = 1      # バッチサイズ
epoch = 250         # エポック数
target_func = lambda x: np.sin(x)       # 境界線となる関数(サインカーブ)
model_dir = "/root/practice/model/"     # 保存したモデルファイルを格納するディレクトリ

# ディレクトリを空にする
shutil.rmtree(model_dir)
os.makedirs(model_dir, exist_ok=True)

各種設定です。前回の分類問題となるべく同じになるように設定します。numpy.linspace による分割数を20(訓練データ数は20×20=40000)、中間層のニューロン数を20、オンライン学習なのでバッチサイズを1、エポック数を250とします。(総学習回数は訓練データ数40000 × エポック数250 = 100万回となる)

# 訓練・教師データ作成
trainings, teachings = [], []

x = np.linspace(-3, 3, split_num)
y = np.linspace(-1, 1, split_num)
random.shuffle(x)
random.shuffle(y)

for _x in x:
    value = target_func(_x)

    for _y in y:
        teaching = [1, 0] if _y >= value else [0, 1]
        trainings.append([_x, _y])
        teachings.append(teaching)

訓練・教師データを生成します。作り方は、前回分類問題を学習したときと同じです。

# モデル作成
model = Sequential()
model.add(Dense(neuron_num, activation='sigmoid', input_shape=(2,)))    // (1)
model.add(Dense(2, activation='softmax'))   // (2)
model.compile(optimizer='SGD', loss='categorical_crossentropy')   // (3)

モデルを作成します。(1)の中間層・入力層ですが、中間層のニューロン数は neuron_num なので20、活性化関数 activationシグモイド関数を設定します。入力の形状 input_shape は x座標とy座標の2つなので、(2,) となります。(2) の出力層は、出力は2つなのでニューロン数は2、活性化関数 activation はソフトマックス関数を設定します。(3) の model の設定では、最適化アルゴリズム optimizer確率的勾配降下法(Stochastic Gradient Descent)を、損失関数 loss にクロスエントロピー誤差を設定します。

checkpoint = ModelCheckpoint(os.path.join(model_dir, "{epoch:03d}.hdf5"), verbose=0)

# 学習
history = model.fit(
    trainings,
    teachings,
    batch_size=batch_size,
    epochs=epoch,
    verbose=1,
    callbacks=[checkpoint]
)

fit メソッドで学習を実行します。引数に、訓練・教師データやバッチサイズなど、もろもろを渡して実行します。

# 結果表示
plt.plot(history.history["loss"])

# 予測
predicts = model.predict(trainings, 1, verbose=1)
create_result_distribution_chart(trainings, predicts)

誤差の推移をグラフで表示し、学習後のニューラルネットワークを利用して、入力座標がサインカーブの上か下かのどちらに属しているかを予測をします。その結果を、冒頭で定義したcreate_result_distribution_chart() を利用して散布図で表示します。

結果

結果は下記の通りになりました。まずは誤差の推移です。    縦軸が誤差、横軸がエポックです。順調に誤差が減少していくのがわかります。

続いて、学習後のニューラルネットワークで予測した結果です。   x座標の端の方、-3や3のあたりは完全に分類できていませんが、ほとんどの点(座標)で正しく分類できていることがわかります。

学習途中のニューラルネットワークで予測した結果も表示します。下記のソースで散布図を表示します。

# 途中結果表示用
target_model = load_model(os.path.join(model_dir, "001.hdf5"))
target_predicts = target_model.predict(trainings, 1, verbose=1)
create_result_distribution_chart(trainings, target_predicts)

上から順に1エポック終了後、100エポック終了後、200エポック終了後のニューラルネットワークが予測した結果になります。

1エポック終了後では、全然学習が足りず、多くの点(座標)でうまく分類できていません。それから学習が進むにつれて、点(座標)がうまく分類できていくことがわかります。200エポック終了後だと、250エポック終了後とほぼ変わらない結果が出ていることがわかります。