Partager via


Concepts de base de BrainScript

BrainScript- A Walk-Through

Cette section présente les concepts de base du langage « BrainScript ». Une nouvelle langue ? Ne vous inquiétez pas & lire, c’est très simple.

Dans CNTK, les réseaux personnalisés sont définis à l’aide du BrainScriptNetworkBuilder et décrit dans le langage de description réseau CNTK « BrainScript ». De même, les descriptions de réseaux sont appelées scripts de cerveau.

BrainScript fournit un moyen simple de définir un réseau de façon similaire au code, à l’aide d’expressions, de variables, de fonctions primitives et auto-définies, de blocs imbriqués et d’autres concepts bien compris. Il ressemble à un langage de script dans la syntaxe.

Ok, nous allons nous mouiller les pieds avec un exemple BrainScript complet !

Exemple de définition de réseau BrainScript complet

L’exemple suivant montre la description réseau d’un réseau neuronal simple avec une couche masquée et une couche de classification. Nous allons expliquer les concepts dans cet exemple. Avant de passer à autre chose, passez peut-être quelques minutes avec l’exemple et essayez de deviner ce que cela signifie. Vous constaterez peut-être, au fur et à mesure de la lecture, que vous avez deviné la plupart des données correctement.

BrainScriptNetworkBuilder = {   # (we are inside the train section of the CNTK config file)

    SDim = 28*28 # feature dimension
    HDim = 256   # hidden dimension
    LDim = 10    # number of classes

    # define the model function. We choose to name it 'model()'.
    model (features) = {
        # model parameters
        W0 = ParameterTensor {(HDim:SDim)} ; b0 = ParameterTensor {HDim}
        W1 = ParameterTensor {(LDim:HDim)} ; b1 = ParameterTensor {LDim}

        # model formula
        r = RectifiedLinear (W0 * features + b0) # hidden layer
        z = W1 * r + b1                          # unnormalized softmax
    }.z

    # define inputs
    features = Input {SDim}
    labels   = Input {LDim} 

    # apply model to features
    z = model (features)

    # define criteria and output(s)
    ce   = CrossEntropyWithSoftmax (labels, z)  # criterion (loss)
    errs = ErrorPrediction         (labels, z)  # additional metric
    P    = Softmax (z)     # actual model usage uses this

    # connect to the system. These five variables must be named exactly like this.
    featureNodes    = (features)
    inputNodes      = (labels)
    criterionNodes  = (ce)
    evaluationNodes = (errs)
    outputNodes     = (P)
}

Concepts de base de la syntaxe BrainScript

Avant d’approfondir tout de suite, quelques remarques générales sur la syntaxe de BrainScript.

BrainScript utilise une syntaxe simple qui vise à permettre d’exprimer des réseaux neuronaux d’une manière qui ressemble à des formules mathématiques. Par conséquent, l’unité syntaxique fondamentale est l’affectation, qui est utilisée dans les affectations de variables et les définitions de fonction. Par exemple :

Softplus (x) = Log (1 + Exp (x))
h = Softplus (W * v + b)

Lignes, Commentaires, Inclure

Bien qu’une affectation soit généralement écrite sur une seule ligne, les expressions peuvent s’étendre sur plusieurs lignes. Toutefois, pour placer plusieurs affectations sur une seule ligne, vous devez les séparer par un point-virgule. Par exemple :

SDim = 28*28 ; HDim = 256 ; LDim = 10    # feature, hidden, and label dimension

Outre l’exigence d’un point-virgule entre les affectations en l’absence d’un saut de ligne, BrainScript n’est pas sensible aux espaces blancs.

BrainScript comprend les commentaires de ligne en utilisant à la fois le style # Python et le style //C++. Les commentaires inline utilisent la syntaxe C (/* this is a comment*/), mais contrairement à C, celles-ci peuvent ne pas s’étendre sur plusieurs lignes.

Pour BrainScript incorporé dans les fichiers de configuration CNTK (par opposition à BrainScript lu à partir d’un fichier distinct via une include directive), en raison d’une interaction avec l’analyseur de configuration, il existe une restriction supplémentaire (quelque peu étrange) selon laquelle les parenthèses, accolades ou crochets doivent être équilibrés dans les commentaires de style C/C++ et les littéraux de chaîne. Par conséquent, aucun émoticône dans les commentaires de style C/C++ !

