
機械学習の元祖「パーセプトロン」とは?
2021.12.20
パーセプトロンとは?
パーセプトロンは、1958年に発表された、いわば「機械学習の元祖」です。
パーセプトロンはニューラルネットワークの一種で、形式ニューロンを複数用いてネットワーク状に接続したものを指します。
一般的に、以下のように分類されます。
- 2層からなる「単純パーセプトロン」
- 3層以上からなる「多層パーセプトロン」
現在は、後者の多層パーセプトロンが主流です。
しかし、入門編としまして、シンプルでわかりやすい単純パーセプトロンの解説と実装を紹介していきたいと思います。
実行環境
実装はPythonでおこなっていきますが、筆者の実行環境は以下の通りです。
Python | 3.7.3 |
numpy | 1.16.3 |
matplotlib | 3.0.3 |
また、以後紹介するコードでは、以下のようなインポートがされている前提で解説していきます。
1 2 | import numpy as np import matplotlib.pyplot as plt |
それでは、解説していきます!
多層パーセプトロンについての記事はこちら
そもそも形式ニューロンとはなんでしょうか?
形式ニューロンは1943年に発表された、世界初の神経細胞モデルです。
非常にシンプルながら、その汎用性と実装のしやすさから現在も、この考え方が主流です。
形式ニューロンの性質は、非常にシンプルです。
数式で表すと
(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\) を出力する、という性質を持ち合わせています。
このとき、式(3)を活性化関数(あるいは入出力関数)と言います。
パーセプトロンでは、通常、ステップ関数が使われます。
ただ、近年の機械学習では、様々な活性化関数が使われています。
式を実際にPythonで実装
1 2 3 4 5 6 7 8 9 10 | def inputsum(inputs, weights): # (1) return np.dot(inputs, weights) def output(y): # (2) return step(y) def step(x): # (3) return 1 if x > 0 else 0 |
上記のように、とてもシンプルなコードになりました!
単純パーセプトロン

