Partilhar via


Reconhecimento de imagem prático dos laboratórios

Note que este tutorial requer a versão master mais recente, ou o próximo CNTK 1.7 que será lançado em breve. Um download binário intermédio pode ser encontrado nas instruções para o KDD CNTK Hands-On Tutorial para o quais este tutorial foi originalmente concebido para.

Hands-On Lab: Reconhecimento de imagem com redes convolucionais, normalização de lotes 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 comum de reconhecimento de imagem convolucional, adicionamos a Normalização do Lote e, em seguida, alargá-la-emos numa Rede Residual (ResNet-20).

As técnicas que vai praticar incluem:

  • modificando uma definição de rede CNTK para adicionar uma operação predefinida (abandono)
  • criando funções definidas pelo utilizador para extrair peças repetitivas numa rede num módulo reutilizável
  • implementação de estruturas de rede personalizadas (ligação de salto ResNet)
  • criando muitas camadas ao mesmo tempo usando laços recursivos
  • formação paralela
  • redes convolucional
  • normalização do lote

Pré-requisitos

Assumimos que já instalou CNTK e pode executar o comando CNTK. Este tutorial foi realizado na KDD 2016 e requer uma construção recente, por favor consulte aqui para instruções de configuração. Basta seguir as instruções para descarregar um pacote de instalação binário a partir dessa página. Para tarefas relacionadas com a imagem deve fazê-lo numa máquina com uma GPU compatível com CUDA capaz.

Em seguida, descarregue um arquivo ZIP (cerca de 12 MB): Clique neste link e, em seguida, no botão Descarregar. O arquivo contém os ficheiros deste tutorial. Por favor, o arquivo e desemote o seu diretório de trabalho para ImageHandsOn. Estará a trabalhar com os seguintes ficheiros:

Por último, temos de descarregar e converter o conjunto de dados CIFAR-10. O passo de conversão levará cerca de 10 minutos. Execute os seguintes dois scripts Python que 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

Isto converte as imagens em ficheiros PNG, 50000 para treino e 10000 para testes, que serão colocados nos dois diretórios seguintes, respectivamente: cifar-10-batches-py/data/train e cifar-10-batches-py/data/test.

Estrutura modelo

Vamos começar este tutorial com uma simples modelo convolucional. Consiste em 3 camadas de convoluções 5x5 + não linearidade + redução da dimensão 2x por 3x3 max-pooling, que são seguidas por uma camada escondida densa e uma transformação densa para formar a entrada para um classificador softmax de 10 vias.

Ou, como uma CNTK descrição da rede. Por favor, dê uma olhada rápida e combine 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)

Pode encontrar mais informações sobre estes operadores aqui: ConvolutionalLayer{}, MaxPoolingLayer{}, . DenseLayer{}. . LinearLayer{}

Configuração CNTK

Arquivo Config

Para treinar e testar uma modelo em CNTK, precisamos de fornecer um ficheiro de configuração que diga CNTK que operações pretende executar (command variável) e uma secção de parâmetros para cada comando.

Para o comando de treino, CNTK precisa de ser informado:

  • como ler os dados (reader secção)
  • a função modelo e as suas entradas e saídas no gráfico de cálculo (BrainScriptNetworkBuildersecção)
  • hiper-parâmetros para o aluno (SGD secção)

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

  • como ler os dados do teste (reader secção)
  • que métricas para avaliar (evalNodeNames parâmetro)

Segue-se o ficheiro de configuração com que começaremos. Como vê, um ficheiro de configuração CNTK é um ficheiro de texto constituído por definições de parâmetros, que são organizados numa hierarquia de registos. Também pode ver como CNTK suporta a substituição básica dos parâmetros utilizando a $parameterName$ sintaxe. O ficheiro real contém apenas mais alguns parâmetros do que mencionados acima, mas por favor digitalize-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 ter descarregado os dados do CIFAR-10 e executado o CifarConverter.py script como solicitado no início deste tutorial, encontrará um diretório chamado cifar-10-batches-py/data, que contém duas subdiretorias, train e testcheio de ficheiros PNG. O CNTK ImageDeserializer consome formatos de imagem padrão.

Também encontrará dois ficheiros train_map.txt e test_map.txt. Olhando para este ú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 ficheiros consistem em duas colunas, onde a primeira contém o caminho para o ficheiro de imagem, e a segunda a etiqueta de classe, como um índice numérico. Estas 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 secção adicional transforms diz para ImageDeserializer aplicar uma sequência de (comum) transforma-se nas imagens à medida que estão a ser lidas. Para mais informações, consulte aqui.

