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

Udemy の講座ニューラルネットワークを学んでいたのですが、Numpy を使わずにニューラルネットワークを実装してみたい、と思ったので実装してみました。ここでは、ニューラルネットワーク誤差逆伝播法の解説はしません。ニューラルネットワーク誤差逆伝播法について詳しく知りたい方は、この記事の最後にリンクを貼っておきますので、リンク先をご参照ください。

設定

まずは回帰問題用のニューラルネットワークです。入力層・中間層・出力層の3層からなるネットワークを考えます。図にすると下記のようなネットワークです。

設定は下記の通りです。

  • 活性化関数(中間層):シグモイド関数
  • 活性化関数(出力層):恒等関数
  • 誤差:二乗和誤差
  • 学習:オンライン学習

クラスとインスタンス変数

入力層・中間層・出力層でそれぞれクラスを作成します。3つのクラスのインスタンス変数は、だいたい同じになります。先に紹介しておいた方がわかりやすいと思いますので、中間層のクラスを元に紹介します。

class MiddleLayer:
    """中間層"""

    def __init__(self, current_num, previous_num):
        """コンストラクタ

        :param current_num: この層(中間層)のニューロン数
        :param previous_num: 一つ前の層(入力層)のニューロン数
        """
        # この層のニューロン数
        self.num_of_neurons = current_num
        # 各ニューロンの出力値
        self.neurons = []
        # 一つ前の層(入力層)のニューロンとの間の重み
        self.weights = initialize_weights(previous_num, current_num)
        # 各ニューロンのバイアス値
        self.biases = initialize_biases(self.num_of_neurons)
        # 各ニューロンのデルタの値(誤差逆伝播法で利用)
        self.deltas = []

先ほどの図の中間層部分と、インスタンス変数を対応させたものが下記になります。

self.neurons, self.biases, self.deltas の配列の添字はニューロンの番号で、0から数えます。self.weights の配列の添字は、self.weights[一つ前の層のニューロン番号][この層のニューロン番号]で、この2つのニューロンの間の重みが入っています。

入力層

まずは、入力層のクラスから説明します。

class InputLayer:
    """入力層"""

    def __init__(self, num_of_neurons):
        """コンストラクタ

        :param num_of_neurons: 入力層のニューロン数
        """
        # この層のニューロン数
        self.num_of_neurons = num_of_neurons
        # 各ニューロンの出力値
        self.neurons = []

    def set_neurons(self, inputs):
        """ニューロンの出力値(=入力値)を設定する

        :param inputs: 入力値のリスト
        """
        self.neurons = inputs

入力層クラスは単純で、インスタンス作成時にニューロン数を設定し、set_neurons() で入力値のリストを渡して設定します。

出力層

次に中間層を飛ばして、先に出力層のクラスです。

learning_factor = 0.1

class OutputLayer:
    """出力層"""

    def __init__(self, current_num, previous_num):
        """コンストラクタ

        :param current_num: この層(中間層)のニューロン数
        :param previous_num: 一つ前の層(入力層)のニューロン数
        """
        # この層のニューロン数
        self.num_of_neurons = current_num
        # 各ニューロンの出力値
        self.neurons = []
        # 一つ前の層(中間層)のニューロンとの間の重み
        self.weights = initialize_weights(previous_num, current_num)
        # 各ニューロンのバイアス値
        self.biases = initialize_biases(self.num_of_neurons)
        # 各ニューロンのデルタの値(誤差逆伝播法で利用)
        self.deltas = []

    def calculate_output(self, previous_outputs):
        """各ニューロンの出力値を計算する

        :param previous_outputs: 中間層の出力値のリスト
        """
        self.neurons = []

        for current_index in range(self.num_of_neurons):
            # 重み付き線形和を求める
            sum = 0.0
            for previous_index, previous_output in enumerate(previous_outputs):
                sum += previous_output * self.weights[previous_index][current_index]
            sum += self.biases[current_index]

            # 活性化関数にかける
            output = self.activation_function(sum)
            self.neurons.append(output)

    def calculate_delta(self, teachings):
        """出力層各ニューロンのデルタの値を計算する

        :param teachings: 教師データのリスト
        """
        # 出力層のデルタの値を返す
        self.deltas = []
        for index in range(self.num_of_neurons):
            derivative_value = self.derivative_function(self.neurons[index])
            delta = (self.neurons[index] - teachings[index]) * derivative_value
            self.deltas.append(delta)

    def update_weights(self, previous_outputs):
        """重みを更新する

        :param previous_outputs: 一つ前の層(入力層)の出力値のリスト
        """
        previous_num = len(previous_outputs)

        for current_index in range(self.num_of_neurons):
            for previous_index in range(previous_num):
                tmp = (
                    learning_factor
                    * self.deltas[current_index]
                    * previous_outputs[previous_index]
                )
                self.weights[previous_index][current_index] -= tmp

    def update_biases(self):
        """バイアスを更新する"""
        for index in range(self.num_of_neurons):
            self.biases[index] -= learning_factor * self.deltas[index]

    def activation_function(self, value):
        """活性化関数(恒等関数)

        :param value: 入力値
        :return: 活性化関数(恒等関数)にかけた値
        """
        return identity_function(value)

    def derivative_function(self, value):
        """活性化関数(恒等関数)の導関数

        :param value: 入力値
        :return: 活性化関数(恒等関数)の導関数にかけた値
        """
        return 1

