MNISTデータファイルの内容を理解する～機械学習エンジニアなら知っておいて欲しいこと

## :pick: この記事の目的

画像認識技術を学ぶ者にとってまず初めに学ぶMNISTですが、提供されているデータファイルの内容を実際によく確認したことのある人はどれ程いらっしゃるでしょうか。

Python言語を学んだ人は、例えばmnist.pyのload_mnist()関数や、scikit-learnのfetch_mldata()関数などを呼び出せば画像データが配列の形でなんとなく簡単に得られるもの、と記憶している人も多いと思います。

しかしながら、なぜ簡単に得られるのか、実際の画像データはどのような形式で構成されているのか、各関数は何をやっているのか、などについてしっかり確認するとなると、なかなかそこまで確認していないケースも少なくないと思います。

この記事ではこの部分の、実際にはどんなデータがどんな形式で配布され、どのように扱えば良いか、についてしっかりと確認したいと思います。



## :pick: MNISTとは

機械学習やディープラーニングの学習において、画像認識の最初の課題例として定番となっているものが**MNIST(エムニスト)**です。

MNISTとは人が手で書いた0から9まで数字を大量に集めた画像データ集(※1)で、Yann LeCun氏らによってインターネット上で著作権無しに公開されています。

※1　訓練用データとして60,000枚、テストデータとして10,000枚用意されています。

学習者はこれらのデータを教師データとして取り込みモデルを学習させ、そのモデルをテストすることで学習がどのように実行されるかを学びます。MNISTはこのように学習教材として最もよく使用されています。

:paperclip: THE MNIST DATABASE of handwritten digits
http://yann.lecun.com/exdb/mnist/
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/399135/7e886497-bc6f-5755-9477-9dccacc01dbf.png)

MNISTのデータを画像に変換して表示すると、このような手書きの数字の画像となります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/399135/a90be33d-c748-f415-2f9d-c913f4800d87.png)
しかしながら、上記のサイトから画像データが提供されているわけではありません。
ダウンロードして実際に取得できるデータはバイナリーデータとなっています。
画像データとはどのような関係があるでしょうか。

詳しく見ていきましょう。



## :pick: MNISTデータファイルをダウンロードする

ではさっそくこちらのURLからMNISTデータが公開されているサイトを開き、データファイルをダウンロードします。
http://yann.lecun.com/exdb/mnist/

データファイルは以下の4つです。4つともダウンロードして下さい。
以下にそれぞれのファイルの用途も記載します。

　**①train-images-idx3-ubyte.gz**
　　訓練データのうち、学習対象となる画像データ
　**②train-labels-idx1-ubyte.gz**
　　訓練データのうち、正解データ(正解ラベル)
　**③t10k-images-idx3-ubyte.gz**
　　テストデータのうち、予測対象となる画像データ
　**④t10k-labels-idx1-ubyte.gz**
　　テストデータのうち、正解データ(正解ラベル)

## :pick: MNISTデータファイルの内容を確認する

ここではLinuxマシン上にこれらのファイルをダウンロードして内容を確認してみたいと思います。
MacOSの場合はLinuxマシンとほぼ同じ手順でファイル内容を確認できますが、Windowsマシンの場合はgzファイルの解凍ソフト及びバイナリエディタのインストールなど予め準備が必要となります。

ダウンロード後、gzファイルを解凍します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/399135/68811b18-fccd-6a78-c688-45fc7e9848f3.png)

ここで、どのファイル名にも語尾に"**ubyte**"と単語が付いていることに注目して下さい。
ubyteは、データの型が**符号無し8bit整数**であることを意味します。
つまり、いずれのファイルもその内容を8bitごとに区切り、それぞれのデータの塊を整数として読み取れば良いということになります。

さらに8bitは1バイトなので、ちょうど一文字分のデータに等しいことがわかります。
よってデータファイルの中身は、整数が隙間なく並べられていると考えれば良いことになります。

次に、ファイルの内容を参照するためのコマンドを紹介します。

　` od (ファイル名) `

このコマンドは、ファイルの内容を8進数や16進数として捉え、その内容を画面に出力するコマンドです。
基本的にバイナリーデータファイル(※)に対して使用されます。

※バイナリーデータとは、テキストデータに対するデータの形式で、人が読んでも理解できない無秩序な数値データと言えます。その実態は0と1の数字の並びで、コンピュータが扱いやすい形式となっています。一方、テキストデータとは人が認識できる文字をそのままの形式で保存したデータとなります。

