• トップ
  • ブログ一覧
  • 多層パーセプトロンを実装してみよう!【機械学習】
  • 多層パーセプトロンを実装してみよう!【機械学習】

    広告メディア事業部広告メディア事業部
    2019.05.30

    IT技術

    多層パーセプトロンとは?

    今回は、現在の機械学習の基盤となっている「多層パーセプトロン」を実装します。

    また、それと並行して、多層パーセプトロンについて解説も加えていきたいと思っています。

    多層パーセプトロンとは?

    実装する前に簡単に多層パーセプトロン(MLP: Multilayer perceptron)について解説しておきます。

    多層パーセプトロンは以下の図のように、複数の形式ニューロンが多層に接続されたネットワークを指します。

    単純パーセプトロンは入力層と出力層のみであったのに対し、多層パーセプトロンは中間層(隠れ層)と呼ばれる、層が複数追加されたネットワーク構造を持ちます。

    単純パーセプトロンと違い複数のクラス分類を可能とし、線形分離不可能な問題も解くことができます。

    現在は、この多層パーセプトロンの形式を拡張したものがよく使われています。

    単純パーセプトロンについての記事はこちら

    featureImg2019.05.21機械学習の元祖「パーセプトロン」とは?パーセプトロンとは?パーセプトロンは、1958年に発表された、いわば「機械学習の元祖」です。パーセプトロンはニューラル...

    多層パーセプトロンを実装する

    実行環境

    1. Python 3.7.3
    2. numpy 1.16.3
    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)y=i=1nwixi\displaystyle y = \sum_{i=1}^n{w_i x_i}
    (2)z=f(y)\displaystyle z = f(y)
    (3)f(x)={1amp;(x>0)0amp;(x0)\displaystyle f(x)= \begin{cases}1 & ( x \gt 0 ) \\ 0 & ( x \leq 0 ) \end{cases}

    形式ニューロンは上の図と式で示すように、前層ニューロンからの入力xi{i=1,2,3,,n}x_i \{i=1,2,3,\cdots,n\}に対して、それぞれ重みwi{i=1,2,3,,n}w_i \{i=1,2,3,\cdots,n\}がかけられ、その和yyがニューロンへの最終的な入力になります。

    その後、その入力があるしきい値(バイアスともいう)θ\thetaを超えていれば11を、超えてなければ00を出力する、という性質を持ち合わせています。

    ここでは、コードの簡略化のためにしきい値は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)Ep=tpf(xp)E_p=t_p-f(x_p)
    (5)Δw=ηEpxp\Delta w=\eta E_{p}x_{p}

    ここで、ppは入力パターンを示し、tpt_pはその入力パターンの教師信号(通常1 or 0)を指します。

    f(xp)f(x_p)は、出力層ニューロンの出力です。

    ということで、これらの式を実装していきます。

    重みの更新式

    学習部全体を担う関数を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  # ラベルに該当する教師ニューロンの出力を18
    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  # ラベルに該当する教師ニューロンの出力を19
    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  # ラベルに該当する教師ニューロンの出力を112
    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データセットを学習してみました!

    安定感はあまりなかったものの、しっかり学習ができていることが分かります。

    しかし実際、今回の学習法では最終層の重みしか学習できていないので、性能がなかなか上がりません。

    誤差逆伝播学習法

    そこで、提案されたのが「誤差逆伝播学習法」となります。

    誤差逆伝播学習法は、今でもメジャーな学習法として使われており、全ての重みを学習できるので性能の良いネットワークが作れます。

    誤差逆伝播学習法については、次の記事で解説していきたいと思います。

    では、最後に、今回実装した最終的なコードを載せて終わりにしたいと思います!

    ありがとうございました!

    誤差逆伝播学習法についての記事はこちら

    featureImg2019.06.06機械学習の要「誤差逆伝播学習法」を解説・実装してみる!「誤差逆伝播学習法」とは?誤差逆伝播学習法(BP: Backpropagation)とは、ニューラルネットワークの学習...

    単純パーセプトロンについての記事はこちら

    featureImg2019.05.21機械学習の元祖「パーセプトロン」とは?パーセプトロンとは?パーセプトロンは、1958年に発表された、いわば「機械学習の元祖」です。パーセプトロンはニューラル...

    こちらの記事もオススメ!

    featureImg2020.07.28機械学習 特集知識編人工知能・機械学習でよく使われるワード徹底まとめ!機械学習の元祖「パーセプトロン」とは?【人工知能】ニューラルネ...

    featureImg2020.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  # ラベルに該当する教師ニューロンの出力を1110
    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  # ラベルに該当する教師ニューロンの出力を1137
    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()

     

    ライトコードでは、エンジニアを積極採用中!

    ライトコードでは、エンジニアを積極採用しています!社長と一杯しながらお話しする機会もご用意しております。そのほかカジュアル面談等もございますので、くわしくは採用情報をご確認ください。

    採用情報へ

    広告メディア事業部

    広告メディア事業部

    おすすめ記事

    エンジニア大募集中!

    ライトコードでは、エンジニアを積極採用中です。

    特に、WEBエンジニアとモバイルエンジニアは是非ご応募お待ちしております!

    また、フリーランスエンジニア様も大募集中です。

    background