y_uti のブログ

統計、機械学習、自然言語処理などに興味を持つエンジニアの技術ブログです

MNIST 手書き数字データを画像ファイルに変換する

MNIST 手書き数字データは、0 から 9 までの手書きの数字 70,000 点を収録したデータセットです。機械学習パターン認識の手法を確認するために利用できます。以下のウェブサイトからデータをダウンロードできます。
MNIST handwritten digit database, Yann LeCun, Corinna Cortes and Chris Burges

データは、訓練データ 60,000 点 (train) とテストデータ 10,000 点 (t10k) に分けられています。それぞれ、手書きの数字を表すピクセルデータ (images) と、それが 0 から 9 のどの数字なのかを示すラベルデータ (labels) からなります。いずれも、バイナリ形式で提供されています。

今回は、これらのデータをバイナリ形式からテキスト形式に変換して、簡単に、データの内容を確認したりスクリプト言語で処理したりできるようにします。さらに、データを一字ずつ画像ファイルに出力して、どのような数字が書かれているかを確認してみます。

データの確認

まず、ウェブサイトからダウンロードした圧縮ファイルを展開します。以下では、ダウンロードできるファイルのうち訓練データを対象に説明しますが、テストデータでも同様です。

$ gzip -dc train-images-idx3-ubyte.gz >train-images-idx3-ubyte
$ gzip -dc train-labels-idx1-ubyte.gz >train-labels-idx1-ubyte

さて、MNIST のウェブページの説明によると、画像ファイルのフォーマットは以下のとおりです。ヘッダ領域が 16 バイトで、その後に 28 * 28 バイトのピクセルデータが 60,000 画像分だけ続きます*1。はじめに、画像ファイルが確かにこのフォーマットになっていることを、簡単に確認します。

