Julho de 2018

Volume 33 – Número 7

Execução de Teste - Introdução à Classificação de imagens do DNN usando CNTK

Por James McCaffrey

James McCaffreyClassificação de imagem consiste em determinar qual categoria de uma imagem de entrada pertence, por exemplo, identificando uma fotografia como uma que contém "apples" ou "laranjas" ou "bananas". As duas abordagens mais comuns para classificação de imagem são usando uma padrão rede neural profunda (DNN) ou usando uma rede neural convolucional (CNN). Neste artigo, explicarei a abordagem DNN, usando a biblioteca CNTK.

Veja a Figura 1 para saber o que será mostrado neste artigo. O programa de demonstração cria um modelo de classificação de imagem para um subconjunto do conjunto de dados modificado Instituto Nacional de padrões e tecnologia (MNIST). O conjunto de dados de treinamento de demonstração consiste em 1.000 imagens de dígitos manuscritos. Cada imagem é 28 alta por 28 pixels de largura (784 pixels) e representa um dígito, 0 a 9.

Classificação de imagens usando uma DNN com CNTK
Figura 1 classificação de imagem usando uma DNN com CNTK

O programa de demonstração cria uma rede neural padrão conosco de entrada 784 (um para cada pixel), duas camadas de processamento ocultos (cada um com 400 nós) e 10 nós de saída (um para cada dígito possíveis). O modelo é treinado usando 10.000 iterações. A perda (também conhecido como o erro de treinamento) diminui lentamente e a precisão de previsão aumenta lentamente, indicando treinamento está funcionando.

Após a conclusão do treinamento, a demonstração se aplica o modelo treinado para um conjunto de dados de teste de 100 itens. A precisão do modelo é 84.00%, portanto 84 das imagens de 100 teste foram classificados corretamente.

Este artigo pressupõe que você tiver habilidades com uma linguagem da família C de programação intermediárias ou melhores, mas não pressupõe que você saiba muito sobre CNTK ou redes neurais. A demonstração é codificada usando Python, mas mesmo se você não conhecer o Python, deverá ser capaz de acompanhar sem muita dificuldade. O código-fonte do programa de demonstração é apresentado por completo neste artigo. Os dados de dois arquivos usados estão disponíveis no download que acompanha este artigo.

Compreendendo os dados

O conjunto de dados MNIST completo consiste em 60.000 imagens para imagens de treinamento e 10.000 para teste. Um pouco incomum, o conjunto de treinamento é contido em dois arquivos, uma que contém todos os valores de pixel e outra que contém os valores de rótulo associado (0 a 9). As imagens de teste também estão contidas em dois arquivos.

Além disso, os arquivos de quatro origem são armazenados em um formato binário proprietário. Ao trabalhar com redes neurais profundas, obtendo os dados em um formulário utilizável é quase sempre difícil e demorado. Figura 2 mostra o conteúdo da primeira imagem de treinamento. O ponto principal é que cada imagem tem 784 pixels, e cada pixel é um valor entre 00h (0 decimal) e FFh (255 decimal).

Uma imagem MNIST
Figura 2 uma imagem MNIST

Antes de escrever o programa de demonstração, eu escrevi um programa utilitário para ler os arquivos de origem binários e gravar um subconjunto de seu conteúdo em arquivos de texto que podem ser facilmente consumidos por um objeto de leitor de CNTK. Arquivo mnist_train_1000_cntk.txt é semelhante a:

|digit 0 0 0 0 0 1 0 0 0 0 |pixels 0 .. 170 52 .. 0
|digit 0 1 0 0 0 0 0 0 0 0 |pixels 0 .. 254 66 .. 0
etc.

Obter os dados binários brutos do MNIST no formato CNTK não é trivial. O código-fonte para meu programa utilitário pode ser encontrado em: bit.ly/2ErcCbw.

Houver 1.000 linhas de dados e cada um representa uma imagem. As marcas "| dígito" e "| pixels" indicar o início do valor a ser previsto e os valores de previsão. O rótulo de dígito é one-hot codificados em que a posição do bit 1 indica o dígito. Portanto, no código anterior, as duas primeiras imagens representam "5" e "1". Cada linha de dados tem os valores de pixel 784, cada um deles está entre 0 e 255. Arquivo mnist_test_100_cntk.txt tem 100 imagens e usa o mesmo formato amigável para o CNTK.

