
多層パーセプトロンを実装してみよう!【機械学習】
2021.12.20
多層パーセプトロンとは?
今回は、現在の機械学習の基盤となっている「多層パーセプトロン」を実装します。
また、それと並行して、多層パーセプトロンについて解説も加えていきたいと思っています。
多層パーセプトロンとは?
実装する前に簡単に多層パーセプトロン(MLP: Multilayer perceptron)について解説しておきます。
多層パーセプトロンは以下の図のように、複数の形式ニューロンが多層に接続されたネットワークを指します。

単純パーセプトロンは入力層と出力層のみであったのに対し、多層パーセプトロンは中間層(隠れ層)と呼ばれる、層が複数追加されたネットワーク構造を持ちます。
単純パーセプトロンと違い複数のクラス分類を可能とし、線形分離不可能な問題も解くことができます。
現在は、この多層パーセプトロンの形式を拡張したものがよく使われています。
単純パーセプトロンについての記事はこちら
多層パーセプトロンを実装する
実行環境
- Python 3.7.3
- numpy 1.16.3
- matplotlib3.03
下準備
実装の前に、使うライブラリのインポートをしておききましょう。
今回使うのが数値計算用の numpy と、可視化するための matplotlib です。
(ちなみに今回は、matplotlib はなくても問題ありません)
おなじみの形式でインポートしておきます。
1 2 | import numpy as np import matplotlib.pyplot as plt |
ネットワーククラスを作る
今回は、後から扱いやすくするために、ネットワークを Network クラスとして定義して実装していこうと思います。
クラスインスタンスとして、ニューロン情報と重み情報をベクトルとして保持しておく形をとります。
したがって、クラスとそのコンストラクタは以下のように定義しました。
1 2 3 4 5 6 | class Network: def __init__(self, *args): self.layers = list(args) self.weights = ([]) self.patterns = ([]) 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 2 3 4 5 6 7 8 9 10 11 | def initWeights(self, a=0.0, b=1.0): """ 重みを[a, b]の一様乱数で生成 :param a: minimum = 0.0 :param b: maximum = 1.0 :return: """ for i in range(len(self.layers)): if i + 1 >= len(self.layers): break self.weights.append((b - a) * np.random.rand(self.layers[i + 1], self.layers[i]) + a) |
(クラス内のメンバ関数ということでインデントをひとつ下げています。)
あとで実験条件を変更しやすいように、引数で一様乱数の下限と上限を設定できるようにしておきます。
確認
ここで一旦、ネットワークが構築できているかを確認してみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | if __name__ == '__main__': net = Network(4, 50, 3) # ネットワークをプリントしてみる print(net.layers) net.initWeights(-1.0, 1.0) # 3次元重みベクトルを一つの配列に格納 weights = ([]) for lw in net.weights: for nw in lw: for w in nw: weights.append(w) # ヒストグラムとして描画 plt.hist(weights, bins=100, ec='black') 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | def loadIris(self): data = open('iris.data', 'r') lines = data.read().split() dataset = ([]) for line in lines: pattern = line.split(',') dataset.append(pattern) data.close() for pattern in dataset: self.patterns.append(list(map(float, pattern[0:-1]))) # setosaは0番目ニューロン、versicolorは1番目ニューロン、virginicaは2番目ニューロンが担当する if pattern[-1] == 'Iris-setosa': self.labels.append(0) elif pattern[-1] == 'Iris-versicolor': self.labels.append(1) else: self.labels.append(2) |
形式ニューロンの実装
次に、重要な形式ニューロンを定義していきます。
(1) | $$\displaystyle y = \sum_{i=1}^n{w_i x_i}$$ |
(2) | $$\displaystyle z = f(y)$$ |
(3) | $$\displaystyle f(x)= \begin{cases}1 & ( x \gt 0 ) \\ 0 & ( x \leq 0 ) \end{cases}$$ |
形式ニューロンは上の図と式で示すように、前層ニューロンからの入力\(x_i \{i=1,2,3,\cdots,n\}\)に対して、それぞれ重み\(w_i \{i=1,2,3,\cdots,n\}\)がかけられ、その和\(y\)がニューロンへの最終的な入力になります。
その後、その入力があるしきい値(バイアスともいう)\(\theta\)を超えていれば\(1\)を、超えてなければ\(0\)を出力する、という性質を持ち合わせています。
ここでは、コードの簡略化のためにしきい値は0としてしまいます。
(本来の多層パーセプトロンではしきい値についても学習しますが、学習しなくても結果に大きな差は出ません)
これらの式をコーディングしていきます。
式(1)
1 2 3 4 5 6 7 8 9 | @staticmethod def inputSum(inputs, weights): """ 前ニューロンからの入力和を返す関数. :param inputs: :param weights: :return inputs dot weights (float): """ return np.dot(inputs, weights) |
この関数では、インスタンスメソッドには触れないので、 @staticmethod を明示的に記述しておきます。
(もちろん記述せずに第1引数に self を入れても構いません)
式(2)と(3)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | def output(self, inputs, weights): """ ニューロンの出力を返す関数. :param inputs: :param weights: :return 1 or 0: """ return self.stepFunc(self.inputSum(inputs, weights)) @staticmethod def stepFunc(x): """ ステップ関数(活性化関数) :param x: :return 1 or 0: """ return 1 if x > 0 else 0 |
上記のようになります!
順伝播処理の実装
次に、ネットワーク構築のうえで重要な順伝播処理(forward propagation)について実装していきます。
順伝播処理とは、その名前の通りネットワークを入力層から出力層にデータを流す過程のことです。
ここが多層パーセプトロン実装のなかで一番複雑かもしれませんが、やっていることは形式ニューロン処理の集合です。
まず、第0層目(入力層)と第1層目では、第0層目ニューロンの出力はデータセットの入力パターンをそのまま使うことにします。
第1層目以降では、保持しておいた前層の出力を入力として扱います。
実際に実装してみる
関数名は、 forward() としました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | def forward(self, pattern): """ 順方向処理をする関数. :param pattern: :return 出力層ニューロンの出力値ベクトル(1 or 0): """ tmp = ([]) outputs = ([]) # 前層ニューロンの出力ベクトル for layer in range(len(self.layers)): outputs = ([]) if layer == 1: # 第1中間層ならば入力パターンが前層ニューロンの出力となる for n in range(self.layers[layer]): outputs.append(self.output(pattern, self.weights[layer-1][n])) tmp = outputs elif layer > 1: # 第1中間層以降の層では前層の出力を使う if layer == len(self.layers)-1: # 最終中間層の出力をとっておく self.lastOutputs = tmp for n in range(self.layers[layer]): outputs.append(self.output(tmp, self.weights[layer-1][n])) return outputs |
コードを見ると、そこまで複雑な処理はしていないことが分かるかと思います。
17,18行目で最終中間層の出力を保持しているのは、学習で使うためです。
またここで、重みベクトルを[層][後ニューロン][前ニューロン]と定義したことが、活きていることもわかりますね! (13行目)
多層パーセプトロンやニューラルネットワークでは、計算が後ニューロン主体で考えることが多いので、前ニューロンのインデックスを最後(一番内側のループ)に配置すると、便利なことが多いです。
動作確認
今、定義した関数を使って出力層ニューロンの出力を観察してみましょう!
1 2 3 4 5 6 7 8 | if __name__ == '__main__': net = Network(4, 20, 3) net.initWeights(-0.5, 0.5) net.loadIris() # 3つのパターンを伝播してみる print(net.forward(net.patterns[0])) print(net.forward(net.patterns[70])) print(net.forward(net.patterns[140])) |
[1, 1, 0]
[0, 1, 1]
[0, 1, 1]
このように出力層ニューロンの3つの出力が出てきたら上手くできています。
まだ学習前なので、出力が似通ってしまっていることもわかりますね。
学習部の実装
いよいよ、多層パーセプトロンの要である学習部の実装をしていきます。
通常、勾配降下法という手法が用いられることがほとんどですが、今回はもっと簡易な学習法を用いることにします。
今回用いる学習法は、単純パーセプトロンの時と同じで、以下のような誤差関数(4)と重みの更新式(5)を使います。
(4) | $$E_p=t_p-f(x_p)$$ |
(5) | $$\Delta w=\eta E_{p}x_{p} $$ |
ここで、\(p\)は入力パターンを示し、\(t_p\)はその入力パターンの教師信号(通常1 or 0)を指します。
\(f(x_p)\)は、出力層ニューロンの出力です。
ということで、これらの式を実装していきます。
重みの更新式
学習部全体を担う関数を train() としておき、引数に最大学習回数をとるようにします。
train() をまず、ざっくりと大枠だけ書いてみます。
1 2 3 4 5 | def train(self, epoch): for e in range(epoch): # 最大学習回数で回す for p in range(len(self.patterns)): # 入力パターンごとに回す #--- 重みの更新 ---# |
この大枠の中に、重み更新式や学習終了条件などを細かい実装していきます。
まず重みの更新の前に各入力パターンごとに順伝播をさせ、ネットワークの出力を得る必要があります。
その後、教師ニューロンを作成し、各出力層ニューロンの出力と教師ニューロンとの誤差を取ります。
以上を先ほどのコードに加えてみましょう!
1 2 3 4 5 6 7 8 9 10 | def train(self, epoch): for e in range(epoch): # 最大学習回数で回す for p in range(len(self.patterns)): # 入力パターンごとに回す outputs = self.forward(self.patterns[p]) teacher = ([0, 0, 0]) # 教師ニューロンを作成 teacher[self.labels[p]] = 1 # ラベルに該当する教師ニューロンの出力を1に for o in range(len(outputs)): # 各出力ニューロンごとに誤差を取る err = self.error(teacher[o], outputs[o]) |
誤差関数
1 2 3 | @staticmethod def error(label, output): return label - output |
ここまできたら、あと少しです!
さきほど取っておいた最終中間層の出力 self.lastOutputs を使って重みを更新式を書きます。
またここで、一緒に学習終了条件も決めちゃいます。
ここでは、誤差の絶対値和が0になった時としておきましょう!
(だいぶ厳しい条件ですが...笑)。
すると最終的には以下のようになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | def train(self, epoch): for e in range(epoch): # 最大学習回数で回す errSum = 0 for p in range(len(self.patterns)): # 入力パターンごとに回す outputs = self.forward(self.patterns[p]) teacher = ([0, 0, 0]) # 教師ニューロンを作成 teacher[self.labels[p]] = 1 # ラベルに該当する教師ニューロンの出力を1に for o in range(len(outputs)): # 各出力ニューロンごとに誤差を取る err = self.error(teacher[o], outputs[o]) errSum += abs(err) for i in range(len(self.lastOutputs)): # 重みの更新 self.weights[len(self.layers) - 2][o][i] += self.rate * err * self.lastOutputs[i] print(str(e+1) + ' / ' + str(epoch) + ' epoch.' + ' [ error: ' + str(errSum) + ' ]') if errSum == 0: break |
学習率については変更しやすいようにクラスインスタンスとしました。
1 2 | class Network: rate = 0.01 |
これで学習部の実装が完了しました!
テスト部の実装
最後は、テスト部の実装です。
学習してもテストしないと作ったネットワークの意味がありません。
今回は、訓練データをそのままテストデータとしてしまいます。
なので、 train() 関数とあまり大きく変わりません。
識別率(Accuracy)なども表示できるようしたいので以下のように定義しました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | def test(self): """ テスト関数 :return: """ correct = 0 for p in range(len(self.patterns)): outputs = self.forward(self.patterns[p]) teacher = ([0, 0, 0]) # 教師ニューロンを作成 teacher[self.labels[p]] = 1 # ラベルに該当する教師ニューロンの出力を1に if outputs == teacher: # 出力と教師が同じならば correct += 1 print('Accuracy: ' + str(correct / len(self.patterns) * 100) + '%') |
以上で、クラスの実装は終わりです!
お疲れ様でした!
いざ、動作確認!
それでは、動作確認してみましょう!
クラスを用いたオブジェクト指向で実装していたので、メイン関数は以下のようにとても簡単です!
1 2 3 4 5 6 | if __name__ == '__main__': net = Network(4, 50, 3) net.initWeights(-1.0, 1.0) net.loadIris() net.train(100) net.test() |
今回は 4 - 50 - 3 のネットワークで最大100エポック分の学習としました。
結果は...
1 2 3 4 5 6 7 8 9 | 1 / 100 epoch. [ error: 46 ] 2 / 100 epoch. [ error: 26 ] 3 / 100 epoch. [ error: 24 ] 4 / 100 epoch. [ error: 21 ] 5 / 100 epoch. [ error: 19 ] #--- 省略 ---# 99 / 100 epoch. [ error: 4 ] 100 / 100 epoch. [ error: 4 ] Accuracy: 98.0% |
なんと、98%!
誤差は「0」にならなかったものの、高い識別精度を獲得することができました。
ただ、何度も回してみると安定感がなく、上のような結果は常には出てきません...。
多層パーセプトロンはネットワークの大きさや、学習率で大きく性能が変わってきます。
値を変えて色々な条件で試してみると、結果がまた違ってきます。
これが機械学習の難しいところでもあり、楽しいところと言えますね!
さいごに
今回は簡単ではありますが、多層パーセプトロンを実装して、 Irisデータセットを学習してみました!
安定感はあまりなかったものの、しっかり学習ができていることが分かります。
しかし実際、今回の学習法では最終層の重みしか学習できていないので、性能がなかなか上がりません。
誤差逆伝播学習法
そこで、提案されたのが「誤差逆伝播学習法」となります。
誤差逆伝播学習法は、今でもメジャーな学習法として使われており、全ての重みを学習できるので性能の良いネットワークが作れます。
誤差逆伝播学習法については、次の記事で解説していきたいと思います。
では、最後に、今回実装した最終的なコードを載せて終わりにしたいと思います!
ありがとうございました!
誤差逆伝播学習法についての記事はこちら
単純パーセプトロンについての記事はこちら
こちらの記事もオススメ!
最終的なコード
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 | import numpy as np import matplotlib.pyplot as plt class Network: rate = 0.01 def __init__(self, *args): self.layers = list(args) self.weights = ([]) self.patterns = ([]) self.labels = ([]) def initWeights(self, a=0.0, b=1.0): """ 重みを[a, b]の一様乱数で生成 :param a: minimum = 0.0 :param b: maximum = 1.0 :return: """ for i in range(len(self.layers)): if i + 1 >= len(self.layers): break self.weights.append((b - a) * np.random.rand(self.layers[i + 1], self.layers[i]) + a) def loadIris(self): data = open('iris.data', 'r') lines = data.read().split() dataset = ([]) for line in lines: pattern = line.split(',') dataset.append(pattern) data.close() for pattern in dataset: self.patterns.append(list(map(float, pattern[0:-1]))) # setosaは0番目ニューロン、versicolorは1番目ニューロン、virginicaは2番目ニューロンが担当する if pattern[-1] == 'Iris-setosa': self.labels.append(0) elif pattern[-1] == 'Iris-versicolor': self.labels.append(1) else: self.labels.append(2) @staticmethod def inputSum(inputs, weights): """ 前ニューロンからの入力和を返す関数. :param inputs: :param weights: :return inputs dot weights (float): """ return np.dot(inputs, weights) def output(self, inputs, weights): """ ニューロンの出力を返す関数. :param inputs: :param weights: :return 1 or 0: """ return self.stepFunc(self.inputSum(inputs, weights)) @staticmethod def stepFunc(x): """ ステップ関数(活性化関数) :param x: :return 1 or 0: """ return 1 if x > 0 else 0 def forward(self, pattern): """ 順方向処理をする関数. :param pattern: :return 出力層ニューロンの出力値ベクトル(1 or 0): """ tmp = ([]) outputs = ([]) # 前層ニューロンの出力ベクトル for layer in range(len(self.layers)): outputs = ([]) if layer == 1: # 第1中間層ならば入力パターンが前層ニューロンの出力となる for n in range(self.layers[layer]): outputs.append(self.output(pattern, self.weights[layer-1][n])) tmp = outputs elif layer > 1: # 第1中間層以降の層では前層の出力を使う if layer == len(self.layers)-1: # 最終中間層の出力をとっておく self.lastOutputs = tmp for n in range(self.layers[layer]): outputs.append(self.output(tmp, self.weights[layer-1][n])) return outputs def train(self, epoch): """ 訓練関数 :param epoch: :return: """ for e in range(epoch): # 最大学習回数で回す errSum = 0 for p in range(len(self.patterns)): # 入力パターンごとに回す outputs = self.forward(self.patterns[p]) teacher = ([0, 0, 0]) # 教師ニューロンを作成 teacher[self.labels[p]] = 1 # ラベルに該当する教師ニューロンの出力を1に for o in range(len(outputs)): # 各出力ニューロンごとに誤差を取る err = self.error(teacher[o], outputs[o]) errSum += abs(err) for i in range(len(self.lastOutputs)): # 重みの更新 self.weights[len(self.layers) - 2][o][i] += self.rate * err * self.lastOutputs[i] print(str(e+1) + ' / ' + str(epoch) + ' epoch.' + ' [ error: ' + str(errSum) + ' ]') if errSum == 0: break @staticmethod def error(label, output): return label - output def test(self): """ テスト関数 :return: """ correct = 0 for p in range(len(self.patterns)): outputs = self.forward(self.patterns[p]) teacher = ([0, 0, 0]) # 教師ニューロンを作成 teacher[self.labels[p]] = 1 # ラベルに該当する教師ニューロンの出力を1に if outputs == teacher: # 出力と教師が同じならば correct += 1 print('Accuracy: ' + str(correct / len(self.patterns) * 100) + '%') if __name__ == '__main__': net = Network(4, 50, 3) net.initWeights(-1.0, 1.0) net.loadIris() net.train(100) net.test() |
書いた人はこんな人

- 「好きを仕事にするエンジニア集団」の(株)ライトコードです!
ライトコードは、福岡、東京、大阪の3拠点で事業展開するIT企業です。
現在は、国内を代表する大手IT企業を取引先にもち、ITシステムの受託事業が中心。
いずれも直取引で、月間PV数1億を超えるWebサービスのシステム開発・運営、インフラの構築・運用に携わっています。
システム開発依頼・お見積もり大歓迎!
また、現在「WEBエンジニア」「モバイルエンジニア」「営業」「WEBデザイナー」「WEBディレクター」を積極採用中です!
インターンや新卒採用も行っております。
以下よりご応募をお待ちしております!
https://rightcode.co.jp/recruit
ITエンタメ10月 13, 2023Netflixの成功はレコメンドエンジン?
ライトコードの日常8月 30, 2023退職者の最終出社日に密着してみた!
ITエンタメ8月 3, 2023世界初の量産型ポータブルコンピュータを開発したのに倒産!?アダム・オズボーン
ITエンタメ7月 14, 2023【クリス・ワンストラス】GitHubが出来るまでとソフトウェアの未来