社畜エンジニア発掘戦線

駆けだしAIエンジニア

MNIST手書き数字の画像識別(データ・セット)

おつかれさまです。

今回より、ディープラーニングの発展問題として手書き数字の認識問題にトライしたいと思います。問題と言っても、"how to"が多くて問題にしてると効率が悪いので、ここからは高校数学スタイルはやめて、流れにそってコードを書いていきたいと思います。

CONTENTS

MNISTの手書き数字

このMNISTという言葉ですが、"Mixed National Institute of Standards and Technology database"の略字で、0〜9までの手書き数字画像が7万枚パックになったデータセットです。画像は28x28のピクセルデータです。しかし、7万枚って気が遠くなりますね。

f:id:sutokun:20190321100859p:plain:w500
この7万枚の手書き画像をニューラルネットワークにつっこんで学習させて、例えば自分で書いた数字が0〜9のどれにあたるのかを判別させたりします。まぁ、ここの問題では数字を手書きできるようなソフトウェアを作るのはそれはそれで大変なので、学習が進むにつれて識別正解率がどう上昇していくかを確認できるようなプログラムを作ってみたいと思います。

データのダウンロード

ディープラーニングフレームワーク」という魔法の杖を使うと、コード一発でダウンロードもできるんですが、今回はもとのデータセットをPCにダウンロードして、がちゃがちゃ加工して、どんなふうになっているのか確認しながら扱っていきたいと思います。

データセット自体はYann LeCunという先生のホームページからダウンロードできます。
MNIST handwritten digit database, Yann LeCun, Corinna Cortes and Chris Burges


ホームページにとぶと英語なのでうへぇとなりますが、欲しいのは真ん中のデータです。

f:id:sutokun:20190321102518p:plain:w500

とりあえずリンクポチーして4つのデータをダウンロードします。これら4つのデータがMNISTのデータセットになります。

まず、データセットは7万枚の画像ですがここではトレーニング用とテスト用、それぞれ6万枚と1万枚に分かれています。なぜ、わざわざトレーニング用とテスト用の2つに分けるのかと言うと、学習が完了したニューラルネットワークがどれぐらい賢いかテストするときに、もし1度でも学習に用いた画像を使うと、このニューラルネットワークは「あ、これ進研ゼミでやったやつだ!」とウキウキして、正解率が上がってしまいます。そのためテストに用いる画像をあらかじめ分けておく必要があります。

また、画像データには正解ラベルがセットになっています。例えば「5」という画像には「5」という正解ラベルが対応します、あたりまえですね。この正解ラベルもトレーニング用とテスト用に分かれているので、全部で4つのデータとなります。

コードでダウンロード

ふつうにリンクポチーでダウンロードできるんですが、勉強も兼ねてPythonのコードでダウンロードしてみます。とりあえず「MNIST_DL.py」とか名前つけて、コードを書いていきます。


まずはホームページのURLにアクセスするライブラリ&モジュール(urllib.request)をインポートします。

import urllib.request

次にアクセスしたいURLをurl_baseという変数で定義しておきます。上のYann LeCun先生のURLです。

url_base = 'http://yann.lecun.com/exdb/mnist/'

次に、ダウンロードしたいデータの名前をリストでまとめておきます。後でまとめてダウンロードするときに使います。

dl_list = [
    'train-images-idx3-ubyte.gz',
    'train-labels-idx1-ubyte.gz',
    't10k-images-idx3-ubyte.gz',
    't10k-labels-idx1-ubyte.gz'
]


それから、データセットを保存するディレクトリ(フォルダ)のパスをdataset_dirで定義しておきます。デスクトップに「MNIST_test」とかフォルダを作って、そこへ保存することにします。まぁ、自分で好きなディレクトリを指定してください。

dataset_dir = '/Users/FUTOSHI/desktop/MNIST_test/'

ちなみに、ディレクトリ上で「command+opt+C」を押すと、そのディレクトリのパスがコピーできます。おお、便利!(まぁ、Windowsだったら常に上に出てますけどね)


