Compartilhar via


Reconhecimento prático de imagens do Labs

Observe que este tutorial requer a versão mestra mais recente ou o próximo CNTK 1.7 que será lançado em breve. Um download binário intermediário pode ser encontrado nas instruções do KDD CNTK Hands-On Tutorial para o qual este tutorial foi originalmente projetado.

Hands-On Lab: reconhecimento de imagem com redes convolucionais, normalização em lote e redes residuais

Este laboratório prático mostra como implementar o reconhecimento de imagem baseado em convolução com CNTK. Começaremos com uma arquitetura de reconhecimento de imagem convolucional comum, adicionaremos a Normalização do Lote e, em seguida, a estenderemos para uma Rede Residual (ResNet-20).

As técnicas que você praticará incluem:

  • modificando uma definição de rede CNTK para adicionar uma operação predefinida (dropout)
  • criando funções definidas pelo usuário para extrair partes repetitivas em uma rede em um módulo reutilizável
  • implementando uma estrutura de rede personalizada (conexão de ignorar ResNet)
  • criando muitas camadas ao mesmo tempo usando loops recursivos
  • treinamento paralelo
  • Redes convolucionais
  • normalização do lote

Pré-requisitos

Presumimos que você já instalou CNTK e pode executar o comando CNTK. Este tutorial foi realizado no KDD 2016 e requer um build recente, consulte aqui para obter instruções de instalação. Basta seguir as instruções para baixar um pacote de instalação binária dessa página. Para tarefas relacionadas à imagem, você deve fazer isso em um computador com uma GPU compatível com CUDA compatível com CUDA.

Em seguida, baixe um arquivo ZIP (cerca de 12 MB): clique neste link e, em seguida, no botão Baixar. O arquivo contém os arquivos deste tutorial. Aguarde o arquivo e defina o diretório de trabalho como ImageHandsOn. Você trabalhará com os seguintes arquivos:

Por fim, devemos baixar e converter o conjunto de dados CIFAR-10. A etapa de conversão levará cerca de 10 minutos. Execute os dois scripts python a seguir que você também encontrará no diretório de trabalho:

wget -rc http://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz
tar xvf www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz
python CifarConverter.py cifar-10-batches-py

Isso converte as imagens em arquivos PNG, 50000 para treinamento e 10000 para teste, que serão colocados nos dois diretórios a seguir, respectivamente: cifar-10-batches-py/data/train e cifar-10-batches-py/data/test.

Estrutura do Modelo

Iniciaremos este tutorial com um modelo convolucional simples. Ele consiste em 3 camadas de 5x5 convoluções + não linearidade + redução de dimensão 2x por pool máximo 3x3, que são seguidas por uma camada oculta densa e uma transformação densa para formar a entrada para um classificador softmax de 10 vias.

Ou, como uma descrição de rede CNTK. Dê uma olhada rápida e combine-a com a descrição acima:

featNorm = features - Constant (128)
l1 = ConvolutionalLayer {32, (5:5), pad = true, activation = ReLU,
                         init = "gaussian", initValueScale = 0.0043} (featNorm)
p1 = MaxPoolingLayer {(3:3), stride = (2:2)} (l1)
l2 = ConvolutionalLayer {32, (5:5), pad = true, activation = ReLU,
                         init = "gaussian", initValueScale = 1.414} (p1)
p2 = MaxPoolingLayer {(3:3), stride = (2:2)} (l2)
l3 = ConvolutionalLayer {64, (5:5), pad = true, activation = ReLU,
                         init = "gaussian", initValueScale = 1.414} (p2)
p3 = MaxPoolingLayer {(3:3), stride = (2:2)} (l3)
d1 = DenseLayer {64, activation = ReLU, init = "gaussian", initValueScale = 12} (p3)
z  = LinearLayer {10, init = "gaussian", initValueScale = 1.5} (d1)

Você pode encontrar mais informações sobre esses operadores aqui: ConvolutionalLayer{}, , MaxPoolingLayer{}, DenseLayer{}. LinearLayer{}

