TensorFlow や Numpy を使わずにニューラルネットワークを実装する 分類編(2)
今回は、前回準備した分類問題用のニューラルネットワークで、実際に、分類問題を学習していきたいと思います。
学習する問題
水平方向にx軸、垂直方向にy軸というxy平面を考えます。x軸方向は-3から3まで、y軸方向は-1から1までとし、このxy平面に等間隔に配置された400個の点(座標)を用意します。このxy平面を y = sin(x)
という、いわゆるサインカーブで2つの領域(サインカーブの上か下か)に区切ります。400個の点(座標)がこの2つの領域のどちらにあるのかを学習していきます。文字だけではイメージが湧きにくいかと思いますので、学習前の点(座標)が分類されていない状態の図を用意しました。今回も学習のみになります。
準備(分類用ユーティリティ関数)
学習を始める前に、訓練・教師データの作成や、学習経過を表示するための関数を作成します。
import math import random import matplotlib.pyplot as plt target_func = lambda x: math.sin(x) def cross_entropy_error(outputs, teachings): """クロスエントロピー誤差を計算する :param teaching: 教師データ :param output: 出力値 :return: クロスエントロピー誤差 """ sum = 0.0 for output, teaching in zip(outputs, teachings): big = max([output, 1e-7]) sum += teaching * math.log(big) return -sum def create_learning_data(): """学習用データを作成する :return: 訓練データのリストと教師データのリスト """ x = linspace(-3, 3, 20) y = linspace(-1, 1, 20) random.shuffle(x) random.shuffle(y) trainings_list = [] teachings_list = [] for _x in x: value = target_func(_x) for _y in y: trainings_list.append([_x, _y]) teaching = [1, 0] if _y >= value else [0, 1] teachings_list.append(teaching) return trainings_list, teachings_list def calculate_output_by_current_weights( inputs_list, input_layer, middle_layer, output_layer ): """現時点の重みで出力を計算する :param inputs_list: 入力値のリスト :param input_layer: 入力層インスタンス :param middle_layer: 中間層インスタンス :param output_layer: 出力層インスタンス :return: 出力値のリスト """ outputs_list = [] for inputs in inputs_list: input_layer.set_neurons(inputs) middle_layer.calculate_output(input_layer.neurons) output_layer.calculate_output(middle_layer.neurons) outputs_list.append(output_layer.neurons) return outputs_list def create_result_distribution_chart(inputs_list, outputs_list, path): """結果の散布図を作成する :param inputs_list: 入力値のリスト :param outputs_list: 出力値のリスト :param path: 散布図を出力するファイルパス """ fig = plt.figure() ax = fig.add_subplot(1, 1, 1) 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) ax.scatter(x1, y1, s=20, c="blue", marker="*") ax.scatter(x2, y2, s=20, c="green") # 境界となる線を書く pi = 3.141592 # 円周率 x = linspace(-pi, pi, 100) y = [target_func(_x) for _x in x] ax.plot(x, y, color="orange") plt.savefig(path)
一つ一つ説明していきます。
def cross_entropy_error(outputs, teachings): """クロスエントロピー誤差を計算する :param teaching: 教師データ :param output: 出力値 :return: クロスエントロピー誤差 """ sum = 0.0 for output, teaching in zip(outputs, teachings): big = max([output, 1e-7]) sum += teaching * math.log(big) return -sum
分類問題では、誤差関数としてクロスエントロピー誤差を利用するので、その実装になります。数式通りの実装ですが、オーバーフロー対策がしてあります。
def create_learning_data(): """学習用データを作成する :return: 訓練データのリストと教師データのリスト """ x = linspace(-3, 3, 20) y = linspace(-1, 1, 20) random.shuffle(x) random.shuffle(y) trainings_list = [] teachings_list = [] for _x in x: value = target_func(_x) for _y in y: trainings_list.append([_x, _y]) teaching = [1, 0] if _y >= value else [0, 1] teachings_list.append(teaching) return trainings_list, teachings_list
学習用に訓練データと教師データを作成する関数です。x軸方向に-3から3まで等間隔に20個の点(座標)を生成し、そのx軸の点(座標)それぞれに対してy軸方向に-1から1まで等間隔に20個の点(座標)を生成します。すなわち格子状に等間隔に20 × 20 = 400個の点(座標)を作成するイメージです。順番はランダムにしておきます。生成した点(座標)のy座標と、x座標を target_func()
(今回は sin(x)
)に通した値とを比較(つまり、サインカーブの上にあるか下にあるか)して、教師データを生成します。点(座標)(x, y) がサインカーブより上にあれば、教師データとして[1, 0]
を生成し、下にあれば[0, 1]
を生成します。これらのデータを trainings_list
, teachings_list
に詰めて返します。
def create_result_distribution_chart(inputs_list, outputs_list, path): """結果の散布図を作成する :param inputs_list: 入力値のリスト :param outputs_list: 出力値のリスト :param path: 散布図を出力するファイルパス """ fig = plt.figure() ax = fig.add_subplot(1, 1, 1) 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) ax.scatter(x1, y1, s=20, c="blue", marker="*") ax.scatter(x2, y2, s=20, c="green") # 境界となる線を書く pi = 3.141592 # 円周率 x = linspace(-pi, pi, 100) y = [target_func(_x) for _x in x] ax.plot(x, y, color="orange") plt.savefig(path)
学習の途中経過を散布図で出力するための関数です。matplotlib の説明はここではしません。inputs_list
は入力値のリスト(つまり点(座標))、outputs_list
は出力値のリストで calculate_output_by_current_weights()
で計算・出力された値になります。forループでoutputs_list
の中身を取り出して、ニューラルネットワークがどちらに分類したのかを判断していきます。教師データをサインカーブより上にあれば[1, 0]
、下にあれば[0, 1]
というように生成したので、outputs[0] >= outputs[1]
ならば、入力データ点(座標)(x, y)はサインカーブより上、outputs[0] < outputs[1]
ならばサインカーブより下、とニューラルネットワークが判定したことになります。入力値のx座標・y座標を2つのグループ、上グループ(x1, y1)と 下グループ(x2, y2) に振り分けていきます。そして、この2グループを散布図として描画し、わかりやすいように境界となるサインカーブも合わせて描画して、ファイルに出力します。
学習
それでは実際に学習していきます。
if __name__ == "__main__": input_num = 2 # 入力層のニューロン数 middle_num = 20 # 中間層のニューロン数 output_num = 2 # 出力層のニューロン数 loop_num = 250 # ループ回数 -> 学習回数は loop_num * 学習データ数 になる input_layer = InputLayer(input_num) middle_layer = MiddleLayer(middle_num, input_num) output_layer = OutputLayer(output_num, middle_num) trainings_list, teachings_list = create_learning_data() indexes, errors = [], [] index = 0 for _ in range(loop_num): for trainings, teachings in zip(trainings_list, teachings_list): # 順伝播 input_layer.set_neurons(trainings) middle_layer.calculate_output(input_layer.neurons) output_layer.calculate_output(middle_layer.neurons) # 逆伝播 output_layer.calculate_delta(teachings) output_layer.update_weights(middle_layer.neurons) output_layer.update_biases() middle_layer.calculate_delta(output_layer.deltas, output_layer.weights) middle_layer.update_weights(input_layer.neurons) middle_layer.update_biases() index += 1 # 途中結果保存 if index == 1 or index % 10000 == 0: indexes.append(index) error = cross_entropy_error(output_layer.neurons, teachings) errors.append(error) # 現時点の重みで計算し結果を散布図にする outputs_list = calculate_output_by_current_weights( trainings_list, input_layer, middle_layer, output_layer ) create_result_distribution_chart( trainings_list, outputs_list, f"png/{index}.png" ) # 誤差の推移を出力する create_line_graph(indexes, errors, "png/result.png")
前々回の回帰問題とほとんど変わらないソースなので、違う部分のみを記載します。まずは各層のニューロン数ですが、入力は点のx座標とy座標なので2つ、出力は、対象の点(座標)がサインカーブの上か下かどちらに分類したのかを表すものなので2つ、中間層は良い結果が出そうな値(今回は20)を設定します。前々回の回帰問題では生成した学習用データの個数分だけ学習が行われましたが、今回の分類問題では、学習用データが400個しか生成されないため、それをそのまま使うだけでは学習が足りません。そのため、学習データを繰り返し使うためのforループを用意し、その繰り返し回数を loop_num
で制御しています。また、誤差関数はクロスエントロピー誤差を利用し、学習の途中経過を散布図で描画していきます。
学習結果
今回の学習回数は、学習データ 400 × ループ回数 250 = 100000回になります。途中経過は10000回毎に出力しています。この設定で学習結果は下記の通りになりました。まずは誤差の推移です。
学習回数10000回くらいまでで、誤差がガクッと落ち、その後も少しずつ誤差が小さくなっていくのがわかります。
続いて学習の経過です。上から順に、学習前・20000万回・40000万回・60000万回・80000万回・100000万回学習した時点のニューラルネットワークによる分類結果です。
学習前は緑色の丸のみで、何も分類されていません。20000万回学習後だとかなり分類されていますが、x座標が-3や3のような端っこは、サインカーブの下に青の星があったり、サインカーブの上に緑の丸があったりします。その後学習が進むにつれて、x座標-3あたりのサインカーブ下の青い星が、緑の丸に変わっていき、x座標3あたりのサインカーブ上の緑の丸が、青い星に変わっていく様子がわかります。このように学習が進むにつれ、少しずつ正解に近づいていく様子がわかります。