Compartir a través de


Expresiones brainScript

Esta sección es la especificación de expresiones BrainScript, aunque usamos intencionadamente lenguaje informal para que sea legible y accesible. Su homólogo es la especificación de la sintaxis de definición de función de BrainScript, que se puede encontrar aquí.

Cada script de cerebro es una expresión, que a su vez consta de expresiones asignadas a variables miembro de registro. El nivel más externo de una descripción de red es una expresión de registro implícita. BrainScript tiene los siguientes tipos de expresiones:

  • literales como números y cadenas
  • Operaciones de infijo y unaria similares a matemáticas, como a + b
  • una expresión condicional ternaria
  • invocaciones de función
  • registros, accesos a miembros de registro
  • matrices, accesos a elementos de matriz
  • expresiones de función (lambdas)
  • Construcción integrada de objetos de C++

Hemos mantenido intencionadamente la sintaxis de cada uno de ellos lo más cerca posible de los lenguajes populares, por lo que gran parte de lo que encuentras a continuación será muy familiar.

Conceptos

Antes de describir los tipos individuales de expresión, primero algunos conceptos básicos.

Cálculo inmediato frente a aplazado

BrainScript conoce dos tipos de valores: inmediatos y diferidos. Los valores inmediatos se calculan durante el procesamiento de BrainScript, mientras que los valores diferidos son objetos que representan nodos en la red de cálculo. La red de cálculo describe el cálculo real realizado por el motor de ejecución CNTK durante el entrenamiento y el uso del modelo.

Los valores inmediatos de BrainScript están diseñados para parametrizar el cálculo. Denotan dimensiones tensor, el número de capas de red, un nombre de ruta de acceso desde el que cargar un modelo, etc. Dado que las variables brainScript son inmutables, los valores inmediatos siempre son constantes.

Los valores diferidos surgen del propósito principal de los scripts cerebrales: para describir la red de cálculo. La red de cálculo se puede ver como una función que se pasa a la rutina de entrenamiento o inferencia, que luego ejecuta la función de red a través del motor de ejecución de CNTK. Por lo tanto, el resultado de muchas expresiones BrainScript es un nodo de cálculo en una red de cálculo, en lugar de un valor real. Desde el punto de vista de BrainScript, un valor diferido es un objeto de C++ de tipo ComputationNode que representa un nodo de red. Por ejemplo, al tomar la suma de dos nodos de red, se crea un nuevo nodo de red que representa la operación de suma que toma los dos nodos como entradas.

Escalares frente a matrices frente a tensores

Todos los valores de la red de cálculo son matrices numéricas n dimensionales que llamamos tensores y n denota el rango de tensor. Las dimensiones tensor se especifican explícitamente para las entradas y los parámetros del modelo; y deducen automáticamente los operadores.

El tipo de datos más común para el cálculo, las matrices, son solo tensores de rango 2. Los vectores de columna son tensores de rango 1, mientras que los vectores de fila son 2. El producto de matriz es una operación común en redes neuronales.

Los tensores siempre son valores aplazados, es decir, objetos en el gráfico de cálculo diferido. Cualquier operación que implique una matriz o tensor se convierte en parte del gráfico de cálculo y se evalúa durante el entrenamiento y la inferencia. Sin embargo, las dimensiones tensor se deducen o comprueban por adelantado en el tiempo de procesamiento de BS.

Los escalares pueden ser valores inmediatos o diferidos. Los escalares que parametrizan la propia red de cálculo, como las dimensiones de tensor, deben ser inmediatos, es decir, computables en el momento del procesamiento del BrainScript. Los escalares diferidos son tensores rank-1 de dimensión [1]. Forman parte de la propia red, incluidos parámetros escalares aprendibles, como auto-estabilizadores, y constantes como en Log (Constant (1) + Exp(x)).

Escritura dinámica

BrainScript es un lenguaje de tipo dinámico con un sistema de tipos extremadamente simple. Los tipos se comprueban durante el procesamiento de BrainScript cuando se usa un valor.

Los valores inmediatos son de tipo number, Boolean, string, record, array, function/lambda o una de las clases predefinidas de C++ de CNTK. Sus tipos se comprueban en el momento de su uso (por ejemplo, se comprueba que el COND argumento de la if instrucción es , Booleany un acceso de elemento de matriz requiere que el objeto sea una matriz).