Configuração do CNTK

Arquivo de configuração

Para treinar e testar um modelo no CNTK, precisamos fornecer um arquivo de configuração que informe CNTK quais operações você deseja executar (commandvariável) e uma seção de parâmetro para cada comando.

Para o comando de treinamento, CNTK precisa ser informado:

  • como ler os dados (reader seção)
  • a função de modelo e suas entradas e saídas no grafo de computação (BrainScriptNetworkBuilder seção)
  • hipermetrâmetros para o aprendiz (SGD seção)

Para o comando de avaliação, CNTK precisa saber:

  • como ler os dados de teste (reader seção)
  • quais métricas avaliar (evalNodeNames parâmetro)

Veja a seguir o arquivo de configuração com o qual começaremos. Como você vê, um arquivo de configuração CNTK é um arquivo de texto que consiste em definições de parâmetros, que são organizados em uma hierarquia de registros. Você também pode ver como CNTK dá suporte à substituição de parâmetro básica usando a $parameterName$ sintaxe. O arquivo real contém apenas mais alguns parâmetros do que mencionado acima, mas verifique-o e localize os itens de configuração mencionados:

# CNTK Configuration File for training a simple CIFAR-10 convnet.
# During the hands-on tutorial, this will be fleshed out into a ResNet-20 model.

command = TrainConvNet:Eval

makeMode = false ; traceLevel = 0 ; deviceId = "auto"

rootDir = "." ; dataDir  = "$rootDir$" ; modelDir = "$rootDir$/Models"

modelPath = "$modelDir$/cifar10.cmf"

# Training action for a convolutional network
TrainConvNet = {
    action = "train"

    BrainScriptNetworkBuilder = {
        imageShape = 32:32:3
        labelDim = 10

        model (features) = {
            featNorm = features - Constant (128)
            l1 = ConvolutionalLayer {32, (5:5), pad=true, activation=ReLU,
                                     init="gaussian", initValueScale=0.0043} (featNorm)
            p1 = MaxPoolingLayer {(3:3), stride=(2:2)} (l1)
            l2 = ConvolutionalLayer {32, (5:5), pad=true, activation=ReLU,
                                     init="gaussian", initValueScale=1.414} (p1)
            p2 = MaxPoolingLayer {(3:3), stride=(2:2)} (l2)
            l3 = ConvolutionalLayer {64, (5:5), pad=true, activation=ReLU,
                                     init="gaussian", initValueScale=1.414} (p2)
            p3 = MaxPoolingLayer {(3:3), stride=(2:2)} (l3)
            d1 = DenseLayer {64, activation=ReLU, init="gaussian", initValueScale=12} (p3)
            z  = LinearLayer {10, init="gaussian", initValueScale=1.5} (d1)
        }.z

        # inputs
        features = Input {imageShape}
        labels   = Input {labelDim}

        # apply model to features
        z = model (features)

        # connect to system
        ce       = CrossEntropyWithSoftmax (labels, z)
        errs     = ErrorPrediction         (labels, z)

        featureNodes    = (features)
        labelNodes      = (labels)
        criterionNodes  = (ce)
        evaluationNodes = (errs)
        outputNodes     = (z)
    }

    SGD = {
        epochSize = 50000

        maxEpochs = 30 ; minibatchSize = 64
        learningRatesPerSample = 0.00015625*10:0.000046875*10:0.000015625
        momentumAsTimeConstant = 600*20:6400
        L2RegWeight = 0.03

        firstMBsToShowResult = 10 ; numMBsToShowResult = 100
    }

    reader = {
        verbosity = 0 ; randomize = true
        deserializers = ({
            type = "ImageDeserializer" ; module = "ImageReader"
            file = "$dataDir$/cifar-10-batches-py/train_map.txt"
            input = {
                features = { transforms = (
                    { type = "Crop" ; cropType = "RandomSide" ; sideRatio = 0.8 ; jitterType = "UniRatio" } :
                    { type = "Scale" ; width = 32 ; height = 32 ; channels = 3 ; interpolations = "linear" } :
                    { type = "Transpose" }
                )}
                labels = { labelDim = 10 }
            }
        })
    }
}