Executando-o

Pode encontrar o ficheiro de configuração acima no nome ImageHandsOn.cntk da pasta de trabalho. Para executá-lo, execute a configuração acima por este comando:

cntk  configFile=ImageHandsOn.cntk

O seu ecrã ganhará vida com uma onda de mensagens de registo (CNTK pode ser falador às vezes), mas se tudo correr bem, logo verá isto:

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

seguido por uma produção 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

Isto diz-te que está a aprender. Cada época representa uma passagem através das 50000 imagens de treino. Diz-lhe também que após a segunda época, o critério de formação, a que a configuração nomeou ce, atingiu 1,33 como medido nas 50000 amostras desta época, e que a taxa de erro é de 47% nas mesmas 50000 amostras de treino.

Note que as máquinas só com CPU são cerca de 20 vezes mais lentas. Levará muitos minutos até ver a primeira saída de log. Para garantir que o sistema está a progredir, pode permitir rastrear 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 treino estiver concluído (que leva cerca de 3 minutos numa Surface Book e numa máquina de 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 sucesso a ce perda, e atingiu um erro de classificação de 25,5% no conjunto de treino. Como a nossa command variável especifica um segundo comandoEval, CNTK seguirão com essa ação. Mede a taxa de erro de classificação nas imagens 10000 do conjunto de testes.

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

A taxa de erro de teste está próxima do treino. Uma vez que o CIFAR-10 é um conjunto de dados bastante pequeno, este é um indicador de que o nosso modelo ainda não convergiu totalmente (e, na verdade, executá-lo por 30 épocas vai levá-lo a cerca de 20%).

Se não quiser esperar até que isto termine, pode executar uma modelo intermédia, por exemplo.

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

ou executar a nossa modelo pré-treinada também:

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

Modificar o Modelo

No seguinte, serão-lhe dadas tarefas para praticar a modificação das configurações CNTK. As soluções são dadas no final deste documento... mas, por favor, tente sem!

Tarefa 1: Adicionar Abandono

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

  • adicionar uma chamada à função Dropout() CNTK onde pretende inserir a operação de abandono
  • adicionar um parâmetro dropoutRate à SGD secção chamada para definir a probabilidade de abandono

Nesta tarefa específica, por favor especifique nenhum abandono para a primeira época de 1, seguido de uma taxa de abandono de 50%. Por favor, dê uma olhada na Dropout() documentação para ver como fazê-lo.

Se tudo correr bem, não se notará qualquer alteração para a primeira época de 1, mas uma melhoria muito menor de ce uma vez que o abandono começa com a segunda época. Isto era esperado. (Para esta configuração específica, a precisão do reconhecimento não melhora, na verdade.) O resultado final quando se treina apenas 10 épocas é de cerca de 32%. 10 épocas não são suficientes para desistir.

Por favor, veja a solução aqui.

Tarefa 2: Simplificar a definição do modelo extraindo peças repetitivas numa função

No exemplo, a sequência (Convolution >> ReLU >> Pooling) é repetida três vezes. A sua tarefa é escrever uma função BrainScript que agruta estas três operações num módulo reutilizável. Note que as três utilizações desta sequência utilizam parâmetros diferentes (dimensões de saída, peso de inicialização). Assim, a função que escreve deve tomar estes dois como parâmetros, além dos dados de entrada. Por exemplo, poderia ser definido como

MyLayer (x, depth, initValueScale)

Quando se faz isto, espera-se que os valores resultantes ce e errs os valores sejam os mesmos. No entanto, se correr na GPU, o não determinismo na implementação de propagação de back-propagação da CUDNN causará pequenas variações.

Por favor, veja a solução aqui.

Tarefa 3: Adicionar LotNormalização

(Esta tarefa requer uma GPU, uma vez que a implementação da normalização do lote por CNTK se baseia em cuDNN.)

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

A forma espacial (onde todas as posições dos pixels são normalizadas com parâmetros partilhados) é invocada por um parâmetro opcional: BatchNormalizationLayer{spatialRank=2}.

Por favor, adicione a normalização do lote às três camadas de convolução e entre as duas camadas densas. Note que a normalização do lote deve ser inserida imediatamente antes da não linearidade. Por isso, deve remover o activation parâmetro e, em vez disso, inserir chamadas explícitas na função ReLU()CNTK .

Além disso, a normalização do lote altera a velocidade de convergência. Por isso, aumentemos as taxas de aprendizagem das primeiras 7 épocas 3 vezes, e desativemos o ímpeto e a regularização L2, fixando os seus parâmetros para 0.

Ao correr, verá parâmetros aprecíveis adicionais listados no registo de treino. O resultado final será de cerca de 28%, o que é 4 pontos melhor do que sem normalização do lote após o mesmo número de iterações. A convergência acelera, de facto.

Por favor, veja a solução aqui.

Tarefa 4: Converter para Rede Residual

A configuração acima é um exemplo de "brinquedo" para sujar as mãos com as configurações de funcionamento e modificação CNTK, e intencionalmente não corremos para a convergência total para manter os tempos de reviravolta baixos. Então 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 onde camadas, em vez de aprender um mapeamento da entrada para a saída, aprendem um termo de correção.

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

Para começar, por favor modifique a configuração anterior. Primeiro, substitua a função model(features) do 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 alterar a configuração 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
}

