二値データを整える

データは様々な形で与えられる。その中でもよく扱うのは、二値(バイナリ)データ。本当にゼロとイチでの表現なので、読み書きの際は一定の注意が必要であるが、何も難しいことはない。

データセットの概要

二値データの事例は数えきれぬほどあるが、今回は著名なMNIST手書き文字のデータに着目する。

Stimuli Image

ダウンロード方法はいくつもあるが、Y. LeCun氏のサイト( http://yann.lecun.com/exdb/mnist/ )から入手することが多い。彼らの説明には、

"The MNIST database of handwritten digits, available from this page, has a training set of 60,000 examples, and a test set of 10,000 examples. It is a subset of a larger set available from NIST. The digits have been size-normalized and centered in a fixed-size image"

とあり、その中身を収容するファイルが下記の4つである。

train-images-idx3-ubyte.gz: training set images (9912422 bytes) train-labels-idx1-ubyte.gz: training set labels (28881 bytes) t10k-images-idx3-ubyte.gz: test set images (1648877 bytes) t10k-labels-idx1-ubyte.gz: test set labels (4542 bytes)

いずれもIDXという二値形式で保存されている。まずは圧縮されたファイルを解凍する。

$ cd data/MNIST
$ gunzip train-images-idx3-ubyte.gz
$ gunzip train-labels-idx1-ubyte.gz
$ gunzip t10k-images-idx3-ubyte.gz
$ gunzip t10k-labels-idx1-ubyte.gz

残るのは肝心のバイナリデータだけだ。


入力パターンを調べる

まずは、訓練データのファイルを開くことにしよう。

In [1]:
import numpy as np
import matplotlib.pyplot as plt

toread = "data/MNIST/train-images-idx3-ubyte"

f_bin = open(toread, mode="rb")

print(f_bin)
<_io.BufferedReader name='data/MNIST/train-images-idx3-ubyte'>

ここで、正しく読み込めているかどうか確かめる必要が出てくる。その唯一の方法は、データの提供者が「あるはずだ」という内容と照合することである。 LeCunらの説明より:

TRAINING SET IMAGE FILE (train-images-idx3-ubyte):
[offset] [type]          [value]          [description]
0000     32 bit integer  0x00000803(2051) magic number
0004     32 bit integer  60000            number of images
0008     32 bit integer  28               number of rows
0012     32 bit integer  28               number of columns
0016     unsigned byte   ??               pixel
0017     unsigned byte   ??               pixel
........
xxxx     unsigned byte   ??               pixel

ここでいう「offset」とは、ファイル開始点から何バイト読み進んだか示す数値である。ゼロならば一バイトも読んでおらず、1バイト目を目前にする地点である。offsetが0004であれば5バイト目、0008であれば9バイト目など、以下同様。期待どおりの中身が読み込めているかチェックしてみよう。

In [2]:
print("First four bytes:") # should be magic number, 2051.
b = f_bin.read(4)
print("bytes: ", b)
print(" int: ", int.from_bytes(b, byteorder="big", signed=False))
First four bytes:
bytes:  b'\x00\x00\x08\x03'
 int:  2051

ここに出てきたPythonのバイトデータb'\x00\x00\x08\x03'は、最初の4バイトを十六進法(hexadecimal)の表記で示したものである。上記の表の一行目の「value」、つまり0x00000803と一致しているので、一安心。これらの\xはただ単にバイトの区切りを表している。思い出してみると、十六進法の2桁を使うと、$0, 1, 2, \ldots$から$(15 \times 16^{1} + 15 \times 16^{0}) = 255$までの整数を表現することができる。二進法の8桁、つまり8ビット(=1バイト)と同じである。さて、十進法に変換して$3 \times 16^{0} + 8 \times 16^{2} = 2051$が得られるので、期待どおりの「magic number」である。

次は、readというメソッドを使い、4バイトずつ読み進めるようにして、残りの内容を確認していく。

In [3]:
print("Second four bytes:") # should be number of imgs = 60000
b = f_bin.read(4)
print("bytes: ", b)
print(" int: ", int.from_bytes(b, byteorder="big", signed=False))
Second four bytes:
bytes:  b'\x00\x00\xea`'
 int:  60000
In [4]:
print("Third four bytes:") # should be number of rows = 28
b = f_bin.read(4)
print("bytes: ", b)
print(" int: ", int.from_bytes(b, byteorder="big", signed=False))
Third four bytes:
bytes:  b'\x00\x00\x00\x1c'
 int:  28
In [5]:
print("Fourth four bytes:") # should be number of cols = 28
b = f_bin.read(4)
print("bytes: ", b)
print(" int: ", int.from_bytes(b, byteorder="big", signed=False))
Fourth four bytes:
bytes:  b'\x00\x00\x00\x1c'
 int:  28

これでもっとも重要なファイル情報を無事に入手できた。これを使って、肝心の入力パターンを取り出していくことにしよう。幸いにも、デジタル画像の画素値なので、中身のもっともらしさは目視でほぼ確認できる。

In [6]:
n = 60000 # (anticipated) number of images.
d = 28*28 # number of entries (int values) per image.
times_todo = 5 # number of images to view.
bytes_left = d
data_x = np.zeros((d,), dtype=np.uint8) # initialize.

データの提供者の説明より:

"Pixels are organized row-wise. Pixel values are 0 to 255. 0 means background (white), 255 means foreground (black)."

つまり、画素値が0〜255の整数値を取り、2次元画像の一行ずつ読んでいくことになる。値の範囲がわかっているので、uint8(無符号の1バイト整数)のデータ型とする。まだ読んでいない中身として、

0016     unsigned byte   ??               pixel
0017     unsigned byte   ??               pixel
........
xxxx     unsigned byte   ??               pixel

と1バイトずつファイルの最後まで読んでいくと、ちょうど60000枚の画像の全画素を読み込むことになるはずである。1枚につき画素数が$28 \times 28 = 784$である。

In [7]:
for t in range(times_todo):

    idx = 0
    while idx < bytes_left:
        # Iterate one byte at a time.
        b = f_bin.read(1)
        data_x[idx] = int.from_bytes(b, byteorder="big", signed=False)
        idx += 1

    img_x = data_x.reshape( (28,28) ) # populate one row at a time.
    
    # binary colour map highlights foreground (black) against background(white)
    plt.imshow(img_x, cmap=plt.cm.binary)
    #plt.savefig(("MNIST_train_"+str(t)+".png"))
    plt.show()

f_bin.close()
if f_bin.closed:
    print("Successfully closed.")
Successfully closed.

練習問題 (A):

  1. 上記のコードに出没するintのメソッドfrom_bytesを使うとき、引数signedFalseからTrueへ変えてみてください。変換の結果が変わるか。変わる場合は、例示しながら説明すること。なぜその変化が起こるかも併せて説明すること。

  2. 同様にbyteorder"big"から"little"へ変換し、何がどう変わるか調べてみてください。背景はhelp(int.from_bytes)を参照すること。

  3. 多数のカラーマップが提供されている( https://matplotlib.org/users/colormaps.html )。上記コード内のbinaryに代わって、たとえばgraybonepinkなどほかの配色方法を試してみてください。

  4. コメント記号で無効になっているsavefigを「活き」の状態にし、最初の十枚の画像をディスクに保存すること。また、テスト用のデータについても同様の操作を行うこと(ファイル名は適宜変更すること)。


ラベルを調べる

「何が書いてあるか」という典型的な識別課題を見据えて、「事例」としての画像が正しく読み込まれている模様である。正解ラベルはtrain-labels-idx1-ubyteという別のファイルに格納されているため、次はラベルに焦点を当てる。最初のtimes_todo枚の画像と対応するはずのラベルを照らし合わせることにしよう。

In [8]:
toread = "data/MNIST/train-labels-idx1-ubyte"

f_bin = open(toread, mode="rb")

print(f_bin)
<_io.BufferedReader name='data/MNIST/train-labels-idx1-ubyte'>

正解ラベルのファイルについては、再びLeCun等の説明を参考にする:

TRAINING SET LABEL FILE (train-labels-idx1-ubyte):
[offset] [type]          [value]          [description]
0000     32 bit integer  0x00000801(2049) magic number (MSB first)
0004     32 bit integer  60000            number of items
0008     unsigned byte   ??               label
0009     unsigned byte   ??               label
........
xxxx     unsigned byte   ??               label

最初の8バイトを見ていく。

In [9]:
print("First four bytes:") # should be magic number, 2049.
b = f_bin.read(4)
print("bytes: ", b)
print(" int: ", int.from_bytes(b, byteorder="big"))
First four bytes:
bytes:  b'\x00\x00\x08\x01'
 int:  2049
In [10]:
print("Second four bytes:") # should be number of observations, 60000.
b = f_bin.read(4)
print("bytes: ", b)
print(" int: ", int.from_bytes(b, byteorder="big"))
Second four bytes:
bytes:  b'\x00\x00\xea`'
 int:  60000

以降はデータだけである。ラベルの値は0から9までの整数値を取るという。事例とラベルが一致していることを確かめる。

In [11]:
for t in range(times_todo):
    b = f_bin.read(1)
    mylabel = int.from_bytes(b, byteorder="big", signed=False)
    print("Label =", mylabel)
    
Label = 5
Label = 0
Label = 4
Label = 1
Label = 9

練習問題 (B):

  1. 最初の10枚分のラベルを表示し、先ほど調べた画像の中身と合っているかどうか確かめること(訓練データと検証データ、両方とも)。

  2. (おまけ) 上記のごとくreadを使うと一方的に前進するだけだが、seekというメソッドを使うことで任意のバイトオフセットに飛ぶことができて、便利である。上の例でいうと、f_bin.seek(0)でファイル開始点に戻り、f_bin.seek(8)で最初のラベルを読み込む直前の地点へ移ることができる(最初の8バイトは補足情報)。この点を念頭において、seekを用いて、$k$という整数を与えられたときに$k$番目の画像の正解ラベルを表示するような関数を造りなさい。

  3. (おまけ) 同様に、seekを使って$k$番目の画像を表示するような関数も造りなさい。


作業用のデータを仕上げる

やや粗雑ではあったが、先ほどのようにデータをざっくりチェックすることで、正しく読み込めているかどうかは概ねわかる。

次は、機械学習という「作業」に使うことを前提に、高速に読める形式にすべてのデータを換えることにする。先ほどのやり方では、1バイトずつ読むのに一定の時間がかかるのだが、Python特有のバイナリ形式に変換すると、きわめて高速に読み込むことができる(但し、読むマシンと書くマシンが同一であることとする)。

第一歩として、訓練データからすべての画像分の画素値を読み込んでおく。

In [12]:
toread = "data/MNIST/train-images-idx3-ubyte"
n = 60000
d = 28*28
bytes_left = n * d
data_X = np.empty((n*d,), dtype=np.uint8)

with open(toread, mode="rb") as f_bin:

    f_bin.seek(16) # go to start of images.
    idx = 0
    
    print("Reading binary file...", end=" ")
    while bytes_left > 0:
        b = f_bin.read(1)
        data_X[idx] = int.from_bytes(b, byteorder="big", signed=False)
        bytes_left -= 1
        idx += 1
    print("Done reading...", end=" ")
print("OK, file closed.")
Reading binary file... Done reading... OK, file closed.

無符号の整数uint8型を使い、 一つの長いベクトル(配列)に全画像のデータを羅列した。いくつかの統計量を見てみよう。

In [13]:
print("Min:", np.min(data_X))
print("Mean:", np.mean(data_X))
print("Median:", np.median(data_X))
print("Max:", np.max(data_X))
print("StdDev:", np.std(data_X))

print(np.bincount(data_X))
Min: 0
Mean: 33.318421449829934
Median: 0.0
Max: 255
StdDev: 78.56748998339798
[38045844    22896    33653    36040    38267    39148    37692    38856
    30878    38234    35282    36020    30139    40100    26939    28869
    29115    27551    26849    34431    29955    35496    26750    22910
    25950    29995    24260    24025    25434    37160    22913    26205
    28890    15556    19906    21516    22128    24760    25922    18250
    20675    27023    22349    21227    19030    21122    17326    24237
    20083    17919    23964    25003    14588    19230    18195    18068
    23511    31905    14330    18140    18144    18133    19805    23909
    46754    16050    17514    15914    16302    16742    19288    18444
    17313    19307    13816    15875    17877    13535    17569    18085
    15872    16527    21112    15514    27088    25496    25837    12645
    15796    17628    12695    17876    18525    17225    16655    16244
    17902    14246    16820    17710    15217    14210    21721    14854
    16395    14871    18334    16385    17914    18293    15737    16052
    18288    19994    24774    15056    19329    14702    17061    15472
    16419    17300    16392    17752    13804    16490    16800    16455
    74946    16409    17908    19269    18685    15030    15490    16616
    16088    17451    17378    19446    17683    27102    17965    15986
    18827    18303    15873    16743    17012    16590    16943    21650
    17821    15547    17938    18754    18648    14733    16788    15946
    18066    17143    17994    18337    15435    15288    15029    14355
    25069    25562    29853    16744    18505    19517    16904    16539
    17251    16800    16862    18169    17153    17183    16052    20007
    18052    17278    17009    16589    17257    18658    20487    51987
    19899    18840    18961    20001    22659    23195    26112    18878
    18097    17640    19653    28907    18440    19399    21994    20848
    18141    19604    21901    22590    23084    25289    25724    24176
    21708    24890    21828    19632    20554    24794    25839    26221
    24175    27285    35378    25350    29370    23646    26957    29365
    30253    36800    33219    29130    31555    29676    26914    27474
    33422    35573    31218    37029    37199    35397    33844    37941
    35418    39342    59559   117808   653888  1513207   801557   314282]

ここで注意すべきことがある。識別機を設計するときに使うモデルにもよるが、入力ベクトルの数値が大きすぎると、後の計算では不都合である。今回のデータをそのままにしておくと$\{0,1,\ldots,255\}$から値を取るが、このような数値を指数関数にかますモデル(例:ロジスティック回帰など)では格納できないほど大きな数値が出てしまうおそれがある。

このような事態を回避すべく、正規化して単位区間へ写像する。公式は(値 - 最小値)/(最大値 - 最小値)と至って簡単。整数よりも浮動小数点数のほうが容量を食うが、学習が楽になる。

In [14]:
data_X_float = np.empty(data_X.shape, dtype=np.float32)
data_X_float = np.float32((data_X - np.min(data_X))/(np.max(data_X) - np.min(data_X)))
In [15]:
print("Min:", np.min(data_X_float))
print("Mean:", np.mean(data_X_float))
print("Median:", np.median(data_X_float))
print("Max:", np.max(data_X_float))
print("StdDev:", np.std(data_X_float))
Min: 0.0
Mean: 0.13066062
Median: 0.0
Max: 1.0
StdDev: 0.30810776

あとはnumpytofileを用いて、ディスクに書き込むだけだ。ここで重要なのはデータのdtypeを備忘しておくこと。書くときは何も指定はしないが、fromfileを使ってファイルの中身を読もうとするときにdtypeを誤ると、正しく読めなくなってしまうので要注意である。

In [16]:
print("Writing binary file...", end=" ")
towrite = "data/MNIST/X_tr.dat"
with open(towrite, mode="bw") as g_bin:
    data_X_float.tofile(g_bin) # don't forget the dtype used.
print("OK.")
Writing binary file... OK.

では、書き込んだファイルを新たに読み込み、正しく再構成できるかどうか念のために確認してみよう。IDX形式から読む場合と比べて、格段に速くなっていることも確かである。

In [17]:
with open(towrite, mode="br") as g_bin:
    data_X_check = np.fromfile(g_bin, dtype=np.float32)
print("OK.")

print("Shapes:", data_X_check.shape, data_X_float.shape)
print("Difference =", np.linalg.norm(data_X_check-data_X_float))
OK.
Shapes: (47040000,) (47040000,)
Difference = 0.0

正解ラベルに対しても同様な操作をする。今回は入力を意味するXではなく、ラベルを表すyと呼ぶ。

In [18]:
toread = "data/MNIST/train-labels-idx1-ubyte"
n = 60000
bytes_left = n
data_y = np.empty((n,), dtype=np.uint8)

with open(toread, mode="rb") as f_bin:

    f_bin.seek(8) # go to start of the labels.
    idx = 0
    
    print("Reading binary file...", end=" ")
    while bytes_left > 0:
        b = f_bin.read(1)
        data_y[idx] = int.from_bytes(b, byteorder="big", signed=False)
        bytes_left -= 1
        idx += 1
    print("Done reading...", end=" ")
print("OK, file closed.")
Reading binary file... Done reading... OK, file closed.

先ほどと同様に、無符号の整数uint8型を利用し、すべての訓練データのラベルを集める。統計量を計算してみると下記の通りになる。

In [19]:
print("Min:", np.min(data_y))
print("Mean:", np.mean(data_y))
print("Median:", np.median(data_y))
print("Max:", np.max(data_y))
print("StdDev:", np.std(data_y))

print("Bin counts:")
print(np.bincount(data_y))

plt.hist(np.hstack(data_y), bins='auto')
plt.show()
Min: 0
Mean: 4.4539333333333335
Median: 4.0
Max: 9
StdDev: 2.889246360020012
Bin counts:
[5923 6742 5958 6131 5842 5421 5918 6265 5851 5949]

もう一度、Pythonの二値形式として書き込むこと、再構成を試すことにする。

In [20]:
print("Writing binary file...", end=" ")
towrite = "data/MNIST/y_tr.dat"
with open(towrite, mode="bw") as g_bin:
    data_y.tofile(g_bin) # don't forget the dtype used.
print("OK.")
Writing binary file... OK.
In [21]:
with open(towrite, mode="br") as g_bin:
    data_y_check = np.fromfile(g_bin, dtype=np.uint8)
print("OK.")

print("Shapes:", data_y_check.shape, data_y.shape)
print("Difference =", np.linalg.norm(data_y_check-data_y))
OK.
Shapes: (60000,) (60000,)
Difference = 0.0

練習問題 (C):

  1. 上記の一連の作業は訓練データのみでした。同じ作業を検証データに対しても行なうこと。訓練データの呼称としてX_try_trtraining dataから)を使ったが、検証データはX_tey_teと呼ぶことにしよう(test dataから)。それぞれの.datを先述のディレクトリーに保存しておいてください。

  2. 訓練・検証データ両方の正解ラベルのヒストグラムを画像として保存してください。ラベルの分布はおおむね一様か。頻出度の高い・低い数字はそれぞれどれか。訓練と検証とではこの傾向が変わるか。

  3. (おまけ)持っているデータの経験分布が平均ゼロ、分散1.0になるように標準化することが多い。画像データに対してこの操作を行なうために、まず配列のreshapeメソッドを使って、data_X_floatを長いベクトルから$n \times d$の行列に変換すること($n$は標本数、$d$は一枚辺りの画素数)。各列の平均、標準偏差を算出し、適当に引き算と割り算をすること。