BrainScript 式
このセクションは BrainScript 式の仕様ですが、非公式の言語を意図的に使用して読みやすくアクセスできるようにします。 対応する関数定義構文は BrainScript 関数定義構文の仕様であり、ここで見 つけることができます。
すべての頭脳スクリプトは式であり、レコード メンバー変数に割り当てられた式で構成されます。 ネットワーク記述の最も外側のレベルは、暗黙的なレコード式です。 BrainScript には、次の種類の式があります。
- 数値や文字列などのリテラル
- 数学のようなインフィックスや単項演算など
a + b
- 三項条件式
- 関数の呼び出し
- レコード、レコード メンバー アクセス
- 配列、配列要素アクセス
- 関数式 (ラムダ)
- 組み込みの C++ オブジェクトの構築
これらの各構文は、一般的な言語にできるだけ近い形式に意図的に保持されているため、以下に示す内容の多くは非常によく知られています。
概念
個々の種類の式について説明する前に、まずいくつかの基本的な概念を説明します。
即時計算と遅延計算
BrainScript は、 即時 と 遅延の 2 種類の値を認識します。 即時値は BrainScript の処理中に計算されますが、遅延値は計算ネットワーク内のノードを表すオブジェクトです。 計算ネットワークは、モデルのトレーニングと使用中にCNTK実行エンジンによって実行される実際の計算を記述します。
BrainScript の即時値は、計算をパラメーター化するためのものです。 テンソル ディメンション、ネットワーク レイヤーの数、モデルを読み込むパス名などを示します。BrainScript 変数は不変であるため、即時値は常に定数です。
遅延値は、計算ネットワークを記述する脳スクリプトの主な目的から発生します。 計算ネットワークは、トレーニングルーチンまたは推論ルーチンに渡される関数と見なされ、CNTK実行エンジンを介してネットワーク関数が実行されます。 そのため、多くの BrainScript 式の結果は、実際の値ではなく、計算ネットワーク内の計算ノードになります。 BrainScript の観点から見ると、遅延値は、ネットワーク ノードを表す型 ComputationNode
の C++ オブジェクトです。 たとえば、2 つのネットワーク ノードの合計を取得すると、2 つのノードを入力として受け取る合計操作を表す新しいネットワーク ノードが作成されます。
スカラーとマトリックスとテンソル
計算ネットワーク内のすべての値は、テンソルと呼ぶ数値の n 次元配列であり、n はテンソルランクを表します。 テンソル ディメンションは、入力とモデル パラメーターに対して明示的に指定されます。演算子によって自動的に推論されます。
計算の最も一般的なデータ型である マトリックスは、ランク 2 のテンソルにすぎません。 列ベクトルはランク 1 のテンソルですが、行ベクトルはランク 2 です。 マトリックス積は、ニューラル ネットワークでの一般的な操作です。
テンソルは常に遅延値、つまり遅延計算グラフ内のオブジェクトです。 行列またはテンソルを含む演算は、計算グラフの一部になり、トレーニングと推論中に評価されます。 ただし、テンソル ディメンションはBS処理時間に先行して推論/チェックされます。
スカラーは、即時値または遅延値のいずれかになります。 テンソル ディメンションなど、計算ネットワーク自体をパラメーター化するスカラーは、即時である必要があります。つまり、BrainScript の処理時に計算可能である必要があります。 遅延スカラーは、次元 [1]
のランク 1 テンソルです。 これらはネットワーク自体の一部であり、自己スタビライザーなどの学習可能なスカラー パラメーターや、次のような定数が含まれます Log (Constant (1) + Exp(x))
。
動的な型指定
BrainScript は、非常に単純な型システムを持つ動的に型指定された言語です。 値を使用すると、BrainScript の処理中に型がチェックされます。
即時値は、型番号、ブール値、文字列、レコード、配列、関数/ラムダ、またはCNTKの定義済み C++ クラスのいずれかです。 使用時に型がチェックされます (たとえば、 COND
ステートメントの if
引数が a Boolean
であることが確認され、配列要素アクセスではオブジェクトが配列である必要があります)。
遅延値はすべてテンソルです。 テンソルディメンションは、BrainScript の処理中にチェックまたは推論される型の一部です。
即時スカラーと遅延テンソルの間の式は、スカラーを遅延 Constant()
型に明示的に変換する必要があります。 たとえば、Softplus の非線形性は次のように Log (Constant(1) + Exp (x))
記述する必要があります。 (今後の更新プログラムでは、この要件を削除する予定です)。
式の型
リテラル
リテラルは、予想どおり、数値、ブール、または文字列の定数です。 次に例を示します。
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')
インフィックス操作と単項演算
BrainScript では、以下に示す演算子がサポートされています。 BrainScript 演算子は、(要素ごとの積)、(行列積) *
、および要素ごとの操作の.*
特別なブロードキャスト セマンティクスを除き、一般的な言語から期待されることを意味するために選択されます。
数値のインフィックス演算子+
、 -
, , *
/
.*
+
、-
および*
スカラー、マトリックス、テンソルに適用されます。.*
は要素ごとの積を表します。 Python ユーザーの場合の注意: これは numpy と同等です*
。/
はスカラーに対してのみサポートされます。 要素ごとの除算は、組み込みのコンピューティングを使用して要素単位1/x
でReciprocal(x)
記述できます。
ブール型の Infix 演算子 &&
, ||
これらは、それぞれブール値 AND と OR を表します。
文字列連結 (+
)
文字列は連結されます +
。 例: BS.Networks.Load (dir + "/model.dnn")
.
比較演算子
6 つの比較演算子は<
、 , ==
, >
とその否定 >=
, !=
. <=
これらは、期待どおりにすべての即時値に適用できます。結果はブール値です。
テンソルに比較演算子を適用するには、代わりに組み込み関数 (例: Greater()
.
単項-
、 !
これらは、それぞれ否定と論理否定を示します。 !
は現在、スカラーにのみ使用できます。
要素ごとの操作 と ブロードキャスト セマンティクス
行列/テンソル、,に-
.*
適用すると、+
要素単位で適用されます。
すべての要素ごとの操作は 、ブロードキャスト セマンティクスをサポートします。 ブロードキャストとは、1 として指定されたディメンションが、任意のディメンションと一致するように自動的に繰り返されることを意味します。
たとえば、次元の行ベクトルは、次元[1 x N]
[M x N]
の行列に直接追加できます。 ディメンションは 1
自動的に繰り返されます M
。 さらに、テンソル寸法は寸法で 1
自動的に埋め込まれます。 たとえば、行列の次元[M]
[M x N]
の列ベクトルを追加できます。 この場合、列ベクトルの次元は、行列のランクと一致するように [M x 1]
自動的に埋め込まれます。
Python ユーザーへの注意: numpy とは異なり、ブロードキャストディメンションは左揃えです。
Matrix-Product 演算子*
この演算は A * B
マトリックス積を表します。 スパース マトリックスにも適用できます。これは、1 ホット ベクターとして表されるテキスト入力またはラベルを処理する効率を向上させます。 CNTKでは、マトリックス積には拡張解釈があり、ランク > 2 のテンソルで使用できます。 たとえば、rank-3 テンソルのすべての列にマトリックスを個別に乗算することができます。
マトリックス積とそのテンソル拡張機能について、 ここで詳しく説明します。
注: スカラーを乗算するには、要素ごとの積 .*
を使用します。
Python ユーザーは、行列積ではなく、要素ごとの積に演算子を使用*
することをお勧めしますnumpy
。 CNTKの*
演算子は 'sdot()
' にnumpy
対応しますが、CNTK配列の Python *
演算子numpy
と同等です.*
。
条件演算子 if
BrainScript の条件は、C++ 演算子のような式です ?
。 BrainScript 構文はif COND then TVAL else EVAL
、COND
即時のブール式である必要があり、式の結果が true EVAL
の場合、それ以外の場合COND
ですTVAL
。 この if
式は、同じ BrainScript で複数の同様のフラグパラメーター化された構成を実装する場合や再帰に役立ちます。
(演算子は if
、即時スカラー値に対してのみ機能します。遅延オブジェクトの条件を実装するには、組み込み関数 BS.Boolean.If()
を使用します。これにより、フラグ テンソルに基づいて 2 つのテンソルのいずれかから値を選択できます。これは、フォーム If (cond, tval, eval)
を持っています.)
関数の呼び出し
BrainScript には、組み込みのプリミティブ (C++ 実装を使用)、ライブラリ関数 (BrainScript で記述)、ユーザー定義 (BrainScript) の 3 種類の 関数があります。 組み込み関数の例は次 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)
ここでは、2 つの呼び出しが実行されています。 1 つ目は、 DenseLayer{1024}
関数オブジェクトを作成する関数呼び出しで、次にデータ (v)
に適用されます。
学習可能なパラメーターを持つ関数オブジェクトを返す DenseLayer{}
ので、これを表すために使用 { }
します。
レコードとRecord-Member アクセス
レコード式は、中かっこで囲まれた代入です。 次に例を示します。
{
x = 13
y = x * factorParameter
f (z) = y + z
}
この式は、関数である f
3 つのメンバー 、x
、y
および f
を持つレコードを定義します。
レコード内では、式は名前だけで他のレコード メンバーを参照できます。これは、 x
上記の y
割り当てでアクセスします。
ただし、多くの言語とは異なり、レコード エントリは任意の 順序で宣言できます。 たとえば、 x
次の後 y
に宣言できます。 これは、再発ネットワークの定義を容易にするためです。 レコード メンバーには、他のレコード メンバーの式からアクセスできます。 これは Python とは異なります。F#と同様です let rec
。 循環参照は禁止されており、特別な例外とFutureValue()
操作がありますPastValue()
。
レコードが入れ子になっている場合 (他のレコード内で使用されるレコード式)、レコード メンバーは、囲むスコープの階層全体を検索します。 実際、 すべての 変数代入はレコードの一部です。BrainScript の外部レベルも暗黙的なレコードです。 上記の例では、 factorParameter
外側のスコープのレコード メンバーとして割り当てる必要があります。
レコード内で割り当てられた関数は、参照するレコード メンバーをキャプチャします。 たとえば、f()
キャプチャします。これは、次に依存x
し、外部で定義されていますfactorParameter
y
。 これらのキャプチャは、ラムダとして、ラムダを含factorParameter
まない、またはアクセス権を持たない外部スコープに渡すことができることをf()
意味します。
外部から、レコード メンバーには演算子を .
使用してアクセスします。 たとえば、上記のレコード式を変数 r
に割り当てた場合、 r.x
値 13
が生成されます。 演算子は .
スコープを囲むスキャンを行いません。 r.factorParameter
エラーで失敗します。
(中かっこ{ ... }
の代わりに 1.6 CNTKまで、レコードでは角かっこが[ ... ]
使用されることに注意してください。これは引き続き許可されますが、非推奨です)。)
配列と配列のアクセス
BrainScript には、即時値用の 1 次元配列型があります (テンソルと混同しないでください)。 配列は、 [index]
. 多次元配列は、配列の配列としてエミュレートできます。
演算子を使用して、少なくとも 2 つの要素の配列を :
宣言できます。 たとえば、次の例では、という名前 imageDims
の 3 次元配列を宣言します。この配列は、rank-3 パラメーター テンソルを宣言するために渡されます ParameterTensor{}
。
imageDims = (256 : 256 : 3)
inputFilter = ParameterTensor {imageDims}
値が相互に参照される配列を宣言することもできます。 このためには、より複雑な配列代入構文を使用する必要があります。
arr[i:i0..i1] = f(i)
これは、より低いインデックスバインドと上限インデックスバインドi0
i1
を持つ配列arr
を構築し、 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で対処される予定です。
関数の式とラムダ
BrainScript では、関数は値です。 名前付き関数を変数に割り当て、引数として渡すことができます(例:
Layer (x, m, n, f) = f (ParameterTensor {(m:n)} * x + ParameterTensor {n})
h = Layer (x, 512, 40, Sigmoid)
ここで Sigmoid
内部で使用 Layer()
される関数として渡されます。 または、C#のようなラムダ構文 (x => f(x))
を使用すると、匿名関数をインラインで作成できます。 たとえば、これにより、Softplus ライセンス認証を使用するネットワーク レイヤーが定義されます。
h = Layer (x, 512, 40, (x => Log (Constant(1) + Exp (x)))
ラムダ構文は現在、1 つのパラメーターを持つ関数に制限されています。
レイヤー パターン
上記 Layer()
の例では、パラメーターの作成と関数アプリケーションを組み合わせています。
推奨されるパターンは、これらを 2 つの手順に分ける方法です。
- パラメーターを作成し、これらのパラメーターを保持する関数オブジェクトを返します
- 入力にパラメーターを適用する関数を作成する
具体的には、後者も関数オブジェクトのメンバーです。 上記の例は、次のように書き換えられます。
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()
より簡単に書き込むことができる入力に 1 つの関数を次々に適用することで構成されるためです。
CNTKには、定義済みのレイヤーの豊富なセットが付属しています。ここでは説明します。
組み込みの C++ CNTK オブジェクトの構築
最終的には、すべての BrainScript 値は C++ オブジェクトです。 特殊な BrainScript 演算子new
は、基になる CNTK C++ オブジェクトとのインターフェイスに使用されます。 この形式new TYPE ARGRECORD
TYPE
は、BrainScript に公開されている定義済みの C++ オブジェクトのハードコーディングされたセットの 1 つでありARGRECORD
、C++ コンストラクターに渡されるレコード式です。
ここで説明するように、かっこ形式 (つまりBrainScriptNetworkBuilder = (new ComputationNetwork { ... })
) を使用している場合にのみ、このフォームBrainScriptNetworkBuilder
が表示される可能性があります。
しかし、ここで new ComputationNetwork
説明するように、内部 ComputationNetwork
C++ オブジェクトの C++ コンストラクターに渡されるレコードを定義する、型ComputationNetwork
{ ... }
の新しい C++ オブジェクトを作成し、次に 5 つの特定のメンバー featureNodes
、labelNodes
、criterionNodes
、evaluationNodes
およびoutputNodes
を探します。
内部では、組み込み関数はすべて実際にはnew
、CNTK C++ クラスComputationNode
のオブジェクトを構築する式です。 図については、組み込み関数が Tanh()
実際に C++ オブジェクトの作成として定義されている方法を参照してください。
Tanh (z, tag='') = new ComputationNode { operation = 'Tanh' ; inputs = z /plus the function args/ }
式の評価セマンティクス
BrainScript 式は、最初の使用時に評価されます。 BrainScript の主な目的はネットワークを記述するため、多くの場合、式の値は遅延計算のための計算グラフ内のノードです。 たとえば、上記の例の BrainScript 角度 W1 * r + b1
から、数値ではなくオブジェクトに ComputationNode
評価されます。一方、関係する実際の数値はグラフ実行エンジンによって計算されます。 BrainScript が解析された時点では、スカラーの BrainScript 式 (例: 28*28
) のみが '計算' されます。 使用されない式 (条件など) は評価されません (型エラーのチェックも行いません)。
式の一般的な使用パターン
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では、次の代わりにBrainScriptNetworkBuilder
非推奨NDLNetworkBuilder
になりました。 NDLNetworkBuilder
は、大幅に削減されたバージョンの BrainScript を実装しました。 これには次の制限がありました。
- インフィックス構文はありません。 すべての演算子は、関数呼び出しを通じて呼び出す必要があります。 例:
Plus (Times (W1, r), b1)
W1 * r + b1
- 入れ子になったレコード式はありません。 暗黙的な外部レコードは 1 つだけです。
- 条件式または再帰関数の呼び出しはありません。
- ユーザー定義関数は特別な
load
ブロックで宣言する必要があり、入れ子にすることはできません。 - 最後のレコード割り当ては、関数の値として自動的に使用されます。
NDLNetworkBuilder
言語バージョンが Turing-complete ではありません。
NDLNetworkBuilder
はもう使用しないでください。