[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
MNIST handwritten digit database, Yann LeCun, Corinna Cortes and Chris Burges

まず、ファイルサイズを確認します。上記の説明から、ファイルサイズは 16 + 60,000 * 28 * 28 = 47,040,016 バイトになっているはずです。wc コマンドで調べてみると、たしかにそのとおりであることが確認できます。

$ wc -c train-images-idx3-ubyte
47040016 train-images-idx3-ubyte

次に、ヘッダ領域の値を確認します。head コマンドに -c オプションを指定すると、行数ではなくバイト数を指定して切り出せます。od はファイルをダンプするコマンドです。-t は出力形式を指定するオプションで、x1 を指定すると 1 バイトずつ区切って 16 進数で表示するという意味になります。

$ head -c 16 train-images-idx3-ubyte | od -tx1
0000000 00 00 08 03 00 00 ea 60 00 00 00 1c 00 00 00 1c
0000020

先頭の 4 バイトは "00 00 08 03" で、magic number の値に一致しています。次の 4 バイトは "00 00 ea 60" ですが、10 進数に変換してみると確かに 60,000 になります。それに続く "00 00 00 1c" も同様に 10 進数に変換すると 28 になります。

$ printf "%d\n" 0xea60
60000
$ printf "%d\n" 0x1c
28

なお、ヘッダ領域の値はいずれも 32 ビット整数ですが、od -td4 として表示させても Intel プロセッサでは正しい値になりません。これは、データがビッグエンディアンで格納されているためです。ウェブページに以下のように記載されているとおりです。

All the integers in the files are stored in the MSB first (high endian) format used by most non-Intel processors. Users of Intel processors and other low-endian machines must flip the bytes of the header.

MNIST handwritten digit database, Yann LeCun, Corinna Cortes and Chris Burges
テキスト形式への変換

それでは、このバイナリファイルから、ピクセルデータ本体部分をテキスト形式に変換します。od コマンドを利用すれば、以下のように簡単に変換できます。

$ od -An -v -tu1 -j16 -w784 train-images-idx3-ubyte | sed 's/^ *//' | tr -s ' ' >train-images.txt

実行例では od コマンドのオプションをいろいろと指定していますが、それぞれ以下の意味になります。

-An
ファイル先頭からのオフセットを非表示にする
-v
同一の値が続く場合でも表示を省略しない
-tu1
出力形式は 1 バイト単位の符号無し整数とする
-j16
先頭の 16 バイトをスキップする
-w784
784 バイトごとに改行する (28 * 28 = 784 です。一画像分のデータを一行に出力するために指定しています)

ラベルファイルも同様に、ウェブページに記載されているフォーマットを確認して、その内容に沿って変換します。こちらは、ヘッダ領域が 8 バイトで、画像ごとに 1 バイトのデータ (0 から 9 の値のいずれか) なので、od のオプションに -j8 と -w1 を指定します。また、出力は画像ごとに 1 列のみなので、空白は単純に削除しています。

$ od -An -v -tu1 -j8 -w1 train-labels-idx1-ubyte | tr -d ' ' >train-labels.txt

ラベルファイルの内容と、ピクセルデータの内容を突き合わせて、変換結果を確認します。ラベルファイルの先頭の 5 行は以下のようになっています。これは、手書きの数字の最初の 5 文字が、順に 5, 0, 4, 1, 9 を書いたものであることを示しています。

$ head -n 5 train-labels.txt
5
0
4
1
9

そこで、実際にそのような数字が書かれているのか、ピクセルデータを確認してみます。各画像が一行のデータになっているので、最初の画像は次のようにして確認できます。先頭行を 28 列ごとに改行して表示しています。ピクセルデータは 0 から 255 のグレースケールなのですが、表示を見やすくするために 0 か 1 以上かで区別して、空白文字か "■" 文字を表示しています。

$ head -n 1 train-images.txt |\
  awk '{ for (i = 1; i <= NF; i++) printf("%s%s", $i > 0 ? "■" : " ", i % 28 ? "" : "\n") }'
                            
                            
                            
                            
                            
            ■■■■■■■■■■■■    
        ■■■■■■■■■■■■■■■■    
       ■■■■■■■■■■■■■■■■     
       ■■■■■■■■■■■          
        ■■■■■■■ ■■          
         ■■■■■              
           ■■■■             
           ■■■■             
            ■■■■■■          
             ■■■■■■         
              ■■■■■■        
               ■■■■■        
                 ■■■■       
              ■■■■■■■       
            ■■■■■■■■        
          ■■■■■■■■■         
        ■■■■■■■■■■          
      ■■■■■■■■■■            
    ■■■■■■■■■■              
    ■■■■■■■■                
                            
                            
                            

これが 5 だということです。そのように見えないこともないですが、この数字ではいまいちはっきりしないようにも思われます。念のため、他の画像も見てみます。3 番目の画像は、ラベルファイルによると数字の 4 だということです。

$ head -n 3 train-images.txt | tail -n 1 |\
  awk '{ for (i = 1; i <= NF; i++) printf("%s%s", $i > 0 ? "■" : " " i % 28 ? "" : "\n") }'
                            
                            
                            
                            
                            
                    ■■■     
    ■■              ■■■     
    ■■             ■■■■     
    ■■             ■■■      
    ■■             ■■■      
   ■■■             ■■■      
   ■■■            ■■■■      
   ■■■            ■■■■      
   ■■■         ■■■■■■       
   ■■■   ■■■■■■■■■■■■       
   ■■■■■■■■■■■■■■■■■        
    ■■■■■■■■     ■■■        
                 ■■■        
                 ■■■        
                 ■■■        
                 ■■■        
                 ■■■        
                 ■■■        
                 ■■■        
                 ■■■        
                            
                            
                            

こちらははたしかに 4 に見えます。よさそうです。

画像ファイルへの変換

次に、テキスト形式のファイルを元に画像ファイルを生成して、上の実行例のような出力を実際の画像として確認できるようにしてみます。

今回のようなデータから画像ファイルを作成するには、Netpbm 形式を利用するのが手軽です。Netpbm 形式は、簡単なヘッダとピクセルデータで画像を表現します。以下のウェブページに、ファイルフォーマットなどが記載されています。
User manual for Netpbm

Netpbm には PBM/PGM/PPM の三種類の形式がありますが、今回はグレースケールのピクセルデータから画像を作成するので、PGM 形式を使います。PGM 形式のフォーマットは以下に定められています。
PGM Format Specification

以下のようにして、ピクセルデータから PNG 形式の画像ファイルを生成できます。

$ head -n 3 train-images.txt | tail -n 1 |\
  awk '
    BEGIN { print "P2 28 28 255" }
    { for (i = 1; i <= NF; i++) printf("%d%s", $i, i % 14 ? " " : "\n") }' |\
  pnmtopng - >image.png

awk の BEGIN ブロックで、ヘッダとして "P2 28 28 255" を出力します。ヘッダの意味は次のとおりです。

P2
このファイルが Plain PGM 形式であることを示すマジックナンバー
28
画像の横のサイズ
28
画像の縦のサイズ
255
グレースケールの最大値

本体ではピクセルデータを出力します。この部分は先ほどテキストデータの出力を確認した処理と同様ですが、Plain PGM のフォーマットとして 1 行を 70 文字以下に収めるように記載されているので、14 列ごとに改行しています*2

この awk の出力そのものが PGM 形式の画像ファイルになっています。ですが、PGM 形式はあまり一般的ではないので、最後の pnmtopng コマンドで、より一般的な PNG 形式に変換しています*3

この処理で出力された image.png は次の画像になります。たしかに、上で見た数字 "4" が画像として出力されています。
f:id:y_uti:20140723024455p:plain

このようにして、一字分のデータを画像ファイルに変換できました。テキストファイルの各行についてこの処理を繰り返すことで、それぞれの手書き数字を画像ファイルに変換できます。

*1:テストデータでは 10,000 画像になります。

*2:17 列ごとの改行でも 70 文字に収まりますが、画像サイズが 28x28 なので、区切りのよいところで 14 列ごととしています。

*3:CentOS 7 では、pnmtopng は netpbm-progs パッケージに含まれます。他の方法として、ImageMagick に含まれる convert コマンドでも変換できます。