TensorFlow や Numpy を使わずにニューラルネットワークを実装する 回帰編(2)

今回は、前回作成したニューラルネットワークで、回帰問題を学習していきたいと思います。

学習する問題

今回は y = sin(x) という、いわゆるサインカーブを学習(学習のみ)します。入力 x に対して、出力 y の値が sin(x) に近いほど良いということになります。

準備(回帰用Utilクラス)

学習を始める前に、訓練・教師データの作成や、学習経過を表示するための Util クラスを作成します。

import random
import matplotlib.pyplot as plt
import math

from util import linspace

func = lambda x: math.sin(x)

class UtilRegression:
    def __init__(self):
        """コンストラクタ"""
        # 順番に並んでいるx座標のデータ
        self.ordered_x = []
        # 順番に並んでいるy座標のデータ
        self.ordered_y = []

    def sum_of_squared_error(self, teaching, output):
        """二乗和誤差を計算する

        :param teaching: 教師データ
        :param output: 出力値
        :return: 二乗和誤差
        """
        return ((teaching - output) ** 2) / 2

    def create_learning_data(self, start, end, num=50):
        """学習用データを作成する

        :param start: x軸の範囲(開始)
        :param end: y軸の範囲(終了)
        :param num: データ数
        :return: 訓練データのリストと教師データのリスト
        """
        trainings_list, teachings_list = [], []

        self.ordered_x = linspace(start, end, num)
        self.ordered_y = [func(x_) for x_ in self.ordered_x]

        random_x = random.sample(self.ordered_x, len(self.ordered_x))
        random_y = [func(x_) for x_ in random_x]

        for _x, _y in zip(random_x, random_y):
            trainings_list.append([_x])
            teachings_list.append([_y])

        return trainings_list, teachings_list

    def calculate_output_by_current_weights(
        self, input_layer, middle_layer, output_layer
    ):
        """現時点の重みで出力を計算する

        :param input_layer: 入力層インスタンス
        :param middle_layer: 中間層インスタンス
        :param output_layer: 出力層インスタンス
        :return: 出力値
        """
        #
        outputs = []
        for input in self.ordered_x:
            input_layer.set_neurons([input])
            middle_layer.calculate_output(input_layer.neurons)
            output_layer.calculate_output(middle_layer.neurons)
            outputs.append(output_layer.neurons[0])
        return outputs

    def create_result_line_graph(self, calculate_outputs, path):
        """結果の折れ線グラフを作成する

        :param calculate_outputs: 出力値
        :param path: 折れ線グラフを出力するファイルパス
        """
        fig = plt.figure()
        ax = fig.add_subplot(1, 1, 1)
        ax.spines["left"].set(position=("data", 0.0))
        ax.spines["bottom"].set(position=("data", 0.0))

        ax.plot(self.ordered_x, calculate_outputs, linestyle="--", color="b")
        ax.plot(self.ordered_x, self.ordered_y, color="g")
        plt.savefig(path)

ひとつずつ説明します。

    def __init__(self):
        """コンストラクタ"""
        # 順番に並んでいるx座標のデータ
        self.ordered_x = []
        # 順番に並んでいるy座標のデータ
        self.ordered_y = []

コンストラクタですが、学習の途中経過を表示するための変数を定義(初期化)します。これについては訓練・教師データを作成するメソッドと一緒に説明します。

    def sum_of_squared_error(self, teaching, output):
        """二乗和誤差を計算する

        :param teaching: 教師データ
        :param output: 出力値
        :return: 二乗和誤差
        """
        return ((teaching - output) ** 2) / 2

