你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn

BrainScript 表达式

本部分是 BrainScript 表达式的规范,尽管我们有意使用非正式语言来使其保持可读性和可访问性。 其对应项是 BrainScript 函数定义语法的规范, 可在此处找到。

每个大脑脚本都是一个表达式,而表达式又由分配给记录成员变量的表达式组成。 网络说明的最外部级别是隐含的记录表达式。 BrainScript 具有以下类型的表达式:

  • 文本(如数字和字符串)
  • 类似于数学的内缀和一元运算,例如 a + b
  • 三元条件表达式
  • 函数调用
  • 记录、记录成员访问权限
  • arrays、array-element accesses
  • 函数表达式 (lambda)
  • 内置 C++ 对象构造

我们有意将其中每种语言的语法保留为尽可能接近常用语言,因此您在下面找到的大部分内容将非常熟悉。

概念

在描述各个表达式类型之前,请先介绍一些基本概念。

即时计算与延迟计算

BrainScript 知道两种值: 立即延迟。 在处理 BrainScript 期间会计算即时值,而延迟值是表示计算网络中节点的对象。 计算网络描述了在训练和使用模型期间由CNTK执行引擎执行的实际计算。

BrainScript 中的即时值旨在参数化计算。 它们表示张量维度、网络层数、从中加载模型的路径名等。由于 BrainScript 变量不可变,即时值始终是常量。

延迟值源于大脑脚本的主要用途:描述计算网络。 计算网络可以被视为传递给训练或推理例程的函数,然后通过CNTK执行引擎执行网络函数。 因此,许多 BrainScript 表达式的结果是计算网络中计算节点,而不是实际值。 从 BrainScript 的角度来看,延迟值是表示网络节点类型的 ComputationNode C++ 对象。 例如,采用两个网络节点的总和会创建一个新的网络节点,该节点表示将两个节点用作输入的求和操作。

标量与矩阵与 Tensors

计算网络中的所有值都是数值 n 维数组,我们调用 张量n 表示 张量排名。 为输入和模型参数显式指定张量维度;由运算符自动推断。

计算的最常见数据类型( 矩阵)只是排名 2 的张量。 列向量是排名 1 的张量,而行向量是排名 2。 矩阵产品是神经网络中的常见操作。

张量始终是延迟值,即延迟计算图中的对象。 涉及矩阵或张量的任何操作都会成为计算图的一部分,并在训练和推理期间进行评估。 但是,在 BS 处理时间预先推断/检查张量 维度

标量可以是即时值或延迟值。 参数化计算网络本身(如张量维度)的标量必须是即时的,即在处理 BrainScript 时可理解。 延迟标量是维度 [1]的排名 1 张量。 它们是网络本身的一部分,包括可学习的标量参数,如自稳定器和常量。Log (Constant (1) + Exp(x))

动态键入

BrainScript 是一种具有极其简单类型系统的动态类型语言。 使用值时,在处理 BrainScript 期间检查类型。

即时值的类型为数字、布尔值、字符串、记录、数组、函数/lambda 或CNTK预定义的 C++ 类之一。 使用时检查其类型 (例如,CONDif语句的参数已验证为 aBoolean,数组元素访问要求对象成为数组) 。

所有延迟值都是张量。 张量维度是其类型的一部分,在处理 BrainScript 期间检查或推断。

即时标量和延迟张量之间的表达式必须显式转换为延迟 Constant()的标量。 例如,Softplus 非线性必须编写为 Log (Constant(1) + Exp (x))。 (计划在即将推出的 update.) 中删除此要求

表达式类型

文本

文本是数字、布尔值或字符串常量,正如预期的那样。 示例:

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

数值文本始终是双精度浮点。 BrainScript 中没有显式整数类型,尽管某些表达式(如数组索引)如果呈现的值不是整数,则失败并出现错误。

字符串文本可能使用单引号或双引号,但不能转义包含单引号和双引号 (的字符串中的其他字符,例如 "He'd say " + '"Yes!" in a jiffy.') 。 字符串文本可以跨越多行;例如:

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

Infix 和 Unary 操作

BrainScript 支持下面给出的运算符。 选择 BrainScript 运算符意味着人们预期来自常用语言的内容,除了 (元素型产品) 、 * (矩阵产品) 和元素操作的特殊广播语义之外.*

数字不合数运算符+-、、*/.*

  • +-* 应用于标量、矩阵和张量。
  • .* 表示元素明智的产品。 Python 用户的注意:这相当于 numpy 的 *
  • / 仅支持标量。 可以使用内置 Reciprocal(x) 计算元素-wise 1/x来编写元素除法。

布尔 Infix 运算符 &&||

它们分别表示布尔和 OR。

字符串串联 (+)

字符串与 +. 示例:BS.Networks.Load (dir + "/model.dnn")

比较运算符

六个比较运算符是<==>以及它们的否定>=!=以及<=它们。 它们可以按预期应用于所有即时值;其结果为布尔值。

若要将比较运算符应用于张量,必须改用内置函数,例如 Greater()

-一元!