Une include "PATH" directive peut être utilisée à n’importe quel endroit pour insérer le contenu d’un fichier au point de l’instruction. Ici, PATH peut être un chemin absolu ou relatif relatif (avec ou sans sous-répertoires). S’il s’agit d’un chemin relatif, les emplacements suivants sont recherchés dans l’ordre : répertoire de travail actuel ; répertoire(s) contenant des fichiers externes, y compris le cas échéant ; répertoire(s) contenant le ou les fichiers de configuration ; et enfin le répertoire contenant l’exécutable CNTK. Toutes les fonctions BrainScript intégrées sont incluses de cette façon à partir d’un fichier appelé CNTK.core.bs qui se trouve en regard de l’exécutable CNTK.

Expressions

Ensuite, vous devez connaître les expressions BrainScript, c’est-à-dire les formules qui décrivent votre réseau. Les expressions BrainScript sont écrites de façon mathématique dans une syntaxe similaire aux langages de programmation courants. Les expressions les plus simples sont des littéraux, par exemple des nombres et des chaînes. Un exemple mathématique est W1 * r + b, où * fait référence à un produit scalaire, matriciel ou tenseur en fonction du type des variables. Un autre type d’expression courant est l’appel de fonction, par exemple RectifiedLinear (.).

BrainScript est un langage typé dynamiquement. Un type d’expression important est l’enregistrement, défini à l’aide de la {...} syntaxe et accessible via la syntaxe point. Par exemple, r = { x = 13 ; y = 42 } affecte un enregistrement avec deux membres à r, où le premier membre est accessible en tant que r.x.

En plus des opérateurs mathématiques habituels, BrainScript a une expression conditionnelle (if c then t else f), une expression de tableau et des lambdas simples. Enfin, pour interfacer avec le code CNTK C++, BrainScript peut instancier directement un ensemble limité d’objets C++ prédéfinis, principalement les ComputationNode réseaux de calcul composés. Pour plus d’informations, consultez Expressions.

Les expressions BrainScript sont évaluées lors de la première utilisation. L’objectif principal de BrainScript est de décrire le réseau, de sorte que la valeur d’une expression n’est souvent pas une valeur finale, mais plutôt un nœud dans un graphique de calcul pour le calcul différé (comme dans W1 * r + b). 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.

Remarque : la version désormais déconseillée NDLNetworkBuilder ne prend en charge que la syntaxe d’appel de fonction ; par exemple, il faudrait écrire Plus (Times (W1, r), b1).

Variables

Les variables peuvent contenir la valeur de n’importe quelle expression BrainScript (nombre, chaîne, enregistrement, tableau, lambda, objet CNTK C++) et sont remplacées lorsqu’elles sont utilisées dans une expression. Les variables sont immuables, c’est-à-dire affectées une seule fois. Par exemple, la définition de réseau ci-dessus commence par :

SDim = 28*28  
HDim = 256
LDim = 10

Ici, les variables sont définies sur des valeurs numériques scalaires qui sont utilisées comme paramètres dans les expressions suivantes. Ces valeurs sont les dimensions des exemples de données, des couches masquées et des étiquettes utilisées dans l’entraînement. Cet exemple d’installation particulier concerne le jeu de données MNIST, qui est une collection d’images [28 x 28]-pixel. Chaque image étant un chiffre manuscrit (0-9), il existe 10 étiquettes possibles qui peuvent être appliquées à chaque image. La dimension HDim d’activation masquée est un choix utilisateur.

La plupart des variables sont des membres d’enregistrement (le bloc BrainScript externe est implicitement un enregistrement). En outre, les variables peuvent être des arguments de fonction ou stockées en tant qu’éléments tableaux.

OK, prêt à passer par la définition du modèle.

Définition du réseau

Un réseau est principalement décrit par la formule de calcul des sorties du réseau à partir des entrées. Nous appelons cela la fonction de modèle, qui est souvent définie comme une fonction réelle dans BrainScript. Dans le cadre de la fonction de modèle, l’utilisateur doit déclarer les paramètres du modèle. Enfin, il faut définir les entrées du réseau, ainsi que les critères/sorties. Tous ces éléments sont définis en tant que variables. Les variables d’entrée et de critères/sortie doivent ensuite être communiquées au système.

Fonction de modèle et paramètres de modèle du réseau

La fonction de modèle contient les formules réseau réelles et les paramètres de modèle respectifs. Notre exemple utilise le produit de matrice et l’addition, ainsi que la fonction « primitive » (intégrée) pour la fonction RectifiedLinear()d’énergie , de sorte que le cœur de la fonction réseau se compose des équations suivantes :

r = RectifiedLinear (W0 * features + b0)
z = W1 * r + b1 

Les paramètres de modèle sont des matrices, des vecteurs de biais ou tout autre tenseur qui constituent le modèle appris à la fin de l’entraînement. Les tenseurs de paramètre de modèle sont utilisés pour transformer les exemples de données d’entrée en sortie souhaitée et sont mis à jour par le processus d’apprentissage. L’exemple de réseau ci-dessus contient les paramètres de matrice suivants :