誤差関数は二乗和誤差を利用するので、計算式をそのまま実装しています。

    def create_learning_data(self, start, end, num=50):
        """学習用データを作成する

        :param start: x軸の範囲(開始)
        :param end: x軸の範囲(終了)
        :param num: データ数
        :return: 訓練データのリストと教師データのリスト
        """
        trainings_list, teachings_list = [], []

        self.ordered_x = linspace(start, end, num)
        self.ordered_y = [func(x_) for x_ in self.ordered_x]

        random_x = random.sample(self.ordered_x, len(self.ordered_x))
        random_y = [func(x_) for x_ in random_x]

        for _x, _y in zip(random_x, random_y):
            trainings_list.append([_x])
            teachings_list.append([_y])

        return trainings_list, teachings_list

学習用データを作成するメソッドです。引数はx軸のデータの範囲(開始・終了)とデータ数です。例えば create_learning_data(-3.14, 3.14, 10) ならば、x軸の方向に -π から π の範囲で10個の(訓練)データを等間隔で生成し、それらのデータをfunc()にかけてy軸方向の(教師)データを生成します。

linspace()start から end まで num 個のデータを等間隔で生成するという numpy の linspace メソッドと同様のメソッドです。numpy を使わないという前提だったので、自作した関数で、これについては後述します。

まず、x軸方向のデータとして生成したリストは、まず self.orderd_x(順番に並んだx座標のデータという意味)に格納します。生成した self.orderd_x の値を一つずつ func() (今回は math.sin(x))にかけて、self.orderd_y を生成します。これらは、学習の途中経過を作成するために利用します。

続いて訓練データを作成します。順番に並んでいる self.orderd_x を利用して、順番がランダムな random_x を作成します。この random_xfunc() をかけて random_y を生成します。この random_y が正解=教師データになります。あとは生成した random_x, random_ytrainings_list, teachings_list に詰めているだけです。

    def calculate_output_by_current_weights(
        self, input_layer, middle_layer, output_layer
    ):
        """現時点の重みで出力を計算する

        :param input_layer: 入力層インスタンス
        :param middle_layer: 中間層インスタンス
        :param output_layer: 出力層インスタンス
        :return: 出力値
        """
        #
        outputs = []
        for input in self.ordered_x:
            input_layer.set_neurons([input])
            middle_layer.calculate_output(input_layer.neurons)
            output_layer.calculate_output(middle_layer.neurons)
            outputs.append(output_layer.neurons[0])
        return outputs

こちらは学習の途中経過を表示するために、その時点での重みとバイアスを利用して出力を計算するメソッドです。引数は、結果を表示したい時点での入力層・中間層・出力層のインスタンスで、これらのインスタンスはその時点での重みとバイアスの値を持っています。そこに、順番に並んだx座標のデータ self.orderd_x を入力層に渡して、中間層→出力層と順に出力を計算し、outputs というリストに格納して返します。

    def create_result_line_graph(self, calculate_outputs, path):
        """結果の折れ線グラフを作成する

        :param calculate_outputs: 出力値
        :param path: 折れ線グラフを出力するファイルパス
        """
        fig = plt.figure()
        ax = fig.add_subplot(1, 1, 1)
        ax.spines["left"].set(position=("data", 0.0))
        ax.spines["bottom"].set(position=("data", 0.0))

        ax.plot(self.ordered_x, calculate_outputs, linestyle="--", color="b")
        ax.plot(self.ordered_x, self.ordered_y, color="g")
        plt.savefig(path)

どれくらい学習ができているのか途中経過を描画するためのメソッドです。引数 calculate_outputscalculate_output_by_current_weights() の計算結果で、path は結果ファイルを出力するパスです。ここでは matplotlib の解説は割愛します。現時点でのニューラルネットワークで計算した出力による曲線と、正解のサインカーブの2つを描画して、ファイルに出力しています。

その他メソッド

他に利用するメソッドです。

def create_line_graph(x, y, file_name):
    """折れ線グラフを作成する

    :param x: x座標の値(リスト)
    :param y: y座標の値(リスト)
    :param file_name: 出力するファイル名
    """
    fig = plt.figure()
    ax = fig.add_subplot(1, 1, 1)

    ax.plot(x, y, linestyle="--", color="b")
    plt.savefig(file_name)

