你当前正在访问 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++ 类之一。 使用时检查其类型 (例如,COND
if
语句的参数已验证为 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)
计算元素-wise1/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 COND
, EVAL
否则为 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
}
此表达式定义一个记录,其中包含三个成员,x
y
以及f
一个函数f
。
在记录中,表达式只需按名称引用其他记录成员,就像在赋值y
中访问上述一样x
。
但是,与多种语言不同,可以 按任意顺序声明记录条目。 例如 x
,可以在之后 y
声明 。 这是为了方便定义循环网络。 可从任何其他记录成员的表达式访问任何记录成员。 这与 Python 不同;与 F# 类似 let rec
。 禁止循环引用,但特殊例外PastValue()
。FutureValue()
当记录嵌套 (其他记录中使用的记录表达式) 时,记录成员将在整个封闭范围层次结构中查找。 事实上, 每个 变量赋值都是记录的一部分:BrainScript 的外部级别也是隐含的记录。 在上面的示例中, factorParameter
必须作为封闭范围的记录成员进行分配。
在记录内分配的函数将捕获他们引用的记录成员。 例如,将捕获,f()
而后者又依赖于x
外部定义factorParameter
。y
捕获这些意味着 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
以及outputNodes
criterionNodes
evaluationNodes
此处的说明。
在后台,所有内置函数都是构造 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
,而不是 BrainScriptNetworkBuilder
。 NDLNetworkBuilder
实现了大量减少的 BrainScript 版本。 它具有以下限制:
- 没有不包含任何语法。 必须通过函数调用调用调用所有运算符。 例如,
Plus (Times (W1, r), b1)
而不是W1 * r + b1
。 - 没有嵌套的记录表达式。 只有一个隐含的外部记录。
- 无条件表达式或递归函数调用。
- 用户定义的函数必须在特殊
load
块中声明,并且不能嵌套。 - 最后一个记录赋值自动用作函数的值。
- 语言
NDLNetworkBuilder
版本未完成。
NDLNetworkBuilder
不应再使用。