Dela via


BrainScript-uttryck

Det här avsnittet är specifikationen för BrainScript-uttryck, även om vi avsiktligt använder informellt språk för att hålla det läsbart och tillgängligt. Dess motsvarighet är specifikationen av BrainScript-funktionsdefinitionssyntax, som finns här.

Varje hjärnskript är ett uttryck som i sin tur består av uttryck som tilldelats för att registrera medlemsvariabler. Den yttersta nivån för en nätverksbeskrivning är ett underförstådt postuttryck. BrainScript har följande typer av uttryck:

  • literaler som tal och strängar
  • matematiska infix- och unary-åtgärder som a + b
  • ett villkorsuttryck för ternary
  • funktionsanrop
  • poster, postmedlemsåtkomster
  • matriser, åtkomst till matriselement
  • funktionsuttryck (lambdas)
  • inbyggd C++-objektkonstruktion

Vi höll avsiktligt syntaxen för var och en av dessa så nära populära språk som möjligt, så mycket av det du hittar nedan kommer att se mycket bekant ut.

Begrepp

Innan du beskriver de enskilda typerna av uttryck börjar du med några grundläggande begrepp.

Omedelbar kontra uppskjuten beräkning

BrainScript känner till två typer av värden: omedelbar ochuppskjuten. Omedelbara värden beräknas under bearbetningen av BrainScript, medan uppskjutna värden är objekt som representerar noder i beräkningsnätverket. Beräkningsnätverket beskriver den faktiska beräkningen som utförs av CNTK-körningsmotorn under träning och användning av modellen.

Omedelbara värden i BrainScript är avsedda att parametrisera beräkningen. De anger tensordimensioner, antalet nätverksskikt, ett sökvägsnamn att läsa in en modell från osv. Eftersom BrainScript-variabler är oföränderliga är omedelbara värden alltid konstanter.

Uppskjutna värden uppstår från det primära syftet med hjärnskript: att beskriva beräkningsnätverket. Beräkningsnätverket kan ses som en funktion som skickas till tränings- eller slutsatsdragningsrutinen, som sedan kör nätverksfunktionen via den CNTK körningsmotorn. Resultatet av många BrainScript-uttryck är därför en beräkningsnod i ett beräkningsnätverk i stället för ett faktiskt värde. Ur BrainScript-synvinkel är ett uppskjutet värde ett C++-objekt av typen ComputationNode som representerar en nätverksnod. Om du till exempel tar summan av två nätverksnoder skapas en ny nätverksnod som representerar sammanfattningsåtgärden som tar de två noderna som indata.

Scalars vs. Matrices vs. Tensors

Alla värden i beräkningsnätverket är numeriska n-dimensionella matriser som vi kallar tensorer och n anger tensor-rangordningen. Tensor-dimensioner anges uttryckligen för indata och modellparametrar. och härleds automatiskt av operatorer.

Den vanligaste datatypen för beräkning, matriser, är bara tensorer i rangordning 2. Kolumnvektorer är tensorer av rangordning 1, medan radvektorer är rank 2. Matrisprodukten är en vanlig åtgärd i neurala nätverk.

Tensorer är alltid uppskjutna värden, d.v.s. objekt i diagrammet för uppskjuten beräkning. Alla åtgärder som omfattar en matris eller tensor blir en del av beräkningsdiagrammet och utvärderas under träning och slutsatsdragning. Tensor-dimensioner härleds/kontrolleras i förväg vid BS-bearbetningstid.

Skalärer kan vara antingen omedelbara eller uppskjutna värden. Skalärer som parametriserar själva beräkningsnätverket, till exempel tensordimensioner, måste vara omedelbara, dvs. beräkningsbara vid tidpunkten för bearbetningen av BrainScript. Uppskjutna skalärer är dimensionens rank-1 tensorer [1]. De ingår i själva nätverket, inklusive lärbara skalära parametrar som självstabilisatorer och konstanter som i Log (Constant (1) + Exp(x)).

Dynamisk inmatning

BrainScript är ett dynamiskt typat språk med ett extremt enkelt typsystem. Typer kontrolleras under bearbetning av BrainScript när ett värde används.