# Eval action
Eval = {
    action = "eval"
    minibatchSize = 16
    evalNodeNames = errs
    reader = {
        verbosity = 0 ; randomize = true
        deserializers = ({
            type = "ImageDeserializer" ; module = "ImageReader"
            file = "$dataDir$/cifar-10-batches-py/test_map.txt"
            input = {
                features = { transforms = (
                   { type = "Scale" ; width = 32 ; height = 32 ; channels = 3 ; interpolations = "linear" } :
                   { type = "Transpose" }
                )}
                labels = { labelDim = 10 }
            }
        })
    }
}

Leitura de dados e dados

Depois de baixar os dados cifar-10 e executar o CifarConverter.py script conforme solicitado no início deste tutorial, você encontrará um diretório chamado cifar-10-batches-py/data, que contém dois subdiretórios train e test, cheio de arquivos PNG. O CNTK ImageDeserializer consome formatos de imagem padrão.

Você também encontrará dois arquivos train_map.txt e test_map.txt. Olhando para o último,

% more cifar-10-batches-py/test_map.txt
cifar-10-batches-py/data/test/00000.png 3
cifar-10-batches-py/data/test/00001.png 8
cifar-10-batches-py/data/test/00002.png 8
...

Ambos os arquivos consistem em duas colunas, em que a primeira contém o caminho para o arquivo de imagem e a segunda o rótulo de classe, como um índice numérico. Essas colunas correspondem às entradas features do leitor e labels que foram definidas como:

 features = { transforms = (
     { type = "Crop" ; cropType = "RandomSide" ; sideRatio = 0.8 ; jitterType = "UniRatio" } :
     { type = "Scale" ; width = 32 ; height = 32 ; channels = 3 ; interpolations = "linear" } :
     { type = "Transpose" }
 )}
 labels = { labelDim = 10 }

A seção adicional transforms informa para ImageDeserializer aplicar uma sequência de transformações (comuns) às imagens conforme elas estão sendo lidas. Para obter mais informações, confira aqui.

Executando-o

Você pode encontrar o arquivo de configuração acima sob o nome ImageHandsOn.cntk na pasta de trabalho. Para executá-lo, execute a configuração acima por este comando:

cntk  configFile=ImageHandsOn.cntk

Sua tela ganhará vida com uma enxurrada de mensagens de log (CNTK pode ser falante às vezes), mas se tudo der certo, você verá isso em breve:

Training 116906 parameters in 10 out of 10 parameter tensors and 28 nodes with gradient

seguido por uma saída como esta:

Finished Epoch[ 1 of 10]: [Training] ce = 1.66950797 * 50000; errs = 61.228% * 50000
Finished Epoch[ 2 of 10]: [Training] ce = 1.32699016 * 50000; errs = 47.394% * 50000
Finished Epoch[ 3 of 10]: [Training] ce = 1.17140398 * 50000; errs = 41.168% * 50000

Isso informa que ele está aprendendo. Cada época representa uma passagem pelas 50000 imagens de treinamento. Ele também informa que após a segunda época, o critério de treinamento, que a configuração nomeou ce, atingiu 1,33 conforme medido nos 5.0000 exemplos dessa época, e que a taxa de erro é de 47% nesses mesmos 50000 amostras de treinamento.

Observe que os computadores somente CPU são cerca de 20 vezes mais lentos. Levará muitos minutos até você ver a primeira saída de log. Para verificar se o sistema está progredindo, você pode habilitar o rastreamento para ver resultados parciais, que devem aparecer razoavelmente rapidamente:

cntk  configFile=ImageHandsOn.cntk  traceLevel=1

