テストの実行

MNIST 画像データ セットを変形する

James McCaffrey

コード サンプルのダウンロード

James McCaffrey国立標準技術研究所の混合データ セット (MNIST データ セット) は、手書き数字を表す 70,000 枚の小さな画像のコレクションです。MNIST データ セットは、画像認識アルゴリズムのベンチマークとして機能させるように作成されました。MNIST 画像は小さく (28 x 28 ピクセル)、認識対象の選択肢は 10 個の数字だけ (0 ~ 9) で、画像認識モデルを作成するための学習用画像は 60,000 枚ありますが (モデルの精度をテストするための画像 10,000 枚を除外した場合)、経験から、MNIST 画像の認識は難しい問題であることがわかっています。

画像認識など、パターン分類の難問に取り組む方法の 1 つは、使用する学習用データを増やすことです。さらに、より多くの学習用データをプログラムで生成する優れた方法の 1 つは、元の各画像を変形することです。図 1 のデモ プログラムをご覧ください。図の左側には、元の MNIST 形式で数字の 4 が表示され、右側には、弾性変形を使用して変形した数字が表示されています。デモ アプリケーションの右上隅に表示されているパラメーターは、変位ランダム化のシード、カーネル サイズ、カーネルの標準偏差、および明度に基づいて変形していることを示しています。

A Distorted MNIST Image
図 1 変形した MNIST 画像

大部分の作業環境では画像を変形する必要はないかもしれませんが、今回提供する情報は以下の 3 つの理由で役立つのではないでしょうか。第 1 に、実際のコードを確認して画像変形の具体的なしくみを理解すれば、多数の画像認識関連記事を理解しやすくなります。第 2 に、画像変形で利用するいくつかのプログラミング手法は、他の一般的プログラミング シナリオに利用できます。そして第 3 に、画像変形はそれ自体興味深いトピックと思っていただけるはずです。

今回のコラムは、高度なレベルのプログラミング スキルを備えた読者を想定していますが、画像変形についてはまったく知らなくても問題ありません。デモ プログラムは C# でコーディングし、Microsoft .NET Framework に大きく依存しているため、.NET 以外の言語にリファクタリングするのは難しいと思われます。また、コードを小さく抑え、中心となる考え方を明瞭にするため、通常のエラー チェックの大部分は削除しています。デモは Visual Studio で作成した Windows フォーム アプリケーションなので、コードの大半が UI 関連で、複数のファイルに分けています。ただし、1 つの C# ソース コード ファイルにリファクタリングしたデモ コードを、msdn.microsoft.com/magazine/msdnmag0714 からダウンロードできます。MNIST データはインターネット上で数か所から入手できます。主なリポジトリは、yann.lecun.com/exdb/mnist にあります。

プログラムの全体構造

デモ プログラムを作成するため、Visual Studio を起動し、MnistDistort という Windows フォーム アプリケーションを作成します。UI には 8 つの TextBox コントロールを配置します。それぞれの用途は、解凍した MNIST データ ファイルへのパス (ピクセル ファイル用に 1 つ、ラベル ファイル用に 1 つ)、現在表示中の画像と次の画像のインデックス、および変形処理に使用するシード値、カーネル サイズ、カーネルの標準偏差、明度の値です。DropDown コントロールは、画像表示を拡大する倍率値を保持します。3 つの Button コントロールの役割は、60,000 枚の MNIST 画像とそのラベルをメモリに読み込んで、画像を表示し、表示中の画像を変形することです。また、通常の画像と変形した画像を表示する、2 つの PictureBox コントロールもあります。最後に、ListBox コントロールは進行状況の表示とメッセージのログに使用します。

ソース コードの先頭にある不要な名前空間への参照を削除し、MNIST データ ファイルを読み取れるように System.IO 名前空間への参照を追加します。

続いて、クラス スコープの trainImages という配列を追加します。この配列は、プログラム定義の DigitImage オブジェクトへの参照を保持します。また、2 つの MNIST データ ファイルの場所を保持する変数も追加します。

DigitImage[] trainImages = null;
string pixelFile = @"C:\MnistDistort\train-images.idx3-ubyte"; // Edit
string labelFile = @"C:\MnistDistort\train-labels.idx1-ubyte"; // Edit

Form コンストラクターには、6 行のコードを追加します。

textBox1.Text = pixelFile;
textBox2.Text = labelFile;
comboBox1.SelectedItem = "6"; // Magnification
textBox3.Text = "NA"; // Curr index
textBox4.Text = "0"; // Next index
this.ActiveControl = button1;

Button1 のクリック イベント ハンドラーは、60,000 枚の画像をメモリに読み込みます。