Em problemas de rede neurais mais, você deseja normalizar os valores de previsão. Em vez de diretamente normalizar os valores de pixel nos arquivos de dados, o programa de demonstração normaliza os dados em tempo real, como você verá em breve.

O programa de demonstração

O programa de demonstração completo, com algumas pequenas edições para economizar espaço, é apresentado na Figura 3. Todas as verificações de erros normais foram removidas. Eu recuo dois caracteres de espaço em vez dos quatro usuais para economizar espaço. Observe que o caractere "\" é usado pelo Python para continuação de linha.

Figura 3 de demonstração completo listagem de programas

# mnist_dnn.py
# MNIST using a 2-hidden layer DNN (not a CNN)
# Anaconda 4.1.1 (Python 3.5.2), CNTK 2.4
import numpy as np
import cntk as C
def create_reader(path, input_dim, output_dim, rnd_order, m_swps):
  x_strm = C.io.StreamDef(field='pixels', shape=input_dim,
    is_sparse=False)
  y_strm = C.io.StreamDef(field='digit', shape=output_dim,
    is_sparse=False)
  streams = C.io.StreamDefs(x_src=x_strm, y_src=y_strm)
  deserial = C.io.CTFDeserializer(path, streams)
  mb_src = C.io.MinibatchSource(deserial, randomize=rnd_order,
    max_sweeps=m_swps)
  return mb_src
# ===================================================================
def main():
  print("\nBegin MNIST classification using a DNN \n")
  train_file = ".\\Data\\mnist_train_1000_cntk.txt"
  test_file  = ".\\Data\\mnist_test_100_cntk.txt"
  C.cntk_py.set_fixed_random_seed(1)
  input_dim = 784  # 28 x 28 pixels
  hidden_dim = 400
  output_dim = 10  # 0 to 9
  X = C.ops.input_variable(input_dim, dtype=np.float32)
  Y = C.ops.input_variable(output_dim)  # float32 is default
  print("Creating a 784-(400-400)-10 ReLU classifier")
  with C.layers.default_options(init=\
    C.initializer.uniform(scale=0.01)):
    h_layer1 = C.layers.Dense(hidden_dim, activation=C.ops.relu,
      name='hidLayer1')(X/255) 
    h_layer2 = C.layers.Dense(hidden_dim, activation=C.ops.relu,
      name='hidLayer2')(h_layer1)
    o_layer = C.layers.Dense(output_dim, activation=None,
      name='outLayer')(h_layer2)
  dnn = o_layer               # train this
  model = C.ops.softmax(dnn)  # use for prediction
  tr_loss = C.cross_entropy_with_softmax(dnn, Y)
  tr_eror = C.classification_error(dnn, Y)
  max_iter = 10000   # num batches, not epochs
  batch_size = 50   
  learn_rate = 0.01
  learner = C.sgd(dnn.parameters, learn_rate)
  trainer = C.Trainer(dnn, (tr_loss, tr_eror), [learner]) 
  # 3. create reader for train data
  rdr = create_reader(train_file, input_dim, output_dim,
    rnd_order=True, m_swps=C.io.INFINITELY_REPEAT)
  mnist_input_map = {
    X : rdr.streams.x_src,
    Y : rdr.streams.y_src
  } 
  # 4. train
  print("\nStarting training \n")
  for i in range(0, max_iter):
    curr_batch = rdr.next_minibatch(batch_size, \
      input_map=mnist_input_map)
    trainer.train_minibatch(curr_batch)
    if i % int(max_iter/10) == 0:
      mcee = trainer.previous_minibatch_loss_average
      macc = (1.0 - trainer.previous_minibatch_evaluation_average) \
        * 100
      print("batch %4d: mean loss = %0.4f, accuracy = %0.2f%% " \
        % (i, mcee, macc))
  print("\nTraining complete \n")
  # 5. evaluate model on test data
  rdr = create_reader(test_file, input_dim, output_dim,
    rnd_order=False, m_swps=1)
  mnist_input_map = {
    X : rdr.streams.x_src,
    Y : rdr.streams.y_src
  }
  num_test = 100
  test_mb = rdr.next_minibatch(num_test, input_map=mnist_input_map)
  test_acc = (1.0 - trainer.test_minibatch(test_mb)) * 100
  print("Model accuracy on the %d test items = %0.2f%%" \
    % (num_test,test_acc)) 
  print("\nEnd MNIST classification using a DNN \n")