它们分别表示否定和逻辑否定。 ! 目前只能用于标量。

元素操作广播语义

应用于矩阵/张量时,+-.*按元素方式应用。

所有元素操作都支持 广播语义。 广播意味着指定为 1 的任何维度将自动重复以匹配任何维度。

例如,可以直接将维度 [1 x N] 的行向量添加到维度 [M x N]矩阵中。 维度 1 将自动重复 M 。 此外,张量维度会自动填充 1 维度。 例如,允许添加矩阵维度[M][M x N]的列向量。 在这种情况下,将自动填充列向量维度以 [M x 1] 匹配矩阵的排名。

注意 Python 用户:与 numpy 不同,广播维度与左对齐。

Matrix-Product 运算符*

A * B 操作表示矩阵积。 它还可应用于稀疏矩阵,从而提高处理以单热向量表示的文本输入或标签的效率。 在CNTK中,矩阵积具有扩展的解释,允许它与排名 > 2 的张量一起使用。 例如,可以使用矩阵单独将排名 3 张量中的每个列相乘。

此处 详细介绍矩阵乘积及其张量扩展。

注意:若要与标量相乘,请使用元素明智乘积 .*

建议 Python 用户将numpy*运算符用于元素明智产品,而不是矩阵产品。 CNTK的*运算符对应于 numpy'sdot(),而CNTK的等效于 Python *numpy数组运算符是.*

条件运算符 if

BrainScript 中的条件是表达式,如 C++ ? 运算符。 BrainScript 语法是 if COND then TVAL else EVAL,其中 COND 必须是即时布尔表达式,并且表达式的结果为 TVAL true CONDEVAL 否则为 true。 表达式 if 可用于在同一 BrainScript 中实现多个类似的标志参数化配置,也可用于递归。

if (运算符仅适用于即时标量值。若要实现延迟对象的条件,请使用内置函数,该函数BS.Boolean.If()允许基于标志张量从两个张量之一中选择一个值。它具有 .If (cond, tval, eval))

函数调用

BrainScript 具有三种 函数:使用 C++ 实现的内置基元 () 、使用 BrainScript) 编写的库函数 (,以及用户定义的 (BrainScript) 。 内置函数的示例和 Sigmoid()MaxPooling()。 库和用户定义的函数在机械上相同,只是保存在不同的源文件中。 使用表单 f (arg1, arg2, ...)调用各种类型,类似于数学和常用语言。

某些函数接受可选参数。 可选参数作为命名参数传递,例如 f (arg1, arg2, option1=..., option2=...)

可以递归方式调用函数,例如:

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)

请注意运算符如何 if 用于结束递归。

层创建

函数可以创建整个层或模型,这些层或模型的行为也类似于函数。 根据惯例,创建具有可学习参数的层的函数使用大括号 { } 而不是括号 ( )。 你将遇到如下所示的表达式:

h = DenseLayer {1024} (v)

在这里,两个调用正在发挥作用。 第一个函数 DenseLayer{1024}调用是一个函数调用,用于创建函数对象,然后又应用于数据 (v)。 由于 DenseLayer{} 返回具有可学习参数的函数对象,因此它用于 { } 表示这一点。

记录和Record-Member访问

记录表达式是大括号包围的赋值。 例如:

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

此表达式定义一个记录,其中包含三个成员,xy以及f一个函数f。 在记录中,表达式只需按名称引用其他记录成员,就像在赋值y中访问上述一样x

但是,与多种语言不同,可以 按任意顺序声明记录条目。 例如 x ,可以在之后 y声明 。 这是为了方便定义循环网络。 可从任何其他记录成员的表达式访问任何记录成员。 这与 Python 不同;与 F# 类似 let rec。 禁止循环引用,但特殊例外PastValue()FutureValue()

当记录嵌套 (其他记录中使用的记录表达式) 时,记录成员将在整个封闭范围层次结构中查找。 事实上, 每个 变量赋值都是记录的一部分:BrainScript 的外部级别也是隐含的记录。 在上面的示例中, factorParameter 必须作为封闭范围的记录成员进行分配。

在记录内分配的函数将捕获他们引用的记录成员。 例如,将捕获,f()而后者又依赖于x外部定义factorParametery 捕获这些意味着 f() 可以将 lambda 作为 lambda 传递到不包含 factorParameter 或有权访问它的外部范围。

在外部,使用 . 运算符访问记录成员。 例如,如果已将上述记录表达式分配给变量 r,则 r.x 生成值 13。 运算符 . 不遍历封闭范围: r.factorParameter 失败并出现错误。

(请注意,直到CNTK 1.6,而不是大括号{ ... },记录使用方括号[ ... ]。仍允许此操作,但已弃用。)

数组和数组访问

BrainScript 具有一维数组类型,用于即时值, (不会与张量混淆) 。 数组使用 [index]. 多维数组可以模拟为数组数组。

可以使用运算符声明至少 2 个元素的 : 数组。 例如,以下声明一个名为 3 维数组 imageDims ,然后传递给 ParameterTensor{} 声明排名 3 参数张量:

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