string pixelFile = textBox1.Text;
string labelFile = textBox2.Text;
trainImages = LoadData(pixelFile, labelFile);
listBox1.Items.Add("MNIST training images loaded into memory");

Button2 のクリック イベント ハンドラーは、次の画像を表示し、UI コントロールを更新します。

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;
textBox3.Text = textBox4.Text; // Update curr index
textBox4.Text = (nextIndex + 1).ToString(); // next index
textBox8.Text = textBox3.Text;  // Random seed
listBox1.Items.Add("Curr image index = " + textBox3.Text +
  " label = " + currImage.label);

付属のコード ダウンロードには、LoadData メソッドと MakeBitmap メソッドが含まれています。変形処理の大半は、Button3 のクリック イベント ハンドラーから呼び出すメソッドで実行します。このイベント ハンドラーは図 2 に示すとおりです。

図 2 画像変形の呼び出しコード

private void button3_Click(object sender, EventArgs e)
{
  int currIndex = int.Parse(textBox3.Text);
  int mag = int.Parse(comboBox1.SelectedItem.ToString());
  int kDim = int.Parse(textBox5.Text); // Kernel dimension
  double kStdDev = double.Parse(textBox6.Text); // Kernel std dev
  double intensity = double.Parse(textBox7.Text);
  int rndSeed = int.Parse(textBox8.Text);  // Randomization
  DigitImage currImage = trainImages[currIndex];
  DigitImage distorted = Distort(currImage, kDim, kStdDev,
    intensity, rndSeed);
  Bitmap bitMapDist = MakeBitmap(distorted, mag);
  pictureBox2.Image = bitMapDist;
}

Distort メソッドは、MakeKernel メソッド (平滑化行列の作成処理)、MakeDisplace メソッド (画像内の各ピクセルを変形するための方向と距離)、および Displace メソッド (元の画像に対する実際の変形処理) を呼び出します。MakeDisplace ヘルパー メソッドは、ApplyKernel サブヘルパー メソッドを呼び出し、変位値を平滑化します。ApplyKernel サブヘルパー メソッドは、Pad サブサブヘルパー メソッドを呼び出します。

弾性変形

弾性変形を使用した画像変形の基本的な考え方は、かなりシンプルです。MNIST 画像の場合は、既存の各ピクセルを少しずつ移動します。しかし、細部はこれほど単純な話ではありません。各ピクセルを単独で移動するだけの単純な手法を実行すると、新しい画像は引き伸ばされたのではなくばらばらになったように見えます。たとえば、図 3 に示す概念の画像をご覧ください。2 つの画像は、ある画像の 5 x 5 セクションに対する変形を表しています。各矢印は、対応するピクセルが移動する方向と距離を示しています。左側の画像に表示しているベクトルはかなりランダムなので、画像は変形するのではなくばらばらになります。右側の画像に表示しているベクトルは相互に関連しているので、引き伸ばされた画像が生成されます。

Random vs. Smoothed Displacement Fields
図 3 ランダムな変位フィールドと平滑化変位フィールド

つまりポイントは、互いに近接するピクセルの移動する方向と距離がかなり似ていてもまったく同じではなくなるような方法で、各ピクセルの変位を実行することです。この処理は、連続ガウス カーネルと呼ばれる行列を使用して実現できます。全体的な考え方については、コードを使用して説明するのが最もわかりやすいでしょう。デモに含まれている次のメソッドについて考えてみましょう。

private DigitImage Distort(DigitImage dImage, int kDim,
  double kStdDev, double intensity, int seed)
{
  double[][] kernel = MakeKernel(kDim, kStdDev);
  double[][] xField = MakeDisplace(dImage.width, dImage.height,
   seed, kernel, intensity);
  double[][] yField = MakeDisplace(dImage.width, dImage.height,
    seed + 1, kernel, intensity);
  byte[][] newPixels = Displace(dImage.pixels, xField, yField);
  return new DigitImage(dImage.width, dImage.height,
    newPixels, dImage.label);
}

Distort メソッドは、DigitImage オブジェクトと、カーネルに関連する 4 つの数値パラメーターを受け取ります。DigitImage 型はプログラム定義のクラスであり、1 つの MNIST 画像を構成する 28 x 28 バイトを表します。メソッド内では最初に、カーネルのサイズに kDim を指定し、ピクセル変位の類似性に影響する値に kStdDev を指定して、カーネルを作成します。