Omedelbara värden är av typen number, Boolean, string, record, array, function/lambda eller någon av CNTK fördefinierade C++-klasser. Deras typer kontrolleras vid användningstillfället (till exempel COND verifieras argumentet till -instruktionen if som en Boolean, och en matriselementåtkomst kräver att objektet är en matris).

Alla uppskjutna värden är tensorer. Tensor-dimensioner är en del av deras typ, som kontrolleras eller härleds under bearbetningen av BrainScript.

Uttryck mellan en omedelbar skalär och en uppskjuten tensor måste uttryckligen konvertera skalären till en uppskjuten Constant(). Till exempel måste Softplus icke-linjäritet skrivas som Log (Constant(1) + Exp (x)). (Det är planerat att ta bort det här kravet i en kommande uppdatering.)

Uttryckstyper

Literaler

Literaler är numeriska, booleska eller strängkonstanter, som förväntat. Exempel:

  • 13, 42, 3.1415926538, 1e30
  • true, false
  • "my_model.dnn", 'single quotes'

Numeriska literaler är alltid flyttal med dubbel precision. Det finns ingen explicit heltalstyp i BrainScript, även om vissa uttryck, till exempel matrisindex, misslyckas med ett fel om det visas värden som inte är heltal.

Strängliteraler kan använda enkla eller dubbla citattecken, men har inget sätt att undvika citattecken eller andra tecken inuti (sträng som innehåller både enkla och dubbla citattecken måste beräknas, t.ex. "He'd say " + '"Yes!" in a jiffy.'). Strängliteraler kan sträcka sig över flera rader. till exempel:

I3 = Parameter (3, 3, init='fromLiteral', initFromLiteral = '1 0 0
                                                             0 1 0
                                                             0 0 1')

Infix- och unary-åtgärder

BrainScript stöder de operatorer som anges nedan. BrainScript-operatorer väljs för att betyda vad man kan förvänta sig från populära språk, med undantag för .* (elementvis produkt), * (matrisprodukt) och särskild sändningssemantik för elementvisa åtgärder.

Numeriska infixoperatorer+, -, *, /, .*

  • +, -och * gäller för skalärer, matriser och tensorer.
  • .* anger en elementvis produkt. Obs! För Python användare: Detta motsvarar numpys *.
  • / stöds endast för skalärer. En elementvis division kan skrivas med hjälp av inbyggda Reciprocal(x) beräkningar som ett elementmässigt 1/x.

Booleska infixoperatorer &&, ||

Dessa anger boolesk AND respektive OR.

Sammanfogning av strängar (+)

Strängar sammanfogas med +. Exempel: BS.Networks.Load (dir + "/model.dnn").

Jämförelseoperatorer

De sex jämförelseoperatorerna är <, ==, >och deras negationer >=, !=, <=. De kan tillämpas på alla omedelbara värden som förväntat. deras resultat är ett booleskt värde.

Om du vill använda jämförelseoperatorer för tensorer måste du använda inbyggda funktioner i stället, till exempel Greater().

Enställig-, !

Dessa anger negation respektive logisk negation. ! kan för närvarande endast användas för skalärer.

Elementwise Operations and Broadcasting Semantics

När de tillämpas på matriser/tensorer +tillämpas , -och .* elementmässigt.

Alla elementbaserade åtgärder stöder sändningssemantik. Sändning innebär att alla dimensioner som anges som 1 automatiskt upprepas för att matcha alla dimensioner.

En radvektor med dimension [1 x N] kan till exempel läggas till direkt i en matris med dimensionen [M x N]. Dimensionen 1 upprepas M automatiskt gånger. Dessutom vaderas tensordimensioner automatiskt med 1 dimensioner. Det är till exempel tillåtet att lägga till en kolumnvektor för [M] en [M x N] matris. I det här fallet vadläggs kolumnvektorns dimensioner automatiskt till för [M x 1] att matcha matrisens rangordning.

Obs! Till Python användare: Till skillnad från numpy är sändningsdimensionerna vänsterjusterade.

Operatorn Matrix-Product*

Åtgärden A * B anger matrisprodukten. Det kan också tillämpas på glesa matriser, vilket förbättrar effektiviteten för att hantera textinmatningar eller etiketter som representeras som one-hot-vektorer. I CNTK har matrisprodukten en utökad tolkning som gör att den kan användas med tensorer av rang > 2. Det är till exempel möjligt att multiplicera varje kolumn i en tensor för rangordning-3 individuellt med en matris.

Matrisprodukten och dess tensortillägg beskrivs i detalj här.

Obs! Om du vill multiplicera med en skalär använder du den elementvisa produkten .*.

Python användare uppmanas att numpy använda operatorn * för den elementvisa produkten, inte matrisprodukten. * CNTK-operatorn numpymotsvarar ' s dot(), medan CNTK motsvarar Python * operator för numpy matriser är .*.

Villkorsoperatorn if

Villkor i BrainScript är uttryck, till exempel C++ ? -operatorn. BrainScript-syntaxen är if COND then TVAL else EVAL, där COND måste vara ett omedelbart booleskt uttryck och resultatet av uttrycket är om COND är TVAL sant och EVAL annars. Uttrycket if är användbart för att implementera flera liknande flaggparameteriserade konfigurationer i samma BrainScript och även för rekursion.

(Operatorn if fungerar endast för omedelbara skalärvärden. Om du vill implementera villkor för uppskjutna objekt använder du den inbyggda funktionen BS.Boolean.If(), som gör det möjligt att välja ett värde från en av två tensorer baserat på en flagga tensor. Den har formuläret If (cond, tval, eval).)

Funktionsanrop

BrainScript har tre typer av funktioner: inbyggda primitiver (med C++-implementeringar), biblioteksfunktioner (skrivna i BrainScript) och användardefinierade (BrainScript). Exempel på inbyggda funktioner är Sigmoid() och MaxPooling(). Biblioteks- och användardefinierade funktioner är mekaniskt likadana och sparas bara i olika källfiler. Alla typer anropas, på samma sätt som matematik och vanliga språk, med hjälp av formuläret f (arg1, arg2, ...).

Vissa funktioner accepterar valfria parametrar. Valfria parametrar skickas som namngivna parametrar, till exempel f (arg1, arg2, option1=..., option2=...).

Funktioner kan anropas rekursivt, till exempel:

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)

