Compartilhar via


Execução de teste

Trabalhar com o conjunto de dados de reconhecimento de imagem MNIST

James McCaffrey

Baixar o código de exemplo

James McCaffreyUm dos temas mais fascinantes no campo da aprendizagem de máquina é o reconhecimento de imagem (IR). Exemplos de sistemas que empregam IR incluem programas de logon de computador que usam impressão digital ou a identificação de retina e sistemas de segurança do aeroporto que digitalizar rostos de passageiros à procura de indivíduos numa espécie de lista de procurados. O conjunto de dados MNIST é uma coleção de imagens simples que pode ser usado para exper­iment com algoritmos de IR. Este artigo apresenta e explica um programa c# relativamente simples que apresenta o conjunto de dados MNIST, que por sua vez, familiariza-te com os conceitos de IR.

É improvável que você precisará usar IR na maioria das aplicações de software, mas acho que você pode encontrar as informações neste artigo útil para quatro razões diferente. Primeiro, não há nenhuma melhor maneira de entender o conjunto de dados MNIST e conceitos de IR do que por experiências com código real. Em segundo lugar, ter uma compreensão básica do IR ajudará a compreender as capacidades e limitações do reais, sofisticados sistemas de IR. Em terceiro lugar, várias das técnicas de programação explicadas neste artigo podem ser usadas para tarefas diferentes, mais comuns. E em quarto lugar, você pode encontrar IR interessante em sua própria direita.

A melhor maneira de ver para onde vai este artigo é para dar uma olhada no programa de demonstração em Figura 1. O programa de demonstração é um aplicativo de formulários do Windows clássico. O controle de botão rotulado carregar imagens lê na memória um conjunto de dados de reconhecimento de imagem padrão chamado o conjunto de dados MNIST. O conjunto de dados consiste de 60.000 manuscritos dígitos de 0 a 9 que foram digitalizados. O demo tem a capacidade de exibir a imagem selecionada como uma imagem bitmap (no lado esquerdo do Figura 1) e como uma matriz de valores de pixel em formato hexadecimal (à direita).

Displaying MNIST Images
Figura 1 exibir imagens MNIST

As seções a seguir, apresentarei o código para o programa de demonstração. Porque o demo é uma aplicação Windows Forms, o código está relacionada com a funcionalidade de interface do usuário e está contido em vários arquivos. Aqui focar a lógica. Eu refatorado o código de demonstração em um único c# arquivo de fonte que está disponível em msdn.microsoft.com/magazine/msdnmag0614. Para compilar o download, você pode salvá-lo no computador local como MnistViewer.cs, em seguida, crie um novo projeto de Visual Studio e adicionar o arquivo ao seu projeto. Como alternativa, você pode iniciar um shell de comando de Visual Studio (que sabe a localização do compilador c#), então navegue até o diretório onde você salvou o download e execute o comando > /target: winexe csc.exe MnistViewer.cs para criar o executável MnistViewer.exe. Antes de executar o programa de demonstração, você precisará baixar e salvar os dois arquivos de dados MNIST, como eu explicar na próxima seção e editar o código-fonte demo para apontar para a localização desses dois arquivos.

Este artigo pressupõe que você tem habilidades de pelo menos nível intermediário com c# (ou linguagem similar), mas não assumir que sabe alguma coisa sobre ir O código de demonstração faz uso extensivo do Microsoft .NET Framework tão Refatorando o código de demonstração para uma linguagem não-.NET, tais como JavaScript seria uma tarefa difícil.

A terminologia usada na literatura de IR tende a variar bastante. Reconhecimento de imagem também pode ser chamado de classificação de imagens, reconhecimento de padrões, a correspondência de padrões ou classificação padrão. Embora estes termos têm significados diferentes, são às vezes usados como sinônimos, que pode fazer a pesquisa na Internet para obter informações relevantes um pouco difícil.

O conjunto de dados MNIST

O misto Instituto Nacional de padrões e tecnologia (MNIST para o short) conjunto de dados foi criado por pesquisadores de IR para atuar como um benchmark para comparar diferentes algoritmos de IR. A idéia básica é que, se você tiver um sistema de software ou algoritmo de IR que você deseja testar, você pode executar seu algoritmo ou sistema contra o conjunto de dados MNIST e comparar seus resultados com os resultados publicados anteriormente por outros sistemas.

O conjunto de dados consiste de um total de 70.000 imagens; 60.000 imagens de formação (usadas para criar um modelo de IR) e 10.000 imagens de teste (usadas para avaliar a precisão do modelo). Cada imagem MNIST é uma foto digitalizada de um caractere único dígito manuscritas. Cada imagem é de 28 x 28 pixels de tamanho. Cada valor de pixel é entre 0, que representa o branco, e 255, que representa o preto. Valores de pixels intermediários representam tons de cinza. Figura 2 mostra as primeiras oito imagens no conjunto de treinamento. O dígito real que corresponde a cada imagem é óbvio para os seres humanos, mas identificar os dígitos é um desafio muito difícil para computadores.

