Dela via


Grundläggande begrepp i BrainScript

BrainScript – en Walk-Through

I det här avsnittet beskrivs de grundläggande begreppen för språket "BrainScript". Ett nytt språk? Oroa dig inte & läsa vidare, det är väldigt rakt fram.

I CNTK definieras anpassade nätverk med hjälp av BrainScriptNetworkBuilder och beskrivs i cntk-nätverksbeskrivningsspråket "BrainScript". På samma sätt kallas nätverksbeskrivningar för hjärnskript.

BrainScript är ett enkelt sätt att definiera ett nätverk på ett kodliknande sätt med hjälp av uttryck, variabler, primitiva och självdefinierade funktioner, kapslade block och andra välkända begrepp. Det liknar ett skriptspråk i syntaxen.

Okej, låt oss få våra fötter våta med ett komplett BrainScript-exempel!

En fullständig BrainScript-exempelnätverksdefinition

I följande exempel visas nätverksbeskrivningen för ett enkelt neuralt nätverk med ett dolt lager och ett klassificeringslager. Vi förklarar begreppen i det här exemplet. Innan du går vidare kan du ägna några minuter åt exemplet och försöka gissa vad det innebär. När du läser vidare kanske du gissade det mesta korrekt.

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

Grundläggande om BrainScript-syntax

Innan vi går in direkt kan du läsa några allmänna anteckningar om BrainScripts syntax.

BrainScript använder en enkel syntax som syftar till att tillåta att neurala nätverk uttrycks på ett sätt som ser ut som matematiska formler. Därför är den grundläggande syntaktiska enheten tilldelningen, som används i både variabeltilldelningar och funktionsdefinitioner. Exempel:

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

Rader, kommentarer, inkludera

Även om en tilldelning vanligtvis skrivs på en enda rad kan uttryck sträcka sig över flera rader. Om du vill placera flera tilldelningar på en enda rad måste du dock avgränsa dem med semikolon. Exempel:

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

Förutom att kräva semikolon mellan tilldelningar utan radbrytning är BrainScript inte blankstegskänsligt.

BrainScript förstår linjekommentarer med både Python-format # och C++-stil //. Infogade kommentarer använder C-syntax (/* this is a comment*/), men till skillnad från C kanske de inte sträcker sig över flera rader.

För BrainScript inbäddade i CNTK-konfigurationsfiler (till skillnad från BrainScript-läsning från en separat fil via ett include direktiv), på grund av en interaktion med konfigurationsparsern, finns det den (något konstiga) ytterligare begränsningen att alla parenteser, klammerparenteser eller hakparenteser måste balanseras inuti kommentarer och strängliteraler i C/C++. Därför, inga smileys i C / C + + stil kommentarer!

Ett include "PATH" direktiv kan användas på valfri plats för att infoga innehållet i en fil vid punkten för instruktionen. PATH Här kan vara en absolut eller relativ relativ sökväg (med eller utan underkataloger). Om det är en relativ sökväg genomsöks följande platser i ordning: aktuell arbetskatalog; katalog(er) som innehåller yttre inklusive filer om det finns några; kataloger som innehåller konfigurationsfilerna. och slutligen katalogen som innehåller den körbara CNTK-filen. Alla inbyggda BrainScript-funktioner ingår på det här sättet från en fil med namnet CNTK.core.bs som finns bredvid den körbara CNTK-filen.

Uttryck

Därefter behöver du känna till BrainScript-uttryck – formler som beskriver ditt nätverk. BrainScript-uttryck skrivs matematikliknande i en syntax som liknar populära programmeringsspråk. De enklaste uttrycken är literaler, t.ex. tal och strängar. Ett matematiskt exempel är W1 * r + b, där * refererar till en skalär, matris eller tensorprodukt beroende på typen av variabler. En annan vanlig typ av uttryck är funktionsanropet, t.ex. RectifiedLinear (.).

BrainScript är ett dynamiskt typat språk. En viktig uttryckstyp är posten, som definieras med hjälp av syntaxen {...} och nås via punktsyntaxen. Till exempel r = { x = 13 ; y = 42 } tilldelar en post med två medlemmar till r, där den första medlemmen kan nås som r.x.

Förutom de vanliga matematiska operatorerna har BrainScript ett villkorsuttryck (if c then t else f), ett matrisuttryck och enkla lambdas. Slutligen kan BrainScript direkt instansiera en begränsad uppsättning fördefinierade C++-objekt för att interagera med CNTK C++-koden, främst ComputationNode de databehandlingsnätverk som består av. Mer information finns i Uttryck.

BrainScript-uttryck utvärderas vid första användning. Det primära syftet med BrainScript är att beskriva nätverket, så värdet för ett uttryck är ofta inte ett slutligt värde, utan snarare en nod i en beräkningsgraf för uppskjuten beräkning (som i W1 * r + b). Endast BrainScript-uttryck av 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.

