Partilhar via


Conceitos Básicos do BrainScript

BrainScript --A Walk-Through

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

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

O BrainScript fornece uma forma simples de definir uma rede de forma semelhante a um código, utilizando expressões, variáveis, funções primitivas e autodefinidas, blocos aninhados e outros conceitos bem compreendidos. Tem um aspeto semelhante a uma linguagem de scripting na sintaxe.

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

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

O exemplo seguinte mostra a descrição de rede de uma Rede Neural simples com uma camada oculta e uma camada de classificação. Vamos explicar os conceitos ao longo deste exemplo. Antes de seguir em frente, talvez passe alguns minutos com o exemplo e tente adivinhar o que significa. Pode descobrir, como leu, 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 da Sintaxe BrainScript

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

O BrainScript utiliza uma sintaxe simples que visa permitir a expressão de redes neurais de uma forma semelhante às fórmulas matemáticas. Por conseguinte, a unidade sintática fundamental é a atribuição, que é utilizada tanto nas atribuições de variáveis como nas 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 seja normalmente escrita numa única linha, as expressões podem abranger múltiplas linhas. No entanto, para colocar várias atribuições numa única linha, tem de 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 atribuições na ausência de uma quebra de linha, o BrainScript não é sensível ao espaço em branco.

O BrainScript compreende os comentários de extremidade de linha com o estilo Python e o estilo #//C++. Os comentários inline utilizam sintaxe C (/* this is a comment*/), mas, ao contrário de C, estes podem não abranger várias linhas.

Para o BrainScript incorporado em ficheiros de configuração CNTK (por oposição ao BrainScript lido a partir de um ficheiro separado através de uma include diretiva), devido a uma interação com o analisador de configuração, existe a restrição adicional (um pouco estranha) de que quaisquer parênteses, chavetas ou parênteses têm de ser equilibrados dentro de comentários de estilo C/C++ e literais de cadeia. Portanto, sem sorrisos em comentários de estilo C/C++!

Uma include "PATH" diretiva pode ser utilizada em qualquer local para inserir o conteúdo de um ficheiro no ponto da instrução. Aqui, PATH pode ser um caminho absoluto ou relativo (com ou sem subdiretórios). Se for um caminho relativo, as seguintes localizações são pesquisadas por ordem: diretório de trabalho atual; diretórios que contenham externos, incluindo ficheiros, se existirem; diretórios que contêm o(s) ficheiro(s) de configuração; e, por último, o diretório que contém o executável CNTK. Todas as funções Incorporadas do BrainScript são incluídas desta forma a partir de um ficheiro chamado CNTK.core.bs que está localizado junto ao executável CNTK.

Expressions (Expressões)

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

O BrainScript é uma linguagem escrita dinamicamente. Um tipo de expressão importante é o registo, definido com a {...} sintaxe e acedido através da sintaxe do ponto. Por exemplo, r = { x = 13 ; y = 42 } atribui um registo com dois membros a r, onde o primeiro membro pode ser acedido como r.x.

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

As expressões BrainScript são avaliadas após a primeira utilização. O principal objetivo do BrainScript é descrever a rede, pelo que o valor de uma expressão geralmente não é um valor final, mas sim um nó num gráfico de computação para computação diferida (como em W1 * r + b). Apenas as expressões BrainScript de escalares (por exemplo, 28*28) são "calculadas" no momento em que o BrainScript é analisado. As expressões que nunca são utilizadas (por exemplo, devido a uma condição) nunca são avaliadas.

Nota: a versão preterida NDLNetworkBuilder agora suportava apenas a sintaxe de invocação de funções; por exemplo, teria de escrever Plus (Times (W1, r), b1).

Variáveis

