テストの実行
MNIST 画像認識データ セットに取り組む
機械学習の分野で最も魅力的なトピックの 1 つは、画像認識 (IR) です。IR を用いたシステムの例としては、指紋や網膜による認証を使用するコンピューター ログイン プログラム、指名手配リストに載っている人物を見つけるために搭乗者の顔をスキャンする空港のセキュリティ システムなどが挙げられます。MNIST データ セットは、IR アルゴリズムの実験に使用できるシンプルな画像のコレクションです。今回は、比較的シンプルな C# プログラムを使って MNIST データ セットについて紹介し、IR の考え方に慣れていただきます。
大部分のソフトウェア アプリケーションでは IR を使用する必要はないかもしれませんが、今回提供する情報は以下の 4 つの理由で役立つのではないかと考えます。第 1 に、MNIST データ セットと IR の概念について理解するには、実際のコードを使って試してみるのが一番です。第 2 に、IR の基礎を理解することで、実際の洗練された IR システムの機能と限界を理解しやすくなります。第 3 に、今回説明するいくつかのプログラミング手法は、さまざまな一般的課題に利用できます。そして第 4 に、IR はそれ自体興味深いものと思っていただけるはずです。
このコラムの趣旨を理解するには、図 1 のデモ プログラムを見るのが一番です。デモ プログラムは、標準的な Windows フォーム アプリケーションです。Load Images (画像の読み込み) というラベルを付けたボタン コントロールをクリックすると、MNIST データ セットという標準画像認識データ セットをメモリに読み取ります。データ セットには、0 から 9 を手書きした数値をデジタル化した情報が 60,000 個含まれています。デモでは、選択した画像を、ビットマップ画像 (図 1 左) と 16 進数のピクセル値の行列 (右) として表示します。
図 1 MNIST 画像の表示
ここからは、デモ プログラムのコードを順を追って見ていきます。デモは Windows フォーム アプリケーションなので、コードの大半が UI 機能関連で、複数のファイルに分けて収めています。ここでは、そのロジックに注目します。1 つの C# ソース ファイルにリファクタリングしたデモ コードは、msdn.microsoft.com/magazine/msdnmag0614 からダウンロードできます。ダウンロードしたコードをコンパイルするには、ローカル コンピューターに MnistViewer.cs という名前を付けて保存し、Visual Studio の新規プロジェクトを作成して、プロジェクトに保存したファイルを追加します。または、(C# コンパイラの所在を認識する) Visual Studio コマンド シェルを起動して、ダウンロードを保存したディレクトリに移動し、>csc.exe /target:winexe MnistViewer.cs コマンドを発行して MnistViewer.exe 実行可能ファイルを作成します。デモ プログラムを実行できるようにするには、後ほど説明するように MNIST データ ファイルを 2 つダウンロードして保存し、これら 2 つのファイルの場所を指定するようにデモのソース コードを編集する必要があります。
今回は、C# (または同様の言語) の中級以上のスキルがある読者を対象としていますが、IR についてはまったく知らなくても問題ありません。デモ コードは Microsoft .NET Framework に大きく依存しているため、デモ コードを JavaScript などの .NET 以外の言語にリファクタリングするのは難しいと思われます。
IR に関する文献は用語があまり統一されていません。画像認識は、画像分類、パターン認識、パターン マッチング、パターン分類などとも呼ばれています。これらの用語の実際の意味は異なりますが、同じものを指すことがあるため、インターネットで関連情報を検索する際少し戸惑うことがあります。
MNIST データ セット
国立標準技術研究所の混合データ セット (MNIST データ セット) は、さまざまな IR アルゴリズムを比較する際のベンチマークとして機能させるように、IR の研究者によって作成されました。基本的な考え方としては、テストする IR アルゴリズムまたはソフトウェア システムがある場合、MNIST データ セットに対してアルゴリズムまたはシステムを実行し、他のシステムについて以前発行された結果と比較します。
データ セットには計 70,000 枚の画像が含まれており、そのうち 60,000 枚は学習用画像 (IR モデルの作成に使用) で 10,000 枚は判別用画像 (モデルの精度の評価に使用) です。各 MNIST 画像は、1 つの手書き数字をデジタル化したものです。サイズはそれぞれ 28 x 28 ピクセルです。各ピクセル値は 0 (白) ~ 255 (黒) の値で、中間のピクセル値は灰色の網かけを表します。図 2 に示すのは、学習用セットの最初の画像 8 枚です。各画像に対応した実際の数値を識別するのは、人間には簡単ですが、コンピューターにとっては至難の業です。
図 2 最初の 8 枚の MNIST 学習用画像
興味深いことに、学習用データと判別用データは 1 つのファイルではなく、2 つの別のファイルに保存されています。1 つのファイルには画像のピクセル値、もう 1 つのファイルには画像のラベル情報 (0 ~ 9) が含まれます。4 つのファイルにはヘッダー情報も含まれ、すべて gzip 形式で圧縮されたバイナリ フォーマットです。
図 1 で、デモ プログラムは 60,000 項目の学習用セットのみを使用しています。判別用セットの形式は学習用セットと同じです。MNIST ファイルの初期リポジトリは、現在 yann.lecun.com/exdb/mnist にあります。学習用ピクセル データは train-images-idx3-ubyte.gz ファイルに、判別用ラベル データは train-labels-idx1-ubyte.gz ファイルに保存されています。デモ プログラムを実行するには、MNIST リポジトリ サイトに移動し、2 つの学習用データ ファイルをダウンロードして解凍する必要があります。ファイルの解凍には、無料のオープン ソース 7-Zip ユーティリティを使いました。
MNIST ビューアーを作成する
MNIST デモ プログラムを作成するため、Visual Studio を起動し、MnistViewer という新しい C# Windows フォーム プロジェクトを作成します。このデモは特定の .NET バージョンとの依存関係はないため、どのバージョンの Visual Studio でも動作します。
Visual Studio エディターにテンプレート コードが読み込まれたら、UI コントロールをセットアップします。2 つの TextBox コントロール (textBox1、textBox2) を追加し、解凍した 2 つの学習用ファイルへのパスを含めます。Button コントロール (button1) を追加し、「Load Images」(画像の読み込み) というラベルを付けます。現在の画像インデックスと次の画像インデックスの値を保持する TextBox コントロール (textBox3、textBox4) も追加します。Visual Studio デザイナーを使用し、2 つのコントロールの初期値をそれぞれ "NA" と "0" に設定します。
画像の倍率値用に ComboBox コントロール (comboBox1) を追加します。デザイナーを使って、コントロールの項目コレクションに移動し、"1" から "10" までの文字列を追加します。2 つ目の Button コントロール (button2) を追加し、「Display Next」(次を表示) というラベルを付けます。PictureBox コントロール (pictureBox1) を追加し、その BackColor プロパティを ControlDark に設定し、コントロールの輪郭をはっきりさせます。PictureBox のサイズを 280 x 280 に設定し、最大 10 倍までの倍率を可能にします (MNIST 画像は 28 x 28 ピクセルでした)。画像の 16 進値を表示する 5 番目の TextBox (textBox5) を追加し、Multiline プロパティを True に、Font プロパティを Courier New の 8.25 ポイントに設定後、サイズを 606 x 412 に広げます。最後に、メッセージのログ用の ListBox コントロール (listBox1) を追加します。
UI コントロールを Windows フォームに配置したら、クラス スコープのフィールドを 3 つ追加します。
public partial class Form1 : Form
{
private string pixelFile =
@"C:\MnistViewer\train-images.idx3-ubyte";
private string labelFile =
@"C:\MnistViewer\train-labels.idx1-ubyte";
private DigitImage[] trainImages = null;
...
最初の 2 つの文字列は、解凍した学習用データ ファイルの場所を指します。デモを実行するには、これら 2 つの文字列を編集する必要があります。3 つ目のフィールドはプログラム定義の DigitImage オブジェクトの配列です。
今回は、Form コンストラクターを少し編集して textBox1 と textBox2 にファイル パスを配置し、倍率の初期値を 6 にします。
public Form1()
{
InitializeComponent();
textBox1.Text = pixelFile;
textBox2.Text = labelFile;
comboBox1.SelectedItem = "6";
this.ActiveControl = button1;
}
ActiveControl プロパティを使って、使いやすさのためだけに初期フォーカスを button1 コントロールに設定します。
MNIST 画像を保持するクラスを作成する
MNIST 画像を 1 つ表すために小さなコンテナー クラスを作成します (図 3 参照)。DigitImage クラスと名前を付けましたが、MnistImage など、より具体的な名前に変更してもかまいません。
図 3 DigitImage クラスの定義
public class DigitImage
{
public int width; // 28
public int height; // 28
public byte[][] pixels; // 0(white) - 255(black)
public byte label; // '0' - '9'
public DigitImage(int width, int height,
byte[][] pixels, byte label)
{
this.width = width; this.height = height;
this.pixels = new byte[height][];
for (int i = 0; i < this.pixels.Length; ++i)
this.pixels[i] = new byte[width];
for (int i = 0; i < height; ++i)
for (int j = 0; j < width; ++j)
this.pixels[i][j] = pixels[i][j];
this.label = label;
}
}
分かりやすくするためにすべてのクラス メンバーをパブリック スコープで宣言し、コードを小さく抑えるために通常のエラー チェックは省略しています。MNIST 画像はすべて 28 x 28 ピクセルなので、幅と高さのフィールドは省略できますが、幅と高さのフィールドを用意しておけばクラスの柔軟性が高まります。pixels フィールドは、配列の配列形式の行列です。多くの言語とは異なり、C# には真の多次元配列があるため、そちらを使用してもかまいません。各セル値は byte 型で、0 ~ 255 までの整数値にすぎません。label フィールドも byte 型として宣言しますが、int 型、または char 型か string 型にすることもできます。
DigitImage クラスのコンストラクターは幅、高さ、ピクセル行列、およびラベルの値を受け取り、これらのパラメーター値を関連フィールドにコピーするだけです。値ではなく参照でピクセル値をコピーすることもできますが、ソース ピクセルの値が変更された場合に望ましくない副作用が出る可能性があります。
MNIST データを読み込む
イベント ハンドラーを登録するために button1 コントロールをダブルクリックします。イベント ハンドラーは、作業の大半を LoadData メソッドに任せます。
private void button1_Click(object sender, EventArgs e)
{
this.pixelFile = textBox1.Text;
this.labelFile = textBox2.Text;
this.trainImages = LoadData(pixelFile, labelFile);
listBox1.Items.Add("MNIST images loaded into memory");
}
LoadData メソッドのリストを図 4 に示します。LoadData メソッドは、ピクセル ファイルとラベル ファイルを両方開き、同時に読み取ります。メソッドでは、まず 28 x 28 のピクセル値の行列をローカルに作成します。便利な .NET BinaryReader クラスは、バイナリ ファイルの読み取り専用に設計されています。
図 4 LoadData メソッド
public static DigitImage[] LoadData(string pixelFile, string labelFile)
{
int numImages = 60000;
DigitImage[] result = new DigitImage[numImages];
byte[][] pixels = new byte[28][];
for (int i = 0; i < pixels.Length; ++i)
pixels[i] = new byte[28];
FileStream ifsPixels = new FileStream(pixelFile, FileMode.Open);
FileStream ifsLabels = new FileStream(labelFile, FileMode.Open);
BinaryReader brImages = new BinaryReader(ifsPixels);
BinaryReader brLabels = new BinaryReader(ifsLabels);
int magic1 = brImages.ReadInt32(); // stored as big endian
magic1 = ReverseBytes(magic1); // convert to Intel format
int imageCount = brImages.ReadInt32();
imageCount = ReverseBytes(imageCount);
int numRows = brImages.ReadInt32();
numRows = ReverseBytes(numRows);
int numCols = brImages.ReadInt32();
numCols = ReverseBytes(numCols);
int magic2 = brLabels.ReadInt32();
magic2 = ReverseBytes(magic2);
int numLabels = brLabels.ReadInt32();
numLabels = ReverseBytes(numLabels);
for (int di = 0; di < numImages; ++di)
{
for (int i = 0; i < 28; ++i) // get 28x28 pixel values
{
for (int j = 0; j < 28; ++j) {
byte b = brImages.ReadByte();
pixels[i][j] = b;
}
}
byte lbl = brLabels.ReadByte(); // get the label
DigitImage dImage = new DigitImage(28, 28, pixels, lbl);
result[di] = dImage;
} // Each image
ifsPixels.Close(); brImages.Close();
ifsLabels.Close(); brLabels.Close();
return result;
}
MNIST 学習用ピクセル ファイルの形式には、値 2051 を保持する初期マジック整数 (32 ビット) が含まれ、画像数を表す整数、行数と列数を表す整数、そして画像 60,000 枚 x 28 ピクセル x 28 ピクセル = 47,040,000 のバイト値が続きます。そのため、バイナリ ファイルを開いたら、最初の 4 つの整数を ReadInt32 メソッドを使って読み取ります。たとえば、画像の数は次のように読み取ります。
int imageCount = brImages.ReadInt32();
imageCount = ReverseBytes(imageCount);
興味深いことに、MNIST ファイルは整数値を、Microsoft のソフトウェアを実行するハードウェアで最も一般的な通常のリトルエンディアン形式ではなく、ビッグエンディアン形式 (インテル以外のプロセッサで使用) で格納します。そのため、通常の PC 型のハードウェアを使用している場合、整数値を表示または使用するには、ビッグエンディアンからリトルエンディアンに変換しなければなりません。つまり、整数を構成する 4 バイトの順序を反転します。たとえば、ビッグエンディアン形式のマジック数 2051 は、次のようになっています。
00000011 00001000 00000000 00000000
同じ値をリトルエンディアン形式で保存すると、次のようになります。
00000000 00000000 00001000 00000011
反転する必要があるのは、32 ビット シーケンス全体ではなく、4 つのバイト列であることに注意します。バイトを反転する方法はたくさんありますが、今回は低レベルのビット操作ではなく、.NET BitConverter クラスを活用する高レベルの方法を採用します。
public static int ReverseBytes(int v)
{
byte[] intAsBytes = BitConverter.GetBytes(v);
Array.Reverse(intAsBytes);
return BitConverter.ToInt32(intAsBytes, 0);
}
LoadData メソッドでは、ヘッダー情報を読み取っていますが、使用していません。4 つの値 (2051、60000、28、28) をチェックし、ファイルが破損していないことを検証することもできます。両方のファイルを開き、ヘッダーの整数値を読み取ったら、ピクセル ファイルから 28 x 28 = 784 個の連続するピクセル値を読み取り、それらの値を格納します。次に、ラベル ファイルから 1 つのラベル値を読み取り、ピクセル値と共に DigitImage オブジェクトにまとめ、これをクラス スコープの trainData 配列に格納します。明示的な画像 ID はないことに注意してください。それぞれの画像には暗黙のインデックス ID があり、これは画像シーケンスのゼロから始まる画像位置です。
画像を表示する
イベント ハンドラーを登録するため button2 コントロールをダブルクリックします。画像を表示するコードを、図 5 に示します。
図 5 MNIST 画像の表示
private void button2_Click(object sender, EventArgs e)
{
// Display 'next' image
int nextIndex = int.Parse(textBox4.Text);
DigitImage currImage = trainImages[nextIndex];
int mag = int.Parse(comboBox1.SelectedItem.ToString());
Bitmap bitMap = MakeBitmap(currImage, mag);
pictureBox1.Image = bitMap;
string pixelVals = PixelValues(currImage);
textBox5.Text = pixelVals;
textBox3.Text = textBox4.Text; // Update curr idx
textBox4.Text = (nextIndex + 1).ToString(); // ++next index
listBox1.Items.Add("Curr image index = " +
textBox3.Text + " label = " + currImage.label);
}
表示する画像のインデックスを textBox4 (次の画像のインデックス) コントロールから取得し、その画像への参照を trainImage 配列から取り出します。チェックを追加して、画像にアクセスする前に画像データがメモリに読み込まれていることを確認することもできます。画像は、まず PictureBox コントロールのビジュアル フォームとして表示し、次に大きな TextBox コントロール内に 16 進数値として表示します。PictureBox コントロールの Image プロパティは、Bitmap オブジェクトを受け取り、次にオブジェクトをレンダリングします。結果は非常に良好です。Bitmap オブジェクトは、基本的には画像と考えることができます。.NET Image クラスがありますが、これは Bitmap クラスの定義に使用する抽象基本クラスです。そのため、画像を表示する際に重要なのは、プログラム定義の DigitImage オブジェクトから Bitmap オブジェクトを生成することです。これは、図 6 にリストした MakeBitmap ヘルパー メソッドで実行します。
図 6 MakeBitmap メソッド
public static Bitmap MakeBitmap(DigitImage dImage, int mag)
{
int width = dImage.width * mag;
int height = dImage.height * mag;
Bitmap result = new Bitmap(width, height);
Graphics gr = Graphics.FromImage(result);
for (int i = 0; i < dImage.height; ++i)
{
for (int j = 0; j < dImage.width; ++j)
{
int pixelColor = 255 - dImage.pixels[i][j]; // black digits
Color c = Color.FromArgb(pixelColor, pixelColor, pixelColor);
SolidBrush sb = new SolidBrush(c);
gr.FillRectangle(sb, j * mag, i * mag, mag, mag);
}
}
return result;
}
このメソッドは長くはありませんが、なかなか精緻にできています。Bitmap コンストラクターは、幅と高さを整数として受け取り、基本的な MNIST データでは常に 28 と 28 になります。倍率値が 3 の場合、Bitmap 画像のサイズは (28 * 3) x (28 * 3) = 84 x 84 ピクセルになり、Bitmap の 3 x 3 の各四角形が元の画像の 1 ピクセルを表します。
Bitmap オブジェクトの値は、Graphics オブジェクトによって間接的に提供されます。結果の画像で白の背景の上に黒/灰色の数値が表示されるように、入れ子状のループ内で現在のピクセル値を 255 を使って反転します。この反転を行わなければ、黒の背景の上に白/灰色の数値が表示された画像になります。グレースケールにするため、FromArgb メソッドには赤、緑、青の各パラメーターに同じ値を渡します。RGB パラメーターの 1 つのみにピクセル値を渡し、グレースケールの画像ではなく、カラーの画像 (赤、緑、または青の網かけ) にする方法もあります。
FillRectangle メソッドは、Bitmap オブジェクトの領域を塗りつぶします。最初のパラメーターは塗りつぶす色です。2 番目と 3 番目のパラメーターは、四角形の左上隅の x 座標と y 座標です。x は上下方向で、元の画像のピクセル行列へのインデックス j と対応します。FillRectangleへの 4 番目と 5 番目のパラメーターは塗りつぶす四角形領域の幅と高さを表します。この幅と高さは 2 番目と 3 番目のパラメーターで指定した左上隅を基点とします。
たとえば、現在のピクセルが元の画像の i = 2 と j = 5 の位置に表示されており、値 200 (濃い灰色) を保持しているとします。倍率値を 3 に設定すると、Bitmap オブジェクトのサイズは 84 x 84 ピクセルになります。FillRectangle メソッドは Bitmap の x = (5 * 3) = 列 15 と y = (2 * 3) = 行 6 から開始し、3 x 3 ピクセルの四角形を色 (55,55,55) = 濃い灰色で塗りつぶします。
画像のピクセル値を表示する
図 5 のコードを見直してみると、画像のピクセル値を 16 進数で表すために PixelValues ヘルパー メソッドを使用しているのが分かります。このメソッドは短くシンプルです。
public static string PixelValues(DigitImage dImage)
{
string s = "";
for (int i = 0; i < dImage.height; ++i) {
for (int j = 0; j < dImage.width; ++j) {
s += dImage.pixels[i][j].ToString("X2") + " ";
}
s += Environment.NewLine;
}
return s;
}
PixelValues ヘルパー メソッドは、簡潔にするため文字列の連結を使用し、改行文字を埋め込んだ 1 つの長い文字列を構築します。Multiline プロパティに True を設定した TextBox コントロールに文字列を配置すると、図 1 のように文字列が表示されます。16 進数値は 10 進数値よりもやや解釈が難しい点もありますが、16 進数値の方が整然とした形式になります。
今後の展開
画像認識の考え方はシンプルですが、実際にはきわめて難しい問題です。IR を理解するための第一歩として、よく知られた MNIST データ セットをここでデモしたように表示できるようになることをお勧めします。図 1 を見ると、すべての MNIST 画像が実際には "4" などの関連ラベルが付いた 784 個の値にすぎないことがわかります。そのため、画像認識とはこの 784 個の値を入力として受け取り、それぞれの入力が 0 ~ 9 を意味している確率を表す 10 個の可能性を出力として返す関数を見つけることだと言えます。
IR でよく用いられるのは、なんらかの形式のニューラル ネットワークを使用する方法です。たとえば、784 個の入力ノード、1,000 ノードの非表示の層、および 10 ノードの出力層を持ったニューラル ネットワークを作成できます。こうしたネットワークには、判定のために合計 (784 * 1000) + (1000 * 10) + (1000 + 10) = 795,010 の重みとバイアス値があることになります。学習用画像が 60,000 枚だったとしても、これは非常に難しい課題でしす。ただし、優れた画像認識機能を作成するために役立つ優れた方法がいくつかあります。畳み込みニューラル ネットワークを使用する方法や、弾性歪みを使って追加の学習用画像を生成する方法などです。
James McCaffrey 博士は、ワシントン州レドモンドにある Microsoft Research に勤務しています。これまでに、Internet Explorer、Bing などの複数のマイクロソフト製品にも携わってきました。連絡先は jammc@microsoft.com (英語のみ) です。
この記事のレビューに協力してくれた技術スタッフの Wolf Kienzle (Microsoft Research) に心より感謝いたします。