Observera hur operatorn if används för att avsluta rekursionen.

Skapa lager

Funktioner kan skapa hela lager eller modeller som också är funktionsobjekt som fungerar som funktioner. Enligt konventionen använder en funktion som skapar ett lager med lärbara parametrar klammerparenteser { } i stället för parenteser ( ). Du kommer att stöta på uttryck som detta:

h = DenseLayer {1024} (v)

Här är två anrop på spel. Det första, DenseLayer{1024}, är ett funktionsanrop som skapar ett funktionsobjekt, som sedan i sin tur tillämpas på data (v). Eftersom DenseLayer{} returnerar ett funktionsobjekt med inlärbara parametrar används { } det för att ange detta.

Poster och Record-Member Access

Postuttryck är tilldelningar omgivna av klammerparenteser. Exempel:

{
    x = 13
    y = x * factorParameter
    f (z) = y + z
}

Det här uttrycket definierar en post med tre medlemmar, x, yoch f, där f är en funktion. I posten kan uttryck referera till andra postmedlemmar bara efter deras namn, som x används ovan i tilldelningen av y.

Till skillnad från många språk kan postposter dock deklareras i valfri ordning. Kan till exempel x deklareras efter y. Detta för att underlätta definitionen av återkommande nätverk. Alla postmedlemmar är tillgängliga från andra postmedlemmars uttryck. Detta skiljer sig från till exempel Python och liknar F#:s let rec. Cykliska referenser är förbjudna, med särskilt undantag för PastValue() åtgärderna och FutureValue() .

När poster kapslas (postuttryck som används i andra poster) söks postmedlemmarna upp genom hela hierarkin med omfång. Faktum är att varje variabeltilldelning är en del av en post: Den yttre nivån för en BrainScript är också en underförstådd post. I exemplet ovan factorParameter måste tilldelas som postmedlem i ett omfång som omsluter.

Funktioner som tilldelats i en post samlar in de postmedlemmar som de refererar till. Till exempel f() avbildas y, som i sin tur är beroende av x och den externt definierade factorParameter. Att samla in dessa innebär att f() kan skickas som en lambda till externa omfång som inte innehåller factorParameter eller har åtkomst till den.