一つ一つ説明します。

    def __init__(self, current_num, previous_num):
        """コンストラクタ

        :param current_num: この層(出力層)のニューロン数
        :param previous_num: 一つ前の層(中間層)のニューロン数
        """
        # この層のニューロン数
        self.num_of_neurons = current_num
        # 各ニューロンの出力値
        self.neurons = []
        # 一つ前の層(中間層)のニューロンとの間の重み
        self.weights = initialize_weights(previous_num, current_num)
        # 各ニューロンのバイアス値
        self.biases = initialize_biases(self.num_of_neurons)
        # 各ニューロンのデルタの値(誤差逆伝播法で利用)
        self.deltas = []

まず、コンストラクタでは、引数として、出力層のニューロンcurrent_num と中間層のニューロンprevious_num をそれぞれ受け取ります。initialize_weights()initialize_biases() は重みとバイアスを初期化する関数で、後ほど説明します。

    def calculate_output(self, previous_outputs):
        """各ニューロンの出力値を計算する

        :param previous_outputs: 中間層の出力値のリスト
        """
        self.neurons = []

        for current_index in range(self.num_of_neurons):
            # 重み付き線形和を求める
            sum = 0.0
            for previous_index, previous_output in enumerate(previous_outputs):
                sum += previous_output * self.weights[previous_index][current_index]
            sum += self.biases[current_index]

            # 活性化関数にかける
            output = self.activation_function(sum)
            self.neurons.append(output)

ニューロンの出力値を計算します。一つ前の層(中間層)のニューロンの出力が、出力層のニューロンの入力となるので、それを引数 previous_outputs として受け取ります。そして for ループで出力層0番目のニューロンの値から順に計算します。中間層のニューロンの出力値と重みをかけて足し合わせます。重み self.weights の添字は前述した通り[中間層のニューロン番号][出力層のニューロン番号]の順です。この合計値にバイアスを足して、活性化関数(後述)を通した値が出力層の各ニューロンの出力値になり、self.neurons に格納します。

    def activation_function(self, value):
        """活性化関数(恒等関数)

        :param value: 入力値
        :return: 活性化関数(恒等関数)にかけた値
        """
        return identity_function(value)

    def derivative_function(self, value):
        """活性化関数(恒等関数)の導関数

        :param value: 入力値
        :return: 活性化関数(恒等関数)の導関数にかけた値
        """
        return 1

順番が前後しますが、先ほど、活性化関数が出てきたので先に説明します。見ての通り活性化関数は identity_function()(恒等関数)を呼ぶだけ、そしてその導関数は1を返しているだけです。identity_function() はまた後ほど説明します。恒等関数の導関数は1なので、ベタ書きしています。

    def calculate_delta(self, teachings):
        """出力層各ニューロンのデルタの値を計算する

        :param teachings: 教師データのリスト
        """
        # 出力層のデルタの値を返す
        self.deltas = []
        for index in range(self.num_of_neurons):
            derivative_value = self.derivative_function(self.neurons[index])
            delta = (self.neurons[index] - teachings[index]) * derivative_value
            self.deltas.append(delta)

