• トップ
  • ブログ一覧
  • 【後編】自作の誤差逆伝播学習法で手書き数字を認識させてみよう!【機械学習】
  • 【後編】自作の誤差逆伝播学習法で手書き数字を認識させてみよう!【機械学習】

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

    IT技術

    後編〜手書き数字を認識するプログラムを作る~

    今回は、機械学習の要「誤差逆伝播学習法」を解説・実装してみる【人工知能】の記事で作成したコードを元に手書き数字を認識するプログラムを作ってみるの後半です!

    前編はこちら

    featureImg2019.06.11【前編】自作の誤差逆伝播学習法で手書き数字を認識させてみよう!【機械学習】前編〜手書き数字を認識するプログラムを作る~誤差逆伝播学習法は、教師信号とネットワークの実際の出力との誤差情報と勾配降...

    実際に手書き数字を認識させてみよう!

    それでは、実際に動かして実験してみましょう!

    実験は、以下のような実験条件とコード(main関数)で行なっています。

    それぞれ処理ごとにコメントを書いているので参考にしてください。

    ちなみにディレクトリツリーは、以下のようになっています。

    1User:project User$ tree
    2.
    3├── OptDigits
    4│   ├── optdigits.tes
    5│   └── optdigits.tra
    6├── main.py
    7└── myDigits
    8    ├── 0.png
    9    ├── 1.png
    10    ├── 2.png
    11    ├── 3.png
    12    ├── 4.png
    13    ├── 5.png
    14    ├── 6.png
    15    ├── 7.png
    16    ├── 8.png
    17    └── 9.png
    18
    192 directories, 13 files

     

    実験条件

    初期学習率0.1
    学習率減衰×0.5 / 10 epochs

    コード

    1if __name__ == '__main__':
    2    # ネットワークを作成
    3    net = Network(64, 100, 10)
    4    net.init_weights(-0.1, 0.1)
    5
    6    # データセットを読み込み
    7    net.load_optdigits()
    8
    9    #  誤差逆伝播学習法
    10    net.train(50)
    11
    12    # 最終的な訓練精度とテスト精度を出力
    13    acc = net.validate()
    14    print('Training Accuracy: ' + str(acc) + '%')
    15    acc = net.test()
    16    print('Test Accuracy: ' + str(acc) + '%')
    17
    18    # 自分の手書き数字を認識させてみる
    19    net.prop_my_digits('myDigits/0.png')
    20    net.prop_my_digits('myDigits/1.png')
    21    net.prop_my_digits('myDigits/2.png')
    22    net.prop_my_digits('myDigits/3.png')
    23    net.prop_my_digits('myDigits/4.png')
    24    net.prop_my_digits('myDigits/5.png')
    25    net.prop_my_digits('myDigits/6.png')
    26    net.prop_my_digits('myDigits/7.png')
    27    net.prop_my_digits('myDigits/8.png')
    28    net.prop_my_digits('myDigits/9.png')

    出力結果

    11 / 50 epoch. [ error: 90.16479204812974 ]
    22 / 50 epoch. [ error: 24.274130264190433 ]
    33 / 50 epoch. [ error: 6.5655244572325415 ]
    44 / 50 epoch. [ error: 5.283808527334557 ]
    55 / 50 epoch. [ error: 4.786816636149609 ]
    66 / 50 epoch. [ error: 4.4729270206644 ]
    77 / 50 epoch. [ error: 4.1851948731362825 ]
    88 / 50 epoch. [ error: 4.054407533350769 ]
    99 / 50 epoch. [ error: 3.8451477896939537 ]
    1010 / 50 epoch. [ error: 3.6620455139942436 ]
    11# --- 省略 --- #
    1246 / 50 epoch. [ error: 2.3541721161391536 ]
    1347 / 50 epoch. [ error: 2.3541721161391536 ]
    1448 / 50 epoch. [ error: 2.328014648182048 ]
    1549 / 50 epoch. [ error: 2.301857180224957 ]
    1650 / 50 epoch. [ error: 2.301857180224957 ]
    17Training Accuracy: 97.69814281977504%
    18Test Accuracy: 44.8339000784724%
    19myDigits/0.png is  0
    20myDigits/1.png is  1
    21myDigits/2.png is  2
    22myDigits/3.png is  3
    23myDigits/4.png is  8
    24myDigits/5.png is  5
    25myDigits/6.png is  5
    26myDigits/7.png is  7
    27myDigits/8.png is  8
    28myDigits/9.png is  9

    なんと訓練精度が約97.7%で、自分の手書き数字はいくつか間違えてはいるものの、大体予測ができています!

    間違えた数字を見てみると、「6」「5」と間違えたりと確かに形が似ている数字です。

    また何度か実験してみるとわかりますが、「9」「4」も比較的間違えやすいです。

    なぜ完璧に識別できなかったのか?

    いくつか原因が考えられますが、ひとつあげられるとしたら過学習(または過適合: Overfitting)していることが考えられます。

    過学習
    訓練データの精度は良くなるものの、未知のデータに対して精度が得られない現象

    今回はテスト精度、つまり学習に使っていない未知データを使った精度も計算しています。

    そのテスト精度は、「約44.8%」と高い精度とは到底言えません...。

    また、学習過程での訓練精度とテスト精度を見てみてもテスト精度はなかなか伸びません。

    【図 30エポックまでの訓練精度とテスト精度】

    過学習の対策

    過学習の簡単な対策としては、

    1. 学習データを増やす
    2. 良い学習データを使う
    3. 正則化項を誤差関数に加える

    などがよく挙げられます。

    1. 2. の学習データについては、今回8×8の小さな手書き数字データセットを用いたので、文字が潰れかけていて、普段私たちが書くような数字とは少しかけ離れています。

    したがって手書き数字データセットは28×28の比較的解像度が良くデータ数の多いMNISTデータセットがよく用いられます

    3. の正則化項については、過学習に陥ると重みが大きな値に発散してしまう現象がよく見られます

    【図 大きな値に分散した重みが見られる例】

    この現象から、重みが大きな値に発散しないように学習を進める手法があります。

    他にも、Dropoutなど過学習を抑制する手法はいくつかありますがここでは割愛します。

    実験に時間がかかる上にコーディングが複雑に

    これも今回の実験でわかりますが、ネットワークが大きくなるほど、膨大な計算時間がかかります。

    また、先のDropoutや正則化項などの新たな手法を加えていくほどコーディングは複雑になってきます。

    自分でコードを書くことは大切ですが、最適なコードをかかないと無駄な処理が積み重なって時間がかかってしまい、トータルで見て時間と労力の無駄になることも多々あります。

    フレームワーク

    そこで、機械学習にはたくさんのフレームワークが提供されています。

    有名どころであれば、「Tensorflow」「Keras」などが挙げられます。

    これらは、基本的にPythonで動作します。

    それゆえに「機械学習といえばPython」と言われているのです。

    なぜ、同じPythonなのに処理速度に大きな差が出るかというと、Pythonで動く機械学習フレームワークの処理自体はCやC++で書かれているからです。

    さいごに

    今回は、自作の機械学習プログラムを使って、少し実用的な実験をしてみました。

    実装を通してわかるように、シンプルな機械学習であればゼロから構築することはさほど難しい話ではありません。

    しかし、ネットワークを大きくしたり、新たな機能を加えていくと一筋縄ではいかなくなってきます。

    ですので、ある程度機械学習のしくみを理解できたら、既存の優秀なフレームワークに移行して快適な機械学習ライフに切り替えてみましょう!

    また別の記事で、フレームワークを使った機械学習についてお話しできればと思います。

    前編はこちら

    featureImg2019.06.11【前編】自作の誤差逆伝播学習法で手書き数字を認識させてみよう!【機械学習】前編〜手書き数字を認識するプログラムを作る~誤差逆伝播学習法は、教師信号とネットワークの実際の出力との誤差情報と勾配降...

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

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

    featureImg2020.07.17ライトコード的「やってみた!」シリーズ「やってみた!」を集めました!(株)ライトコードが今まで作ってきた「やってみた!」記事を集めてみました!※作成日が新し...

    コード全体

    1import numpy as np
    2import matplotlib.pyplot as plt
    3
    4from PIL import Image, ImageOps  # New!
    5import random  # New!
    6
    7
    8class Network:
    9    rate = 0.1     # 学習率
    10    decay = 0.1    # 学習率減衰
    11    per = 10       # 何エポックごとに学習率を減衰させるか
    12
    13    epsilon = 1.0  # シグモイド関数の傾き
    14
    15    def __init__(self, *args):
    16        self.layers = list(args)  # 各層のニューロン数
    17        self.weights = ([])       # 各層間の重みベクトル
    18        self.patterns = ([])      # 訓練パターンベクトル
    19        self.labels = ([])        # 訓練パターンの教師ニューロンインデックス
    20
    21        # --- New! --- #
    22        self.test_patterns = ([])  # テストパターンベクトル
    23        self.test_labels = ([])    # テストパターンの教師ニューロンインデックス
    24        # ------------ #
    25
    26        self.outputs = ([])       # 各ニューロンの出力値ベクトル
    27
    28    def init_weights(self, a=0.0, b=1.0):
    29        """
    30        重みを[a, b]の一様乱数で生成
    31        :param a: minimum = 0.0
    32        :param b: maximum = 1.0
    33        :return:
    34        """
    35        for i in range(len(self.layers)):
    36            if i + 1 >= len(self.layers):
    37                break
    38            self.weights.append((b - a) * np.random.rand(self.layers[i + 1], self.layers[i]) + a)
    39
    40    # New!
    41    def load_optdigits(self):
    42        train = open('OptDigits/optdigits.tra', 'r')  # 訓練データ
    43        test = open('OptDigits/optdigits.tes', 'r')  # テストデータ
    44
    45        # 訓練データ
    46        lines = train.read().split()
    47
    48        # データをランダムにシャッフル
    49        random.shuffle(lines)
    50
    51        dataset = ([])
    52        for line in lines:
    53            pattern = line.split(',')
    54            dataset.append(pattern)
    55        train.close()
    56
    57        for pat in dataset:
    58            # 入力は[0,1]に正規化する
    59            self.patterns.append(list(map(lambda x: float(x)*(1.0/16.0), pat[0:-1])))
    60
    61            # OptDigitsは最後にラベル[0,1,2,...,9]がある
    62            self.labels.append(int(pat[-1]))
    63
    64        # テストデータ
    65        lines = test.read().split()
    66
    67        # データをレンダムにシャッフル
    68        random.shuffle(lines)
    69
    70        dataset = ([])
    71        for line in lines:
    72            pattern = line.split(',')
    73            dataset.append(pattern)
    74        test.close()
    75
    76        for pat in dataset:
    77            self.test_patterns.append(list(map(lambda x: float(x)*(1.0/16.0), pat[0:-1])))
    78
    79            # OptDigitsは最後にラベル[0,1,2,...,9]がある
    80            self.test_labels.append(int(pat[-1]))
    81
    82    @staticmethod
    83    def input_sum(inputs, weights):
    84        """
    85        前ニューロンからの入力和を返す関数.式(1)
    86        :param inputs:
    87        :param weights:
    88        :return inputs dot weights (float):
    89        """
    90        return np.dot(inputs, weights)
    91
    92    def output(self, inputs, weights):
    93        """
    94        ニューロンの出力を返す関数.式(2)
    95        :param inputs:
    96        :param weights:
    97        :return (0, 1):
    98        """
    99        return self.sigmoid(self.input_sum(inputs, weights))
    100
    101    def sigmoid(self, x):
    102        """
    103        シグモイド関数 (3)
    104        :param x:
    105        :return:
    106        """
    107        return 1.0/(1.0 + np.exp(-self.epsilon * x))
    108
    109    def forward(self, pattern):
    110        """
    111        順方向処理をする関数.
    112        :param pattern:
    113        :return 出力層ニューロンの出力値ベクトル [0,1]:
    114        """
    115        self.outputs.clear()  # まず出力値情報をクリア
    116
    117        out = ([])  # 前層ニューロンの出力ベクトル
    118        self.outputs.append(pattern)  # まず入力層ニューロンの出力(入力パターン)を追加
    119
    120        for layer in range(len(self.layers)):
    121            out = ([])
    122            if layer == 1:  # 第1中間層ならば入力パターンが前層ニューロンの出力となる
    123                for n in range(self.layers[layer]):
    124                    out.append(self.output(pattern, self.weights[layer-1][n]))
    125
    126                self.outputs.append(out)  # 出力値を追加
    127
    128            elif layer > 1:  # 第1中間層以降の層では前層の出力を使う
    129                for n in range(self.layers[layer]):
    130                    out.append(self.output(self.outputs[-1], self.weights[layer-1][n]))
    131
    132                self.outputs.append(out)  # 出力値を追加
    133
    134        return out
    135
    136    def backward(self, pattern):
    137        """
    138        誤差逆伝播学習法の大枠
    139        :param pattern:
    140        :return:
    141        """
    142        deltas = self.calc_delta(pattern)  # δを計算
    143        for l in reversed(range(1, len(self.layers))):  # ネットワークを逆順処理していく
    144            for j, jj in enumerate(self.outputs[l]):
    145                for i, ii in enumerate(self.outputs[l-1]):
    146                    self.weights[l-1][j][i] += self.rate * deltas[l-1][j] * ii  # 重みを更新
    147
    148    # 一部変更
    149    def calc_delta(self, pattern):
    150        """
    151        δを計算する関数
    152        :param pattern: パターンインデックス
    153        :return: δベクトル
    154        """
    155        teacher = [0.1] * 10  # 10クラス分の教師ニューロンを作成
    156        teacher[self.labels[pattern]] = 0.9  # ラベルに該当する教師ニューロンの出力を0.9157        deltas = ([])  # 全ニューロンのδ
    158
    159        for l in reversed(range(1, len(self.layers))):  # ネットワークを逆順処理していく
    160            tmp_delta = ([])
    161            for j in range(self.layers[l]):
    162                if l == len(self.layers) - 1:  # 最終層ならば
    163                    delta = self.epsilon * \
    164                            (teacher[j] - self.outputs[l][j]) * \
    165                            self.outputs[l][j] * (1 - self.outputs[l][j])
    166
    167                    tmp_delta.append(delta)
    168
    169                else:  # 最終層以外
    170                    t_weights = np.array(self.weights[l]).T
    171                    delta = self.epsilon * \
    172                            self.outputs[l][j] * (1 - self.outputs[l][j]) * \
    173                            np.dot(deltas[-1], t_weights[j])
    174
    175                    tmp_delta.append(delta)
    176
    177            deltas.append(tmp_delta)
    178
    179        return deltas[::-1]  # δは逆順で返す
    180
    181    def train(self, epoch):
    182        """
    183        訓練関数
    184        :param epoch:
    185        :return:
    186        """
    187        trains = ([])
    188        tests = ([])
    189        for e in range(epoch):  # 最大学習回数で回す
    190            train_acc = self.validate()
    191            test_acc = self.test()
    192            for p, pat in enumerate(self.patterns):  # 入力パターンごとに回す
    193
    194                self.forward(pat)  # 順伝播処理で出力値を計算
    195
    196                self.backward(p)  # 逆伝播で重みを更新
    197
    198            print(str(e+1) + ' / ' + str(epoch) + ' epoch.')
    199            print('[ Training Acc.: ' + str(train_acc), ', Test Acc.: ' + str(test_acc) + ' ]')
    200
    201            if (e+1) % self.per == 0:  # 学習率減衰
    202                self.rate *= self.decay
    203
    204            trains.append(train_acc)  # 訓練精度
    205            tests.append(test_acc)    # テスト精度
    206
    207        #  精度を描画
    208        plt.xlabel('epochs')
    209        plt.ylabel('accuracy (%)')
    210        plt.plot(trains, label='Train')
    211        plt.plot(tests, label='Test')
    212        plt.legend()
    213        plt.savefig('error.png', dpi=300)
    214        plt.close()
    215
    216        # 重みのヒストグラムを描画
    217        ws = ([])
    218        for lw in self.weights:
    219            for iw in lw:
    220                for w in iw:
    221                    ws.append(w)
    222        plt.hist(ws, bins=100, ec='black')
    223        plt.xlabel('Weights')
    224        plt.ylabel('Frequency')
    225        plt.savefig('weights_hist.png', dpi=300)
    226
    227    # 関数名を変更
    228    def validate(self):
    229        """
    230        訓練精度を計算
    231        :return: accuracy (%)
    232        """
    233        correct = 0
    234        for p in range(len(self.patterns)):
    235            self.forward(self.patterns[p])
    236            max = 0
    237            ans = -1
    238            # 一番出力値の高いニューロンを取ってくる
    239            for o, out in enumerate(self.outputs[len(self.layers)-1]):
    240                if max < out:
    241                    max = out
    242                    ans = o
    243            # もしそのニューロンの番号とラベルの番号があっていれば正解!
    244            if ans == self.labels[p]:
    245                correct += 1
    246
    247        accuracy = correct / len(self.patterns) * 100
    248        return accuracy
    249
    250    # New!
    251    def test(self):
    252        """
    253        テスト精度を計算
    254        :return: accuracy (%)
    255        """
    256        correct = 0
    257        for p in range(len(self.test_patterns)):
    258            self.forward(self.test_patterns[p])
    259            max = 0
    260            ans = -1
    261            # 一番出力値の高いニューロンを取ってくる
    262            for o, out in enumerate(self.outputs[len(self.layers)-1]):
    263                if max < out:
    264                    max = out
    265                    ans = o
    266            # もしそのニューロンの番号とラベルの番号があっていれば正解!
    267            if ans == self.test_labels[p]:
    268                correct += 1
    269
    270        accuracy = correct / len(self.patterns) * 100
    271        return accuracy
    272
    273    # New!
    274    def prop_my_digits(self, img_path):
    275        """
    276        自分で作った画像をネットワークに流して出力を得る関数。
    277        :param img_path:
    278        :return:
    279        """
    280        img = Image.open(img_path).convert('L')  # グレースケールで画像を読み込む
    281        resized_img = img.resize((8, 8))  # 画像リサイズ
    282        input_img = ImageOps.invert(resized_img)  # ネガポジ(白黒)反転
    283        array = np.array(input_img) * (1.0/255.0)  # [0,1]に変換
    284
    285        # リサイズした画像を保存して確認する場合
    286        # plt.imshow(array)
    287        # name = img_path + '_resize.png'
    288        # plt.savefig(name, dpi=300)
    289
    290        input_pattern = ([])
    291        for h in array:
    292            for w in h:
    293                input_pattern.append(w)  # 1次元の配列に変換
    294        ans = np.array(self.forward(input_pattern)).argmax()  # 出力値の大きいニューロンのインデックスを取得
    295        print(img_path + ' is ', ans)  # ネットワークの識別結果を出力
    296
    297
    298if __name__ == '__main__':
    299    # ネットワークを作成
    300    net = Network(64, 10)
    301    net.init_weights(-0.2, 0.2)
    302
    303    # データセットを読み込み
    304    net.load_optdigits()
    305
    306    #  誤差逆伝播学習法
    307    net.train(30)
    308
    309    # 最終的な訓練精度とテスト精度を出力
    310    acc = net.validate()
    311    print('Training Accuracy: ' + str(acc) + '%')
    312    acc = net.test()
    313    print('Test Accuracy: ' + str(acc) + '%')
    314
    315    # 自分の手書き数字を認識させてみる
    316    net.prop_my_digits('myDigits/0.png')
    317    net.prop_my_digits('myDigits/1.png')
    318    net.prop_my_digits('myDigits/2.png')
    319    net.prop_my_digits('myDigits/3.png')
    320    net.prop_my_digits('myDigits/4.png')
    321    net.prop_my_digits('myDigits/5.png')
    322    net.prop_my_digits('myDigits/6.png')
    323    net.prop_my_digits('myDigits/7.png')
    324    net.prop_my_digits('myDigits/8.png')
    325    net.prop_my_digits('myDigits/9.png')

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

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

    採用情報へ

    広告メディア事業部

    広告メディア事業部

    おすすめ記事

    エンジニア大募集中!

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

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

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

    background