Epoch[ 1 of 10]-Minibatch[-498-   1, 0.13%]: ce = 2.30260658 * 64; errs = 90.625% * 64
...
Epoch[ 1 of 10]-Minibatch[   1- 100, 12.80%]: ce = 2.10434176 * 5760; errs = 78.472% * 5760
Epoch[ 1 of 10]-Minibatch[ 101- 200, 25.60%]: ce = 1.82372971 * 6400; errs = 68.172% * 6400
Epoch[ 1 of 10]-Minibatch[ 201- 300, 38.40%]: ce = 1.69708496 * 6400; errs = 62.469% * 6400

Quando o treinamento for concluído (o que leva cerca de 3 minutos em um Surface Book e em um computador desktop com uma GPU Titan-X), a mensagem final será semelhante a esta:

Finished Epoch[10 of 10]: [Training] ce = 0.74679766 * 50000; errs = 25.486% * 50000

o que mostra que a rede reduziu com êxito a ce perda e atingiu um erro de classificação de 25,5% no conjunto de treinamento. Como nossa command variável especifica um segundo comandoEval, CNTK prosseguirá com essa ação. Ele mede a taxa de erro de classificação nas 10.000 imagens do conjunto de testes.

Final Results: Minibatch[1-625]: errs = 24.180% * 10000

A taxa de erro de teste está próxima do treinamento. Como CIFAR-10 é um conjunto de dados bastante pequeno, esse é um indicador de que nosso modelo ainda não convergiu totalmente (e, de fato, executá-lo por 30 épocas levará você a cerca de 20%).

Se você não quiser esperar até que isso seja concluído, poderá executar um modelo intermediário, por exemplo.

cntk  configFile=ImageHandsOn.cntk  command=Eval  modelPath=Models/cifar10.cmf.5
Final Results: Minibatch[1-625]: errs = 31.710% * 10000

ou execute nosso modelo pré-treinado também:

cntk  configFile=ImageHandsOn.cntk  command=Eval  modelPath=cifar10.pretrained.cmf
Final Results: Minibatch[1-625]: errs = 24.180% * 10000

Modificando o modelo

No seguinte, você receberá tarefas para praticar a modificação CNTK configurações. As soluções são fornecidas no final deste documento... mas tente sem!

Tarefa 1: Adicionar Dropout

Uma técnica comum para melhorar a generalizabilidade dos modelos é o abandono. Para adicionar o dropout a um modelo CNTK, você precisa

  • adicionar uma chamada à função Dropout() CNTK em que você deseja inserir a operação de dropout
  • adicionar um parâmetro dropoutRate à SGD seção chamada para definir a probabilidade de abandono

Nesta tarefa específica, não especifique nenhum abandono para a primeira época, seguido por uma taxa de abandono de 50%. Dê uma olhada na Dropout() documentação para ver como fazer isso.

Se tudo correu bem, você não observará nenhuma alteração para a primeira época, mas uma melhoria muito menor de ce uma vez dropout começa com a segunda época. Isso é esperado. (Para essa configuração específica, a precisão do reconhecimento não melhora, na verdade.) O resultado final quando o treinamento de apenas 10 épocas é de cerca de 32%. 10 épocas não são suficientes para Dropout.

Confira a solução aqui.

Tarefa 2: simplificar a definição de modelo extraindo partes repetitivas em uma função

No exemplo, a sequência (Pooling de ReLU >> de Convolução>>) é repetida três vezes. Sua tarefa é escrever uma função BrainScript que agrupa essas três operações em um módulo reutilizável. Observe que todos os três usos dessa sequência usam parâmetros diferentes (dimensões de saída, peso de inicialização). Portanto, a função que você escreve deve tomar esses dois como parâmetros, além dos dados de entrada. Por exemplo, ele pode ser definido como

MyLayer (x, depth, initValueScale)

Ao executar isso, você esperaria que os valores e errs os valores resultantes ce fossem os mesmos. No entanto, se você executar na GPU, o não determinismo na implementação de propagação traseira da cuDNN causará pequenas variações.

Confira a solução aqui.

Tarefa 3: Adicionando BatchNormalization