Obs! Den nu inaktuella NDLNetworkBuilder versionen stöder endast syntax för funktionsanrop, t.ex. måste man skriva Plus (Times (W1, r), b1).

Variabler

Variabler kan innehålla värdet för valfritt BrainScript-uttryck (tal, sträng, post, matris, lambda, CNTK C++-objekt) och ersätts när de används i ett uttryck. Variabler är oföränderliga, dvs. tilldelas bara en gång. Nätverksdefinitionen ovan börjar till exempel med:

SDim = 28*28  
HDim = 256
LDim = 10

Här är variablerna inställda på skalära numeriska värden som används som parametrar i efterföljande uttryck. Dessa värden är dimensionerna för dataexemplen, dolda lager och etiketter som används i träningen. Det här specifika exemplet är för MNIST-datauppsättningen, som är en samling bilder med [28 x 28]bildpunkter. Varje bild är en handskriven siffra (0–9), så det finns 10 möjliga etiketter som kan tillämpas på varje bild. Den dolda aktiveringsdimensionen HDim är ett användarval.

De flesta variabler är postmedlemmar (det yttre BrainScript-blocket är implicit en post). Dessutom kan variabler vara funktionsargument eller lagras som matriselement.

OK, redo att gå igenom modelldefinitionen.

Definiera nätverket

Ett nätverk beskrivs främst av formler för hur nätverkets utdata beräknas från indata. Vi kallar detta modellfunktionen, som ofta definieras som en faktisk funktion i BrainScript. Som en del av modellfunktionen måste användaren deklarera modellparametrarna. Slutligen måste man definiera nätverkets indata och kriterier/utdata. Alla dessa definieras som variabler. Variablerna indata och villkor/utdata måste sedan kommuniceras till systemet.

Nätverkets modellfunktion och modellparametrar

Modellfunktionen innehåller de faktiska nätverksformlerna och respektive modellparametrar. Vårt exempel använder matrisprodukt och addition, och funktionen "primitiv" (inbyggd) för energifunktionen RectifiedLinear(), så kärnan i nätverksfunktionen består av följande ekvationer:

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

Modellparametrar är matriser, biasvektorer eller andra tensorer som utgör den inlärda modellen när träningen har slutförts. Tensorerna för modellparametern används för att transformera indataexempeldata till önskade utdata och uppdateras av inlärningsprocessen. Exempelnätverket ovan innehåller följande matrisparametrar:

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

I det här fallet W0 är viktmatrisen och b0 biasvektorn. ParameterTensor{} anger en särskild CNTK-primitiv, som instansierar en vektor, matris eller tensor med godtycklig rangordning och tar dimensionsparametrarna som en BrainScript-matris (tal sammanfogade med ett kolon :). En vektordimension är ett enda tal, medan en matrisdimension ska anges som (numRows:numCols). Som standard initieras parametrar med enhetliga slumptal när de instansieras direkt och heNormal när de används via lager, men det finns andra alternativ (se här) för den fullständiga listan. Till skillnad från vanliga funktioner tar ParameterTensor{} dess argument inom klammerparenteser i stället för parenteser. Klammerparenteser är BrainScript-konventionen för funktioner som skapar parametrar eller objekt, till skillnad från funktioner.

Allt detta omsluts sedan till en BrainScript-funktion. BrainScript-funktioner deklareras i formatet f(x) = an expression of x. Är till exempel Sqr (x) = x * x en giltig BrainScript-funktionsdeklaration. Kan inte vara mycket enklare och direkt, eller hur?

Nu är den faktiska modellfunktionen i vårt exempel ovan lite mer komplex:

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

Det yttre { ... } och det sista .z förtjänar en förklaring. De yttre curliesna { ... } och deras innehåll definierar faktiskt en post med 6 postmedlemmar (W0, , W1b0, b1r, och z). Men modellfunktionens värde är bara z; alla andra är interna för funktionen. Därför använder .z vi för att välja den postmedlem som vi vill returnera. Det här är bara punktsyntaxen för åtkomst till postmedlemmar. På så sätt är de andra postmedlemmarna inte tillgängliga utifrån. Men de fortsätter att finnas som en del av uttrycket för att beräkna z. Mönstret { ... ; x = ... }.x är ett sätt att använda lokala variabler.

Observera att postsyntaxen inte är nödvändig. Alternativt kunde också ha deklarerats model(features) utan omväg via posten, som ett enda uttryck:

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

Det här är mycket svårare att läsa, och det är ännu viktigare att inte tillåta att samma parameter används på flera platser i formeln.

Indata

Indata till nätverket definieras av exempeldata och de etiketter som är associerade med exemplen:

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

Input{} är den andra speciella CNTK-primitiven som behövs för modelldefinitionen (den första är Parameter{}). Den skapar en variabel som tar emot indata utanför nätverket: från läsaren. Argumentet Input{} för är datadimensionen. I det här exemplet features har indata måtten för exempeldata (som vi definierade i variabeln SDim) och labels indata kommer att ha etiketternas dimensioner. Variabelnamnen för indata förväntas matcha motsvarande poster i läsardefinitionen.

