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)
活性化関数に利用する関数とその導関数です。恒等関数は、その意味通り入力値をそのまま返しています。シグモイド関数とその導関数については、数式そのままの計算しているだけですが、シグモイド関数についてはオーバーフローの対策がしてあります。
少し長くなりましたので、今回はここで終わりにして、次回はこのニューラルネットワークを利用して学習をしていきたいと思います。
参考リンク
誤差逆伝播法を理解するにあたって、とてもわかりやすかったサイト(動画)を紹介します。