単純パーセプトロンは、図のような2層からなるネットワークを指します。
上の図では、1層目に複数のニューロンが用いられています。
しかし、実際には入力層には入力データ(訓練データ、学習データ)がセットされるので、実質1つの形式ニューロンを用いたシンプルな構造になっています。
パーセプトロンは、入力層と出力層のニューロンを結ぶシナプス結合荷重(重み)を学習していきます。
では、学習方法について話を進める前に、そもそも「学習」についてご説明したいと思います。
パーセプトロンの学習
単純パーセプトロンでは、出力層ニューロンは1つで、その出力値は0か1の二値です。
つまり、ある入力データに対してクラスに属しているか否かを判別することが最終的な目標となります。
先ほどのパーセプトロンの図と式から、出力値というのは以下のような一つの関数で表すことができます。
$$f(x)=-w_0+w_1x_1+w_2x_2+\cdots+w_nx_n$$
この関数 \(f(x)\) が、正なら「1」を、負なら「0」を出力するわけですから、重みを調整して2つのクラス分類が可能というわけです。
アヤメの分類問題
ちょっと文字だけではわかりづらいので、簡単に入力が2次元として図を用いて見てみましょう。
機械学習でよく使われる、アヤメの分類問題を例に考えてみます。
ちなみに、「アヤメデータセット(UCI Machine Learning Repository: Iris Data Set)」とは、4次元の特徴データから、3つのクラス分類をする、比較的、簡単な識別ベンチマークのひとつです。
4次元の特徴データ:がくの長さ、がくの幅、花びらの長さ、花びらの幅
このデータセットから2クラス(青:setosa, 緑:versicolor)を、x軸が「がくの長さ+幅」、y軸が「花びらの長さ+幅」として実際にプロットしてみましょう。
【UCI Irisデータセットのsetosaクラスとversicolorクラス】
このようにこの2クラスでは、なんとなくクラスごとにデータが固まっているように見えます。
このような図を特徴空間とも言います。
もし、このグループを一つの直線で分割するとすれば、例えば以下のような直線が望ましそうですね。
【決定境界の例】
このような直線が引ければ、答えが分からない未知データに対しても、直線より下に位置するデータ(すなわち \(f(x)\leq0\) )はsetosaで、上に位置するデータ(すなわち \(f(x)>0\) )ならversicolorだと分類できます。
この直線を決定境界(Decision Boundary)と言い、先ほどの関数 \(f(x)\) に当たります。
しかし、単純パーセプトロンの重み \(w\) は、最初はランダムな値に初期化されているので、図のような綺麗な直線は引けていません。
この決定境界と呼ばれるクラス分類の境界線を引くことが学習のゴールです。
学習法
次に、どうやって決定境界を学習していくかを解説していきます。
特徴空間上に決定境界を引くわけですが、さきほど書いたように最初は重みがランダムで初期化されているので、デタラメな決定境界が引かれています。
【デタラメな決定境界】
そこで学習の方針としては、一つ一つの学習データに対して今の決定境界で自身が正しいクラスに分類されているか、を見ていきます。
誤差関数
そのためにまずは、誤差関数 \(E\) というものを定義します。
$$E_p=t_p-f(x_p) \ \ \ \ \ (4)$$
ここで、式(4)は、ある学習パターン \(p\) の入力ベクトル \(x_p\) が与えられた時の出力 \(f(x_p)\) と、それに対応するクラスラベル(教師信号) \(t_p\) の差を意味します。
もし、ある学習パターン \(p\) の出力値がsetosa( \(f(x_p)=0\) )で、教師信号もsetosa( \(t_p=0\) )であれば、 \(E_p=0\) となります。
教師信号がversicolor( \(t_p=1\) )ならば \(E_p=1\) となります。
これらの関係を表にまとめてみると
\(E_p\) | \(f(x_p)=0\) | \(f(x_p)=1\) |
\(t_p=0\) | 0 | -1 |
\(t_p=1\) | 1 | 0 |
上記のようになります。
ここで重要なのは、誤差関数は重みの修正の向きを表しています。
- \(E=0\) なら修正する必要なし。
- \(E=1\) なら重みを大きくする方向に修正する。
- \(E=-1\) なら重みを小さくする方向に修正する。
といった具合に学習を進めていけば、最終的に全ての学習パターンに対して誤差が0になるはずです。
しかし、この誤差関数をそのまま使うと、修正量が必ず1の大きさなので、一般的には以下のような重み更新式を用います。
重み更新式
$$\Delta w=\eta E_{p}x_{p} \ \ \ \ (5)$$
ここで \(\eta\) は、学習率といいあらかじめ自分で設定する必要があります。
通常、0.1~0.0001のような小さな値が使われます。
以上が学習の流れになります。
Pythonでコーディング
では、実際に、Pythonでコーディングしてみます
(今回は学習率 0.001 を使用)。
1 2 3 4 5 6 7 | def error(output, label): # 誤差関数 (4) return label - output def update(weights, err, x): # 重み更新式 (5) for i in range(len(x)): weights[i] += 0.001 * float(err) * x[i] |
動作確認
ここからは、実際にPythonを用いて動作確認していきましょう!
動作確認条件
今回は、以下のような条件で学習をしていきます。
併せて、この後、紹介するコードでどの行に対応しているかも書いておきます。
学習率\(\eta\) | 0.001 | 28行目 |
最大学習回数 | 100 epochs | 84行目 |
重み初期値 | 0~1の乱数 | 17行目 |
データセット | Irisデータセット | 31行目 |
学習終了条件 | 最大学習回数に到達または全てのデータに対する誤差が0になったとき | 94行目 |
最終的なPythonコード
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 | import numpy as np import matplotlib.pyplot as plt def inputsum(inputs, weights): # (1) return np.dot(inputs, weights) def output(inputs, weights): # (2) return step(inputsum(inputs, weights)) def step(x): # (3) return 1 if x > 0 else 0 def init(): # 閾値(バイアス)も一緒に初期化 return np.random.rand(5) def error(output, label): # 誤差関数 (4) return label - output def update(weights, err, x): # 重み更新式 (5) for i in range(len(x)): weights[i] += 0.001 * float(err) * x[i] def iris(): data = open('iris.data', 'r') lines = data.read().split() dataset = ([]) for line in lines: pattern = line.split(',') dataset.append(pattern) data.close() return dataset def result(weights, dataset): # 決定境界を描画する fig = plt.figure() ax = fig.add_subplot(1, 1, 1) boundary = ([]) for pattern in dataset: if pattern[-1] == 'Iris-setosa': ax.scatter(float(pattern[0]) + float(pattern[1]), float(pattern[2]) + float(pattern[3]), color='blue') if pattern[-1] == 'Iris-versicolor': ax.scatter(float(pattern[0]) + float(pattern[1]), float(pattern[2]) + float(pattern[3]), color='green') plt.xlim(6, 11) for i in range(15): boundary.append(-((weights[1] + weights[2])/(weights[3] + weights[4]))*i + weights[0]/(weights[3] + weights[4])) ax.plot(boundary, color='red') ax.set_title('Decision Boundary') ax.set_xlabel('sepal length + sepal width') ax.set_ylabel('petal length + petal width') # fig.savefig('result.png', format='png', dpi=300) fig.show() def test(weights, data, labels): # 識別率を計算 correct = 0 for p in range(len(data)): out = output(data[p], weights) if out == labels[p]: correct += 1 print('識別率:' + str(correct*100/len(data)) + '%') if __name__ == '__main__': dataset = iris() # 今回は2クラス分しか使わない patterns = ([]) labels = ([]) for pattern in dataset: if pattern[-1] != 'Iris-virginica': # 閾値(バイアス)入力を0番目に設定 patterns.append([-1.0] + list(map(float, pattern[0:-1]))) # setosaは0、versicolorは1の教師信号とする labels.append(0 if pattern[-1] == 'Iris-setosa' else 1) # 入力パターンを表示 print(patterns) weights = init() for epoch in range(100): sumE = 0 for p in range(len(patterns)): e = error(output(patterns[p], weights), labels[p]) update(weights, e, patterns[p]) sumE += e**2 print(str(epoch) + '/100 epoch.') print(sumE) if sumE == 0: break result(weights, dataset) test(weights, patterns, labels) |
動作結果
動作結果の一例は、以下のようになります。
もちろん、重みの初期値によって結果は変動します。
そのため、この結果の限りではありません。
【動作結果例】
0/100 epoch.
50
1/100 epoch.
50
2/100 epoch.
50
3/100 epoch.
8
4/100 epoch.
0
識別率:100.0%
結果を見てわかるように、きれいに決定境界が引けていて、識別率もわずか4回の学習で「100%」を獲得できていることがわかります。
(偶に識別率は100%でもうまく決定境界が引けないことがあるのは、4次元のデータを無理やり2次元で描写しているからです。)
これでこのパーセプトロンはIrisデータセットのうち、2クラスは分類できるネットワークになりました。
これが初期の人工知能で、今ある人工知能技術の基盤となっています。
単純パーセプトロンの欠点
最後に、単純パーセプトロンの欠点を紹介してこの記事を終わりにします。
- 先ほどの動作確認でわかったように、2クラス分類しかできない
- また、重要な欠点として、線形分離可能な問題にしか使うことができない
Irisのsetosaとversicolorを表した図3のように、綺麗にクラスごとに分かれていれば良いのですが、以下のように2クラスが混同しているとうまく決定境界が引けません。
【Irisデータセットのversicolorとvirginicaで実験した場合】
この問題を解決するために多層パーセプトロンが提案されたのです。
多層パーセプトロンについての記事はこちら
こちらの記事もオススメ!
書いた人はこんな人

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