こちらは学習が進むにつれて、誤差がどのように変化するのか折れ線グラフで描画するために利用します。

def linspace(start, end, num):
    """等差数列を作成する numpy.linspace と同様の関数

    :param start: 数列の始まりの値
    :param end: 数列の終わりの値
    :param num: 数列の要素数
    :return: 等差数列
    """
    num -= 1
    incremental = (end - start) / num

    value = start
    values = [value]
    for _ in range(num):
        value += incremental
        values.append(value)

    return values

前述した numpy の linspace と同様の関数を自作したものです。

学習

それでは実際に学習していきたいと思います。

pi = 3.141592   # 円周率

if __name__ == "__main__":
    input_num = 1
    middle_num = 15
    output_num = 1

    input_layer = InputLayer(input_num)
    middle_layer = MiddleLayer(middle_num, input_num)
    output_layer = OutputLayer(output_num, middle_num)

    util = UtilRegression()
    trainings_list, teachings_list = util.create_learning_data(-pi, pi, 20000)

    indexes, errors = [], []
    index = 0

    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 % 2000 == 0:
            indexes.append(index)
            error = util.sum_of_squared_error(teachings[0], output_layer.neurons[0])
            errors.append(error)

            # 現時点の重みで計算してみる
            calculate_outputs = util.calculate_output_by_current_weights(
                input_layer, middle_layer, output_layer
            )
            util.create_result_line_graph(calculate_outputs, f"png/{index}.png")

    # 誤差の推移を出力する
    create_line_graph(indexes, errors, "png/result.png")

一つずつ説明します。

if __name__ == "__main__":
    input_num = 1   # 入力層のニューロン数
    middle_num = 15 # 中間層のニューロン数
    output_num = 1  # 出力層のニューロン数

    input_layer = InputLayer(input_num)
    middle_layer = MiddleLayer(middle_num, input_num)
    output_layer = OutputLayer(output_num, middle_num)

    util = UtilRegression()
    trainings_list, teachings_list = util.create_learning_data(-pi, pi, 20000)

    indexes, errors = [], []
    index = 0

まずは、各層のニューロン数を定義します。入力はx座標の1つだけ、出力はy座標の1つだけなので、入力層・出力層のニューロン数は1になります。中間層は良い結果が出そうな値を設定します。その後、各層のインスタンスを生成します。回帰用 Util クラスのインスタンスを生成、訓練・教師データを生成し、利用する変数を初期化しています。

    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 % 2000 == 0:
            indexes.append(index)
            error = util.sum_of_squared_error(teachings[0], output_layer.neurons[0])
            errors.append(error)

            # 現時点の重みで計算してみる
            calculate_outputs = util.calculate_output_by_current_weights(
                input_layer, middle_layer, output_layer
            )
            util.create_result_line_graph(calculate_outputs, f"png/{index}.png")

    # 誤差の推移を出力する
    create_line_graph(indexes, errors, "png/result.png")

学習の途中、一定間隔で誤差を計算しリストに格納します。また、その時点でのネットワークの重みで出力を計算し、結果を描画します。 そして、最後に誤差の推移を描画します。

学習結果

今回は、20000個の学習データを生成したので、学習回数は20000回になります。途中経過は2000回毎に出力するようにしました。この設定で出力した結果は下記のようになりました。まずは、誤差の推移です。

学習回数2500回手前までで、誤差が大きく減少しており、その後はあまり変化が無い様子がわかります。

続いて、学習の経過です。上から順に学習前・4000回・8000回・12000回・16000回・20000回学習後の結果です。 青の破線がニューラルネットワークで学習した結果、緑の実線が正解のサインカーブです。

学習前は何でも無いぐにゃっとした直線のようなものが、学習を重ねるにつれて、正解のサインカーブに近づいていってることがわかります。

次回は分類問題を学習していきます。