Compartilhar via


Conceitos básicos do BrainScript

BrainScript – Um Walk-Through

Esta seção apresenta os conceitos básicos da linguagem "BrainScript". Um novo idioma? Não se preocupe & lido, é muito direto.

No CNTK, as redes personalizadas são definidas usando e BrainScriptNetworkBuilder descritas na linguagem de descrição de rede CNTK "BrainScript". Da mesma forma, as descrições de redes são chamadas de scripts cerebrais.

O BrainScript fornece uma maneira simples de definir uma rede de forma semelhante a código, usando expressões, variáveis, funções primitivas e autodefinidas, blocos aninhados e outros conceitos bem compreendidos. Ele é semelhante a uma linguagem de script na sintaxe.

Ok, vamos molhar os pés com um exemplo de BrainScript completo!

Uma definição de rede de exemplo do BrainScript completa

O exemplo a seguir mostra a descrição da rede de uma rede neural simples com uma camada oculta e uma camada de classificação. Explicaremos os conceitos neste exemplo. Antes de seguir em frente, talvez passe alguns minutos com o exemplo e tente adivinhar o que significa. Você pode descobrir, conforme lido, que adivinhou a maior parte corretamente.

BrainScriptNetworkBuilder = {   # (we are inside the train section of the CNTK config file)

    SDim = 28*28 # feature dimension
    HDim = 256   # hidden dimension
    LDim = 10    # number of classes

    # define the model function. We choose to name it 'model()'.
    model (features) = {
        # model parameters
        W0 = ParameterTensor {(HDim:SDim)} ; b0 = ParameterTensor {HDim}
        W1 = ParameterTensor {(LDim:HDim)} ; b1 = ParameterTensor {LDim}

        # model formula
        r = RectifiedLinear (W0 * features + b0) # hidden layer
        z = W1 * r + b1                          # unnormalized softmax
    }.z

    # define inputs
    features = Input {SDim}
    labels   = Input {LDim} 

    # apply model to features
    z = model (features)

    # define criteria and output(s)
    ce   = CrossEntropyWithSoftmax (labels, z)  # criterion (loss)
    errs = ErrorPrediction         (labels, z)  # additional metric
    P    = Softmax (z)     # actual model usage uses this

    # connect to the system. These five variables must be named exactly like this.
    featureNodes    = (features)
    inputNodes      = (labels)
    criterionNodes  = (ce)
    evaluationNodes = (errs)
    outputNodes     = (P)
}

Noções básicas de sintaxe do BrainScript

Antes de nos aprofundarmos, algumas notas gerais sobre a sintaxe do BrainScript.

O BrainScript usa uma sintaxe simples que visa permitir expressar redes neurais de uma forma semelhante a fórmulas matemáticas. Portanto, a unidade sintática fundamental é a atribuição, que é usada em atribuições de variáveis e definições de função. Por exemplo:

Softplus (x) = Log (1 + Exp (x))
h = Softplus (W * v + b)

Linhas, Comentários, Incluir

Embora uma atribuição normalmente seja escrita em uma única linha, as expressões podem abranger várias linhas. Para colocar várias atribuições em uma única linha, no entanto, você deve separá-las por ponto e vírgula. Por exemplo:

SDim = 28*28 ; HDim = 256 ; LDim = 10    # feature, hidden, and label dimension

Além de exigir um ponto e vírgula entre as atribuições na ausência de uma quebra de linha, o BrainScript não diferencia espaço em branco.

O BrainScript entende os comentários de fim de linha usando o estilo # python e o estilo //C++. Os comentários embutidos usam a sintaxe C (/* this is a comment*/), mas, ao contrário de C, eles podem não abranger várias linhas.

Para o BrainScript inserido em arquivos de configuração CNTK (em oposição à leitura do BrainScript de um arquivo separado por meio de uma include diretiva), devido a uma interação com o analisador de configuração, há a restrição adicional (um pouco estranha) de que quaisquer parênteses, chaves ou colchetes devem ser equilibrados dentro de comentários de estilo C/C++ e literais de cadeia de caracteres. Portanto, sem sorrisos em comentários no estilo C/C++!