(Esta Tarefa requer uma GPU, uma vez que a implementação da normalização em lotes do CNTK é baseada em cuDNN.)

A normalização do lote é uma técnica popular para acelerar e melhorar a convergência. Em CNTK, a normalização em lote é implementada como BatchNormalizationLayer{}.

A forma espacial (em que todas as posições de pixel são normalizadas com parâmetros compartilhados) é invocada por um parâmetro opcional: BatchNormalizationLayer{spatialRank=2}.

Adicione normalização em lote a todas as três camadas de convolução e entre as duas camadas densas. Observe que a normalização em lote deve ser inserida antes da não linearidade. Portanto, você deve remover o activation parâmetro e, em vez disso, inserir chamadas explícitas à função ReLU()CNTK.

Além disso, a normalização em lote altera a velocidade de convergência. Portanto, vamos aumentar as taxas de aprendizado para as primeiras 7 épocas três vezes e desabilitar o momento e a regularização L2 definindo seus parâmetros como 0.

Ao executar, você verá parâmetros de aprendizado adicionais listados no log de treinamento. O resultado final será em torno de 28%, que é 4 pontos melhor do que sem normalização em lote após o mesmo número de iterações. A convergência acelera de fato.

Confira a solução aqui.

Tarefa 4: Converter em Rede Residual