W0 = ParameterTensor {(HDim:SDim)}
b0 = ParameterTensor {(HDim)}

Dans ce cas, W0 est la matrice de pondération et b0 est le vecteur de biais. ParameterTensor{} désigne une primitive CNTK spéciale, qui instancie un vecteur, une matrice ou un tenseur de rang arbitraire, et prend les paramètres de dimension en tant que tableau BrainScript (nombres concaténés par un signe deux-points :). La dimension d’un vecteur est un nombre unique, tandis qu’une dimension de matrice doit être spécifiée en tant que (numRows:numCols). Par défaut, les paramètres sont initialisés avec des nombres aléatoires uniformes lorsqu’ils sont instanciés directement et heNormal lorsqu’ils sont utilisés via des couches, mais d’autres options existent (voir ici) pour la liste complète. Contrairement aux fonctions régulières, ParameterTensor{} prend ses arguments en accolades au lieu de parenthèses. Les accolades sont la convention BrainScript pour les fonctions qui créent des paramètres ou des objets, par opposition aux fonctions.

Tout est ensuite encapsulé dans une fonction BrainScript. Les fonctions BrainScript sont déclarées sous la forme f(x) = an expression of x. Par exemple, Sqr (x) = x * x est une déclaration de fonction BrainScript valide. Je ne pourrais pas être beaucoup plus simple et direct, n’est-ce pas ?

Maintenant, la fonction de modèle réelle de notre exemple ci-dessus est un peu plus complexe :

model (features) = {
    # model parameters
    W0 = ParameterTensor {(HDim:SDim)} ; b0 = ParameterTensor {HDim}  
    W1 = ParameterTensor {(LDim:HDim)} ; b1 = ParameterTensor {LDim}

    # model formula
    r = RectifiedLinear (W0 * features + b0) # hidden layer
    z = W1 * r + b1                          # unnormalized softmax
}.z

L’extérieur { ... } et que la finale .z mérite une explication. Les boucles { ... } externes et leur contenu définissent en fait un enregistrement avec 6 membres d’enregistrement (W0, b0, W1, b1r, et z). Toutefois, la valeur de la fonction de modèle est juste z; tous les autres sont internes à la fonction. Par conséquent, nous utilisons .z pour sélectionner le membre d’enregistrement que nous voulons retourner. Il s’agit simplement de la syntaxe de point pour accéder aux membres d’enregistrement. De cette façon, les autres membres d’enregistrement ne sont pas accessibles de l’extérieur. Mais ils continuent d’exister dans le cadre de l’expression pour calculer z. Le { ... ; x = ... }.x modèle est un moyen d’utiliser des variables locales.

Notez que la syntaxe d’enregistrement n’est pas nécessaire. Sinon, model(features) peut également avoir été déclaré sans le détour via l’enregistrement, en tant qu’expression unique :

model (features) = ParameterTensor {(LDim:HDim)} * (RectifiedLinear (ParameterTensor {(HDim:SDim)}
                   * features + ParameterTensor {HDim})) + ParameterTensor {LDim}

Cela est beaucoup plus difficile à lire et, plus important encore, ne permet pas d’utiliser le même paramètre à plusieurs endroits de la formule.

Entrées

Les entrées dans le réseau sont définies par les exemples de données et les étiquettes associées aux exemples :

features = Input {SDim}
labels   = Input {LDim}

Input{} est la deuxième primitive CNTK spéciale nécessaire pour la définition de modèle (la première étant Parameter{}). Elle crée une variable qui reçoit des entrées provenant de l’extérieur du réseau : du lecteur. L’argument de Input{} est la dimension de données. Dans cet exemple, l’entrée features aura les dimensions des exemples de données (que nous avons définies dans la variable SDim), et l’entrée labels aura les dimensions des étiquettes. Les noms de variables des entrées sont censés correspondre aux entrées correspondantes dans la définition du lecteur.

Critères d’entraînement et sorties réseau

Nous devons toujours déclarer comment la sortie du réseau interagit avec le monde. Notre fonction de modèle calcule les valeurs logit (probabilités logarithmiques non normalisées). Ces valeurs logit peuvent être utilisées pour

  • définir le critère d’entraînement,
  • précision des mesures, et
  • calculer la probabilité par rapport aux classes de sortie en fonction d’une entrée, pour baser une décision de classification (notez que le journal non normalisé posterior z peut souvent être utilisé directement pour la classification).