if __name__ == "__main__":
  main()

A demonstração de mnist_dnn.py tem uma função auxiliar, create_reader. Toda a lógica de controle é a única função main. Como o CNTK é jovem e em desenvolvimento contínuo, é uma boa ideia adicionar um comentário detalhando qual versão está sendo usada (2.4 neste caso).

Instalar o CNTK pode ser um pouco complicada, se você for novo no mundo do Python. Primeiro, você instala uma distribuição Anaconda do Python, que contém o intérprete Python necessário, os pacotes necessários, como NumPy e SciPy e utilitários úteis, como o pip. Eu usei a Anaconda3 4.1.1 de 64 bits, que inclui o Python 3.5. Depois de instalar o Anaconda, você instala o CNTK como um pacote do Python, não é um sistema autônomo, usando o utilitário pip. De um shell comum, o comando que usei foi:

>pip install https://cntk.ai/PythonWheel/CPU-Only/cntk-2.4-cp35-cp35m-win_amd64.whl

Observe o "cp35" no arquivo de roda que indica o arquivo é para uso com o Python 3.5. Tenha cuidado; quase todas as falhas de instalação do CNTK que já vi foram devido a incompatibilidades de versão do Anaconda CNTK.

A assinatura da função leitor é create_reader (caminho, input_dim, output_dim, rnd_order, m_swps). O parâmetro de caminho aponta para um arquivo de treinamento ou teste que está no formato CNTK. O parâmetro rnd_order é um sinalizador booliano que será definido como True para os dados de treinamento, porque você deseja processar dados de treinamento em ordem aleatória para evitar oscilando para parar sem fazer progresso do treinamento. O parâmetro será definido como False durante a leitura de dados de teste para avaliar a precisão do modelo porque a ordem não é importante, em seguida. O parâmetro m_swps ("máximo sujo") será definido para a constante INFINITELY_REPEAT para dados de treinamento (de modo que ela possa ser processada várias vezes) e definido como 1 para a avaliação de dados de teste.

Criando o modelo

A demonstração prepara uma rede neural profunda com:

train_file = ".\\Data\\mnist_train_1000_cntk.txt"
test_file  = ".\\Data\\mnist_test_100_cntk.txt"
C.cntk_py.set_fixed_random_seed(1)
input_dim = 784
hidden_dim = 400
output_dim = 10
X = C.ops.input_variable(input_dim, dtype=np.float32)
Y = C.ops.input_variable(output_dim)  # 32 is default

Normalmente, é uma boa ideia definir explicitamente a propagação de número aleatório global CNTK, para que os resultados serão reproduzíveis. O número de nós de entrada e saída é determinado por seus dados , mas o número de nós de processamento ocultos é um parâmetro livre e deve ser determinado através de tentativa e erro. Usando variáveis de 32 bits é o padrão para CNTK e é incorridos normal para redes neurais, porque a precisão obtida pelo uso de 64 bits não vale a pena a penalidade de desempenho.

A rede é criada assim:

with C.layers.default_options(init=
  C.initializer.uniform(scale=0.01)):
  h_layer1 = C.layers.Dense(hidden_dim,
    activation=C.ops.relu, name='hidLayer1')(X/255) 
  h_layer2 = C.layers.Dense(hidden_dim,
  activation=C.ops.relu, name='hidLayer2')(h_layer1)
  o_layer = C.layers.Dense(output_dim, activation=None,
    name='outLayer')(h_layer2)
dnn = o_layer               # train this
model = C.ops.softmax(dnn)  # use for prediction

O Python com instrução é um atalho sintático para aplicar um conjunto de argumentos comuns a várias funções. Aqui, ele é usado para inicializar todos os pesos de rede para valores aleatórios entre-0.01 e +0.01. O objeto X contém os valores de entrada 784 para uma imagem. Observe que cada valor é normalizada, dividindo por 255, portanto, os valores de entrada reais será no intervalo [0,0, 1,0].

 O ato de valores de entrada normalizados como entrada para a primeira camada oculta. As saídas de primeira camada oculta atuam como entradas para a segunda camada oculta. Em seguida, as saídas da segunda camada oculta são enviadas para a camada de saída. As duas camadas ocultas usarem ativação ReLU (unidades lineares retificadas), que, para classificação de imagem, geralmente funciona melhor do que a ativação tanh padrão.