※人はテキストデータでないと意味を理解できないためテキストデータを扱いますが、コンピュータにとってはバイナリーデータの方が都合が良い場合もあります。特に、ファイルサイズを小さくでき、言語やエンコーディング形式の違いなど意識せずに直接的に命令を実行できる点などがメリットと言えます。

odコマンドにはさまざまなオプションが用意されています。
今回は以下のオプションを使用します。

|オプション記号|内容|
|:---------:|:---|
|-A|一番左にある列のオフセットの基数を変更します。オフセットとは文字の開始位置を表す値です。今回はデータ形式が8進数なので、"o"を選択します("o"はデフォルト値)。|
|-j|入力バイトの開始位置の指定です。先頭から読み込むのではなく、指定したバイト位置からデータを読み込みます。今回の場合は1バイト=1文字に対応するため、読み飛ばす先頭の文字数です。|
|-N|出力バイト数を指定します。画面に表示するデータのバイト数です。今回の場合は1バイト=1文字に対応するため、画面に表示する文字数と捉えれば良いです。|
|-t|表示するバイナリデータの形式を指定します。符号なし整数で表示する場合は"u"、16進数で表示する場合は"x"を指定します。|

次に、それぞれのファイルごとの内容を見ていきます。


#### (1)データファイル　train-labels-idx1-ubyte

MNISTデータファイル提供サイトから、このデータファイルの内容について以下の説明が補足されています[補足1-1]。

[補足1-1]
![スクリーンショット 2021-10-22 18.43.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/399135/2efcc742-8a78-b214-6acb-084e2320cdd6.png)
上記補足①〜④は以下となります。

　①[offset] オフセットです。各行のデータの開始位置を示しています。
　②[type] データの種類です。どんなデータなのかを説明しています。
　③[value] データの値を示しています。
　④[description] データの説明です。

実際のデータの内容を見てみます。

[補足1-1]　(一行目)

まずは[補足1-1]の内容を確認します。

・一行目のオフセット①は"0000"なので、データはファイルの先頭から始まります。
・データの種類②は"32 bit integer"となっています。すなわち、4バイト分で整数値を表しています。
・データの値③は16進数で0x00000801、10進数で2049であると書かれています。
・データの説明④は、この数字がマジックナンバーであると書かれています。また、"MSB first"であるとも書かれています。マジックナンバーとは、当事者以外には意味がわからない数字のことです。"MSB first"については本記事の下部に「**MSB firstを理解する**」節を書きましたので、こちらをご参照下さい。

実際のファイルのデータを見てみましょう。
以下のコマンドを実行します。

```ruby:sample
od -Ao -j0 -N4 -tx train-labels-idx1-ubyte
```

このコマンドの意味は、**ファイルの内容を先頭(オフセット位置0)から4バイト(32bit)分読み出し、16進数形式で表示する。ただし、オフセット表示は8進数表示とする**という意味になります。

以下が実行結果です。
![スクリーンショット 2021-10-22 22.29.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/399135/efd94353-efb9-2fce-eb75-e041d25b093d.png)
読み取った値が16進数で表示されています。
ここで読み取った時のエンディアンは「リトルエンディアン」ですので、これをビッグエンディアンに変換すると以下となります(16進数を意味する"0x"を先頭に付加しています)。

>0x00000801　...(A)


上記の内容と、先ほど[補足1-1]で確認した、データの値③の値を比較してみましょう。
データの値③は16進数形式で"0x00000801"です(B)。
※**MNISTデータファイルは"MSB first"です。つまり、ビッグエンディアンです。**