Från utsidan används postmedlemmar med hjälp av operatorn . . Om vi till exempel hade tilldelat postuttrycket ovan till en variabel rskulle det ge r.x värdet 13. Operatorn . bläddrar inte igenom omfång: r.factorParameter skulle misslyckas med ett fel.

(Observera att tills CNTK 1.6, i stället för klammerparenteser{ ... }, används hakparenteser [ ... ]i poster. Detta är fortfarande tillåtet, men inaktuellt.)

Matriser och matrisåtkomst

BrainScript har en endimensionell matristyp för omedelbara värden (ska inte förväxlas med tensorer). Matriser indexeras med .[index] Flerdimensionella matriser kan emuleras som matriser med matriser.

Matriser med minst 2 element kan deklareras med operatorn : . Följande deklarerar till exempel en 3-dimensionell matris med namnet imageDims som sedan skickas till för ParameterTensor{} att deklarera en tensor för parametern rank-3:

imageDims = (256 : 256 : 3)
inputFilter = ParameterTensor {imageDims}

Det går också att deklarera matriser vars värden refererar till varandra. För detta måste man använda den något mer involverade matristilldelningssyntaxen:

arr[i:i0..i1] = f(i)

som konstruerar en matris med namnet arr med lägre indexbunden i0 och övre indexbunden i1, i som anger variabeln för att ange indexvariabeln i initieringsuttrycketf(i), vilket i sin tur anger värdet för arr[i]. Matrisens värden utvärderas lattja. Det gör att initieringsuttrycket för ett specifikt index i kan komma åt andra element arr[j] i samma matris, så länge det inte finns något cykliskt beroende. Detta kan till exempel användas för att deklarera en stack med nätverkslager:

layers[l:1..L] =
    if l == 1
    then DNNLayer (x, hiddenDim, featDim)
    else DNNLayer (layers[l-1], hiddenDim, hiddenDim)

Till skillnad från den rekursiva versionen av detta som vi introducerade tidigare bevarar den här versionen åtkomsten till varje enskilt lager genom att säga layers[i].

Alternativt finns det också en uttryckssyntax array[i0..i1] (i => f(i)), vilket är mindre praktiskt men ibland användbart. Ovanstående skulle se ut så här:

layers = array[1..L] (l =>
    if l == 1
    then DNNLayer (x, hiddenDim, featDim)
    else DNNLayer (layers[l-1], hiddenDim, hiddenDim)
)

Obs! För närvarande finns det inget sätt att deklarera en matris med 0 element. Detta kommer att åtgärdas i en framtida version av CNTK.

Funktionsuttryck och Lambdas

I BrainScript är funktioner värden. En namngiven funktion kan tilldelas till en variabel och skickas som ett argument, till exempel:

Layer (x, m, n, f) = f (ParameterTensor {(m:n)} * x + ParameterTensor {n})
h = Layer (x, 512, 40, Sigmoid)

där Sigmoid skickas som en funktion som används i Layer(). Alternativt kan en C#-liknande lambda-syntax (x => f(x)) skapa anonyma funktioner infogade. Detta definierar till exempel ett nätverkslager med en Softplus-aktivering:

h = Layer (x, 512, 40, (x => Log (Constant(1) + Exp (x)))

Lambda-syntaxen är för närvarande begränsad till funktioner med en enda parameter.

Lagermönster

I exemplet ovan Layer() kombineras skapande av parametrar och funktionsprogram. Ett föredraget mönster är att dela upp dessa i två steg:

  • skapa parametrar och returnerar ett funktionsobjekt som innehåller dessa parametrar
  • skapa funktionen som tillämpar parametrarna på indata

Mer specifikt är den senare också medlem i funktionsobjektet. Exemplet ovan kan skrivas om som:

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

och skulle anropas som:

h = Layer {512, 40, Sigmoid} (x)

Anledningen till det här mönstret är att typiska nätverkstyper består av att tillämpa en funktion efter en annan på indata, som kan skrivas enklare med hjälp av Sequential() funktionen.

CNTK levereras med en omfattande uppsättning fördefinierade lager, som beskrivs här.

Skapa inbyggda C++ CNTK-objekt

I slutändan är alla BrainScript-värden C++-objekt. Den särskilda BrainScript-operatorn new används för att interagera med underliggande CNTK C++-objekt. Det har formuläret new TYPE ARGRECORD där TYPE är ett av en hårdkodad uppsättning av fördefinierade C++-objekt som exponeras för BrainScript och ARGRECORD är ett postuttryck som skickas till C++-konstruktorn.

Du kommer förmodligen bara att få se det här formuläret om du använder parentesformen BrainScriptNetworkBuilder, dvs. BrainScriptNetworkBuilder = (new ComputationNetwork { ... })enligt beskrivningen här. Men nu vet du vad det innebär: new ComputationNetwork skapar ett nytt C++-objekt av typen ComputationNetwork, där { ... } helt enkelt definierar en post som skickas till C++-konstruktorn för det interna ComputationNetwork C++-objektet, som sedan söker efter 5 specifika medlemmar featureNodes, , labelNodescriterionNodes, evaluationNodesoch outputNodes, som förklaras här.

Under huven är alla inbyggda funktioner uttryck new som konstruerar objekt i klassen CNTK C++ComputationNode. En bild finns i hur det Tanh() inbyggda faktiskt definieras som att skapa ett C++-objekt:

Tanh (z, tag='') = new ComputationNode { operation = 'Tanh' ; inputs = z /plus funktionen args/ }

Uttrycksutvärderingssemantik

BrainScript-uttryck utvärderas vid första användningen. Eftersom det primära syftet med BrainScript är att beskriva nätverket är värdet för ett uttryck ofta en nod i ett beräkningsdiagram för uppskjuten beräkning. Från BrainScript-vinkeln W1 * r + b1 beräknas till exempel i exemplet ovan till ett ComputationNode objekt i stället för ett numeriskt värde, medan de faktiska numeriska värdena beräknas av grafkörningsmotorn. Endast BrainScript-uttryck för skalärer (t.ex. 28*28) är "beräknade" när BrainScript parsas. Uttryck som aldrig används (t.ex. på grund av ett villkor) utvärderas aldrig (eller kontrolleras inte för typfel).

Vanliga användningsmönster för uttryck

Nedan visas några vanliga mönster som används med BrainScript.

Namnområden för Functions

Genom att gruppera funktionstilldelningar i poster kan man uppnå en form av namnavstånd. Exempel:

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))

Lokalt begränsade variabler

Ibland är det önskvärt att ha lokalt begränsade variabler och/eller funktioner för mer komplexa uttryck. Detta kan uppnås genom att omsluta hela uttrycket i en post och omedelbart komma åt dess resultatvärde. Exempel:

{ x = 13 ; y = x * x }.y

skapar en "tillfällig" post med en medlem y som omedelbart läss upp. Den här posten är "tillfällig" eftersom den inte är tilldelad till en variabel och därför är dess medlemmar inte tillgängliga förutom för y.

Det här mönstret används ofta för att göra NN-skikt med inbyggda parametrar mer läsbara, till exempel:

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 Här kan du tänka på "returvärdet" för den här funktionen.

Nästa: Lär dig mer om att definiera BrainScript Functions

NDLNetworkBuilder (inaktuell)

Tidigare versioner av CNTK använde den nu inaktuella NDLNetworkBuilder i stället för BrainScriptNetworkBuilder. NDLNetworkBuilder implementerat en mycket reducerad version av BrainScript. Den hade följande begränsningar:

  • Ingen infixsyntax. Alla operatorer måste anropas via funktionsanrop. T.ex. Plus (Times (W1, r), b1) i stället för W1 * r + b1.
  • Inga kapslade postuttryck. Det finns bara en underförstådd yttre post.
  • Inget villkorsuttryck eller rekursiv funktionsanrop.
  • Användardefinierade funktioner måste deklareras i särskilda load block och kan inte kapslas.
  • Den senaste posttilldelningen används automatiskt som värde för en funktion.
  • Språkversionen NDLNetworkBuilder är inte Turing-komplett.

NDLNetworkBuilder ska inte användas längre.