Todos los valores diferidos son tensores. Las dimensiones tensor forman parte de su tipo, que se comprueban o deducen durante el procesamiento de BrainScript.

Las expresiones entre un escalar inmediato y un tensor diferido deben convertir explícitamente el escalar en un aplazado Constant(). Por ejemplo, la no linealidad de Softplus debe escribirse como Log (Constant(1) + Exp (x)). (Está previsto quitar este requisito en una próxima actualización).

Tipos de expresión

Literales

Los literales son constantes numéricas, booleanas o de cadena, como cabría esperar. Ejemplos:

  • 13, 42, 3.1415926538, 1e30
  • true, false
  • "my_model.dnn", 'single quotes'

Los literales numéricos siempre son punto flotante de precisión doble. No hay ningún tipo entero explícito en BrainScript, aunque algunas expresiones como índices de matriz producirán un error si se presentan valores que no son enteros.

Los literales de cadena pueden usar comillas simples o dobles, pero no tienen ninguna manera de escapar comillas u otros caracteres dentro (la cadena que contiene comillas simples y dobles debe calcularse, por ejemplo "He'd say " + '"Yes!" in a jiffy.', ). Los literales de cadena pueden abarcar varias líneas; por ejemplo:

I3 = Parameter (3, 3, init='fromLiteral', initFromLiteral = '1 0 0
                                                             0 1 0
                                                             0 0 1')

Infix y Unary Operations

BrainScript admite los operadores que se indican a continuación. Los operadores brainScript se eligen para significar lo que cabría esperar de los lenguajes populares, con la excepción de .* (producto de elemento), * (producto de matriz) y semántica de difusión especial de operaciones de elementos.

Operadores de +infijo numéricos, -, *, , /.*

  • +, -y * se aplican a escalares, matrices y tensores.
  • .* denota un producto orientado a elementos. Nota para los usuarios de Python: este es el equivalente de numpy.*
  • / solo se admite para escalares. Una división por elementos se puede escribir mediante procesos integrados Reciprocal(x) de un elemento.1/x

Operadores &&de infijo booleano , ||

Estos denotan AND booleano y OR, respectivamente.

Concatenación de cadenas (+)

Las cadenas se concatenan con +. Ejemplo: BS.Networks.Load (dir + "/model.dnn").

Operadores de comparación

Los seis operadores de comparación son <, ==, >y sus negaciones >=, !=, <=. Se pueden aplicar a todos los valores inmediatos según lo previsto; su resultado es un valor booleano.

Para aplicar operadores de comparación a tensores, debe usar funciones integradas en su lugar, como Greater().

Unario-, !

Estas denotan negación y negación lógica, respectivamente. ! actualmente solo se puede usar para escalares.

Semántica de operaciones y difusión en elementos

Cuando se aplica a matrices/tensores, +, -y .* se aplican en cuanto a elementos.

Todas las operaciones en cuanto a elementos admiten la semántica de difusión. La difusión significa que cualquier dimensión especificada como 1 se repetirá automáticamente para que coincida con cualquier dimensión.

Por ejemplo, un vector de fila de dimensión [1 x N] se puede agregar directamente a una matriz de dimensión [M x N]. La 1 dimensión repetirá M automáticamente los tiempos. Además, las dimensiones tensor se rellenan automáticamente con 1 dimensiones. Por ejemplo, se permite agregar un vector de columna de una [M x N] matriz de dimensión[M]. En este caso, las dimensiones del vector de columna se rellenan automáticamente para que [M x 1] coincidan con el rango de la matriz.

Nota para los usuarios de Python: A diferencia de numpy, las dimensiones de difusión están alineadas a la izquierda.

Operador matrix-product*

La A * B operación indica el producto de matriz. También se puede aplicar a matrices dispersas, lo que mejora la eficacia para controlar las entradas de texto o las etiquetas que se representan como vectores de un solo uso. En CNTK, el producto de matriz tiene una interpretación extendida que permite su uso con tensores de rango > 2. Por ejemplo, es posible multiplicar cada columna de un tensor rank-3 individualmente con una matriz.

El producto de matriz y su extensión tensor se describen en detalle aquí.

Nota: Para multiplicar con un escalar, use el producto .*en función del elemento .

Se recomienda a los usuarios de Python que numpy usen el * operador para el producto en función del elemento , no para el producto de matriz. * CNTK operador corresponde a numpydot(), mientras que CNTK equivalente al operador de * Python para numpy matrices es .*.

Operador condicional if

Los condicionales de BrainScript son expresiones, como el operador de C++ ? . La sintaxis de BrainScript es if COND then TVAL else EVAL, donde COND debe ser una expresión booleana inmediata y el resultado de la expresión es si COND es TVAL true yEVAL, de lo contrario, . La if expresión es útil para implementar varias configuraciones con parámetros de marca similares en el mismo BrainScript y también para la recursividad.

(El if operador solo funciona para valores escalares inmediatos. Para implementar condicionales para objetos diferidos, use la función BS.Boolean.If()integrada , que permite seleccionar un valor de uno de dos tensores basados en un tensor de marca. Tiene el formato If (cond, tval, eval).

Invocaciones de función

BrainScript tiene tres tipos de funciones: primitivos integrados (con implementaciones de C++), funciones de biblioteca (escritas en BrainScript) y definidas por el usuario (BrainScript). Algunos ejemplos de funciones integradas son Sigmoid() y MaxPooling(). Las funciones definidas por el usuario y biblioteca son mecánicamente iguales, simplemente guardadas en archivos de código fuente diferentes. Se invocan todos los tipos, de forma similar a los lenguajes matemáticos y comunes, con el formato f (arg1, arg2, ...).

Algunas funciones aceptan parámetros opcionales. Los parámetros opcionales se pasan como parámetros con nombre, por ejemplo f (arg1, arg2, option1=..., option2=...).

Las funciones se pueden invocar de forma recursiva, por ejemplo:

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)

Observe cómo se usa el if operador para finalizar la recursividad.

Creación de capas

Las funciones también pueden crear capas o modelos completos que son objetos de función que también se comportan como funciones. Por convención, una función que crea una capa con parámetros aprendices usa llaves { } en lugar de paréntesis ( ). Encontrará expresiones como esta:

h = DenseLayer {1024} (v)

Aquí, dos invocaciones están en juego. La primera, DenseLayer{1024}, es una llamada de función que crea un objeto de función, que a su vez se aplica a los datos (v). Dado que DenseLayer{} devuelve un objeto de función con parámetros aprendibles, usa { } para indicar esto.

Registros y acceso Record-Member

Las expresiones de registro son asignaciones rodeadas de llaves. Por ejemplo:

{
    x = 13
    y = x * factorParameter
    f (z) = y + z
}

Esta expresión define un registro con tres miembros, x, yy f, donde f es una función. Dentro del registro, las expresiones pueden hacer referencia a otros miembros de registro solo por su nombre, como x se accede anteriormente en la asignación de y.

A diferencia de muchos idiomas, sin embargo, las entradas de registro se pueden declarar en cualquier orden. Por ejemplo, x podría declararse después yde . Esto es para facilitar la definición de redes recurrentes. Cualquier miembro de registro es accesible desde cualquier otra expresión del miembro de registro. Esto es diferente de, por ejemplo, Python; y similar a F#' s let rec. Las referencias cíclicas están prohibidas, con la excepción especial de las PastValue() operaciones y FutureValue() .

Cuando los registros se anidan (expresiones de registro usadas dentro de otros registros), los miembros del registro se buscan en toda la jerarquía de ámbitos envolventes. De hecho, cada asignación de variable forma parte de un registro: el nivel externo de un BrainScript también es un registro implícito. En el ejemplo anterior, factorParameter tendría que asignarse como miembro de registro de un ámbito envolvente.

Las funciones asignadas dentro de un registro capturarán los miembros de registro a los que hacen referencia. Por ejemplo, f() capturará y, que a su vez depende x de y del definido factorParameterexternamente. La captura de estos significa que f() se puede pasar como una expresión lambda a ámbitos externos que no contienen factorParameter o tienen acceso a él.

Desde fuera, se accede a los miembros del registro mediante el . operador . Por ejemplo, si se hubiera asignado la expresión de registro anterior a una variable r, r.x se produciría el valor 13. El . operador no atraviesa ámbitos envolventes: r.factorParameter produciría un error.

(Tenga en cuenta que hasta CNTK 1.6, en lugar de llaves { ... }, los registros usan corchetes [ ... ]. Esto todavía está permitido, pero está en desuso).

Acceso a matrices y matrices

BrainScript tiene un tipo de matriz unidimensional para valores inmediatos (no confundirse con tensores). Las matrices se indexan mediante [index]. Las matrices multidimensionales se pueden emular como matrices de matrices.

Las matrices de al menos 2 elementos se pueden declarar mediante el : operador . Por ejemplo, lo siguiente declara una matriz de 3 dimensiones denominada imageDims que, a continuación, se pasa a ParameterTensor{} para declarar un tensor de parámetro rank-3:

imageDims = (256 : 256 : 3)
inputFilter = ParameterTensor {imageDims}

También es posible declarar matrices cuyos valores hacen referencia entre sí. Para ello, debe usar la sintaxis de asignación de matriz algo más implicada:

arr[i:i0..i1] = f(i)

que construye una matriz denominada arr con límite de índice inferior y límite i0i1de índice superior , i que indica la variable para indicar la variable de índice en la expresiónf(i) de inicializador, que a su vez denota el valor de arr[i]. Los valores de la matriz se evalúan diferidamente. Esto permite que la expresión del inicializador para un índice i específico tenga acceso a otros elementos arr[j] de la misma matriz, siempre y cuando no haya ninguna dependencia cíclica. Por ejemplo, esto se puede usar para declarar una pila de capas de red:

layers[l:1..L] =
    if l == 1
    then DNNLayer (x, hiddenDim, featDim)
    else DNNLayer (layers[l-1], hiddenDim, hiddenDim)

A diferencia de la versión recursiva de esta que se introdujo anteriormente, esta versión conserva el acceso a cada capa individual diciendo layers[i].

Como alternativa, también hay una sintaxis array[i0..i1] (i => f(i))de expresión , que es menos conveniente pero a veces útil. El aspecto de lo anterior debería ser parecido a este:

layers = array[1..L] (l =>
    if l == 1
    then DNNLayer (x, hiddenDim, featDim)
    else DNNLayer (layers[l-1], hiddenDim, hiddenDim)
)

Nota: Actualmente no hay ninguna manera de declarar una matriz de 0 elementos. Esto se abordará en una versión futura de CNTK.

Expresiones y expresiones lambda de Functions

En BrainScript, las funciones son valores. Una función con nombre se puede asignar a una variable y pasarla como argumento, por ejemplo:

Layer (x, m, n, f) = f (ParameterTensor {(m:n)} * x + ParameterTensor {n})
h = Layer (x, 512, 40, Sigmoid)

donde Sigmoid se pasa como una función que se usa dentro Layer()de . Como alternativa, una sintaxis (x => f(x)) lambda similar a C# permite crear funciones anónimas insertadas. Por ejemplo, esto define una capa de red con una activación de Softplus:

h = Layer (x, 512, 40, (x => Log (Constant(1) + Exp (x)))

La sintaxis lambda se limita actualmente a las funciones con un único parámetro.

Patrón de capa

En el ejemplo anterior Layer() se combinan la creación de parámetros y la aplicación de función. Un patrón preferido consiste en separarlos en dos pasos:

  • create parameters y devuelve un objeto de función que contiene estos parámetros.
  • cree la función que aplica los parámetros a una entrada.

En concreto, este último también es miembro del objeto de función. El ejemplo anterior podría reescribirse 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

y se invocarían como:

h = Layer {512, 40, Sigmoid} (x)

El motivo de este patrón es que los tipos de red típicos consisten en aplicar una función después de otra a una entrada, que se puede escribir más fácilmente mediante la Sequential() función .

CNTK viene con un amplio conjunto de capas predefinidas, que se describen aquí.

Construcción de objetos integrados de C++ CNTK

En última instancia, todos los valores de BrainScript son objetos de C++. El operador new BrainScript especial se usa para interactuar con los objetos subyacentes de CNTK C++. Tiene el formulario new TYPE ARGRECORD donde TYPE es uno de un conjunto codificado de forma rígida de los objetos de C++ predefinidos expuestos a BrainScript, y ARGRECORD es una expresión de registro que se pasa al constructor de C++.

Es probable que solo llegue a ver este formulario si usa el paréntesis de BrainScriptNetworkBuilder, es BrainScriptNetworkBuilder = (new ComputationNetwork { ... })decir, , como se describe aquí. Pero ahora sabe lo que significa: new ComputationNetwork crea un nuevo objeto de C++ de tipo ComputationNetwork, donde { ... } simplemente define un registro que se pasa al constructor de C++ del objeto de C++ internoComputationNetwork, que luego buscará cinco miembros featureNodesespecíficos , , labelNodescriterionNodes, evaluationNodesy outputNodes, como se explica aquí.

En segundo plano, todas las funciones integradas son expresiones que new construyen objetos de la clase ComputationNodeCNTK C++ . Para obtener una ilustración, vea cómo se define realmente el Tanh() elemento integrado como crear un objeto de C++:

Tanh (z, tag='') = new ComputationNode { operation = 'Tanh' ; inputs = z /plus the function args/ }

Semántica de evaluación de expresiones

Las expresiones brainScript se evalúan al usarse por primera vez. Dado que el propósito principal de BrainScript es describir la red, el valor de una expresión suele ser un nodo en un gráfico de cálculo para el cálculo diferido. Por ejemplo, desde el ángulo brainScript, W1 * r + b1 en el ejemplo anterior 'evaluates' a un ComputationNode objeto en lugar de un valor numérico; mientras que los valores numéricos reales implicados se calcularán mediante el motor de ejecución de grafos. Solo las expresiones BrainScript de los escalares (por ejemplo 28*28, ) se "calculan" en el momento en que se analiza BrainScript. Las expresiones que nunca se usan (por ejemplo, debido a una condición) nunca se evalúan (ni se comprueban si hay errores de tipo).

Patrones de uso comunes de expresiones

A continuación se muestran algunos patrones comunes que se usan con BrainScript.

Espacios de nombres para Functions

Mediante la agrupación de asignaciones de funciones en registros, se puede lograr una forma de espaciado de nombres. Por ejemplo:

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

Variables con ámbito local

A veces es conveniente tener variables con ámbito local o funciones para expresiones más complejas. Esto se puede lograr mediante la inclusión de toda la expresión en un registro y el acceso inmediato a su valor de resultado. Por ejemplo:

{ x = 13 ; y = x * x }.y

creará un registro "temporal" con un miembro y que se lee inmediatamente. Este registro es "temporal", ya que no está asignado a una variable y, por tanto, sus miembros no son accesibles excepto para y.

Este patrón se usa a menudo para hacer que las capas de NN con parámetros integrados sean más legibles, por ejemplo:

SigmoidLayer (m, n, x) = {
    W = Parameter (m, n, init='uniform')
    b = Parameter (m, 1, init='value', initValue=0)
    h = Sigmoid (W * x + b)
}.h

Aquí, h se puede considerar el "valor devuelto" de esta función.

Siguiente: Más información sobre cómo definir funciones de BrainScript

NDLNetworkBuilder (en desuso)

Las versiones anteriores de CNTK usaron ahora en desuso NDLNetworkBuilder en lugar de BrainScriptNetworkBuilder. NDLNetworkBuilder implementó una versión muy reducida de BrainScript. Tenía las siguientes restricciones:

  • Sintaxis de infijo. Todos los operadores se deben invocar a través de invocaciones de función. Por ejemplo, Plus (Times (W1, r), b1) en lugar de W1 * r + b1.
  • No hay expresiones de registro anidadas. Solo hay un registro externo implícito.
  • Ninguna invocación de función recursiva o expresión condicional.
  • Las funciones definidas por el usuario deben declararse en bloques especiales load y no pueden anidar.
  • La última asignación de registros se usa automáticamente como valor de una función.
  • La NDLNetworkBuilder versión del idioma no es Turing-complete.

NDLNetworkBuilder ya no debe usarse.