(A)及び(B)から実際のデータファイルの内容が補足の通りであることが確認できました。
(A)の導出は以下のようにバイトデータをエンディアン変換するようにして下さい。
![image (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/399135/d498b5f4-47da-d10b-ecf1-b5b6ea9b70c1.png)


[補足1-1]　(二行目)

[補足1-1]の二行目の内容を確認します。

・二行目のオフセット①は"0004"なので、データはファイルの5文字目先頭から始まります。
・データの種類②は"32 bit integer"となっています。すなわち、4バイト分で整数値を表しています。
・データの値③は10進数で60000であると書かれています。
・データの説明④は、「number of items」つまりデータの数という意味です。

実際のファイルのデータを見てみましょう。
以下のコマンドを実行します。

```ruby:sample
od -Ao -j4 -N4 -tx train-labels-idx1-ubyte
```

このコマンドの意味は、ファイルの内容をオフセット位置4から4バイト(32bit)分読み出し、16進数形式で表示する。ただし、オフセット表示は8進数表示とするという意味になります。

以下が実行結果です。
![スクリーンショット 2021-10-23 0.15.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/399135/fa60925a-ed87-5a1c-c938-104cdf93697a.png)

オフセットが"0000004"なので4バイト目から始まります。
読み取った値は"60ea0000"ですが、これはリトルエンディアンの16進数ですので、一行目同様にエンディアンを変換すると以下となります(16進数を意味する"0x"を先頭に付加しています)。

>0x0000ea60

さらにこの値を10進数に変換すると60000となります。
先ほど[補足1-1]で確認した、データの値③の値に一致しました。

[補足1-1]　(三行目)

三行目から最終行までは同じ形式となります。

[補足1-1]の内容を確認します。

・一行目のオフセット①は"0008"なので、データはファイルの9文字目先頭から始まります。
・データの種類②は"unsigned byte"となっています。これは符号なし整数という意味です。
・データの値③は"??"と書かれています。行ごとに異なる値をとるため、どんな値かはわからないという意味です。
・データの説明④は"label"となっています。正解ラベルデータということを意味しています。

さらにオフセット①部分に注目すると、この行以降のオフセットの値は1ずつ増えていることがわかります。どの行もその長さが1バイトとなります。つまり、1文字です。

実際のファイルのデータを見てみましょう。
以下のコマンドを実行します。

```ruby:sample
od -Ao -j8 -N1 -tu train-labels-idx1-ubyte
```

このコマンドの意味は、ファイルの内容をオフセット位置8から1バイト分読み出し、10進数形式で表示する。ただし、オフセット表示は8進数表示とするという意味になります。
数値の値を10進数で確認したいためにオプションを"-tu"としています。

以下が実行結果です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/399135/f88f6a3b-0553-ee18-2a15-d518a662aa7b.png)

得られた結果は"5"でした。
これは正解ラベルの数字の値が"5"であることを意味しています。つまり、**手書き数字の画像の数字は"5"であることを教えています。**

試しに、四行目～六行目の三行分を同じように見てみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/399135/41abccaf-2f54-e04c-1108-e53146398d94.png)
ご覧の通り、正解ラベルの値は"0"、"4"、"1"と得られました。

ここで見た値がMNISTデータファイルの最も大事な部分です。
今回見ているファイルは訓練データのうちの正解ラベルデータです。
正解ラベル数は60,000行分あり、それぞれの値はデータファイルの9バイト目から1バイトずつ読み取れば良いことがわかりました。

#### (2)データファイル　train-images-idx3-ubyte

MNISTデータファイル提供サイトから、このデータファイルの内容について以下の説明が補足されています[補足2-1]。

[補足2-1]
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/399135/122b36a1-cda7-c26d-2037-02631d9c9a53.png)
上記補足①〜④は前回確認したデータファイル「train-labels-idx1-ubyte」と同様ですが、以下となります。

　①[offset] オフセットです。各業のデータの開始位置を示しています。
　②[type] データの種類です。どんなデータなのかを説明しています。
　③[value] データの値を示しています。
　④[description] データの説明です。

また、データの内容を見てみます。

[補足2-1]　(一行目)

一行目はナジックナンバーです。特に注目する部分ではないので、ここは飛ばします。

[補足2-1]　(二行目)

前回同様に、データの行数が60,000行あることを表しています。

[補足2-1]　(三行目)

[補足2-1]の三行目の内容を確認します。

・三行目のオフセット①は"0008"なので、データはファイルの9文字目先頭から始まります。
・データの種類②は"32 bit integer"となっています。すなわち、4バイト分で整数値を表しています。
・データの値③は10進数で28であると書かれています。
・データの説明④は「number of rows」で、行数を意味しています。行数とは、この後ピクセル(pixel)について説明しますが、手書き文字の画像が何行分のピクセルで構成されているか(縦方向に何ピクセルあるか)を説明する値です。

実際のファイルのデータを見てみましょう。
以下のコマンドを実行します。