Träningskriterier och nätverksutdata

Vi måste fortfarande deklarera hur nätverkets utdata interagerar med världen. Vår modellfunktion beräknar logit-värden (icke-normaliserade loggannolikheter). Dessa logit-värden kan användas för att

  • definiera träningskriteriet.
  • mätnoggrannhet och
  • beräkna sannolikheten över utdataklasser som ges indata, för att basera ett klassificeringsbeslut på (observera att dennormaliserade loggens posterior z ofta kan användas för klassificering direkt).

Exempelnätverket använder kategorietiketter som representeras som one-hot-vektorer. I MNIST-exemplet visas dessa som en matris med 10 flyttalsvärden, som alla är noll förutom rätt etikettkategori som är 1,0. Klassificeringsuppgifter som vår använder SoftMax() ofta funktionen för att hämta sannolikheterna för varje etikett. Nätverket optimeras sedan för att maximera loggens sannolikhet för rätt klass (korsentropy) och minimera den för alla andra klasser. Det här är vårt träningskriterium eller förlustfunktion. I CNTK kombineras dessa två åtgärder vanligtvis i en funktion för effektivitet:

ce = CrossEntropyWithSoftmax (labels, z)

CrossEntropyWithSoftmax() funktionen tar indata, beräknar SoftMax() funktionen, beräknar felet från det faktiska värdet med hjälp av korsentropy och den felsignalen används för att uppdatera parametrarna i nätverket via bakåtspridning. I exemplet ovan används därför inte det normaliserade Softmax() värdet, som vi beräknade som P, under träningen. Det kommer dock att behövas för att använda nätverket (observera återigen att det i många fall z ofta räcker för klassificering. I så fall z skulle det i sig vara utdata).

CNTK använder SGD (Stochastic Gradient Descent) som inlärningsalgoritm. SGD måste beräkna målfunktionens toning med avseende på alla modellparametrar. Viktigt är att CNTK inte kräver att användarna anger dessa toningar. I stället har varje inbyggd funktion i CNTK också en derivatmotsvarighetsfunktion, och systemet utför automatiskt bakåtspridningsuppdateringen av nätverksparametrarna. Detta är inte synligt för användaren. Användarna behöver aldrig bry sig om toningar. Någonsin.

Förutom träningskriteriet beräknas ofta förutsagda felfrekvenser under träningsfasen för att validera förbättringen av systemet när träningen går vidare. Detta hanteras i CNTK med hjälp av följande funktion:

errs = ClassificationError (labels, z)

Sannolikheterna som genereras av nätverket jämförs med den faktiska etiketten och felfrekvensen beräknas. Detta visas vanligtvis av systemet. Även om detta är användbart är det inte obligatoriskt att använda ClassificationError().

Kommunicera indata, utdata och kriterier till systemet

Nu när alla variabler har definierats måste vi tala om för systemet vilka av variablerna det ska behandla som indata, utdata och kriterier. Detta görs genom att definiera 5 särskilda variabler som måste ha exakt följande namn:

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

Värdena är matriser, där värdena ska avgränsas med ett kolon (kolonet : är en BrainScript-operator som bildar en matris genom att sammanfoga två värden eller matriser). Detta visas ovan för outputNodes, som deklarerar både z och P som utdata.

(Obs! De inaktuella NDLNetworkBuilder krävde att matriselementen skulle avgränsas med kommatecken i stället.)

Sammanfattning av specialnamn

Som vi såg ovan finns det 7 specialnamn som vi måste vara medvetna om, som bär "magiska" egenskaper:

  • ParameterTensor{}: Deklarerar och initierar en lärbar parameter.
  • Input{}: Deklarerar en variabel som ansluts och matas från en dataläsare.
  • featureNodes, labelNodes, criterionNodes, evaluationNodesoch outputNodes: Deklarerar för systemet vilka av våra variabler som ska användas som indata, utdata och kriterier.

Dessutom finns det ytterligare 3 specialfunktioner med inbyggd "magi" i CNTK, som diskuteras någon annanstans:

  • Constant(): Deklarerar en konstant.
  • PastValue() och FutureValue(): Åtkomst till en variabel i en nätverksfunktion i ett annat tidssteg för återkommande formloopar.

Ett annat fördefinierat namn är antingen en inbyggd primitiv funktion som Sigmoid() eller med en C++-implementering, en fördefinierad biblioteksfunktion som realiseras i BrainScript som BS.RNNs.LSTMP(), eller en post som fungerar som ett namnområde för biblioteksfunktioner (t.ex. BS.RNNsConvolution() ). En fullständig lista finns i BrainScript-Full-Function-Reference .

Nästa: BrainScript-uttryck