Uma include "PATH" diretiva pode ser usada em qualquer lugar para inserir o conteúdo de um arquivo no ponto da instrução . Aqui, PATH pode ser um caminho relativo ou absoluto (com ou sem subdiretórios). Se for um caminho relativo, os seguintes locais serão pesquisados na ordem: diretório de trabalho atual; diretórios que contêm arquivos externos, incluindo, se houver; directory(s) que contém os arquivos de configuração; e, por fim, o diretório que contém o executável CNTK. Todas as funções internas do BrainScript são incluídas dessa forma de um arquivo chamado CNTK.core.bs que está localizado ao lado do executável CNTK.

Expressões

Em seguida, você precisa saber sobre expressões BrainScript– as fórmulas que descrevem sua rede. As expressões BrainScript são escritas como matemáticas em uma sintaxe semelhante às linguagens de programação populares. As expressões mais simples são literais, por exemplo, números e cadeias de caracteres. Um exemplo de matemática é W1 * r + b, em que * se refere a um produto escalar, matriz ou tensor, dependendo do tipo das variáveis. Outro tipo comum de expressão é a invocação de função, por exemplo, RectifiedLinear (.).

O BrainScript é uma linguagem de tipo dinâmico. Um tipo de expressão importante é o registro, definido usando a {...} sintaxe e acessado por meio da sintaxe do ponto. Por exemplo, r = { x = 13 ; y = 42 } atribui um registro com dois membros a r, em que o primeiro membro pode ser acessado como r.x.

Além dos operadores matemáticos usuais, o BrainScript tem uma expressão condicional (if c then t else f), uma expressão de matriz e lambdas simples. Por fim, para fazer a interface com o código C++ do CNTK, o BrainScript pode instanciar diretamente um conjunto limitado de objetos C++ predefinidos, predominantemente os dos quais as ComputationNode redes computacionais são compostas. Para obter mais detalhes, consulte Expressões.

As expressões BrainScript são avaliadas após o primeiro uso. A principal finalidade do BrainScript é descrever a rede, portanto, o valor de uma expressão geralmente não é um valor final, mas sim um nó em um grafo de computação para computação adiada (como em W1 * r + b). Somente as expressões BrainScript de escalares (por exemplo, 28*28) são "computadas" no momento em que o BrainScript é analisado. Expressões que nunca são usadas (por exemplo, devido a uma condição) nunca são avaliadas.

Observação: a versão agora preterida NDLNetworkBuilder só tinha suporte para sintaxe de invocação de função; por exemplo, seria necessário escrever Plus (Times (W1, r), b1).

Variáveis

As variáveis podem conter o valor de qualquer expressão BrainScript (número, cadeia de caracteres, registro, matriz, lambda, objeto CNTK C++) e são substituídas quando usadas em uma expressão. As variáveis são imutáveis, ou seja, atribuídas apenas uma vez. Por exemplo, a definição de rede acima começa com:

SDim = 28*28  
HDim = 256
LDim = 10

Aqui, as variáveis são definidas como valores numéricos escalares que são usados como parâmetros em expressões subsequentes. Esses valores são as dimensões dos exemplos de dados, camadas ocultas e rótulos usados no treinamento. Esta configuração de exemplo específica é para o conjunto de dados MNIST, que é uma coleção de [28 x 28]imagens -pixel. Cada imagem é um dígito manuscrito (0-9), portanto, há 10 rótulos possíveis que podem ser aplicados a cada imagem. A dimensão HDim de ativação oculta é uma escolha do usuário.

A maioria das variáveis são membros de registro (o bloco BrainScript externo é implicitamente um registro). Além disso, as variáveis podem ser argumentos de função ou armazenadas como elementos de matrizes.

OK, pronto para passar pela definição do modelo.

Definindo a rede

Uma rede é descrita principalmente por fórmulas de como as saídas da rede são calculadas a partir das entradas. Chamamos isso de função de modelo, que geralmente é definida como uma função real no BrainScript. Como parte da função de modelo, o usuário deve declarar os parâmetros do modelo. Por fim, é necessário definir as entradas e critérios/saídas da rede. Todas elas são definidas como variáveis. As variáveis de entrada e critérios/saída devem ser comunicadas ao sistema.

Parâmetros de modelo e função de modelo da rede

A função de modelo contém as fórmulas de rede reais e os respectivos parâmetros de modelo. Nosso exemplo usa o produto e a adição de matriz e a função "primitiva" (interna) para a função RectifiedLinear()de energia , portanto, o núcleo da função de rede consiste nessas equações:

r = RectifiedLinear (W0 * features + b0)
z = W1 * r + b1 

Os parâmetros de modelo são matrizes, vetores de viés ou qualquer outro tensor que constitua o modelo aprendido após a conclusão do treinamento. Os tensores de parâmetro de modelo são usados na transformação dos dados de exemplo de entrada na saída desejada e são atualizados pelo processo de aprendizagem. A rede de exemplo acima contém os seguintes parâmetros de matriz:

W0 = ParameterTensor {(HDim:SDim)}
b0 = ParameterTensor {(HDim)}

Nesse caso, W0 é a matriz de peso e b0 é o vetor de viés. ParameterTensor{} indica um primitivo CNTK especial, que cria uma instância de um vetor, matriz ou tensor de classificação arbitrária e usa os parâmetros de dimensão como uma matriz BrainScript (números concatenados por dois-pontos :). A dimensão de um vetor é um único número, enquanto uma dimensão de matriz deve ser especificada como (numRows:numCols). Por padrão, os parâmetros são inicializados com números aleatórios uniformes quando instanciados diretamente e heNormal quando usados por meio de camadas, mas existem outras opções (veja aqui) para a lista completa. Ao contrário das funções regulares, ParameterTensor{} usa seus argumentos em chaves em vez de parênteses. Chaves são a convenção BrainScript para funções que criam parâmetros ou objetos, em vez de funções.

Em seguida, tudo isso é encapsulado em uma função BrainScript. As funções BrainScript são declaradas no formato f(x) = an expression of x. Por exemplo, Sqr (x) = x * x é uma declaração de função BrainScript válida. Não poderia ser muito mais simples e direto, certo?

Agora, a função de modelo real do nosso exemplo acima é um pouco mais complexa:

model (features) = {
    # model parameters
    W0 = ParameterTensor {(HDim:SDim)} ; b0 = ParameterTensor {HDim}  
    W1 = ParameterTensor {(LDim:HDim)} ; b1 = ParameterTensor {LDim}

    # model formula
    r = RectifiedLinear (W0 * features + b0) # hidden layer
    z = W1 * r + b1                          # unnormalized softmax
}.z

O exterior { ... } e essa final .z merecem alguma explicação. Os curlies externos { ... } e seu conteúdo realmente definem um registro com 6 membros de registro (W0, b0, W1, b1, re z). No entanto, o valor da função de modelo é apenas z; todos os outros são internos para a função. Portanto, usamos .z para selecionar o membro de registro que desejamos retornar. Essa é apenas a sintaxe de ponto para acessar membros de registro. Dessa forma, os outros membros do registro não podem ser acessados de fora. Mas eles continuam a existir como parte da expressão para calcular z. O { ... ; x = ... }.x padrão é uma maneira de usar variáveis locais.

Observe que a sintaxe do registro não é necessária. Como alternativa, model(features) também poderia ter sido declarado sem o desvio por meio do registro, como uma única expressão:

model (features) = ParameterTensor {(LDim:HDim)} * (RectifiedLinear (ParameterTensor {(HDim:SDim)}
                   * features + ParameterTensor {HDim})) + ParameterTensor {LDim}

Isso é muito mais difícil de ler e, mais importante, não permitirá usar o mesmo parâmetro em vários locais na fórmula.

Entradas

As entradas na rede são definidas pelos dados de exemplo e pelos rótulos associados aos exemplos:

features = Input {SDim}
labels   = Input {LDim}

Input{} é o segundo primitivo CNTK especial necessário para a definição de modelo (o primeiro é Parameter{}). Ele cria uma variável que recebe entrada de fora da rede: do leitor. O argumento de é a dimensão de Input{} dados. Neste exemplo, a features entrada terá as dimensões dos dados de exemplo (que definimos na variável SDim), e a labels entrada terá as dimensões dos rótulos. Espera-se que os nomes das variáveis das entradas correspondam às entradas correspondentes na definição do leitor.

Critérios de treinamento e saídas de rede

Ainda precisamos declarar como a saída da rede interage com o mundo. Nossa função de modelo calcula valores de logit (probabilidades de log não normalizadas). Esses valores de logit podem ser usados para

  • definir o critério de treinamento,
  • medir a precisão e
  • compute a probabilidade sobre as classes de saída dada uma entrada, para basear uma decisão de classificação em (observe que o log posterior z não desnormalizado geralmente pode ser usado para classificação diretamente).

