MNISTバイナリファイルから画像とラベルを抽出してみたよ

やあ子供たち。機械学習やってるか。最近は猫も杓子も機械学習だよな。教師データで「トレーニング」をして覚えさせたら今度はその覚えさせたものを使って、「予測」させる。言い換えれば、「これはこう、これはこう、でこういうのはこう」とさんざん教えた挙句、いきなり教えてないものを持ってきて、「じゃこれは?」と聞くというか、そうやって、使うわけだけども。
でいきなり機械学習しよったって、何かいい教師データがないと実験すらできないよな。なのでそういう実験データとして、有名なものがいくつか用意されている。例えばアヤメの分類問題とかもあるけど、今回は、手書き文字画像のセット「MNIST」に焦点をあてた話だよ。
MNISTは、0から9までの数字を手書きで書いた画像と、それに対応する、正解の数字が、セットになった教師データと、「じゃこの画像は何の数字?」と、当てさせる(予測させる)ためのテストデータからなる、有名なサンプルデータなんだけど、本家のサイトで配布しているデータ形式が、画像もラベル(正解の数字のこと)も、独自のバイナリフォーマットになっていて、なんとも中身が見えにくいということになってる。おいおい、画像なら画像ファイルとしてみたいし、数字のラベルならテキストファイルで見たい!と思うのが人の常というものだろ?
●まずはC++コードの紹介なので今回はおじさんが、これらフォーマットを、画像ファイルや、テキストに、分離するための短いC++プログラムを作ったので紹介するぞ。

#define _CRT_SECURE_NO_WARNINGS

#include <iostream>
#include <vector>
#include <cstring>
using namespace std;

#define EXTRACT_IMAGES

// settings
char image_file[] = "<your path>\\train-images.idx3-ubyte";
char label_file[] = "<your path>\\train-labels.idx1-ubyte";
char extracted_image_file[] = "<your path>\\tak_%04d.bmp";
char extracted_label_file[] = "<your path>\\labels.txt";
//

template<class T>
void flip_endian(T& val)
{
	char* b = reinterpret_cast<char*>(&val);
	std::reverse(b, b+sizeof(T));
	return;
}
int main(int argc, char* argv[])
{
#ifdef EXTRACT_IMAGES
	FILE* fp = fopen(image_file, "rb");
#else
	FILE* fp = fopen(label_file, "rb");
#endif
	if (!fp)
	{
		cout << "failed to open file" << endl;
		return 0;
	}
	cout << "hi" << endl;
	unsigned int word;
	fread(&word, 1, 4, fp);
	fread(&word, 1, 4, fp);
	flip_endian(word);
	const int num_entry = word;
#ifdef EXTRACT_IMAGES
	// extract images
	fread(&word, 1, 4, fp);
	flip_endian(word);
	const int width = word;
	fread(&word, 1, 4, fp);
	flip_endian(word);
	const int height = word;
	for (int k = 0; k<num_entry*.01; ++k)
	{
		vector<unsigned char> buff;
		buff.reserve(height*width * 3);
		for (int i = 0; i < height; ++i)
		{
			vector<unsigned char> line;
			for (int j = 0; j < width; ++j)
			{
				unsigned char bt;
				fread(&bt, 1, 1, fp);
				line.push_back(i*8);// ←背景青グラデーション
				line.push_back(bt);
				line.push_back(bt);
			}// j
			reverse(line.begin(), line.end());
			buff.insert(buff.end(), line.begin(), line.end());
		}// i
		std::reverse(buff.begin(), buff.end());
		char ss[234];
		sprintf(ss, extracted_image_file, k);
		write_bmp(ss, width, height, 3, buff.data());
	}// k
#else
	// extract labels
	FILE* fp2 = fopen(extracted_label_file, "w");
	for (int k = 0; k<num_entry*.01; ++k)
	{
		unsigned char label;
		fread(&label, 1, 1, fp);
		flip_endian(label);
		fprintf(fp2, "%d\n", label);
	}// k
	fclose(fp2);
#endif
	if (fp)
	{
		fclose(fp);
	}
	getchar();
	return 0;
}

おっと、write_bmpは、ここからとってきてくれたまえ。(その中で使われているFourBytePaddingのコードもな。まそこの記事読んでくれな)そして上記プログラムからこれら関数を呼び出せるようにしてしまおう。無事にコンパイルを終えたなら、走らせてみよう。(VisualStudio2017でのコンパイルを確認しているよ。新規作成>プロジェクト>C++>空のプロジェクトを作成し、上記コードをmain.cppとして保存しそのままビルドしてみてくれ。)
●画像の抽出(画像化、可視化)さてさて結果の画像だ。
コード冒頭にある//settings以下に定義した、image_fileで指定された、yanさんのサイトからとってきたMNISTの画像バイナリファイル(例のオリジナルバイナリ形式のやつ)を指定して、なおかつ、extracted_image_filesに、抽出した各画像の出力ファイルの場所およびファイル名の書式を指定してくれよな。

●ラベルの抽出次にラベルファイルを抽出してみるぞ。このコード冒頭にある、「#define EXTRACT_IMAGES」という行をコメントアウトして再ビルドし、
コード冒頭にある//settings以下に定義した、label_fileで指定された、yanさんのサイトからとってきたMNISTのラベルバイナリファイル(例のオリジナルバイナリ形式のやつ)を指定して、なおかつ、extracted_label_filesに、抽出した各画像の出力ファイルの場所およびファイル名の書式を指定してくれよな。
実行すると、
ちなみにこの画像と対応するラベル列はlabel.txtにこのように出力されるよ。

5
0
4
1
9
2
1
3
1
4
3
5
3
6
1
7
2

こんな感じにうまく抽出できたなら、今日はこれでおしまいだよ。
チャオ!