ピクセルの変位を実行するには、左右方向と上下方向の移動距離を取得する必要があります。これらの情報はそれぞれ xField 配列と yField 配列に格納し、MakeDisplace ヘルパー メソッドを使用して計算します。Displace ヘルパー メソッドは、DigitImage 画像のピクセル値を受け取り、変位フィールドを使用して、新しいピクセル値を生成します。新しいピクセル値を DigitImage コンストラクターに渡して、新しい変形画像を生み出します。つまり、画像を変形するにはカーネルを作成します。このカーネルを使用して、独立しているのではなく互いに関連している x と y の方向フィールドを生成します。方向フィールドを元の画像に適用して、この画像の変形バージョンを生成します。

ガウス カーネル

連続ガウス カーネルは、合計で 1.0 になる値の行列です。中央の値が最も大きく、放射相称になっています。標準偏差が 1.0 の、5 x 5 のガウス カーネルを次に示します。

0.0030   0.0133   0.0219   0.0133   0.0030
0.0133   0.0596   0.0983   0.0596   0.0133
0.0219   0.0983   0.1621   0.0983   0.0219
0.0133   0.0596   0.0983   0.0596   0.0133
0.0030   0.0133   0.0219   0.0133   0.0030

近接する値どうしが、異なっているものの似ていることに注目してください。標準偏差の値によって、カーネル値の類似性が決まります。標準偏差が大きくなるほど、似通った値が生成されます。たとえば、1.5 という標準偏差を使用すると、5 x 5 のカーネルにおける 1 行目の値は次のようになります。

0.0232   0.0338   0.0383   0.0338   0.0232

標準偏差はデータ分散の尺度であり、あるデータ セットの標準偏差値が大きいほど広範囲に分散していることを表すので、最初は奇妙に思えるかもしれません。しかし、ガウス カーネルの場合、標準偏差の用途は値の生成であり、生成されたカーネル内の分散の尺度ではありません。ガウス カーネルを生成するためにデモ プログラムで使用したメソッドを、図 4 に示します。

図 4 MakeKernel メソッド

private static double[][] MakeKernel(int dim, double sd)
{
  if (dim % 2 == 0)
    throw new Exception("kernel dim must be odd");
  double[][] result = new double[dim][];
  for (int i = 0; i < dim; ++i)
    result[i] = new double[dim];
  int center = dim / 2; // Note truncation
  double coef = 1.0 / (2.0 * Math.PI * (sd * sd));
  double denom = 2.0 * (sd * sd);
  double sum = 0.0; // For more accurate normalization
  for (int i = 0; i < dim; ++i) {
    for (int j = 0; j < dim; ++j) {
      int x = Math.Abs(center - j);
      int y = Math.Abs(center - i);
      double num = -1.0 * ((x * x) + (y * y));
      double z = coef * Math.Exp(num / denom);
      result[i][j] = z;
      sum += z;
    }
  }
  for (int i = 0; i < dim; ++i)
    for (int j = 0; j < dim; ++j)
      result[i][j] = result[i][j] / sum;
  return result;
}

ガウス カーネルの生成は、多少混乱を招く作業になることがあります。なぜなら、カーネルで意図している用途によってアルゴリズムには多数のバリエーションがあり、近似の手法にもいくつかのバリエーションがあるためです。2 次元の連続ガウス カーネルに含まれる値についての、基本となる数学的定義は次のとおりです。

z = (1.0 / (2.0 * pi^2)) * exp((-(x^2 + y^2)) / (2 * sd^2))

ここでの x と y は、カーネル内で中央のセルを基準とするセルの x 座標と y 座標です。pi は数学定数で、exp は指数関数です。sd は、指定の標準偏差です。1.0 / (2.0 * Pi^2) という最初の係数項は、実際には 1 次元バージョンのガウス関数を正規化するための項です。一方 2D カーネルの場合は、最終的な値の合計が 1.0 (丸めエラーはありますが) になるように、カーネルの暫定値すべてを合計してからこの合計で各暫定値を除算することをお勧めします。図 4 では、この最終的な正規化を実現するために sum という変数を使用しています。したがって、coef という変数は冗長なのでコードから省略できます。今回のデモに coef 変数を含めた理由は、ほとんどの研究論文でカーネルの記述に係数項が使用されているためです。

変位フィールド

画像を変形するには、各ピクセルを (文字どおりではなく実質的に) 左右および上下に一定距離だけ移動する必要があります。MakeDisplace メソッドの定義を図 5 に示します。MakeDisplace メソッドが返す行列は "配列の配列" スタイルで、図 3 に示した概念の行列の半分に相当します。つまり、返される行列内のセル値は、X 方向または Y 方向へのピクセル移動の方向と距離に相当します。MNIST 画像のサイズは 28 x 28 ピクセルなので、MakeDisplace メソッドから返す行列も 28 x 28 になります。

図 5 変位フィールドの作成

