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)
ソフトマックス関数とその導関数です。これらは数式そのままの実装になります。
それでは次回のブログで、実際に分類問題を学習していきます。