Partager via


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ément 1/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, xyet , où ff 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, yqui dépend x à son tour et de la définition factorParameterexterne. 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 i1d’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)

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 ARGRECORDTYPE 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 featureNodesspécifiques, labelNodes, criterionNodeset evaluationNodesoutputNodes, comme expliqué ici.

Sous le capot, toutes les fonctions intégrées sont vraiment new des expressions qui construisent des objets de la classe ComputationNodeC++ 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 de W1 * 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é.