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)

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

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