A rede de exemplo usa rótulos de categoria, que são representados como vetores únicos. Para o exemplo MNIST, eles aparecerão como uma matriz de 10 valores de ponto flutuante, todos os quais são zero, exceto para a categoria de rótulo adequada, que é 1,0. Tarefas de classificação como a nossa geralmente usam a SoftMax() função para obter as probabilidades de cada rótulo. Em seguida, a rede é otimizada para maximizar a probabilidade de log da classe correta (entropia cruzada) e minimizar a de todas as outras classes. Esse é nosso critério de treinamento ou função de perda. No CNTK, essas duas ações normalmente são combinadas em uma função para eficiência:

ce = CrossEntropyWithSoftmax (labels, z)

CrossEntropyWithSoftmax() A função usa a entrada, calcula a SoftMax() função, calcula o erro do valor real usando entropia cruzada e esse sinal de erro é usado para atualizar os parâmetros na rede por meio da propagação de volta. Portanto, no exemplo acima, o valor normalizado Softmax() , que calculamos como P, não é usado durante o treinamento. No entanto, ele será necessário para usar a rede (observe novamente que, em muitos casos, z geralmente é suficiente para classificação; nesse caso, z seria a saída).

O CNTK usa o SGD (Gradiente Stochastic Descent) como o algoritmo de aprendizagem. O SGD precisa calcular o gradiente da função objetiva em relação a todos os parâmetros de modelo. É importante ressaltar que o CNTK não exige que os usuários especifiquem esses gradientes. Em vez disso, cada função interna no CNTK também tem uma função de equivalente derivada e o sistema executa automaticamente a atualização de propagação traseira dos parâmetros de rede. Isso não é visível para o usuário. Os usuários nunca precisam se preocupar com gradientes. Já.

Além do critério de treinamento, as taxas de erro previstas geralmente são calculadas durante a fase de treinamento para validar a melhoria do sistema à medida que o treinamento avança. Isso é tratado no CNTK usando a seguinte função:

errs = ClassificationError (labels, z)

As probabilidades produzidas pela rede são comparadas com o rótulo real e a taxa de erro é calculada. Isso geralmente é exibido pelo sistema. Embora isso seja útil, não é obrigatório usar ClassificationError().

Comunicando entradas, saídas e critérios para o sistema

Agora que todas as variáveis são definidas, devemos informar ao sistema quais das variáveis devem ser tratadas como entradas, saídas e critérios. Isso é feito definindo cinco variáveis especiais que devem ter exatamente estes nomes:

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

Os valores são matrizes, em que os valores devem ser separados por dois-pontos (os dois-pontos : são um operador BrainScript que forma uma matriz concatenando dois valores ou matrizes). Isso é mostrado acima para outputNodes, que declara e zP como saídas.

(Observação: o preterido exigiu NDLNetworkBuilder que os elementos da matriz fossem separados por vírgulas em vez disso.)

Resumo de nomes especiais

Como vimos acima, há 7 nomes especiais que devemos estar cientes, que carregam propriedades "mágicas":

  • ParameterTensor{}: declara e inicializa um parâmetro de aprendizado.
  • Input{}: declara uma variável que é conectada e alimentada por um leitor de dados.
  • featureNodes, labelNodes, criterionNodes, evaluationNodese outputNodes: declara ao sistema qual de nossas variáveis usar como entradas, saídas e critérios.

Além disso, há mais três funções especiais com "magic" interno no CNTK, que são discutidas em outro lugar:

  • Constant(): declara uma constante.
  • PastValue() e FutureValue(): acesse uma variável em uma função de rede em uma etapa de tempo diferente, para loops recorrentes de formulário.

Qualquer outro nome predefinido é uma função primitiva interna, como Sigmoid() ou Convolution() com uma implementação C++, uma função de biblioteca predefinida realizada no BrainScript, como BS.RNNs.LSTMP(), ou um registro que atua como um namespace para funções de biblioteca (por exemplo, BS.RNNs). Consulte BrainScript-Full-Function-Reference para obter uma lista completa.

Próximo: Expressões BrainScript