As variáveis podem conter o valor de qualquer expressão BrainScript (número, cadeia, registo, matriz, lambda, CNTK C++ objeto) e são substituídas quando utilizadas numa 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 para valores numéricos escalares que são utilizados como parâmetros em expressões subsequentes. Estes valores são as dimensões dos exemplos de dados, camadas ocultas e etiquetas utilizadas na preparação. Esta configuração de exemplo específica destina-se ao conjunto de dados MNIST, que é uma coleção de [28 x 28]imagens -pixel. Cada imagem é um dígito manuscrito (0-9), pelo que existem 10 etiquetas possíveis que podem ser aplicadas a cada imagem. A dimensão HDim de ativação oculta é uma escolha do utilizador.

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

OK, pronto para percorrer a definição do modelo.

Definir a Rede

Uma rede é descrita principalmente por fórmulas de como as saídas da rede são calculadas a partir das entradas. Chamamos a isto a função de modelo, que é muitas vezes definida como uma função real no BrainScript. Como parte da função de modelo, o utilizador tem de declarar os parâmetros do modelo. Por último, é necessário definir as entradas e os critérios/saídas da rede. Todas estas são definidas como variáveis. As variáveis de entrada e critérios/saída têm de ser comunicadas ao sistema.

Função de Modelo e Parâmetros de Modelo da Rede

A função de modelo contém as fórmulas de rede reais e os respetivos parâmetros de modelo. O nosso exemplo utiliza o produto de matriz e a adição, e a função "primitiva" (incorporada) para a função RectifiedLinear()energética , pelo que o núcleo da função de rede consiste nestas equações:

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

Os parâmetros do modelo são matrizes, vetores de desvio ou qualquer outro tensor que constitua o modelo aprendido após a conclusão da preparação. Os tensors model-parameter são utilizados na transformação dos dados de exemplo de entrada na saída pretendida 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)}

Neste caso, W0 é a matriz de peso e b0 é o vetor de desvio. ParameterTensor{} denota um primitivo CNTK especial, que instancia um tensor de vetor, matriz ou classificação arbitrária, e assume 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 predefinição, os Parâmetros são inicializados com números aleatórios uniformes quando são instanciados diretamente e heNormal quando utilizados através de camadas, mas existem outras opções (veja aqui) para obter a lista completa. Ao contrário das funções normais, ParameterTensor{} utiliza os seus argumentos em chavetas em vez de parênteses. As chavetas são a convenção BrainScript para funções que criam parâmetros ou objetos, em oposição às funções.

Isto é, em seguida, tudo moldado numa função BrainScript. As funções BrainScript são declaradas na forma f(x) = an expression of x. Por exemplo, Sqr (x) = x * x é uma declaração de função BrainScript válida. Não pode 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 a final .z merecem alguma explicação. Os curlies exteriores e os respetivos { ... } conteúdos definem um registo com 6 membros de registo (W0, b0, W1, b1, re z). No entanto, o valor da função do modelo é apenas z; todos os outros são internos à função. Assim, utilizamos .z para selecionar o membro do registo que pretendemos devolver. Esta é apenas a sintaxe de pontos para aceder aos membros do registo. Desta forma, os outros membros do registo não estão acessíveis a partir de fora. Mas continuam a existir como parte da expressão para calcular z. O { ... ; x = ... }.x padrão é uma forma de utilizar variáveis locais.

Tenha em atenção que a sintaxe do registo não é necessária. Em alternativa, model(features) também poderia ter sido declarado sem o desvio através do registo, como uma única expressão:

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

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

Entradas

As entradas na rede são definidas pelos dados de exemplo e pelas etiquetas associadas aos exemplos:

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

Input{} é o segundo CNTK primitivo especial necessário para a definição do modelo (o primeiro é Parameter{}). Cria uma variável que recebe entradas 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 das etiquetas. Espera-se que os nomes das variáveis das entradas correspondam às entradas correspondentes na definição do leitor.

Critérios de Preparação e Saídas de Rede

