Expressões BrainScript
Esta secção é a especificação das expressões BrainScript, embora usemos intencionalmente linguagem informal para mantê-la legível e acessível. A sua contrapartida é a especificação da sintaxe de definição de função BrainScript, que pode ser encontrada aqui.
Cada script cerebral é uma expressão, que por sua vez consiste em expressões atribuídas a variáveis de membros de gravação. O nível mais externo de uma descrição da rede é uma expressão de registo implícita. BrainScript tem os seguintes tipos de expressões:
- literales como números e cordas
- infixo de matemática e operações unary como
a + b
- uma expressão ternária condicional
- invocações de função
- registos, acessos de membros de registo
- matrizes, acessos de elementos de matriz
- expressões de função (lambdas)
- construção de objetos C++ incorporados
Mantivemos intencionalmente a sintaxe para cada uma destas línguas populares, tanto do que se encontra abaixo parecerá muito familiar.
Conceitos
Antes de descrever os tipos individuais de expressão, primeiro alguns conceitos básicos.
Imediato vs. Computação Diferida
BrainScript conhece dois tipos de valores: imediatos e adiados. Os valores imediatos são calculados durante o processamento do BrainScript, enquanto os valores diferidos são objetos que representam nós na rede de cálculo. A rede de cálculo descreve o cálculo real realizado pelo motor de execução CNTK durante o treino e utilização do modelo.
Os valores imediatos no BrainScript destinam-se a parametrizar a computação. Eles denotam dimensões de tensor, o número de camadas de rede, um nome de pathname para carregar um modelo de, etc. Uma vez que as variáveis BrainScript são imutáveis, os valores imediatos são sempre constantes.
Os valores diferidos surgem do objetivo primordial dos scripts cerebrais: descrever a rede de cálculo. A rede de cálculo pode ser vista como uma função que é passada para a rotina de treino ou inferência, que executa então a função de rede através do motor de execução CNTK. Assim, o resultado de muitas expressões BrainScript é um nó de computação numa rede de computação, em vez de um valor real. Do ponto de vista do BrainScript, um valor diferido é um objeto C++ do tipo ComputationNode
que representa um nó de rede. Por exemplo, a soma de dois nós de rede cria um novo nó de rede que representa a operação de síntese que toma os dois nós como entradas.
Scalars vs. Matrices vs. Tensors
Todos os valores na rede de computação são matrizes n-dimensional numéricas a que chamamos tensors, e n denota a classificação de tensor. As dimensões do tensor são explicitamente especificadas para entradas e parâmetros modelo; e inferidas automaticamente pelos operadores.
O tipo de dados mais comum para a computação, matrizes, são apenas tensores da classificação 2. Os vetores de coluna são tensores da classificação 1, enquanto os vetores de linha são de classificação 2. O produto matriz é uma operação comum em redes neurais.
Os tensores são sempre valores diferidos, ou seja, objetos no gráfico de computação diferido. Qualquer operação que envolva uma matriz ou tensor torna-se parte do gráfico de cálculo e é avaliada durante o treino e inferência. No entanto, as dimensões do tensor são inferidas/verificadas antecipadamente no tempo de processamento da BS.
Os escalares podem ser valores imediatos ou diferidos. Os escalares que parametrizem a própria rede de cálculo, como as dimensões do tensor, devem ser imediatos, ou seja, computáveis no momento do processamento do BrainScript. Os escalões diferidos são tensores de dimensão de nível [1]
1. Fazem parte da própria rede, incluindo parâmetros escalares aprecáveis, tais como auto-estabilizadores, e constantes como em Log (Constant (1) + Exp(x))
.
Dactilografia dinâmica
BrainScript é uma linguagem de tipo dinâmico com um sistema de tipo extremamente simples. Os tipos são verificados durante o processamento do BrainScript quando um valor é usado.
Os valores imediatos são de número tipo, Boolean, string, record, array, function/lambda, ou uma das classes C++ predefinidas da CNTK. Os seus tipos são verificados no momento da utilização (por exemplo, o COND
argumento da if
declaração é verificado como um Boolean
, e um elemento de acesso de matriz requer que o objeto seja uma matriz).
Todos os valores diferidos são tensores. As dimensões do tensor fazem parte do seu tipo, que são verificadas ou inferidas durante o processamento do BrainScript.
As expressões entre um escalar imediato e um tensor diferido devem converter explicitamente o escalar num diferido Constant()
. Por exemplo, a não-linearidade softplus deve ser escrita como Log (Constant(1) + Exp (x))
. (Está previsto remover este requisito numa próxima atualização.)
Tipos de expressão
Literais
Os literais são numéricos, booleanos ou constantes de cordas, como seria de esperar. Exemplos:
13
,42
,3.1415926538
,1e30
true
,false
"my_model.dnn"
,'single quotes'
Os literais numéricos são sempre um ponto flutuante de dupla precisão. Não existe um tipo inteiro explícito no BrainScript, embora algumas expressões como índices de matriz falhem com um erro se forem apresentados valores que não são inteiros.
As letras de corda podem utilizar citações simples ou duplas, mas não têm forma de escapar a citações ou outros caracteres no interior (as cordas que contêm citações simples e duplas devem ser calculadas, por exemplo). "He'd say " + '"Yes!" in a jiffy.'
As cordas literais podem abranger várias linhas; Por exemplo:
I3 = Parameter (3, 3, init='fromLiteral', initFromLiteral = '1 0 0
0 1 0
0 0 1')
Operações Infix e Unary
O BrainScript suporta os operadores abaixo. Os operadores BrainScript são escolhidos para significar o que se esperaria vindo de línguas populares, com exceção do (produto em termos de .*
elementos), *
e semântica de transmissão especial de operações em termos de elementos.
Operadores de infixos numéricos+
, -
, *
, /
.*
+
,-
e*
aplicar-se a escalões, matrizes e tensores..*
denota um produto em termos de elementos. Nota para os utilizadores Python: Este é o equivalente ao de numpy's*
./
é apenas suportado para escalões. Uma divisão em termos de elementos pode ser escrita utilizando cálculos incorporadosReciprocal(x)
em termos1/x
de elementos .
Operadores &&
de infixos Boolean, ||
Estes denotam Boolean E e OR, respectivamente.
Concatenação de cordas (+
)
As cordas são concatenadas com +
. Exemplo: BS.Networks.Load (dir + "/model.dnn")
.
Operadores de Comparação
Os seis operadores de comparação são<
, >
==
e as suas negações, <=
!=
e as suas negações>=
. Podem ser aplicados a todos os valores imediatos, como esperado; seu resultado é um Boolean.
Para aplicar operadores de comparação a tensores, é necessário utilizar funções incorporadas, tais como Greater()
.
Unary-
, !
Estes denotam negação e negação lógica, respectivamente. !
Atualmente só pode ser usado para escalões.
Operações elemento eSemântica de Radiodifusão
Quando aplicados às matrizes/tensores, +
, -
e .*
são aplicados em termos de elementos.
Todas as operações em termos de elementos apoiam a semântica de radiodifusão. A radiodifusão significa que qualquer dimensão especificada como 1 será automaticamente repetida para corresponder a qualquer dimensão.
Por exemplo, um vetor de linha de dimensão [1 x N]
pode ser adicionado diretamente a uma matriz de dimensão [M x N]
. A 1
dimensão repetirá M
automaticamente os tempos. Além disso, as dimensões do tensor são automaticamente acolchoadas com 1
dimensões. Por exemplo, é permitido adicionar um vetor de coluna de dimensão [M]
uma [M x N]
matriz. Neste caso, as dimensões do vetor da coluna são automaticamente acolchoadas para corresponder à [M x 1]
classificação da matriz.
Nota para os utilizadores Python: Ao contrário do que é que as dimensões de radiodifusão estão alinhadas à esquerda.
O Operador de Produto Matrix*
A A * B
operação denota o produto matriz. Também pode ser aplicado a matrizes escassas, o que melhora a eficiência para o manuseamento de entradas de texto ou rótulos que são representados como vetores de um só calor. Em CNTK, o produto matriz tem uma interpretação alargada que permite a sua utilização com tensores da categoria > 2. É, por exemplo, possível multiplicar cada coluna num tensor de nível 3 individualmente com uma matriz.
O produto matriz e a sua extensão de tensor são descritos em detalhe aqui.
Nota: Para multiplicar com um escalar, utilize o produto .*
em termos de elementos .
Python utilizadores sejam avisados de que numpy
utiliza o *
operador para o produto em termos de elementos, e não para o produto matriz. O operador da *
CNTK corresponde ao numpy
de dot()
, enquanto o equivalente a CNTK ao operador numpy
de matrizes da *
Python é .*
.
O Operador Condicional if
Os condicionalismos no BrainScript são expressões, como o operador C++ ?
. A sintaxe BrainScript é if COND then TVAL else EVAL
, onde COND
deve ser uma expressão booleana imediata, e o resultado da expressão é se COND
é TVAL
verdade, e EVAL
não é. A if
expressão é útil para implementar múltiplas configurações semelhantes de bandeiras no mesmo BrainScript, e também para recursão.
(O if
operador só funciona para valores de escalação imediatas. Para implementar condicionals para objetos diferidos, utilize a função BS.Boolean.If()
incorporada, que permite selecionar um valor de um de dois tensores com base num tensor de bandeira. Tem o formulário If (cond, tval, eval)
.)
Invocações de funções
O BrainScript tem três tipos de funções: primitivos incorporados (com implementações C++), funções de biblioteca (escritas no BrainScript) e definidos pelo utilizador (BrainScript). Exemplos de funções incorporadas são Sigmoid()
e MaxPooling()
. As funções definidas pela biblioteca e pelo utilizador são mecanicamente as mesmas, apenas guardadas em diferentes ficheiros de origem. Todos os tipos são invocados, à semelhança da matemática e das línguas comuns, utilizando o formulário f (arg1, arg2, ...)
.
Algumas funções aceitam parâmetros opcionais. Os parâmetros opcionais são passados como parâmetros nomeados, por exemplo f (arg1, arg2, option1=..., option2=...)
.
As funções podem ser invocadas novamente, por exemplo:
DNNLayerStack (x, numLayers) =
if numLayers == 1
then DNNLayer (x, hiddenDim, featDim)
else DNNLayer (DNNLayerStack (x, numLayers-1), # add a layer to a stack of numLayers-1
hiddenDim, hiddenDim)
Note como o if
operador é utilizado para acabar com a recursão.
Criação de Camadas
As funções podem criar camadas ou modelos inteiros que são objetos de função que também se comportam como funções.
Por convenção, uma função que cria uma camada com parâmetros apreciáveis usa aparelhos { }
encaracolados em vez de parênteses ( )
.
Encontrará expressões como esta:
h = DenseLayer {1024} (v)
Aqui, duas invocações estão em jogo. A primeira, DenseLayer{1024}
é uma chamada de função que cria um objeto de função, que depois, por sua vez, é aplicado aos dados (v)
.
Uma DenseLayer{}
vez que devolve um objeto de função com parâmetros aprecáveis, ele usa { }
para denotar isto.
Registos e Acesso Record-Member
Expressões de registo são atribuições rodeadas por aparelhos encaracolados. Por exemplo:
{
x = 13
y = x * factorParameter
f (z) = y + z
}
Esta expressão define um registo com três membros, x
y
e f
onde f
está uma função.
Dentro do registo, as expressões podem referenciar outros membros do registo apenas pelo seu nome, como x
é acedido acima na atribuição de y
.
No entanto, ao contrário de muitas línguas, as entradas de registo podem ser declaradas por qualquer ordem. Por exemplo, x
pode ser declarado depois y
de . Isto é para facilitar a definição de redes recorrentes. Qualquer membro do registo está acessível a partir da expressão de qualquer outro membro do registo. Isto é diferente, digamos, de Python; e semelhante ao F#'s let rec
. São proibidas referências cíclicas, com exceção especial das PastValue()
operações e FutureValue()
operações.
Quando os registos são aninhados (expressões de registo usadas dentro de outros registos), os membros dos registos são examinados através de toda a hierarquia de âmbitos de escamamento. Na verdade, cada atribuição variável é parte de um registo: O nível exterior de um BrainScript também é um registo implícito. No exemplo acima, factorParameter
teria de ser designado como membro de registo de um âmbito de encerramento.
As funções atribuídas dentro de um registo capturarão os membros do registo que referenciam. Por exemplo, f()
irá capturar y
, o que por sua vez depende x
e o definido factorParameter
externamente . Capturar estes meios que f()
podem ser passados como uma lambda em âmbitos externos que não contêm factorParameter
ou têm acesso a ela.
Do exterior, os membros dos registos são acedidos através do .
operador. Por exemplo, se tivéssemos atribuído a expressão de registo acima a uma variável r
, então r.x
produziria o valor 13
. O .
operador não atravessa os âmbitos de esdição: r.factorParameter
falharia com um erro.
(Note que até CNTK 1.6, em vez de aparelhos { ... }
encaracolados, regista suportes usados [ ... ]
. Isto ainda é permitido, mas precotado.)
Matrizes e Acesso à Matriz
BrainScript tem um tipo de matriz unidimensional para valores imediatos (não confundir com tensores). As matrizes são indexadas usando [index]
. Matrizes multidimensionais podem ser emuladas como matrizes de matrizes.
Podem ser declaradas matrizes de pelo menos 2 elementos utilizando o :
operador. Por exemplo, o seguinte declara uma matriz tridimensional nomeada imageDims
que depois é passada para ParameterTensor{}
declarar um tensor de parâmetro de grau 3:
imageDims = (256 : 256 : 3)
inputFilter = ParameterTensor {imageDims}
Também é possível declarar matrizes cujos valores se referem uns aos outros. Para isso, deve-se utilizar a sintaxe de atribuição de matrizes um pouco mais envolvida:
arr[i:i0..i1] = f(i)
que constrói uma matriz nomeada arr
com limite de i0
índice inferior e indexado superior, i
i1
denotando a variável para denotar a variável de índice na expressãof(i)
inicializadora, que por sua vez denota o valor de arr[i]
. Os valores da matriz são avaliados preguiçosamente. Isso permite que a expressão do inicializador para um índice i
específico aceda a outros elementos arr[j]
da mesma matriz, desde que não haja dependência cíclica. Por exemplo, isto pode ser usado para declarar uma pilha de camadas de rede:
layers[l:1..L] =
if l == 1
then DNNLayer (x, hiddenDim, featDim)
else DNNLayer (layers[l-1], hiddenDim, hiddenDim)
Ao contrário da versão recursiva desta que introduzimos anteriormente, esta versão preserva o acesso a cada camada individual dizendo .layers[i]
Em alternativa, existe também uma sintaxe array[i0..i1] (i => f(i))
de expressão , que é menos conveniente, mas por vezes útil. O acima seria assim:
layers = array[1..L] (l =>
if l == 1
then DNNLayer (x, hiddenDim, featDim)
else DNNLayer (layers[l-1], hiddenDim, hiddenDim)
)
Nota: Atualmente não há como declarar uma matriz de 0 elementos. Isto será abordado numa futura versão de CNTK.
Funções Expressões e Lambdas
No BrainScript, as funções são valores. Uma função nomeada pode ser atribuída a uma variável e passada como argumento, por exemplo:
Layer (x, m, n, f) = f (ParameterTensor {(m:n)} * x + ParameterTensor {n})
h = Layer (x, 512, 40, Sigmoid)
onde Sigmoid
é passado como uma função que é usada no interior Layer()
. Alternativamente, uma sintaxe (x => f(x))
de lambda semelhante a C#permite criar funções anónimas em linha. Por exemplo, isto define uma camada de rede com uma ativação Softplus:
h = Layer (x, 512, 40, (x => Log (Constant(1) + Exp (x)))
A sintaxe lambda está atualmente limitada a funções com um único parâmetro.
Padrão de camada
O exemplo acima Layer()
combina criação de parâmetros e aplicação de função.
Um padrão preferido é separá-los em dois passos:
- criar parâmetros e devolver um objeto de função que detém estes parâmetros
- criar a função que aplica os parâmetros a uma entrada
Especificamente, este último é um membro do objeto de função também. O exemplo acima pode ser reescrito como:
Layer {m, n, f} = {
W = ParameterTensor {(m:n)} # parameter creation
b = ParameterTensor {n}
apply (x) = f (W * x + b) # the function to apply to data
}.apply
e seria invocado como:
h = Layer {512, 40, Sigmoid} (x)
A razão para este padrão é que os tipos típicos de rede consistem em aplicar uma função após a outra a uma entrada, que pode ser escrita mais facilmente usando a Sequential()
função.
CNTK vem com um rico conjunto de camadas predefinidas, que são descritas aqui.
Construção de objetos de CNTK C++ incorporados
Em última análise, todos os valores brainScript são objetos C++. O operador new
brainScript especial é utilizado para interagir com os objetos CNTK C++ subjacentes. Tem a forma new TYPE ARGRECORD
onde TYPE
é um conjunto codificado de código rígido dos objetos C++ predefinidos expostos ao BrainScript, e ARGRECORD
é uma expressão de registo que é passada para o construtor C++.
É provável que só consiga ver este formulário se estiver a usar a forma de BrainScriptNetworkBuilder
parênteses, BrainScriptNetworkBuilder = (new ComputationNetwork { ... })
como descrito aqui.
Mas agora sabe o que significa: new ComputationNetwork
cria um novo tipo ComputationNetwork
C++ onde { ... }
simplesmente define um registo que é passado para o construtor C++ do objeto C++ internoComputationNetwork
, que depois procurará 5 membros específicos featureNodes
, labelNodes
eoutputNodes
criterionNodes
evaluationNodes
, como explicado aqui.
Sob o capot, todas as funções incorporadas são realmente new
expressões que constroem objetos da classe ComputationNode
C++ CNTK. Para ilustração, veja como o Tanh()
incorporado é realmente definido como criando um objeto C+++
Tanh (z, tag=') = nova operação ComputationNode { operação = 'Tanh' ; entradas = z /mais a função args/ }
Semântica de Avaliação de Expressão
As expressões BrainScript são avaliadas após a primeira utilização. Uma vez que o principal objetivo do BrainScript é descrever a rede, o valor de uma expressão é frequentemente um nó num gráfico de computação para computação diferida. Por exemplo, do ângulo BrainScript, W1 * r + b1
no exemplo acima 'avalia' um ComputationNode
objeto em vez de um valor numérico; enquanto os valores numéricos reais envolvidos serão calculados pelo motor de execução de gráficos. Apenas 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 utilizadas (por exemplo, devido a uma condição) nunca são avaliadas (nem verificadas por erros de tipo).
Padrões de utilização comuns de expressões
Abaixo estão alguns padrões comuns usados com BrainScript.
Espaços de nome para funções
Ao agrupar as atribuições de funções em registos, pode-se obter uma forma de espaçamento de nomes. Por exemplo:
Layers = {
Affine (x, m, n) = ParameterTensor {(m:n)} * x + ParameterTensor {n}
Sigmoid (x, m, n) = Sigmoid (Affine (x, m, n))
ReLU (x, m, n) = RectifiedLinear (Affine (x, m, n))
}
# 1-hidden layer MLP
ce = CrossEntropyWithSoftmax (Layers.Affine (Layers.Sigmoid (feat, 512, 40), 9000, 512))
Variáveis de âmbito local
Por vezes, é desejável ter variáveis e/ou funções localmente procuradas para expressões mais complexas. Isto pode ser conseguido através da divulgação de toda a expressão num registo e de acesso imediato ao seu valor de resultado. Por exemplo:
{ x = 13 ; y = x * x }.y
irá criar um registo "temporário" com um membro y
que é imediatamente lido. Este registo é «temporário», uma vez que não é atribuído a uma variável, pelo que os seus membros não são acessíveis, exceto para y
.
Este padrão é frequentemente usado para tornar as camadas de NN com parâmetros incorporados mais legíveis, por exemplo:
SigmoidLayer (m, n, x) = {
W = Parameter (m, n, init='uniform')
b = Parameter (m, 1, init='value', initValue=0)
h = Sigmoid (W * x + b)
}.h
Aqui, h
pode-se pensar no "valor de retorno" desta função.
Seguinte: Saiba como definir funções brainscript
NDLNetworkBuilder (Deprecado)
Versões anteriores de CNTK usaram o agora prevadizado NDLNetworkBuilder
em vez de BrainScriptNetworkBuilder
. NDLNetworkBuilder
implementou uma versão muito reduzida do BrainScript. Tinha as seguintes restrições:
- Sem sintaxe infixo. Todos os operadores devem ser invocados através de invocações de funções. Por exemplo,
Plus (Times (W1, r), b1)
em vez deW1 * r + b1
. - Sem expressões de registo aninhados. Há apenas um registo exterior implícito.
- Sem expressão condicional ou invocação de função recursiva.
- As funções definidas pelo utilizador devem ser declaradas em blocos especiais
load
e não podem nidificar. - A última atribuição de registo é automaticamente utilizada como o valor de uma função.
- A
NDLNetworkBuilder
versão linguística não é turing-completa.
NDLNetworkBuilder
não deve ser usado mais.