Выражения BrainScript
Этот раздел является спецификацией выражений BrainScript, хотя мы намеренно используем неофициальный язык, чтобы обеспечить его читаемую и доступную. Его аналогом является спецификация синтаксиса определения функции BrainScript, который можно найти здесь.
Каждый скрипт мозга — это выражение, которое, в свою очередь, состоит из выражений, назначенных переменным члена записи. Самый внешний уровень описания сети — это подразумеваемое выражение записи. BrainScript имеет следующие виды выражений:
- литералы, такие как числа и строки
- математические и унарные операции, такие как
a + b
- тернарное условное выражение
- вызовы функций
- записи, доступ к записи-члену
- массивы, доступ к элементам массива
- выражения функций (лямбда-выражения)
- встроенное построение объектов C++
Мы намеренно сохранили синтаксис для каждого из этих языков как можно ближе к популярным языкам, так что многое из того, что вы найдете ниже, будет выглядеть очень знакомо.
Концепции
Прежде чем описывать отдельные виды выражений, сначала некоторые основные понятия.
Немедленное и отложенное вычисление
BrainScript знает два типа значений: немедленные и отложенные. Непосредственные значения вычисляются во время обработки BrainScript, а отложенные значения — это объекты, представляющие узлы в вычислительной сети. В вычислительной сети описываются фактические вычисления, выполняемые подсистемой выполнения CNTK во время обучения и использования модели.
Непосредственные значения в BrainScript предназначены для параметризации вычислений. Они указывают тензорные измерения, количество сетевых слоев, имя пути для загрузки модели и т. д. Так как переменные BrainScript неизменяемы, непосредственные значения всегда являются константами.
Отложенные значения возникают из основной цели скриптов мозга: для описания вычислительной сети. Вычислительная сеть может рассматриваться как функция, передаваемая в подпрограмму обучения или вывода, которая затем выполняет сетевую функцию через подсистему выполнения CNTK. Таким образом, результатом многих выражений BrainScript является вычислительный узел в вычислительной сети, а не фактическое значение. С точки зрения BrainScript отложенное значение — это объект C++ типа ComputationNode
, представляющий сетевой узел. Например, при принятии суммы двух сетевых узлов создается новый сетевой узел, представляющий операцию суммирования, которая принимает два узла в качестве входных данных.
Скаляры против Матрицы и Тензоры
Все значения в вычислительной сети — числовые n-мерные массивы, которые мы называем тензорами, а n обозначает тензор ранг. Измерения Tensor явно указаны для входных и параметров модели; и автоматически выводится операторами.
Наиболее распространенный тип данных для вычислений, матрицы, являются только тензорами ранга 2. Векторы столбцов — это тензоры ранга 1, а векторы строк — 2. Матричный продукт — это распространенная операция в нейронных сетях.
Tensors всегда являются отложенными значениями, то есть объектами в отложенном графе вычислений. Любая операция, которая включает матрицу или тензор, становится частью графа вычислений и оценивается во время обучения и вывода. Тензор измерения, однако, выводятся или проверяются заранее во время обработки BS.
Скаляры могут быть либо непосредственными, либо отложенными значениями. Скаляры, параметризующие сам вычислительный сети, такие как тензорные измерения, должны быть немедленной, т. е. сомнительны во время обработки BrainScript. Отложенные скаляры — это тензоры ранг-1 [1]
измерения. Они являются частью самой сети, включая обучаемые скалярные параметры, такие как самостабилизаторы, и константы, как в Log (Constant (1) + Exp(x))
.
Динамическое ввод
BrainScript — это динамически типизированный язык с чрезвычайно простой системой типов. Типы проверяются во время обработки BrainScript при использовании значения.
Непосредственные значения : число типа, логическое значение, строка, запись, массив, функция или лямбда-код или один из предопределенных классов CNTK. Их типы проверяются во время использования (например, аргумент COND
инструкции if
проверяется как 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 выбираются для того, чтобы означать, что следует ожидать от популярных языков, за исключением .*
(элементно-мудрый продукт), *
(матричный продукт) и специальная семантика вещания операций, мудрых элементов.
Числовые операторы Infix+
, -
, *
, /
, .*
-
+
,-
и*
применяются к скалярным, матрицам и тензорам. -
.*
обозначает продукт, мудрый элементом. Примечание для пользователей Python: это эквивалент*
numpy. -
/
поддерживается только для скалярных. Деление с помощью встроенныхReciprocal(x)
вычисляет1/x
с помощью элементов.
Логические операторы Infix &&
, ||
Они указывают логическое значение AND и 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
dot()
, а оператор CNTK *
Python для массивов numpy
.*
.
Условный оператор if
Условные выражения в BrainScript — это выражения, такие как оператор ?
C++. Синтаксис BrainScript if COND then TVAL else EVAL
, где COND
должен быть непосредственным логическим выражением, и результат выражения TVAL
, если COND
имеет значение true, и EVAL
в противном случае. Выражение 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 Access
Выражения записей — это назначения, окруженные фигурными скобками. Например:
{
x = 13
y = x * factorParameter
f (z) = y + z
}
Это выражение определяет запись с тремя элементами, x
, y
и f
, где f
является функцией.
Внутри записи выражения могут ссылаться на другие элементы записи только по имени, например x
доступ к ней осуществляется выше в назначении y
.
В отличие от многих языков, записи можно объявлять в любом порядке. Например, x
можно объявить после y
. Это позволяет упростить определение повторяющихся сетей. Любой элемент записи доступен из любого выражения другого элемента записи. Это отличается от, скажем, Python; и аналогично let rec
F#. Циклические ссылки запрещены, за исключением PastValue()
и FutureValue()
операций.
Когда записи вложены (выражения записей, используемые внутри других записей), элементы записей просматриваются по всей иерархии вложенных областей. Фактически, каждое назначение переменных является частью записи: внешний уровень BrainScript также является подразумеваемой записью. В приведенном выше примере factorParameter
необходимо назначить в качестве члена записи в заключенной области.
Функции, назначенные внутри записи, будут записывать элементы записи, которые они ссылаются. Например, f()
будет записывать y
, которые, в свою очередь, зависят от x
и внешних определенных factorParameter
. Захват этих средств означает, что f()
можно передать как лямбда-лямбда-область в внешние области, которые не содержат factorParameter
или имеют к нему доступ.
Доступ к элементам записи извне выполняется с помощью оператора .
. Например, если мы назначили приведенное выше выражение записи переменной r
, то r.x
даст значение 13
. Оператор .
не проходит по вложенным областям: r.factorParameter
завершится ошибкой.
(Обратите внимание, что до CNTK 1.6 вместо фигурных скобок { ... }
, записи использовали скобки [ ... ]
. Это по-прежнему разрешено, но не рекомендуется.)
Массивы и доступ к массивам
BrainScript имеет одномерный тип массива для немедленных значений (не следует путать с тензорами). Массивы индексируются с помощью [index]
. Многомерные массивы можно эмулировать как массивы массивов.
Массивы не менее 2 элементов можно объявить с помощью оператора :
. Например, в следующем случае объявляется трехмерный массив с именем imageDims
, который затем передается в ParameterTensor{}
для объявления тензора параметра ранг-3:
imageDims = (256 : 256 : 3)
inputFilter = ParameterTensor {imageDims}
Кроме того, можно объявить массивы, значения которых ссылаются друг на друга. Для этого необходимо использовать несколько более задействованный синтаксис назначения массива:
arr[i:i0..i1] = f(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)))
В настоящее время лямбда-синтаксис ограничен функциями с одним параметром.
Шаблон слоя
Приведенный выше 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
, т. е. BrainScriptNetworkBuilder = (new ComputationNetwork { ... })
, как описано здесь.
Но теперь вы знаете, что это означает: new ComputationNetwork
создает новый объект C++ типа ComputationNetwork
, где { ... }
просто определяет запись, передаваемую конструктору C++ внутреннего объекта ComputationNetwork
C++, который затем будет искать 5 определенных элементов featureNodes
, labelNodes
, criterionNodes
, evaluationNodes
и outputNodes
, как описано здесь.
Под капотом все встроенные функции действительно new
выражения, которые создают объекты класса CNTK C++ ComputationNode
. На иллюстрации см. сведения о том, как Tanh()
встроенный объект на самом деле определен как создание объекта C++:
Tanh (z, tag=') = new ComputationNode { operation = 'Tanh' ; inputs = z /плюс функция 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. У него были следующие ограничения:
- Нет синтаксиса infix. Все операторы должны вызываться с помощью вызовов функций. Например,
Plus (Times (W1, r), b1)
вместоW1 * r + b1
. - Не вложенные выражения записей. Существует только одна подразумеваемая внешняя запись.
- Не вызывается условное выражение или рекурсивное вызов функции.
- Определяемые пользователем функции должны быть объявлены в специальных
load
блоках и не могут вложены. - Последнее назначение записи автоматически используется в качестве значения функции.
- Версия языка
NDLNetworkBuilder
не завершена.
NDLNetworkBuilder
больше не следует использовать.