测试运行

扭曲 MNIST 图像数据集

James McCaffrey

下载代码示例

James McCaffrey混合的美国国家标准和技术 (MNIST) 数据集是 70,000 小图像的手写体数字的集合。创建的数据,作为基准的图像识别算法。虽然 MNIST 图像很小 (28 x 28 像素),和仅有 10 可能的数字 (0 到 9),承认和有 60,000 训练图像 (用 10000 幅伸出来测试模型的准确性) 创建的图像识别模型,经验表明认识到 MNIST 图像是一个困难的问题。

对付困难模式分类问题,其中包括图像识别的一个方法是使用更多的训练数据。一个聪明的方法以编程方式生成更多的训练数据是扭曲每个原始图像。看一看该演示程序中图 1。图 4 中在左边,原始的 MNIST 形式的数字和数字后使用右边的弹性变形失真。中的演示应用程序右上角的参数指示失真取决于位移随机种子、 内核的大小和标准偏差和强度值。

A Distorted MNIST Image
图 1 扭曲 MNIST 图像

不太可能你需要扭曲图像中大多数的工作环境,但你可能有用的信息在这篇文章有三个原因。第一,了解确切地图像失真是如何通过看实际的代码将帮助您了解图像识别的很多文章。第二,在图像的失真中使用的编程技术的几个可以在其他,更常见的编程方案中很有用。第三,你会发现图像失真一个有趣的话题为其自身的缘故。

这篇文章假设你有高级编程技能,但不会假定你知道任何图像失真。该演示程序在 C# 中的编码和 Microsoft.NET 框架的广泛利用,所以重构为非.NET 的语言将是具有挑战性。演示了最正常的错误检查去除,以保持较小的代码大小和主要思路明确。因为是由 Visual Studio 创建一个 Windows 窗体应用程序的演示,很多代码与 UI 相关,传播到几个文件。不过,我已经重构到单个 C# 源代码文件,现已在演示代码 msdn.microsoft.com/magazine/msdnmag0714。你可以在几个地方在互联网上找到的 MNIST 数据。主存储库是在 yann.lecun.com/exdb/mnist

程序的整体结构

要创建该演示程序我启动 Visual Studio,并创建一个名为 MnistDistort 的 Windows 窗体应用程序。用户界面有八个 textbox 控件的路径到解压缩后的 MNIST 数据文件 (一个像素文件、 一个标签文件) ; 指数的当前显示和下一个图像 ; 和种子值、 内核大小、 内核标准偏差和变形过程的强度值。一个下拉列表控件保存值以放大图像视图。有三个按钮控件加载到内存中的 60,000 的 MNIST 图像和标签、 显示的图像,和扭曲所显示的图像。有两个 PictureBox 控件来显示经常和扭曲的图像。最后,一个 ListBox 控件用于显示进度和日志记录消息。

顶部的源代码,该代码我删除对不需要命名空间的引用,然后添加 System.IO 命名空间必须能够读取 MNIST 数据文件的引用。

我添加了名为 trainImages,其中包含对程序定义的 DigitImage 对象和变量以保存两个 MNIST 的数据文件的位置的引用类范围数组:

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

在窗体构造函数中我添加了这些六行代码:

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

Button1 click 事件处理程序加载到内存中的 60,000 的图像:

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

Button2 click 事件处理程序将显示下一个图像并更新 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 click 事件处理程序,调用是在提出的方法图 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;
}

扭曲了的方法调用的方法 MakeKernel (来创建一个平滑的矩阵),MakeDisplace (方向和变形图像中的每个像素的距离) 和置换 (以实际变形源映像)。 帮助器方法,MakeDisplace 调用子帮手 ApplyKernel 到光滑的位移值。 小组的帮助器方法,ApplyKernel 调用分一分帮助器方法垫。

弹性变形

扭曲图像使用弹性变形的基本思想是相当简单的。 如果是 MNIST 的图像,您想要稍微移动现有的每个像素。 但细节不是那么简单。 一个幼稚的做法,独立移动的每个像素会导致新的图像出现破裂,而不是拉伸。 例如,考虑中的概念图像图 3。 这两个图像表示一个 5 x 5 部分的图像的失真。 每个箭头指示的方向和一个移动的一个对应的像素距离。 左侧的图像显示更多或更少的随机向量,其中会分手,而不是扭曲图像。 右图中都相互关联的导致拉伸图像的矢量。

Random vs. Smoothed Displacement Fields
图 3 随机 vs。 平滑的位移场

所以诀窍在于取代彼此接近像素移动相对较相似,但不确切,相同、 方向和距离的一种中每个像素。 这可以通过使用称为连续的高斯核函数矩阵。 整体思路是可能最好的解释使用的代码。 在演示此方法,请考虑:

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);
}

