机器学习领域中最迷人的主题之一是图像识别 (IR)。 使用红外系统的示例包括使用指纹或视网膜识别的计算机登录程序和机场安全系统的扫描乘客脸寻找某种通缉名单上的个人。MNIST 数据集是可用于实验的简单图像集合沙用红外的算法。 这篇文章并介绍了一个相对简单 C# 程序,向您介绍的 MNIST 数据集,这反过来你接触到红外的概念。
它不太可能你会需要使用红外大多数软件应用程序,但我觉得你可能有用的信息在这篇文章为四个不同的原因。 第一,没有更好的方法,了解的 MNIST 数据集和 IR 概念比通过试验与实际的代码。 第二,有一个基本的理解的红外将帮助您了解的功能和真实的、 先进的红外系统的限制。 第三,在这篇文章中解释的编程技术的几个可用于不同的、 更常见的任务。 第四,你就会找到红外有趣的在其自己的权利。
看到这篇文章去哪儿的最佳方法是要看一看在演示程序图 1。 演示计划是一个经典的 Windows 窗体应用程序。 标签为加载图像的按钮控件读入内存称为MNIST 的数据集的标准图像识别数据集。 数据集包含的 60,000 手写体数字从 0 到 9,已经数码化。 演示了作为一位映射的图像显示当前所选的图像的能力 (左边的图 1),和作为一个十六进制形式 (右侧) 中的像素值的矩阵。
图 1 显示 MNIST 图像
在后面的部分,我送你通过演示程序的代码。 因为该演示是一个 Windows 窗体应用程序的大部分代码与相关的 UI 的功能,并包含在多个文件中。 我在这里集中逻辑。 我到单个 C# 源文件中的可用在重构的演示代码msdn.microsoft.com/magazine/msdnmag0614。 若要编译下载,你可以将它保存在 MnistViewer.cs,作为本地计算机上创建一个新的 Visual Studio 项目然后将该文件添加到您的项目。 或者,您可以启动 Visual Studio 命令外壳程序 (其中知道,C# 编译器的位置)、 然后导航到保存下载,目录和发出命令 > csc.exe /target:winexe MnistViewer.cs 来创建可执行文件 MnistViewer.exe。 您可以运行该演示程序之前,您需要下载并保存两个 MNIST 的数据文件,如我在下一节中,解释和编辑演示的源代码以指向那些两个文件的位置。
本文假定您有至少中级技能与 C# (或类似的语言),但不会假设你知道任何有关 ir。 演示代码如此演示的代码重构为向非.NET 语言如 JavaScript 将是一个困难的任务使 Microsoft.NET 框架的广泛使用。
IR 文献中使用的术语往往差别很大。 图像分类、 模式识别、 模式匹配或模式分类,也可能会调用图像识别。虽然这些条款有不同的含义,他们是有时互换使用,这可以使有点困难的相关信息在互联网上寻找。
MNIST 数据集
混合的国家标准和技术 (简称 MNIST) 由红外研究员,作为基准来比较不同的红外算法创建数据集。 其基本思想是如果你有你想要测试红外的算法或软件的系统,可以运行您的算法或系统针对 MNIST 的数据集和比较您的结果与其他系统以前发布成果。
数据集包含的共 70,000 图像 ; 60,000 训练图像 (用于创建红外模型) 和 10,000 测试图像 (用于评估模型的精度)。 每个 MNIST 图像是一个单一的手写的数字字符的数字化的图片。 每个图像是 28 x 28 像素大小。 每个像素值是 0,表示白色,至 255,表示黑。 中间像素值表示的灰度级。 图 2 显示了训练集的前八位的图像。 对应于每个图像的实际数字是显然对人,但确定数字是非常困难的挑战的计算机。
图 2 首八 MNIST 训练图像
奇怪的是,训练数据和测试数据均存储在两个文件中,而不是在单个文件中。 其中一个文件包含图像的像素值和,另一个包含图像的标签信息 (0 到 9)。 每个的四个文件还包含标头信息,和所有的四个文件都存储在已经使用 gzip 格式压缩的二进制格式。
注意在图 1,该演示程序使用仅 60,000 项目训练集。 测试集的格式是相同的训练集。 MNIST 文件的主存储库是目前位于 yann.lecun.com/exdb/mnist。 培训的像素数据存储在文件火车-图像-idx3-ubyte.gz 和培训标签数据存储在文件火车-标签-idx1-ubyte.gz。 若要运行该演示程序,您需要转到 MNIST 的存储库站点,下载并解压的两个培训数据文件。 将文件解压缩,我用的免费的开源 7-Zip 实用程序。
创建 MNIST 查看器
若要创建 MNIST 演示程序,我发起了 Visual Studio,创建一个名为 MnistViewer 的新 C# Windows 窗体项目。 演示有没有重大的.NET 版本依赖关系,因此,任何版本的 Visual Studio 应该工作。
模板代码加载到 Visual Studio 编辑器后,我设置的 UI 控件。 我添加了两个 TextBox 控件 (textBox1,textBox2) 要坚持两个解压后的培训文件的路径。 我添加一个按钮控件 (button1),并给了它一个标签加载图像。 我添加了两个多个 TextBox 控件 (textBox3,textBox4) 以保存当前图像索引和下一个图像索引的值。 我使用 Visual Studio 设计器,分别设置"NA"和"0,"这些控件的初始值。
我添加了一个 ComboBox 控件 (comboBox1) 的图像放大倍数值。 使用设计器,我去到该控件的项集合,添加字符串"1"到"10"。我添加了第二个按钮控件 (button2),并给了它一个标签的显示下一次。 我添加了 PictureBox 控件 (pictureBox1),将其背景色属性设置为 ControlDark,以便看到控件的轮廓。 我将图片框大小设置为 280 x 280 允许最多 10 倍的放大倍率 (回顾 MNIST 图像是 28 x 28 像素为单位)。 我添加了第五个 (textBox5) 文本框以显示十六进制值的图像,然后将其多行属性设置为 True 和其字体属性设置为 8.25 磅 Courier New 和扩大其大小到 606 x 412。 而且,最后,我添加了一个列表框控件 (listBox1) 的日志记录消息。
后放置 UI 控件拖到 Windows 窗体,添加三个类范围字段:
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; ...
第一次两个字符串指向解压后的培训数据文件的位置。 你会需要编辑这些要运行演示的两个字符串。 第三个字段是一个程序定义 DigitImage 对象的数组。
我编辑窗体的构造函数略成 textBox1 和 textBox2 地点的文件路径,并给予放大倍数初始值 6:
public Form1() { InitializeComponent(); textBox1.Text = pixelFile; textBox2.Text = labelFile; comboBox1.SelectedItem = "6"; this.ActiveControl = button1; }
我用的 ActiveControl 属性来设置初始焦点到 button1 控件,只是为了方便。
创建一个类来保存 MNIST 映像
我创建了一个小容器类来表示单个 MNIST 图像,如中所示图 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 像素为单位),但添加的宽度和高度字段使类更大的灵活性。 字段的像素是数组的数组样式矩阵。 很多与语言不同,C# 具有真实的多维数组和你可能想要使用它。每个单元格的值是 byte 类型的只是一个介于 0 和 255 之间的整数值。 字段标签还声明为类型字节,但可能已经类型 int 或 char 或字符串。
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 培训像素文件的格式已初始魔法的整数 (32 位) 值 2051,其次是映像的数量随着其次 60,000 图像 x 28 x 28 像素为单位) 的整数,后面的行和列作为整数,数数 = 47,040,000 字节值。 所以,在打开后的二进制文件,第一次四个整数是阅读使用 ReadInt32 方法。 例如,映像的数量是由读取:
int imageCount = brImages.ReadInt32(); imageCount = ReverseBytes(imageCount);
有趣的是,MNIST 文件而不是在运行 Microsoft 软件的硬件最常用的更加通常小字节序格式存储中大 endian 格式 (使用的一些非英特尔处理器) 的整数值。 所以,如果您使用正常的电脑风格硬件,若要查看或使用的任何整数值,它们必须将从转换大字节序小字节序。 这意味着扭转四个字节组成整数的顺序。 例如,幻数 2051年大字节序形式是:
00000011 00001000 00000000 00000000
小字节序形式存储在相同的值是:
00000000 00000000 00001000 00000011
请注意这是四个字节为单位),必须扭转,而不是整个的 32 位序列。 有许多方法来扭转字节为单位)。 我用了一种高级别的方法,利用了.NET BitConverter 类,而不是使用一种低级、 位操作方法:
public static int ReverseBytes(int v) { byte[] intAsBytes = BitConverter.GetBytes(v); Array.Reverse(intAsBytes); return BitConverter.ToInt32(intAsBytes, 0); }
方法 LoadData 读取,但不会使用的标头信息。 您可能要检查的四个值 (2051、 60000、 28、 28) 来验证该文件没有被损坏。 后打开这两个文件并读取头整数,LoadData 读取 28 x 28 = 784 连续像素值的像素文件和存储这些值,然后从标签文件中读取单个标签的值,并将它具有的像素值组合成一个 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 控件中的十六进制值。PictureBox 控件的图像属性可以接受一个位图对象,然后呈现该对象。 很好! 可以将一个位图对象视为实质上的图像。 请注意那里是一个.NET 图像类,但它是一个抽象基类,用于定义 Bitmap 类。 所以显示图像的关键是要从程序定义的 DigitImage 对象生成一个位图对象。 这通过帮助器方法 MakeBitmap 中列出的图 6。
图 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; }
该方法不是很长,但它是有点微妙。 位图构造函数接受一个宽度和一个高度为整数,其中为 MNIST 的基本数据将永远 28 和 28。 如果放大倍数值为 3,则位图图像将 (28 * 3) 由 (28 * 3) = 84 由 84 像素大小的位图中的每个 3 由 3 广场将代表原始图像的一个像素。
提供一个位图对象的值是通过间接的图形对象。 在嵌套循环中,当前的像素值被辅之以 255 这样生成的图像将是白色背景上的黑色/灰色数字。 如果没有补充,该图像将黑色背景上的白色/灰色数字。 若要使灰度颜色,红色、 绿色和蓝色的参数相同的值被传递给 FromArgb 方法。 替代方法是将像素值传递到要获得一个彩色的图像 (深浅的红色、 绿色或蓝色),而不是一种灰度图像的 RGB 参数之一。
FillRectangle 方法绘制位图对象的区域。 第一个参数是颜色。 第二和第三个参数是 x 和 y 的矩形的左上角的坐标。 请注意,x 是向上向下进源图像像素矩阵对应于索引 j。 FillRectangle 的第四和第五个参数是要绘制的矩形区域的高度与宽度从第二和第三个参数指定的角开始。
例如,假设当前要显示的像素是在我 = 2 和 j = 5 在源图像中,并且值 = 200 (代表深灰色)。 如果放大倍数值设置为 3,该位图对象将由 84 84 像素大小。 FillRectangle 方法将开始绘画在 x = (5 * 3) = 15 列和 y = (2 * 3) = 行 6 的位图和油漆颜色 (55,55,55) 3 由 3 像素的矩形 = 暗灰色。
显示图像的像素值
如果你看回中的代码图 5,你会看到 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; }
方法构造一个长字符串包含嵌入式的换行符的字符,使用字符串串联为简单起见。 如中所示当该字符串将被放入一个 TextBox 控件具有的多行属性设置为 True 时,将显示该字符串图 1。 尽管十六进制值可能会有点更难以解释比基础值 10,十六进制值格式更好。
从这里去哪儿?
图像识别是一个概念上很简单,但非常难以在实践中的问题。 了解红外的好第一步是要能够可视化知名的 MNIST 数据集,这篇文章中所示。 如果你看看图 1,你会看到任何 MNIST 图像是真的什么都不超过 784 值与关联的标签,如"4"。所以图像识别归结为找到一些接受 784 值作为输入并返回,作为输出,10 概率表示投入的意思分别是 0 到 9,likelihoods 的函数。
红外的常用方法是使用某种形式的神经网络。 例如,您可以使用 10 个节点创建 784 输入节点与神经网络、 1,000 节点的隐藏图层和输出层。 这种网络会有共 (784 * 1000) + (1000 * 10) + (1000 + 10) = 795,010 权重和偏置值来确定。 甚至,60,000 训练图像,这将是一个非常困难的问题。 但有几个令人着迷的技术,您可以使用来帮助获取良好形象识别器。 这些技术包括使用卷积神经网络和生成额外的培训图像使用弹性变形。