L’exemple de réseau utilise des étiquettes de catégorie, qui sont représentées sous forme de vecteurs à chaud unique. Pour l’exemple MNIST, celles-ci apparaissent sous la forme d’un tableau de 10 valeurs à virgule flottante, qui sont toutes nulles, sauf pour la catégorie d’étiquette appropriée qui est 1.0. Les tâches de classification comme la nôtre utilisent généralement la SoftMax() fonction pour obtenir les probabilités pour chaque étiquette. Le réseau est ensuite optimisé pour optimiser la probabilité logarithmique de la classe correcte (entropie croisée) et réduire celle de toutes les autres classes. Il s’agit de notre critère d’entraînement, ou fonction de perte. Dans CNTK, ces deux actions sont généralement combinées dans une seule fonction pour plus d’efficacité :

ce = CrossEntropyWithSoftmax (labels, z)

CrossEntropyWithSoftmax() la fonction prend l’entrée, calcule la SoftMax() fonction, calcule l’erreur à partir de la valeur réelle à l’aide de l’entropie croisée, et ce signal d’erreur est utilisé pour mettre à jour les paramètres dans le réseau via la propagation inverse. Par conséquent, dans l’exemple ci-dessus, la valeur normalisée Softmax() , que nous avons calculée comme P, n’est pas utilisée pendant l’entraînement. Toutefois, elle sera nécessaire pour l’utilisation du réseau (encore une fois, notez que dans de nombreux cas, z est souvent suffisante pour la classification ; dans ce cas, z elle-même serait la sortie).

CNTK utilise la descente de gradient stochastique (SGD) comme algorithme d’apprentissage. SGD doit calculer le gradient de la fonction objective par rapport à tous les paramètres du modèle. Il est important de noter que CNTK n’exige pas que les utilisateurs spécifient ces dégradés. Au lieu de cela, chaque fonction intégrée dans CNTK a également une fonction dérivée équivalente, et le système effectue automatiquement la mise à jour de propagation arrière des paramètres réseau. Cela n’est pas visible par l’utilisateur. Les utilisateurs n’ont jamais à se soucier des dégradés. Jamais.

En plus du critère d’entraînement, les taux d’erreur prédits sont souvent calculés pendant la phase d’entraînement pour valider l’amélioration du système à mesure que l’entraînement va plus loin. Cette opération est gérée dans CNTK à l’aide de la fonction suivante :

errs = ClassificationError (labels, z)

Les probabilités produites par le réseau sont comparées à l’étiquette réelle et le taux d’erreur est calculé. Cela est généralement affiché par le système. Bien que cela soit utile, il n’est pas obligatoire d’utiliser ClassificationError().

Communication des entrées, des sorties et des critères au système

Maintenant que toutes les variables sont définies, nous devons indiquer au système quelles variables il doit traiter en tant qu’entrées, sorties et critères. Pour ce faire, définissez 5 variables spéciales qui doivent avoir exactement ces noms :

featureNodes    = (features)
labelNodes      = (labels)
criterionNodes  = (ce)
evaluationNodes = (errs)
outputNodes     = (z:P)

Les valeurs sont des tableaux, où les valeurs doivent être séparées par deux-points (le signe deux-points : est un opérateur BrainScript qui forme un tableau en concaténant deux valeurs ou tableaux). Ceci est illustré ci-dessus pour outputNodes, qui déclare à la fois z et P en tant que sorties.

(Remarque : Le déprécié NDLNetworkBuilder exigeait que les éléments du tableau soient séparés par des virgules à la place.)

Résumé des noms spéciaux

Comme nous l’avons vu ci-dessus, il existe 7 noms spéciaux dont nous devons être conscients, qui portent des propriétés « magiques » :

  • ParameterTensor{}: déclare et initialise un paramètre appris.
  • Input{}: déclare une variable qui est connectée et alimentée à partir d’un lecteur de données.
  • featureNodes, labelNodes, criterionNodes, evaluationNodes, et outputNodes: déclare au système quelles variables utiliser comme entrées, sorties et critères.

En outre, il existe 3 fonctions spéciales supplémentaires avec la « magie » intégrée dans CNTK, qui sont abordées ailleurs :

  • Constant(): déclare une constante.
  • PastValue() et FutureValue(): accédez à une variable dans une fonction réseau à une autre étape de temps, pour former des boucles récurrentes.

Tout autre nom prédéfini est une fonction primitive intégrée telle que Sigmoid() ou Convolution() avec une implémentation C++, une fonction de bibliothèque prédéfinie réalisée dans BrainScript comme BS.RNNs.LSTMP(), ou un enregistrement qui joue le rôle d’espace de noms pour les fonctions de bibliothèque (par exemple BS.RNNs). Pour obtenir la liste complète, consultez Référence BrainScript-Full-Function.

Suivant : Expressions BrainScript