誤差逆伝播法で利用するデルタの値を計算します。誤差逆伝播法やデルタとは何ぞや?ということはここでは説明しないので、この記事の最後に紹介したリンクなどを参照してください。引数として、教師データのリストを受け取ります。出力層0番目のニューロンに対するデルタから順次計算し、self.deltas に格納していきます。self.derivative_function() は前述した恒等関数の導関数です。

    def update_weights(self, previous_outputs):
        """重みを更新する

        :param previous_outputs: 一つ前の層(中間層)の出力値のリスト
        """
        previous_num = len(previous_outputs)

        for current_index in range(self.num_of_neurons):
            for previous_index in range(previous_num):
                tmp = (
                    learning_factor
                    * self.deltas[current_index]
                    * previous_outputs[previous_index]
                )
                self.weights[previous_index][current_index] -= tmp

    def update_biases(self):
        """バイアスを更新する"""
        for index in range(self.num_of_neurons):
            self.biases[index] -= learning_factor * self.deltas[index]

誤差逆伝播法により、重みとバイアスを更新します。引数として、中間層のニューロンの出力リスト previous_outputs を受け取ります。ここで出てくるlearning_factor は学習率です。先ほど計算したデルタの値と、中間層のニューロンの出力を用いて、中間層と出力層の間の重みを一つ一つ更新してきます。バイアスも0番目のニューロンに対するバイアスから順に更新します。

中間層

中間層のクラスは下記のようになります。

class MiddleLayer:
    """中間層"""

    def __init__(self, current_num, previous_num):
        """コンストラクタ

        :param current_num: この層(中間層)のニューロン数
        :param previous_num: 一つ前の層(入力層)のニューロン数
        """
        # この層のニューロン数
        self.num_of_neurons = current_num
        # 各ニューロンの出力値
        self.neurons = []
        # 一つ前の層(入力層)のニューロンとの間の重み
        self.weights = initialize_weights(previous_num, current_num)
        # 各ニューロンのバイアス値
        self.biases = initialize_biases(self.num_of_neurons)
        # 各ニューロンのデルタの値(誤差逆伝播法で利用)
        self.deltas = []

    def calculate_output(self, previous_outputs):
        """各ニューロンの出力値を計算する

        :param previous_outputs: 入力層の出力値のリスト
        """
        # 出力を計算する
        self.neurons = []

        for current_index in range(self.num_of_neurons):
            # 重み付き線形和を求める
            sum = 0.0
            for previous_index, previous_output in enumerate(previous_outputs):
                sum += previous_output * self.weights[previous_index][current_index]
            sum += self.biases[current_index]

            # 活性化関数にかける
            output = self.activation_function(sum)
            self.neurons.append(output)

    def calculate_delta(self, next_deltas, next_weights):
        """中間層各ニューロンのデルタの値を計算する

        :param next_deltas: 次の層(出力層)のデルタ値のリスト
        :param next_weights: 次の層(出力層)のこの層(中間層)の間の重みのリスト
        """
        self.deltas = []

        next_num = len(next_deltas)
        for current_index in range(self.num_of_neurons):
            derivative_value = self.derivative_function(self.neurons[current_index])

            tmp = 0.0
            for next_index in range(next_num):
                tmp += next_deltas[next_index] * next_weights[current_index][next_index]
            sum = tmp * derivative_value

            self.deltas.append(sum)

    def update_weights(self, previous_outputs):
        """重みを更新する

        :param previous_outputs: 一つ前の層(入力層)の出力値のリスト
        """
        previous_num = len(previous_outputs)

        for current_index in range(self.num_of_neurons):
            for previous_index in range(previous_num):
                tmp = (
                    learning_factor
                    * self.deltas[current_index]
                    * previous_outputs[previous_index]
                )
                self.weights[previous_index][current_index] -= tmp

    def update_biases(self):
        """バイアスを更新する"""
        for index in range(self.num_of_neurons):
            self.biases[index] -= learning_factor * self.deltas[index]

    def activation_function(self, value):
        """活性化関数(シグモイド関数)

        :param value: 入力値
        :return: 活性化関数(シグモイド関数)にかけた値
        """
        return sigmoid_function(value)

    def derivative_function(self, value):
        """活性化関数(シグモイド関数)の導関数

        :param value: 入力値
        :return: 活性化関数(シグモイド関数)の導関数にかけた値
        """
        return derivative_sigmoid_function(value)