Ainda temos de declarar como a saída da rede interage com o mundo. A nossa função de modelo calcula valores de logit (probabilidades de registo não normalizadas). Estes valores de logit podem ser utilizados para

  • definir o critério de preparação,
  • precisão da medida e
  • calcule a probabilidade sobre as classes de saída fornecidas por uma entrada, para basear uma decisão de classificação em (tenha em atenção que o posterior z de registo não normalizado pode, muitas vezes, ser utilizado para classificação diretamente).

A rede de exemplo utiliza etiquetas de categoria, que são representadas como vetores únicos. Para o exemplo MNIST, estes serão apresentados como uma matriz de 10 valores de vírgula flutuante, todos eles zero, exceto a categoria de etiqueta adequada que é 1,0. Tarefas de classificação como a nossa utilizam frequentemente a SoftMax() função para obter as probabilidades de cada etiqueta. Em seguida, a rede é otimizada para maximizar a probabilidade de registo da classe correta (entropia cruzada) e minimizar a de todas as outras classes. Este é o nosso critério de preparação ou função de perda. No CNTK, estas duas ações são normalmente combinadas numa função para eficiência:

ce = CrossEntropyWithSoftmax (labels, z)

CrossEntropyWithSoftmax() a função utiliza a entrada, calcula a SoftMax() função, calcula o erro do valor real com a entropia cruzada e esse sinal de erro é utilizado para atualizar os parâmetros na rede através da propagação anterior. Assim, no exemplo acima, o valor normalizado Softmax() , que calculámos como P, não é utilizado durante a preparação. No entanto, será necessário para utilizar a rede (repare novamente que, em muitos casos, z é muitas vezes suficiente para a classificação; nesse caso, z seria a saída).

O CNTK utiliza o SGD (Stochastic Gradient Descent) como algoritmo de aprendizagem. O SGD tem de calcular a gradação da função de objetivo em relação a todos os parâmetros de modelo. Importante, o CNTK não requer que os utilizadores especifiquem essas gradações. Em vez disso, cada função incorporada no CNTK também tem uma função de contraparte derivada e o sistema executa automaticamente a atualização de propagação anterior dos parâmetros de rede. Isto não está visível para o utilizador. Os utilizadores nunca têm de se preocupar com gradações. Sempre.

Além do critério de preparação, as taxas de erro previstas são muitas vezes calculadas durante a fase de preparação para validar a melhoria do sistema à medida que a preparação vai mais longe. Esta ação é processada no CNTK com a seguinte função:

errs = ClassificationError (labels, z)

As probabilidades produzidas pela rede são comparadas com a etiqueta real e a taxa de erro é calculada. Geralmente, isto é apresentado pelo sistema. Embora isto seja útil, não é obrigatório utilizar ClassificationError().

Comunicar Entradas, Saídas e Critérios ao Sistema

Agora que todas as variáveis estão definidas, temos de indicar ao sistema qual das variáveis deve tratar como entradas, saídas e critérios. Isto é feito ao definir 5 variáveis especiais que têm de 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 (o dois pontos : é um operador BrainScript que forma uma matriz ao concatenar dois valores ou matrizes). Isto é mostrado acima para outputNodes, que declara tanto z como P como saídas.

(Nota: o preterido NDLNetworkBuilder exigia que os elementos da matriz fossem separados por vírgulas.)

Resumo dos Nomes Especiais

Como vimos acima, há 7 nomes especiais que temos de ter em atenção, que transportam propriedades "mágicas":

  • ParameterTensor{}: declara e inicializa um parâmetro passível de aprendizagem.
  • Input{}: declara uma variável que é ligada e fornecida a partir de um leitor de dados.
  • featureNodes, labelNodes, criterionNodes, evaluationNodese outputNodes: declara ao sistema qual das nossas variáveis deve utilizar como entradas, saídas e critérios.

Além disso, existem mais 3 funções especiais com "magia" incorporada no CNTK, que são abordadas noutros locais:

  • Constant(): declara uma constante.
  • PastValue() e FutureValue(): Aceda a uma variável numa função de rede num passo de tempo diferente, para ciclos recorrentes de formulários.

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

Seguinte: Expressões BrainScript