これで準備は整ったので、ダウンロードを行うコードを書きます。ダウンロードにはurllibライブラリの「urllib.request.urlretrieve(目的のURL, 保存先のパス)」というメソッドを使います。目的のURLはurl_baseで定義したホームページにあるデータ:train-images-idx3-ubyte.gz(ひとつ目)です。なので、こんな感じ。

url_download = url_base + 'train-images-idx3-ubyte.gz'


次に保存先のパスですが、上で保存したいディレクトリのパスをdataset_dirで定義しておきました。これにファイル名をくっつければ完了です。これ、真ん中に区切りの'/'が必要なので注意。

file_path = dataset_dir + '/' + 'train-images-idx3-ubyte.gz'


最後にダウンロードを実行するコードを書いて完了です。

urllib.request.urlretrieve(url_download, file_path)

いったんプログラムを保存して、ターミナルで実行してみます。保存先に指定したフォルダに'train-images-idx3-ubyte.gz'がダウンロードされています、成功です、わーい。

f:id:sutokun:20190329101814p:plain

無事にデータをダウンロードできたところで、この処理をあと3回繰り返します。

…いやいや、それはめんどくさい。(てか、そもそも、ポチーでダウンロードできるのにわざわざコードを書いてる時点でもっとめんどくさいことをしている、、とか言わないで)

「繰り返し」といえばfor文です。この処理をfor文で書いてみたいと思います。「dl_list」が最初に名前をまとめておいたリストです。

for i in dl_list:
    file_path = dataset_dir + '/' + i 
    urllib.request.urlretrieve(url_base + i, file_path) 


こっちのコードに書き換えて、もう一度プログラムを実行して保存先のフォルダを確認してみます。

f:id:sutokun:20190329103223p:plain
おお、ちゃんと保存されてる、わーい。

ひとまず、これでデータのダウンロードは完了です。ポチーで終わるのに長くなりました、まぁ勉強にはなったかな。次はデータの展開に進みます。

MNIST_DL.py : 全体コード
github.com


データの展開

さて、MNISTのデータはダウンロードできたものの、使えるように展開して加工しなくてはいけません。Pythonのファイルを一度新しくして、適当に「MNIST_SAVE.py」とか名前をつけて、デートを展開するためのコードを書いていきます。

まず、ダウンロードしたデータを見てみると拡張子が.gzとあります。これはgzipという圧縮ファイルです、プログラミング独学マンのわたしは初めて見たのでうろたえました、、うろたえました!この圧縮ファイルを解凍するためには、gzipというライブラリをインポートしなければいけません。numpyも使うのでこちらもインポート。

import gzip
import numpy as np

Pythonのファイルが新しくなったので、保存したデータの名前をもう一度リスト化しておきます。

dl_list = [
    'train-images-idx3-ubyte.gz',
    'train-labels-idx1-ubyte.gz',
    't10k-images-idx3-ubyte.gz',
    't10k-labels-idx1-ubyte.gz'
]

あと、データを保存しているディレクトリももう一度書いておきます。

dataset_dir = '/Users/FUTOSHI/Desktop/MNIST_test'


では進めていきましょう。まずはひとつ目のデータを展開してみます。ここも、ファイルのパスを定義しておきます。

file_path = dataset_dir + '/' + dl_list[0]

このfile_pathを使ってデータを展開します。

data = gzip.open(file_path, 'rb')

open関数は「open(ファイルパス、処理モード)」という使い方です。読み込みモードはとりあえず「rb」にしとけって、田舎のばあちゃんが言ってました。とりあえず「バイナリー読み込みモード」というものです。ここはまた時間があるときに詳しく見てみることにします。とりあえずこのモードは下のテーブルみたいな分けられ方(他にもあるけど)をしています。

変数 処理モード
w テキスト書き込み
wb バイナリ書き込み
r テキスト読み込み
rb バイナリ読み込み