还可以声明其值相互引用的数组。 为此,必须使用涉及的数组赋值语法:

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

该数组构造一 arr 个名为低索引边界 i0 和上限索引绑定 i1的数组, i 表示变量以表示 初始值设定项表达式f(i)中的索引变量,后者又表示值 arr[i]。 数组的值计算得很迟缓。 这允许特定索引 i 的初始值设定项表达式访问同一数组的其他元素 arr[j] ,前提是没有循环依赖项。 例如,这可用于声明网络层堆栈:

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

与我们之前引入的递归版本不同,此版本通过说来 layers[i]保留对每个各个层的访问。

或者,还有一个表达式语法 array[i0..i1] (i => f(i)),这不太方便,但有时很有用。 上述内容如下所示:

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

注意:目前无法声明 0 个元素的数组。 这将在CNTK的未来版本中解决此问题。

函数表达式和 Lambda

在 BrainScript 中,函数是值。 可以将命名函数分配给变量并作为参数传递,例如:

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

其中 Sigmoid 作为在内部 Layer()使用的函数传递。 或者,类似于 C# 的 lambda 语法 (x => f(x)) 允许内联创建匿名函数。 例如,这定义了具有 Softplus 激活的网络层:

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

lambda 语法目前仅限于具有单个参数的函数。

层模式

上面的 Layer() 示例结合了参数创建和函数应用程序。 首选模式是将这些模式分为两个步骤:

  • 创建参数并返回保存这些参数的函数对象
  • 创建将参数应用于输入的函数

具体而言,后者也是函数对象的成员。 上述示例可以重写为:

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

并将调用为:

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

此模式的原因是,典型的网络类型包括将一个函数一个接一个应用到输入,这可以更轻松地使用 Sequential() 函数编写。

CNTK附带了一组丰富的预定义层,此处进行了介绍

构造内置 C++ CNTK对象

最终,所有 BrainScript 值都是 C++ 对象。 特殊的 BrainScript 运算符new用于与基础 CNTK C++ 对象相交。 它有一个表单 new TYPE ARGRECORD ,其中 TYPE 一组硬编码的预定义 C++ 对象公开给 BrainScript,并且 ARGRECORD 是传递给 C++ 构造函数的记录表达式。

如果使用括号形式(例如BrainScriptNetworkBuilder = (new ComputationNetwork { ... })如此处所述),则可能只能看到此窗体BrainScriptNetworkBuilder。 但现在,你知道它的含义:new ComputationNetwork创建一个新的 C++ 对象,ComputationNetwork其中{ ... }只需定义传递给内部 ComputationNetwork C++ 对象的 C++ 构造函数的记录,该记录将查找 5 个特定成员featureNodes、成员、labelNodes以及outputNodescriterionNodesevaluationNodes此处的说明

在后台,所有内置函数都是构造 CNTK C++ 类ComputationNode的对象的真正new表达式。 有关插图,请参阅内置如何 Tanh() 实际定义为创建 C++ 对象:

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

表达式计算语义

首次使用时计算 BrainScript 表达式。 由于 BrainScript 的主要用途是描述网络,因此表达式的值通常是计算图中用于延迟计算的节点。 例如,从 BrainScript 角度,在上面的示例中, W1 * r + b1 “计算”对象 ComputationNode 而不是数值;而涉及的实际数值将由图形执行引擎计算。 只有标量 (的 BrainScript 表达式,例如 28*28 ,在分析 BrainScript 时,) 才会“计算”。 从未使用过 (的表达式,例如,由于条件) 永远不会计算 (,也不会检查类型错误) 。

表达式的常见使用模式

下面是与 BrainScript 一起使用的一些常见模式。

函数的命名空间

通过将函数赋值分组到记录中,可以实现一种名称格式。 例如:

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

本地范围的变量

有时,对于更复杂的表达式,需要有本地范围的变量和/或函数。 这可以通过将整个表达式括在一条记录中,并立即访问其结果值来实现。 例如:

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

将创建包含立即读出的成员 y 的“临时”记录。此记录为“临时”,因为它未分配给变量,因此其成员不可访问,但除外 y

此模式通常用于使具有内置参数的 NN 层更具可读性,例如:

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

在这里, h 可以考虑此函数的“返回值”。

下一步:了解如何 定义 BrainScript 函数

NDLNetworkBuilder (弃用的)

早期版本的CNTK使用现已弃用NDLNetworkBuilder,而不是 BrainScriptNetworkBuilderNDLNetworkBuilder 实现了大量减少的 BrainScript 版本。 它具有以下限制:

  • 没有不包含任何语法。 必须通过函数调用调用调用所有运算符。 例如, Plus (Times (W1, r), b1) 而不是 W1 * r + b1
  • 没有嵌套的记录表达式。 只有一个隐含的外部记录。
  • 无条件表达式或递归函数调用。
  • 用户定义的函数必须在特殊 load 块中声明,并且不能嵌套。
  • 最后一个记录赋值自动用作函数的值。
  • 语言 NDLNetworkBuilder 版本未完成。

NDLNetworkBuilder 不应再使用。