BrainScript Expressions
Cette section est la spécification des expressions BrainScript, bien que nous utilisions intentionnellement un langage informel pour le rendre lisible et accessible. Son équivalent est la spécification de la syntaxe de définition de fonction BrainScript, qui se trouve ici.
Chaque script de cerveau est une expression, qui se compose à son tour d’expressions affectées aux variables membres d’enregistrement. Le niveau le plus externe d’une description réseau est une expression d’enregistrement implicite. BrainScript a les types d’expressions suivants :
- littéraux tels que des nombres et des chaînes
- opérations de type mathématique et unaire telles que
a + b
- une expression conditionnelle ternaire
- appels de fonction
- enregistrements, accès aux membres d’enregistrement
- tableaux, accès à des éléments de tableau
- expressions de fonction (lambdas)
- Construction d’objets C++ intégrée
Nous avons intentionnellement conservé la syntaxe de chacun de ces langages aussi proches que possible des langages populaires, une grande partie de ce que vous trouvez ci-dessous sera très familière.
Concepts
Avant de décrire les différents types d’expression, commencez par quelques concepts de base.
Calcul différé et immédiat
BrainScript connaît deux types de valeurs : immédiate et différée. Les valeurs immédiates sont calculées pendant le traitement de BrainScript, tandis que les valeurs différées sont des objets qui représentent des nœuds dans le réseau de calcul. Le réseau de calcul décrit le calcul réel effectué par le moteur d’exécution CNTK pendant l’entraînement et l’utilisation du modèle.
Les valeurs immédiates dans BrainScript sont destinées à paramétrer le calcul. Ils indiquent les dimensions de tenseur, le nombre de couches réseau, un nom de chemin d’accès à partir duquel charger un modèle, etc. Étant donné que les variables BrainScript sont immuables, les valeurs immédiates sont toujours des constantes.
Les valeurs différées proviennent de l’objectif principal des scripts de cerveau : pour décrire le réseau de calcul. Le réseau de calcul peut être considéré comme une fonction passée à la routine d’entraînement ou d’inférence, qui exécute ensuite la fonction réseau via le moteur d’exécution CNTK. Par conséquent, le résultat de nombreuses expressions BrainScript est un nœud de calcul dans un réseau de calcul, plutôt qu’une valeur réelle. Du point de vue de BrainScript, une valeur différée est un objet C++ de type ComputationNode
qui représente un nœud réseau. Par exemple, la somme de deux nœuds réseau crée un nœud réseau qui représente l’opération de somme qui prend les deux nœuds comme entrées.
Scalaires et matrices par rapport aux tenseurs
Toutes les valeurs du réseau de calcul sont des tableaux numériques ndimensionnels que nous appelons des tenseurs, et n indique le rang de tensoriel. Les dimensions Tensor sont explicitement spécifiées pour les entrées et les paramètres de modèle ; et déduit automatiquement par les opérateurs.
Le type de données le plus courant pour le calcul, les matrices, ne sont que des tenseurs de rang 2. Les vecteurs de colonne sont des tenseurs de rang 1, tandis que les vecteurs de ligne sont classés 2. Le produit de matrice est une opération courante dans les réseaux neuronaux.
Les tenseurs sont toujours des valeurs différées, c’est-à-dire des objets dans le graphique de calcul différé. Toute opération impliquant une matrice ou un tenseur fait partie du graphique de calcul et est évaluée pendant l’apprentissage et l’inférence. Toutefois, les dimensions Tensor sont déduites/vérifiées à l’avance au moment du traitement BS.
Les scalaires peuvent être des valeurs immédiates ou différées. Les scalaires qui paramétrent le réseau de calcul lui-même, tels que les dimensions de tenseur, doivent être immédiats, c’est-à-dire compréhensibles au moment du traitement du BrainScript. Les scalaires différés sont des tenseurs de rang 1 de dimension [1]
. Ils font partie du réseau lui-même, y compris les paramètres scalaires appris tels que les auto-stabilisateurs et les constantes comme dans Log (Constant (1) + Exp(x))
.
Saisie dynamique
BrainScript est un langage typé dynamiquement avec un système de type extrêmement simple. Les types sont vérifiés pendant le traitement de BrainScript lorsqu’une valeur est utilisée.
Les valeurs immédiates sont de type number, Boolean, string, record, array, function/lambda ou l’une des classes C++ prédéfinies de CNTK. Leurs types sont vérifiés au moment de l’utilisation (par exemple, l’argument COND
de l’instruction if
est vérifié pour être un Boolean
, et un accès à un élément de tableau nécessite que l’objet soit un tableau).
Toutes les valeurs différées sont des tenseurs. Les dimensions Tensor font partie de leur type, qui sont vérifiées ou déduites pendant le traitement de BrainScript.
Les expressions entre un scalaire immédiat et un tensoriel différé doivent convertir explicitement le scalaire en un scalaire différé Constant()
. Par exemple, la non-linéarité Softplus doit être écrite en tant que Log (Constant(1) + Exp (x))
. (Il est prévu de supprimer cette exigence dans une prochaine mise à jour.)
Types d’expressions
Littéraux
Les littéraux sont des constantes numériques, booléennes ou de chaînes, comme prévu. Exemples :
13
,42
,3.1415926538
,1e30
true
,false
"my_model.dnn"
,'single quotes'
Les littéraux numériques sont toujours à virgule flottante double précision. Il n’existe aucun type entier explicite dans BrainScript, bien que certaines expressions telles que les index de tableau échouent avec une erreur si les valeurs présentées ne sont pas des entiers.
Les littéraux de chaîne peuvent utiliser des guillemets simples ou doubles, mais n’ont aucun moyen d’échapper des guillemets ou d’autres caractères à l’intérieur (la chaîne contenant à la fois des guillemets simples et doubles doit être calculée, par exemple "He'd say " + '"Yes!" in a jiffy.'
). Les littéraux de chaîne peuvent s’étendre sur plusieurs lignes ; par exemple :
I3 = Parameter (3, 3, init='fromLiteral', initFromLiteral = '1 0 0
0 1 0
0 0 1')
Opérations fixes et unaires
BrainScript prend en charge les opérateurs indiqués ci-dessous. Les opérateurs BrainScript sont choisis pour désigner ce que l’on attendrait des langages populaires, à l’exception de .*
(produit basé sur les éléments), (produit matriciel) *
et d’une sémantique de diffusion spéciale des opérations basées sur les éléments.
Opérateurs de correction+
numérique, , *
-
, /
,.*
+
,-
et*
s’applique aux scalaires, matrices et tenseurs..*
désigne un produit basé sur des éléments. Remarque pour les utilisateurs Python : il s’agit de l’équivalent de numpy’s*
./
est uniquement pris en charge pour les scalaires. Une division par élément peut être écrite àReciprocal(x)
l’aide d’un calcul intégré calcule un élément1/x
.
Opérateurs &&
infix booléens , ||
Celles-ci indiquent respectivement booléen AND et OR.
Concaténation de chaînes (+
)
Les chaînes sont concaténées avec +
. Exemple : BS.Networks.Load (dir + "/model.dnn")
.
Opérateurs de comparaison
Les six opérateurs de comparaison sont <
, et ==
>
leurs négations >=
, !=
, . <=
Elles peuvent être appliquées à toutes les valeurs immédiates comme prévu ; leur résultat est un booléen.
Pour appliquer des opérateurs de comparaison aux tenseurs, il faut plutôt utiliser des fonctions intégrées, telles que Greater()
.
Unaire-
, !
Ces éléments désignent la négation et la négation logique, respectivement. !
ne peut actuellement être utilisé que pour les scalaires.
Sémantique des opérations et de la diffusion d’éléments
Lorsqu’il est appliqué à des matrices/tenseurs, +
et -
.*
sont appliqués au niveau de l’élément.
Toutes les opérations basées sur les éléments prennent en charge la sémantique de diffusion. La diffusion signifie que toute dimension spécifiée comme 1 est automatiquement répétée pour correspondre à n’importe quelle dimension.
Par exemple, un vecteur de ligne de dimension [1 x N]
peut être ajouté directement à une matrice de dimension [M x N]
. La 1
dimension est automatiquement répétée M
. De plus, les dimensions de tenseur sont automatiquement complétées par 1
des dimensions. Par exemple, il est autorisé à ajouter un vecteur de colonne de dimension [M]
à une [M x N]
matrice. Dans ce cas, les dimensions du vecteur de colonne sont automatiquement complétées pour correspondre au [M x 1]
rang de la matrice.
Remarque pour les utilisateurs python : contrairement à numpy, les dimensions de diffusion sont alignées à gauche.
Opérateur matrix-product*
L’opération A * B
indique le produit de matrice. Elle peut également être appliquée à des matrices éparses, ce qui améliore l’efficacité de la gestion des entrées de texte ou des étiquettes représentées sous forme de vecteurs à chaud. Dans CNTK, le produit de matrice a une interprétation étendue qui lui permet d’être utilisé avec des tenseurs de rang > 2. Par exemple, il est possible de multiplier chaque colonne d’un tenseur de rang 3 individuellement avec une matrice.
Le produit de matrice et son extension de tenseur sont décrits en détail ici.
Remarque : Pour multiplier par un scalaire, utilisez le produit .*
basé sur les éléments.
Il est conseillé aux utilisateurs Python d’utiliser numpy
l’opérateur *
pour le produit basé sur les éléments , et non sur le produit de matrice. L’opérateur CNTK *
correspond à numpy
'sdot()
, tandis que l’équivalent de CNTK à l’opérateur *
Python pour numpy
les tableaux est .*
.
Opérateur conditionnel if
Les conditions dans BrainScript sont des expressions, comme l’opérateur C++ ?
. La syntaxe BrainScript est if COND then TVAL else EVAL
, où COND
doit être une expression booléenne immédiate, et le résultat de l’expression est s’il COND
est TVAL
vrai, et EVAL
sinon. L’expression if
est utile pour implémenter plusieurs configurations paramétrées par indicateur similaires dans le même BrainScript, ainsi que pour la récursivité.
(L’opérateur if
fonctionne uniquement pour les valeurs scalaires immédiates. Pour implémenter des conditions pour les objets différés, utilisez la fonction BS.Boolean.If()
intégrée, qui permet de sélectionner une valeur d’un des deux tenseurs en fonction d’un tensor d’indicateur. Il a le formulaire If (cond, tval, eval)
.)
Appels de fonction
BrainScript a trois types de fonctions : primitives intégrées (avec implémentations C++), fonctions de bibliothèque (écrites dans BrainScript) et définies par l’utilisateur (BrainScript). Les exemples de fonctions intégrées sont Sigmoid()
et MaxPooling()
. Les fonctions de bibliothèque et définies par l’utilisateur sont mécaniquement les mêmes, juste enregistrées dans différents fichiers sources. Toutes sortes sont appelées, de la même façon que les langues mathématiques et communes, à l’aide du formulaire f (arg1, arg2, ...)
.
Certaines fonctions acceptent les paramètres facultatifs. Les paramètres facultatifs sont passés en tant que paramètres nommés, par exemple f (arg1, arg2, option1=..., option2=...)
.
Les fonctions peuvent être appelées de manière récursive, par exemple :
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)
Notez comment l’opérateur if
est utilisé pour mettre fin à la récursivité.
Création de couches
Les fonctions peuvent également créer des couches ou des modèles entiers qui sont des objets de fonction qui se comportent comme des fonctions.
Par convention, une fonction qui crée une couche avec des paramètres apprenants { }
utilise des accolades au lieu de parenthèses ( )
.
Vous rencontrerez des expressions comme ceci :
h = DenseLayer {1024} (v)
Ici, deux appels sont en jeu. Le premier, DenseLayer{1024}
est un appel de fonction qui crée un objet de fonction, qui est ensuite appliqué aux données (v)
.
Étant donné que DenseLayer{}
retourne un objet de fonction avec des paramètres apprenants, il utilise { }
pour indiquer cela.
Enregistrements et accès Record-Member
Les expressions d’enregistrement sont des affectations entourées d’accolades. Par exemple :
{
x = 13
y = x * factorParameter
f (z) = y + z
}
Cette expression définit un enregistrement avec trois membres, x
y
et , où f
f
est une fonction.
À l’intérieur de l’enregistrement, les expressions peuvent référencer d’autres membres d’enregistrement uniquement par leur nom, comme x
celui-ci est accessible ci-dessus dans l’affectation de y
.
Contrairement à de nombreuses langues toutefois, les entrées d’enregistrement peuvent être déclarées dans n’importe quel ordre. Par exemple, x
peut être déclaré après y
. Il s’agit de faciliter la définition des réseaux récurrents. Tout membre d’enregistrement est accessible à partir de l’expression d’un autre membre d’enregistrement. Ceci est différent de, par exemple, Python; et similaire à F#'s let rec
. Les références cycliques sont interdites, à l’exception spéciale des opérations et FutureValue()
des PastValue()
opérations.
Lorsque les enregistrements sont imbriqués (expressions d’enregistrement utilisées à l’intérieur d’autres enregistrements), les membres d’enregistrement sont recherchés via la hiérarchie entière des étendues englobantes. En fait, chaque attribution de variable fait partie d’un enregistrement : le niveau externe d’un BrainScript est également un enregistrement implicite. Dans l’exemple ci-dessus, factorParameter
doit être affecté en tant que membre d’enregistrement d’une étendue englobante.
Les fonctions affectées à l’intérieur d’un enregistrement capturent les membres d’enregistrement qu’ils référencent. Par exemple, f()
la capture, y
qui dépend x
à son tour et de la définition factorParameter
externe. La capture de ces moyens signifie qu’il f()
peut être transmis en tant qu’lambda dans des étendues extérieures qui ne contiennent factorParameter
pas ou n’ont pas accès à celui-ci.
En dehors, les membres d’enregistrement sont accessibles à l’aide de l’opérateur .
. Par exemple, si nous avions attribué l’expression d’enregistrement ci-dessus à une variable r
, alors r.x
générerait la valeur 13
. L’opérateur .
ne traverse pas les étendues englobantes : r.factorParameter
échouerait avec une erreur.
(Notez que jusqu’à CNTK 1.6, au lieu de accolades{ ... }
, enregistrements utilisés entre crochets[ ... ]
. Cela est toujours autorisé, mais déconseillé.)
Tableaux et accès au tableau
BrainScript a un type de tableau unidimensionnel pour les valeurs immédiates (pas à confondre avec les tenseurs). Les tableaux sont indexés à l’aide [index]
de . Les tableaux multidimensionnels peuvent être émulés en tant que tableaux de tableaux.
Les tableaux d’au moins 2 éléments peuvent être déclarés à l’aide de l’opérateur :
. Par exemple, l’exemple suivant déclare un tableau 3 dimensions nommé imageDims
qui est ensuite passé pour ParameterTensor{}
déclarer un tensor de paramètre de classement 3 :
imageDims = (256 : 256 : 3)
inputFilter = ParameterTensor {imageDims}
Il est également possible de déclarer des tableaux dont les valeurs font référence les unes aux autres. Pour cela, il faut utiliser la syntaxe d’affectation de tableau un peu plus impliquée :
arr[i:i0..i1] = f(i)
qui construit un tableau nommé arr
avec une limite i0
d’index inférieure et une limite i1
d’index supérieur, i
indiquant la variable pour indiquer la variable d’index dans l’expression f(i)
initialiseur, qui indique à son tour la valeur de arr[i]
. Les valeurs du tableau sont évaluées de manière différée. Cela permet à l’expression initialiseur d’un index i
spécifique d’accéder à d’autres éléments arr[j]
du même tableau, tant qu’il n’existe aucune dépendance cyclique. Par exemple, cela peut être utilisé pour déclarer une pile de couches réseau :
layers[l:1..L] =
if l == 1
then DNNLayer (x, hiddenDim, featDim)
else DNNLayer (layers[l-1], hiddenDim, hiddenDim)
Contrairement à la version récursive de celle-ci que nous avons introduite précédemment, cette version conserve l’accès à chaque couche individuelle en disant layers[i]
.
Il existe également une syntaxe array[i0..i1] (i => f(i))
d’expression, qui est moins pratique, mais parfois utile. La section ci-dessus ressemblerait à ceci :
layers = array[1..L] (l =>
if l == 1
then DNNLayer (x, hiddenDim, featDim)
else DNNLayer (layers[l-1], hiddenDim, hiddenDim)
)
Remarque : Actuellement, il n’existe aucun moyen de déclarer un tableau de 0 éléments. Cela sera traité dans une prochaine version de CNTK.
Expressions de fonctions et lambdas
Dans BrainScript, les fonctions sont des valeurs. Une fonction nommée peut être affectée à une variable et passée en tant qu’argument, par exemple :
Layer (x, m, n, f) = f (ParameterTensor {(m:n)} * x + ParameterTensor {n})
h = Layer (x, 512, 40, Sigmoid)
où Sigmoid
est passé en tant que fonction qui est utilisée à l’intérieur Layer()
. Une syntaxe (x => f(x))
lambda similaire à C#permet également de créer des fonctions anonymes inline. Par exemple, cela définit une couche réseau avec une activation Softplus :
h = Layer (x, 512, 40, (x => Log (Constant(1) + Exp (x)))
La syntaxe lambda est actuellement limitée aux fonctions avec un seul paramètre.
Modèle de couche
L’exemple ci-dessus Layer()
combine la création de paramètres et l’application de fonction.
Un modèle préféré consiste à les séparer en deux étapes :
- créer des paramètres et retourner un objet de fonction qui contient ces paramètres
- créer la fonction qui applique les paramètres à une entrée
Plus précisément, ce dernier est également membre de l’objet de fonction. L’exemple ci-dessus peut être réécrit comme suit :
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
et serait appelé comme suit :
h = Layer {512, 40, Sigmoid} (x)
Le motif de ce modèle est que les types réseau typiques se composent d’appliquer une fonction après une autre à une entrée, qui peut être écrite plus facilement à l’aide de la Sequential()
fonction.
CNTK est fourni avec un ensemble riche de couches prédéfinies, qui sont décrites ici.
Construction d’objets CNTK C++ intégrés
En fin de compte, toutes les valeurs BrainScript sont des objets C++. L’opérateur new
BrainScript spécial est utilisé pour interagir avec les objets CNTK C++ sous-jacents. Il a le formulaire new TYPE ARGRECORD
où TYPE
est l’un d’un ensemble codé en dur des objets C++ prédéfinis exposés à BrainScript, et ARGRECORD
est une expression d’enregistrement passée au constructeur C++.
Vous ne verrez probablement jamais ce formulaire si vous utilisez la forme de parenthèse de BrainScriptNetworkBuilder
, c’est-à-dire BrainScriptNetworkBuilder = (new ComputationNetwork { ... })
, comme décrit ici.
Mais maintenant, vous savez ce qu’il signifie : new ComputationNetwork
crée un objet C++ de typeComputationNetwork
, où { ... }
définit simplement un enregistrement passé au constructeur C++ de l’objet C++ interneComputationNetwork
, qui recherche ensuite 5 membres featureNodes
spécifiques, labelNodes
, criterionNodes
et evaluationNodes
outputNodes
, comme expliqué ici.
Sous le capot, toutes les fonctions intégrées sont vraiment new
des expressions qui construisent des objets de la classe ComputationNode
C++ CNTK. Pour obtenir une illustration, découvrez comment le Tanh()
composant intégré est réellement défini comme créant un objet C++ :
Tanh (z, tag='') = new ComputationNode { operation = 'Tanh' ; inputs = z /plus the function args/ }
Sémantique d’évaluation d’expression
Les expressions BrainScript sont évaluées lors de la première utilisation. Étant donné que l’objectif principal de BrainScript est de décrire le réseau, la valeur d’une expression est souvent un nœud dans un graphique de calcul pour le calcul différé. Par exemple, à partir de l’angle BrainScript, W1 * r + b1
dans l’exemple ci-dessus « évalue » à un ComputationNode
objet plutôt qu’une valeur numérique; alors que les valeurs numériques réelles impliquées seront calculées par le moteur d’exécution du graphique. Seules les expressions BrainScript des scalaires (par exemple 28*28
) sont « calculées » au moment où le BrainScript est analysé. Les expressions qui ne sont jamais utilisées (par exemple, en raison d’une condition) ne sont jamais évaluées (ni vérifiées pour les erreurs de type).
Modèles d’utilisation courants des expressions
Voici quelques modèles courants utilisés avec BrainScript.
Espaces de noms pour fonctions
En regroupant des attributions de fonction dans des enregistrements, il est possible d’obtenir une forme d’espacement de noms. Par exemple :
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 délimitées localement
Parfois, il est souhaitable d’avoir des variables et/ou des fonctions localement étendues pour des expressions plus complexes. Cela peut être obtenu en englobant l’expression entière dans un enregistrement et en accédant immédiatement à sa valeur de résultat. Par exemple :
{ x = 13 ; y = x * x }.y
crée un enregistrement « temporaire » avec un membre y
qui est immédiatement lu. Cet enregistrement est « temporaire », car il n’est pas affecté à une variable, et ses membres ne sont donc pas accessibles à l’exception de y
.
Ce modèle est souvent utilisé pour rendre les couches NN avec des paramètres intégrés plus lisibles, par exemple :
SigmoidLayer (m, n, x) = {
W = Parameter (m, n, init='uniform')
b = Parameter (m, 1, init='value', initValue=0)
h = Sigmoid (W * x + b)
}.h
Ici, h
vous pouvez penser à la « valeur de retour » de cette fonction.
Suivant : En savoir plus sur la définition des fonctions BrainScript
NDLNetworkBuilder (déconseillé)
Les versions antérieures de CNTK utilisaient le maintenant déconseillé NDLNetworkBuilder
au lieu de BrainScriptNetworkBuilder
. NDLNetworkBuilder
implémenté une version beaucoup réduite de BrainScript. Elle avait les restrictions suivantes :
- Aucune syntaxe de correction. Tous les opérateurs doivent être appelés par le biais d’appels de fonction. Par exemple,
Plus (Times (W1, r), b1)
au lieu deW1 * r + b1
. - Aucune expression d’enregistrement imbriquée. Il n’y a qu’un seul enregistrement externe implicite.
- Aucune expression conditionnelle ni appel de fonction récursive.
- Les fonctions définies par l’utilisateur doivent être déclarées dans des blocs spéciaux
load
et ne peuvent pas être imbriquées. - La dernière affectation d’enregistrement est automatiquement utilisée comme valeur d’une fonction.
- La
NDLNetworkBuilder
version linguistique n’est pas complète.
NDLNetworkBuilder
ne doit plus être utilisé.