データは展開できたんですが、さらにread関数を使って読み込み、データとして返してもらわなければいけません。

data = data.read()

一度、printを使って中身を見てみましょう。

print(data)

\x00\x00\x00\x00\x00\x00{\xffY\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00
\x00\x10\xe6\xfd\xe2\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00I\xfd\xfd
\xe2\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x10\xf1\xfd\xfd\x8f\x00\x00
\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x0…

嗚呼ぁぁあああああ(ctr+c:強制終了)

なんか大量のバイナリがなだれ込んできました、そりゃ(バイナリで読み込んだんだから)そうか。ということは、分かる値に変換しなければいけません。今回はnumpyのfrombufferを使います。さらに画像のデータなので「np.uint8」も添えなければいけないらしい。

このfrombufferの詳細はリンク貼っときます。。。
【NumPy】np.frombuffer() – 既存のデータを高速に読み込みndarrayを生成する – 青の弾丸


とりあえず、コードを書いてみましょう。

data = np.frombuffer(data, np.uint8)
print(data) #[0 0 8 ... 0 0 0]

おお、なんかそれっぽい配列が見えた。この配列が画像ピクセルの値っぽい。MNISTの画像は28x28のピクセルで構成されているので、この要素数は「28x28x60000 = 47040000」のはず。lenで要素数を確認してみる。

print(len(data)) #47040016

ファッ!?なんか16個多ない??どうも、変換すると16個はみ出るらしいので、これをカットしなければいけません。カット自体は簡単で、frombufferに「offset=16」を添えると尻の16個を取り除くことができる。

data = np.frombuffer(data,np.uint8,offset=16)
print(len(data)) #47040000


ここまでdataという変数を上書き&上書きでブサイクな加工してきましたが、withブロックを使うとまとめて書くことができます。

with gzip.open(file_path, 'rb') as f:
    data = np.frombuffer(f.read(), np.uint8)


最後に、今のままでは1次元の数字配列が47040000個並んでいるだけなので、60000枚の画像に分けておきます。ひとつあたりの画像は「28x28 = 784」ピクセル、この配列にリシェイプしておきます。ここでデータの名前を「train_img」にしておきます。

train_img = data.reshape(-1, 784)

「reshape(-1, 784)」という型ですが、マイナスってなんぞや、なんか784ずつブツブツブツと切って配列にしていくらしい。今回はこれでうまくいくっぽいので、また時間があるときによく調べてみます。

print(train_img.shape) #(60000, 784)

おっしゃー、ひとつ目が完成。では残り3つ、ここは繰り返しのforを…と思ったんですが、画像と正解ラベルでデータ数も違うし、frombufferのはみ出し個数も違います(正解ラベルは8個だった)。頑張ってまとめてもあんまり恩恵ないかな〜と思い、そこまで行数もないので3つとも力技で書いちゃいます。


…あとは書き下すだけなので全体コードを参照ください(すいません、疲れた…)。


4つのデータは「train_img、train_label、test_img、test_label」という変数名で保存しました。最後に、この変数をひとつのリストにまとめておきます。この後、データを保存するときに使います。

dataset = [train_img,train_label,test_img,test_label]

ひとまずこれでデータの展開と加工が完了です。

pickle形式で保存

とりあえずそれらしいデータの形に加工することができました。MNISTの問題を扱うとき、毎回このコードを書かなければいけないのはなんともブサイクです。というわけで、加工したデータを保存して、簡単に取り出せるようにしておきましょう。


今回、加工したデータを保存する形式はpickle形式です。緑色の戦闘力高いヤツじゃなくて、pickleです。

pickleとはハンバーガーとかに入ってるアレ、ピクルスの単数形です。

f:id:sutokun:20190328113845j:plain:w200
ピクルスは塩漬けにして野菜をそのまま保存した食べ物ですが、この保存方法がこの名前を由来となっていて、データをそのままの形で保存する、ということのようです(よく分からんけど)。データの保存には、他にCSVやバイナリといった方法がありますが、このpickleはデータの読み込みや出力が手軽なのでpython界隈ではよく使われているようです、知らんかった。

とりあえず使ってみます、こちらもpickle形式を使うためのライブラリをインポート。

import pickle


次に、保存するフォルダのパスを指定しておきます。ここらへんの流れは上で書いた内容と同じですね。今回は「mnist.pkl」という名前で保存します。

save_file = dataset_dir + '/mnist.pkl'


データの書き込みにもopen関数を使います。今回は処理モードは書き込みなので「wb」となります。バイナリで保存します。

with open(save_file, 'wb') as f:
    pickle.dump(dataset, f) 

pickle.dumpで保存を実行します。


ここでプログラムを実行してみて、ちゃんと保存できているか、ディレクトリを確認してみます。

f:id:sutokun:20190330132749p:plain
おお、できてる(感動)、「.pkl」で保存することができました。

MNIST_SAVE.py : 全体コード
github.com


pickleデータのロード

無事にMNISTのデータをpickle形式で保存できました。では、このデータを再度ロードしてきちんとデータとして扱えるのか確認してみます。「MNIST_OPEN.py」とでも名前をつけて、また新しいPythonファイルにコードを書いていきます。

pickle形式のデータロードにはもちろんpickleのライブラリが必要なので、先にこちらをインポート。

import pickle


そして、pickleで保存したデータのファイルパスを指定しておきます。

save_file = '/Users/FUTOSHI/Desktop/MNIST_test/mnist.pkl'

データのロードにはもちろんopen関数を使います、これで3度目の登場。

with open(save_file, 'rb') as f:
    dataset = pickle.load(f)

datasetという変数に、pickle.load(f)でデータをロードします。上の方で、このpickleデータは配列で保存しました。pickleというと塩漬けの名の通り、このデータがそのままの形で保存されているはずです。

dataset = [train_img,train_label,test_img,test_label]


ということで、ロードしたデータを再度これらの変数に割り当てなければいけません。

train_img,train_label,test_img,test_label = dataset


これでデータのロードが完了です。きちんと画像、そして正解ラベルが格納できているか確認してみます。まずは画像をグラフで表示するので、そのライブラリのインポートから。いつものmatplotlibです。

import matplotlib.pyplot as plt


次に、何番目の画像をチョイスするか「n」で指定しておきます。例えばtrain_imgだったら、6万画像ファイルが1次元の数字配列784でまとめられています。なので、6万枚の中からn番目のデータをチョイスして、imgという変数に28x28でreshapeします。とりあえず10000番目の画像を展開してみましょう。

n = 10000
img = train_img[n].reshape((28, 28))

さて、準備は整いました。画像を表示してみます。printでその正解ラベルもターミナルに表示させてみます。

plt.imshow(img)
plt.show()

print(train_label[n])


ファイルを保存してプログラムを実行します。おお、「ゼロ」っぽい画像が出てきました。

f:id:sutokun:20190330224740p:plain:w300

そしてターミナルには

> 0


素晴らしい、成功です。たまたま画像とラベルがそろっただけかもしれないので、nの値を変えて何度か試してみましたが、ちゃんとマッチしている、大丈夫そうです。

MNIST_OPEN.py : 全体コード
github.com


まとめ

今回のデータ処理は機械学習の「前処理」と呼ばれる工程のようです。前処理とは苦痛を伴うのが一般的らしいですが、苦痛でした。いつも機械学習アルゴリズムばっかり考えていたので、この手のデータ処理はとても苦手です。けっこうゆるふわなところが多いので、また理解が深まればそれぞれ記事にしたいと思います。機械学習をしっかりするならばデータ処理とは嫌でも向き合わなければいけないので、いずれガッツリやるときが来るでしょう…。

次回から、このデータセットを使って画像識別の問題にトライしていきたいと思います。


元の記事
週末のディープラーニング - 社畜エンジニア発掘戦線

Twitter
世界の社畜 (@sekai_syachiku) | Twitter