扭曲了的方法接受一个 DigitImage 对象和相关到内核的四个数值参数。 类型 DigitImage 是一个程序 — —­定义类,表示弥补 MNIST 图像的像素 28 x 28 字节。 该方法首先创建一个内核,内核和 kStdDev 的大小是影响位移的像素为单位) 将是多么的相似的值 kDim 在哪里。

来取代一个像素,是有必要知道如何远左-右的方向,和上下方向移动。 此信息存储在数组 xField 和 yField,分别,和使用 helper 方法 MakeDisplace 计算。 帮助器方法置换接受 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 内核:

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;
}

生成高斯核可以有些令人费解的任务,因为有很多算法变化取决于内核打算如何使用,以及近似技术的几个变化。 基本的数学定义为一个二维连续高斯内核中的值是:

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) 是实际上一个归一化处理期限为一维高斯函数的版本。 但为 2D 的内核,您想要所有内核初步值都相加,然后除以每个初步的值以便最后的所有值将都添加到 1.0 (舍入错误)。 在图 4,这最后的正常化使用名为 sum 的变量来完成。 因此,名为系数的变量是多余的可以省略从代码 ; 变量系数是包括在这里,因为大多数研究论文描述了内核使用系数一词。

位移场

要扭曲图像,必须移动的每个像素 (实际上,不能随便) 一定的距离向左或向右,并向上或向下。 在中定义的方法 MakeDisplace 图 5。 方法 MakeDisplace 返回一个数组的数组样式矩阵对应到一半中的概念矩阵的图 3。 那就是,返回矩阵的单元格中的值对应于一个方向,在 x 方向或 y 方向移动的像素的大小。 因为 MNIST 图像的大小为 28 × 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。 左边的部分的图表示-1 和 + 1 在 x 方向为 8 x 8 的图像之间的初步随机位移值。 时行 [3],[6] (0.40) 列的值是使用一个 3 x 3 内核被平滑。 新的位移是加权的平均数的当前值和值的八个最接近的邻居。 因为每个新的位移值在本质上是它的邻居的平均数,净效应是生成彼此相关的值。

平滑处理后, 的位移值乘以强度因子 (有时称为研究文献的阿尔法)。 例如,如果强度因子是 20,然后在中的最后 x 位移图 6 为图像的像素在 (3,6) 将会为 0.16 * 20 = +3.20。 会有一个类似的 y 位移矩阵。 假设最终值在 (3,6) 在 y 位移矩阵是-1.50。 对应于在像素的值 +3.20 和-1.50 (3,6) 现在应用到的源图像产生失真的图像,但不是完全明显的方式。

Applying a Kernel to a Displacement Matrix
图 6 将内核应用于位移矩阵

第一,上限与下限确定。 为 +3.20 x-位移,这些都是 3 和 4。 为-1.50 y 位移,他们是-2,-1。 四个边界生成四 (x,y) 位移对:(3, -2), (3, -1), (4, -2), (4, -1). 回忆起这些值对应于原始图像的像素值在指数 (3,6)。 像素指标结合的四个位移对生成四个指标对:(6, 4), (6, 5), (7, 4), (7, 5). 最后,在的扭曲图像的像素值 (3,6) 指数 (6,4),在原始的像素值的平均值是 5 3),(7,4) 和 (7,5)。

由于使用的几何形状,它是最常见的将内核限制为 3、 5 等奇数尺寸。 请注意将尝试平滑位移矩阵的边缘附近的任何初步的位移值,因为内核这么说,会超出矩阵的边缘问题。 有几种方法来处理的边缘问题。 一种方法是垫初步位移矩阵与虚拟的行和列。 要垫行或列的数目将等于二分之一 (使用整数截断) 的维度的内核。

总结

在这篇文章描述的图像弹性变形过程是很多可能的办法之一。 最多,但不是全部,提出的失真算法的这里是改编的研究文章,"最佳做法为卷积神经网络应用于视觉文档分析,"这是可用在线在 bit.ly/REzsnM

演示程序产生变形的影像创造额外的培训数据的图像识别系统。 如果你实际上训练图像识别系统您可以重构的演示代码来生成新的培训数据的飞行,或您可以重构代码以生成,然后将扭曲的图像保存为文本或二进制文件。

博士。 James McCaffrey 为在雷德蒙微软研究院工作 他曾在几个 Microsoft 产品,包括互联网资源管理器和必应。联系到他在 jammc@microsoft.com

衷心感谢以下技术专家对本文的审阅:狼 Kienzle (微软研究)