多層パーセプトロンを実装してみよう!【機械学習】
IT技術
多層パーセプトロンとは?
今回は、現在の機械学習の基盤となっている「多層パーセプトロン」を実装します。
また、それと並行して、多層パーセプトロンについて解説も加えていきたいと思っています。
多層パーセプトロンとは?
実装する前に簡単に多層パーセプトロン(MLP: Multilayer perceptron)について解説しておきます。
多層パーセプトロンは以下の図のように、複数の形式ニューロンが多層に接続されたネットワークを指します。
単純パーセプトロンは入力層と出力層のみであったのに対し、多層パーセプトロンは中間層(隠れ層)と呼ばれる、層が複数追加されたネットワーク構造を持ちます。
単純パーセプトロンと違い複数のクラス分類を可能とし、線形分離不可能な問題も解くことができます。
現在は、この多層パーセプトロンの形式を拡張したものがよく使われています。
単純パーセプトロンについての記事はこちら
2019.05.21機械学習の元祖「パーセプトロン」とは?パーセプトロンとは?パーセプトロンは、1958年に発表された、いわば「機械学習の元祖」です。パーセプトロンはニューラル...
多層パーセプトロンを実装する
実行環境
- Python 3.7.3
- numpy 1.16.3
- matplotlib3.03
下準備
実装の前に、使うライブラリのインポートをしておききましょう。
今回使うのが数値計算用の numpy と、可視化するための matplotlib です。
(ちなみに今回は、matplotlib はなくても問題ありません)
おなじみの形式でインポートしておきます。
1import numpy as np
2import matplotlib.pyplot as plt
ネットワーククラスを作る
今回は、後から扱いやすくするために、ネットワークをNetwork クラスとして定義して実装していこうと思います。
クラスインスタンスとして、ニューロン情報と重み情報をベクトルとして保持しておく形をとります。
したがって、クラスとそのコンストラクタは以下のように定義しました。
1class Network:
2 def __init__(self, *args):
3 self.layers = list(args)
4 self.weights = ([])
5 self.patterns = ([])
6 self.labels = ([])
各層のニューロン数を可変長引数としてコンストラクタに渡します。
コンストラクタ内でシナプス結合強度(以下 重み)ベクトルやデータセットを格納しておく配列も宣言しておきます。
例えば、net = Network(4, 10, 3) のようにすれば、「入力層4 - 中間層10 - 出力層3 」の3層からなるネットワークが作成できるようにします。
これ以降定義する関数は、基本的にこのNetwork クラスに属するメンバ関数だと思ってください。
重みを初期化する関数
ネットワークの各層の情報を決めたら、次にそのニューロン同士を接続する重みを初期化する必要があります。
先に定義しておきますが、入力層は第0層目、最初のニューロンは0番目ニューロンとして扱っていきます。
この重みベクトルは、weights[0][1][3] ならば第0層目 3番目ニューロン と 第1層目 1番目ニューロン 間の重みを指すこととします。
ここで、weights[層][前ニューロン][後ニューロン]としないのは、後でコーディングを楽にするためです。
一旦ここで頭の中を整理
【重みベクトルの読み方】
したがって、もし 4 - 10 - 3 のネットワークであれば、第0-1層間の重みは 10×4 の行列として表せます。
実際に書いてみる
これを実際にinitWeights() 関数という名前でコーディングしてみます。
1 def initWeights(self, a=0.0, b=1.0):
2 """
3 重みを[a, b]の一様乱数で生成
4 :param a: minimum = 0.0
5 :param b: maximum = 1.0
6 :return:
7 """
8 for i in range(len(self.layers)):
9 if i + 1 >= len(self.layers):
10 break
11 self.weights.append((b - a) * np.random.rand(self.layers[i + 1], self.layers[i]) + a)
(クラス内のメンバ関数ということでインデントをひとつ下げています。)
あとで実験条件を変更しやすいように、引数で一様乱数の下限と上限を設定できるようにしておきます。
確認
ここで一旦、ネットワークが構築できているかを確認してみましょう。
1if __name__ == '__main__':
2 net = Network(4, 50, 3)
3 # ネットワークをプリントしてみる
4 print(net.layers)
5
6 net.initWeights(-1.0, 1.0)
7 # 3次元重みベクトルを一つの配列に格納
8 weights = ([])
9 for lw in net.weights:
10 for nw in lw:
11 for w in nw:
12 weights.append(w)
13 # ヒストグラムとして描画
14 plt.hist(weights, bins=100, ec='black')
15 plt.show()
すると出力は...
[4, 50, 3]
うまく動作していそうです!
【重みヒストグラム】
データセットを取り込む
データセットについても、先にコーディングしていきたいと思います。
今回は、Irisデータセットと呼ばれる「アヤメの分類問題」を使用します。
アヤメの分類問題
データセットは以下のページです。
UCI Machine Learning Repository: Iris Data Set
Irisデータセットは、{(がくの長さ), (がくの幅), (花びらの長さ), (花びらの幅)}の4次元の特徴データを入力として、{Iris-setosa, Iris-versicolor, Iris-virginica}の3クラスに分類をする問題です。
このデータセットを配列に格納する関数を作ります。
名前はloadIris() とでもしておきましょう。
またIrisデータは150パターン分あり、それぞれ[5.1,3.5,1.4,0.2,Iris-setosa]のようにデータが並んでいます。
実際に書いてみる
したがって、コードはsplit() を使って以下のように書くことができます。
1 def loadIris(self):
2 data = open('iris.data', 'r')
3 lines = data.read().split()
4 dataset = ([])
5
6 for line in lines:
7 pattern = line.split(',')
8 dataset.append(pattern)
9 data.close()
10
11 for pattern in dataset:
12 self.patterns.append(list(map(float, pattern[0:-1])))
13 # setosaは0番目ニューロン、versicolorは1番目ニューロン、virginicaは2番目ニューロンが担当する
14 if pattern[-1] == 'Iris-setosa':
15 self.labels.append(0)
16 elif pattern[-1] == 'Iris-versicolor':
17 self.labels.append(1)
18 else:
19 self.labels.append(2)
形式ニューロンの実装
次に、重要な形式ニューロンを定義していきます。
(1) | |
(2) | |
(3) |
形式ニューロンは上の図と式で示すように、前層ニューロンからの入力に対して、それぞれ重みがかけられ、その和がニューロンへの最終的な入力になります。
その後、その入力があるしきい値(バイアスともいう)を超えていればを、超えてなければを出力する、という性質を持ち合わせています。
ここでは、コードの簡略化のためにしきい値は0としてしまいます。
(本来の多層パーセプトロンではしきい値についても学習しますが、学習しなくても結果に大きな差は出ません)
これらの式をコーディングしていきます。
式(1)
1 @staticmethod
2 def inputSum(inputs, weights):
3 """
4 前ニューロンからの入力和を返す関数.
5 :param inputs:
6 :param weights:
7 :return inputs dot weights (float):
8 """
9 return np.dot(inputs, weights)
この関数では、インスタンスメソッドには触れないので、@staticmethod を明示的に記述しておきます。
(もちろん記述せずに第1引数にself を入れても構いません)
式(2)と(3)
1 def output(self, inputs, weights):
2 """
3 ニューロンの出力を返す関数.
4 :param inputs:
5 :param weights:
6 :return 1 or 0:
7 """
8 return self.stepFunc(self.inputSum(inputs, weights))
9
10 @staticmethod
11 def stepFunc(x):
12 """
13 ステップ関数(活性化関数)
14 :param x:
15 :return 1 or 0:
16 """
17 return 1 if x > 0 else 0
上記のようになります!
順伝播処理の実装
次に、ネットワーク構築のうえで重要な順伝播処理(forward propagation)について実装していきます。
順伝播処理とは、その名前の通りネットワークを入力層から出力層にデータを流す過程のことです。
ここが多層パーセプトロン実装のなかで一番複雑かもしれませんが、やっていることは形式ニューロン処理の集合です。
まず、第0層目(入力層)と第1層目では、第0層目ニューロンの出力はデータセットの入力パターンをそのまま使うことにします。
第1層目以降では、保持しておいた前層の出力を入力として扱います。
実際に実装してみる
関数名は、forward() としました。
1 def forward(self, pattern):
2 """
3 順方向処理をする関数.
4 :param pattern:
5 :return 出力層ニューロンの出力値ベクトル(1 or 0):
6 """
7 tmp = ([])
8 outputs = ([]) # 前層ニューロンの出力ベクトル
9 for layer in range(len(self.layers)):
10 outputs = ([])
11 if layer == 1: # 第1中間層ならば入力パターンが前層ニューロンの出力となる
12 for n in range(self.layers[layer]):
13 outputs.append(self.output(pattern, self.weights[layer-1][n]))
14 tmp = outputs
15
16 elif layer > 1: # 第1中間層以降の層では前層の出力を使う
17 if layer == len(self.layers)-1: # 最終中間層の出力をとっておく
18 self.lastOutputs = tmp
19 for n in range(self.layers[layer]):
20 outputs.append(self.output(tmp, self.weights[layer-1][n]))
21
22 return outputs
コードを見ると、そこまで複雑な処理はしていないことが分かるかと思います。
17,18行目で最終中間層の出力を保持しているのは、学習で使うためです。
またここで、重みベクトルを[層][後ニューロン][前ニューロン]と定義したことが、活きていることもわかりますね! (13行目)
多層パーセプトロンやニューラルネットワークでは、計算が後ニューロン主体で考えることが多いので、前ニューロンのインデックスを最後(一番内側のループ)に配置すると、便利なことが多いです。
動作確認
今、定義した関数を使って出力層ニューロンの出力を観察してみましょう!
1if __name__ == '__main__':
2 net = Network(4, 20, 3)
3 net.initWeights(-0.5, 0.5)
4 net.loadIris()
5 # 3つのパターンを伝播してみる
6 print(net.forward(net.patterns[0]))
7 print(net.forward(net.patterns[70]))
8 print(net.forward(net.patterns[140]))
[1, 1, 0]
[0, 1, 1]
[0, 1, 1]
このように出力層ニューロンの3つの出力が出てきたら上手くできています。
まだ学習前なので、出力が似通ってしまっていることもわかりますね。
学習部の実装
いよいよ、多層パーセプトロンの要である学習部の実装をしていきます。
通常、勾配降下法という手法が用いられることがほとんどですが、今回はもっと簡易な学習法を用いることにします。
今回用いる学習法は、単純パーセプトロンの時と同じで、以下のような誤差関数(4)と重みの更新式(5)を使います。
(4) | |
(5) |
ここで、は入力パターンを示し、はその入力パターンの教師信号(通常1 or 0)を指します。
は、出力層ニューロンの出力です。
ということで、これらの式を実装していきます。
重みの更新式
学習部全体を担う関数をtrain() としておき、引数に最大学習回数をとるようにします。
train() をまず、ざっくりと大枠だけ書いてみます。
1 def train(self, epoch):
2 for e in range(epoch): # 最大学習回数で回す
3 for p in range(len(self.patterns)): # 入力パターンごとに回す
4
5 #--- 重みの更新 ---#
この大枠の中に、重み更新式や学習終了条件などを細かい実装していきます。
まず重みの更新の前に各入力パターンごとに順伝播をさせ、ネットワークの出力を得る必要があります。
その後、教師ニューロンを作成し、各出力層ニューロンの出力と教師ニューロンとの誤差を取ります。
以上を先ほどのコードに加えてみましょう!
1 def train(self, epoch):
2 for e in range(epoch): # 最大学習回数で回す
3 for p in range(len(self.patterns)): # 入力パターンごとに回す
4 outputs = self.forward(self.patterns[p])
5
6 teacher = ([0, 0, 0]) # 教師ニューロンを作成
7 teacher[self.labels[p]] = 1 # ラベルに該当する教師ニューロンの出力を1に
8
9 for o in range(len(outputs)): # 各出力ニューロンごとに誤差を取る
10 err = self.error(teacher[o], outputs[o])
誤差関数
1 @staticmethod
2 def error(label, output):
3 return label - output
ここまできたら、あと少しです!
さきほど取っておいた最終中間層の出力self.lastOutputs を使って重みを更新式を書きます。
またここで、一緒に学習終了条件も決めちゃいます。
ここでは、誤差の絶対値和が0になった時としておきましょう!
(だいぶ厳しい条件ですが...笑)。
すると最終的には以下のようになります。
1 def train(self, epoch):
2 for e in range(epoch): # 最大学習回数で回す
3 errSum = 0
4 for p in range(len(self.patterns)): # 入力パターンごとに回す
5 outputs = self.forward(self.patterns[p])
6
7 teacher = ([0, 0, 0]) # 教師ニューロンを作成
8 teacher[self.labels[p]] = 1 # ラベルに該当する教師ニューロンの出力を1に
9
10 for o in range(len(outputs)): # 各出力ニューロンごとに誤差を取る
11 err = self.error(teacher[o], outputs[o])
12 errSum += abs(err)
13 for i in range(len(self.lastOutputs)):
14 # 重みの更新
15 self.weights[len(self.layers) - 2][o][i] += self.rate * err * self.lastOutputs[i]
16
17 print(str(e+1) + ' / ' + str(epoch) + ' epoch.' + ' [ error: ' + str(errSum) + ' ]')
18 if errSum == 0:
19 break
学習率については変更しやすいようにクラスインスタンスとしました。
1class Network:
2 rate = 0.01
これで学習部の実装が完了しました!
テスト部の実装
最後は、テスト部の実装です。
学習してもテストしないと作ったネットワークの意味がありません。
今回は、訓練データをそのままテストデータとしてしまいます。
なので、train() 関数とあまり大きく変わりません。
識別率(Accuracy)なども表示できるようしたいので以下のように定義しました。
1 def test(self):
2 """
3 テスト関数
4 :return:
5 """
6 correct = 0
7 for p in range(len(self.patterns)):
8 outputs = self.forward(self.patterns[p])
9
10 teacher = ([0, 0, 0]) # 教師ニューロンを作成
11 teacher[self.labels[p]] = 1 # ラベルに該当する教師ニューロンの出力を1に
12
13 if outputs == teacher: # 出力と教師が同じならば
14 correct += 1
15
16 print('Accuracy: ' + str(correct / len(self.patterns) * 100) + '%')
以上で、クラスの実装は終わりです!
お疲れ様でした!
いざ、動作確認!
それでは、動作確認してみましょう!
クラスを用いたオブジェクト指向で実装していたので、メイン関数は以下のようにとても簡単です!
1if __name__ == '__main__':
2 net = Network(4, 50, 3)
3 net.initWeights(-1.0, 1.0)
4 net.loadIris()
5 net.train(100)
6 net.test()
今回は 4 - 50 - 3 のネットワークで最大100エポック分の学習としました。
結果は...
11 / 100 epoch. [ error: 46 ]
22 / 100 epoch. [ error: 26 ]
33 / 100 epoch. [ error: 24 ]
44 / 100 epoch. [ error: 21 ]
55 / 100 epoch. [ error: 19 ]
6#--- 省略 ---#
799 / 100 epoch. [ error: 4 ]
8100 / 100 epoch. [ error: 4 ]
9Accuracy: 98.0%
なんと、98%!
誤差は「0」にならなかったものの、高い識別精度を獲得することができました。
ただ、何度も回してみると安定感がなく、上のような結果は常には出てきません...。
多層パーセプトロンはネットワークの大きさや、学習率で大きく性能が変わってきます。
値を変えて色々な条件で試してみると、結果がまた違ってきます。
これが機械学習の難しいところでもあり、楽しいところと言えますね!
さいごに
今回は簡単ではありますが、多層パーセプトロンを実装して、 Irisデータセットを学習してみました!
安定感はあまりなかったものの、しっかり学習ができていることが分かります。
しかし実際、今回の学習法では最終層の重みしか学習できていないので、性能がなかなか上がりません。
誤差逆伝播学習法
そこで、提案されたのが「誤差逆伝播学習法」となります。
誤差逆伝播学習法は、今でもメジャーな学習法として使われており、全ての重みを学習できるので性能の良いネットワークが作れます。
誤差逆伝播学習法については、次の記事で解説していきたいと思います。
では、最後に、今回実装した最終的なコードを載せて終わりにしたいと思います!
ありがとうございました!
誤差逆伝播学習法についての記事はこちら
2019.06.06機械学習の要「誤差逆伝播学習法」を解説・実装してみる!「誤差逆伝播学習法」とは?誤差逆伝播学習法(BP: Backpropagation)とは、ニューラルネットワークの学習...
単純パーセプトロンについての記事はこちら
2019.05.21機械学習の元祖「パーセプトロン」とは?パーセプトロンとは?パーセプトロンは、1958年に発表された、いわば「機械学習の元祖」です。パーセプトロンはニューラル...
こちらの記事もオススメ!
2020.07.28機械学習 特集知識編人工知能・機械学習でよく使われるワード徹底まとめ!機械学習の元祖「パーセプトロン」とは?【人工知能】ニューラルネ...
2020.07.17ライトコード的「やってみた!」シリーズ「やってみた!」を集めました!(株)ライトコードが今まで作ってきた「やってみた!」記事を集めてみました!※作成日が新し...
最終的なコード
1import numpy as np
2import matplotlib.pyplot as plt
3
4
5class Network:
6 rate = 0.01
7
8 def __init__(self, *args):
9 self.layers = list(args)
10 self.weights = ([])
11 self.patterns = ([])
12 self.labels = ([])
13
14 def initWeights(self, a=0.0, b=1.0):
15 """
16 重みを[a, b]の一様乱数で生成
17 :param a: minimum = 0.0
18 :param b: maximum = 1.0
19 :return:
20 """
21 for i in range(len(self.layers)):
22 if i + 1 >= len(self.layers):
23 break
24 self.weights.append((b - a) * np.random.rand(self.layers[i + 1], self.layers[i]) + a)
25
26 def loadIris(self):
27 data = open('iris.data', 'r')
28 lines = data.read().split()
29 dataset = ([])
30
31 for line in lines:
32 pattern = line.split(',')
33 dataset.append(pattern)
34 data.close()
35
36 for pattern in dataset:
37 self.patterns.append(list(map(float, pattern[0:-1])))
38 # setosaは0番目ニューロン、versicolorは1番目ニューロン、virginicaは2番目ニューロンが担当する
39 if pattern[-1] == 'Iris-setosa':
40 self.labels.append(0)
41 elif pattern[-1] == 'Iris-versicolor':
42 self.labels.append(1)
43 else:
44 self.labels.append(2)
45
46 @staticmethod
47 def inputSum(inputs, weights):
48 """
49 前ニューロンからの入力和を返す関数.
50 :param inputs:
51 :param weights:
52 :return inputs dot weights (float):
53 """
54 return np.dot(inputs, weights)
55
56 def output(self, inputs, weights):
57 """
58 ニューロンの出力を返す関数.
59 :param inputs:
60 :param weights:
61 :return 1 or 0:
62 """
63 return self.stepFunc(self.inputSum(inputs, weights))
64
65 @staticmethod
66 def stepFunc(x):
67 """
68 ステップ関数(活性化関数)
69 :param x:
70 :return 1 or 0:
71 """
72 return 1 if x > 0 else 0
73
74 def forward(self, pattern):
75 """
76 順方向処理をする関数.
77 :param pattern:
78 :return 出力層ニューロンの出力値ベクトル(1 or 0):
79 """
80 tmp = ([])
81 outputs = ([]) # 前層ニューロンの出力ベクトル
82 for layer in range(len(self.layers)):
83 outputs = ([])
84 if layer == 1: # 第1中間層ならば入力パターンが前層ニューロンの出力となる
85 for n in range(self.layers[layer]):
86 outputs.append(self.output(pattern, self.weights[layer-1][n]))
87 tmp = outputs
88
89 elif layer > 1: # 第1中間層以降の層では前層の出力を使う
90 if layer == len(self.layers)-1: # 最終中間層の出力をとっておく
91 self.lastOutputs = tmp
92 for n in range(self.layers[layer]):
93 outputs.append(self.output(tmp, self.weights[layer-1][n]))
94
95 return outputs
96
97 def train(self, epoch):
98 """
99 訓練関数
100 :param epoch:
101 :return:
102 """
103 for e in range(epoch): # 最大学習回数で回す
104 errSum = 0
105 for p in range(len(self.patterns)): # 入力パターンごとに回す
106 outputs = self.forward(self.patterns[p])
107
108 teacher = ([0, 0, 0]) # 教師ニューロンを作成
109 teacher[self.labels[p]] = 1 # ラベルに該当する教師ニューロンの出力を1に
110
111 for o in range(len(outputs)): # 各出力ニューロンごとに誤差を取る
112 err = self.error(teacher[o], outputs[o])
113 errSum += abs(err)
114 for i in range(len(self.lastOutputs)):
115 # 重みの更新
116 self.weights[len(self.layers) - 2][o][i] += self.rate * err * self.lastOutputs[i]
117
118 print(str(e+1) + ' / ' + str(epoch) + ' epoch.' + ' [ error: ' + str(errSum) + ' ]')
119 if errSum == 0:
120 break
121
122 @staticmethod
123 def error(label, output):
124 return label - output
125
126 def test(self):
127 """
128 テスト関数
129 :return:
130 """
131 correct = 0
132 for p in range(len(self.patterns)):
133 outputs = self.forward(self.patterns[p])
134
135 teacher = ([0, 0, 0]) # 教師ニューロンを作成
136 teacher[self.labels[p]] = 1 # ラベルに該当する教師ニューロンの出力を1に
137
138 if outputs == teacher: # 出力と教師が同じならば
139 correct += 1
140
141 print('Accuracy: ' + str(correct / len(self.patterns) * 100) + '%')
142
143
144if __name__ == '__main__':
145 net = Network(4, 50, 3)
146 net.initWeights(-1.0, 1.0)
147 net.loadIris()
148 net.train(100)
149 net.test()
ライトコードでは、エンジニアを積極採用中!
ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。
採用情報へ
「好きを仕事にするエンジニア集団」の(株)ライトコードです! ライトコードは、福岡、東京、大阪、名古屋の4拠点で事業展開するIT企業です。 現在は、国内を代表する大手IT企業を取引先にもち、ITシステムの受託事業が中心。 いずれも直取引で、月間PV数1億を超えるWebサービスのシステム開発・運営、インフラの構築・運用に携わっています。 システム開発依頼・お見積もり大歓迎! また、現在「WEBエンジニア」「モバイルエンジニア」「営業」「WEBデザイナー」を積極採用中です! インターンや新卒採用も行っております。 以下よりご応募をお待ちしております! https://rightcode.co.jp/recruit
おすすめ記事
immichを知ってほしい
2024.10.31