First Eight MNIST Training Images
Figura 2 primeiras oito imagens de treinamento MNIST

Curiosamente, os dados de treinamento e os dados de teste são cada um armazenado em dois arquivos, em vez de um único arquivo. Um arquivo contém os valores de pixel para imagens e o outro contém as informações de rótulo (0 a 9) para as imagens. Cada um dos quatro arquivos também contém informações de cabeçalho, e todos os quatro arquivos são armazenados em um formato binário que foi comprimido utilizando o formato gzip.

Observe na Figura 1, o programa de demonstração usa apenas o conjunto de treinamento de 60.000-item. O formato do conjunto de teste é idêntico do conjunto de treinamento. O principal repositório para os arquivos MNIST está atualmente localizado no yann.lecun.com/exdb/mnist. Os dados de pixel treinamento são armazenados no arquivo trem-imagens-idx3-ubyte.gz e os dados do rótulo de treinamento são armazenados no arquivo trem-etiquetas-idx1-ubyte.gz. Para executar o programa de demonstração, você precisa ir ao site do repositório MNIST e baixar e descompactar os dois arquivos de dados de treinamento. Para descompactar os arquivos, usei o utilitário do 7-Zip livre, open source.

Criando o MNIST Viewer

Para criar o programa de demonstração MNIST, lançou o Visual Studio e criado um novo projeto de c# Windows Forms chamado MnistViewer. O demo não tem significativa .NET versão dependências para que qualquer versão do Visual Studio deve funcionar.

Após o código do modelo carregado no editor do Visual Studio , configurar os controles de interface do usuário. Eu adicionei dois controles TextBox (textBox1, textBox2) para manter os caminhos para os arquivos descompactados formação dois. Eu adicionei um controle de botão (button1) e deu um rótulo de carregar imagens. Eu adicionei dois controles TextBox mais (textBox3, textBox4) para armazenar os valores de índice de imagem atual e o próximo índice de imagem. Usando o designer de Visual Studio , eu definir os valores iniciais desses controles para "NA" e "0", respectivamente.

Eu adicionei um controle ComboBox (comboBox1) para o valor de ampliação de imagem. Usando o designer, fui para a coleção de itens do controle e adicionado as seqüências de caracteres "1" a "10". Eu adicionei um segundo controle de botão (button2) e deu um rótulo de exibição próximo. Eu adicionei um controle PictureBox (pictureBox1) e defina sua propriedade BackColor para ControlDark para que o contorno do controle pode ser visto. Eu definir o tamanho do PictureBox para 280 x 280 para permitir uma ampliação de até 10 vezes (Lembre-se de uma imagem MNIST é de 28 x 28 pixels). Eu adicionei um quinto TextBox (textBox5) para exibir os valores hexadecimais de uma imagem, em seguida, definir sua propriedade Multiline como True e sua propriedade Font para Courier New, pt. 8,25 e expandiu seu tamanho para 606 x 412. E, finalmente, adicionei um controle ListBox (listBox1) para as mensagens de log.

Depois de colocar os controles de interface do usuário para o formulário do Windows, eu adicionei três campos de escopo de classe:

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

As duas primeiras cordas apontam para os locais dos arquivos de dados de treinamento descompactado. Você precisará editar essas duas seqüências de caracteres para executar o demo. O terceiro campo é uma matriz de objetos definidos pelo programa de DigitImage.

Eu editei o construtor de formulário ligeiramente para colocar os caminhos de arquivo em textBox1 e textBox2 e dar a ampliação de um valor inicial de 6:

public Form1()
{
  InitializeComponent();
  textBox1.Text = pixelFile;
  textBox2.Text = labelFile;
  comboBox1.SelectedItem = "6";
  this.ActiveControl = button1;
}

Eu usei a propriedade ActiveControl para definir o foco inicial para o controle button1, apenas para sua conveniência.

Criando uma classe para manter uma imagem MNIST

Eu criei uma classe de recipiente pequeno para representar uma única imagem MNIST, como mostrado em Figura 3. Eu nomeei a classe DigitImage mas você pode querer renomeá-lo para algo mais específico, tais como MnistImage.

Definição de classe de DigitImage Figura 3

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