実は出力層とあまり変わりません。出力層と違う部分だけ説明します。

    def activation_function(self, value):
        """活性化関数(シグモイド関数)

        :param value: 入力値
        :return: 活性化関数(シグモイド関数)にかけた値
        """
        return sigmoid_function(value)

    def derivative_function(self, value):
        """活性化関数(シグモイド関数)の導関数

        :param value: 入力値
        :return: 活性化関数(シグモイド関数)の導関数にかけた値
        """
        return derivative_sigmoid_function(value)

中間層では活性化関数としてシグモイド関数を利用するので sigmoid_function() に変わっています。それに合わせて導関数derivative_sigmoid_function() に変わります。これらについては後ほど説明します。

    def calculate_delta(self, next_deltas, next_weights):
        """中間層各ニューロンのデルタの値を計算する

        :param next_deltas: 次の層(出力層)のデルタ値のリスト
        :param next_weights: 次の層(出力層)のこの層(中間層)の間の重みのリスト
        """
        self.deltas = []

        next_num = len(next_deltas)
        for current_index in range(self.num_of_neurons):
            derivative_value = self.derivative_function(self.neurons[current_index])

            tmp = 0.0
            for next_index in range(next_num):
                tmp += next_deltas[next_index] * next_weights[current_index][next_index]
            sum = tmp * derivative_value

            self.deltas.append(sum)

中間層ではデルタの計算式が変わるので、それに合わせて処理も変えています。計算に必要となる値も、出力層のデルタの値 next_deltas 、中間層と出力層の間の重み next_weights が必要となるので引数として受け取ります。そしてforループで中間層の各ニューロンに対するデルタの値を計算し self.deltas に格納していきます。重みとバイアスの更新処理については、出力層と同じです。

その他メソッド

説明を後回しにしたものがあるので、その説明をします。

def initialize_weights(previous_num, current_num):
    """重みを初期化する

    :param previous_num: 前の層のニューロン数
    :param current_num: 自分の層のニューロン数
    :return: 初期化された重み(リスト)
    """
    weights = [[None] * current_num for _ in range(previous_num)]
    for previous_index in range(previous_num):
        for current_index in range(current_num):
            weights[previous_index][current_index] = random.random()

    return weights


def initialize_biases(num_of_neuron):
    """バイアスを初期化する

    :param num_of_neuron: ニューロン数
    :return: 初期化されたバイアス(リスト)
    """
    biases = []
    for _ in range(num_of_neuron):
        biases.append(random.random())
    return biases

重みとバイアスを初期化する関数です。まず、initialize_weights() ですが、ひとつ手前の層のニューロンprevious_num と自分の層のニューロンcurrent_num を引数として受け取ります。そして 0.0 〜 1.0 の範囲でランダムな値を重みの初期値として設定し、2次元のリスト weights を返します。initialize_biases() は自分の層のニューロンnum_of_neuron を引数として受け取り、0.0 〜 1.0 の範囲でランダムな値をバイアスの初期値として設定、リスト biases を返します。

def identity_function(input):
    """恒等関数

    :param input: 入力値
    :return: 出力値
    """
    return input


def sigmoid_function(input):
    """シグモイド関数

    :param input: 入力値
    :return: 出力値
    """
    sigmoid_range = 34.538776394910684

    if input <= -sigmoid_range:
        return 1e-15
    if input >= sigmoid_range:
        return 1.0 - 1e-15

    return 1.0 / (1.0 + math.exp(-input))


def derivative_sigmoid_function(input):
    """シグモイド関数の導関数

    :param input: 入力値
    :return: 出力値
    """
    return input * (1 - input)

活性化関数に利用する関数とその導関数です。恒等関数は、その意味通り入力値をそのまま返しています。シグモイド関数とその導関数については、数式そのままの計算しているだけですが、シグモイド関数についてはオーバーフローの対策がしてあります。

少し長くなりましたので、今回はここで終わりにして、次回はこのニューラルネットワークを利用して学習をしていきたいと思います。

参考リンク

誤差逆伝播法を理解するにあたって、とてもわかりやすかったサイト(動画)を紹介します。