Observe que não há ativação aplicada aos nós de saída. Essa é uma peculiaridade do CNTK, já que a função de treinamento do CNTK espera valores brutos e desativados. O objeto de dnn é apenas um alias de conveniência. O objeto model tem a ativação softmax para poder ser usado após o treinamento para fazer previsões. Como o Python atribui por referência, o objeto de dnn de treinamento também treina que o objeto de modelo.

Treinando a rede neural

A rede neural é preparada para treinamento com:

tr_loss = C.cross_entropy_with_softmax(dnn, Y)
tr_eror = C.classification_error(dnn, Y)
max_iter = 10000 
batch_size = 50   
learn_rate = 0.01
learner = C.sgd(dnn.parameters, learn_rate)
trainer = C.Trainer(dnn, (tr_loss, tr_eror), [learner])

O objeto de perda (tr_loss) de treinamento informa ao CNTK como medir erro durante o treinamento. Geralmente, o erro de entropia cruzada é a melhor opção para problemas de classificação. O objeto de erro (tr_eror) de classificação de treinamento pode ser usado para calcular automaticamente a porcentagem de previsões incorretas durante o treinamento ou após o treinamento. É necessário especificar uma função de perda, mas a especificação de uma função de erro de classificação é opcional.

Os valores para o número máximo de iterações de treinamento, o número de itens em um lote para treinar uma vez e a taxa de aprendizagem é gratuitos todos os parâmetros que devem ser determinados por tentativa e erro. Você pode pensar no objeto learner como um algoritmo e o objeto treinar como o objeto que usa o learner para encontrar bons valores para os valores de desvios e pesos da rede neural. O aprendiz descendente do gradiente estocástico (sgd) é o algoritmo mais primitivo, mas funciona bem para problemas simples. Alternativas incluem a estimativa de momento adaptável (adam) e a propagação de raiz quadrada média (rmsprop).

Um objeto do leitor para os dados de treinamento é criado com estas instruções:

rdr = create_reader(train_file, input_dim, output_dim,
  rnd_order=True, m_swps=C.io.INFINITELY_REPEAT)
mnist_input_map = {
  X : rdr.streams.x_src,
  Y : rdr.streams.y_src
}

Se você examinar o código create_reader Figura 3, você verá que ela especifica os nomes de marca ("pixels" e "dígitos") usados no arquivo de dados. Você pode considerar create_reader e o código para criar um objeto reader como código clichê para problemas de classificação de imagem DNN. Tudo o que você precisa alterar é os nomes de marca e o nome do dicionário de mapeamento (mnist_input_map).

Depois de tudo preparado, o treinamento é realizado, conforme mostrado na Figura 4.

Figura 4 treinamento

print("\nStarting training \n")
for i in range(0, max_iter):
  curr_batch = rdr.next_minibatch(batch_size, \
    input_map=mnist_input_map)
  trainer.train_minibatch(curr_batch)
  if i % int(max_iter/10) == 0:
    mcee = trainer.previous_minibatch_loss_average
    macc = (1.0 - \
      trainer.previous_minibatch_evaluation_average) \
        * 100
    print("batch %4d: mean loss = %0.4f, accuracy = \
      %0.2f%% " % (i, mcee, macc))

O programa de demonstração é projetado para que cada iteração processa um lote de itens de treinamento. Muitas bibliotecas de rede neural usam o termo "epoch" para se referir a uma passagem por todos os itens de treinamento. Neste exemplo, porque há 1.000 itens de treinamento e o tamanho do lote é definido como 50, uma época seria 20 iterações.

Uma alternativa ao treinamento com um número fixo de iterações é interromper o treinamento quando a perda/erro cair abaixo de determinado limite. É importante exibir a perda/erro durante o treinamento porque a falha de treinamento é a regra, em vez da exceção. Erro de entropia cruzada é difícil de interpretar diretamente, mas você deseja ver valores que tendem a ficar menores. Em vez de exibir o erro de média de classificação ("25 por cento errado"), a demonstração calcula e imprime a precisão de classificação média ("75 por cento correto"), que é uma métrica mais natural em minha opinião.