A sua tarefa é modificar ResNetNode() e ResNetNodeInc() implementar a estrutura estabelecida na seguinte arte ASCII premiada:

            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

Por favor, confirme com a saída de validação no registo que o fez corretamente.

Isto vai demorar muito até ao fim. A produção 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

que o modelo incorreto sem as ligações de salto é 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

Por favor, veja a solução aqui.

Tarefa 5: Gerar automaticamente muitas camadas

Por último, o ResNet com melhor desempenho tem 152 camadas. Uma vez que seria muito entediante e com tendência a escrever 152 expressões individuais, vamos agora modificar a definição para gerar automaticamente uma pilha de ResNetNode().

A sua tarefa é escrever uma função com esta assinatura:

ResNetNodeStack (x, depth, L)

onde L denota quantos devem ser empilhados ResNetNodes , de modo a que possamos substituir a expressão por cima 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 que vai precisar são a expressão condicional :

z = if cond then x else y

e recursão.

Este treino vai funcionar por cerca de metade do nosso num Titan-X. Se o fez bem, no início do registo 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. Pode medir a taxa de erro com este comando:

cntk  configFile=ImageHandsOn.ResNet.cntk  command=Eval

e deve ver um resultado como este:

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

Esta taxa de erro é muito próxima da reportada no papel original da ResNet (https://arxiv.org/pdf/1512.03385v1.pdf, Quadro 6).

Por favor, veja a solução aqui.

Tarefa 6: Formação Paralela

Por último, se tiver várias GPUs, CNTK permite-lhe paralelizar o treino utilizando MPI (Interface de passagem de mensagens). Este modelo é demasiado pequeno para se esperar de muita velocidade sem afinar mais, por exemplo, de tamanhos de minibatch (a configuração atual do tamanho de minibatch é demasiado pequena para obter uma utilização completa dos núcleos de GPU disponíveis). No entanto, passemos pelas moções, para que saiba como fazê-lo quando passar para a carga de trabalho no mundo real.

Por favor, adicione a seguinte linha ao SGD bloco:

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

e, em seguida, executar este comando:

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

O que se segue?

Este tutorial tem praticado para tomar uma configuração existente, e modificá-lo de formas específicas:

  • adicionando uma operação predefinida (abandono)
  • extrair peças repetitivas em módulos reutilizáveis (funções)
  • refactorização (para inserir a normalização do lote)
  • estruturas de rede personalizadas (ligação de salto ResNet)
  • parametrizar estruturas repetitivas usando a recursão

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

Então, para onde vamos a partir daqui? Já deve ter descoberto que o padrão usado nestes exemplos, a que chamamos estilo de construção de gráficos , pode ser bastante propenso a erros. Detetou 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 forma de evitar este erro é utilizar a composição da função. Segue-se uma forma alternativa, 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}
)

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

Soluções

Solução 1: Adicionar Abandono

Modificar a definição do modelo da seguinte forma:

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 secção SGD:

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

Solução 2: Simplificar a definição do modelo extraindo peças repetitivas numa função

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

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 de modelo para usá-lo

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: Adicionar LotNormalização

Modificar 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 usá-lo. Insira também a normalização do lote antes zde:

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 atualizar estes parâmetros na secçã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 para ResNetNode() e ResNetNodeInc() são:

    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: Gerar 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)

Também precisa modificar a função do seu 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