Espressioni BrainScript
Questa sezione è la specifica delle espressioni BrainScript, anche se si usa intenzionalmente il linguaggio informale per mantenerlo leggibile e accessibile. La controparte è la specifica della sintassi della definizione della funzione BrainScript, che è disponibile qui.
Ogni script del cervello è un'espressione, che a sua volta è costituita da espressioni assegnate alle variabili membro. Il livello più esterno di una descrizione di rete è un'espressione di record implicita. BrainScript include i tipi di espressioni seguenti:
- valori letterali come numeri e stringhe
- operazioni matematiche come il prefisso e le operazioni unarie come
a + b
- espressione condizionale ternaria
- chiamate di funzione
- record, accesso ai membri dei record
- matrici, accesso all'elemento matrice
- espressioni di funzione (lambda)
- costruzione di oggetti C++ predefiniti
Abbiamo intenzionalmente mantenuto la sintassi per ognuno di questi più vicino possibile alle lingue popolari, così gran parte di ciò che troverete di seguito sarà molto familiare.
Concetti
Prima di descrivere i singoli tipi di espressione, prima di tutto alcuni concetti di base.
Calcolo immediato e posticipato
BrainScript conosce due tipi di valori: immediato e posticipato. I valori immediati vengono calcolati durante l'elaborazione di BrainScript, mentre i valori posticipati sono oggetti che rappresentano nodi nella rete di calcolo. La rete di calcolo descrive il calcolo effettivo eseguito dal motore di esecuzione CNTK durante il training e l'uso del modello.
I valori immediati in BrainScript sono destinati a parametrizzare il calcolo. Indicano le dimensioni del tensore, il numero di livelli di rete, un nome percorso da cui caricare un modello e così via. Poiché le variabili BrainScript non sono modificabili, i valori immediati sono sempre costanti.
I valori posticipati derivano dallo scopo principale degli script del cervello: per descrivere la rete di calcolo. La rete di calcolo può essere vista come una funzione passata alla routine di training o inferenza, che quindi esegue la funzione di rete tramite il motore di esecuzione CNTK. Di conseguenza, il risultato di molte espressioni BrainScript è un nodo di calcolo in una rete di calcolo, anziché un valore effettivo. Dal punto di vista di BrainScript, un valore posticipato è un oggetto C++ di tipo ComputationNode
che rappresenta un nodo di rete. Ad esempio, prendendo la somma di due nodi di rete crea un nuovo nodo di rete che rappresenta l'operazione di somma che accetta i due nodi come input.
Scalari vs. Matrici e Tensor
Tutti i valori della rete di calcolo sono matrici numeriche ndimensionali che chiamiamo tensori e n indica la classificazione tensore. Le dimensioni del tensore vengono specificate in modo esplicito per i parametri di input e modello; e dedotto automaticamente dagli operatori.
Il tipo di dati più comune per il calcolo, le matrici, sono solo tensori di rango 2. I vettori di colonna sono tensori di rango 1, mentre i vettori di riga sono di rango 2. Il prodotto matrice è un'operazione comune nelle reti neurali.
I tensori sono sempre valori posticipati, ovvero oggetti nel grafico di calcolo posticipato. Qualsiasi operazione che implica una matrice o un tensore diventa parte del grafico di calcolo e viene valutata durante il training e l'inferenza. Le dimensioni del tensore, tuttavia, vengono posticipate/controllate in anticipo in fase di elaborazione BS.
I scalari possono essere valori immediati o posticipati. Scalari che parametrizzano la rete di calcolo stessa, ad esempio le dimensioni del tensore, devono essere immediate, ad esempio calcolabili al momento dell'elaborazione di BrainScript. I scalari posticipati sono tensor di rango-1 della dimensione [1]
. Fanno parte della rete stessa, inclusi i parametri scalari appresi, ad esempio gli stabilizzatori auto-stabilizzatori e le costanti come in Log (Constant (1) + Exp(x))
.
Digitazione dinamica
BrainScript è un linguaggio tipizzato dinamicamente con un sistema di tipi estremamente semplice. I tipi vengono controllati durante l'elaborazione di BrainScript quando viene usato un valore.
I valori immediati sono di tipo numero, booleano, stringa, record, matrice, funzione/lambda o una delle classi C++ predefinite di CNTK. I relativi tipi vengono controllati al momento dell'uso( ad esempio, l'argomento COND
dell'istruzione if
viene verificato come un elemento di matrice e l'accesso a un elemento matrice richiede che l'oggetto sia una Boolean
matrice).
Tutti i valori posticipati sono tensor. Le dimensioni del tensore fanno parte del loro tipo, che vengono controllate o posticipate durante l'elaborazione di BrainScript.
Le espressioni tra un scalare immediato e un tensore posticipato devono convertire in modo esplicito il scalare in un posticipato Constant()
. Ad esempio, la non linearità Softplus deve essere scritta come Log (Constant(1) + Exp (x))
. È previsto rimuovere questo requisito in un aggiornamento successivo.
Tipi di espressione
Valori letterali
I valori letterali sono costanti numeriche, booleane o stringhe, come previsto. Esempi:
13
,42
,3.1415926538
,1e30
true
,false
"my_model.dnn"
,'single quotes'
I valori letterali numerici sono sempre a virgola mobile a precisione doppia. Non esiste alcun tipo intero esplicito in BrainScript, anche se alcune espressioni come gli indici di matrice avranno esito negativo con un errore se i valori presentati non sono interi.
I valori letterali stringa possono usare virgolette singole o doppie, ma non hanno alcun modo di esprimere virgolette o altri caratteri all'interno (stringa contenente virgolette singole e doppie devono essere calcolate, ad esempio "He'd say " + '"Yes!" in a jiffy.'
). I valori letterali stringa possono estendersi su più righe; Per esempio:
I3 = Parameter (3, 3, init='fromLiteral', initFromLiteral = '1 0 0
0 1 0
0 0 1')
Operazioni infix e Unary
BrainScript supporta gli operatori indicati di seguito. Gli operatori BrainScript vengono scelti per indicare ciò che ci si aspetta dalle lingue popolari, ad eccezione di (prodotto a livello di .*
elemento), (prodotto matrice) *
e della semantica speciale di trasmissione delle operazioni a livello di elemento.
Operatori+
di prefisso numerico, -
, *
, , /
.*
+
,-
e*
si applicano a scalari, matrici e tensori..*
indica un prodotto a livello di elemento. Nota per gli utenti Python: equivale a numpy.*
/
è supportato solo per scalari. Una divisione a livello di elemento può essere scritta usando calcoli predefinitiReciprocal(x)
di un elemento-wise1/x
.
Operatori &&
di prefisso booleano , ||
Questi denotano rispettivamente BOolean AND e OR.
Concatenazione stringa (+
)
Le stringhe sono concatenate con +
. Esempio: BS.Networks.Load (dir + "/model.dnn")
.
Operatori di confronto
I sei operatori di confronto sono , , e le relative negazioni >=
, , <=
!=
. >
==
<
Possono essere applicati a tutti i valori immediati come previsto; il loro risultato è un booleano.
Per applicare operatori di confronto a tensori, è necessario usare invece funzioni predefinite, ad esempio Greater()
.
Unary-
, !
Questi denotano rispettivamente negazione e negazione logica. !
attualmente può essere usato solo per scalari.
Operazioni esemantiche di trasmissione elementi
Quando applicato a matrici/tensori, +
, -
e .*
vengono applicati a livello di elemento.
Tutte le operazioni a livello di elemento supportano la semantica di trasmissione. La trasmissione indica che qualsiasi dimensione specificata come 1 verrà ripetuta automaticamente in modo da corrispondere a qualsiasi dimensione.
Ad esempio, un vettore di riga della dimensione può essere aggiunto direttamente a una matrice di dimensione [1 x N]
[M x N]
. La 1
dimensione verrà ripetuta M
automaticamente. Inoltre, le dimensioni del tensore vengono riempite automaticamente con 1
le dimensioni. Ad esempio, è consentito aggiungere un vettore di colonna di una [M x N]
matrice[M]
. In questo caso, le dimensioni del vettore di colonna vengono automaticamente riempite per [M x 1]
corrispondere alla classificazione della matrice.
Nota agli utenti Python: a differenza di numpy, le dimensioni di trasmissione vengono allineate a sinistra.
Operatore matrice-prodotto*
L'operazione A * B
indica il prodotto matrice. Può anche essere applicato a matrici sparse, che migliora l'efficienza per la gestione di input di testo o etichette rappresentati come vettori a caldo. In CNTK, il prodotto matrice ha un'interpretazione estesa che consente di usarlo con tensori di rango > 2. È, ad esempio, possibile moltiplicare ogni colonna in un tensore di rango 3 singolarmente con una matrice.
Il prodotto matrice e la relativa estensione tensore sono descritti in dettaglio qui.
Nota: per moltiplicare con un scalare, usare il prodotto .*
a livello di elemento .
È consigliabile che numpy
gli utenti Python usino l'operatore *
per il prodotto a livello di elemento , non il prodotto matrice. l'operatore *
di CNTK corrisponde a numpy
, dot()
mentre CNTK equivale all'operatore Python *
per numpy
le matrici è .*
.
Operatore condizionale if
Le condizionali in BrainScript sono espressioni, ad esempio l'operatore C++ ?
. La sintassi BrainScript è if COND then TVAL else EVAL
, dove COND
deve essere un'espressione booleana immediata e il risultato dell'espressione è TVAL
se COND
è true e EVAL
in caso contrario. L'espressione if
è utile per implementare più configurazioni con parametri flag simili nello stesso BrainScript e anche per la ricorsione.
L'operatore if
funziona solo per i valori scalari immediati. Per implementare i condizionali per gli oggetti posticipati, usare la funzione BS.Boolean.If()
predefinita , che consente di selezionare un valore da uno dei due tensori in base a un tensore di flag. Ha il formato If (cond, tval, eval)
.)
Chiamate di funzione
BrainScript include tre tipi di funzioni: primitive predefinite (con implementazioni C++), funzioni di libreria (scritte in BrainScript) e definite dall'utente (BrainScript). Esempi di funzioni predefinite sono Sigmoid()
e MaxPooling()
. Le funzioni definite dall'utente e libreria sono meccanichemente uguali, appena salvate in file di origine diversi. Tutti i tipi vengono richiamati, analogamente alle lingue matematiche e comuni, usando il formato f (arg1, arg2, ...)
.
Alcune funzioni accettano parametri facoltativi. I parametri facoltativi vengono passati come parametri denominati, ad esempio f (arg1, arg2, option1=..., option2=...)
.
Le funzioni possono essere richiamate in modo ricorsivo, ad esempio:
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)
Si noti come viene usato l'operatore if
per terminare la ricorsione.
Creazione di livelli
Le funzioni possono creare interi livelli o modelli che sono oggetti funzione che si comportano anche come funzioni.
Per convenzione, una funzione che crea un livello con parametri appresi usa parentesi graffe { }
anziché parentesi graffe ( )
.
Verranno visualizzate espressioni come questa:
h = DenseLayer {1024} (v)
Qui, due chiamate sono in gioco. Il primo, DenseLayer{1024}
è una chiamata di funzione che crea un oggetto funzione, che a sua volta viene applicato ai dati (v)
.
Poiché DenseLayer{}
restituisce un oggetto funzione con parametri appresi, viene usato { }
per indicare questo oggetto.
Record e accesso Record-Member
Le espressioni di record sono assegnazioni circondate da parentesi graffe. Ad esempio:
{
x = 13
y = x * factorParameter
f (z) = y + z
}
Questa espressione definisce un record con tre membri, x
, y
e f
, dove f
è una funzione.
All'interno del record, le espressioni possono fare riferimento ad altri membri del record solo in base al nome, ad esempio x
è accessibile sopra nell'assegnazione di y
.
A differenza di molte lingue, tuttavia, le voci di record possono essere dichiarate in qualsiasi ordine. Ad esempio, x
potrebbe essere dichiarato dopo y
. Si tratta di facilitare la definizione delle reti ricorrenti. Qualsiasi membro del record è accessibile da qualsiasi altra espressione del membro del record. Questo è diverso da, ad esempio, Python; e simile a F#'s let rec
. I riferimenti ciclici sono vietati, con l'eccezione speciale delle PastValue()
operazioni e FutureValue()
.
Quando i record vengono annidati (espressioni di record usate all'interno di altri record), i membri dei record vengono cercati nell'intera gerarchia di ambiti racchiusi. Infatti, ogni assegnazione di variabile fa parte di un record: il livello esterno di un BrainScript è anche un record implicito. Nell'esempio precedente, factorParameter
deve essere assegnato come membro record di un ambito di inclusione.
Le funzioni assegnate all'interno di un record acquisiscono i membri dei record a cui fanno riferimento. Ad esempio, acquisisce y
, f()
che a sua volta dipende x
da e dall'oggetto definito factorParameter
esternamente . L'acquisizione di questi significa che f()
può essere passata come lambda in ambiti esterni che non contengono factorParameter
o hanno accesso a esso.
Dall'esterno, i membri del record sono accessibili usando l'operatore .
. Ad esempio, se è stata assegnata l'espressione di record precedente a una variabile r
, verrà r.x
restituito il valore 13
. L'operatore .
non attraversa gli ambiti racchiusi: r.factorParameter
si verifica un errore.
Si noti che fino a CNTK 1.6, anziché parentesi graffe{ ... }
, i record usati sono parentesi graffe[ ... ]
. Questo è ancora consentito, ma deprecato.
Matrici e accesso alla matrice
BrainScript ha un tipo di matrice unidimensionale per i valori immediati (non da confondere con i tensori). Le matrici vengono indicizzate usando [index]
. Le matrici multidimensionali possono essere emulate come matrici di matrici.
È possibile dichiarare le matrici di almeno 2 elementi usando l'operatore :
. Ad esempio, il codice seguente dichiara una matrice 3dimensionale denominata imageDims
che viene quindi passata per ParameterTensor{}
dichiarare un tensore di parametri rank-3:
imageDims = (256 : 256 : 3)
inputFilter = ParameterTensor {imageDims}
È anche possibile dichiarare matrici i cui valori fanno riferimento tra loro. Per questo motivo, è necessario usare la sintassi di assegnazione di matrici più coinvolte:
arr[i:i0..i1] = f(i)
che costruisce una matrice denominata arr
con un limite di indice inferiore e un limite i1
i
i0
di indice superiore, denotando la variabile di indice nell'espressione f(i)
inizializzatore, che a sua volta indica il valore di .arr[i]
I valori della matrice vengono valutati in modo più pigre. Ciò consente all'espressione inizializzatore di un indice i
specifico di accedere ad altri elementi arr[j]
della stessa matrice, purché non esista alcuna dipendenza ciclico. Ad esempio, questo può essere usato per dichiarare uno stack di livelli di rete:
layers[l:1..L] =
if l == 1
then DNNLayer (x, hiddenDim, featDim)
else DNNLayer (layers[l-1], hiddenDim, hiddenDim)
A differenza della versione ricorsiva di questa versione introdotta in precedenza, questa versione mantiene l'accesso a ogni singolo livello dicendo layers[i]
.
In alternativa, esiste anche una sintassi array[i0..i1] (i => f(i))
dell'espressione , che è meno conveniente ma talvolta utile. L'aspetto precedente sarà simile al seguente:
layers = array[1..L] (l =>
if l == 1
then DNNLayer (x, hiddenDim, featDim)
else DNNLayer (layers[l-1], hiddenDim, hiddenDim)
)
Nota: attualmente non è possibile dichiarare una matrice di 0 elementi. Questo verrà risolto in una versione futura di CNTK.
Espressioni di funzioni e lambda
In BrainScript le funzioni sono valori. Una funzione denominata può essere assegnata a una variabile e passata come argomento, ad esempio:
Layer (x, m, n, f) = f (ParameterTensor {(m:n)} * x + ParameterTensor {n})
h = Layer (x, 512, 40, Sigmoid)
dove Sigmoid
viene passato come funzione utilizzata all'interno Layer()
di . In alternativa, una sintassi (x => f(x))
lambda simile a C#consente di creare funzioni anonime inline. Ad esempio, questo definisce un livello di rete con un'attivazione Softplus:
h = Layer (x, 512, 40, (x => Log (Constant(1) + Exp (x)))
La sintassi lambda è attualmente limitata alle funzioni con un singolo parametro.
Modello di livello
L'esempio precedente Layer()
combina la creazione dei parametri e l'applicazione per le funzioni.
Un modello preferito consiste nel separare questi elementi in due passaggi:
- creare parametri e restituisce un oggetto funzione che contiene questi parametri
- creare la funzione che applica i parametri a un input
In particolare, quest'ultimo è un membro dell'oggetto funzione. L'esempio precedente può essere riscritto come:
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
e verrà richiamato come:
h = Layer {512, 40, Sigmoid} (x)
Il motivo di questo modello è che i tipi di rete tipici sono costituiti dall'applicazione di una funzione dopo un altro a un input, che può essere scritto più facilmente usando la Sequential()
funzione.
CNTK include un set ricco di livelli predefiniti, descritti qui.
Costruzione di oggetti C++ predefiniti CNTK
In definitiva, tutti i valori BrainScript sono oggetti C++. L'operatore new
BrainScript speciale viene usato per interfacciarsi con gli oggetti CNTK C++ sottostanti. Ha il modulo new TYPE ARGRECORD
in cui TYPE
è un set hardcoded degli oggetti C++ predefiniti esposti a BrainScript ed ARGRECORD
è un'espressione di record passata al costruttore C++.
Probabilmente si otterrà mai solo questo modulo se si usa la forma parentesi di BrainScriptNetworkBuilder
, ad esempio BrainScriptNetworkBuilder = (new ComputationNetwork { ... })
, come descritto qui.
Ma ora si sa cosa significa: new ComputationNetwork
crea un nuovo oggetto C++ di tipo ComputationNetwork
, dove { ... }
definisce semplicemente un record passato al costruttore C++ dell'oggetto C++ internoComputationNetwork
, che cercherà quindi 5 membri featureNodes
specifici , , labelNodes
evaluationNodes
criterionNodes
, e outputNodes
, come illustrato qui.
Sotto la cappa, tutte le funzioni predefinite sono new
davvero espressioni che costruiscono oggetti della classe ComputationNode
CNTK C++ . Per illustrazione, vedere come viene effettivamente definito il Tanh()
valore predefinito come creazione di un oggetto C++:
Tanh (z, tag='') = new ComputationNode { operation = 'Tanh' ; inputs = z /plus la funzione args/ }
Semantica di valutazione delle espressioni
Le espressioni BrainScript vengono valutate per la prima volta. Poiché lo scopo principale di BrainScript è descrivere la rete, il valore di un'espressione è spesso un nodo in un grafico di calcolo per il calcolo posticipato. Ad esempio, dall'angolo BrainScript, W1 * r + b1
nell'esempio precedente "valuta" a un oggetto anziché a un ComputationNode
valore numerico; mentre i valori numerici effettivi coinvolti verranno calcolati dal motore di esecuzione del grafico. Solo le espressioni BrainScript dei scalari (ad esempio 28*28
) sono "calcolate" al momento dell'analisi di BrainScript. Le espressioni che non vengono mai usate (ad esempio a causa di una condizione) non vengono mai valutate (né controllate per errori di tipo).
Modelli di utilizzo comuni di espressioni
Di seguito sono riportati alcuni modelli comuni usati con BrainScript.
Spazi dei nomi per funzioni
Raggruppando le assegnazioni di funzione nei record, è possibile ottenere una forma di nomipacing. Ad esempio:
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))
Variabili con ambito locale
A volte è consigliabile avere variabili con ambito locale e/o funzioni per espressioni più complesse. Ciò può essere ottenuto racchiudendo l'intera espressione in un record e accedendo immediatamente al relativo valore di risultato. Ad esempio:
{ x = 13 ; y = x * x }.y
creerà un record "temporaneo" con un membro y
che viene immediatamente letto. Questo record è 'temporaneo' poiché non è assegnato a una variabile e pertanto i relativi membri non sono accessibili tranne per y
.
Questo modello viene spesso usato per rendere più leggibili i livelli NN con parametri predefiniti, ad esempio:
SigmoidLayer (m, n, x) = {
W = Parameter (m, n, init='uniform')
b = Parameter (m, 1, init='value', initValue=0)
h = Sigmoid (W * x + b)
}.h
In questo caso, h
può essere considerato il "valore restituito" di questa funzione.
Successivamente: Informazioni sulla definizione delle funzioni BrainScript
NDLNetworkBuilder (deprecato)
Versioni precedenti di CNTK usato l'oggetto ora deprecato NDLNetworkBuilder
anziché BrainScriptNetworkBuilder
. NDLNetworkBuilder
implementato una versione molto ridotta di BrainScript. Aveva le restrizioni seguenti:
- Nessuna sintassi del prefisso. Tutti gli operatori devono essere richiamati tramite chiamate di funzione. Ad esempio
Plus (Times (W1, r), b1)
, invece diW1 * r + b1
. - Nessuna espressione di record annidata. C'è solo un record esterno implicito.
- Nessuna espressione condizionale o chiamata a funzione ricorsiva.
- Le funzioni definite dall'utente devono essere dichiarate in blocchi speciali
load
e non possono annidare. - L'ultima assegnazione di record viene usata automaticamente come valore di una funzione.
- La
NDLNetworkBuilder
versione della lingua non è completata da Turing.
NDLNetworkBuilder
non deve più essere usato.