Avaliação e uso do modelo

Depois que um classificador de imagem tiver sido treinado, você geralmente deseja avaliar o modelo treinado nos dados de teste que tiverem sido validados. A demonstração calcula a precisão da classificação conforme mostrado na Figura 5.

Figura 5 computação precisão de classificação

rdr = create_reader(test_file, input_dim, output_dim,
  rnd_order=False, m_swps=1)
mnist_input_map = {
  X : rdr.streams.x_src,
  Y : rdr.streams.y_src
}
num_test = 100
test_mb = rdr.next_minibatch(num_test,
  input_map=mnist_input_map)
test_acc = (1.0 - trainer.test_minibatch(test_mb)) * 100
print("Model accuracy on the %d test items = %0.2f%%" \
  % (num_test,test_acc)))

Um novo leitor (reader) de dados é criado. Observe que, ao contrário do reader usado para treinamento, o novo reader não atravessa os dados em ordem aleatória, e o número de limpezas é definido como 1. O objeto de dicionário mnist_input_map é recriado. Um erro comum é tentar usar o leitor original — mas o objeto rdr foi alterado, portanto, você precisará recriar o mapeamento. A função test_minibatch retorna o erro de média de classificação para seu argumento mini-batch, que nesse caso é o conjunto de teste de 100 itens todo.

Após o treinamento, ou durante o treinamento, normalmente você desejará salvar o modelo. No CNTK, salvando pareceria com:

mdl_name = ".\\Models\\mnist_dnn.model"
model.save(mdl_name)

Isso salvará usando o formato do padrão CNTK v2. Uma alternativa é usar o formato do Open Neural Network Exchange (ONNX). Observe que você geralmente desejará salvar o objeto de modelo (com a ativação softmax) em vez do objeto de dnn (ativação de saída). De um programa diferente, um modelo salvo poderia ser carregado na memória junto com as linhas de:

mdl_name = ".\\Models\\mnist_dnn.model"
model = C.ops.functions.Function.load(mdl_name)

Após o carregamento, o modelo poderá ser usado como se tivesse acabado de ser treinado. O programa de demonstração não usa o modelo treinado para fazer uma previsão. Código de previsão pode ter esta aparência:

input_list = [0.55] * 784  # [0.55, 0.55, . . 0.55]
input_vec = np.array(input_list, dtype=np.float32)
pred_probs = model.eval(input_vec)
pred_digit = np.argmax(pred_probs)
print(pred_digit)

O input_list tem uma entrada fictícia de valores de pixel 784, cada um com valor 0,55 (Lembre-se o modelo foi treinado nos dados normalizados, portanto, você deverá preencher os dados normalizados). Os valores de pixel são copiados para uma matriz NumPy. A chamada para a função eval retornaria uma matriz de 10 valores que somam 1,0 e pode ser vagamente interpretada como probabilidades. A função argmax retorna o índice (0 a 9) do maior valor convenientemente é o mesmo que o previsto dígito. Legal!

Conclusão

Usar uma rede neural profunda costumava ser a abordagem mais comum para classificação de imagens simples. No entanto, as DNNs tem pelo menos duas limitações importantes. Em primeiro lugar, as DNNs não escalam bem a imagens que tenham um grande número de pixels. Em segundo lugar, as DNNs não explicitamente levam em conta a geometria de pixels da imagem. Por exemplo, em uma imagem MNIST, um pixel que está diretamente abaixo de um segundo pixel é 28 posições longe primeiro pixel no arquivo de entrada.

Devido a essas limitações e por outros motivos, também, o uso de uma rede neural convolucional (CNN) agora é mais comum para a classificação de imagem. Dito isso, para classificação de imagem simples, tarefas, usando uma DNN é mais fácil e geralmente apenas como (ou mais) eficiente do que usando uma CNN.


Dr. James McCaffreytrabalha para a Microsoft Research em Redmond, Washington. Ele trabalhou em vários produtos da Microsoft, incluindo Internet Explorer e Bing. Entre em contato com o Dr. McCaffrey pelo email jamccaff@microsoft.com.

Agradecemos aos seguintes especialistas técnicos da Microsoft pela revisão deste artigo: Chris Lee, Ricky Loynd, Kenneth Tran


Discuta esse artigo no fórum do MSDN Magazine