A configuração acima é um exemplo de "toy" para sujar as mãos com a execução e a modificação CNTK configurações, e intencionalmente não executamos a convergência total para manter os tempos de reviravolta baixos. Portanto, vamos agora avançar para uma configuração mais real: uma Rede Residual. A Rede Residual (https://arxiv.org/pdf/1512.03385v1.pdf) é uma estrutura de rede profunda modificada em que as camadas, em vez de aprender um mapeamento da entrada para a saída, aprendem um termo de correção.

(Essa Tarefa também requer uma GPU para a operação de normalização em lote, embora se você tiver muito tempo, poderá tentar executá-la em uma CPU editando a normalização do lote de chamadas, com alguma perda de precisão.)

Para começar, modifique a configuração anterior. Primeiro, substitua a função model(features) de modelo por esta:

        MySubSampleBN (x, depth, stride) =
        {
            s = Splice ((MaxPoolingLayer {(1:1), stride = (stride:stride)} (x) : ConstantTensor (0, (1:1:depth/stride))), axis = 3)  # sub-sample and pad: [W x H x depth/2] --> [W/2 x H/2 x depth]
            b = BatchNormalizationLayer {spatialRank = 2, normalizationTimeConstant = 4096} (s)
        }.b
        MyConvBN (x, depth, initValueScale, stride) =
        {
            c = ConvolutionalLayer {depth, (3:3), pad = true, stride = (stride:stride), bias = false,
                                    init = "gaussian", initValueScale = initValueScale} (x)
            b = BatchNormalizationLayer {spatialRank = 2, normalizationTimeConstant = 4096} (c)
        }.b
        ResNetNode (x, depth) =
        {
            c1 = MyConvBN (x,  depth, 7.07, 1)
            r1 = ReLU (c1)
            c2 = MyConvBN (r1, depth, 7.07, 1)
            r  = ReLU (c2)
        }.r
        ResNetIncNode (x, depth) =
        {
            c1 = MyConvBN (x,  depth, 7.07, 2)  # note the 2
            r1 = ReLU (c1)
            c2 = MyConvBN (r1, depth, 7.07, 1)
            r  = ReLU (c2)
        }.r
        model (features) =
        {
            conv1 = ReLU (MyConvBN (features, 16, 0.26, 1))
            rn1   = ResNetNode (ResNetNode (ResNetNode (conv1, 16), 16), 16)

            rn2_1 = ResNetIncNode (rn1, 32)
            rn2   = ResNetNode (ResNetNode (rn2_1, 32), 32)

            rn3_1 = ResNetIncNode (rn2, 64)
            rn3   = ResNetNode (ResNetNode (rn3_1, 64), 64)

            pool = AveragePoolingLayer {(8:8)} (rn3)

            z = LinearLayer {labelDim, init = "gaussian", initValueScale = 0.4} (pool)
        }.z

e altere a configuração do SGD para:

SGD = {
    epochSize = 50000

    maxEpochs = 160 ; minibatchSize = 128
    learningRatesPerSample = 0.0078125*80:0.00078125*40:0.000078125
    momentumAsTimeConstant = 1200
    L2RegWeight = 0.0001

    firstMBsToShowResult = 10 ; numMBsToShowResult = 500
}

Sua tarefa é modificar ResNetNode() e ResNetNodeInc() , para que eles implementem a estrutura disposta na seguinte arte ASCII digna de prêmio:

            ResNetNode                   ResNetNodeInc

                |                              |
         +------+------+             +---------+----------+
         |             |             |                    |
         V             |             V                    V
    +----------+       |      +--------------+   +----------------+
    | Conv, BN |       |      | Conv x 2, BN |   | SubSample, BN  |
    +----------+       |      +--------------+   +----------------+
         |             |             |                    |
         V             |             V                    |
     +-------+         |         +-------+                |
     | ReLU  |         |         | ReLU  |                |
     +-------+         |         +-------+                |
         |             |             |                    |
         V             |             V                    |
    +----------+       |        +----------+              |
    | Conv, BN |       |        | Conv, BN |              |
    +----------+       |        +----------+              |
         |             |             |                    |
         |    +---+    |             |       +---+        |
         +--->| + |<---+             +------>+ + +<-------+
              +---+                          +---+
                |                              |
                V                              V
            +-------+                      +-------+
            | ReLU  |                      | ReLU  |
            +-------+                      +-------+
                |                              |
                V                              V

Confirme com a saída de validação no log que você fez isso corretamente.

Isso levará muito tempo para ser executado até a conclusão. A saída esperada será semelhante a esta:

Finished Epoch[ 1 of 160]: [Training] ce = 1.57037109 * 50000; errs = 58.940% * 50000
Finished Epoch[ 2 of 160]: [Training] ce = 1.06968234 * 50000; errs = 38.166% * 50000
Finished Epoch[ 3 of 160]: [Training] ce = 0.85858969 * 50000; errs = 30.316% * 50000

enquanto o modelo incorreto sem as conexões de ignorar é mais parecido com este:

Finished Epoch[ 1 of 160]: [Training] ce = 1.72901219 * 50000; errs = 66.232% * 50000
Finished Epoch[ 2 of 160]: [Training] ce = 1.30180430 * 50000; errs = 47.424% * 50000
Finished Epoch[ 3 of 160]: [Training] ce = 1.04641961 * 50000; errs = 37.568% * 50000

Confira a solução aqui.

Tarefa 5: Gerar automaticamente muitas camadas

Por fim, a ResNet com melhor desempenho tem 152 camadas. Como seria muito tedioso e um erro propenso a escrever 152 expressões individuais, agora modificaremos a definição para gerar automaticamente uma pilha de ResNetNode().

Sua tarefa é gravar uma função com esta assinatura:

ResNetNodeStack (x, depth, L)

onde L indica quantos ResNetNodes devem ser empilhados, para que possamos substituir a expressão acima por rn1 uma chamada parametrizada:

rn1   = ResNetNodeStack (conv1, 16, 3)  # 3 means 3 such nodes

e da mesma forma para rn2 e rn3. As ferramentas necessárias são a expressão condicional :

z = if cond then x else y

e recursão.

Este treinamento será executado por cerca de metade de um nosso em um Titan-X. Se você fez isso direito, no início do log conterá esta mensagem:

Training 200410 parameters in 51 out of 51 parameter tensors and 112 nodes with gradient:

Para referência, incluímos uma versão pré-treinada deste modelo. Você pode medir a taxa de erros com este comando:

cntk  configFile=ImageHandsOn.ResNet.cntk  command=Eval

e deverá ver um resultado como este:

Final Results: Minibatch[1-625]: errs = 8.400% * 10000; top5Errs = 0.290% * 10000

Essa taxa de erro é muito próxima à relatada no artigo original da ResNet (https://arxiv.org/pdf/1512.03385v1.pdfTabela 6).

Confira a solução aqui.

Tarefa 6: Treinamento paralelo

Por fim, se você tiver várias GPUs, CNTK permitirá que você paralelize o treinamento usando MPI (Interface de Passagem de Mensagem). Esse modelo é muito pequeno para esperar muita velocidade sem ajuste adicional, por exemplo, de tamanhos de minibatch (a configuração de tamanho de minibatch atual é muito pequena para obter a utilização completa dos núcleos de GPU disponíveis). No entanto, vamos passar pelos movimentos, para que você saiba como fazê-lo depois de passar para cargas de trabalho do mundo real.

Adicione a seguinte linha ao SGD bloco:

SGD = {
    ...
    parallelTrain = {
        parallelizationMethod = "DataParallelSGD"
        parallelizationStartEpoch = 2
        distributedMBReading = true
        dataParallelSGD = { gradientBits = 1 }
    }
}

e execute este comando:

mpiexec -np 4 cntk  configFile=ImageHandsOn.cntk  stderr=Models/log  parallelTrain=true

O que vem a seguir?

Este tutorial praticou a possibilidade de usar uma configuração existente e modificá-la de maneiras específicas:

  • adicionando uma operação predefinida (dropout)
  • extraindo partes repetitivas em módulos reutilizáveis (funções)
  • refatoração (para inserir a normalização em lote)
  • estruturas de rede personalizadas (conexão de ignorar ResNet)
  • parametrizando estruturas repetitivas usando a recursão

e vimos como acelerar o treinamento por paralelização.

Então, para onde vamos daqui? Talvez você já tenha descoberto que o padrão usado nesses exemplos, que chamamos de estilo de criação de grafo , pode ser bastante propenso a erros. Detectar o erro?

model (features) =
{
    l1 = ConvolutionalLayer {32, (5:5), pad = true, activation = ReLU,
                             init = "gaussian", initValueScale = 0.0043} (featNorm)
    p1 = MaxPoolingLayer {(3:3), stride = (2:2)} (l1)
    l2 = ConvolutionalLayer {64, (5:5), pad = true, activation = ReLU,
                             init = "gaussian", initValueScale = 1.414} (p1)
    p2 = MaxPoolingLayer {(3:3), stride = (2:2)} (l1)
    d1 = DenseLayer {64, activation = ReLU, init = "gaussian", initValueScale = 12} (p2)
    z  = LinearLayer {10, init = "gaussian", initValueScale = 1.5} (d1)
}.z

Uma maneira de evitar esse erro é usar a composição da função. Veja a seguir uma maneira alternativa e mais concisa de escrever o mesmo:

model = Sequential (
    ConvolutionalLayer {32, (5:5), pad = true, activation = ReLU,
                        init = "gaussian", initValueScale = 0.0043} :
    MaxPoolingLayer {(3:3), stride = (2:2)} :
    ConvolutionalLayer {64, (5:5), pad = true, activation = ReLU,
                        init = "gaussian", initValueScale = 1.414} :
    MaxPoolingLayer {(3:3), stride = (2:2)} :
    DenseLayer {64, activation = ReLU, init = "gaussian", initValueScale = 12} :
    LinearLayer {10, init = "gaussian", initValueScale = 1.5}
)

Esse estilo será introduzido e usado no próximo tutorial prático, Compreensão de Texto com Redes Recorrentes.

Soluções

Solução 1: adicionando dropout

Modifique a definição do modelo da seguinte maneira:

p3 = MaxPoolingLayer {(3:3), stride = (2:2)} (l3)
d1 = DenseLayer {64, activation = ReLU, init = "gaussian", initValueScale = 12} (p3)
d1_d = Dropout (d1)    ##### added
z  = LinearLayer {10, init = "gaussian", initValueScale = 1.5} (d1_d)  ##### d1 -> d1_d

e a seção SGD:

SGD = {
    ...
    dropoutRate = 0*5:0.5   ##### added
    ...
}

Solução 2: simplificar a definição de modelo extraindo partes repetitivas em uma função

Adicione uma definição de função da seguinte maneira:

MyLayer (x, depth, initValueScale) =
{
    c = ConvolutionalLayer {depth, (5:5), pad = true, activation = ReLU,
                            init = "gaussian", initValueScale = initValueScale} (x)
    p = MaxPoolingLayer {(3:3), stride = (2:2)} (c)
}.p

e atualizar a definição do modelo para usá-la

featNorm = features - Constant (128)
p1 = MyLayer (featNorm, 32, 0.0043)  ##### replaced
p2 = MyLayer (p1,       32, 1.414)   ##### replaced
p3 = MyLayer (p2,       64, 1.414)   ##### replaced
d1 = DenseLayer {64, activation = ReLU, init = "gaussian", initValueScale = 12} (p3)

Solução 3: Adicionando BatchNormalization

Modifique MyLayer():

MyLayer (x, depth, initValueScale) =
{
    c = ConvolutionalLayer {depth, (5:5), pad = true,  ##### no activation=ReLU
                            init = "gaussian", initValueScale = initValueScale} (x)
    b = BatchNormalizationLayer {spatialRank = 2} (c)
    r = ReLU (b)   ##### now called explicitly
    p = MaxPoolingLayer {(3:3), stride = (2:2)} (r)
}.p

e use-o. Insira também a normalização do lote antes z:

d1 = DenseLayer {64, init = "gaussian", initValueScale = 12} (p3)
d1_bnr = ReLU (BatchNormalizationLayer {} (d1))  ##### added BN and explicit ReLU
d1_d = Dropout (d1_bnr)                          ##### d1 -> d1_bnr
z  = LinearLayer {10, init = "gaussian", initValueScale = 1.5} (d1_d)

E atualize esses parâmetros na seção SGD:

SGD = {
    ....
    learningRatesPerSample = 0.00046875*7:0.00015625*10:0.000046875*10:0.000015625
    momentumAsTimeConstant = 0
    L2RegWeight = 0
    ...
}

Solução 4: Converter em Rede Residual

As implementações corretas são ResNetNode()ResNetNodeInc() :

    ResNetNode (x, depth) =
    {
        c1 = MyConvBN (x,  depth, 7.07, 1)
        r1 = ReLU (c1)
        c2 = MyConvBN (r1, depth, 7.07, 1)
        r  = ReLU (x + c2)   ##### added skip connection
    }.r
    ResNetIncNode (x, depth) =
    {
        c1 = MyConvBN (x,  depth, 7.07, 2)  # note the 2
        r1 = ReLU (c1)
        c2 = MyConvBN (r1, depth, 7.07, 1)

        xs = MySubSampleBN (x, depth, 2)

        r  = ReLU (xs + c2)   ##### added skip connection
    }.r

Solução 5: Gerando automaticamente muitas camadas

Esta é a implementação:

    ResNetNodeStack (x, depth, L) =
    {
        r = if L == 0
            then x
            else ResNetNode (ResNetNodeStack (x, depth, L-1), depth)
    }.r

ou, mais curto:

    ResNetNodeStack (x, depth, L) =
        if L == 0
        then x
        else ResNetNode (ResNetNodeStack (x, depth, L-1), depth)

Você também precisa modificar sua função de modelo:

        conv1 = ReLU (MyConvBN (features, 16, 0.26, 1))
        rn1   = ResNetNodeStack (conv1, 16, 3)  ##### replaced

        rn2_1 = ResNetIncNode (rn1, 32)
        rn2   = ResNetNodeStack (rn2_1, 32, 2)  ##### replaced

        rn3_1 = ResNetIncNode (rn2, 64)
        rn3   = ResNetNodeStack (rn3_1, 64, 2)  ##### replaced