機械学習の要「誤差逆伝播学習法」を解説・実装してみる!
IT技術
「誤差逆伝播学習法」とは?
誤差逆伝播学習法(BP: Backpropagation)とは、ニューラルネットワークの学習法の1つで、今現在もっとも主流で強力な学習法を指します。
その名の通り、ネットワークを誤差情報が逆伝播することから名前がつけられていますが、ちょっとそれだけでは分かりづらいですね。
この記事では、誤差逆伝播学習法の仕組みとその実装を解説していきます。
解説部では、少し数式が多いですが、ひとつひとつ丁寧に見ていけば必ず理解できると思います。
また、誤差逆伝播学習法の考え方は、様々な学習方法に応用されている学習法なのでしっかりと理解しておきましょう!
実行環境
以下は筆者の実行環境です。
今回も数値計算用のNumPyとグラフ描画用のmatplotlibを使います。
- Python 3.7.3
- NumPy 1.16.3
- matplotlib 3.0.3
「誤差逆伝播学習法」実装の前準備
ネットワークの初期化に関するもの
実装を踏まえて解説していきますが、今回も、多層パーセプトロンの時と同じようにNetwork クラスを定義してオブジェクト指向で実装していきます。
Networkクラスのコンストラクタ__init__() や重みの初期化関数init_weights() 、今回用いるIrisデータセットを読みこむ関数load_iris() は以下のようになります。
1class Network:
2 def __init__(self, *args):
3 self.layers = list(args) # 各層のニューロン数
4 self.weights = ([]) # 各層間の重みベクトル
5 self.patterns = ([]) # 入力パターンベクトル
6 self.labels = ([]) # 教師ニューロンインデックス
7 self.outputs = ([]) # 各ニューロンの出力値ベクトル
8
9 def init_weights(self, a=0.0, b=1.0):
10 """
11 重みを[a, b]の一様乱数で生成
12 :param a: minimum = 0.0
13 :param b: maximum = 1.0
14 :return:
15 """
16 for i in range(len(self.layers)):
17 if i + 1 >= len(self.layers):
18 break
19 self.weights.append((b - a) * np.random.rand(self.layers[i + 1], self.layers[i]) + a)
20
21 def load_iris(self):
22 data = open('iris.data', 'r')
23 lines = data.read().split()
24 dataset = ([])
25
26 for line in lines:
27 pattern = line.split(',')
28 dataset.append(pattern)
29 data.close()
30
31 for pattern in dataset:
32 self.patterns.append(list(map(float, pattern[0:-1])))
33 # setosaは0番目ニューロン、versicolorは1番目ニューロン、virginicaは2番目ニューロンが担当する
34 if pattern[-1] == 'Iris-setosa':
35 self.labels.append(0)
36 elif pattern[-1] == 'Iris-versicolor':
37 self.labels.append(1)
38 else:
39 self.labels.append(2)
実装の前準備【形式ニューロン編】
次の前準備として、形式ニューロンの処理に関するものを定義していきます。
形式ニューロンは以下の図と式で表されるような性質を持っています。
【図. 形式ニューロン】
(1) | |
(2) |
上の図と式の意味は、ある外部からの入力 に対して、それぞれ重み がかけられ、その和 がニューロンへの最終的な入力値になります。
その後、その入力を引数とする活性化関数 の値がそのニューロンの最終的な出力値 となります。
シグモイド関数
形式ニューロンでは活性化関数はステップ関数でしたが、ここではシグモイド関数と呼ばれる活性化関数を用います。
(3) |
ここで は、シグモイド関数の傾きと言い、関係性は以下のような図になります。
なぜ、シグモイド関数を用いるかは、後で詳しく説明したいと思います!
【図. シグモイド関数】
実装
とりあえず、これらの特性を実装してみます。
1 @staticmethod
2 def input_sum(inputs, weights):
3 """
4 前ニューロンからの入力和を返す関数.式(1)
5 :param inputs:
6 :param weights:
7 :return inputs dot weights (float):
8 """
9 return np.dot(inputs, weights)
10
11 def output(self, inputs, weights):
12 """
13 ニューロンの出力を返す関数.式(2)
14 :param inputs:
15 :param weights:
16 :return (0, 1):
17 """
18 return self.sigmoid(self.input_sum(inputs, weights))
19
20 def sigmoid(self, x):
21 """
22 シグモイド関数 式(3)
23 :param x:
24 :return:
25 """
26 return 1.0/(1.0 + np.exp(-self.epsilon * x))
このようになります!
ここでは、シグモイド関数の傾きはクラスインスタンスとして定義しています。
1class Network:
2 epsilon = 1.0 # シグモイド関数の傾き
「誤差逆伝播学習法」について解説!
それでは、「誤差逆伝播学習法」について解説していきます。
考え方自体はシンプルですが、実装するために序盤は式変形など数学的な内容が多いです。
少し気合いが必要ですが頑張っていきましょう!
誤差逆伝播学習法についてザックリ知ろう!
まずは、誤差逆伝播学習法について大枠だけ理解しましょう。
先ほども言ったように、誤差逆伝播学習法は、教師信号と実際と出力信号との間に生じる誤差情報を使ってネットワーク全体を学習していきます。
【図. 誤差逆伝播学習法の概略】
今までの学習法と違い、全ての重みの学習が可能で、その性能の良さから現在でも主流な学習法です。
この記事では最終層(出力層)を第 層とし、中間層を第 層のように表記します。
誤差関数の定義
まずは、教師信号と実際の出力との誤差情報を示す、誤差関数を定義します。
- 誤差関数が大きいほど、理想状態とは遠い
誤差関数が大きいほど理想状態とは遠いということを示し、学習の要と言えます。
誤差逆伝播学習法では一般的に誤差関数は以下のような式で表されます。
(4) |
ここで、 は出力層( 第層 ) 番目ニューロンの出力値で、 はそれに対応する教師信号を指します。
が付いている理由は、後々計算を楽にするためだけなので、深い意味はありません。
このような誤差関数を、「二乗誤差関数」と言います。
- 誤差関数が小さくなれば、学習ができている
この誤差関数が小さくなれば、学習ができていると言えます。
勾配降下法
次に、先ほど定義した誤差関数を小さくするために、勾配降下法(または最急降下法)と呼ばれる手法をとります。
勾配降下法のイメーシとしては、以下のような図がよく用いられます。
【図. 勾配降下法の概略図】
上の図で示すように、最小化させたい誤差関数の傾き(勾配)を計算し、その傾きの大きさとは逆方向に重みを調整すれば、誤差関数の値を小さくできます。
「誤差逆伝播学習法」の重みの更新式
したがって、誤差逆伝播学習法では、「重みの更新式」は以下のように定義します。
(5) |
パっと見ると複雑そうですが、そんなに難しくありません。
ここで は、第 層 番目ニューロンと、第 層 番目ニューロンとの間の重みです。
【図. 重みの見方】
つまり式(5)は、現在の重みで誤差関数を偏微分して得られた傾きとは、逆方向に重みを更新している式を表しています。
ちなみに は学習率で、通常0.1や0.01などの小さな値を使います。
これは学習の進行速度を表しています。
しかし式(5)の形では、まだ実装するには難しそうです。
そもそも は、出力値 の関数なので、重みで偏微分できません。
では、どのように計算したらよいでしょうか?
式変形をする
ここから少しややこしく、複雑になっていきますが、順を追ってゆっくり理解していきましょう。
まず、式(5)の右辺は、以下のように変形してみます(連鎖率)。
(6) |
ここで は、第 層目 番目ニューロンの入力値です。
さらに今出てきた、式(6)右辺について考えてみましょう。
まずは、入力値の偏微分の部分については、以下のように簡単な形に導出できます。
(7) |
問題は、式(6)右辺の前半です。
ここで一旦以下のように、新たに という変数を定義してみます。
(8) |
このように定義したことにより、式(5)は、以下のようなジンプルな形で一旦書き換えることができます。
(9) |
あとは、この が解決できれば実装ができそうです!
もう少し頑張っていきましょう。
δについて
この の導出が、「誤差逆伝播学習法の要」と言えます。
まず、 について、連鎖率を使って分解してみます。
(10) |
ここで、式(10)右辺の後半については、
(11) |
とシンプルな活性化関数の導関数になります。
ですが、重要なのは、 の部分です。
ここで以下の2パターンの場合で導出方法が異なってきます。
- 最終層(第 層)のとき:
- 中間層(第 層)のとき:
最終層のとき
最終層(第 層)のときは、シンプルです。
(4) |
ですので、
(12) |
となります。
したがって、 は、以下のようになります。
(13) |
中間層のとき
中間層(第 層)のときは、少し工夫が必要です。
が中間層の時の ですが、 には が含まれていないのでこのままでは偏微分不可能です。
そこで、また「連鎖率」を使って式変形を行なっていきます。
(14) |
このように少し複雑ですが変形ができます。
したがって中間層(第層)の時の は、以下のようになります。
(15) |
ここで重要なのは、最後に得られた式で、一つ後ろの層の情報が含まれていることです。
すなわち、最終層で計算した誤差情報 が連鎖的に入力層側に伝わっていくことがわかります。
これが「誤差逆伝播学習法」という名前の由縁です。
誤差逆伝播学習法の重み更新式
それでは、まとめると、誤差逆伝播学習法の重み更新式は以下のようになります。
(16) |
- 最終層(第層)のとき
- 中間層(第層)のとき
活性化関数(シグモイド関数)の微分
解説の最後に「活性化関数(シグモイド関数)の微分」を考えます。
ここで、「なぜステップ関数ではダメだったのか」をお話します。
それは、ずばり不連続な関数(微分不可能)だからです。
また、 のような単純な線形関数でもよいのですが、勾配=傾きなので常に一定になってしまい不適切です。
そこで、今回用いるようなシグモイド関数を使っているのです。
他にも、(ハイパボリックタンジェント)などもよく使われます。
シグモイド関数を微分すると以下のようになります。
このように微分するとまた中にシグモイド関数が出てくるので、無限に微分が可能です。
これがシグモイド関数の良いところです。
シグモイド関数を使う場合の誤差逆伝播学習法の重み更新式
したがって、シグモイド関数を使う場合の誤差逆伝播学習法の重み更新式は、
(16’) |
- 最終層(第層)のとき
- 中間層(第層)のとき
以上が誤差逆伝播学習法の学習法導出になります。
お疲れ様でした!
「誤差逆伝播学習法」を実装する!
それでは、誤差逆伝播学習法を実際に実装していきたいと思います。
誤差逆伝播学習法は、導出は複雑なものの、アルゴリズム自体はいたって簡単です。
アルゴリズム
1. 入力パターンをネットワークに順伝播して出力値を得る
↓
2. 出力値と教師信号を用いて、 を計算
↓
3. を用いて重み更新量を出力層側から計算
↓
4. 重みを更新
↓
5. 学習終了基準を満たしていなければ、1. に戻る
実装例
今回は、学習率減衰というものを導入して、学習が進むごとに減衰させることにしてみます。
学習率減衰は、適切に設定をすれば収束を早める効果があります。
また、最後に、学習途中の誤答率をプロットするようにしています。
ちなみに、 を計算する関数はcalc_delta() で、誤差逆伝播の関数はbackward() としています。
教師信号は、以前は0や1を使っていましたが、シグモイド関数では重みが発散しないように0.1や0.9を代わりに用いることが多いです。
1import numpy as np
2import matplotlib.pyplot as plt
3
4
5class Network:
6 rate = 0.1 # 学習率
7 decay = 0.1 # 学習率減衰
8 per = 50 # 何エポックごとに学習率を減衰させるか
9
10 epsilon = 1.0 # シグモイド関数の傾き
11
12 def __init__(self, *args):
13 self.layers = list(args) # 各層のニューロン数
14 self.weights = ([]) # 各層間の重みベクトル
15 self.patterns = ([]) # 入力パターンベクトル
16 self.labels = ([]) # 教師ニューロンインデックス
17 self.outputs = ([]) # 各ニューロンの出力値ベクトル
18
19 def init_weights(self, a=0.0, b=1.0):
20 """
21 重みを[a, b]の一様乱数で生成
22 :param a: minimum = 0.0
23 :param b: maximum = 1.0
24 :return:
25 """
26 for i in range(len(self.layers)):
27 if i + 1 >= len(self.layers):
28 break
29 self.weights.append((b - a) * np.random.rand(self.layers[i + 1], self.layers[i]) + a)
30
31 def load_iris(self):
32 data = open('iris.data', 'r')
33 lines = data.read().split()
34 dataset = ([])
35
36 for line in lines:
37 pattern = line.split(',')
38 dataset.append(pattern)
39 data.close()
40
41 for pattern in dataset:
42 self.patterns.append(list(map(float, pattern[0:-1])))
43 # setosaは0番目ニューロン、versicolorは1番目ニューロン、virginicaは2番目ニューロンが担当する
44 if pattern[-1] == 'Iris-setosa':
45 self.labels.append(0)
46 elif pattern[-1] == 'Iris-versicolor':
47 self.labels.append(1)
48 else:
49 self.labels.append(2)
50
51 @staticmethod
52 def input_sum(inputs, weights):
53 """
54 前ニューロンからの入力和を返す関数.式(1)
55 :param inputs:
56 :param weights:
57 :return inputs dot weights (float):
58 """
59 return np.dot(inputs, weights)
60
61 def output(self, inputs, weights):
62 """
63 ニューロンの出力を返す関数.式(2)
64 :param inputs:
65 :param weights:
66 :return (0, 1):
67 """
68 return self.sigmoid(self.input_sum(inputs, weights))
69
70 def sigmoid(self, x):
71 """
72 シグモイド関数 式(3)
73 :param x:
74 :return:
75 """
76 return 1.0/(1.0 + np.exp(-self.epsilon * x))
77
78 def forward(self, pattern):
79 """
80 順方向処理をする関数.
81 :param pattern:
82 :return 出力層ニューロンの出力値ベクトル [0,1]:
83 """
84 self.outputs.clear() # まず出力値情報をクリア
85
86 out = ([]) # 前層ニューロンの出力ベクトル
87 self.outputs.append(pattern) # まず入力層ニューロンの出力(入力パターン)を追加
88
89 for layer in range(len(self.layers)):
90 out = ([])
91 if layer == 1: # 第1中間層ならば入力パターンが前層ニューロンの出力となる
92 for n in range(self.layers[layer]):
93 out.append(self.output(pattern, self.weights[layer-1][n]))
94
95 self.outputs.append(out) # 出力値を追加
96
97 elif layer > 1: # 第1中間層以降の層では前層の出力を使う
98 for n in range(self.layers[layer]):
99 out.append(self.output(self.outputs[-1], self.weights[layer-1][n]))
100
101 self.outputs.append(out) # 出力値を追加
102
103 return out
104
105 def backward(self, pattern):
106 """
107 誤差逆伝播学習法の大枠
108 :param pattern:
109 :return:
110 """
111 deltas = self.calc_delta(pattern) # δを計算
112 for l in reversed(range(1, len(self.layers))): # ネットワークを逆順処理していく
113 for j, jj in enumerate(self.outputs[l]):
114 for i, ii in enumerate(self.outputs[l-1]):
115 self.weights[l-1][j][i] += self.rate * deltas[l-1][j] * ii # 重みを更新
116
117 def calc_delta(self, pattern):
118 """
119 δを計算する関数
120 :param pattern: パターンインデックス
121 :return: δベクトル
122 """
123 teacher = ([0.1, 0.1, 0.1]) # 教師ニューロンを作成
124 teacher[self.labels[pattern]] = 0.9 # ラベルに該当する教師ニューロンの出力を0.9に
125 deltas = ([]) # 全ニューロンのδ
126
127 for l in reversed(range(1, len(self.layers))): # ネットワークを逆順処理していく
128 tmp_delta = ([])
129 for j in range(self.layers[l]):
130 if l == len(self.layers) - 1: # 最終層ならば
131
132 delta = self.epsilon * \
133 (teacher[j] - self.outputs[l][j]) * \
134 self.outputs[l][j] * (1 - self.outputs[l][j])
135
136 tmp_delta.append(delta)
137
138 else: # 最終層以外
139 t_weights = np.array(self.weights[l]).T
140 delta = self.epsilon * \
141 self.outputs[l][j] * (1 - self.outputs[l][j]) * \
142 np.dot(deltas[-1], t_weights[j])
143
144 tmp_delta.append(delta)
145
146 deltas.append(tmp_delta)
147
148 return deltas[::-1] # δは逆順で返す
149
150 def train(self, epoch):
151 """
152 訓練関数
153 :param epoch:
154 :return:
155 """
156 errors = ([])
157 for e in range(epoch): # 最大学習回数で回す
158 error = 100 - self.test()
159 for p, pat in enumerate(self.patterns): # 入力パターンごとに回す
160
161 self.forward(pat) # 順伝播処理で出力値を計算
162
163 self.backward(p) # 逆伝播で重みを更新
164
165 print(str(e+1) + ' / ' + str(epoch) + ' epoch.' + ' [ error: ' + str(error) + ' ]')
166
167 if (e+1) % self.per == 0: # 学習率減衰
168 self.rate *= self.decay
169
170 errors.append(error) # 不正解率
171
172 # 不正解立を描画
173 plt.xlabel('epochs')
174 plt.ylabel('error (%)')
175 plt.plot(errors)
176 plt.savefig('error.png', dpi=300)
177
178 def test(self):
179 """
180 テスト関数
181 :return: accuracy (%)
182 """
183 correct = 0
184 for p in range(len(self.patterns)):
185 self.forward(self.patterns[p])
186 max = 0
187 ans = -1
188 # 一番出力値の高いニューロンを取ってくる
189 for o, out in enumerate(self.outputs[len(self.layers)-1]):
190 if max < out:
191 max = out
192 ans = o
193 # もしそのニューロンの番号とラベルの番号があっていれば正解!
194 if ans == self.labels[p]:
195 correct += 1
196
197 accuracy = correct / len(self.patterns) * 100
198 return accuracy
199
200
201if __name__ == '__main__':
202 net = Network(4, 20, 3)
203 net.init_weights(-1.0, 1.0)
204 net.load_iris()
205 net.train(300)
206 acc = net.test()
207 print('Accuracy: ' + str(acc) + '%')
いざ、実行!
それでは、実際に上記コードを実行してみましょう。
11 / 300 epoch. [ error: 66.66666666666667 ]
22 / 300 epoch. [ error: 66.66666666666667 ]
33 / 300 epoch. [ error: 66.66666666666667 ]
44 / 300 epoch. [ error: 66.66666666666667 ]
55 / 300 epoch. [ error: 66.66666666666667 ]
66 / 300 epoch. [ error: 59.333333333333336 ]
77 / 300 epoch. [ error: 43.333333333333336 ]
88 / 300 epoch. [ error: 34.0 ]
99 / 300 epoch. [ error: 34.0 ]
1010 / 300 epoch. [ error: 34.0 ]
11# --- 省略 --- #
12299 / 300 epoch. [ error: 2.666666666666657 ]
13300 / 300 epoch. [ error: 2.666666666666657 ]
14Accuracy: 97.33333333333334%
【図. 誤答率の遷移】
見事学習がうまくいっていることが分かります!
また安定感もよく、高確率で高精度を得られることも実際に動かしてみるとわかります。
実際に10試行やってみると平均精度97.4%でした。
もう少しパラメータなどを工夫してみると良い結果が得られるかもしれません。
また、Irisデータセットは普通98%あたりが限度なので、別のデータセットを使ってみるのも良いですね。
こちらの記事もオススメ!
2020.07.28機械学習 特集知識編人工知能・機械学習でよく使われるワード徹底まとめ!機械学習の元祖「パーセプトロン」とは?【人工知能】ニューラルネ...
2020.07.17ライトコード的「やってみた!」シリーズ「やってみた!」を集めました!(株)ライトコードが今まで作ってきた「やってみた!」記事を集めてみました!※作成日が新し...
ライトコードでは、エンジニアを積極採用中!
ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。
採用情報へ
「好きを仕事にするエンジニア集団」の(株)ライトコードです! ライトコードは、福岡、東京、大阪、名古屋の4拠点で事業展開するIT企業です。 現在は、国内を代表する大手IT企業を取引先にもち、ITシステムの受託事業が中心。 いずれも直取引で、月間PV数1億を超えるWebサービスのシステム開発・運営、インフラの構築・運用に携わっています。 システム開発依頼・お見積もり大歓迎! また、現在「WEBエンジニア」「モバイルエンジニア」「営業」「WEBデザイナー」を積極採用中です! インターンや新卒採用も行っております。 以下よりご応募をお待ちしております! https://rightcode.co.jp/recruit