測試回合

使用 MNIST 影像辨識資料集

James McCaffrey

下載代碼示例

James McCaffrey機器學習領域中最迷人的主題之一是圖像識別 (IR)。 使用紅外系統的示例包括使用指紋或視網膜識別的電腦登錄程式和機場安全系統的掃描乘客臉尋找某種通緝名單上的個人。 MNIST 資料集是可用於實驗的簡單圖像集合­沙用紅外的演算法。 這篇文章並介紹了一個相對簡單 C# 程式,向您介紹的 MNIST 資料集,這反過來你接觸到紅外的概念。

它不太可能你會需要使用紅外大多數軟體應用程式,但我覺得你可能有用的資訊在這篇文章為四個不同的原因。第一,沒有更好的方法,瞭解的 MNIST 資料集和 IR 概念比通過試驗與實際的代碼。第二,有一個基本的理解的紅外將説明您瞭解的功能和真實的、 先進的紅外系統的限制。第三,在這篇文章中解釋的程式設計技術的幾個可用於不同的、 更常見的任務。第四,你就會找到紅外有趣的在其自己的權利。

看到這篇文章去哪兒的最佳方法是要看一看在演示程式圖 1。演示計畫是一個經典的 Windows 表單應用程式。標籤為載入圖像的按鈕控制項讀入記憶體稱為 MNIST 的資料集的標準圖像識別資料集。資料集包含的 60,000 手寫體數位從 0 到 9,已經數碼化。演示了作為一位映射的圖像顯示當前所選的圖像的能力 (左邊的圖 1),和作為一個十六進位形式 (右側) 中的圖元值的矩陣。

Displaying MNIST Images圖 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 顯示了訓練集的前八位的圖像。對應于每個圖像的實際數位是顯然對人,但確定數位是非常困難的挑戰的電腦。

First Eight MNIST Training Images圖 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 訓練圖像,這將是一個非常困難的問題。 但有幾個令人著迷的技術,您可以使用來説明獲取良好形象識別器。 這些技術包括使用卷積神經網路和生成額外的培訓圖像使用彈性變形。

**博士。**James McCaffrey 為微軟在華盛頓州雷德蒙德的研究工作 他曾在幾個 Microsoft 產品,包括互聯網資源管理器和 Bing。麥卡弗裡也可以撥打 jammc@microsoft.com

由於以下的技術專家對本文的審閱:狼 Kienzle (Microsoft 研究)