Eu declarado todos os membros da classe com escopo público pela simplicidade e removido normal verificação de erros para manter o tamanho do código pequeno. Altura e largura de campos poderiam ter foi omitidos porque todas as imagens MNIST são 28 x 28 pixels, mas adicionar os campos largura e altura oferece a classe mais flexibilidade. Campo pixels é uma matriz de matrizes-estilo-matriz. Ao contrário de muitas linguagens, c# tem uma matriz multidimensional de verdade e você pode querer usá-lo. Cada valor de célula é tipo byte, que é apenas um valor inteiro entre 0 e 255. Rótulo do campo também é declarado como tipo byte, mas poderia ter sido tipo int ou char ou string.

O construtor da classe DigitImage aceita valores para largura, altura, a matriz de pixels e a etiqueta e só copia os valores de parâmetro para os campos associados. Poderia copiei os valores de pixel por referência em vez de pelo valor, mas que pode levar a efeitos colaterais indesejados se mudaram os valores de pixel de origem.

Carregar os dados MNIST

Eu cliquei duas vezes no controle button1 para registrar seu manipulador de eventos. O manipulador de eventos fazendas a maioria do trabalho para o método 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");
}

O método LoadData está listado em Figura 4. LoadData abre arquivos o pixel e o rótulo e lê-los simultaneamente. O método começa com a criação de uma matriz de 28 x 28 local dos valores de pixel. Classe BinaryReader .NET útil é projetado especificamente para a leitura de arquivos binários.

Figura 4 o método 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;
}

O formato do arquivo de pixels do treinamento de MNIST tem um mágico inteiro inicial (32 bits) tem valor 2051, seguido do número de imagens como um inteiro, seguido do número de linhas e o número de colunas como inteiros, seguido por 60.000 imagens x 28 x 28 pixels = 47,040,000 byte valores. Então, depois de abrir os arquivos binários, os quatro primeiros números inteiros são lidos usando o método ReadInt32. Por exemplo, o número de imagens é lido por:

int imageCount = brImages.ReadInt32();
imageCount = ReverseBytes(imageCount);

Curiosamente, os arquivos MNIST armazenam valores inteiros em formato big endian (usado por alguns processadores não-Intel) ao invés de formato endian pouco mais normal que é mais comumente usado em hardware que executa o software Microsoft. Então, se você estiver usando hardware de PC estilo normal, para exibir ou usar qualquer um dos valores de número inteiro, eles devem ser convertidos de big endian para little endian. Isto significa inverter a ordem dos quatro bytes que compõem o inteiro. Por exemplo, é o número mágico 2051 no formulário endian grande:

00000011 00001000 00000000 00000000

Esse mesmo valor armazenado em little endian formulário é:

00000000 00000000 00001000 00000011

Reparou que são os quatro bytes que deve ser invertidos, em vez de toda a sequência de 32 bits. Há muitas maneiras de reverter bytes. Eu usei uma abordagem de alto nível que utiliza o .NET BitConverter classe, em vez de usar uma abordagem de baixo nível, manipulação de bits:

public static int ReverseBytes(int v)
{
  byte[] intAsBytes = BitConverter.GetBytes(v);
  Array.Reverse(intAsBytes);
  return BitConverter.ToInt32(intAsBytes, 0);
}

Método LoadData lê, mas não usa, as informações de cabeçalho. Você pode querer verificar os quatro valores (2051, 60000, 28, 28) para verificar se o arquivo não foi danificado. Após abrir os dois arquivos e ler os inteiros de cabeçalho, leituras LoadData 28 x 28 = 784 valores consecutivos de pixel do arquivo pixel e lojas aqueles valores, em seguida lê um valor de rótulo único do arquivo de rótulo e combina com os valores de pixel em um objeto DigitImage, que então armazena na matriz de trainData de escopo de classe. Observe a que imagem explícita aparece. Cada imagem possui um ID de índice implícito, que é a posição baseada em zero da imagem na seqüência de imagens.

Exibir uma imagem

Eu cliquei duas vezes no controle button2 para registrar seu manipulador de eventos. O código para exibir uma imagem é mostrada em Figura 5.

Figura 5-exibir uma imagem 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);
}

O índice da imagem para exibir é obtido a partir da textBox4 (índice de imagem seguinte) de controle, em seguida, uma referência para a imagem é puxada da matriz trainImage. Você pode querer adicionar uma verificação para certificar-se de que os dados de imagem foi carregados na memória antes de tentar acessar uma imagem. A imagem é exibida em duas maneiras, primeiro em uma forma visual no controle PictureBox e em segundo lugar, como valores hexadecimais no controle TextBox grande. Propriedade Image de um controle PictureBox pode aceitar um objeto Bitmap e em seguida, processar o objeto. Muito bom! Você pode pensar de um objeto Bitmap como essencialmente uma imagem. Observe que há uma classe .NET imagem, mas é uma classe base abstrata que é usada para definir a classe Bitmap. Então é a chave para exibir uma imagem para gerar um objeto Bitmap do objeto definido pelo programa DigitImage. Isto é feito pelo método auxiliar MakeBitmap, que está listado em Figura 6.