```ruby:sample
od -Ao -j8 -N4 -tx train-images-idx3-ubyte
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/399135/e3932bff-0e23-e9ca-d018-685164cab52e.png)

ビッグエンディアンに置き換えると、

> 0x0000001c

となり、10進数では28です。
確かに[補足2-1]の通りです。

[補足2-1]　(四行目)

[補足2-1]の四行目の内容を確認します。

・四行目のオフセット①は"0012"なので、データはファイルの13文字目先頭から始まります。
・データの種類②は"32 bit integer"となっています。すなわち、4バイト分で整数値を表しています。
・データの値③は10進数で28であると書かれています。
・データの説明④は「number of columns」で、行数を意味しています。手書き文字の画像が何列分のピクセルで構成されているか(横方向に何ピクセルあるか)を説明する値です。

実際のファイルのデータを見てみましょう。
以下のコマンドを実行します。

```ruby:sample
od -Ao -j12 -N4 -tx train-images-idx3-ubyte
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/399135/948313c6-c843-6daf-cde2-eec2fe055e65.png)

結果は三行目の結果と同じで、10進数で28です。
確かに[補足2-1]の通りです。

[補足2-1]　(五行目)

五行目から最終行までは同じ形式となります。
[補足2-1]の五行目の内容を確認します。

・五行目のオフセット①は"0016"なので、データはファイルの17文字目先頭から始まります。
・データの種類②は"unsigned byte"で、符号なし整数です。
・データの値③は何らかの整数が書かれています。行によりその整数は異なります。
・データの説明④は「pixel」で、画像1ピクセル(pixel)の情報であることを意味しています。ピクセルと数値の関係についてはこの後に説明します。


実際のファイルのデータを見てみましょう。
以下のコマンドを実行します。

