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

回帰問題に引き続き、分類問題をニューラルネットワークで学習していきます。

設定

回帰問題と同じく入力層・中間層・出力層の3層からなるネットワークを考えます。設定は下記の通りです。

回帰問題のときと同様に、各層ごとにクラスを作成します。インスタンス変数も同じです。

入力層

まずは入力層ですが、回帰問題のニューラルネットワークと全く同じなので、説明は割愛します。

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

中間層

次に中間層です。中間層も回帰問題のニューラルネットワークと同じなので、説明は割愛します。

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)

出力層

次に出力層です。

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 = []

        sums = []
        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]
            sums.append(sum)

        for index in range(self.num_of_neurons):
            # 活性化関数にかける
            output = self.activation_function(sums, index)
            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, inputs, index):
        """活性化関数(ソフトマックス関数)

        :param value: 入力値
        :return: 活性化関数(ソフトマックス関数)にかけた値
        """
        return softmax_function(inputs, index)

    def derivative_function(self, value):
        """活性化関数(ソフトマックス関数)の導関数

        :param value: 入力値
        :return: 活性化関数(ソフトマックス関数)の導関数にかけた値
        """
        return derivative_softmax_function(value)

出力層も回帰問題のニューラルネットワークとほぼ同じです。変更点のみ説明します。

    def activation_function(self, inputs, index):
        """活性化関数(ソフトマックス関数)

        :param value: 入力値
        :return: 活性化関数(ソフトマックス関数)にかけた値
        """
        return softmax_function(inputs, index)

    def derivative_function(self, value):
        """活性化関数(ソフトマックス関数)の導関数

        :param value: 入力値
        :return: 活性化関数(ソフトマックス関数)の導関数にかけた値
        """
        return derivative_softmax_function(value)

出力層では、活性化関数としてソフトマックス関数を利用するので、そのように実装しています。ソフトマックス関数の実装については後ほど説明します。

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

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

        sums = []
        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]
            sums.append(sum)

        for index in range(self.num_of_neurons):
            # 活性化関数にかける
            output = self.activation_function(sums, index)
            self.neurons.append(output)

活性化関数としてソフトマックス関数を利用するために、実装を少し変えています。ソフトマックス関数では、出力を求めるために複数の入力値(=各ニューロンの重み付き線形和にバイアスを足したもの)が必要なので、それを配列sums に詰めて、計算対象の self.neurons のインデックス index と共に self.activation_function() に渡して、各ニューロンの出力値を計算しています。

その他メソッド

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

def softmax_function(inputs, index):
    """ソフトマックス関数

    :param inputs: 入力値のリスト
    :param index: 計算対象のインデックス
    :return: 出力値
    """
    sum = 0.0
    for input in inputs:
        sum += math.exp(input)

    return math.exp(inputs[index]) / sum


def derivative_softmax_function(input):
    """ソフトマックス関数の導関数

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

ソフトマックス関数とその導関数です。これらは数式そのままの実装になります。

それでは次回のブログで、実際に分類問題を学習していきます。

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回学習後の結果です。 青の破線がニューラルネットワークで学習した結果、緑の実線が正解のサインカーブです。

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

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

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)

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

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

参考リンク

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