Figura 6 o método de 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;
}

O método não é longo, mas é um pouco sutil. O Construtor Bitmap aceita uma largura e uma altura como inteiros, que, para dados de base MNIST, serão sempre 28 e 28. Se o valor de ampliação é 3, então a imagem Bitmap será (28 * 3) por (28 * 3) = 84 por 84 pixels de tamanho, e cada quadrado de 3 por 3 no Bitmap irá representar um pixel da imagem original.

Fornecendo os valores para um objeto de Bitmap é feito indirectamente através de um Graphics objeto. Dentro do loop aninhado, o valor do pixel atual é complementado por 255 para que a imagem resultante será um dígito preto/cinza contra um fundo branco. Sem a complementar, a imagem seria um dígito branco/cinza contra um fundo preto. Para que uma cor de escala de cinza, os mesmos valores para os parâmetros de vermelhos, verdes e azuis são passados para o método FromArgb. Uma alternativa é passar o valor de pixel para apenas um dos parâmetros RGB para obter uma imagem colorida (tons de vermelho, verde ou azul), ao invés de uma imagem em tons de cinza.

O método FillRectangle pinta uma área do objeto Bitmap. O primeiro parâmetro é a cor. O segundo e terceiro parâmetros são o x e y coordenadas do canto superior esquerdo do retângulo. Observe que x é de cima para baixo, que corresponde ao índice j na matriz de pixel da imagem de origem. Os quarto e quinto parâmetros para FillRectangle são a largura e altura da área retangular para pintar, a partir do canto especificado pelos parâmetros de segunda e terceiros.

Por exemplo, suponha que a corrente é de pixel a ser exibido no i = 2 e j = 5 na imagem de origem, e tem valor = 200 (representando um cinza escuro). Se o valor de ampliação é definido como 3, o objeto Bitmap será 84-por-84 pixels de tamanho. O método FillRectangle iria começar a pintar em x = (5 * 3) = coluna 15 e y = (2 * 3) = linha 6 do Bitmap e pinta um retângulo de pixel de 3 por 3 com cor (55,55,55) = cinza escuro.

Exibir os valores de Pixel de uma imagem

Se você referir o código em Figura 5, você verá esse método auxiliar PixelValues é usado para gerar a representação hexadecimal de valores de pixel de uma imagem. O método é simples e curta:

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

O método constrói uma longa seqüência de caracteres com caracteres de nova linha incorporada, usando concatenação de seqüência de caracteres para a simplicidade. Quando a seqüência de caracteres é colocada em um TextBox controle que tem sua propriedade Multiline definida como True, a seqüência de caracteres será exibida conforme mostrado no Figura 1. Embora os valores hexadecimais podem ser um pouco mais difícil de interpretar do que a base 10 valores, valores hexadecimais formato mais muito bem.

Onde a partir daqui?

Reconhecimento de imagem é um problema que é conceitualmente simples, mas extremamente difícil na prática. Um bom primeiro passo em direção a compreensão de IR é para ser capaz de visualizar o conjunto de dados MNIST conhecido como mostrado neste artigo. Se você olhar para Figura 1, você verá que qualquer imagem MNIST é realmente nada mais de 784 valores com um rótulo associado, tais como "4". Modo de reconhecimento de imagem se resume a encontrar alguma função que aceita 784 valores como entradas e retorna, como saída, 10 probabilidades que representa as probabilidades de que as entradas dizer 0 a 9, respectivamente.

Uma abordagem comum para IR é usar algum tipo de rede neural. Por exemplo, você poderia criar uma rede neural com 784 nós de entrada, uma camada oculta de 1.000 nós e uma camada de saída com 10 nós. Essa rede teria um total de (784 * 1000) + (1000 * 10) + (1000 + 10) = 795.010 pesos e valores viés para determinar. Mesmo com 60.000 imagens de treinamento, isso seria um problema muito difícil. Mas existem várias técnicas de fascinantes, que você pode usar para ajudar a obter um reconhecedor de boa imagem. Estas técnicas incluem usando uma rede neural convolucional e gerar imagens de treinamento adicional usando distorção elástica.

**Dr.**James McCaffrey funciona para Microsoft Research em Redmond, Wash. Ele trabalhou em vários produtos Microsoft, incluindo o Internet Explorer e Bing. McCaffrey pode ser contatado em jammc@microsoft.com.

Agradecemos ao seguinte especialista técnico pela revisão deste artigo: Lobo Kienzle (Microsoft Research)