```ruby:sample
od -Ao -j17 -N4 -tu train-images-idx3-ubyte
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/399135/284f90fb-7b50-0305-0050-48ef9a00bd14.png)

得られた結果は0でした。画像1ピクセルの情報が0であることを意味しています。
これ以降のデータはオフセットの値を参考に4バイトずつ順番に読み取れば良さそうです。
ただし、最終行まで一律に読み取るのではなく、以下に説明する画像データのピクセルと行列数を考慮する必要があります。

:diamonds: **画像データのピクセルと行列数について**

今回扱うのは数字の手書き文字を画像にした画像のデータですが、画像データがどう数値で表されるかを考慮する必要があります。

画像は下図のように、**ピクセル(pixel)**と呼ばれる最小単位の画素で構成されます。
ピクセルは単色の正方形で、タイルのようなものと考えれば良いです。小さなタイルがたくさん集まって大きな画像を作っているということになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/399135/11b36680-c048-3bd0-a8df-1140b7a57db6.png)
ピクセルは一般的にはカラー値(R：赤,G：緑,B：青)の値をとりますが、グレースケールの場合は0～255までの値で示されます。
さらにピクセルは縦方向横方向の二次元状に並んでいて、それぞれがいくつ並んでいるかで画像の大きさが決まります。

ここで、[補足2-1]（三行目）データの説明④の「number of rows」と（四行目）データの説明④「number of columns」にある通り、**MNISTの画像データは28(縦)x28(横)ピクセルで構成されます**。

次に、[補足2-1]の説明の最下部に以下のような説明書き[補足2-2]があります。

[補足2-2]
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/399135/d22ba458-606f-fafb-23de-9443abc8dc59.png)
これはMNISTのデータの並びを理解する上で重要な情報となります。

「Pixels are organized row-wise.」とは、ピクセル情報が**row-wise型**で並んでいるという意味です。row-wise型とは、下図の左枠のようなデータの並び方を指します。row-wise型の他にはcolumn-wise型があります。

|名称|データの並び方|
|:--:|:-----------|
|row-wise型|データは横方向に先に並び、一行分が終わったら二行目に移り、これを繰り返す。|
|column-wise型|データは縦方向に先に並び、一列分が終わったら二列目に移り、これを繰り返す。|
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/399135/a9e4c376-0887-0788-ef6f-d221ed63c715.png)

また「Pixel values are 0 to 255.0 means background(white), 255 means foreground(black)」とは、各ピクセルの値が0(白色)から255(黒色)の数値で表されるという意味です。
**MNISTのデータはグレースケールで、0(白色)～255(黒色)の数値で表されます。**


[補足2-1]について五行目までの内容を確認してきましたが、上記で説明した「画像データのピクセルと行列数について」を考慮すると、データファイルの残りのデータは以下のサイクルで読み取れば良いことがわかります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/399135/cbddbdbd-9794-e738-fe3f-ba6ac9c9b4ec.png)
以上でデータファイル「train-images-idx3-ubyte」の取得が完了です。

取得したデータは0～255までの整数の二次元配列や、各要素を255で割り最大値を1.0とする正規化された実数の二次元配列等で保持し、画像認識の処理に利用することが多いようです。


#### (3)データファイル　t10k-labels-idx1-ubyte

MNISTデータファイル提供サイトから、このデータファイルの内容について以下の説明が補足されています[補足3-1]。

[補足3-1]
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/399135/03fdad26-8a72-bc3b-4f57-a9775d31460a.png)
こちらの内容は「(1)データファイル　train-labels-idx1-ubyte」同じ形式データとなります。
ただし、行数は10,000行です。
同様の方法で読み取りを行えば良いです。


#### (4)データファイル　t10k-images-idx3-ubyte

MNISTデータファイル提供サイトから、このデータファイルの内容について以下の説明が補足されています[補足4-1]。

[補足4-1]
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/399135/fe7c3c2d-d50e-a9dc-550d-3d48706f0486.png)
こちらの内容は「(2)データファイル　train-images-idx3-ubyte」同じ形式データとなります。
行数は10,000行です。
同様の方法で読み取りを行います。

## :pick: 終わりに

以上でMNISTデータファイルの内容と読み取り方法について確認しました。
これらの説明からわかる通り、実際には画像ファイルが提供されているのではなく、画像ファイル(各ピクセルの情報)を数値化したデータがバイナリー形式で提供されています。
これらのデータをどう扱うかは、エンジニアの現場の目的・手段に委ねられます。

ぜひ、皆様の学習やお仕事に役立てて頂ければと思います。


## :pick: MSB firstを理解する

この節では"MSB first"について意味を確認します。

**MSB**とは、**最上位ビット**(Most Significant Bit)または**最上位バイト**(Most Significant Byte)のことで、コンピュータがある数値を表す際に、大きい位(くらい)の値を先に、つまり最初(first)に取り扱うということを意味しています。

例えば10進数で23という値の数値があったとします。10の位の値は"2"、1の位の値は"3"であり、大きい位は10の位の方なので、10の位を先に書き出して、次に1の位の"3"を書き出しています。よって"23"という書き方になります。

上記の説明は当たり前と思ってしまいますが、私たちが日常的に使用している数値の取り扱いも"MSB first"なので、違和感なく聞こえてしまうのです。

”MSB first”のことをコンピュータの専門用語で「**ビッグエンディアン(Big Endian)**」とも呼びます。

コンピュータの世界では、"MSB first"と反対の処理方式を取る場合もあります。その一つが"**LSB fist**"です。

**LSB**とは**最下位ビット**(Least Significant Bit)または**最下位バイト**(Least Significant Byte)で、MSBの反対の取り扱いを意味します。

この方式の場合、位が小さい値を先に取り扱います。そして、このような方式のことを「**リトルエンディアン(Little Endian)**」と呼びます。

ここで、10進数及び16進数の両方を使用してビッグエンディアン、リトルエンディアンを表してみましょう。取り扱う数値は例として10進数で43とします。

10進数と16進数それぞれの場合のビッグエンディアン、リトルエンディアンでの表示は以下となります(16進数の場合は、16進数を意味する記号"0x"を先頭に付けます)。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/399135/ece16fa9-c335-5577-9e79-aee2c435475c.png)


:warning: **ファイルの読み込み処理時に気をつけること**

今回のようなバイナリーファイルを読み込むときに、ビッグエンディアンなのかリトルエンディアンなのかを考慮する必要があります。

ファイルからデータを読み取る際、エンディアンの違いによってデータの取り扱いが異なりますので注意しましょう。画面に表示されるバイナリーデータの値を正しく理解するのに、この注意を念頭に置いて考えることが求められます。

話の付け加えとなりますが、私たちの身の回りのコンピュータはビッグエンディアン、リトルエンディアンのどちらでしょうか。実はこの違いはCPUの処理方式によって決まります。

**インテルやARM系のCPUはリトルエンディアンを採用しています。**
一方、一部の古いコンピュータにはビッグエンディアンも見られます。
また、以前のMac PowerBookはビッグエンディアンとリトルエンディアンの両方を採用しているモデルもあるようです。
さらに、ネットワーク上でのデータの取り扱いはビッグエンディアンを採用しています。


## :pick: 関連情報

:paperclip: THE MNIST DATABASE of handwritten digits
http://yann.lecun.com/exdb/mnist/

## :pick: ご意見など

ご意見、間違い訂正などございましたらお寄せ下さい。