private static double[][] MakeDisplace(int width, int height, int seed,
  double[][] kernel, double intensity)
{
  double[][] dField = new double[height][];
  for (int i = 0; i < dField.Length; ++i)
    dField[i] = new double[width];
  Random rnd = new Random(seed);
  for (int i = 0; i < dField.Length; ++i)
    for (int j = 0; j < dField[i].Length; ++j)
      dField[i][j] = 2.0 * rnd.NextDouble() - 1.0;
  dField = ApplyKernel(dField, kernel); // Smooth
  for (int i = 0; i < dField.Length; ++i)
    for (int j = 0; j < dField[i].Length; ++j)
      dField[i][j] *= intensity;
  return dField;
}

MakeDisplace メソッドは、初期のランダム値が -1 ~ +1 の行列を生成します。ApplyKernel ヘルパー メソッドは、図 3 に示したようにランダム値を平滑化します。平滑値は基本的に、距離が 0 ~ 1 の方向成分です。最後に、すべての値に明度のパラメーターを乗算して、引き延ばす距離を増やします。

カーネルと変位を適用する

カーネルを変位の行列に適用し、得られた平滑化変位を使用して新しいピクセル値を生成する作業は、かなり厄介です。この作業の最初の部分を図 6 に示します。図の左側は、8 x 8 の画像の X 方向におけるランダムな暫定変位値 (-1 ~ +1) を表しています。この図では、行 [3] 列 [6] の値 (0.40) を 3 x 3 のカーネルで平滑化しています。新しい変位は、現在の値とその 8 つの隣接値に対する加重平均です。それぞれの新しい変位値は言うなればその近接値の平均なので、最終的には互いに関連する値が生成されます。

平滑化が完了したら、変位値に (研究文献ではアルファとも呼ばれる) 明度係数を乗算します。たとえば、明度係数が 20 の場合、画像ピクセル (3, 6) に対する図 6 の最終的な X 変位は、0.16 * 20 = +3.20 になります。同様の Y 変位行列も考えられます。Y 変位行列 (3, 6) の最終的な値が -1.50 だとしましょう。ここでピクセル (3, 6) に対応する +3.20 と -1.50 の値を元の画像に適用すると、変形した画像を得られます。ただし、明確な違いは現れません。

Applying a Kernel to a Displacement Matrix
図 6 変位行列へのカーネルの適用

最初に、下限と上限を特定します。X 変位が +3.20 の場合、下限と上限はそれぞれ 3 と 4 になり、Y 変位が -1.50 の場合は -2 と -1 になります。4 つの境界値から、(3, -2)、(3, -1)、(4, -2)、および (4, -1) という 4 組の (x, y) 変位を生成します。これらの値が、インデックス (3, 6) にある元の画像ピクセル値に対応していることに注目してください。ピクセル インデックスを 4 組の変位と組み合わせて、(6, 4)、(6, 5)、(7, 4)、および (7, 5) という 4 組のインデックスを生成します。最終的に、(3, 6) にある変形画像のピクセル値は、インデックス (6, 4)、(5, 3)、(7, 4)、および (7, 5) にある元のピクセル値の平均になります。

使用する幾何学が原因で、多くの場合はカーネルのサイズを 3、5 などの奇数に制限します。変位行列の端付近にある暫定変位値を平滑化しようとすると、行列の端の外側までカーネルが "拡張" するために、問題が発生しやすいことに注意してください。この端の問題への対処方法はいくつかあります。1 つは、暫定変位行列にダミーの行と列を埋め込む方法です。埋め込む行数や列数は、カーネルのサイズの半分 (小数部切り捨てを使用) になります。

まとめ

今回説明した画像の弾性変形処理は、利用できる多数の手法の 1 つにすぎません。ここで紹介した変形アルゴリズムのうち、すべてではありませんがほとんどの出典は、研究論文「Best Practices for Convolutional Neural Networks Applied to Visual Document Analysis」(視覚的ドキュメント分析に応用した畳み込みニューラル ネットワークのベスト プラクティス、英語) です。この論文は、bit.ly/REzsnM (英語) からオンラインで参照できます。

デモ プログラムでは、画像認識システム向けの追加の学習用データを作成するために、変形画像を生成します。実際に画像認識システムの学習を行っている場合は、新しい学習用データをその場で生成するようデモ コードをリファクタリングすることも、変形画像を生成してからテキスト ファイルやバイナリ ファイルに保存するようリファクタリングすることもできます。

Dr. James McCaffrey は、ワシントン州レドモンドにある Microsoft Research に勤務しています。これまでに、Internet Explorer、Bing などの複数のマイクロソフト製品にも携わってきました。連絡先は donnm@microsoft.com (英語のみ) です。

この記事のレビューに協力してくれた技術スタッフの Wolf Kienzle (Microsoft Research) に心より感謝いたします。